mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-21 05:15:15 +02:00
Merge branch 'main' into add-jest-setup
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
COPY src/aws-kb-retrieval-server /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
FROM node:22-alpine AS release
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,79 +0,0 @@
|
||||
# AWS Knowledge Base Retrieval MCP Server
|
||||
|
||||
An MCP server implementation for retrieving information from the AWS Knowledge Base using the Bedrock Agent Runtime.
|
||||
|
||||
## Features
|
||||
|
||||
- **RAG (Retrieval-Augmented Generation)**: Retrieve context from the AWS Knowledge Base based on a query and a Knowledge Base ID.
|
||||
- **Supports multiple results retrieval**: Option to retrieve a customizable number of results.
|
||||
|
||||
## Tools
|
||||
|
||||
- **retrieve_from_aws_kb**
|
||||
- Perform retrieval operations using the AWS Knowledge Base.
|
||||
- Inputs:
|
||||
- `query` (string): The search query for retrieval.
|
||||
- `knowledgeBaseId` (string): The ID of the AWS Knowledge Base.
|
||||
- `n` (number, optional): Number of results to retrieve (default: 3).
|
||||
|
||||
## Configuration
|
||||
|
||||
### Setting up AWS Credentials
|
||||
|
||||
1. Obtain AWS access key ID, secret access key, and region from the AWS Management Console.
|
||||
2. Ensure these credentials have appropriate permissions for Bedrock Agent Runtime operations.
|
||||
|
||||
### Usage with Claude Desktop
|
||||
|
||||
Add this to your `claude_desktop_config.json`:
|
||||
|
||||
#### Docker
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"aws-kb-retrieval": {
|
||||
"command": "docker",
|
||||
"args": [ "run", "-i", "--rm", "-e", "AWS_ACCESS_KEY_ID", "-e", "AWS_SECRET_ACCESS_KEY", "-e", "AWS_REGION", "mcp/aws-kb-retrieval-server" ],
|
||||
"env": {
|
||||
"AWS_ACCESS_KEY_ID": "YOUR_ACCESS_KEY_HERE",
|
||||
"AWS_SECRET_ACCESS_KEY": "YOUR_SECRET_ACCESS_KEY_HERE",
|
||||
"AWS_REGION": "YOUR_AWS_REGION_HERE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"aws-kb-retrieval": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-aws-kb-retrieval"
|
||||
],
|
||||
"env": {
|
||||
"AWS_ACCESS_KEY_ID": "YOUR_ACCESS_KEY_HERE",
|
||||
"AWS_SECRET_ACCESS_KEY": "YOUR_SECRET_ACCESS_KEY_HERE",
|
||||
"AWS_REGION": "YOUR_AWS_REGION_HERE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Docker:
|
||||
|
||||
```sh
|
||||
docker build -t mcp/aws-kb-retrieval -f src/aws-kb-retrieval-server/Dockerfile .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
|
||||
This README assumes that your server package is named `@modelcontextprotocol/server-aws-kb-retrieval`. Adjust the package name and installation details if they differ in your setup. Also, ensure that your server script is correctly built and that all dependencies are properly managed in your `package.json`.
|
||||
@@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import {
|
||||
BedrockAgentRuntimeClient,
|
||||
RetrieveCommand,
|
||||
RetrieveCommandInput,
|
||||
} from "@aws-sdk/client-bedrock-agent-runtime";
|
||||
|
||||
// AWS client initialization
|
||||
const bedrockClient = new BedrockAgentRuntimeClient({
|
||||
region: process.env.AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
||||
},
|
||||
});
|
||||
|
||||
interface RAGSource {
|
||||
id: string;
|
||||
fileName: string;
|
||||
snippet: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
async function retrieveContext(
|
||||
query: string,
|
||||
knowledgeBaseId: string,
|
||||
n: number = 3
|
||||
): Promise<{
|
||||
context: string;
|
||||
isRagWorking: boolean;
|
||||
ragSources: RAGSource[];
|
||||
}> {
|
||||
try {
|
||||
if (!knowledgeBaseId) {
|
||||
console.error("knowledgeBaseId is not provided");
|
||||
return {
|
||||
context: "",
|
||||
isRagWorking: false,
|
||||
ragSources: [],
|
||||
};
|
||||
}
|
||||
|
||||
const input: RetrieveCommandInput = {
|
||||
knowledgeBaseId: knowledgeBaseId,
|
||||
retrievalQuery: { text: query },
|
||||
retrievalConfiguration: {
|
||||
vectorSearchConfiguration: { numberOfResults: n },
|
||||
},
|
||||
};
|
||||
|
||||
const command = new RetrieveCommand(input);
|
||||
const response = await bedrockClient.send(command);
|
||||
const rawResults = response?.retrievalResults || [];
|
||||
const ragSources: RAGSource[] = rawResults
|
||||
.filter((res) => res?.content?.text)
|
||||
.map((result, index) => {
|
||||
const uri = result?.location?.s3Location?.uri || "";
|
||||
const fileName = uri.split("/").pop() || `Source-${index}.txt`;
|
||||
return {
|
||||
id: (result.metadata?.["x-amz-bedrock-kb-chunk-id"] as string) || `chunk-${index}`,
|
||||
fileName: fileName.replace(/_/g, " ").replace(".txt", ""),
|
||||
snippet: result.content?.text || "",
|
||||
score: (result.score as number) || 0,
|
||||
};
|
||||
})
|
||||
.slice(0, 3);
|
||||
|
||||
const context = rawResults
|
||||
.filter((res): res is { content: { text: string } } => res?.content?.text !== undefined)
|
||||
.map(res => res.content.text)
|
||||
.join("\n\n");
|
||||
|
||||
return {
|
||||
context,
|
||||
isRagWorking: true,
|
||||
ragSources,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("RAG Error:", error);
|
||||
return { context: "", isRagWorking: false, ragSources: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Define the retrieval tool
|
||||
const RETRIEVAL_TOOL: Tool = {
|
||||
name: "retrieve_from_aws_kb",
|
||||
description: "Performs retrieval from the AWS Knowledge Base using the provided query and Knowledge Base ID.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "The query to perform retrieval on" },
|
||||
knowledgeBaseId: { type: "string", description: "The ID of the AWS Knowledge Base" },
|
||||
n: { type: "number", default: 3, description: "Number of results to retrieve" },
|
||||
},
|
||||
required: ["query", "knowledgeBaseId"],
|
||||
},
|
||||
};
|
||||
|
||||
// Server setup
|
||||
const server = new Server(
|
||||
{
|
||||
name: "aws-kb-retrieval-server",
|
||||
version: "0.2.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Request handlers
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [RETRIEVAL_TOOL],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
if (name === "retrieve_from_aws_kb") {
|
||||
const { query, knowledgeBaseId, n = 3 } = args as Record<string, any>;
|
||||
try {
|
||||
const result = await retrieveContext(query, knowledgeBaseId, n);
|
||||
if (result.isRagWorking) {
|
||||
return {
|
||||
content: [
|
||||
{ type: "text", text: `Context: ${result.context}` },
|
||||
{ type: "text", text: `RAG Sources: ${JSON.stringify(result.ragSources)}` },
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: [{ type: "text", text: "Retrieval failed or returned no results." }],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error occurred: ${error}` }],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Server startup
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("AWS KB Retrieval Server running on stdio");
|
||||
}
|
||||
|
||||
runServer().catch((error) => {
|
||||
console.error("Fatal error running server:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-aws-kb-retrieval",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for AWS Knowledge Base retrieval using Bedrock Agent Runtime",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-aws-kb-retrieval": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"@aws-sdk/client-bedrock-agent-runtime": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"composite": true,
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
# Must be entire project because `prepare` script is run during `npm install` and requires all files.
|
||||
COPY src/brave-search /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
FROM node:22-alpine AS release
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,92 +0,0 @@
|
||||
# Brave Search MCP Server
|
||||
|
||||
An MCP server implementation that integrates the Brave Search API, providing both web and local search capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- **Web Search**: General queries, news, articles, with pagination and freshness controls
|
||||
- **Local Search**: Find businesses, restaurants, and services with detailed information
|
||||
- **Flexible Filtering**: Control result types, safety levels, and content freshness
|
||||
- **Smart Fallbacks**: Local search automatically falls back to web when no results are found
|
||||
|
||||
## Tools
|
||||
|
||||
- **brave_web_search**
|
||||
- Execute web searches with pagination and filtering
|
||||
- Inputs:
|
||||
- `query` (string): Search terms
|
||||
- `count` (number, optional): Results per page (max 20)
|
||||
- `offset` (number, optional): Pagination offset (max 9)
|
||||
|
||||
- **brave_local_search**
|
||||
- Search for local businesses and services
|
||||
- Inputs:
|
||||
- `query` (string): Local search terms
|
||||
- `count` (number, optional): Number of results (max 20)
|
||||
- Automatically falls back to web search if no local results found
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
### Getting an API Key
|
||||
1. Sign up for a [Brave Search API account](https://brave.com/search/api/)
|
||||
2. Choose a plan (Free tier available with 2,000 queries/month)
|
||||
3. Generate your API key [from the developer dashboard](https://api.search.brave.com/app/keys)
|
||||
|
||||
### Usage with Claude Desktop
|
||||
Add this to your `claude_desktop_config.json`:
|
||||
|
||||
### Docker
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"brave-search": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"BRAVE_API_KEY",
|
||||
"mcp/brave-search"
|
||||
],
|
||||
"env": {
|
||||
"BRAVE_API_KEY": "YOUR_API_KEY_HERE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"brave-search": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-brave-search"
|
||||
],
|
||||
"env": {
|
||||
"BRAVE_API_KEY": "YOUR_API_KEY_HERE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
Docker build:
|
||||
|
||||
```bash
|
||||
docker build -t mcp/brave-search:latest -f src/brave-search/Dockerfile .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,376 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
const WEB_SEARCH_TOOL: Tool = {
|
||||
name: "brave_web_search",
|
||||
description:
|
||||
"Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. " +
|
||||
"Use this for broad information gathering, recent events, or when you need diverse web sources. " +
|
||||
"Supports pagination, content filtering, and freshness controls. " +
|
||||
"Maximum 20 results per request, with offset for pagination. ",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query (max 400 chars, 50 words)"
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Number of results (1-20, default 10)",
|
||||
default: 10
|
||||
},
|
||||
offset: {
|
||||
type: "number",
|
||||
description: "Pagination offset (max 9, default 0)",
|
||||
default: 0
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
};
|
||||
|
||||
const LOCAL_SEARCH_TOOL: Tool = {
|
||||
name: "brave_local_search",
|
||||
description:
|
||||
"Searches for local businesses and places using Brave's Local Search API. " +
|
||||
"Best for queries related to physical locations, businesses, restaurants, services, etc. " +
|
||||
"Returns detailed information including:\n" +
|
||||
"- Business names and addresses\n" +
|
||||
"- Ratings and review counts\n" +
|
||||
"- Phone numbers and opening hours\n" +
|
||||
"Use this when the query implies 'near me' or mentions specific locations. " +
|
||||
"Automatically falls back to web search if no local results are found.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Local search query (e.g. 'pizza near Central Park')"
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Number of results (1-20, default 5)",
|
||||
default: 5
|
||||
},
|
||||
},
|
||||
required: ["query"]
|
||||
}
|
||||
};
|
||||
|
||||
// Server implementation
|
||||
const server = new Server(
|
||||
{
|
||||
name: "example-servers/brave-search",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Check for API key
|
||||
const BRAVE_API_KEY = process.env.BRAVE_API_KEY!;
|
||||
if (!BRAVE_API_KEY) {
|
||||
console.error("Error: BRAVE_API_KEY environment variable is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const RATE_LIMIT = {
|
||||
perSecond: 1,
|
||||
perMonth: 15000
|
||||
};
|
||||
|
||||
let requestCount = {
|
||||
second: 0,
|
||||
month: 0,
|
||||
lastReset: Date.now()
|
||||
};
|
||||
|
||||
function checkRateLimit() {
|
||||
const now = Date.now();
|
||||
if (now - requestCount.lastReset > 1000) {
|
||||
requestCount.second = 0;
|
||||
requestCount.lastReset = now;
|
||||
}
|
||||
if (requestCount.second >= RATE_LIMIT.perSecond ||
|
||||
requestCount.month >= RATE_LIMIT.perMonth) {
|
||||
throw new Error('Rate limit exceeded');
|
||||
}
|
||||
requestCount.second++;
|
||||
requestCount.month++;
|
||||
}
|
||||
|
||||
interface BraveWeb {
|
||||
web?: {
|
||||
results?: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
language?: string;
|
||||
published?: string;
|
||||
rank?: number;
|
||||
}>;
|
||||
};
|
||||
locations?: {
|
||||
results?: Array<{
|
||||
id: string; // Required by API
|
||||
title?: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface BraveLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
address: {
|
||||
streetAddress?: string;
|
||||
addressLocality?: string;
|
||||
addressRegion?: string;
|
||||
postalCode?: string;
|
||||
};
|
||||
coordinates?: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
phone?: string;
|
||||
rating?: {
|
||||
ratingValue?: number;
|
||||
ratingCount?: number;
|
||||
};
|
||||
openingHours?: string[];
|
||||
priceRange?: string;
|
||||
}
|
||||
|
||||
interface BravePoiResponse {
|
||||
results: BraveLocation[];
|
||||
}
|
||||
|
||||
interface BraveDescription {
|
||||
descriptions: {[id: string]: string};
|
||||
}
|
||||
|
||||
function isBraveWebSearchArgs(args: unknown): args is { query: string; count?: number } {
|
||||
return (
|
||||
typeof args === "object" &&
|
||||
args !== null &&
|
||||
"query" in args &&
|
||||
typeof (args as { query: string }).query === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isBraveLocalSearchArgs(args: unknown): args is { query: string; count?: number } {
|
||||
return (
|
||||
typeof args === "object" &&
|
||||
args !== null &&
|
||||
"query" in args &&
|
||||
typeof (args as { query: string }).query === "string"
|
||||
);
|
||||
}
|
||||
|
||||
async function performWebSearch(query: string, count: number = 10, offset: number = 0) {
|
||||
checkRateLimit();
|
||||
const url = new URL('https://api.search.brave.com/res/v1/web/search');
|
||||
url.searchParams.set('q', query);
|
||||
url.searchParams.set('count', Math.min(count, 20).toString()); // API limit
|
||||
url.searchParams.set('offset', offset.toString());
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': BRAVE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as BraveWeb;
|
||||
|
||||
// Extract just web results
|
||||
const results = (data.web?.results || []).map(result => ({
|
||||
title: result.title || '',
|
||||
description: result.description || '',
|
||||
url: result.url || ''
|
||||
}));
|
||||
|
||||
return results.map(r =>
|
||||
`Title: ${r.title}\nDescription: ${r.description}\nURL: ${r.url}`
|
||||
).join('\n\n');
|
||||
}
|
||||
|
||||
async function performLocalSearch(query: string, count: number = 5) {
|
||||
checkRateLimit();
|
||||
// Initial search to get location IDs
|
||||
const webUrl = new URL('https://api.search.brave.com/res/v1/web/search');
|
||||
webUrl.searchParams.set('q', query);
|
||||
webUrl.searchParams.set('search_lang', 'en');
|
||||
webUrl.searchParams.set('result_filter', 'locations');
|
||||
webUrl.searchParams.set('count', Math.min(count, 20).toString());
|
||||
|
||||
const webResponse = await fetch(webUrl, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': BRAVE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (!webResponse.ok) {
|
||||
throw new Error(`Brave API error: ${webResponse.status} ${webResponse.statusText}\n${await webResponse.text()}`);
|
||||
}
|
||||
|
||||
const webData = await webResponse.json() as BraveWeb;
|
||||
const locationIds = webData.locations?.results?.filter((r): r is {id: string; title?: string} => r.id != null).map(r => r.id) || [];
|
||||
|
||||
if (locationIds.length === 0) {
|
||||
return performWebSearch(query, count); // Fallback to web search
|
||||
}
|
||||
|
||||
// Get POI details and descriptions in parallel
|
||||
const [poisData, descriptionsData] = await Promise.all([
|
||||
getPoisData(locationIds),
|
||||
getDescriptionsData(locationIds)
|
||||
]);
|
||||
|
||||
return formatLocalResults(poisData, descriptionsData);
|
||||
}
|
||||
|
||||
async function getPoisData(ids: string[]): Promise<BravePoiResponse> {
|
||||
checkRateLimit();
|
||||
const url = new URL('https://api.search.brave.com/res/v1/local/pois');
|
||||
ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id));
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': BRAVE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
||||
}
|
||||
|
||||
const poisResponse = await response.json() as BravePoiResponse;
|
||||
return poisResponse;
|
||||
}
|
||||
|
||||
async function getDescriptionsData(ids: string[]): Promise<BraveDescription> {
|
||||
checkRateLimit();
|
||||
const url = new URL('https://api.search.brave.com/res/v1/local/descriptions');
|
||||
ids.filter(Boolean).forEach(id => url.searchParams.append('ids', id));
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Accept-Encoding': 'gzip',
|
||||
'X-Subscription-Token': BRAVE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Brave API error: ${response.status} ${response.statusText}\n${await response.text()}`);
|
||||
}
|
||||
|
||||
const descriptionsData = await response.json() as BraveDescription;
|
||||
return descriptionsData;
|
||||
}
|
||||
|
||||
function formatLocalResults(poisData: BravePoiResponse, descData: BraveDescription): string {
|
||||
return (poisData.results || []).map(poi => {
|
||||
const address = [
|
||||
poi.address?.streetAddress ?? '',
|
||||
poi.address?.addressLocality ?? '',
|
||||
poi.address?.addressRegion ?? '',
|
||||
poi.address?.postalCode ?? ''
|
||||
].filter(part => part !== '').join(', ') || 'N/A';
|
||||
|
||||
return `Name: ${poi.name}
|
||||
Address: ${address}
|
||||
Phone: ${poi.phone || 'N/A'}
|
||||
Rating: ${poi.rating?.ratingValue ?? 'N/A'} (${poi.rating?.ratingCount ?? 0} reviews)
|
||||
Price Range: ${poi.priceRange || 'N/A'}
|
||||
Hours: ${(poi.openingHours || []).join(', ') || 'N/A'}
|
||||
Description: ${descData.descriptions[poi.id] || 'No description available'}
|
||||
`;
|
||||
}).join('\n---\n') || 'No local results found';
|
||||
}
|
||||
|
||||
// Tool handlers
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [WEB_SEARCH_TOOL, LOCAL_SEARCH_TOOL],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
if (!args) {
|
||||
throw new Error("No arguments provided");
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "brave_web_search": {
|
||||
if (!isBraveWebSearchArgs(args)) {
|
||||
throw new Error("Invalid arguments for brave_web_search");
|
||||
}
|
||||
const { query, count = 10 } = args;
|
||||
const results = await performWebSearch(query, count);
|
||||
return {
|
||||
content: [{ type: "text", text: results }],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
case "brave_local_search": {
|
||||
if (!isBraveLocalSearchArgs(args)) {
|
||||
throw new Error("Invalid arguments for brave_local_search");
|
||||
}
|
||||
const { query, count = 5 } = args;
|
||||
const results = await performLocalSearch(query, count);
|
||||
return {
|
||||
content: [{ type: "text", text: results }],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Brave Search MCP Server running on stdio");
|
||||
}
|
||||
|
||||
runServer().catch((error) => {
|
||||
console.error("Fatal error running server:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-brave-search",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for Brave Search API integration",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-brave-search": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
COPY src/everart /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
FROM node:22-alpine AS release
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -1,97 +0,0 @@
|
||||
# EverArt MCP Server
|
||||
|
||||
Image generation server for Claude Desktop using EverArt's API.
|
||||
|
||||
## Install
|
||||
```bash
|
||||
npm install
|
||||
export EVERART_API_KEY=your_key_here
|
||||
```
|
||||
|
||||
## Config
|
||||
Add to Claude Desktop config:
|
||||
|
||||
### Docker
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"everart": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "-e", "EVERART_API_KEY", "mcp/everart"],
|
||||
"env": {
|
||||
"EVERART_API_KEY": "your_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"everart": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-everart"],
|
||||
"env": {
|
||||
"EVERART_API_KEY": "your_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### generate_image
|
||||
Generates images with multiple model options. Opens result in browser and returns URL.
|
||||
|
||||
Parameters:
|
||||
```typescript
|
||||
{
|
||||
prompt: string, // Image description
|
||||
model?: string, // Model ID (default: "207910310772879360")
|
||||
image_count?: number // Number of images (default: 1)
|
||||
}
|
||||
```
|
||||
|
||||
Models:
|
||||
- 5000: FLUX1.1 (standard)
|
||||
- 9000: FLUX1.1-ultra
|
||||
- 6000: SD3.5
|
||||
- 7000: Recraft-Real
|
||||
- 8000: Recraft-Vector
|
||||
|
||||
All images generated at 1024x1024.
|
||||
|
||||
Sample usage:
|
||||
```javascript
|
||||
const result = await client.callTool({
|
||||
name: "generate_image",
|
||||
arguments: {
|
||||
prompt: "A cat sitting elegantly",
|
||||
model: "7000",
|
||||
image_count: 1
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Response format:
|
||||
```
|
||||
Image generated successfully!
|
||||
The image has been opened in your default browser.
|
||||
|
||||
Generation details:
|
||||
- Model: 7000
|
||||
- Prompt: "A cat sitting elegantly"
|
||||
- Image URL: https://storage.googleapis.com/...
|
||||
|
||||
You can also click the URL above to view the image again.
|
||||
```
|
||||
|
||||
## Building w/ Docker
|
||||
|
||||
```sh
|
||||
docker build -t mcp/everart -f src/everart/Dockerfile .
|
||||
```
|
||||
@@ -1,160 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import EverArt from "everart";
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fetch from "node-fetch";
|
||||
import open from "open";
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "example-servers/everart",
|
||||
version: "0.2.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {}, // Required for image resources
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!process.env.EVERART_API_KEY) {
|
||||
console.error("EVERART_API_KEY environment variable is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new EverArt.default(process.env.EVERART_API_KEY);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "generate_image",
|
||||
description:
|
||||
"Generate images using EverArt Models and returns a clickable link to view the generated image. " +
|
||||
"The tool will return a URL that can be clicked to view the image in a browser. " +
|
||||
"Available models:\n" +
|
||||
"- 5000:FLUX1.1: Standard quality\n" +
|
||||
"- 9000:FLUX1.1-ultra: Ultra high quality\n" +
|
||||
"- 6000:SD3.5: Stable Diffusion 3.5\n" +
|
||||
"- 7000:Recraft-Real: Photorealistic style\n" +
|
||||
"- 8000:Recraft-Vector: Vector art style\n" +
|
||||
"\nThe response will contain a direct link to view the generated image.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
prompt: {
|
||||
type: "string",
|
||||
description: "Text description of desired image",
|
||||
},
|
||||
model: {
|
||||
type: "string",
|
||||
description:
|
||||
"Model ID (5000:FLUX1.1, 9000:FLUX1.1-ultra, 6000:SD3.5, 7000:Recraft-Real, 8000:Recraft-Vector)",
|
||||
default: "5000",
|
||||
},
|
||||
image_count: {
|
||||
type: "number",
|
||||
description: "Number of images to generate",
|
||||
default: 1,
|
||||
},
|
||||
},
|
||||
required: ["prompt"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
return {
|
||||
resources: [
|
||||
{
|
||||
uri: "everart://images",
|
||||
mimeType: "image/png",
|
||||
name: "Generated Images",
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
if (request.params.uri === "everart://images") {
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: "everart://images",
|
||||
mimeType: "image/png",
|
||||
blob: "", // Empty since this is just for listing
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
throw new Error("Resource not found");
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "generate_image") {
|
||||
try {
|
||||
const {
|
||||
prompt,
|
||||
model = "207910310772879360",
|
||||
image_count = 1,
|
||||
} = request.params.arguments as any;
|
||||
|
||||
// Use correct EverArt API method
|
||||
const generation = await client.v1.generations.create(
|
||||
model,
|
||||
prompt,
|
||||
"txt2img",
|
||||
{
|
||||
imageCount: image_count,
|
||||
height: 1024,
|
||||
width: 1024,
|
||||
},
|
||||
);
|
||||
|
||||
// Wait for generation to complete
|
||||
const completedGen = await client.v1.generations.fetchWithPolling(
|
||||
generation[0].id,
|
||||
);
|
||||
|
||||
const imgUrl = completedGen.image_url;
|
||||
if (!imgUrl) throw new Error("No image URL");
|
||||
|
||||
// Automatically open the image URL in the default browser
|
||||
await open(imgUrl);
|
||||
|
||||
// Return a formatted message with the clickable link
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Image generated successfully!\nThe image has been opened in your default browser.\n\nGeneration details:\n- Model: ${model}\n- Prompt: "${prompt}"\n- Image URL: ${imgUrl}\n\nYou can also click the URL above to view the image again.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
console.error("Detailed error:", error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${errorMessage}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("EverArt MCP Server running on stdio");
|
||||
}
|
||||
|
||||
runServer().catch(console.error);
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-everart",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for EverArt API integration",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-everart": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"everart": "^1.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"open": "^9.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -126,7 +126,7 @@ The server sends random-leveled log messages every 15 seconds, e.g.:
|
||||
}
|
||||
```
|
||||
|
||||
## Usage with Claude Desktop
|
||||
## Usage with Claude Desktop (uses [stdio Transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#stdio))
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
|
||||
@@ -143,3 +143,75 @@ Add to your `claude_desktop_config.json`:
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage with VS Code
|
||||
|
||||
For quick installation, use of of the one-click install buttons below...
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=everything&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-everything%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=everything&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-everything%22%5D%7D&quality=insiders)
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=everything&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Feverything%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=everything&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Feverything%22%5D%7D&quality=insiders)
|
||||
|
||||
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
||||
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
||||
|
||||
#### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"everything": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-everything"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Running from source with [HTTP+SSE Transport](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse) (deprecated as of [2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports))
|
||||
|
||||
```shell
|
||||
cd src/everything
|
||||
npm install
|
||||
npm run start:sse
|
||||
```
|
||||
|
||||
## Run from source with [Streamable HTTP Transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http)
|
||||
|
||||
```shell
|
||||
cd src/everything
|
||||
npm install
|
||||
npm run start:streamableHttp
|
||||
```
|
||||
|
||||
## Running as an installed package
|
||||
### Install
|
||||
```shell
|
||||
npm install -g @modelcontextprotocol/server-everything@latest
|
||||
````
|
||||
|
||||
### Run the default (stdio) server
|
||||
```shell
|
||||
npx @modelcontextprotocol/server-everything
|
||||
```
|
||||
|
||||
### Or specify stdio explicitly
|
||||
```shell
|
||||
npx @modelcontextprotocol/server-everything stdio
|
||||
```
|
||||
|
||||
### Run the SSE server
|
||||
```shell
|
||||
npx @modelcontextprotocol/server-everything sse
|
||||
```
|
||||
|
||||
### Run the streamable HTTP server
|
||||
```shell
|
||||
npx @modelcontextprotocol/server-everything streamableHttp
|
||||
```
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ export const createServer = () => {
|
||||
resources: { subscribe: true },
|
||||
tools: {},
|
||||
logging: {},
|
||||
completions: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { createServer } from "./everything.js";
|
||||
// Parse command line arguments first
|
||||
const args = process.argv.slice(2);
|
||||
const scriptName = args[0] || 'stdio';
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
const { server, cleanup } = createServer();
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
// Cleanup on exit
|
||||
process.on("SIGINT", async () => {
|
||||
await cleanup();
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
async function run() {
|
||||
try {
|
||||
// Dynamically import only the requested module to prevent all modules from initializing
|
||||
switch (scriptName) {
|
||||
case 'stdio':
|
||||
// Import and run the default server
|
||||
await import('./stdio.js');
|
||||
break;
|
||||
case 'sse':
|
||||
// Import and run the SSE server
|
||||
await import('./sse.js');
|
||||
break;
|
||||
case 'streamableHttp':
|
||||
// Import and run the streamable HTTP server
|
||||
await import('./streamableHttp.js');
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown script: ${scriptName}`);
|
||||
console.log('Available scripts:');
|
||||
console.log('- stdio');
|
||||
console.log('- sse');
|
||||
console.log('- streamableHttp');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error running script:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Server error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
run();
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch",
|
||||
"start": "node dist/index.js",
|
||||
"start:sse": "node dist/sse.js"
|
||||
"start:sse": "node dist/sse.js",
|
||||
"start:streamableHttp": "node dist/streamableHttp.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1",
|
||||
"@modelcontextprotocol/sdk": "^1.12.0",
|
||||
"express": "^4.21.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
@@ -31,4 +32,4 @@
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,52 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import express from "express";
|
||||
import { createServer } from "./everything.js";
|
||||
|
||||
console.error('Starting SSE server...');
|
||||
|
||||
const app = express();
|
||||
|
||||
const { server, cleanup } = createServer();
|
||||
|
||||
let transport: SSEServerTransport;
|
||||
const transports: Map<string, SSEServerTransport> = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.get("/sse", async (req, res) => {
|
||||
console.log("Received connection");
|
||||
transport = new SSEServerTransport("/message", res);
|
||||
await server.connect(transport);
|
||||
let transport: SSEServerTransport;
|
||||
const { server, cleanup } = createServer();
|
||||
|
||||
if (req?.query?.sessionId) {
|
||||
const sessionId = (req?.query?.sessionId as string);
|
||||
transport = transports.get(sessionId) as SSEServerTransport;
|
||||
console.error("Client Reconnecting? This shouldn't happen; when client has a sessionId, GET /sse should not be called again.", transport.sessionId);
|
||||
} else {
|
||||
// Create and store transport for new session
|
||||
transport = new SSEServerTransport("/message", res);
|
||||
transports.set(transport.sessionId, transport);
|
||||
|
||||
// Connect server to transport
|
||||
await server.connect(transport);
|
||||
console.error("Client Connected: ", transport.sessionId);
|
||||
|
||||
// Handle close of connection
|
||||
server.onclose = async () => {
|
||||
console.error("Client Disconnected: ", transport.sessionId);
|
||||
transports.delete(transport.sessionId);
|
||||
await cleanup();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
server.onclose = async () => {
|
||||
await cleanup();
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
};
|
||||
});
|
||||
|
||||
app.post("/message", async (req, res) => {
|
||||
console.log("Received message");
|
||||
|
||||
await transport.handlePostMessage(req, res);
|
||||
const sessionId = (req?.query?.sessionId as string);
|
||||
const transport = transports.get(sessionId);
|
||||
if (transport) {
|
||||
console.error("Client Message from", sessionId);
|
||||
await transport.handlePostMessage(req, res);
|
||||
} else {
|
||||
console.error(`No transport found for sessionId ${sessionId}`)
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
console.error(`Server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
26
src/everything/stdio.ts
Normal file
26
src/everything/stdio.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { createServer } from "./everything.js";
|
||||
|
||||
console.error('Starting default (STDIO) server...');
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
const {server, cleanup} = createServer();
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
// Cleanup on exit
|
||||
process.on("SIGINT", async () => {
|
||||
await cleanup();
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Server error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
176
src/everything/streamableHttp.ts
Normal file
176
src/everything/streamableHttp.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';
|
||||
import express, { Request, Response } from "express";
|
||||
import { createServer } from "./everything.js";
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
console.error('Starting Streamable HTTP server...');
|
||||
|
||||
const app = express();
|
||||
|
||||
const transports: Map<string, StreamableHTTPServerTransport> = new Map<string, StreamableHTTPServerTransport>();
|
||||
|
||||
app.post('/mcp', async (req: Request, res: Response) => {
|
||||
console.error('Received MCP POST request');
|
||||
try {
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports.has(sessionId)) {
|
||||
// Reuse existing transport
|
||||
transport = transports.get(sessionId)!;
|
||||
} else if (!sessionId) {
|
||||
|
||||
const { server, cleanup } = createServer();
|
||||
|
||||
// New initialization request
|
||||
const eventStore = new InMemoryEventStore();
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
eventStore, // Enable resumability
|
||||
onsessioninitialized: (sessionId: string) => {
|
||||
// Store the transport by session ID when session is initialized
|
||||
// This avoids race conditions where requests might come in before the session is stored
|
||||
console.error(`Session initialized with ID: ${sessionId}`);
|
||||
transports.set(sessionId, transport);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Set up onclose handler to clean up transport when closed
|
||||
server.onclose = async () => {
|
||||
const sid = transport.sessionId;
|
||||
if (sid && transports.has(sid)) {
|
||||
console.error(`Transport closed for session ${sid}, removing from transports map`);
|
||||
transports.delete(sid);
|
||||
await cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
// Connect the transport to the MCP server BEFORE handling the request
|
||||
// so responses can flow back through the same transport
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res);
|
||||
return; // Already handled
|
||||
} else {
|
||||
// Invalid request - no session ID or not initialization request
|
||||
res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Bad Request: No valid session ID provided',
|
||||
},
|
||||
id: req?.body?.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the request with existing transport - no need to reconnect
|
||||
// The existing transport is already connected to the server
|
||||
await transport.handleRequest(req, res);
|
||||
} catch (error) {
|
||||
console.error('Error handling MCP request:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error',
|
||||
},
|
||||
id: req?.body?.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
|
||||
app.get('/mcp', async (req: Request, res: Response) => {
|
||||
console.error('Received MCP GET request');
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
if (!sessionId || !transports.has(sessionId)) {
|
||||
res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Bad Request: No valid session ID provided',
|
||||
},
|
||||
id: req?.body?.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Last-Event-ID header for resumability
|
||||
const lastEventId = req.headers['last-event-id'] as string | undefined;
|
||||
if (lastEventId) {
|
||||
console.error(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
|
||||
} else {
|
||||
console.error(`Establishing new SSE stream for session ${sessionId}`);
|
||||
}
|
||||
|
||||
const transport = transports.get(sessionId);
|
||||
await transport!.handleRequest(req, res);
|
||||
});
|
||||
|
||||
// Handle DELETE requests for session termination (according to MCP spec)
|
||||
app.delete('/mcp', async (req: Request, res: Response) => {
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
if (!sessionId || !transports.has(sessionId)) {
|
||||
res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Bad Request: No valid session ID provided',
|
||||
},
|
||||
id: req?.body?.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`Received session termination request for session ${sessionId}`);
|
||||
|
||||
try {
|
||||
const transport = transports.get(sessionId);
|
||||
await transport!.handleRequest(req, res);
|
||||
} catch (error) {
|
||||
console.error('Error handling session termination:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Error handling session termination',
|
||||
},
|
||||
id: req?.body?.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.listen(PORT, () => {
|
||||
console.error(`MCP Streamable HTTP Server listening on port ${PORT}`);
|
||||
});
|
||||
|
||||
// Handle server shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.error('Shutting down server...');
|
||||
|
||||
// Close all active transports to properly clean up resources
|
||||
for (const sessionId in transports) {
|
||||
try {
|
||||
console.error(`Closing transport for session ${sessionId}`);
|
||||
await transports.get(sessionId)!.close();
|
||||
transports.delete(sessionId);
|
||||
} catch (error) {
|
||||
console.error(`Error closing transport for session ${sessionId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Server shutdown complete');
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
A Model Context Protocol server that provides web content fetching capabilities. This server enables LLMs to retrieve and process content from web pages, converting HTML to markdown for easier consumption.
|
||||
|
||||
> [!CAUTION]
|
||||
> This server can access local/internal IP addresses and may represent a security risk. Exercise caution when using this MCP server to ensure this does not expose any sensitive data.
|
||||
|
||||
The fetch tool will truncate the response, but by using the `start_index` argument, you can specify where to start the content extraction. This lets models read a webpage in chunks, until they find the information they need.
|
||||
|
||||
### Available Tools
|
||||
@@ -52,10 +55,12 @@ Add to your Claude settings:
|
||||
<summary>Using uvx</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -65,10 +70,12 @@ Add to your Claude settings:
|
||||
<summary>Using docker</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "mcp/fetch"]
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "mcp/fetch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -78,10 +85,60 @@ Add to your Claude settings:
|
||||
<summary>Using pip installation</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_fetch"]
|
||||
{
|
||||
"mcpServers": {
|
||||
"fetch": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_fetch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
### Configure for VS Code
|
||||
|
||||
For quick installation, use one of the one-click install buttons below...
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=fetch&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-fetch%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=fetch&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-fetch%22%5D%7D&quality=insiders)
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=fetch&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Ffetch%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=fetch&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Ffetch%22%5D%7D&quality=insiders)
|
||||
|
||||
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
||||
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
> Note that the `mcp` key is needed when using the `mcp.json` file.
|
||||
|
||||
<details>
|
||||
<summary>Using uvx</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"fetch": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-fetch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Using Docker</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"fetch": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "mcp/fetch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -143,6 +143,64 @@ Note: all directories must be mounted to `/projects` by default.
|
||||
}
|
||||
```
|
||||
|
||||
## Usage with VS Code
|
||||
|
||||
For quick installation, click the installation buttons below...
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-filesystem%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-filesystem%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D&quality=insiders)
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fprojects%2Fworkspace%22%2C%22mcp%2Ffilesystem%22%2C%22%2Fprojects%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fprojects%2Fworkspace%22%2C%22mcp%2Ffilesystem%22%2C%22%2Fprojects%22%5D%7D&quality=insiders)
|
||||
|
||||
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open Settings (JSON)`.
|
||||
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
||||
|
||||
You can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server.
|
||||
|
||||
### Docker
|
||||
Note: all directories must be mounted to `/projects` by default.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"filesystem": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--mount", "type=bind,src=${workspaceFolder},dst=/projects/workspace",
|
||||
"mcp/filesystem",
|
||||
"/projects"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-filesystem",
|
||||
"${workspaceFolder}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Docker build:
|
||||
|
||||
@@ -97,6 +97,8 @@ async function validatePath(requestedPath: string): Promise<string> {
|
||||
// Schema definitions
|
||||
const ReadFileArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
tail: z.number().optional().describe('If provided, returns only the last N lines of the file'),
|
||||
head: z.number().optional().describe('If provided, returns only the first N lines of the file')
|
||||
});
|
||||
|
||||
const ReadMultipleFilesArgsSchema = z.object({
|
||||
@@ -127,6 +129,11 @@ const ListDirectoryArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
const ListDirectoryWithSizesArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
sortBy: z.enum(['name', 'size']).optional().default('name').describe('Sort entries by name or size'),
|
||||
});
|
||||
|
||||
const DirectoryTreeArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
@@ -330,6 +337,107 @@ async function applyFileEdits(
|
||||
return formattedDiff;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
if (i === 0) return `${bytes} ${units[i]}`;
|
||||
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
||||
}
|
||||
|
||||
// Memory-efficient implementation to get the last N lines of a file
|
||||
async function tailFile(filePath: string, numLines: number): Promise<string> {
|
||||
const CHUNK_SIZE = 1024; // Read 1KB at a time
|
||||
const stats = await fs.stat(filePath);
|
||||
const fileSize = stats.size;
|
||||
|
||||
if (fileSize === 0) return '';
|
||||
|
||||
// Open file for reading
|
||||
const fileHandle = await fs.open(filePath, 'r');
|
||||
try {
|
||||
const lines: string[] = [];
|
||||
let position = fileSize;
|
||||
let chunk = Buffer.alloc(CHUNK_SIZE);
|
||||
let linesFound = 0;
|
||||
let remainingText = '';
|
||||
|
||||
// Read chunks from the end of the file until we have enough lines
|
||||
while (position > 0 && linesFound < numLines) {
|
||||
const size = Math.min(CHUNK_SIZE, position);
|
||||
position -= size;
|
||||
|
||||
const { bytesRead } = await fileHandle.read(chunk, 0, size, position);
|
||||
if (!bytesRead) break;
|
||||
|
||||
// Get the chunk as a string and prepend any remaining text from previous iteration
|
||||
const readData = chunk.slice(0, bytesRead).toString('utf-8');
|
||||
const chunkText = readData + remainingText;
|
||||
|
||||
// Split by newlines and count
|
||||
const chunkLines = normalizeLineEndings(chunkText).split('\n');
|
||||
|
||||
// If this isn't the end of the file, the first line is likely incomplete
|
||||
// Save it to prepend to the next chunk
|
||||
if (position > 0) {
|
||||
remainingText = chunkLines[0];
|
||||
chunkLines.shift(); // Remove the first (incomplete) line
|
||||
}
|
||||
|
||||
// Add lines to our result (up to the number we need)
|
||||
for (let i = chunkLines.length - 1; i >= 0 && linesFound < numLines; i--) {
|
||||
lines.unshift(chunkLines[i]);
|
||||
linesFound++;
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
} finally {
|
||||
await fileHandle.close();
|
||||
}
|
||||
}
|
||||
|
||||
// New function to get the first N lines of a file
|
||||
async function headFile(filePath: string, numLines: number): Promise<string> {
|
||||
const fileHandle = await fs.open(filePath, 'r');
|
||||
try {
|
||||
const lines: string[] = [];
|
||||
let buffer = '';
|
||||
let bytesRead = 0;
|
||||
const chunk = Buffer.alloc(1024); // 1KB buffer
|
||||
|
||||
// Read chunks and count lines until we have enough or reach EOF
|
||||
while (lines.length < numLines) {
|
||||
const result = await fileHandle.read(chunk, 0, chunk.length, bytesRead);
|
||||
if (result.bytesRead === 0) break; // End of file
|
||||
bytesRead += result.bytesRead;
|
||||
buffer += chunk.slice(0, result.bytesRead).toString('utf-8');
|
||||
|
||||
const newLineIndex = buffer.lastIndexOf('\n');
|
||||
if (newLineIndex !== -1) {
|
||||
const completeLines = buffer.slice(0, newLineIndex).split('\n');
|
||||
buffer = buffer.slice(newLineIndex + 1);
|
||||
for (const line of completeLines) {
|
||||
lines.push(line);
|
||||
if (lines.length >= numLines) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is leftover content and we still need lines, add it
|
||||
if (buffer.length > 0 && lines.length < numLines) {
|
||||
lines.push(buffer);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
} finally {
|
||||
await fileHandle.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Tool handlers
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
@@ -340,7 +448,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
"Read the complete contents of a file from the file system. " +
|
||||
"Handles various text encodings and provides detailed error messages " +
|
||||
"if the file cannot be read. Use this tool when you need to examine " +
|
||||
"the contents of a single file. Only works within allowed directories.",
|
||||
"the contents of a single file. Use the 'head' parameter to read only " +
|
||||
"the first N lines of a file, or the 'tail' parameter to read only " +
|
||||
"the last N lines of a file. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
@@ -387,6 +497,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "list_directory_with_sizes",
|
||||
description:
|
||||
"Get a detailed listing of all files and directories in a specified path, including sizes. " +
|
||||
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
||||
"prefixes. This tool is useful for understanding directory structure and " +
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ListDirectoryWithSizesArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "directory_tree",
|
||||
description:
|
||||
@@ -451,6 +570,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
throw new Error(`Invalid arguments for read_file: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
|
||||
if (parsed.data.head && parsed.data.tail) {
|
||||
throw new Error("Cannot specify both head and tail parameters simultaneously");
|
||||
}
|
||||
|
||||
if (parsed.data.tail) {
|
||||
// Use memory-efficient tail implementation for large files
|
||||
const tailContent = await tailFile(validPath, parsed.data.tail);
|
||||
return {
|
||||
content: [{ type: "text", text: tailContent }],
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.data.head) {
|
||||
// Use memory-efficient head implementation for large files
|
||||
const headContent = await headFile(validPath, parsed.data.head);
|
||||
return {
|
||||
content: [{ type: "text", text: headContent }],
|
||||
};
|
||||
}
|
||||
|
||||
const content = await fs.readFile(validPath, "utf-8");
|
||||
return {
|
||||
content: [{ type: "text", text: content }],
|
||||
@@ -530,11 +670,77 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
};
|
||||
}
|
||||
|
||||
case "directory_tree": {
|
||||
const parsed = DirectoryTreeArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
||||
case "list_directory_with_sizes": {
|
||||
const parsed = ListDirectoryWithSizesArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for list_directory_with_sizes: ${parsed.error}`);
|
||||
}
|
||||
const validPath = await validatePath(parsed.data.path);
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||
|
||||
// Get detailed information for each entry
|
||||
const detailedEntries = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(validPath, entry.name);
|
||||
try {
|
||||
const stats = await fs.stat(entryPath);
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: stats.size,
|
||||
mtime: stats.mtime
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: 0,
|
||||
mtime: new Date(0)
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Sort entries based on sortBy parameter
|
||||
const sortedEntries = [...detailedEntries].sort((a, b) => {
|
||||
if (parsed.data.sortBy === 'size') {
|
||||
return b.size - a.size; // Descending by size
|
||||
}
|
||||
// Default sort by name
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
// Format the output
|
||||
const formattedEntries = sortedEntries.map(entry =>
|
||||
`${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${
|
||||
entry.isDirectory ? "" : formatSize(entry.size).padStart(10)
|
||||
}`
|
||||
);
|
||||
|
||||
// Add summary
|
||||
const totalFiles = detailedEntries.filter(e => !e.isDirectory).length;
|
||||
const totalDirs = detailedEntries.filter(e => e.isDirectory).length;
|
||||
const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0);
|
||||
|
||||
const summary = [
|
||||
"",
|
||||
`Total: ${totalFiles} files, ${totalDirs} directories`,
|
||||
`Combined size: ${formatSize(totalSize)}`
|
||||
];
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: [...formattedEntries, ...summary].join("\n")
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
case "directory_tree": {
|
||||
const parsed = DirectoryTreeArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
||||
}
|
||||
|
||||
interface TreeEntry {
|
||||
name: string;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
COPY src/gdrive /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
|
||||
|
||||
FROM node:22-alpine AS release
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
COPY src/gdrive/replace_open.sh /replace_open.sh
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
RUN sh /replace_open.sh
|
||||
|
||||
RUN rm /replace_open.sh
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,95 +0,0 @@
|
||||
# Google Drive server
|
||||
|
||||
This MCP server integrates with Google Drive to allow listing, reading, and searching over files.
|
||||
|
||||
## Components
|
||||
|
||||
### Tools
|
||||
|
||||
- **search**
|
||||
- Search for files in Google Drive
|
||||
- Input: `query` (string): Search query
|
||||
- Returns file names and MIME types of matching files
|
||||
|
||||
### Resources
|
||||
|
||||
The server provides access to Google Drive files:
|
||||
|
||||
- **Files** (`gdrive:///<file_id>`)
|
||||
- Supports all file types
|
||||
- Google Workspace files are automatically exported:
|
||||
- Docs → Markdown
|
||||
- Sheets → CSV
|
||||
- Presentations → Plain text
|
||||
- Drawings → PNG
|
||||
- Other files are provided in their native format
|
||||
|
||||
## Getting started
|
||||
|
||||
1. [Create a new Google Cloud project](https://console.cloud.google.com/projectcreate)
|
||||
2. [Enable the Google Drive API](https://console.cloud.google.com/workspace-api/products)
|
||||
3. [Configure an OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) ("internal" is fine for testing)
|
||||
4. Add OAuth scope `https://www.googleapis.com/auth/drive.readonly`
|
||||
5. [Create an OAuth Client ID](https://console.cloud.google.com/apis/credentials/oauthclient) for application type "Desktop App"
|
||||
6. Download the JSON file of your client's OAuth keys
|
||||
7. Rename the key file to `gcp-oauth.keys.json` and place into the root of this repo (i.e. `servers/gcp-oauth.keys.json`)
|
||||
|
||||
Make sure to build the server with either `npm run build` or `npm run watch`.
|
||||
|
||||
### Authentication
|
||||
|
||||
To authenticate and save credentials:
|
||||
|
||||
1. Run the server with the `auth` argument: `node ./dist auth`
|
||||
2. This will open an authentication flow in your system browser
|
||||
3. Complete the authentication process
|
||||
4. Credentials will be saved in the root of this repo (i.e. `servers/.gdrive-server-credentials.json`)
|
||||
|
||||
### Usage with Desktop App
|
||||
|
||||
To integrate this server with the desktop app, add the following to your app's server configuration:
|
||||
|
||||
#### Docker
|
||||
|
||||
Authentication:
|
||||
|
||||
Assuming you have completed setting up the OAuth application on Google Cloud, you can now auth the server with the following command, replacing `/path/to/gcp-oauth.keys.json` with the path to your OAuth keys file:
|
||||
|
||||
```bash
|
||||
docker run -i --rm --mount type=bind,source=/path/to/gcp-oauth.keys.json,target=/gcp-oauth.keys.json -v mcp-gdrive:/gdrive-server -e GDRIVE_OAUTH_PATH=/gcp-oauth.keys.json -e "GDRIVE_CREDENTIALS_PATH=/gdrive-server/credentials.json" -p 3000:3000 mcp/gdrive auth
|
||||
```
|
||||
|
||||
The command will print the URL to open in your browser. Open this URL in your browser and complete the authentication process. The credentials will be saved in the `mcp-gdrive` volume.
|
||||
|
||||
Once authenticated, you can use the server in your app's server configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gdrive": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "-v", "mcp-gdrive:/gdrive-server", "-e", "GDRIVE_CREDENTIALS_PATH=/gdrive-server/credentials.json", "mcp/gdrive"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gdrive": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-gdrive"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,219 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { authenticate } from "@google-cloud/local-auth";
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "fs";
|
||||
import { google } from "googleapis";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const drive = google.drive("v3");
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "example-servers/gdrive",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
|
||||
const pageSize = 10;
|
||||
const params: any = {
|
||||
pageSize,
|
||||
fields: "nextPageToken, files(id, name, mimeType)",
|
||||
};
|
||||
|
||||
if (request.params?.cursor) {
|
||||
params.pageToken = request.params.cursor;
|
||||
}
|
||||
|
||||
const res = await drive.files.list(params);
|
||||
const files = res.data.files!;
|
||||
|
||||
return {
|
||||
resources: files.map((file) => ({
|
||||
uri: `gdrive:///${file.id}`,
|
||||
mimeType: file.mimeType,
|
||||
name: file.name,
|
||||
})),
|
||||
nextCursor: res.data.nextPageToken,
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const fileId = request.params.uri.replace("gdrive:///", "");
|
||||
|
||||
// First get file metadata to check mime type
|
||||
const file = await drive.files.get({
|
||||
fileId,
|
||||
fields: "mimeType",
|
||||
});
|
||||
|
||||
// For Google Docs/Sheets/etc we need to export
|
||||
if (file.data.mimeType?.startsWith("application/vnd.google-apps")) {
|
||||
let exportMimeType: string;
|
||||
switch (file.data.mimeType) {
|
||||
case "application/vnd.google-apps.document":
|
||||
exportMimeType = "text/markdown";
|
||||
break;
|
||||
case "application/vnd.google-apps.spreadsheet":
|
||||
exportMimeType = "text/csv";
|
||||
break;
|
||||
case "application/vnd.google-apps.presentation":
|
||||
exportMimeType = "text/plain";
|
||||
break;
|
||||
case "application/vnd.google-apps.drawing":
|
||||
exportMimeType = "image/png";
|
||||
break;
|
||||
default:
|
||||
exportMimeType = "text/plain";
|
||||
}
|
||||
|
||||
const res = await drive.files.export(
|
||||
{ fileId, mimeType: exportMimeType },
|
||||
{ responseType: "text" },
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: exportMimeType,
|
||||
text: res.data,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// For regular files download content
|
||||
const res = await drive.files.get(
|
||||
{ fileId, alt: "media" },
|
||||
{ responseType: "arraybuffer" },
|
||||
);
|
||||
const mimeType = file.data.mimeType || "application/octet-stream";
|
||||
if (mimeType.startsWith("text/") || mimeType === "application/json") {
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: mimeType,
|
||||
text: Buffer.from(res.data as ArrayBuffer).toString("utf-8"),
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: mimeType,
|
||||
blob: Buffer.from(res.data as ArrayBuffer).toString("base64"),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "search",
|
||||
description: "Search for files in Google Drive",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "search") {
|
||||
const userQuery = request.params.arguments?.query as string;
|
||||
const escapedQuery = userQuery.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||
const formattedQuery = `fullText contains '${escapedQuery}'`;
|
||||
|
||||
const res = await drive.files.list({
|
||||
q: formattedQuery,
|
||||
pageSize: 10,
|
||||
fields: "files(id, name, mimeType, modifiedTime, size)",
|
||||
});
|
||||
|
||||
const fileList = res.data.files
|
||||
?.map((file: any) => `${file.name} (${file.mimeType})`)
|
||||
.join("\n");
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Found ${res.data.files?.length ?? 0} files:\n${fileList}`,
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
throw new Error("Tool not found");
|
||||
});
|
||||
|
||||
const credentialsPath = process.env.GDRIVE_CREDENTIALS_PATH || path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../../.gdrive-server-credentials.json",
|
||||
);
|
||||
|
||||
async function authenticateAndSaveCredentials() {
|
||||
console.log("Launching auth flow…");
|
||||
const auth = await authenticate({
|
||||
keyfilePath: process.env.GDRIVE_OAUTH_PATH || path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../../gcp-oauth.keys.json",
|
||||
),
|
||||
scopes: ["https://www.googleapis.com/auth/drive.readonly"],
|
||||
});
|
||||
fs.writeFileSync(credentialsPath, JSON.stringify(auth.credentials));
|
||||
console.log("Credentials saved. You can now run the server.");
|
||||
}
|
||||
|
||||
async function loadCredentialsAndRunServer() {
|
||||
if (!fs.existsSync(credentialsPath)) {
|
||||
console.error(
|
||||
"Credentials not found. Please run with 'auth' argument first.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const credentials = JSON.parse(fs.readFileSync(credentialsPath, "utf-8"));
|
||||
const auth = new google.auth.OAuth2();
|
||||
auth.setCredentials(credentials);
|
||||
google.options({ auth });
|
||||
|
||||
console.error("Credentials loaded. Starting server.");
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
if (process.argv[2] === "auth") {
|
||||
authenticateAndSaveCredentials().catch(console.error);
|
||||
} else {
|
||||
loadCredentialsAndRunServer().catch(console.error);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-gdrive",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for interacting with Google Drive",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-gdrive": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/local-auth": "^3.0.1",
|
||||
"@modelcontextprotocol/sdk": "1.0.1",
|
||||
"googleapis": "^144.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Basic script to replace opn(authorizeUrl, { wait: false }).then(cp => cp.unref()); with process.stdout.write(`Open this URL in your browser: ${authorizeUrl}`);
|
||||
|
||||
sed -i 's/opn(authorizeUrl, { wait: false }).then(cp => cp.unref());/process.stderr.write(`Open this URL in your browser: ${authorizeUrl}\n`);/' node_modules/@google-cloud/local-auth/build/src/index.js
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -153,6 +153,54 @@ Add this to your `claude_desktop_config.json`:
|
||||
```
|
||||
</details>
|
||||
|
||||
### Usage with VS Code
|
||||
|
||||
For quick installation, use one of the one-click install buttons below...
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=git&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-git%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=git&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-git%22%5D%7D&quality=insiders)
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=git&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fworkspace%22%2C%22mcp%2Fgit%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=git&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fworkspace%22%2C%22mcp%2Fgit%22%5D%7D&quality=insiders)
|
||||
|
||||
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open Settings (JSON)`.
|
||||
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"git": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-git"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Docker installation:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"git": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"--mount", "type=bind,src=${workspaceFolder},dst=/workspace",
|
||||
"mcp/git"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Usage with [Zed](https://github.com/zed-industries/zed)
|
||||
|
||||
Add to your Zed settings.json:
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
# Must be entire project because `prepare` script is run during `npm install` and requires all files.
|
||||
COPY src/github /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
FROM node:22.12-alpine AS release
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,372 +0,0 @@
|
||||
# GitHub MCP Server
|
||||
|
||||
**Deprecation Notice:** Development for this project has been moved to GitHub in the http://github.com/github/github-mcp-server repo.
|
||||
|
||||
---
|
||||
|
||||
MCP Server for the GitHub API, enabling file operations, repository management, search functionality, and more.
|
||||
|
||||
### Features
|
||||
|
||||
- **Automatic Branch Creation**: When creating/updating files or pushing changes, branches are automatically created if they don't exist
|
||||
- **Comprehensive Error Handling**: Clear error messages for common issues
|
||||
- **Git History Preservation**: Operations maintain proper Git history without force pushing
|
||||
- **Batch Operations**: Support for both single-file and multi-file operations
|
||||
- **Advanced Search**: Support for searching code, issues/PRs, and users
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
1. `create_or_update_file`
|
||||
- Create or update a single file in a repository
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner (username or organization)
|
||||
- `repo` (string): Repository name
|
||||
- `path` (string): Path where to create/update the file
|
||||
- `content` (string): Content of the file
|
||||
- `message` (string): Commit message
|
||||
- `branch` (string): Branch to create/update the file in
|
||||
- `sha` (optional string): SHA of file being replaced (for updates)
|
||||
- Returns: File content and commit details
|
||||
|
||||
2. `push_files`
|
||||
- Push multiple files in a single commit
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `branch` (string): Branch to push to
|
||||
- `files` (array): Files to push, each with `path` and `content`
|
||||
- `message` (string): Commit message
|
||||
- Returns: Updated branch reference
|
||||
|
||||
3. `search_repositories`
|
||||
- Search for GitHub repositories
|
||||
- Inputs:
|
||||
- `query` (string): Search query
|
||||
- `page` (optional number): Page number for pagination
|
||||
- `perPage` (optional number): Results per page (max 100)
|
||||
- Returns: Repository search results
|
||||
|
||||
4. `create_repository`
|
||||
- Create a new GitHub repository
|
||||
- Inputs:
|
||||
- `name` (string): Repository name
|
||||
- `description` (optional string): Repository description
|
||||
- `private` (optional boolean): Whether repo should be private
|
||||
- `autoInit` (optional boolean): Initialize with README
|
||||
- Returns: Created repository details
|
||||
|
||||
5. `get_file_contents`
|
||||
- Get contents of a file or directory
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `path` (string): Path to file/directory
|
||||
- `branch` (optional string): Branch to get contents from
|
||||
- Returns: File/directory contents
|
||||
|
||||
6. `create_issue`
|
||||
- Create a new issue
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `title` (string): Issue title
|
||||
- `body` (optional string): Issue description
|
||||
- `assignees` (optional string[]): Usernames to assign
|
||||
- `labels` (optional string[]): Labels to add
|
||||
- `milestone` (optional number): Milestone number
|
||||
- Returns: Created issue details
|
||||
|
||||
7. `create_pull_request`
|
||||
- Create a new pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `title` (string): PR title
|
||||
- `body` (optional string): PR description
|
||||
- `head` (string): Branch containing changes
|
||||
- `base` (string): Branch to merge into
|
||||
- `draft` (optional boolean): Create as draft PR
|
||||
- `maintainer_can_modify` (optional boolean): Allow maintainer edits
|
||||
- Returns: Created pull request details
|
||||
|
||||
8. `fork_repository`
|
||||
- Fork a repository
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `organization` (optional string): Organization to fork to
|
||||
- Returns: Forked repository details
|
||||
|
||||
9. `create_branch`
|
||||
- Create a new branch
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `branch` (string): Name for new branch
|
||||
- `from_branch` (optional string): Source branch (defaults to repo default)
|
||||
- Returns: Created branch reference
|
||||
|
||||
10. `list_issues`
|
||||
- List and filter repository issues
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `state` (optional string): Filter by state ('open', 'closed', 'all')
|
||||
- `labels` (optional string[]): Filter by labels
|
||||
- `sort` (optional string): Sort by ('created', 'updated', 'comments')
|
||||
- `direction` (optional string): Sort direction ('asc', 'desc')
|
||||
- `since` (optional string): Filter by date (ISO 8601 timestamp)
|
||||
- `page` (optional number): Page number
|
||||
- `per_page` (optional number): Results per page
|
||||
- Returns: Array of issue details
|
||||
|
||||
11. `update_issue`
|
||||
- Update an existing issue
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `issue_number` (number): Issue number to update
|
||||
- `title` (optional string): New title
|
||||
- `body` (optional string): New description
|
||||
- `state` (optional string): New state ('open' or 'closed')
|
||||
- `labels` (optional string[]): New labels
|
||||
- `assignees` (optional string[]): New assignees
|
||||
- `milestone` (optional number): New milestone number
|
||||
- Returns: Updated issue details
|
||||
|
||||
12. `add_issue_comment`
|
||||
- Add a comment to an issue
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `issue_number` (number): Issue number to comment on
|
||||
- `body` (string): Comment text
|
||||
- Returns: Created comment details
|
||||
|
||||
13. `search_code`
|
||||
- Search for code across GitHub repositories
|
||||
- Inputs:
|
||||
- `q` (string): Search query using GitHub code search syntax
|
||||
- `sort` (optional string): Sort field ('indexed' only)
|
||||
- `order` (optional string): Sort order ('asc' or 'desc')
|
||||
- `per_page` (optional number): Results per page (max 100)
|
||||
- `page` (optional number): Page number
|
||||
- Returns: Code search results with repository context
|
||||
|
||||
14. `search_issues`
|
||||
- Search for issues and pull requests
|
||||
- Inputs:
|
||||
- `q` (string): Search query using GitHub issues search syntax
|
||||
- `sort` (optional string): Sort field (comments, reactions, created, etc.)
|
||||
- `order` (optional string): Sort order ('asc' or 'desc')
|
||||
- `per_page` (optional number): Results per page (max 100)
|
||||
- `page` (optional number): Page number
|
||||
- Returns: Issue and pull request search results
|
||||
|
||||
15. `search_users`
|
||||
- Search for GitHub users
|
||||
- Inputs:
|
||||
- `q` (string): Search query using GitHub users search syntax
|
||||
- `sort` (optional string): Sort field (followers, repositories, joined)
|
||||
- `order` (optional string): Sort order ('asc' or 'desc')
|
||||
- `per_page` (optional number): Results per page (max 100)
|
||||
- `page` (optional number): Page number
|
||||
- Returns: User search results
|
||||
|
||||
16. `list_commits`
|
||||
- Gets commits of a branch in a repository
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `page` (optional string): page number
|
||||
- `per_page` (optional string): number of record per page
|
||||
- `sha` (optional string): branch name
|
||||
- Returns: List of commits
|
||||
|
||||
17. `get_issue`
|
||||
- Gets the contents of an issue within a repository
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `issue_number` (number): Issue number to retrieve
|
||||
- Returns: Github Issue object & details
|
||||
|
||||
18. `get_pull_request`
|
||||
- Get details of a specific pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Pull request details including diff and review status
|
||||
|
||||
19. `list_pull_requests`
|
||||
- List and filter repository pull requests
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `state` (optional string): Filter by state ('open', 'closed', 'all')
|
||||
- `head` (optional string): Filter by head user/org and branch
|
||||
- `base` (optional string): Filter by base branch
|
||||
- `sort` (optional string): Sort by ('created', 'updated', 'popularity', 'long-running')
|
||||
- `direction` (optional string): Sort direction ('asc', 'desc')
|
||||
- `per_page` (optional number): Results per page (max 100)
|
||||
- `page` (optional number): Page number
|
||||
- Returns: Array of pull request details
|
||||
|
||||
20. `create_pull_request_review`
|
||||
- Create a review on a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- `body` (string): Review comment text
|
||||
- `event` (string): Review action ('APPROVE', 'REQUEST_CHANGES', 'COMMENT')
|
||||
- `commit_id` (optional string): SHA of commit to review
|
||||
- `comments` (optional array): Line-specific comments, each with:
|
||||
- `path` (string): File path
|
||||
- `position` (number): Line position in diff
|
||||
- `body` (string): Comment text
|
||||
- Returns: Created review details
|
||||
|
||||
21. `merge_pull_request`
|
||||
- Merge a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- `commit_title` (optional string): Title for merge commit
|
||||
- `commit_message` (optional string): Extra detail for merge commit
|
||||
- `merge_method` (optional string): Merge method ('merge', 'squash', 'rebase')
|
||||
- Returns: Merge result details
|
||||
|
||||
22. `get_pull_request_files`
|
||||
- Get the list of files changed in a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Array of changed files with patch and status details
|
||||
|
||||
23. `get_pull_request_status`
|
||||
- Get the combined status of all status checks for a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Combined status check results and individual check details
|
||||
|
||||
24. `update_pull_request_branch`
|
||||
- Update a pull request branch with the latest changes from the base branch (equivalent to GitHub's "Update branch" button)
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- `expected_head_sha` (optional string): The expected SHA of the pull request's HEAD ref
|
||||
- Returns: Success message when branch is updated
|
||||
|
||||
25. `get_pull_request_comments`
|
||||
- Get the review comments on a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Array of pull request review comments with details like the comment text, author, and location in the diff
|
||||
|
||||
26. `get_pull_request_reviews`
|
||||
- Get the reviews on a pull request
|
||||
- Inputs:
|
||||
- `owner` (string): Repository owner
|
||||
- `repo` (string): Repository name
|
||||
- `pull_number` (number): Pull request number
|
||||
- Returns: Array of pull request reviews with details like the review state (APPROVED, CHANGES_REQUESTED, etc.), reviewer, and review body
|
||||
|
||||
## Search Query Syntax
|
||||
|
||||
### Code Search
|
||||
- `language:javascript`: Search by programming language
|
||||
- `repo:owner/name`: Search in specific repository
|
||||
- `path:app/src`: Search in specific path
|
||||
- `extension:js`: Search by file extension
|
||||
- Example: `q: "import express" language:typescript path:src/`
|
||||
|
||||
### Issues Search
|
||||
- `is:issue` or `is:pr`: Filter by type
|
||||
- `is:open` or `is:closed`: Filter by state
|
||||
- `label:bug`: Search by label
|
||||
- `author:username`: Search by author
|
||||
- Example: `q: "memory leak" is:issue is:open label:bug`
|
||||
|
||||
### Users Search
|
||||
- `type:user` or `type:org`: Filter by account type
|
||||
- `followers:>1000`: Filter by followers
|
||||
- `location:London`: Search by location
|
||||
- Example: `q: "fullstack developer" location:London followers:>100`
|
||||
|
||||
For detailed search syntax, see [GitHub's searching documentation](https://docs.github.com/en/search-github/searching-on-github).
|
||||
|
||||
## Setup
|
||||
|
||||
### Personal Access Token
|
||||
[Create a GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with appropriate permissions:
|
||||
- Go to [Personal access tokens](https://github.com/settings/tokens) (in GitHub Settings > Developer settings)
|
||||
- Select which repositories you'd like this token to have access to (Public, All, or Select)
|
||||
- Create a token with the `repo` scope ("Full control of private repositories")
|
||||
- Alternatively, if working only with public repositories, select only the `public_repo` scope
|
||||
- Copy the generated token
|
||||
|
||||
### Usage with Claude Desktop
|
||||
To use this with Claude Desktop, add the following to your `claude_desktop_config.json`:
|
||||
|
||||
#### Docker
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"mcp/github"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-github"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Docker build:
|
||||
|
||||
```bash
|
||||
docker build -t mcp/github -f src/github/Dockerfile .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,89 +0,0 @@
|
||||
export class GitHubError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number,
|
||||
public readonly response: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = "GitHubError";
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubValidationError extends GitHubError {
|
||||
constructor(message: string, status: number, response: unknown) {
|
||||
super(message, status, response);
|
||||
this.name = "GitHubValidationError";
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubResourceNotFoundError extends GitHubError {
|
||||
constructor(resource: string) {
|
||||
super(`Resource not found: ${resource}`, 404, { message: `${resource} not found` });
|
||||
this.name = "GitHubResourceNotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubAuthenticationError extends GitHubError {
|
||||
constructor(message = "Authentication failed") {
|
||||
super(message, 401, { message });
|
||||
this.name = "GitHubAuthenticationError";
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubPermissionError extends GitHubError {
|
||||
constructor(message = "Insufficient permissions") {
|
||||
super(message, 403, { message });
|
||||
this.name = "GitHubPermissionError";
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubRateLimitError extends GitHubError {
|
||||
constructor(
|
||||
message = "Rate limit exceeded",
|
||||
public readonly resetAt: Date
|
||||
) {
|
||||
super(message, 429, { message, reset_at: resetAt.toISOString() });
|
||||
this.name = "GitHubRateLimitError";
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubConflictError extends GitHubError {
|
||||
constructor(message: string) {
|
||||
super(message, 409, { message });
|
||||
this.name = "GitHubConflictError";
|
||||
}
|
||||
}
|
||||
|
||||
export function isGitHubError(error: unknown): error is GitHubError {
|
||||
return error instanceof GitHubError;
|
||||
}
|
||||
|
||||
export function createGitHubError(status: number, response: any): GitHubError {
|
||||
switch (status) {
|
||||
case 401:
|
||||
return new GitHubAuthenticationError(response?.message);
|
||||
case 403:
|
||||
return new GitHubPermissionError(response?.message);
|
||||
case 404:
|
||||
return new GitHubResourceNotFoundError(response?.message || "Resource");
|
||||
case 409:
|
||||
return new GitHubConflictError(response?.message || "Conflict occurred");
|
||||
case 422:
|
||||
return new GitHubValidationError(
|
||||
response?.message || "Validation failed",
|
||||
status,
|
||||
response
|
||||
);
|
||||
case 429:
|
||||
return new GitHubRateLimitError(
|
||||
response?.message,
|
||||
new Date(response?.reset_at || Date.now() + 60000)
|
||||
);
|
||||
default:
|
||||
return new GitHubError(
|
||||
response?.message || "GitHub API error",
|
||||
status,
|
||||
response
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Base schemas for common types
|
||||
export const GitHubAuthorSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
date: z.string(),
|
||||
});
|
||||
|
||||
export const GitHubOwnerSchema = z.object({
|
||||
login: z.string(),
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
avatar_url: z.string(),
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
type: z.string(),
|
||||
});
|
||||
|
||||
export const GitHubRepositorySchema = z.object({
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
name: z.string(),
|
||||
full_name: z.string(),
|
||||
private: z.boolean(),
|
||||
owner: GitHubOwnerSchema,
|
||||
html_url: z.string(),
|
||||
description: z.string().nullable(),
|
||||
fork: z.boolean(),
|
||||
url: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
pushed_at: z.string(),
|
||||
git_url: z.string(),
|
||||
ssh_url: z.string(),
|
||||
clone_url: z.string(),
|
||||
default_branch: z.string(),
|
||||
});
|
||||
|
||||
export const GithubFileContentLinks = z.object({
|
||||
self: z.string(),
|
||||
git: z.string().nullable(),
|
||||
html: z.string().nullable()
|
||||
});
|
||||
|
||||
export const GitHubFileContentSchema = z.object({
|
||||
name: z.string(),
|
||||
path: z.string(),
|
||||
sha: z.string(),
|
||||
size: z.number(),
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
git_url: z.string(),
|
||||
download_url: z.string(),
|
||||
type: z.string(),
|
||||
content: z.string().optional(),
|
||||
encoding: z.string().optional(),
|
||||
_links: GithubFileContentLinks
|
||||
});
|
||||
|
||||
export const GitHubDirectoryContentSchema = z.object({
|
||||
type: z.string(),
|
||||
size: z.number(),
|
||||
name: z.string(),
|
||||
path: z.string(),
|
||||
sha: z.string(),
|
||||
url: z.string(),
|
||||
git_url: z.string(),
|
||||
html_url: z.string(),
|
||||
download_url: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const GitHubContentSchema = z.union([
|
||||
GitHubFileContentSchema,
|
||||
z.array(GitHubDirectoryContentSchema),
|
||||
]);
|
||||
|
||||
export const GitHubTreeEntrySchema = z.object({
|
||||
path: z.string(),
|
||||
mode: z.enum(["100644", "100755", "040000", "160000", "120000"]),
|
||||
type: z.enum(["blob", "tree", "commit"]),
|
||||
size: z.number().optional(),
|
||||
sha: z.string(),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
export const GitHubTreeSchema = z.object({
|
||||
sha: z.string(),
|
||||
url: z.string(),
|
||||
tree: z.array(GitHubTreeEntrySchema),
|
||||
truncated: z.boolean(),
|
||||
});
|
||||
|
||||
export const GitHubCommitSchema = z.object({
|
||||
sha: z.string(),
|
||||
node_id: z.string(),
|
||||
url: z.string(),
|
||||
author: GitHubAuthorSchema,
|
||||
committer: GitHubAuthorSchema,
|
||||
message: z.string(),
|
||||
tree: z.object({
|
||||
sha: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
parents: z.array(
|
||||
z.object({
|
||||
sha: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const GitHubListCommitsSchema = z.array(z.object({
|
||||
sha: z.string(),
|
||||
node_id: z.string(),
|
||||
commit: z.object({
|
||||
author: GitHubAuthorSchema,
|
||||
committer: GitHubAuthorSchema,
|
||||
message: z.string(),
|
||||
tree: z.object({
|
||||
sha: z.string(),
|
||||
url: z.string()
|
||||
}),
|
||||
url: z.string(),
|
||||
comment_count: z.number(),
|
||||
}),
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
comments_url: z.string()
|
||||
}));
|
||||
|
||||
export const GitHubReferenceSchema = z.object({
|
||||
ref: z.string(),
|
||||
node_id: z.string(),
|
||||
url: z.string(),
|
||||
object: z.object({
|
||||
sha: z.string(),
|
||||
type: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
// User and assignee schemas
|
||||
export const GitHubIssueAssigneeSchema = z.object({
|
||||
login: z.string(),
|
||||
id: z.number(),
|
||||
avatar_url: z.string(),
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
});
|
||||
|
||||
// Issue-related schemas
|
||||
export const GitHubLabelSchema = z.object({
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
url: z.string(),
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
default: z.boolean(),
|
||||
description: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const GitHubMilestoneSchema = z.object({
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
labels_url: z.string(),
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
number: z.number(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
state: z.string(),
|
||||
});
|
||||
|
||||
export const GitHubIssueSchema = z.object({
|
||||
url: z.string(),
|
||||
repository_url: z.string(),
|
||||
labels_url: z.string(),
|
||||
comments_url: z.string(),
|
||||
events_url: z.string(),
|
||||
html_url: z.string(),
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
number: z.number(),
|
||||
title: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
labels: z.array(GitHubLabelSchema),
|
||||
state: z.string(),
|
||||
locked: z.boolean(),
|
||||
assignee: GitHubIssueAssigneeSchema.nullable(),
|
||||
assignees: z.array(GitHubIssueAssigneeSchema),
|
||||
milestone: GitHubMilestoneSchema.nullable(),
|
||||
comments: z.number(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
closed_at: z.string().nullable(),
|
||||
body: z.string().nullable(),
|
||||
});
|
||||
|
||||
// Search-related schemas
|
||||
export const GitHubSearchResponseSchema = z.object({
|
||||
total_count: z.number(),
|
||||
incomplete_results: z.boolean(),
|
||||
items: z.array(GitHubRepositorySchema),
|
||||
});
|
||||
|
||||
// Pull request schemas
|
||||
export const GitHubPullRequestRefSchema = z.object({
|
||||
label: z.string(),
|
||||
ref: z.string(),
|
||||
sha: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
repo: GitHubRepositorySchema,
|
||||
});
|
||||
|
||||
export const GitHubPullRequestSchema = z.object({
|
||||
url: z.string(),
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
html_url: z.string(),
|
||||
diff_url: z.string(),
|
||||
patch_url: z.string(),
|
||||
issue_url: z.string(),
|
||||
number: z.number(),
|
||||
state: z.string(),
|
||||
locked: z.boolean(),
|
||||
title: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
body: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
closed_at: z.string().nullable(),
|
||||
merged_at: z.string().nullable(),
|
||||
merge_commit_sha: z.string().nullable(),
|
||||
assignee: GitHubIssueAssigneeSchema.nullable(),
|
||||
assignees: z.array(GitHubIssueAssigneeSchema),
|
||||
requested_reviewers: z.array(GitHubIssueAssigneeSchema),
|
||||
labels: z.array(GitHubLabelSchema),
|
||||
head: GitHubPullRequestRefSchema,
|
||||
base: GitHubPullRequestRefSchema,
|
||||
});
|
||||
|
||||
// Export types
|
||||
export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
|
||||
export type GitHubRepository = z.infer<typeof GitHubRepositorySchema>;
|
||||
export type GitHubFileContent = z.infer<typeof GitHubFileContentSchema>;
|
||||
export type GitHubDirectoryContent = z.infer<typeof GitHubDirectoryContentSchema>;
|
||||
export type GitHubContent = z.infer<typeof GitHubContentSchema>;
|
||||
export type GitHubTree = z.infer<typeof GitHubTreeSchema>;
|
||||
export type GitHubCommit = z.infer<typeof GitHubCommitSchema>;
|
||||
export type GitHubListCommits = z.infer<typeof GitHubListCommitsSchema>;
|
||||
export type GitHubReference = z.infer<typeof GitHubReferenceSchema>;
|
||||
export type GitHubIssueAssignee = z.infer<typeof GitHubIssueAssigneeSchema>;
|
||||
export type GitHubLabel = z.infer<typeof GitHubLabelSchema>;
|
||||
export type GitHubMilestone = z.infer<typeof GitHubMilestoneSchema>;
|
||||
export type GitHubIssue = z.infer<typeof GitHubIssueSchema>;
|
||||
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
|
||||
export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>;
|
||||
export type GitHubPullRequestRef = z.infer<typeof GitHubPullRequestRefSchema>;
|
||||
@@ -1,138 +0,0 @@
|
||||
import { getUserAgent } from "universal-user-agent";
|
||||
import { createGitHubError } from "./errors.js";
|
||||
import { VERSION } from "./version.js";
|
||||
|
||||
type RequestOptions = {
|
||||
method?: string;
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
async function parseResponseBody(response: Response): Promise<unknown> {
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType?.includes("application/json")) {
|
||||
return response.json();
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
export function buildUrl(baseUrl: string, params: Record<string, string | number | undefined>): string {
|
||||
const url = new URL(baseUrl);
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
url.searchParams.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const USER_AGENT = `modelcontextprotocol/servers/github/v${VERSION} ${getUserAgent()}`;
|
||||
|
||||
export async function githubRequest(
|
||||
url: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<unknown> {
|
||||
const headers: Record<string, string> = {
|
||||
"Accept": "application/vnd.github.v3+json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": USER_AGENT,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) {
|
||||
headers["Authorization"] = `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: options.method || "GET",
|
||||
headers,
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
|
||||
const responseBody = await parseResponseBody(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw createGitHubError(response.status, responseBody);
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
export function validateBranchName(branch: string): string {
|
||||
const sanitized = branch.trim();
|
||||
if (!sanitized) {
|
||||
throw new Error("Branch name cannot be empty");
|
||||
}
|
||||
if (sanitized.includes("..")) {
|
||||
throw new Error("Branch name cannot contain '..'");
|
||||
}
|
||||
if (/[\s~^:?*[\\\]]/.test(sanitized)) {
|
||||
throw new Error("Branch name contains invalid characters");
|
||||
}
|
||||
if (sanitized.startsWith("/") || sanitized.endsWith("/")) {
|
||||
throw new Error("Branch name cannot start or end with '/'");
|
||||
}
|
||||
if (sanitized.endsWith(".lock")) {
|
||||
throw new Error("Branch name cannot end with '.lock'");
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export function validateRepositoryName(name: string): string {
|
||||
const sanitized = name.trim().toLowerCase();
|
||||
if (!sanitized) {
|
||||
throw new Error("Repository name cannot be empty");
|
||||
}
|
||||
if (!/^[a-z0-9_.-]+$/.test(sanitized)) {
|
||||
throw new Error(
|
||||
"Repository name can only contain lowercase letters, numbers, hyphens, periods, and underscores"
|
||||
);
|
||||
}
|
||||
if (sanitized.startsWith(".") || sanitized.endsWith(".")) {
|
||||
throw new Error("Repository name cannot start or end with a period");
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export function validateOwnerName(owner: string): string {
|
||||
const sanitized = owner.trim().toLowerCase();
|
||||
if (!sanitized) {
|
||||
throw new Error("Owner name cannot be empty");
|
||||
}
|
||||
if (!/^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){0,38}$/.test(sanitized)) {
|
||||
throw new Error(
|
||||
"Owner name must start with a letter or number and can contain up to 39 characters"
|
||||
);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export async function checkBranchExists(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/branches/${branch}`
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "status" in error && error.status === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUserExists(username: string): Promise<boolean> {
|
||||
try {
|
||||
await githubRequest(`https://api.github.com/users/${username}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "status" in error && error.status === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// If the format of this file changes, so it doesn't simply export a VERSION constant,
|
||||
// this will break .github/workflows/version-check.yml.
|
||||
export const VERSION = "0.6.2";
|
||||
@@ -1,517 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import fetch, { Request, Response } from 'node-fetch';
|
||||
|
||||
import * as repository from './operations/repository.js';
|
||||
import * as files from './operations/files.js';
|
||||
import * as issues from './operations/issues.js';
|
||||
import * as pulls from './operations/pulls.js';
|
||||
import * as branches from './operations/branches.js';
|
||||
import * as search from './operations/search.js';
|
||||
import * as commits from './operations/commits.js';
|
||||
import {
|
||||
GitHubError,
|
||||
GitHubValidationError,
|
||||
GitHubResourceNotFoundError,
|
||||
GitHubAuthenticationError,
|
||||
GitHubPermissionError,
|
||||
GitHubRateLimitError,
|
||||
GitHubConflictError,
|
||||
isGitHubError,
|
||||
} from './common/errors.js';
|
||||
import { VERSION } from "./common/version.js";
|
||||
|
||||
// If fetch doesn't exist in global scope, add it
|
||||
if (!globalThis.fetch) {
|
||||
globalThis.fetch = fetch as unknown as typeof global.fetch;
|
||||
}
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "github-mcp-server",
|
||||
version: VERSION,
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function formatGitHubError(error: GitHubError): string {
|
||||
let message = `GitHub API Error: ${error.message}`;
|
||||
|
||||
if (error instanceof GitHubValidationError) {
|
||||
message = `Validation Error: ${error.message}`;
|
||||
if (error.response) {
|
||||
message += `\nDetails: ${JSON.stringify(error.response)}`;
|
||||
}
|
||||
} else if (error instanceof GitHubResourceNotFoundError) {
|
||||
message = `Not Found: ${error.message}`;
|
||||
} else if (error instanceof GitHubAuthenticationError) {
|
||||
message = `Authentication Failed: ${error.message}`;
|
||||
} else if (error instanceof GitHubPermissionError) {
|
||||
message = `Permission Denied: ${error.message}`;
|
||||
} else if (error instanceof GitHubRateLimitError) {
|
||||
message = `Rate Limit Exceeded: ${error.message}\nResets at: ${error.resetAt.toISOString()}`;
|
||||
} else if (error instanceof GitHubConflictError) {
|
||||
message = `Conflict: ${error.message}`;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "create_or_update_file",
|
||||
description: "Create or update a single file in a GitHub repository",
|
||||
inputSchema: zodToJsonSchema(files.CreateOrUpdateFileSchema),
|
||||
},
|
||||
{
|
||||
name: "search_repositories",
|
||||
description: "Search for GitHub repositories",
|
||||
inputSchema: zodToJsonSchema(repository.SearchRepositoriesSchema),
|
||||
},
|
||||
{
|
||||
name: "create_repository",
|
||||
description: "Create a new GitHub repository in your account",
|
||||
inputSchema: zodToJsonSchema(repository.CreateRepositoryOptionsSchema),
|
||||
},
|
||||
{
|
||||
name: "get_file_contents",
|
||||
description: "Get the contents of a file or directory from a GitHub repository",
|
||||
inputSchema: zodToJsonSchema(files.GetFileContentsSchema),
|
||||
},
|
||||
{
|
||||
name: "push_files",
|
||||
description: "Push multiple files to a GitHub repository in a single commit",
|
||||
inputSchema: zodToJsonSchema(files.PushFilesSchema),
|
||||
},
|
||||
{
|
||||
name: "create_issue",
|
||||
description: "Create a new issue in a GitHub repository",
|
||||
inputSchema: zodToJsonSchema(issues.CreateIssueSchema),
|
||||
},
|
||||
{
|
||||
name: "create_pull_request",
|
||||
description: "Create a new pull request in a GitHub repository",
|
||||
inputSchema: zodToJsonSchema(pulls.CreatePullRequestSchema),
|
||||
},
|
||||
{
|
||||
name: "fork_repository",
|
||||
description: "Fork a GitHub repository to your account or specified organization",
|
||||
inputSchema: zodToJsonSchema(repository.ForkRepositorySchema),
|
||||
},
|
||||
{
|
||||
name: "create_branch",
|
||||
description: "Create a new branch in a GitHub repository",
|
||||
inputSchema: zodToJsonSchema(branches.CreateBranchSchema),
|
||||
},
|
||||
{
|
||||
name: "list_commits",
|
||||
description: "Get list of commits of a branch in a GitHub repository",
|
||||
inputSchema: zodToJsonSchema(commits.ListCommitsSchema)
|
||||
},
|
||||
{
|
||||
name: "list_issues",
|
||||
description: "List issues in a GitHub repository with filtering options",
|
||||
inputSchema: zodToJsonSchema(issues.ListIssuesOptionsSchema)
|
||||
},
|
||||
{
|
||||
name: "update_issue",
|
||||
description: "Update an existing issue in a GitHub repository",
|
||||
inputSchema: zodToJsonSchema(issues.UpdateIssueOptionsSchema)
|
||||
},
|
||||
{
|
||||
name: "add_issue_comment",
|
||||
description: "Add a comment to an existing issue",
|
||||
inputSchema: zodToJsonSchema(issues.IssueCommentSchema)
|
||||
},
|
||||
{
|
||||
name: "search_code",
|
||||
description: "Search for code across GitHub repositories",
|
||||
inputSchema: zodToJsonSchema(search.SearchCodeSchema),
|
||||
},
|
||||
{
|
||||
name: "search_issues",
|
||||
description: "Search for issues and pull requests across GitHub repositories",
|
||||
inputSchema: zodToJsonSchema(search.SearchIssuesSchema),
|
||||
},
|
||||
{
|
||||
name: "search_users",
|
||||
description: "Search for users on GitHub",
|
||||
inputSchema: zodToJsonSchema(search.SearchUsersSchema),
|
||||
},
|
||||
{
|
||||
name: "get_issue",
|
||||
description: "Get details of a specific issue in a GitHub repository.",
|
||||
inputSchema: zodToJsonSchema(issues.GetIssueSchema)
|
||||
},
|
||||
{
|
||||
name: "get_pull_request",
|
||||
description: "Get details of a specific pull request",
|
||||
inputSchema: zodToJsonSchema(pulls.GetPullRequestSchema)
|
||||
},
|
||||
{
|
||||
name: "list_pull_requests",
|
||||
description: "List and filter repository pull requests",
|
||||
inputSchema: zodToJsonSchema(pulls.ListPullRequestsSchema)
|
||||
},
|
||||
{
|
||||
name: "create_pull_request_review",
|
||||
description: "Create a review on a pull request",
|
||||
inputSchema: zodToJsonSchema(pulls.CreatePullRequestReviewSchema)
|
||||
},
|
||||
{
|
||||
name: "merge_pull_request",
|
||||
description: "Merge a pull request",
|
||||
inputSchema: zodToJsonSchema(pulls.MergePullRequestSchema)
|
||||
},
|
||||
{
|
||||
name: "get_pull_request_files",
|
||||
description: "Get the list of files changed in a pull request",
|
||||
inputSchema: zodToJsonSchema(pulls.GetPullRequestFilesSchema)
|
||||
},
|
||||
{
|
||||
name: "get_pull_request_status",
|
||||
description: "Get the combined status of all status checks for a pull request",
|
||||
inputSchema: zodToJsonSchema(pulls.GetPullRequestStatusSchema)
|
||||
},
|
||||
{
|
||||
name: "update_pull_request_branch",
|
||||
description: "Update a pull request branch with the latest changes from the base branch",
|
||||
inputSchema: zodToJsonSchema(pulls.UpdatePullRequestBranchSchema)
|
||||
},
|
||||
{
|
||||
name: "get_pull_request_comments",
|
||||
description: "Get the review comments on a pull request",
|
||||
inputSchema: zodToJsonSchema(pulls.GetPullRequestCommentsSchema)
|
||||
},
|
||||
{
|
||||
name: "get_pull_request_reviews",
|
||||
description: "Get the reviews on a pull request",
|
||||
inputSchema: zodToJsonSchema(pulls.GetPullRequestReviewsSchema)
|
||||
}
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
if (!request.params.arguments) {
|
||||
throw new Error("Arguments are required");
|
||||
}
|
||||
|
||||
switch (request.params.name) {
|
||||
case "fork_repository": {
|
||||
const args = repository.ForkRepositorySchema.parse(request.params.arguments);
|
||||
const fork = await repository.forkRepository(args.owner, args.repo, args.organization);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(fork, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "create_branch": {
|
||||
const args = branches.CreateBranchSchema.parse(request.params.arguments);
|
||||
const branch = await branches.createBranchFromRef(
|
||||
args.owner,
|
||||
args.repo,
|
||||
args.branch,
|
||||
args.from_branch
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "search_repositories": {
|
||||
const args = repository.SearchRepositoriesSchema.parse(request.params.arguments);
|
||||
const results = await repository.searchRepositories(
|
||||
args.query,
|
||||
args.page,
|
||||
args.perPage
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "create_repository": {
|
||||
const args = repository.CreateRepositoryOptionsSchema.parse(request.params.arguments);
|
||||
const result = await repository.createRepository(args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_file_contents": {
|
||||
const args = files.GetFileContentsSchema.parse(request.params.arguments);
|
||||
const contents = await files.getFileContents(
|
||||
args.owner,
|
||||
args.repo,
|
||||
args.path,
|
||||
args.branch
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "create_or_update_file": {
|
||||
const args = files.CreateOrUpdateFileSchema.parse(request.params.arguments);
|
||||
const result = await files.createOrUpdateFile(
|
||||
args.owner,
|
||||
args.repo,
|
||||
args.path,
|
||||
args.content,
|
||||
args.message,
|
||||
args.branch,
|
||||
args.sha
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "push_files": {
|
||||
const args = files.PushFilesSchema.parse(request.params.arguments);
|
||||
const result = await files.pushFiles(
|
||||
args.owner,
|
||||
args.repo,
|
||||
args.branch,
|
||||
args.files,
|
||||
args.message
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "create_issue": {
|
||||
const args = issues.CreateIssueSchema.parse(request.params.arguments);
|
||||
const { owner, repo, ...options } = args;
|
||||
|
||||
try {
|
||||
console.error(`[DEBUG] Attempting to create issue in ${owner}/${repo}`);
|
||||
console.error(`[DEBUG] Issue options:`, JSON.stringify(options, null, 2));
|
||||
|
||||
const issue = await issues.createIssue(owner, repo, options);
|
||||
|
||||
console.error(`[DEBUG] Issue created successfully`);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
||||
};
|
||||
} catch (err) {
|
||||
// Type guard for Error objects
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
|
||||
console.error(`[ERROR] Failed to create issue:`, error);
|
||||
|
||||
if (error instanceof GitHubResourceNotFoundError) {
|
||||
throw new Error(
|
||||
`Repository '${owner}/${repo}' not found. Please verify:\n` +
|
||||
`1. The repository exists\n` +
|
||||
`2. You have correct access permissions\n` +
|
||||
`3. The owner and repository names are spelled correctly`
|
||||
);
|
||||
}
|
||||
|
||||
// Safely access error properties
|
||||
throw new Error(
|
||||
`Failed to create issue: ${error.message}${
|
||||
error.stack ? `\nStack: ${error.stack}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
case "create_pull_request": {
|
||||
const args = pulls.CreatePullRequestSchema.parse(request.params.arguments);
|
||||
const pullRequest = await pulls.createPullRequest(args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "search_code": {
|
||||
const args = search.SearchCodeSchema.parse(request.params.arguments);
|
||||
const results = await search.searchCode(args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "search_issues": {
|
||||
const args = search.SearchIssuesSchema.parse(request.params.arguments);
|
||||
const results = await search.searchIssues(args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "search_users": {
|
||||
const args = search.SearchUsersSchema.parse(request.params.arguments);
|
||||
const results = await search.searchUsers(args);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_issues": {
|
||||
const args = issues.ListIssuesOptionsSchema.parse(request.params.arguments);
|
||||
const { owner, repo, ...options } = args;
|
||||
const result = await issues.listIssues(owner, repo, options);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "update_issue": {
|
||||
const args = issues.UpdateIssueOptionsSchema.parse(request.params.arguments);
|
||||
const { owner, repo, issue_number, ...options } = args;
|
||||
const result = await issues.updateIssue(owner, repo, issue_number, options);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "add_issue_comment": {
|
||||
const args = issues.IssueCommentSchema.parse(request.params.arguments);
|
||||
const { owner, repo, issue_number, body } = args;
|
||||
const result = await issues.addIssueComment(owner, repo, issue_number, body);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_commits": {
|
||||
const args = commits.ListCommitsSchema.parse(request.params.arguments);
|
||||
const results = await commits.listCommits(
|
||||
args.owner,
|
||||
args.repo,
|
||||
args.page,
|
||||
args.perPage,
|
||||
args.sha
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_issue": {
|
||||
const args = issues.GetIssueSchema.parse(request.params.arguments);
|
||||
const issue = await issues.getIssue(args.owner, args.repo, args.issue_number);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_pull_request": {
|
||||
const args = pulls.GetPullRequestSchema.parse(request.params.arguments);
|
||||
const pullRequest = await pulls.getPullRequest(args.owner, args.repo, args.pull_number);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "list_pull_requests": {
|
||||
const args = pulls.ListPullRequestsSchema.parse(request.params.arguments);
|
||||
const { owner, repo, ...options } = args;
|
||||
const pullRequests = await pulls.listPullRequests(owner, repo, options);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(pullRequests, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "create_pull_request_review": {
|
||||
const args = pulls.CreatePullRequestReviewSchema.parse(request.params.arguments);
|
||||
const { owner, repo, pull_number, ...options } = args;
|
||||
const review = await pulls.createPullRequestReview(owner, repo, pull_number, options);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(review, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "merge_pull_request": {
|
||||
const args = pulls.MergePullRequestSchema.parse(request.params.arguments);
|
||||
const { owner, repo, pull_number, ...options } = args;
|
||||
const result = await pulls.mergePullRequest(owner, repo, pull_number, options);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_pull_request_files": {
|
||||
const args = pulls.GetPullRequestFilesSchema.parse(request.params.arguments);
|
||||
const files = await pulls.getPullRequestFiles(args.owner, args.repo, args.pull_number);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(files, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_pull_request_status": {
|
||||
const args = pulls.GetPullRequestStatusSchema.parse(request.params.arguments);
|
||||
const status = await pulls.getPullRequestStatus(args.owner, args.repo, args.pull_number);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "update_pull_request_branch": {
|
||||
const args = pulls.UpdatePullRequestBranchSchema.parse(request.params.arguments);
|
||||
const { owner, repo, pull_number, expected_head_sha } = args;
|
||||
await pulls.updatePullRequestBranch(owner, repo, pull_number, expected_head_sha);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ success: true }, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_pull_request_comments": {
|
||||
const args = pulls.GetPullRequestCommentsSchema.parse(request.params.arguments);
|
||||
const comments = await pulls.getPullRequestComments(args.owner, args.repo, args.pull_number);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(comments, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "get_pull_request_reviews": {
|
||||
const args = pulls.GetPullRequestReviewsSchema.parse(request.params.arguments);
|
||||
const reviews = await pulls.getPullRequestReviews(args.owner, args.repo, args.pull_number);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(reviews, null, 2) }],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`);
|
||||
}
|
||||
if (isGitHubError(error)) {
|
||||
throw new Error(formatGitHubError(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("GitHub MCP Server running on stdio");
|
||||
}
|
||||
|
||||
runServer().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { githubRequest } from "../common/utils.js";
|
||||
import { GitHubReferenceSchema } from "../common/types.js";
|
||||
|
||||
// Schema definitions
|
||||
export const CreateBranchOptionsSchema = z.object({
|
||||
ref: z.string(),
|
||||
sha: z.string(),
|
||||
});
|
||||
|
||||
export const CreateBranchSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
branch: z.string().describe("Name for the new branch"),
|
||||
from_branch: z.string().optional().describe("Optional: source branch to create from (defaults to the repository's default branch)"),
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type CreateBranchOptions = z.infer<typeof CreateBranchOptionsSchema>;
|
||||
|
||||
// Function implementations
|
||||
export async function getDefaultBranchSHA(owner: string, repo: string): Promise<string> {
|
||||
try {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`
|
||||
);
|
||||
const data = GitHubReferenceSchema.parse(response);
|
||||
return data.object.sha;
|
||||
} catch (error) {
|
||||
const masterResponse = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/master`
|
||||
);
|
||||
if (!masterResponse) {
|
||||
throw new Error("Could not find default branch (tried 'main' and 'master')");
|
||||
}
|
||||
const data = GitHubReferenceSchema.parse(masterResponse);
|
||||
return data.object.sha;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBranch(
|
||||
owner: string,
|
||||
repo: string,
|
||||
options: CreateBranchOptions
|
||||
): Promise<z.infer<typeof GitHubReferenceSchema>> {
|
||||
const fullRef = `refs/heads/${options.ref}`;
|
||||
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
ref: fullRef,
|
||||
sha: options.sha,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return GitHubReferenceSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function getBranchSHA(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string
|
||||
): Promise<string> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`
|
||||
);
|
||||
|
||||
const data = GitHubReferenceSchema.parse(response);
|
||||
return data.object.sha;
|
||||
}
|
||||
|
||||
export async function createBranchFromRef(
|
||||
owner: string,
|
||||
repo: string,
|
||||
newBranch: string,
|
||||
fromBranch?: string
|
||||
): Promise<z.infer<typeof GitHubReferenceSchema>> {
|
||||
let sha: string;
|
||||
if (fromBranch) {
|
||||
sha = await getBranchSHA(owner, repo, fromBranch);
|
||||
} else {
|
||||
sha = await getDefaultBranchSHA(owner, repo);
|
||||
}
|
||||
|
||||
return createBranch(owner, repo, {
|
||||
ref: newBranch,
|
||||
sha,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateBranch(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
sha: string
|
||||
): Promise<z.infer<typeof GitHubReferenceSchema>> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: {
|
||||
sha,
|
||||
force: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return GitHubReferenceSchema.parse(response);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { githubRequest, buildUrl } from "../common/utils.js";
|
||||
|
||||
export const ListCommitsSchema = z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
sha: z.string().optional(),
|
||||
page: z.number().optional(),
|
||||
perPage: z.number().optional()
|
||||
});
|
||||
|
||||
export async function listCommits(
|
||||
owner: string,
|
||||
repo: string,
|
||||
page?: number,
|
||||
perPage?: number,
|
||||
sha?: string
|
||||
) {
|
||||
return githubRequest(
|
||||
buildUrl(`https://api.github.com/repos/${owner}/${repo}/commits`, {
|
||||
page: page?.toString(),
|
||||
per_page: perPage?.toString(),
|
||||
sha
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { githubRequest } from "../common/utils.js";
|
||||
import {
|
||||
GitHubContentSchema,
|
||||
GitHubAuthorSchema,
|
||||
GitHubTreeSchema,
|
||||
GitHubCommitSchema,
|
||||
GitHubReferenceSchema,
|
||||
GitHubFileContentSchema,
|
||||
} from "../common/types.js";
|
||||
|
||||
// Schema definitions
|
||||
export const FileOperationSchema = z.object({
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
export const CreateOrUpdateFileSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
path: z.string().describe("Path where to create/update the file"),
|
||||
content: z.string().describe("Content of the file"),
|
||||
message: z.string().describe("Commit message"),
|
||||
branch: z.string().describe("Branch to create/update the file in"),
|
||||
sha: z.string().optional().describe("SHA of the file being replaced (required when updating existing files)"),
|
||||
});
|
||||
|
||||
export const GetFileContentsSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
path: z.string().describe("Path to the file or directory"),
|
||||
branch: z.string().optional().describe("Branch to get contents from"),
|
||||
});
|
||||
|
||||
export const PushFilesSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
branch: z.string().describe("Branch to push to (e.g., 'main' or 'master')"),
|
||||
files: z.array(FileOperationSchema).describe("Array of files to push"),
|
||||
message: z.string().describe("Commit message"),
|
||||
});
|
||||
|
||||
export const GitHubCreateUpdateFileResponseSchema = z.object({
|
||||
content: GitHubFileContentSchema.nullable(),
|
||||
commit: z.object({
|
||||
sha: z.string(),
|
||||
node_id: z.string(),
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
author: GitHubAuthorSchema,
|
||||
committer: GitHubAuthorSchema,
|
||||
message: z.string(),
|
||||
tree: z.object({
|
||||
sha: z.string(),
|
||||
url: z.string(),
|
||||
}),
|
||||
parents: z.array(
|
||||
z.object({
|
||||
sha: z.string(),
|
||||
url: z.string(),
|
||||
html_url: z.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type FileOperation = z.infer<typeof FileOperationSchema>;
|
||||
export type GitHubCreateUpdateFileResponse = z.infer<typeof GitHubCreateUpdateFileResponseSchema>;
|
||||
|
||||
// Function implementations
|
||||
export async function getFileContents(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
branch?: string
|
||||
) {
|
||||
let url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`;
|
||||
if (branch) {
|
||||
url += `?ref=${branch}`;
|
||||
}
|
||||
|
||||
const response = await githubRequest(url);
|
||||
const data = GitHubContentSchema.parse(response);
|
||||
|
||||
// If it's a file, decode the content
|
||||
if (!Array.isArray(data) && data.content) {
|
||||
data.content = Buffer.from(data.content, "base64").toString("utf8");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createOrUpdateFile(
|
||||
owner: string,
|
||||
repo: string,
|
||||
path: string,
|
||||
content: string,
|
||||
message: string,
|
||||
branch: string,
|
||||
sha?: string
|
||||
) {
|
||||
const encodedContent = Buffer.from(content).toString("base64");
|
||||
|
||||
let currentSha = sha;
|
||||
if (!currentSha) {
|
||||
try {
|
||||
const existingFile = await getFileContents(owner, repo, path, branch);
|
||||
if (!Array.isArray(existingFile)) {
|
||||
currentSha = existingFile.sha;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Note: File does not exist in branch, will create new file");
|
||||
}
|
||||
}
|
||||
|
||||
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`;
|
||||
const body = {
|
||||
message,
|
||||
content: encodedContent,
|
||||
branch,
|
||||
...(currentSha ? { sha: currentSha } : {}),
|
||||
};
|
||||
|
||||
const response = await githubRequest(url, {
|
||||
method: "PUT",
|
||||
body,
|
||||
});
|
||||
|
||||
return GitHubCreateUpdateFileResponseSchema.parse(response);
|
||||
}
|
||||
|
||||
async function createTree(
|
||||
owner: string,
|
||||
repo: string,
|
||||
files: FileOperation[],
|
||||
baseTree?: string
|
||||
) {
|
||||
const tree = files.map((file) => ({
|
||||
path: file.path,
|
||||
mode: "100644" as const,
|
||||
type: "blob" as const,
|
||||
content: file.content,
|
||||
}));
|
||||
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/trees`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
tree,
|
||||
base_tree: baseTree,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return GitHubTreeSchema.parse(response);
|
||||
}
|
||||
|
||||
async function createCommit(
|
||||
owner: string,
|
||||
repo: string,
|
||||
message: string,
|
||||
tree: string,
|
||||
parents: string[]
|
||||
) {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/commits`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
message,
|
||||
tree,
|
||||
parents,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return GitHubCommitSchema.parse(response);
|
||||
}
|
||||
|
||||
async function updateReference(
|
||||
owner: string,
|
||||
repo: string,
|
||||
ref: string,
|
||||
sha: string
|
||||
) {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs/${ref}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: {
|
||||
sha,
|
||||
force: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return GitHubReferenceSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function pushFiles(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
files: FileOperation[],
|
||||
message: string
|
||||
) {
|
||||
const refResponse = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`
|
||||
);
|
||||
|
||||
const ref = GitHubReferenceSchema.parse(refResponse);
|
||||
const commitSha = ref.object.sha;
|
||||
|
||||
const tree = await createTree(owner, repo, files, commitSha);
|
||||
const commit = await createCommit(owner, repo, message, tree.sha, [commitSha]);
|
||||
return await updateReference(owner, repo, `heads/${branch}`, commit.sha);
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { githubRequest, buildUrl } from "../common/utils.js";
|
||||
|
||||
export const GetIssueSchema = z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
issue_number: z.number(),
|
||||
});
|
||||
|
||||
export const IssueCommentSchema = z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
issue_number: z.number(),
|
||||
body: z.string(),
|
||||
});
|
||||
|
||||
export const CreateIssueOptionsSchema = z.object({
|
||||
title: z.string(),
|
||||
body: z.string().optional(),
|
||||
assignees: z.array(z.string()).optional(),
|
||||
milestone: z.number().optional(),
|
||||
labels: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const CreateIssueSchema = z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
...CreateIssueOptionsSchema.shape,
|
||||
});
|
||||
|
||||
export const ListIssuesOptionsSchema = z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
direction: z.enum(["asc", "desc"]).optional(),
|
||||
labels: z.array(z.string()).optional(),
|
||||
page: z.number().optional(),
|
||||
per_page: z.number().optional(),
|
||||
since: z.string().optional(),
|
||||
sort: z.enum(["created", "updated", "comments"]).optional(),
|
||||
state: z.enum(["open", "closed", "all"]).optional(),
|
||||
});
|
||||
|
||||
export const UpdateIssueOptionsSchema = z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
issue_number: z.number(),
|
||||
title: z.string().optional(),
|
||||
body: z.string().optional(),
|
||||
assignees: z.array(z.string()).optional(),
|
||||
milestone: z.number().optional(),
|
||||
labels: z.array(z.string()).optional(),
|
||||
state: z.enum(["open", "closed"]).optional(),
|
||||
});
|
||||
|
||||
export async function getIssue(owner: string, repo: string, issue_number: number) {
|
||||
return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`);
|
||||
}
|
||||
|
||||
export async function addIssueComment(
|
||||
owner: string,
|
||||
repo: string,
|
||||
issue_number: number,
|
||||
body: string
|
||||
) {
|
||||
return githubRequest(`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}/comments`, {
|
||||
method: "POST",
|
||||
body: { body },
|
||||
});
|
||||
}
|
||||
|
||||
export async function createIssue(
|
||||
owner: string,
|
||||
repo: string,
|
||||
options: z.infer<typeof CreateIssueOptionsSchema>
|
||||
) {
|
||||
return githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/issues`,
|
||||
{
|
||||
method: "POST",
|
||||
body: options,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function listIssues(
|
||||
owner: string,
|
||||
repo: string,
|
||||
options: Omit<z.infer<typeof ListIssuesOptionsSchema>, "owner" | "repo">
|
||||
) {
|
||||
const urlParams: Record<string, string | undefined> = {
|
||||
direction: options.direction,
|
||||
labels: options.labels?.join(","),
|
||||
page: options.page?.toString(),
|
||||
per_page: options.per_page?.toString(),
|
||||
since: options.since,
|
||||
sort: options.sort,
|
||||
state: options.state
|
||||
};
|
||||
|
||||
return githubRequest(
|
||||
buildUrl(`https://api.github.com/repos/${owner}/${repo}/issues`, urlParams)
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateIssue(
|
||||
owner: string,
|
||||
repo: string,
|
||||
issue_number: number,
|
||||
options: Omit<z.infer<typeof UpdateIssueOptionsSchema>, "owner" | "repo" | "issue_number">
|
||||
) {
|
||||
return githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/issues/${issue_number}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: options,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { githubRequest } from "../common/utils.js";
|
||||
import {
|
||||
GitHubPullRequestSchema,
|
||||
GitHubIssueAssigneeSchema,
|
||||
GitHubRepositorySchema,
|
||||
} from "../common/types.js";
|
||||
|
||||
// Schema definitions
|
||||
export const PullRequestFileSchema = z.object({
|
||||
sha: z.string(),
|
||||
filename: z.string(),
|
||||
status: z.enum(['added', 'removed', 'modified', 'renamed', 'copied', 'changed', 'unchanged']),
|
||||
additions: z.number(),
|
||||
deletions: z.number(),
|
||||
changes: z.number(),
|
||||
blob_url: z.string(),
|
||||
raw_url: z.string(),
|
||||
contents_url: z.string(),
|
||||
patch: z.string().optional()
|
||||
});
|
||||
|
||||
export const StatusCheckSchema = z.object({
|
||||
url: z.string(),
|
||||
state: z.enum(['error', 'failure', 'pending', 'success']),
|
||||
description: z.string().nullable(),
|
||||
target_url: z.string().nullable(),
|
||||
context: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string()
|
||||
});
|
||||
|
||||
export const CombinedStatusSchema = z.object({
|
||||
state: z.enum(['error', 'failure', 'pending', 'success']),
|
||||
statuses: z.array(StatusCheckSchema),
|
||||
sha: z.string(),
|
||||
total_count: z.number()
|
||||
});
|
||||
|
||||
export const PullRequestCommentSchema = z.object({
|
||||
url: z.string(),
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
pull_request_review_id: z.number().nullable(),
|
||||
diff_hunk: z.string(),
|
||||
path: z.string().nullable(),
|
||||
position: z.number().nullable(),
|
||||
original_position: z.number().nullable(),
|
||||
commit_id: z.string(),
|
||||
original_commit_id: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
body: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
html_url: z.string(),
|
||||
pull_request_url: z.string(),
|
||||
author_association: z.string(),
|
||||
_links: z.object({
|
||||
self: z.object({ href: z.string() }),
|
||||
html: z.object({ href: z.string() }),
|
||||
pull_request: z.object({ href: z.string() })
|
||||
})
|
||||
});
|
||||
|
||||
export const PullRequestReviewSchema = z.object({
|
||||
id: z.number(),
|
||||
node_id: z.string(),
|
||||
user: GitHubIssueAssigneeSchema,
|
||||
body: z.string().nullable(),
|
||||
state: z.enum(['APPROVED', 'CHANGES_REQUESTED', 'COMMENTED', 'DISMISSED', 'PENDING']),
|
||||
html_url: z.string(),
|
||||
pull_request_url: z.string(),
|
||||
commit_id: z.string(),
|
||||
submitted_at: z.string().nullable(),
|
||||
author_association: z.string()
|
||||
});
|
||||
|
||||
// Input schemas
|
||||
export const CreatePullRequestSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
title: z.string().describe("Pull request title"),
|
||||
body: z.string().optional().describe("Pull request body/description"),
|
||||
head: z.string().describe("The name of the branch where your changes are implemented"),
|
||||
base: z.string().describe("The name of the branch you want the changes pulled into"),
|
||||
draft: z.boolean().optional().describe("Whether to create the pull request as a draft"),
|
||||
maintainer_can_modify: z.boolean().optional().describe("Whether maintainers can modify the pull request")
|
||||
});
|
||||
|
||||
export const GetPullRequestSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
export const ListPullRequestsSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
state: z.enum(['open', 'closed', 'all']).optional().describe("State of the pull requests to return"),
|
||||
head: z.string().optional().describe("Filter by head user or head organization and branch name"),
|
||||
base: z.string().optional().describe("Filter by base branch name"),
|
||||
sort: z.enum(['created', 'updated', 'popularity', 'long-running']).optional().describe("What to sort results by"),
|
||||
direction: z.enum(['asc', 'desc']).optional().describe("The direction of the sort"),
|
||||
per_page: z.number().optional().describe("Results per page (max 100)"),
|
||||
page: z.number().optional().describe("Page number of the results")
|
||||
});
|
||||
|
||||
export const CreatePullRequestReviewSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number"),
|
||||
commit_id: z.string().optional().describe("The SHA of the commit that needs a review"),
|
||||
body: z.string().describe("The body text of the review"),
|
||||
event: z.enum(['APPROVE', 'REQUEST_CHANGES', 'COMMENT']).describe("The review action to perform"),
|
||||
comments: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
path: z.string().describe("The relative path to the file being commented on"),
|
||||
position: z.number().describe("The position in the diff where you want to add a review comment"),
|
||||
body: z.string().describe("Text of the review comment")
|
||||
}),
|
||||
z.object({
|
||||
path: z.string().describe("The relative path to the file being commented on"),
|
||||
line: z.number().describe("The line number in the file where you want to add a review comment"),
|
||||
body: z.string().describe("Text of the review comment")
|
||||
})
|
||||
])
|
||||
).optional().describe("Comments to post as part of the review (specify either position or line, not both)")
|
||||
});
|
||||
|
||||
export const MergePullRequestSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number"),
|
||||
commit_title: z.string().optional().describe("Title for the automatic commit message"),
|
||||
commit_message: z.string().optional().describe("Extra detail to append to automatic commit message"),
|
||||
merge_method: z.enum(['merge', 'squash', 'rebase']).optional().describe("Merge method to use")
|
||||
});
|
||||
|
||||
export const GetPullRequestFilesSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
export const GetPullRequestStatusSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
export const UpdatePullRequestBranchSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number"),
|
||||
expected_head_sha: z.string().optional().describe("The expected SHA of the pull request's HEAD ref")
|
||||
});
|
||||
|
||||
export const GetPullRequestCommentsSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
export const GetPullRequestReviewsSchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
pull_number: z.number().describe("Pull request number")
|
||||
});
|
||||
|
||||
// Function implementations
|
||||
export async function createPullRequest(
|
||||
params: z.infer<typeof CreatePullRequestSchema>
|
||||
): Promise<z.infer<typeof GitHubPullRequestSchema>> {
|
||||
const { owner, repo, ...options } = CreatePullRequestSchema.parse(params);
|
||||
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls`,
|
||||
{
|
||||
method: "POST",
|
||||
body: options,
|
||||
}
|
||||
);
|
||||
|
||||
return GitHubPullRequestSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function getPullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof GitHubPullRequestSchema>> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`
|
||||
);
|
||||
return GitHubPullRequestSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function listPullRequests(
|
||||
owner: string,
|
||||
repo: string,
|
||||
options: Omit<z.infer<typeof ListPullRequestsSchema>, 'owner' | 'repo'>
|
||||
): Promise<z.infer<typeof GitHubPullRequestSchema>[]> {
|
||||
const url = new URL(`https://api.github.com/repos/${owner}/${repo}/pulls`);
|
||||
|
||||
if (options.state) url.searchParams.append('state', options.state);
|
||||
if (options.head) url.searchParams.append('head', options.head);
|
||||
if (options.base) url.searchParams.append('base', options.base);
|
||||
if (options.sort) url.searchParams.append('sort', options.sort);
|
||||
if (options.direction) url.searchParams.append('direction', options.direction);
|
||||
if (options.per_page) url.searchParams.append('per_page', options.per_page.toString());
|
||||
if (options.page) url.searchParams.append('page', options.page.toString());
|
||||
|
||||
const response = await githubRequest(url.toString());
|
||||
return z.array(GitHubPullRequestSchema).parse(response);
|
||||
}
|
||||
|
||||
export async function createPullRequestReview(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number,
|
||||
options: Omit<z.infer<typeof CreatePullRequestReviewSchema>, 'owner' | 'repo' | 'pull_number'>
|
||||
): Promise<z.infer<typeof PullRequestReviewSchema>> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: options,
|
||||
}
|
||||
);
|
||||
return PullRequestReviewSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function mergePullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number,
|
||||
options: Omit<z.infer<typeof MergePullRequestSchema>, 'owner' | 'repo' | 'pull_number'>
|
||||
): Promise<any> {
|
||||
return githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/merge`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: options,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPullRequestFiles(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof PullRequestFileSchema>[]> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/files`
|
||||
);
|
||||
return z.array(PullRequestFileSchema).parse(response);
|
||||
}
|
||||
|
||||
export async function updatePullRequestBranch(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number,
|
||||
expectedHeadSha?: string
|
||||
): Promise<void> {
|
||||
await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/update-branch`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: expectedHeadSha ? { expected_head_sha: expectedHeadSha } : undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPullRequestComments(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof PullRequestCommentSchema>[]> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/comments`
|
||||
);
|
||||
return z.array(PullRequestCommentSchema).parse(response);
|
||||
}
|
||||
|
||||
export async function getPullRequestReviews(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof PullRequestReviewSchema>[]> {
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`
|
||||
);
|
||||
return z.array(PullRequestReviewSchema).parse(response);
|
||||
}
|
||||
|
||||
export async function getPullRequestStatus(
|
||||
owner: string,
|
||||
repo: string,
|
||||
pullNumber: number
|
||||
): Promise<z.infer<typeof CombinedStatusSchema>> {
|
||||
// First get the PR to get the head SHA
|
||||
const pr = await getPullRequest(owner, repo, pullNumber);
|
||||
const sha = pr.head.sha;
|
||||
|
||||
// Then get the combined status for that SHA
|
||||
const response = await githubRequest(
|
||||
`https://api.github.com/repos/${owner}/${repo}/commits/${sha}/status`
|
||||
);
|
||||
return CombinedStatusSchema.parse(response);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { githubRequest } from "../common/utils.js";
|
||||
import { GitHubRepositorySchema, GitHubSearchResponseSchema } from "../common/types.js";
|
||||
|
||||
// Schema definitions
|
||||
export const CreateRepositoryOptionsSchema = z.object({
|
||||
name: z.string().describe("Repository name"),
|
||||
description: z.string().optional().describe("Repository description"),
|
||||
private: z.boolean().optional().describe("Whether the repository should be private"),
|
||||
autoInit: z.boolean().optional().describe("Initialize with README.md"),
|
||||
});
|
||||
|
||||
export const SearchRepositoriesSchema = z.object({
|
||||
query: z.string().describe("Search query (see GitHub search syntax)"),
|
||||
page: z.number().optional().describe("Page number for pagination (default: 1)"),
|
||||
perPage: z.number().optional().describe("Number of results per page (default: 30, max: 100)"),
|
||||
});
|
||||
|
||||
export const ForkRepositorySchema = z.object({
|
||||
owner: z.string().describe("Repository owner (username or organization)"),
|
||||
repo: z.string().describe("Repository name"),
|
||||
organization: z.string().optional().describe("Optional: organization to fork to (defaults to your personal account)"),
|
||||
});
|
||||
|
||||
// Type exports
|
||||
export type CreateRepositoryOptions = z.infer<typeof CreateRepositoryOptionsSchema>;
|
||||
|
||||
// Function implementations
|
||||
export async function createRepository(options: CreateRepositoryOptions) {
|
||||
const response = await githubRequest("https://api.github.com/user/repos", {
|
||||
method: "POST",
|
||||
body: options,
|
||||
});
|
||||
return GitHubRepositorySchema.parse(response);
|
||||
}
|
||||
|
||||
export async function searchRepositories(
|
||||
query: string,
|
||||
page: number = 1,
|
||||
perPage: number = 30
|
||||
) {
|
||||
const url = new URL("https://api.github.com/search/repositories");
|
||||
url.searchParams.append("q", query);
|
||||
url.searchParams.append("page", page.toString());
|
||||
url.searchParams.append("per_page", perPage.toString());
|
||||
|
||||
const response = await githubRequest(url.toString());
|
||||
return GitHubSearchResponseSchema.parse(response);
|
||||
}
|
||||
|
||||
export async function forkRepository(
|
||||
owner: string,
|
||||
repo: string,
|
||||
organization?: string
|
||||
) {
|
||||
const url = organization
|
||||
? `https://api.github.com/repos/${owner}/${repo}/forks?organization=${organization}`
|
||||
: `https://api.github.com/repos/${owner}/${repo}/forks`;
|
||||
|
||||
const response = await githubRequest(url, { method: "POST" });
|
||||
return GitHubRepositorySchema.extend({
|
||||
parent: GitHubRepositorySchema,
|
||||
source: GitHubRepositorySchema,
|
||||
}).parse(response);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { githubRequest, buildUrl } from "../common/utils.js";
|
||||
|
||||
export const SearchOptions = z.object({
|
||||
q: z.string(),
|
||||
order: z.enum(["asc", "desc"]).optional(),
|
||||
page: z.number().min(1).optional(),
|
||||
per_page: z.number().min(1).max(100).optional(),
|
||||
});
|
||||
|
||||
export const SearchUsersOptions = SearchOptions.extend({
|
||||
sort: z.enum(["followers", "repositories", "joined"]).optional(),
|
||||
});
|
||||
|
||||
export const SearchIssuesOptions = SearchOptions.extend({
|
||||
sort: z.enum([
|
||||
"comments",
|
||||
"reactions",
|
||||
"reactions-+1",
|
||||
"reactions--1",
|
||||
"reactions-smile",
|
||||
"reactions-thinking_face",
|
||||
"reactions-heart",
|
||||
"reactions-tada",
|
||||
"interactions",
|
||||
"created",
|
||||
"updated",
|
||||
]).optional(),
|
||||
});
|
||||
|
||||
export const SearchCodeSchema = SearchOptions;
|
||||
export const SearchUsersSchema = SearchUsersOptions;
|
||||
export const SearchIssuesSchema = SearchIssuesOptions;
|
||||
|
||||
export async function searchCode(params: z.infer<typeof SearchCodeSchema>) {
|
||||
return githubRequest(buildUrl("https://api.github.com/search/code", params));
|
||||
}
|
||||
|
||||
export async function searchIssues(params: z.infer<typeof SearchIssuesSchema>) {
|
||||
return githubRequest(buildUrl("https://api.github.com/search/issues", params));
|
||||
}
|
||||
|
||||
export async function searchUsers(params: z.infer<typeof SearchUsersSchema>) {
|
||||
return githubRequest(buildUrl("https://api.github.com/search/users", params));
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-github",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for using the GitHub API",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-github": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1",
|
||||
"@types/node": "^22",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"node-fetch": "^3.3.2",
|
||||
"universal-user-agent": "^7.0.2",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
COPY src/gitlab /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
|
||||
|
||||
FROM node:22.12-alpine AS release
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,172 +0,0 @@
|
||||
# GitLab MCP Server
|
||||
|
||||
MCP Server for the GitLab API, enabling project management, file operations, and more.
|
||||
|
||||
### Features
|
||||
|
||||
- **Automatic Branch Creation**: When creating/updating files or pushing changes, branches are automatically created if they don't exist
|
||||
- **Comprehensive Error Handling**: Clear error messages for common issues
|
||||
- **Git History Preservation**: Operations maintain proper Git history without force pushing
|
||||
- **Batch Operations**: Support for both single-file and multi-file operations
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
1. `create_or_update_file`
|
||||
- Create or update a single file in a project
|
||||
- Inputs:
|
||||
- `project_id` (string): Project ID or URL-encoded path
|
||||
- `file_path` (string): Path where to create/update the file
|
||||
- `content` (string): Content of the file
|
||||
- `commit_message` (string): Commit message
|
||||
- `branch` (string): Branch to create/update the file in
|
||||
- `previous_path` (optional string): Path of the file to move/rename
|
||||
- Returns: File content and commit details
|
||||
|
||||
2. `push_files`
|
||||
- Push multiple files in a single commit
|
||||
- Inputs:
|
||||
- `project_id` (string): Project ID or URL-encoded path
|
||||
- `branch` (string): Branch to push to
|
||||
- `files` (array): Files to push, each with `file_path` and `content`
|
||||
- `commit_message` (string): Commit message
|
||||
- Returns: Updated branch reference
|
||||
|
||||
3. `search_repositories`
|
||||
- Search for GitLab projects
|
||||
- Inputs:
|
||||
- `search` (string): Search query
|
||||
- `page` (optional number): Page number for pagination
|
||||
- `per_page` (optional number): Results per page (default 20)
|
||||
- Returns: Project search results
|
||||
|
||||
4. `create_repository`
|
||||
- Create a new GitLab project
|
||||
- Inputs:
|
||||
- `name` (string): Project name
|
||||
- `description` (optional string): Project description
|
||||
- `visibility` (optional string): 'private', 'internal', or 'public'
|
||||
- `initialize_with_readme` (optional boolean): Initialize with README
|
||||
- Returns: Created project details
|
||||
|
||||
5. `get_file_contents`
|
||||
- Get contents of a file or directory
|
||||
- Inputs:
|
||||
- `project_id` (string): Project ID or URL-encoded path
|
||||
- `file_path` (string): Path to file/directory
|
||||
- `ref` (optional string): Branch/tag/commit to get contents from
|
||||
- Returns: File/directory contents
|
||||
|
||||
6. `create_issue`
|
||||
- Create a new issue
|
||||
- Inputs:
|
||||
- `project_id` (string): Project ID or URL-encoded path
|
||||
- `title` (string): Issue title
|
||||
- `description` (optional string): Issue description
|
||||
- `assignee_ids` (optional number[]): User IDs to assign
|
||||
- `labels` (optional string[]): Labels to add
|
||||
- `milestone_id` (optional number): Milestone ID
|
||||
- Returns: Created issue details
|
||||
|
||||
7. `create_merge_request`
|
||||
- Create a new merge request
|
||||
- Inputs:
|
||||
- `project_id` (string): Project ID or URL-encoded path
|
||||
- `title` (string): MR title
|
||||
- `description` (optional string): MR description
|
||||
- `source_branch` (string): Branch containing changes
|
||||
- `target_branch` (string): Branch to merge into
|
||||
- `draft` (optional boolean): Create as draft MR
|
||||
- `allow_collaboration` (optional boolean): Allow commits from upstream members
|
||||
- Returns: Created merge request details
|
||||
|
||||
8. `fork_repository`
|
||||
- Fork a project
|
||||
- Inputs:
|
||||
- `project_id` (string): Project ID or URL-encoded path
|
||||
- `namespace` (optional string): Namespace to fork to
|
||||
- Returns: Forked project details
|
||||
|
||||
9. `create_branch`
|
||||
- Create a new branch
|
||||
- Inputs:
|
||||
- `project_id` (string): Project ID or URL-encoded path
|
||||
- `branch` (string): Name for new branch
|
||||
- `ref` (optional string): Source branch/commit for new branch
|
||||
- Returns: Created branch reference
|
||||
|
||||
## Setup
|
||||
|
||||
### Personal Access Token
|
||||
[Create a GitLab Personal Access Token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html) with appropriate permissions:
|
||||
- Go to User Settings > Access Tokens in GitLab
|
||||
- Select the required scopes:
|
||||
- `api` for full API access
|
||||
- `read_api` for read-only access
|
||||
- `read_repository` and `write_repository` for repository operations
|
||||
- Create the token and save it securely
|
||||
|
||||
### Usage with Claude Desktop
|
||||
Add the following to your `claude_desktop_config.json`:
|
||||
|
||||
#### Docker
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitlab": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"-e",
|
||||
"GITLAB_PERSONAL_ACCESS_TOKEN",
|
||||
"-e",
|
||||
"GITLAB_API_URL",
|
||||
"mcp/gitlab"
|
||||
],
|
||||
"env": {
|
||||
"GITLAB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>",
|
||||
"GITLAB_API_URL": "https://gitlab.com/api/v4" // Optional, for self-hosted instances
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gitlab": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-gitlab"
|
||||
],
|
||||
"env": {
|
||||
"GITLAB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>",
|
||||
"GITLAB_API_URL": "https://gitlab.com/api/v4" // Optional, for self-hosted instances
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Docker build:
|
||||
|
||||
```bash
|
||||
docker build -t vonwig/gitlab:mcp -f src/gitlab/Dockerfile .
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `GITLAB_PERSONAL_ACCESS_TOKEN`: Your GitLab personal access token (required)
|
||||
- `GITLAB_API_URL`: Base URL for GitLab API (optional, defaults to `https://gitlab.com/api/v4`)
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,534 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fetch from "node-fetch";
|
||||
import { z } from 'zod';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import {
|
||||
GitLabForkSchema,
|
||||
GitLabReferenceSchema,
|
||||
GitLabRepositorySchema,
|
||||
GitLabIssueSchema,
|
||||
GitLabMergeRequestSchema,
|
||||
GitLabContentSchema,
|
||||
GitLabCreateUpdateFileResponseSchema,
|
||||
GitLabSearchResponseSchema,
|
||||
GitLabTreeSchema,
|
||||
GitLabCommitSchema,
|
||||
CreateRepositoryOptionsSchema,
|
||||
CreateIssueOptionsSchema,
|
||||
CreateMergeRequestOptionsSchema,
|
||||
CreateBranchOptionsSchema,
|
||||
CreateOrUpdateFileSchema,
|
||||
SearchRepositoriesSchema,
|
||||
CreateRepositorySchema,
|
||||
GetFileContentsSchema,
|
||||
PushFilesSchema,
|
||||
CreateIssueSchema,
|
||||
CreateMergeRequestSchema,
|
||||
ForkRepositorySchema,
|
||||
CreateBranchSchema,
|
||||
type GitLabFork,
|
||||
type GitLabReference,
|
||||
type GitLabRepository,
|
||||
type GitLabIssue,
|
||||
type GitLabMergeRequest,
|
||||
type GitLabContent,
|
||||
type GitLabCreateUpdateFileResponse,
|
||||
type GitLabSearchResponse,
|
||||
type GitLabTree,
|
||||
type GitLabCommit,
|
||||
type FileOperation,
|
||||
} from './schemas.js';
|
||||
|
||||
const server = new Server({
|
||||
name: "gitlab-mcp-server",
|
||||
version: "0.5.1",
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
});
|
||||
|
||||
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
||||
const GITLAB_API_URL = process.env.GITLAB_API_URL || 'https://gitlab.com/api/v4';
|
||||
|
||||
if (!GITLAB_PERSONAL_ACCESS_TOKEN) {
|
||||
console.error("GITLAB_PERSONAL_ACCESS_TOKEN environment variable is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function forkProject(
|
||||
projectId: string,
|
||||
namespace?: string
|
||||
): Promise<GitLabFork> {
|
||||
const url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/fork`;
|
||||
const queryParams = namespace ? `?namespace=${encodeURIComponent(namespace)}` : '';
|
||||
|
||||
const response = await fetch(url + queryParams, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return GitLabForkSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
async function createBranch(
|
||||
projectId: string,
|
||||
options: z.infer<typeof CreateBranchOptionsSchema>
|
||||
): Promise<GitLabReference> {
|
||||
const response = await fetch(
|
||||
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/branches`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
branch: options.name,
|
||||
ref: options.ref
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return GitLabReferenceSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
async function getDefaultBranchRef(projectId: string): Promise<string> {
|
||||
const response = await fetch(
|
||||
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}`,
|
||||
{
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const project = GitLabRepositorySchema.parse(await response.json());
|
||||
return project.default_branch;
|
||||
}
|
||||
|
||||
async function getFileContents(
|
||||
projectId: string,
|
||||
filePath: string,
|
||||
ref?: string
|
||||
): Promise<GitLabContent> {
|
||||
const encodedPath = encodeURIComponent(filePath);
|
||||
let url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`;
|
||||
if (ref) {
|
||||
url += `?ref=${encodeURIComponent(ref)}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = GitLabContentSchema.parse(await response.json());
|
||||
|
||||
if (!Array.isArray(data) && data.content) {
|
||||
data.content = Buffer.from(data.content, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function createIssue(
|
||||
projectId: string,
|
||||
options: z.infer<typeof CreateIssueOptionsSchema>
|
||||
): Promise<GitLabIssue> {
|
||||
const response = await fetch(
|
||||
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/issues`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
assignee_ids: options.assignee_ids,
|
||||
milestone_id: options.milestone_id,
|
||||
labels: options.labels?.join(',')
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return GitLabIssueSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
async function createMergeRequest(
|
||||
projectId: string,
|
||||
options: z.infer<typeof CreateMergeRequestOptionsSchema>
|
||||
): Promise<GitLabMergeRequest> {
|
||||
const response = await fetch(
|
||||
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/merge_requests`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
source_branch: options.source_branch,
|
||||
target_branch: options.target_branch,
|
||||
allow_collaboration: options.allow_collaboration,
|
||||
draft: options.draft
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return GitLabMergeRequestSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
async function createOrUpdateFile(
|
||||
projectId: string,
|
||||
filePath: string,
|
||||
content: string,
|
||||
commitMessage: string,
|
||||
branch: string,
|
||||
previousPath?: string
|
||||
): Promise<GitLabCreateUpdateFileResponse> {
|
||||
const encodedPath = encodeURIComponent(filePath);
|
||||
const url = `${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/files/${encodedPath}`;
|
||||
|
||||
const body = {
|
||||
branch,
|
||||
content,
|
||||
commit_message: commitMessage,
|
||||
...(previousPath ? { previous_path: previousPath } : {})
|
||||
};
|
||||
|
||||
// Check if file exists
|
||||
let method = "POST";
|
||||
try {
|
||||
await getFileContents(projectId, filePath, branch);
|
||||
method = "PUT";
|
||||
} catch (error) {
|
||||
// File doesn't exist, use POST
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return GitLabCreateUpdateFileResponseSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
async function createTree(
|
||||
projectId: string,
|
||||
files: FileOperation[],
|
||||
ref?: string
|
||||
): Promise<GitLabTree> {
|
||||
const response = await fetch(
|
||||
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/tree`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
files: files.map(file => ({
|
||||
file_path: file.path,
|
||||
content: file.content
|
||||
})),
|
||||
...(ref ? { ref } : {})
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return GitLabTreeSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
async function createCommit(
|
||||
projectId: string,
|
||||
message: string,
|
||||
branch: string,
|
||||
actions: FileOperation[]
|
||||
): Promise<GitLabCommit> {
|
||||
const response = await fetch(
|
||||
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/repository/commits`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
branch,
|
||||
commit_message: message,
|
||||
actions: actions.map(action => ({
|
||||
action: "create",
|
||||
file_path: action.path,
|
||||
content: action.content
|
||||
}))
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return GitLabCommitSchema.parse(await response.json());
|
||||
}
|
||||
|
||||
async function searchProjects(
|
||||
query: string,
|
||||
page: number = 1,
|
||||
perPage: number = 20
|
||||
): Promise<GitLabSearchResponse> {
|
||||
const url = new URL(`${GITLAB_API_URL}/projects`);
|
||||
url.searchParams.append("search", query);
|
||||
url.searchParams.append("page", page.toString());
|
||||
url.searchParams.append("per_page", perPage.toString());
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const projects = await response.json();
|
||||
return GitLabSearchResponseSchema.parse({
|
||||
count: parseInt(response.headers.get("X-Total") || "0"),
|
||||
items: projects
|
||||
});
|
||||
}
|
||||
|
||||
async function createRepository(
|
||||
options: z.infer<typeof CreateRepositoryOptionsSchema>
|
||||
): Promise<GitLabRepository> {
|
||||
const response = await fetch(`${GITLAB_API_URL}/projects`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Authorization": `Bearer ${GITLAB_PERSONAL_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: options.name,
|
||||
description: options.description,
|
||||
visibility: options.visibility,
|
||||
initialize_with_readme: options.initialize_with_readme
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitLab API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return GitLabRepositorySchema.parse(await response.json());
|
||||
}
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "create_or_update_file",
|
||||
description: "Create or update a single file in a GitLab project",
|
||||
inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema)
|
||||
},
|
||||
{
|
||||
name: "search_repositories",
|
||||
description: "Search for GitLab projects",
|
||||
inputSchema: zodToJsonSchema(SearchRepositoriesSchema)
|
||||
},
|
||||
{
|
||||
name: "create_repository",
|
||||
description: "Create a new GitLab project",
|
||||
inputSchema: zodToJsonSchema(CreateRepositorySchema)
|
||||
},
|
||||
{
|
||||
name: "get_file_contents",
|
||||
description: "Get the contents of a file or directory from a GitLab project",
|
||||
inputSchema: zodToJsonSchema(GetFileContentsSchema)
|
||||
},
|
||||
{
|
||||
name: "push_files",
|
||||
description: "Push multiple files to a GitLab project in a single commit",
|
||||
inputSchema: zodToJsonSchema(PushFilesSchema)
|
||||
},
|
||||
{
|
||||
name: "create_issue",
|
||||
description: "Create a new issue in a GitLab project",
|
||||
inputSchema: zodToJsonSchema(CreateIssueSchema)
|
||||
},
|
||||
{
|
||||
name: "create_merge_request",
|
||||
description: "Create a new merge request in a GitLab project",
|
||||
inputSchema: zodToJsonSchema(CreateMergeRequestSchema)
|
||||
},
|
||||
{
|
||||
name: "fork_repository",
|
||||
description: "Fork a GitLab project to your account or specified namespace",
|
||||
inputSchema: zodToJsonSchema(ForkRepositorySchema)
|
||||
},
|
||||
{
|
||||
name: "create_branch",
|
||||
description: "Create a new branch in a GitLab project",
|
||||
inputSchema: zodToJsonSchema(CreateBranchSchema)
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
if (!request.params.arguments) {
|
||||
throw new Error("Arguments are required");
|
||||
}
|
||||
|
||||
switch (request.params.name) {
|
||||
case "fork_repository": {
|
||||
const args = ForkRepositorySchema.parse(request.params.arguments);
|
||||
const fork = await forkProject(args.project_id, args.namespace);
|
||||
return { content: [{ type: "text", text: JSON.stringify(fork, null, 2) }] };
|
||||
}
|
||||
|
||||
case "create_branch": {
|
||||
const args = CreateBranchSchema.parse(request.params.arguments);
|
||||
let ref = args.ref;
|
||||
if (!ref) {
|
||||
ref = await getDefaultBranchRef(args.project_id);
|
||||
}
|
||||
|
||||
const branch = await createBranch(args.project_id, {
|
||||
name: args.branch,
|
||||
ref
|
||||
});
|
||||
|
||||
return { content: [{ type: "text", text: JSON.stringify(branch, null, 2) }] };
|
||||
}
|
||||
|
||||
case "search_repositories": {
|
||||
const args = SearchRepositoriesSchema.parse(request.params.arguments);
|
||||
const results = await searchProjects(args.search, args.page, args.per_page);
|
||||
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
|
||||
}
|
||||
|
||||
case "create_repository": {
|
||||
const args = CreateRepositorySchema.parse(request.params.arguments);
|
||||
const repository = await createRepository(args);
|
||||
return { content: [{ type: "text", text: JSON.stringify(repository, null, 2) }] };
|
||||
}
|
||||
|
||||
case "get_file_contents": {
|
||||
const args = GetFileContentsSchema.parse(request.params.arguments);
|
||||
const contents = await getFileContents(args.project_id, args.file_path, args.ref);
|
||||
return { content: [{ type: "text", text: JSON.stringify(contents, null, 2) }] };
|
||||
}
|
||||
|
||||
case "create_or_update_file": {
|
||||
const args = CreateOrUpdateFileSchema.parse(request.params.arguments);
|
||||
const result = await createOrUpdateFile(
|
||||
args.project_id,
|
||||
args.file_path,
|
||||
args.content,
|
||||
args.commit_message,
|
||||
args.branch,
|
||||
args.previous_path
|
||||
);
|
||||
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
|
||||
case "push_files": {
|
||||
const args = PushFilesSchema.parse(request.params.arguments);
|
||||
const result = await createCommit(
|
||||
args.project_id,
|
||||
args.commit_message,
|
||||
args.branch,
|
||||
args.files.map(f => ({ path: f.file_path, content: f.content }))
|
||||
);
|
||||
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
|
||||
case "create_issue": {
|
||||
const args = CreateIssueSchema.parse(request.params.arguments);
|
||||
const { project_id, ...options } = args;
|
||||
const issue = await createIssue(project_id, options);
|
||||
return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }] };
|
||||
}
|
||||
|
||||
case "create_merge_request": {
|
||||
const args = CreateMergeRequestSchema.parse(request.params.arguments);
|
||||
const { project_id, ...options } = args;
|
||||
const mergeRequest = await createMergeRequest(project_id, options);
|
||||
return { content: [{ type: "text", text: JSON.stringify(mergeRequest, null, 2) }] };
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("GitLab MCP Server running on stdio");
|
||||
}
|
||||
|
||||
runServer().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-gitlab",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for using the GitLab API",
|
||||
"license": "MIT",
|
||||
"author": "GitLab, PBC (https://gitlab.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-gitlab": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"node-fetch": "^3.3.2",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// Base schemas for common types
|
||||
export const GitLabAuthorSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
date: z.string()
|
||||
});
|
||||
|
||||
// Repository related schemas
|
||||
export const GitLabOwnerSchema = z.object({
|
||||
username: z.string(), // Changed from login to match GitLab API
|
||||
id: z.number(),
|
||||
avatar_url: z.string(),
|
||||
web_url: z.string(), // Changed from html_url to match GitLab API
|
||||
name: z.string(), // Added as GitLab includes full name
|
||||
state: z.string() // Added as GitLab includes user state
|
||||
});
|
||||
|
||||
export const GitLabRepositorySchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
path_with_namespace: z.string(), // Changed from full_name to match GitLab API
|
||||
visibility: z.string(), // Changed from private to match GitLab API
|
||||
owner: GitLabOwnerSchema.optional(),
|
||||
web_url: z.string(), // Changed from html_url to match GitLab API
|
||||
description: z.string().nullable(),
|
||||
fork: z.boolean().optional(),
|
||||
ssh_url_to_repo: z.string(), // Changed from ssh_url to match GitLab API
|
||||
http_url_to_repo: z.string(), // Changed from clone_url to match GitLab API
|
||||
created_at: z.string(),
|
||||
last_activity_at: z.string(), // Changed from updated_at to match GitLab API
|
||||
default_branch: z.string()
|
||||
});
|
||||
|
||||
// File content schemas
|
||||
export const GitLabFileContentSchema = z.object({
|
||||
file_name: z.string(), // Changed from name to match GitLab API
|
||||
file_path: z.string(), // Changed from path to match GitLab API
|
||||
size: z.number(),
|
||||
encoding: z.string(),
|
||||
content: z.string(),
|
||||
content_sha256: z.string(), // Changed from sha to match GitLab API
|
||||
ref: z.string(), // Added as GitLab requires branch reference
|
||||
blob_id: z.string(), // Added to match GitLab API
|
||||
last_commit_id: z.string() // Added to match GitLab API
|
||||
});
|
||||
|
||||
export const GitLabDirectoryContentSchema = z.object({
|
||||
name: z.string(),
|
||||
path: z.string(),
|
||||
type: z.string(),
|
||||
mode: z.string(),
|
||||
id: z.string(), // Changed from sha to match GitLab API
|
||||
web_url: z.string() // Changed from html_url to match GitLab API
|
||||
});
|
||||
|
||||
export const GitLabContentSchema = z.union([
|
||||
GitLabFileContentSchema,
|
||||
z.array(GitLabDirectoryContentSchema)
|
||||
]);
|
||||
|
||||
// Operation schemas
|
||||
export const FileOperationSchema = z.object({
|
||||
path: z.string(),
|
||||
content: z.string()
|
||||
});
|
||||
|
||||
// Tree and commit schemas
|
||||
export const GitLabTreeEntrySchema = z.object({
|
||||
id: z.string(), // Changed from sha to match GitLab API
|
||||
name: z.string(),
|
||||
type: z.enum(['blob', 'tree']),
|
||||
path: z.string(),
|
||||
mode: z.string()
|
||||
});
|
||||
|
||||
export const GitLabTreeSchema = z.object({
|
||||
id: z.string(), // Changed from sha to match GitLab API
|
||||
tree: z.array(GitLabTreeEntrySchema)
|
||||
});
|
||||
|
||||
export const GitLabCommitSchema = z.object({
|
||||
id: z.string(), // Changed from sha to match GitLab API
|
||||
short_id: z.string(), // Added to match GitLab API
|
||||
title: z.string(), // Changed from message to match GitLab API
|
||||
author_name: z.string(),
|
||||
author_email: z.string(),
|
||||
authored_date: z.string(),
|
||||
committer_name: z.string(),
|
||||
committer_email: z.string(),
|
||||
committed_date: z.string(),
|
||||
web_url: z.string(), // Changed from html_url to match GitLab API
|
||||
parent_ids: z.array(z.string()) // Changed from parents to match GitLab API
|
||||
});
|
||||
|
||||
// Reference schema
|
||||
export const GitLabReferenceSchema = z.object({
|
||||
name: z.string(), // Changed from ref to match GitLab API
|
||||
commit: z.object({
|
||||
id: z.string(), // Changed from sha to match GitLab API
|
||||
web_url: z.string() // Changed from url to match GitLab API
|
||||
})
|
||||
});
|
||||
|
||||
// Input schemas for operations
|
||||
export const CreateRepositoryOptionsSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
visibility: z.enum(['private', 'internal', 'public']).optional(), // Changed from private to match GitLab API
|
||||
initialize_with_readme: z.boolean().optional() // Changed from auto_init to match GitLab API
|
||||
});
|
||||
|
||||
export const CreateIssueOptionsSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(), // Changed from body to match GitLab API
|
||||
assignee_ids: z.array(z.number()).optional(), // Changed from assignees to match GitLab API
|
||||
milestone_id: z.number().optional(), // Changed from milestone to match GitLab API
|
||||
labels: z.array(z.string()).optional()
|
||||
});
|
||||
|
||||
export const CreateMergeRequestOptionsSchema = z.object({ // Changed from CreatePullRequestOptionsSchema
|
||||
title: z.string(),
|
||||
description: z.string().optional(), // Changed from body to match GitLab API
|
||||
source_branch: z.string(), // Changed from head to match GitLab API
|
||||
target_branch: z.string(), // Changed from base to match GitLab API
|
||||
allow_collaboration: z.boolean().optional(), // Changed from maintainer_can_modify to match GitLab API
|
||||
draft: z.boolean().optional()
|
||||
});
|
||||
|
||||
export const CreateBranchOptionsSchema = z.object({
|
||||
name: z.string(), // Changed from ref to match GitLab API
|
||||
ref: z.string() // The source branch/commit for the new branch
|
||||
});
|
||||
|
||||
// Response schemas for operations
|
||||
export const GitLabCreateUpdateFileResponseSchema = z.object({
|
||||
file_path: z.string(),
|
||||
branch: z.string(),
|
||||
commit_id: z.string(), // Changed from sha to match GitLab API
|
||||
content: GitLabFileContentSchema.optional()
|
||||
});
|
||||
|
||||
export const GitLabSearchResponseSchema = z.object({
|
||||
count: z.number(), // Changed from total_count to match GitLab API
|
||||
items: z.array(GitLabRepositorySchema)
|
||||
});
|
||||
|
||||
// Fork related schemas
|
||||
export const GitLabForkParentSchema = z.object({
|
||||
name: z.string(),
|
||||
path_with_namespace: z.string(), // Changed from full_name to match GitLab API
|
||||
owner: z.object({
|
||||
username: z.string(), // Changed from login to match GitLab API
|
||||
id: z.number(),
|
||||
avatar_url: z.string()
|
||||
}),
|
||||
web_url: z.string() // Changed from html_url to match GitLab API
|
||||
});
|
||||
|
||||
export const GitLabForkSchema = GitLabRepositorySchema.extend({
|
||||
forked_from_project: GitLabForkParentSchema // Changed from parent to match GitLab API
|
||||
});
|
||||
|
||||
// Issue related schemas
|
||||
export const GitLabLabelSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
description: z.string().optional()
|
||||
});
|
||||
|
||||
export const GitLabUserSchema = z.object({
|
||||
username: z.string(), // Changed from login to match GitLab API
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
avatar_url: z.string(),
|
||||
web_url: z.string() // Changed from html_url to match GitLab API
|
||||
});
|
||||
|
||||
export const GitLabMilestoneSchema = z.object({
|
||||
id: z.number(),
|
||||
iid: z.number(), // Added to match GitLab API
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
state: z.string(),
|
||||
web_url: z.string() // Changed from html_url to match GitLab API
|
||||
});
|
||||
|
||||
export const GitLabIssueSchema = z.object({
|
||||
id: z.number(),
|
||||
iid: z.number(), // Added to match GitLab API
|
||||
project_id: z.number(), // Added to match GitLab API
|
||||
title: z.string(),
|
||||
description: z.string(), // Changed from body to match GitLab API
|
||||
state: z.string(),
|
||||
author: GitLabUserSchema,
|
||||
assignees: z.array(GitLabUserSchema),
|
||||
labels: z.array(GitLabLabelSchema),
|
||||
milestone: GitLabMilestoneSchema.nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
closed_at: z.string().nullable(),
|
||||
web_url: z.string() // Changed from html_url to match GitLab API
|
||||
});
|
||||
|
||||
// Merge Request related schemas (equivalent to Pull Request)
|
||||
export const GitLabMergeRequestDiffRefSchema = z.object({
|
||||
base_sha: z.string(),
|
||||
head_sha: z.string(),
|
||||
start_sha: z.string()
|
||||
});
|
||||
|
||||
export const GitLabMergeRequestSchema = z.object({
|
||||
id: z.number(),
|
||||
iid: z.number(), // Added to match GitLab API
|
||||
project_id: z.number(), // Added to match GitLab API
|
||||
title: z.string(),
|
||||
description: z.string(), // Changed from body to match GitLab API
|
||||
state: z.string(),
|
||||
merged: z.boolean().optional(),
|
||||
author: GitLabUserSchema,
|
||||
assignees: z.array(GitLabUserSchema),
|
||||
source_branch: z.string(), // Changed from head to match GitLab API
|
||||
target_branch: z.string(), // Changed from base to match GitLab API
|
||||
diff_refs: GitLabMergeRequestDiffRefSchema.nullable(),
|
||||
web_url: z.string(), // Changed from html_url to match GitLab API
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
merged_at: z.string().nullable(),
|
||||
closed_at: z.string().nullable(),
|
||||
merge_commit_sha: z.string().nullable()
|
||||
});
|
||||
|
||||
// API Operation Parameter Schemas
|
||||
const ProjectParamsSchema = z.object({
|
||||
project_id: z.string().describe("Project ID or URL-encoded path") // Changed from owner/repo to match GitLab API
|
||||
});
|
||||
|
||||
export const CreateOrUpdateFileSchema = ProjectParamsSchema.extend({
|
||||
file_path: z.string().describe("Path where to create/update the file"),
|
||||
content: z.string().describe("Content of the file"),
|
||||
commit_message: z.string().describe("Commit message"),
|
||||
branch: z.string().describe("Branch to create/update the file in"),
|
||||
previous_path: z.string().optional()
|
||||
.describe("Path of the file to move/rename")
|
||||
});
|
||||
|
||||
export const SearchRepositoriesSchema = z.object({
|
||||
search: z.string().describe("Search query"), // Changed from query to match GitLab API
|
||||
page: z.number().optional().describe("Page number for pagination (default: 1)"),
|
||||
per_page: z.number().optional().describe("Number of results per page (default: 20)")
|
||||
});
|
||||
|
||||
export const CreateRepositorySchema = z.object({
|
||||
name: z.string().describe("Repository name"),
|
||||
description: z.string().optional().describe("Repository description"),
|
||||
visibility: z.enum(['private', 'internal', 'public']).optional()
|
||||
.describe("Repository visibility level"),
|
||||
initialize_with_readme: z.boolean().optional()
|
||||
.describe("Initialize with README.md")
|
||||
});
|
||||
|
||||
export const GetFileContentsSchema = ProjectParamsSchema.extend({
|
||||
file_path: z.string().describe("Path to the file or directory"),
|
||||
ref: z.string().optional().describe("Branch/tag/commit to get contents from")
|
||||
});
|
||||
|
||||
export const PushFilesSchema = ProjectParamsSchema.extend({
|
||||
branch: z.string().describe("Branch to push to"),
|
||||
files: z.array(z.object({
|
||||
file_path: z.string().describe("Path where to create the file"),
|
||||
content: z.string().describe("Content of the file")
|
||||
})).describe("Array of files to push"),
|
||||
commit_message: z.string().describe("Commit message")
|
||||
});
|
||||
|
||||
export const CreateIssueSchema = ProjectParamsSchema.extend({
|
||||
title: z.string().describe("Issue title"),
|
||||
description: z.string().optional().describe("Issue description"),
|
||||
assignee_ids: z.array(z.number()).optional().describe("Array of user IDs to assign"),
|
||||
labels: z.array(z.string()).optional().describe("Array of label names"),
|
||||
milestone_id: z.number().optional().describe("Milestone ID to assign")
|
||||
});
|
||||
|
||||
export const CreateMergeRequestSchema = ProjectParamsSchema.extend({
|
||||
title: z.string().describe("Merge request title"),
|
||||
description: z.string().optional().describe("Merge request description"),
|
||||
source_branch: z.string().describe("Branch containing changes"),
|
||||
target_branch: z.string().describe("Branch to merge into"),
|
||||
draft: z.boolean().optional().describe("Create as draft merge request"),
|
||||
allow_collaboration: z.boolean().optional()
|
||||
.describe("Allow commits from upstream members")
|
||||
});
|
||||
|
||||
export const ForkRepositorySchema = ProjectParamsSchema.extend({
|
||||
namespace: z.string().optional()
|
||||
.describe("Namespace to fork to (full path)")
|
||||
});
|
||||
|
||||
export const CreateBranchSchema = ProjectParamsSchema.extend({
|
||||
branch: z.string().describe("Name for the new branch"),
|
||||
ref: z.string().optional()
|
||||
.describe("Source branch/commit for new branch")
|
||||
});
|
||||
|
||||
// Export types
|
||||
export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
|
||||
export type GitLabFork = z.infer<typeof GitLabForkSchema>;
|
||||
export type GitLabIssue = z.infer<typeof GitLabIssueSchema>;
|
||||
export type GitLabMergeRequest = z.infer<typeof GitLabMergeRequestSchema>;
|
||||
export type GitLabRepository = z.infer<typeof GitLabRepositorySchema>;
|
||||
export type GitLabFileContent = z.infer<typeof GitLabFileContentSchema>;
|
||||
export type GitLabDirectoryContent = z.infer<typeof GitLabDirectoryContentSchema>;
|
||||
export type GitLabContent = z.infer<typeof GitLabContentSchema>;
|
||||
export type FileOperation = z.infer<typeof FileOperationSchema>;
|
||||
export type GitLabTree = z.infer<typeof GitLabTreeSchema>;
|
||||
export type GitLabCommit = z.infer<typeof GitLabCommitSchema>;
|
||||
export type GitLabReference = z.infer<typeof GitLabReferenceSchema>;
|
||||
export type CreateRepositoryOptions = z.infer<typeof CreateRepositoryOptionsSchema>;
|
||||
export type CreateIssueOptions = z.infer<typeof CreateIssueOptionsSchema>;
|
||||
export type CreateMergeRequestOptions = z.infer<typeof CreateMergeRequestOptionsSchema>;
|
||||
export type CreateBranchOptions = z.infer<typeof CreateBranchOptionsSchema>;
|
||||
export type GitLabCreateUpdateFileResponse = z.infer<typeof GitLabCreateUpdateFileResponseSchema>;
|
||||
export type GitLabSearchResponse = z.infer<typeof GitLabSearchResponseSchema>;
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
# Must be entire project because `prepare` script is run during `npm install` and requires all files.
|
||||
COPY src/google-maps /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
|
||||
|
||||
FROM node:22-alpine AS release
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,114 +0,0 @@
|
||||
# Google Maps MCP Server
|
||||
|
||||
MCP Server for the Google Maps API.
|
||||
|
||||
## Tools
|
||||
|
||||
1. `maps_geocode`
|
||||
- Convert address to coordinates
|
||||
- Input: `address` (string)
|
||||
- Returns: location, formatted_address, place_id
|
||||
|
||||
2. `maps_reverse_geocode`
|
||||
- Convert coordinates to address
|
||||
- Inputs:
|
||||
- `latitude` (number)
|
||||
- `longitude` (number)
|
||||
- Returns: formatted_address, place_id, address_components
|
||||
|
||||
3. `maps_search_places`
|
||||
- Search for places using text query
|
||||
- Inputs:
|
||||
- `query` (string)
|
||||
- `location` (optional): { latitude: number, longitude: number }
|
||||
- `radius` (optional): number (meters, max 50000)
|
||||
- Returns: array of places with names, addresses, locations
|
||||
|
||||
4. `maps_place_details`
|
||||
- Get detailed information about a place
|
||||
- Input: `place_id` (string)
|
||||
- Returns: name, address, contact info, ratings, reviews, opening hours
|
||||
|
||||
5. `maps_distance_matrix`
|
||||
- Calculate distances and times between points
|
||||
- Inputs:
|
||||
- `origins` (string[])
|
||||
- `destinations` (string[])
|
||||
- `mode` (optional): "driving" | "walking" | "bicycling" | "transit"
|
||||
- Returns: distances and durations matrix
|
||||
|
||||
6. `maps_elevation`
|
||||
- Get elevation data for locations
|
||||
- Input: `locations` (array of {latitude, longitude})
|
||||
- Returns: elevation data for each point
|
||||
|
||||
7. `maps_directions`
|
||||
- Get directions between points
|
||||
- Inputs:
|
||||
- `origin` (string)
|
||||
- `destination` (string)
|
||||
- `mode` (optional): "driving" | "walking" | "bicycling" | "transit"
|
||||
- Returns: route details with steps, distance, duration
|
||||
|
||||
## Setup
|
||||
|
||||
### API Key
|
||||
Get a Google Maps API key by following the instructions [here](https://developers.google.com/maps/documentation/javascript/get-api-key#create-api-keys).
|
||||
|
||||
### Usage with Claude Desktop
|
||||
|
||||
Add the following to your `claude_desktop_config.json`:
|
||||
|
||||
#### Docker
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"google-maps": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GOOGLE_MAPS_API_KEY",
|
||||
"mcp/google-maps"
|
||||
],
|
||||
"env": {
|
||||
"GOOGLE_MAPS_API_KEY": "<YOUR_API_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"google-maps": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-google-maps"
|
||||
],
|
||||
"env": {
|
||||
"GOOGLE_MAPS_API_KEY": "<YOUR_API_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Docker build:
|
||||
|
||||
```bash
|
||||
docker build -t mcp/google-maps -f src/google-maps/Dockerfile .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,678 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
// Response interfaces
|
||||
interface GoogleMapsResponse {
|
||||
status: string;
|
||||
error_message?: string;
|
||||
}
|
||||
|
||||
interface GeocodeResponse extends GoogleMapsResponse {
|
||||
results: Array<{
|
||||
place_id: string;
|
||||
formatted_address: string;
|
||||
geometry: {
|
||||
location: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
};
|
||||
address_components: Array<{
|
||||
long_name: string;
|
||||
short_name: string;
|
||||
types: string[];
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface PlacesSearchResponse extends GoogleMapsResponse {
|
||||
results: Array<{
|
||||
name: string;
|
||||
place_id: string;
|
||||
formatted_address: string;
|
||||
geometry: {
|
||||
location: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
};
|
||||
rating?: number;
|
||||
types: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
interface PlaceDetailsResponse extends GoogleMapsResponse {
|
||||
result: {
|
||||
name: string;
|
||||
place_id: string;
|
||||
formatted_address: string;
|
||||
formatted_phone_number?: string;
|
||||
website?: string;
|
||||
rating?: number;
|
||||
reviews?: Array<{
|
||||
author_name: string;
|
||||
rating: number;
|
||||
text: string;
|
||||
time: number;
|
||||
}>;
|
||||
opening_hours?: {
|
||||
weekday_text: string[];
|
||||
open_now: boolean;
|
||||
};
|
||||
geometry: {
|
||||
location: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface DistanceMatrixResponse extends GoogleMapsResponse {
|
||||
origin_addresses: string[];
|
||||
destination_addresses: string[];
|
||||
rows: Array<{
|
||||
elements: Array<{
|
||||
status: string;
|
||||
duration: {
|
||||
text: string;
|
||||
value: number;
|
||||
};
|
||||
distance: {
|
||||
text: string;
|
||||
value: number;
|
||||
};
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ElevationResponse extends GoogleMapsResponse {
|
||||
results: Array<{
|
||||
elevation: number;
|
||||
location: {
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
resolution: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface DirectionsResponse extends GoogleMapsResponse {
|
||||
routes: Array<{
|
||||
summary: string;
|
||||
legs: Array<{
|
||||
distance: {
|
||||
text: string;
|
||||
value: number;
|
||||
};
|
||||
duration: {
|
||||
text: string;
|
||||
value: number;
|
||||
};
|
||||
steps: Array<{
|
||||
html_instructions: string;
|
||||
distance: {
|
||||
text: string;
|
||||
value: number;
|
||||
};
|
||||
duration: {
|
||||
text: string;
|
||||
value: number;
|
||||
};
|
||||
travel_mode: string;
|
||||
}>;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
function getApiKey(): string {
|
||||
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error("GOOGLE_MAPS_API_KEY environment variable is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
const GOOGLE_MAPS_API_KEY = getApiKey();
|
||||
|
||||
// Tool definitions
|
||||
const GEOCODE_TOOL: Tool = {
|
||||
name: "maps_geocode",
|
||||
description: "Convert an address into geographic coordinates",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
address: {
|
||||
type: "string",
|
||||
description: "The address to geocode"
|
||||
}
|
||||
},
|
||||
required: ["address"]
|
||||
}
|
||||
};
|
||||
|
||||
const REVERSE_GEOCODE_TOOL: Tool = {
|
||||
name: "maps_reverse_geocode",
|
||||
description: "Convert coordinates into an address",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
latitude: {
|
||||
type: "number",
|
||||
description: "Latitude coordinate"
|
||||
},
|
||||
longitude: {
|
||||
type: "number",
|
||||
description: "Longitude coordinate"
|
||||
}
|
||||
},
|
||||
required: ["latitude", "longitude"]
|
||||
}
|
||||
};
|
||||
|
||||
const SEARCH_PLACES_TOOL: Tool = {
|
||||
name: "maps_search_places",
|
||||
description: "Search for places using Google Places API",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query"
|
||||
},
|
||||
location: {
|
||||
type: "object",
|
||||
properties: {
|
||||
latitude: { type: "number" },
|
||||
longitude: { type: "number" }
|
||||
},
|
||||
description: "Optional center point for the search"
|
||||
},
|
||||
radius: {
|
||||
type: "number",
|
||||
description: "Search radius in meters (max 50000)"
|
||||
}
|
||||
},
|
||||
required: ["query"]
|
||||
}
|
||||
};
|
||||
|
||||
const PLACE_DETAILS_TOOL: Tool = {
|
||||
name: "maps_place_details",
|
||||
description: "Get detailed information about a specific place",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
place_id: {
|
||||
type: "string",
|
||||
description: "The place ID to get details for"
|
||||
}
|
||||
},
|
||||
required: ["place_id"]
|
||||
}
|
||||
};
|
||||
|
||||
const DISTANCE_MATRIX_TOOL: Tool = {
|
||||
name: "maps_distance_matrix",
|
||||
description: "Calculate travel distance and time for multiple origins and destinations",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
origins: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Array of origin addresses or coordinates"
|
||||
},
|
||||
destinations: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Array of destination addresses or coordinates"
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
description: "Travel mode (driving, walking, bicycling, transit)",
|
||||
enum: ["driving", "walking", "bicycling", "transit"]
|
||||
}
|
||||
},
|
||||
required: ["origins", "destinations"]
|
||||
}
|
||||
};
|
||||
|
||||
const ELEVATION_TOOL: Tool = {
|
||||
name: "maps_elevation",
|
||||
description: "Get elevation data for locations on the earth",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
locations: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
latitude: { type: "number" },
|
||||
longitude: { type: "number" }
|
||||
},
|
||||
required: ["latitude", "longitude"]
|
||||
},
|
||||
description: "Array of locations to get elevation for"
|
||||
}
|
||||
},
|
||||
required: ["locations"]
|
||||
}
|
||||
};
|
||||
|
||||
const DIRECTIONS_TOOL: Tool = {
|
||||
name: "maps_directions",
|
||||
description: "Get directions between two points",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
origin: {
|
||||
type: "string",
|
||||
description: "Starting point address or coordinates"
|
||||
},
|
||||
destination: {
|
||||
type: "string",
|
||||
description: "Ending point address or coordinates"
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
description: "Travel mode (driving, walking, bicycling, transit)",
|
||||
enum: ["driving", "walking", "bicycling", "transit"]
|
||||
}
|
||||
},
|
||||
required: ["origin", "destination"]
|
||||
}
|
||||
};
|
||||
|
||||
const MAPS_TOOLS = [
|
||||
GEOCODE_TOOL,
|
||||
REVERSE_GEOCODE_TOOL,
|
||||
SEARCH_PLACES_TOOL,
|
||||
PLACE_DETAILS_TOOL,
|
||||
DISTANCE_MATRIX_TOOL,
|
||||
ELEVATION_TOOL,
|
||||
DIRECTIONS_TOOL,
|
||||
] as const;
|
||||
|
||||
// API handlers
|
||||
async function handleGeocode(address: string) {
|
||||
const url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
|
||||
url.searchParams.append("address", address);
|
||||
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const data = await response.json() as GeocodeResponse;
|
||||
|
||||
if (data.status !== "OK") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Geocoding failed: ${data.error_message || data.status}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
location: data.results[0].geometry.location,
|
||||
formatted_address: data.results[0].formatted_address,
|
||||
place_id: data.results[0].place_id
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: false
|
||||
};
|
||||
}
|
||||
|
||||
async function handleReverseGeocode(latitude: number, longitude: number) {
|
||||
const url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
|
||||
url.searchParams.append("latlng", `${latitude},${longitude}`);
|
||||
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const data = await response.json() as GeocodeResponse;
|
||||
|
||||
if (data.status !== "OK") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Reverse geocoding failed: ${data.error_message || data.status}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
formatted_address: data.results[0].formatted_address,
|
||||
place_id: data.results[0].place_id,
|
||||
address_components: data.results[0].address_components
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: false
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePlaceSearch(
|
||||
query: string,
|
||||
location?: { latitude: number; longitude: number },
|
||||
radius?: number
|
||||
) {
|
||||
const url = new URL("https://maps.googleapis.com/maps/api/place/textsearch/json");
|
||||
url.searchParams.append("query", query);
|
||||
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||
|
||||
if (location) {
|
||||
url.searchParams.append("location", `${location.latitude},${location.longitude}`);
|
||||
}
|
||||
if (radius) {
|
||||
url.searchParams.append("radius", radius.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const data = await response.json() as PlacesSearchResponse;
|
||||
|
||||
if (data.status !== "OK") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Place search failed: ${data.error_message || data.status}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
places: data.results.map((place) => ({
|
||||
name: place.name,
|
||||
formatted_address: place.formatted_address,
|
||||
location: place.geometry.location,
|
||||
place_id: place.place_id,
|
||||
rating: place.rating,
|
||||
types: place.types
|
||||
}))
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: false
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePlaceDetails(place_id: string) {
|
||||
const url = new URL("https://maps.googleapis.com/maps/api/place/details/json");
|
||||
url.searchParams.append("place_id", place_id);
|
||||
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const data = await response.json() as PlaceDetailsResponse;
|
||||
|
||||
if (data.status !== "OK") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Place details request failed: ${data.error_message || data.status}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
name: data.result.name,
|
||||
formatted_address: data.result.formatted_address,
|
||||
location: data.result.geometry.location,
|
||||
formatted_phone_number: data.result.formatted_phone_number,
|
||||
website: data.result.website,
|
||||
rating: data.result.rating,
|
||||
reviews: data.result.reviews,
|
||||
opening_hours: data.result.opening_hours
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: false
|
||||
};
|
||||
}
|
||||
async function handleDistanceMatrix(
|
||||
origins: string[],
|
||||
destinations: string[],
|
||||
mode: "driving" | "walking" | "bicycling" | "transit" = "driving"
|
||||
) {
|
||||
const url = new URL("https://maps.googleapis.com/maps/api/distancematrix/json");
|
||||
url.searchParams.append("origins", origins.join("|"));
|
||||
url.searchParams.append("destinations", destinations.join("|"));
|
||||
url.searchParams.append("mode", mode);
|
||||
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const data = await response.json() as DistanceMatrixResponse;
|
||||
|
||||
if (data.status !== "OK") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Distance matrix request failed: ${data.error_message || data.status}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
origin_addresses: data.origin_addresses,
|
||||
destination_addresses: data.destination_addresses,
|
||||
results: data.rows.map((row) => ({
|
||||
elements: row.elements.map((element) => ({
|
||||
status: element.status,
|
||||
duration: element.duration,
|
||||
distance: element.distance
|
||||
}))
|
||||
}))
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: false
|
||||
};
|
||||
}
|
||||
|
||||
async function handleElevation(locations: Array<{ latitude: number; longitude: number }>) {
|
||||
const url = new URL("https://maps.googleapis.com/maps/api/elevation/json");
|
||||
const locationString = locations
|
||||
.map((loc) => `${loc.latitude},${loc.longitude}`)
|
||||
.join("|");
|
||||
url.searchParams.append("locations", locationString);
|
||||
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const data = await response.json() as ElevationResponse;
|
||||
|
||||
if (data.status !== "OK") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Elevation request failed: ${data.error_message || data.status}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
results: data.results.map((result) => ({
|
||||
elevation: result.elevation,
|
||||
location: result.location,
|
||||
resolution: result.resolution
|
||||
}))
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: false
|
||||
};
|
||||
}
|
||||
|
||||
async function handleDirections(
|
||||
origin: string,
|
||||
destination: string,
|
||||
mode: "driving" | "walking" | "bicycling" | "transit" = "driving"
|
||||
) {
|
||||
const url = new URL("https://maps.googleapis.com/maps/api/directions/json");
|
||||
url.searchParams.append("origin", origin);
|
||||
url.searchParams.append("destination", destination);
|
||||
url.searchParams.append("mode", mode);
|
||||
url.searchParams.append("key", GOOGLE_MAPS_API_KEY);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const data = await response.json() as DirectionsResponse;
|
||||
|
||||
if (data.status !== "OK") {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Directions request failed: ${data.error_message || data.status}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
routes: data.routes.map((route) => ({
|
||||
summary: route.summary,
|
||||
distance: route.legs[0].distance,
|
||||
duration: route.legs[0].duration,
|
||||
steps: route.legs[0].steps.map((step) => ({
|
||||
instructions: step.html_instructions,
|
||||
distance: step.distance,
|
||||
duration: step.duration,
|
||||
travel_mode: step.travel_mode
|
||||
}))
|
||||
}))
|
||||
}, null, 2)
|
||||
}],
|
||||
isError: false
|
||||
};
|
||||
}
|
||||
|
||||
// Server setup
|
||||
const server = new Server(
|
||||
{
|
||||
name: "mcp-server/google-maps",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Set up request handlers
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: MAPS_TOOLS,
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
switch (request.params.name) {
|
||||
case "maps_geocode": {
|
||||
const { address } = request.params.arguments as { address: string };
|
||||
return await handleGeocode(address);
|
||||
}
|
||||
|
||||
case "maps_reverse_geocode": {
|
||||
const { latitude, longitude } = request.params.arguments as {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
return await handleReverseGeocode(latitude, longitude);
|
||||
}
|
||||
|
||||
case "maps_search_places": {
|
||||
const { query, location, radius } = request.params.arguments as {
|
||||
query: string;
|
||||
location?: { latitude: number; longitude: number };
|
||||
radius?: number;
|
||||
};
|
||||
return await handlePlaceSearch(query, location, radius);
|
||||
}
|
||||
|
||||
case "maps_place_details": {
|
||||
const { place_id } = request.params.arguments as { place_id: string };
|
||||
return await handlePlaceDetails(place_id);
|
||||
}
|
||||
|
||||
case "maps_distance_matrix": {
|
||||
const { origins, destinations, mode } = request.params.arguments as {
|
||||
origins: string[];
|
||||
destinations: string[];
|
||||
mode?: "driving" | "walking" | "bicycling" | "transit";
|
||||
};
|
||||
return await handleDistanceMatrix(origins, destinations, mode);
|
||||
}
|
||||
|
||||
case "maps_elevation": {
|
||||
const { locations } = request.params.arguments as {
|
||||
locations: Array<{ latitude: number; longitude: number }>;
|
||||
};
|
||||
return await handleElevation(locations);
|
||||
}
|
||||
|
||||
case "maps_directions": {
|
||||
const { origin, destination, mode } = request.params.arguments as {
|
||||
origin: string;
|
||||
destination: string;
|
||||
mode?: "driving" | "walking" | "bicycling" | "transit";
|
||||
};
|
||||
return await handleDirections(origin, destination, mode);
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Unknown tool: ${request.params.name}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
||||
}],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Google Maps MCP Server running on stdio");
|
||||
}
|
||||
|
||||
runServer().catch((error) => {
|
||||
console.error("Fatal error running server:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-google-maps",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for using the Google Maps API",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-google-maps": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
# Knowledge Graph Memory Server
|
||||
|
||||
A basic implementation of persistent memory using a local knowledge graph. This lets Claude remember information about the user across chats.
|
||||
|
||||
## Core Concepts
|
||||
@@ -181,6 +182,60 @@ The server can be configured using the following environment variables:
|
||||
|
||||
- `MEMORY_FILE_PATH`: Path to the memory storage JSON file (default: `memory.json` in the server directory)
|
||||
|
||||
# VS Code Installation Instructions
|
||||
|
||||
For quick installation, use one of the one-click installation buttons below:
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=memory&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-memory%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=memory&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-memory%22%5D%7D&quality=insiders)
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=memory&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22-v%22%2C%22claude-memory%3A%2Fapp%2Fdist%22%2C%22--rm%22%2C%22mcp%2Fmemory%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=memory&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22-v%22%2C%22claude-memory%3A%2Fapp%2Fdist%22%2C%22--rm%22%2C%22mcp%2Fmemory%22%5D%7D&quality=insiders)
|
||||
|
||||
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open Settings (JSON)`.
|
||||
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
||||
|
||||
#### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"memory": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-memory"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Docker
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"memory": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"-v",
|
||||
"claude-memory:/app/dist",
|
||||
"--rm",
|
||||
"mcp/memory"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### System Prompt
|
||||
|
||||
The prompt for utilizing memory depends on the use case. Changing the prompt will help the model determine the frequency and types of memories created.
|
||||
|
||||
@@ -189,7 +189,7 @@ const knowledgeGraphManager = new KnowledgeGraphManager();
|
||||
// The server instance and tools exposed to Claude
|
||||
const server = new Server({
|
||||
name: "memory-server",
|
||||
version: "1.0.0",
|
||||
version: "0.6.3",
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
@@ -416,4 +416,4 @@ async function main() {
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
COPY src/postgres /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
|
||||
|
||||
FROM node:22-alpine AS release
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,77 +0,0 @@
|
||||
# PostgreSQL
|
||||
|
||||
A Model Context Protocol server that provides read-only access to PostgreSQL databases. This server enables LLMs to inspect database schemas and execute read-only queries.
|
||||
|
||||
## Components
|
||||
|
||||
### Tools
|
||||
|
||||
- **query**
|
||||
- Execute read-only SQL queries against the connected database
|
||||
- Input: `sql` (string): The SQL query to execute
|
||||
- All queries are executed within a READ ONLY transaction
|
||||
|
||||
### Resources
|
||||
|
||||
The server provides schema information for each table in the database:
|
||||
|
||||
- **Table Schemas** (`postgres://<host>/<table>/schema`)
|
||||
- JSON schema information for each table
|
||||
- Includes column names and data types
|
||||
- Automatically discovered from database metadata
|
||||
|
||||
## Usage with Claude Desktop
|
||||
|
||||
To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
|
||||
|
||||
### Docker
|
||||
|
||||
* when running docker on macos, use host.docker.internal if the server is running on the host network (eg localhost)
|
||||
* username/password can be added to the postgresql url with `postgresql://user:password@host:port/db-name`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"mcp/postgres",
|
||||
"postgresql://host.docker.internal:5432/mydb"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-postgres",
|
||||
"postgresql://localhost/mydb"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `/mydb` with your database name.
|
||||
|
||||
## Building
|
||||
|
||||
Docker:
|
||||
|
||||
```sh
|
||||
docker build -t mcp/postgres -f src/postgres/Dockerfile .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import pg from "pg";
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "example-servers/postgres",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 0) {
|
||||
console.error("Please provide a database URL as a command-line argument");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const databaseUrl = args[0];
|
||||
|
||||
const resourceBaseUrl = new URL(databaseUrl);
|
||||
resourceBaseUrl.protocol = "postgres:";
|
||||
resourceBaseUrl.password = "";
|
||||
|
||||
const pool = new pg.Pool({
|
||||
connectionString: databaseUrl,
|
||||
});
|
||||
|
||||
const SCHEMA_PATH = "schema";
|
||||
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(
|
||||
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'",
|
||||
);
|
||||
return {
|
||||
resources: result.rows.map((row) => ({
|
||||
uri: new URL(`${row.table_name}/${SCHEMA_PATH}`, resourceBaseUrl).href,
|
||||
mimeType: "application/json",
|
||||
name: `"${row.table_name}" database schema`,
|
||||
})),
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const resourceUrl = new URL(request.params.uri);
|
||||
|
||||
const pathComponents = resourceUrl.pathname.split("/");
|
||||
const schema = pathComponents.pop();
|
||||
const tableName = pathComponents.pop();
|
||||
|
||||
if (schema !== SCHEMA_PATH) {
|
||||
throw new Error("Invalid resource URI");
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(
|
||||
"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1",
|
||||
[tableName],
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify(result.rows, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "query",
|
||||
description: "Run a read-only SQL query",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
sql: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "query") {
|
||||
const sql = request.params.arguments?.sql as string;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN TRANSACTION READ ONLY");
|
||||
const result = await client.query(sql);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
client
|
||||
.query("ROLLBACK")
|
||||
.catch((error) =>
|
||||
console.warn("Could not roll back transaction:", error),
|
||||
);
|
||||
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
runServer().catch(console.error);
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-postgres",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for interacting with PostgreSQL databases",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-postgres": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1",
|
||||
"pg": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.11.10",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# for arm64 support we need to install chromium provided by debian
|
||||
# npm ERR! The chromium binary is not available for arm64.
|
||||
# https://github.com/puppeteer/puppeteer/issues/7740
|
||||
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y wget gnupg && \
|
||||
apt-get install -y fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
|
||||
libgtk2.0-0 libnss3 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libgbm1 libasound2 && \
|
||||
apt-get install -y chromium && \
|
||||
apt-get clean
|
||||
|
||||
COPY src/puppeteer /project
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /project
|
||||
|
||||
RUN npm install
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,143 +0,0 @@
|
||||
# Puppeteer
|
||||
|
||||
A Model Context Protocol server that provides browser automation capabilities using Puppeteer. This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment.
|
||||
|
||||
## Components
|
||||
|
||||
### Tools
|
||||
|
||||
- **puppeteer_navigate**
|
||||
- Navigate to any URL in the browser
|
||||
- Inputs:
|
||||
- `url` (string, required): URL to navigate to
|
||||
- `launchOptions` (object, optional): PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: `{ headless: true, args: ['--user-data-dir="C:/Data"'] }`
|
||||
- `allowDangerous` (boolean, optional): Allow dangerous LaunchOptions that reduce security. When false, dangerous args like `--no-sandbox`, `--disable-web-security` will throw errors. Default false.
|
||||
|
||||
- **puppeteer_screenshot**
|
||||
- Capture screenshots of the entire page or specific elements
|
||||
- Inputs:
|
||||
- `name` (string, required): Name for the screenshot
|
||||
- `selector` (string, optional): CSS selector for element to screenshot
|
||||
- `width` (number, optional, default: 800): Screenshot width
|
||||
- `height` (number, optional, default: 600): Screenshot height
|
||||
|
||||
- **puppeteer_click**
|
||||
- Click elements on the page
|
||||
- Input: `selector` (string): CSS selector for element to click
|
||||
|
||||
- **puppeteer_hover**
|
||||
- Hover elements on the page
|
||||
- Input: `selector` (string): CSS selector for element to hover
|
||||
|
||||
- **puppeteer_fill**
|
||||
- Fill out input fields
|
||||
- Inputs:
|
||||
- `selector` (string): CSS selector for input field
|
||||
- `value` (string): Value to fill
|
||||
|
||||
- **puppeteer_select**
|
||||
- Select an element with SELECT tag
|
||||
- Inputs:
|
||||
- `selector` (string): CSS selector for element to select
|
||||
- `value` (string): Value to select
|
||||
|
||||
- **puppeteer_evaluate**
|
||||
- Execute JavaScript in the browser console
|
||||
- Input: `script` (string): JavaScript code to execute
|
||||
|
||||
### Resources
|
||||
|
||||
The server provides access to two types of resources:
|
||||
|
||||
1. **Console Logs** (`console://logs`)
|
||||
- Browser console output in text format
|
||||
- Includes all console messages from the browser
|
||||
|
||||
2. **Screenshots** (`screenshot://<name>`)
|
||||
- PNG images of captured screenshots
|
||||
- Accessible via the screenshot name specified during capture
|
||||
|
||||
## Key Features
|
||||
|
||||
- Browser automation
|
||||
- Console log monitoring
|
||||
- Screenshot capabilities
|
||||
- JavaScript execution
|
||||
- Basic web interaction (navigation, clicking, form filling)
|
||||
- Customizable Puppeteer launch options
|
||||
|
||||
## Configuration to use Puppeteer Server
|
||||
Here's the Claude Desktop configuration to use the Puppeter server:
|
||||
|
||||
### Docker
|
||||
|
||||
**NOTE** The docker implementation will use headless chromium, where as the NPX version will open a browser window.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"puppeteer": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "--init", "-e", "DOCKER_CONTAINER=true", "mcp/puppeteer"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"puppeteer": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Launch Options
|
||||
|
||||
You can customize Puppeteer's browser behavior in two ways:
|
||||
|
||||
1. **Environment Variable**: Set `PUPPETEER_LAUNCH_OPTIONS` with a JSON-encoded string in the MCP configuration's `env` parameter:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mcp-puppeteer": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-puppeteer"],
|
||||
"env": {
|
||||
"PUPPETEER_LAUNCH_OPTIONS": "{ \"headless\": false, \"executablePath\": \"C:/Program Files/Google/Chrome/Application/chrome.exe\", \"args\": [] }",
|
||||
"ALLOW_DANGEROUS": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Tool Call Arguments**: Pass `launchOptions` and `allowDangerous` parameters to the `puppeteer_navigate` tool:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"launchOptions": {
|
||||
"headless": false,
|
||||
"defaultViewport": {"width": 1280, "height": 720}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Docker build:
|
||||
|
||||
```bash
|
||||
docker build -t mcp/puppeteer -f src/puppeteer/Dockerfile .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,484 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
CallToolResult,
|
||||
TextContent,
|
||||
ImageContent,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import puppeteer, { Browser, Page } from "puppeteer";
|
||||
|
||||
// Define the tools once to avoid repetition
|
||||
const TOOLS: Tool[] = [
|
||||
{
|
||||
name: "puppeteer_navigate",
|
||||
description: "Navigate to a URL",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string", description: "URL to navigate to" },
|
||||
launchOptions: { type: "object", description: "PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: { headless: true, args: ['--no-sandbox'] }" },
|
||||
allowDangerous: { type: "boolean", description: "Allow dangerous LaunchOptions that reduce security. When false, dangerous args like --no-sandbox will throw errors. Default false." },
|
||||
},
|
||||
required: ["url"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "puppeteer_screenshot",
|
||||
description: "Take a screenshot of the current page or a specific element",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Name for the screenshot" },
|
||||
selector: { type: "string", description: "CSS selector for element to screenshot" },
|
||||
width: { type: "number", description: "Width in pixels (default: 800)" },
|
||||
height: { type: "number", description: "Height in pixels (default: 600)" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "puppeteer_click",
|
||||
description: "Click an element on the page",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: { type: "string", description: "CSS selector for element to click" },
|
||||
},
|
||||
required: ["selector"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "puppeteer_fill",
|
||||
description: "Fill out an input field",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: { type: "string", description: "CSS selector for input field" },
|
||||
value: { type: "string", description: "Value to fill" },
|
||||
},
|
||||
required: ["selector", "value"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "puppeteer_select",
|
||||
description: "Select an element on the page with Select tag",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: { type: "string", description: "CSS selector for element to select" },
|
||||
value: { type: "string", description: "Value to select" },
|
||||
},
|
||||
required: ["selector", "value"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "puppeteer_hover",
|
||||
description: "Hover an element on the page",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
selector: { type: "string", description: "CSS selector for element to hover" },
|
||||
},
|
||||
required: ["selector"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "puppeteer_evaluate",
|
||||
description: "Execute JavaScript in the browser console",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
script: { type: "string", description: "JavaScript code to execute" },
|
||||
},
|
||||
required: ["script"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Global state
|
||||
let browser: Browser | null;
|
||||
let page: Page | null;
|
||||
const consoleLogs: string[] = [];
|
||||
const screenshots = new Map<string, string>();
|
||||
let previousLaunchOptions: any = null;
|
||||
|
||||
async function ensureBrowser({ launchOptions, allowDangerous }: any) {
|
||||
|
||||
const DANGEROUS_ARGS = [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--single-process',
|
||||
'--disable-web-security',
|
||||
'--ignore-certificate-errors',
|
||||
'--disable-features=IsolateOrigins',
|
||||
'--disable-site-isolation-trials',
|
||||
'--allow-running-insecure-content'
|
||||
];
|
||||
|
||||
// Parse environment config safely
|
||||
let envConfig = {};
|
||||
try {
|
||||
envConfig = JSON.parse(process.env.PUPPETEER_LAUNCH_OPTIONS || '{}');
|
||||
} catch (error: any) {
|
||||
console.warn('Failed to parse PUPPETEER_LAUNCH_OPTIONS:', error?.message || error);
|
||||
}
|
||||
|
||||
// Deep merge environment config with user-provided options
|
||||
const mergedConfig = deepMerge(envConfig, launchOptions || {});
|
||||
|
||||
// Security validation for merged config
|
||||
if (mergedConfig?.args) {
|
||||
const dangerousArgs = mergedConfig.args?.filter?.((arg: string) => DANGEROUS_ARGS.some((dangerousArg: string) => arg.startsWith(dangerousArg)));
|
||||
if (dangerousArgs?.length > 0 && !(allowDangerous || (process.env.ALLOW_DANGEROUS === 'true'))) {
|
||||
throw new Error(`Dangerous browser arguments detected: ${dangerousArgs.join(', ')}. Fround from environment variable and tool call argument. ` +
|
||||
'Set allowDangerous: true in the tool call arguments to override.');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ((browser && !browser.connected) ||
|
||||
(launchOptions && (JSON.stringify(launchOptions) != JSON.stringify(previousLaunchOptions)))) {
|
||||
await browser?.close();
|
||||
browser = null;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
browser = null;
|
||||
}
|
||||
|
||||
previousLaunchOptions = launchOptions;
|
||||
|
||||
if (!browser) {
|
||||
const npx_args = { headless: false }
|
||||
const docker_args = { headless: true, args: ["--no-sandbox", "--single-process", "--no-zygote"] }
|
||||
browser = await puppeteer.launch(deepMerge(
|
||||
process.env.DOCKER_CONTAINER ? docker_args : npx_args,
|
||||
mergedConfig
|
||||
));
|
||||
const pages = await browser.pages();
|
||||
page = pages[0];
|
||||
|
||||
page.on("console", (msg) => {
|
||||
const logEntry = `[${msg.type()}] ${msg.text()}`;
|
||||
consoleLogs.push(logEntry);
|
||||
server.notification({
|
||||
method: "notifications/resources/updated",
|
||||
params: { uri: "console://logs" },
|
||||
});
|
||||
});
|
||||
}
|
||||
return page!;
|
||||
}
|
||||
|
||||
// Deep merge utility function
|
||||
function deepMerge(target: any, source: any): any {
|
||||
const output = Object.assign({}, target);
|
||||
if (typeof target !== 'object' || typeof source !== 'object') return source;
|
||||
|
||||
for (const key of Object.keys(source)) {
|
||||
const targetVal = target[key];
|
||||
const sourceVal = source[key];
|
||||
if (Array.isArray(targetVal) && Array.isArray(sourceVal)) {
|
||||
// Deduplicate args/ignoreDefaultArgs, prefer source values
|
||||
output[key] = [...new Set([
|
||||
...(key === 'args' || key === 'ignoreDefaultArgs' ?
|
||||
targetVal.filter((arg: string) => !sourceVal.some((launchArg: string) => arg.startsWith('--') && launchArg.startsWith(arg.split('=')[0]))) :
|
||||
targetVal),
|
||||
...sourceVal
|
||||
])];
|
||||
} else if (sourceVal instanceof Object && key in target) {
|
||||
output[key] = deepMerge(targetVal, sourceVal);
|
||||
} else {
|
||||
output[key] = sourceVal;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mcpHelper: {
|
||||
logs: string[],
|
||||
originalConsole: Partial<typeof console>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToolCall(name: string, args: any): Promise<CallToolResult> {
|
||||
const page = await ensureBrowser(args);
|
||||
|
||||
switch (name) {
|
||||
case "puppeteer_navigate":
|
||||
await page.goto(args.url);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Navigated to ${args.url}`,
|
||||
}],
|
||||
isError: false,
|
||||
};
|
||||
|
||||
case "puppeteer_screenshot": {
|
||||
const width = args.width ?? 800;
|
||||
const height = args.height ?? 600;
|
||||
await page.setViewport({ width, height });
|
||||
|
||||
const screenshot = await (args.selector ?
|
||||
(await page.$(args.selector))?.screenshot({ encoding: "base64" }) :
|
||||
page.screenshot({ encoding: "base64", fullPage: false }));
|
||||
|
||||
if (!screenshot) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
screenshots.set(args.name, screenshot as string);
|
||||
server.notification({
|
||||
method: "notifications/resources/list_changed",
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Screenshot '${args.name}' taken at ${width}x${height}`,
|
||||
} as TextContent,
|
||||
{
|
||||
type: "image",
|
||||
data: screenshot,
|
||||
mimeType: "image/png",
|
||||
} as ImageContent,
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
}
|
||||
|
||||
case "puppeteer_click":
|
||||
try {
|
||||
await page.click(args.selector);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Clicked: ${args.selector}`,
|
||||
}],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to click ${args.selector}: ${(error as Error).message}`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
case "puppeteer_fill":
|
||||
try {
|
||||
await page.waitForSelector(args.selector);
|
||||
await page.type(args.selector, args.value);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Filled ${args.selector} with: ${args.value}`,
|
||||
}],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to fill ${args.selector}: ${(error as Error).message}`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
case "puppeteer_select":
|
||||
try {
|
||||
await page.waitForSelector(args.selector);
|
||||
await page.select(args.selector, args.value);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Selected ${args.selector} with: ${args.value}`,
|
||||
}],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to select ${args.selector}: ${(error as Error).message}`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
case "puppeteer_hover":
|
||||
try {
|
||||
await page.waitForSelector(args.selector);
|
||||
await page.hover(args.selector);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Hovered ${args.selector}`,
|
||||
}],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Failed to hover ${args.selector}: ${(error as Error).message}`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
case "puppeteer_evaluate":
|
||||
try {
|
||||
await page.evaluate(() => {
|
||||
window.mcpHelper = {
|
||||
logs: [],
|
||||
originalConsole: { ...console },
|
||||
};
|
||||
|
||||
['log', 'info', 'warn', 'error'].forEach(method => {
|
||||
(console as any)[method] = (...args: any[]) => {
|
||||
window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
|
||||
(window.mcpHelper.originalConsole as any)[method](...args);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const result = await page.evaluate(args.script);
|
||||
|
||||
const logs = await page.evaluate(() => {
|
||||
Object.assign(console, window.mcpHelper.originalConsole);
|
||||
const logs = window.mcpHelper.logs;
|
||||
delete (window as any).mcpHelper;
|
||||
return logs;
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Execution result:\n${JSON.stringify(result, null, 2)}\n\nConsole output:\n${logs.join('\n')}`,
|
||||
},
|
||||
],
|
||||
isError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Script execution failed: ${(error as Error).message}`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Unknown tool: ${name}`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
name: "example-servers/puppeteer",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
// Setup request handlers
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
{
|
||||
uri: "console://logs",
|
||||
mimeType: "text/plain",
|
||||
name: "Browser console logs",
|
||||
},
|
||||
...Array.from(screenshots.keys()).map(name => ({
|
||||
uri: `screenshot://${name}`,
|
||||
mimeType: "image/png",
|
||||
name: `Screenshot: ${name}`,
|
||||
})),
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const uri = request.params.uri.toString();
|
||||
|
||||
if (uri === "console://logs") {
|
||||
return {
|
||||
contents: [{
|
||||
uri,
|
||||
mimeType: "text/plain",
|
||||
text: consoleLogs.join("\n"),
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
if (uri.startsWith("screenshot://")) {
|
||||
const name = uri.split("://")[1];
|
||||
const screenshot = screenshots.get(name);
|
||||
if (screenshot) {
|
||||
return {
|
||||
contents: [{
|
||||
uri,
|
||||
mimeType: "image/png",
|
||||
blob: screenshot,
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Resource not found: ${uri}`);
|
||||
});
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: TOOLS,
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) =>
|
||||
handleToolCall(request.params.name, request.params.arguments ?? {})
|
||||
);
|
||||
|
||||
async function runServer() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
runServer().catch(console.error);
|
||||
|
||||
process.stdin.on("close", () => {
|
||||
console.error("Puppeteer MCP Server closed");
|
||||
server.close();
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-puppeteer",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for browser automation using Puppeteer",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-puppeteer": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1",
|
||||
"puppeteer": "^23.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
FROM node:22.12-alpine as builder
|
||||
|
||||
COPY src/redis /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:22-alpine AS release
|
||||
|
||||
COPY --from=builder /app/build /app/build
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "build/index.js"]
|
||||
@@ -1,105 +0,0 @@
|
||||
# Redis
|
||||
|
||||
A Model Context Protocol server that provides access to Redis databases. This server enables LLMs to interact with Redis key-value stores through a set of standardized tools.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Redis server must be installed and running
|
||||
- [Download Redis](https://redis.io/download)
|
||||
- For Windows users: Use [Windows Subsystem for Linux (WSL)](https://redis.io/docs/getting-started/installation/install-redis-on-windows/) or [Memurai](https://www.memurai.com/) (Redis-compatible Windows server)
|
||||
- Default port: 6379
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Connection Errors
|
||||
|
||||
**ECONNREFUSED**
|
||||
- **Cause**: Redis server is not running or unreachable
|
||||
- **Solution**:
|
||||
- Verify Redis is running: `redis-cli ping` should return "PONG"
|
||||
- Check Redis service status: `systemctl status redis` (Linux) or `brew services list` (macOS)
|
||||
- Ensure correct port (default 6379) is not blocked by firewall
|
||||
- Verify Redis URL format: `redis://hostname:port`
|
||||
|
||||
### Server Behavior
|
||||
|
||||
- The server implements exponential backoff with a maximum of 5 retries
|
||||
- Initial retry delay: 1 second, maximum delay: 30 seconds
|
||||
- Server will exit after max retries to prevent infinite reconnection loops
|
||||
|
||||
## Components
|
||||
|
||||
### Tools
|
||||
|
||||
- **set**
|
||||
- Set a Redis key-value pair with optional expiration
|
||||
- Input:
|
||||
- `key` (string): Redis key
|
||||
- `value` (string): Value to store
|
||||
- `expireSeconds` (number, optional): Expiration time in seconds
|
||||
|
||||
- **get**
|
||||
- Get value by key from Redis
|
||||
- Input: `key` (string): Redis key to retrieve
|
||||
|
||||
- **delete**
|
||||
- Delete one or more keys from Redis
|
||||
- Input: `key` (string | string[]): Key or array of keys to delete
|
||||
|
||||
- **list**
|
||||
- List Redis keys matching a pattern
|
||||
- Input: `pattern` (string, optional): Pattern to match keys (default: *)
|
||||
|
||||
## Usage with Claude Desktop
|
||||
|
||||
To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
|
||||
|
||||
### Docker
|
||||
|
||||
* when running docker on macos, use host.docker.internal if the server is running on the host network (eg localhost)
|
||||
* Redis URL can be specified as an argument, defaults to "redis://localhost:6379"
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"redis": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"mcp/redis",
|
||||
"redis://host.docker.internal:6379"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### NPX
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"redis": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-redis",
|
||||
"redis://localhost:6379"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Docker:
|
||||
|
||||
```sh
|
||||
docker build -t mcp/redis -f src/redis/Dockerfile .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-redis",
|
||||
"version": "0.1.0",
|
||||
"description": "MCP server for using Redis",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"redis": "./build/index.js"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x build/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/redis": "^4.0.10",
|
||||
"redis": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
import { createClient } from 'redis';
|
||||
|
||||
// Configuration
|
||||
const REDIS_URL = process.argv[2] || "redis://localhost:6379";
|
||||
const MAX_RETRIES = 5;
|
||||
const MIN_RETRY_DELAY = 1000; // 1 second
|
||||
const MAX_RETRY_DELAY = 30000; // 30 seconds
|
||||
|
||||
// Create Redis client with retry strategy
|
||||
const redisClient = createClient({
|
||||
url: REDIS_URL,
|
||||
socket: {
|
||||
reconnectStrategy: (retries) => {
|
||||
if (retries >= MAX_RETRIES) {
|
||||
console.error(`Maximum retries (${MAX_RETRIES}) reached. Giving up.`);
|
||||
return new Error('Max retries reached');
|
||||
}
|
||||
const delay = Math.min(Math.pow(2, retries) * MIN_RETRY_DELAY, MAX_RETRY_DELAY);
|
||||
console.error(`Reconnection attempt ${retries + 1}/${MAX_RETRIES} in ${delay}ms`);
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Define Zod schemas for validation
|
||||
const SetArgumentsSchema = z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
expireSeconds: z.number().optional(),
|
||||
});
|
||||
|
||||
const GetArgumentsSchema = z.object({
|
||||
key: z.string(),
|
||||
});
|
||||
|
||||
const DeleteArgumentsSchema = z.object({
|
||||
key: z.string().or(z.array(z.string())),
|
||||
});
|
||||
|
||||
const ListArgumentsSchema = z.object({
|
||||
pattern: z.string().default("*"),
|
||||
});
|
||||
|
||||
// Create server instance
|
||||
const server = new Server(
|
||||
{
|
||||
name: "redis",
|
||||
version: "0.0.1"
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// List available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "set",
|
||||
description: "Set a Redis key-value pair with optional expiration",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: {
|
||||
type: "string",
|
||||
description: "Redis key",
|
||||
},
|
||||
value: {
|
||||
type: "string",
|
||||
description: "Value to store",
|
||||
},
|
||||
expireSeconds: {
|
||||
type: "number",
|
||||
description: "Optional expiration time in seconds",
|
||||
},
|
||||
},
|
||||
required: ["key", "value"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get",
|
||||
description: "Get value by key from Redis",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: {
|
||||
type: "string",
|
||||
description: "Redis key to retrieve",
|
||||
},
|
||||
},
|
||||
required: ["key"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "delete",
|
||||
description: "Delete one or more keys from Redis",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
key: {
|
||||
oneOf: [
|
||||
{ type: "string" },
|
||||
{ type: "array", items: { type: "string" } }
|
||||
],
|
||||
description: "Key or array of keys to delete",
|
||||
},
|
||||
},
|
||||
required: ["key"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list",
|
||||
description: "List Redis keys matching a pattern",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
pattern: {
|
||||
type: "string",
|
||||
description: "Pattern to match keys (default: *)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool execution
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
if (name === "set") {
|
||||
const { key, value, expireSeconds } = SetArgumentsSchema.parse(args);
|
||||
|
||||
if (expireSeconds) {
|
||||
await redisClient.setEx(key, expireSeconds, value);
|
||||
} else {
|
||||
await redisClient.set(key, value);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully set key: ${key}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (name === "get") {
|
||||
const { key } = GetArgumentsSchema.parse(args);
|
||||
const value = await redisClient.get(key);
|
||||
|
||||
if (value === null) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Key not found: ${key}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${value}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (name === "delete") {
|
||||
const { key } = DeleteArgumentsSchema.parse(args);
|
||||
|
||||
if (Array.isArray(key)) {
|
||||
await redisClient.del(key);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully deleted ${key.length} keys`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
await redisClient.del(key);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully deleted key: ${key}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
} else if (name === "list") {
|
||||
const { pattern } = ListArgumentsSchema.parse(args);
|
||||
const keys = await redisClient.keys(pattern);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: keys.length > 0
|
||||
? `Found keys:\n${keys.join('\n')}`
|
||||
: "No keys found matching pattern",
|
||||
},
|
||||
],
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(
|
||||
`Invalid arguments: ${error.errors
|
||||
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
async function main() {
|
||||
try {
|
||||
// Set up Redis event handlers
|
||||
redisClient.on('error', (err: Error) => {
|
||||
console.error('Redis Client Error:', err);
|
||||
});
|
||||
|
||||
redisClient.on('connect', () => {
|
||||
console.error(`Connected to Redis at ${REDIS_URL}`);
|
||||
});
|
||||
|
||||
redisClient.on('reconnecting', () => {
|
||||
console.error('Attempting to reconnect to Redis...');
|
||||
});
|
||||
|
||||
redisClient.on('end', () => {
|
||||
console.error('Redis connection closed');
|
||||
});
|
||||
|
||||
// Connect to Redis
|
||||
await redisClient.connect();
|
||||
|
||||
// Set up MCP server
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("Redis MCP Server running on stdio");
|
||||
} catch (error) {
|
||||
console.error("Error during startup:", error);
|
||||
await cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
async function cleanup() {
|
||||
try {
|
||||
await redisClient.quit();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', cleanup);
|
||||
process.on('SIGTERM', cleanup);
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
cleanup();
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.10
|
||||
@@ -1,37 +0,0 @@
|
||||
# Use a Python image with uv pre-installed
|
||||
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv
|
||||
|
||||
# Install the project into `/app`
|
||||
WORKDIR /app
|
||||
|
||||
# Enable bytecode compilation
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
# Copy from the cache instead of linking since it's a mounted volume
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Install the project's dependencies using the lockfile and settings
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project --no-dev --no-editable
|
||||
|
||||
# Then, add the rest of the project source code and install it
|
||||
# Installing separately from its dependencies allows optimal layer caching
|
||||
ADD . /app
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-editable
|
||||
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=uv /root/.local /root/.local
|
||||
COPY --from=uv --chown=app:app /app/.venv /app/.venv
|
||||
|
||||
# Place executables in the environment at the front of the path
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# when running the container, add --db-path and a bind mount to the host's db file
|
||||
ENTRYPOINT ["mcp-server-sentry"]
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
# mcp-server-sentry: A Sentry MCP server
|
||||
|
||||
## Overview
|
||||
|
||||
A Model Context Protocol server for retrieving and analyzing issues from Sentry.io. This server provides tools to inspect error reports, stacktraces, and other debugging information from your Sentry account.
|
||||
|
||||
### Tools
|
||||
|
||||
1. `get_sentry_issue`
|
||||
- Retrieve and analyze a Sentry issue by ID or URL
|
||||
- Input:
|
||||
- `issue_id_or_url` (string): Sentry issue ID or URL to analyze
|
||||
- Returns: Issue details including:
|
||||
- Title
|
||||
- Issue ID
|
||||
- Status
|
||||
- Level
|
||||
- First seen timestamp
|
||||
- Last seen timestamp
|
||||
- Event count
|
||||
- Full stacktrace
|
||||
|
||||
### Prompts
|
||||
|
||||
1. `sentry-issue`
|
||||
- Retrieve issue details from Sentry
|
||||
- Input:
|
||||
- `issue_id_or_url` (string): Sentry issue ID or URL
|
||||
- Returns: Formatted issue details as conversation context
|
||||
|
||||
## Installation
|
||||
|
||||
### Using uv (recommended)
|
||||
|
||||
When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will
|
||||
use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcp-server-sentry*.
|
||||
|
||||
### Using PIP
|
||||
|
||||
Alternatively you can install `mcp-server-sentry` via pip:
|
||||
|
||||
```
|
||||
pip install mcp-server-sentry
|
||||
```
|
||||
|
||||
After installation, you can run it as a script using:
|
||||
|
||||
```
|
||||
python -m mcp_server_sentry
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Usage with Claude Desktop
|
||||
|
||||
Add this to your `claude_desktop_config.json`:
|
||||
|
||||
<details>
|
||||
<summary>Using uvx</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"sentry": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-sentry", "--auth-token", "YOUR_SENTRY_TOKEN"]
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<details>
|
||||
<summary>Using docker</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"sentry": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "mcp/sentry", "--auth-token", "YOUR_SENTRY_TOKEN"]
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Using pip installation</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"sentry": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_sentry", "--auth-token", "YOUR_SENTRY_TOKEN"]
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
### Usage with [Zed](https://github.com/zed-industries/zed)
|
||||
|
||||
Add to your Zed settings.json:
|
||||
|
||||
<details>
|
||||
<summary>Using uvx</summary>
|
||||
|
||||
```json
|
||||
"context_servers": [
|
||||
"mcp-server-sentry": {
|
||||
"command": {
|
||||
"path": "uvx",
|
||||
"args": ["mcp-server-sentry", "--auth-token", "YOUR_SENTRY_TOKEN"]
|
||||
}
|
||||
}
|
||||
],
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Using pip installation</summary>
|
||||
|
||||
```json
|
||||
"context_servers": {
|
||||
"mcp-server-sentry": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_sentry", "--auth-token", "YOUR_SENTRY_TOKEN"]
|
||||
}
|
||||
},
|
||||
```
|
||||
</details>
|
||||
|
||||
## Debugging
|
||||
|
||||
You can use the MCP inspector to debug the server. For uvx installations:
|
||||
|
||||
```
|
||||
npx @modelcontextprotocol/inspector uvx mcp-server-sentry --auth-token YOUR_SENTRY_TOKEN
|
||||
```
|
||||
|
||||
Or if you've installed the package in a specific directory or are developing on it:
|
||||
|
||||
```
|
||||
cd path/to/servers/src/sentry
|
||||
npx @modelcontextprotocol/inspector uv run mcp-server-sentry --auth-token YOUR_SENTRY_TOKEN
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,17 +0,0 @@
|
||||
[project]
|
||||
name = "mcp-server-sentry"
|
||||
version = "0.6.2"
|
||||
description = "MCP server for retrieving issues from sentry.io"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["mcp>=1.0.0"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = ["pyright>=1.1.389", "pytest>=8.3.3", "ruff>=0.8.0"]
|
||||
|
||||
[project.scripts]
|
||||
mcp-server-sentry = "mcp_server_sentry:main"
|
||||
@@ -1,11 +0,0 @@
|
||||
from . import server
|
||||
import asyncio
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the package."""
|
||||
asyncio.run(server.main())
|
||||
|
||||
|
||||
# Optionally expose other important items at package level
|
||||
__all__ = ["main", "server"]
|
||||
@@ -1,4 +0,0 @@
|
||||
from mcp_server_sentry.server import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,285 +0,0 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
import httpx
|
||||
import mcp.types as types
|
||||
from mcp.server import NotificationOptions, Server
|
||||
from mcp.server.models import InitializationOptions
|
||||
from mcp.shared.exceptions import McpError
|
||||
import mcp.server.stdio
|
||||
|
||||
SENTRY_API_BASE = "https://sentry.io/api/0/"
|
||||
MISSING_AUTH_TOKEN_MESSAGE = (
|
||||
"""Sentry authentication token not found. Please specify your Sentry auth token."""
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SentryIssueData:
|
||||
title: str
|
||||
issue_id: str
|
||||
status: str
|
||||
level: str
|
||||
first_seen: str
|
||||
last_seen: str
|
||||
count: int
|
||||
stacktrace: str
|
||||
|
||||
def to_text(self) -> str:
|
||||
return f"""
|
||||
Sentry Issue: {self.title}
|
||||
Issue ID: {self.issue_id}
|
||||
Status: {self.status}
|
||||
Level: {self.level}
|
||||
First Seen: {self.first_seen}
|
||||
Last Seen: {self.last_seen}
|
||||
Event Count: {self.count}
|
||||
|
||||
{self.stacktrace}
|
||||
"""
|
||||
|
||||
def to_prompt_result(self) -> types.GetPromptResult:
|
||||
return types.GetPromptResult(
|
||||
description=f"Sentry Issue: {self.title}",
|
||||
messages=[
|
||||
types.PromptMessage(
|
||||
role="user", content=types.TextContent(type="text", text=self.to_text())
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
def to_tool_result(self) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||
return [types.TextContent(type="text", text=self.to_text())]
|
||||
|
||||
|
||||
class SentryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def extract_issue_id(issue_id_or_url: str) -> str:
|
||||
"""
|
||||
Extracts the Sentry issue ID from either a full URL or a standalone ID.
|
||||
|
||||
This function validates the input and returns the numeric issue ID.
|
||||
It raises SentryError for invalid inputs, including empty strings,
|
||||
non-Sentry URLs, malformed paths, and non-numeric IDs.
|
||||
"""
|
||||
if not issue_id_or_url:
|
||||
raise SentryError("Missing issue_id_or_url argument")
|
||||
|
||||
if issue_id_or_url.startswith(("http://", "https://")):
|
||||
parsed_url = urlparse(issue_id_or_url)
|
||||
if not parsed_url.hostname or not parsed_url.hostname.endswith(".sentry.io"):
|
||||
raise SentryError("Invalid Sentry URL. Must be a URL ending with .sentry.io")
|
||||
|
||||
path_parts = parsed_url.path.strip("/").split("/")
|
||||
if len(path_parts) < 2 or path_parts[0] != "issues":
|
||||
raise SentryError(
|
||||
"Invalid Sentry issue URL. Path must contain '/issues/{issue_id}'"
|
||||
)
|
||||
|
||||
issue_id = path_parts[-1]
|
||||
else:
|
||||
issue_id = issue_id_or_url
|
||||
|
||||
if not issue_id.isdigit():
|
||||
raise SentryError("Invalid Sentry issue ID. Must be a numeric value.")
|
||||
|
||||
return issue_id
|
||||
|
||||
|
||||
def create_stacktrace(latest_event: dict) -> str:
|
||||
"""
|
||||
Creates a formatted stacktrace string from the latest Sentry event.
|
||||
|
||||
This function extracts exception information and stacktrace details from the
|
||||
provided event dictionary, formatting them into a human-readable string.
|
||||
It handles multiple exceptions and includes file, line number, and function
|
||||
information for each frame in the stacktrace.
|
||||
|
||||
Args:
|
||||
latest_event (dict): A dictionary containing the latest Sentry event data.
|
||||
|
||||
Returns:
|
||||
str: A formatted string containing the stacktrace information,
|
||||
or "No stacktrace found" if no relevant data is present.
|
||||
"""
|
||||
stacktraces = []
|
||||
for entry in latest_event.get("entries", []):
|
||||
if entry["type"] != "exception":
|
||||
continue
|
||||
|
||||
exception_data = entry["data"]["values"]
|
||||
for exception in exception_data:
|
||||
exception_type = exception.get("type", "Unknown")
|
||||
exception_value = exception.get("value", "")
|
||||
stacktrace = exception.get("stacktrace")
|
||||
|
||||
stacktrace_text = f"Exception: {exception_type}: {exception_value}\n\n"
|
||||
if stacktrace:
|
||||
stacktrace_text += "Stacktrace:\n"
|
||||
for frame in stacktrace.get("frames", []):
|
||||
filename = frame.get("filename", "Unknown")
|
||||
lineno = frame.get("lineNo", "?")
|
||||
function = frame.get("function", "Unknown")
|
||||
|
||||
stacktrace_text += f"{filename}:{lineno} in {function}\n"
|
||||
|
||||
if "context" in frame:
|
||||
context = frame["context"]
|
||||
for ctx_line in context:
|
||||
stacktrace_text += f" {ctx_line[1]}\n"
|
||||
|
||||
stacktrace_text += "\n"
|
||||
|
||||
stacktraces.append(stacktrace_text)
|
||||
|
||||
return "\n".join(stacktraces) if stacktraces else "No stacktrace found"
|
||||
|
||||
|
||||
async def handle_sentry_issue(
|
||||
http_client: httpx.AsyncClient, auth_token: str, issue_id_or_url: str
|
||||
) -> SentryIssueData:
|
||||
try:
|
||||
issue_id = extract_issue_id(issue_id_or_url)
|
||||
|
||||
response = await http_client.get(
|
||||
f"issues/{issue_id}/", headers={"Authorization": f"Bearer {auth_token}"}
|
||||
)
|
||||
if response.status_code == 401:
|
||||
raise McpError(
|
||||
"Error: Unauthorized. Please check your MCP_SENTRY_AUTH_TOKEN token."
|
||||
)
|
||||
response.raise_for_status()
|
||||
issue_data = response.json()
|
||||
|
||||
# Get issue hashes
|
||||
hashes_response = await http_client.get(
|
||||
f"issues/{issue_id}/hashes/",
|
||||
headers={"Authorization": f"Bearer {auth_token}"},
|
||||
)
|
||||
hashes_response.raise_for_status()
|
||||
hashes = hashes_response.json()
|
||||
|
||||
if not hashes:
|
||||
raise McpError("No Sentry events found for this issue")
|
||||
|
||||
latest_event = hashes[0]["latestEvent"]
|
||||
stacktrace = create_stacktrace(latest_event)
|
||||
|
||||
return SentryIssueData(
|
||||
title=issue_data["title"],
|
||||
issue_id=issue_id,
|
||||
status=issue_data["status"],
|
||||
level=issue_data["level"],
|
||||
first_seen=issue_data["firstSeen"],
|
||||
last_seen=issue_data["lastSeen"],
|
||||
count=issue_data["count"],
|
||||
stacktrace=stacktrace
|
||||
)
|
||||
|
||||
except SentryError as e:
|
||||
raise McpError(str(e))
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise McpError(f"Error fetching Sentry issue: {str(e)}")
|
||||
except Exception as e:
|
||||
raise McpError(f"An error occurred: {str(e)}")
|
||||
|
||||
|
||||
async def serve(auth_token: str) -> Server:
|
||||
server = Server("sentry")
|
||||
http_client = httpx.AsyncClient(base_url=SENTRY_API_BASE)
|
||||
|
||||
@server.list_prompts()
|
||||
async def handle_list_prompts() -> list[types.Prompt]:
|
||||
return [
|
||||
types.Prompt(
|
||||
name="sentry-issue",
|
||||
description="Retrieve a Sentry issue by ID or URL",
|
||||
arguments=[
|
||||
types.PromptArgument(
|
||||
name="issue_id_or_url",
|
||||
description="Sentry issue ID or URL",
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
@server.get_prompt()
|
||||
async def handle_get_prompt(
|
||||
name: str, arguments: dict[str, str] | None
|
||||
) -> types.GetPromptResult:
|
||||
if name != "sentry-issue":
|
||||
raise ValueError(f"Unknown prompt: {name}")
|
||||
|
||||
issue_id_or_url = (arguments or {}).get("issue_id_or_url", "")
|
||||
issue_data = await handle_sentry_issue(http_client, auth_token, issue_id_or_url)
|
||||
return issue_data.to_prompt_result()
|
||||
|
||||
@server.list_tools()
|
||||
async def handle_list_tools() -> list[types.Tool]:
|
||||
return [
|
||||
types.Tool(
|
||||
name="get_sentry_issue",
|
||||
description="""Retrieve and analyze a Sentry issue by ID or URL. Use this tool when you need to:
|
||||
- Investigate production errors and crashes
|
||||
- Access detailed stacktraces from Sentry
|
||||
- Analyze error patterns and frequencies
|
||||
- Get information about when issues first/last occurred
|
||||
- Review error counts and status""",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"issue_id_or_url": {
|
||||
"type": "string",
|
||||
"description": "Sentry issue ID or URL to analyze"
|
||||
}
|
||||
},
|
||||
"required": ["issue_id_or_url"]
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(
|
||||
name: str, arguments: dict | None
|
||||
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||
if name != "get_sentry_issue":
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
if not arguments or "issue_id_or_url" not in arguments:
|
||||
raise ValueError("Missing issue_id_or_url argument")
|
||||
|
||||
issue_data = await handle_sentry_issue(http_client, auth_token, arguments["issue_id_or_url"])
|
||||
return issue_data.to_tool_result()
|
||||
|
||||
return server
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--auth-token",
|
||||
envvar="SENTRY_TOKEN",
|
||||
required=True,
|
||||
help="Sentry authentication token",
|
||||
)
|
||||
def main(auth_token: str):
|
||||
async def _run():
|
||||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||
server = await serve(auth_token)
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name="sentry",
|
||||
server_version="0.4.1",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
asyncio.run(_run())
|
||||
439
src/sentry/uv.lock
generated
439
src/sentry/uv.lock
generated
@@ -1,439 +0,0 @@
|
||||
version = 1
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.6.2.post1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/df/676b7cf674dd1bdc71a64ad393c89879f75e4a0ab8395165b498262ae106/httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", size = 141307 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fb/a19866137577ba60c6d8b69498dc36be479b13ba454f691348ddf428f185/httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc", size = 73551 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-server-sentry"
|
||||
version = "0.6.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "mcp" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pyright" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "mcp", specifier = ">=1.0.0" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "pyright", specifier = ">=1.1.389" },
|
||||
{ name = "pytest", specifier = ">=8.3.3" },
|
||||
{ name = "ruff", specifier = ">=0.8.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/86/a03390cb12cf64e2a8df07c267f3eb8d5035e0f9a04bb20fb79403d2a00e/pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", size = 785401 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/74/da832196702d0c56eb86b75bfa346db9238617e29b0b7ee3b8b4eccfe654/pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e", size = 456364 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.389"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodeenv" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/4e/9a5ab8745e7606b88c2c7ca223449ac9d82a71fd5e31df47b453f2cb39a1/pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220", size = 21940 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/d0/8ff5b189d125f4260f2255d143bf2fa413b69c2610c405ace7a0a8ec81ec/ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", size = 3313222 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/d6/1a6314e568db88acdbb5121ed53e2c52cebf3720d3437a76f82f923bf171/ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5", size = 10532605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a8/a957a8812e31facffb6a26a30be0b5b4af000a6e30c7d43a22a5232a3398/ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", size = 10278243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/23/9db40fa19c453fabf94f7a35c61c58f20e8200b4734a20839515a19da790/ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", size = 9917739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a0/6ee2d949835d5701d832fc5acd05c0bfdad5e89cfdd074a171411f5ccad5/ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", size = 10779153 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/25/9c11dca9404ef1eb24833f780146236131a3c7941de394bc356912ef1041/ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", size = 10304387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/b9/84c323780db1b06feae603a707d82dbbd85955c8c917738571c65d7d5aff/ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", size = 11360351 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e1/9d4bbb2ace7aad14ded20e4674a48cda5b902aed7a1b14e6b028067060c4/ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", size = 12022879 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/28/752ff6120c0e7f9981bc4bc275d540c7f36db1379ba9db9142f69c88db21/ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", size = 11610354 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/967b61c2cc8ebd1df877607fbe462bc1e1220b4a30ae3352648aec8c24bd/ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", size = 12813976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/29/e059f945d6bd2d90213387b8c360187f2fefc989ddcee6bbf3c241329b92/ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", size = 11154564 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/47/cbd05e5a62f3fb4c072bc65c1e8fd709924cad1c7ec60a1000d1e4ee8307/ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", size = 10760604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ee/4c3981c47147c72647a198a94202633130cfda0fc95cd863a553b6f65c6a/ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", size = 10391071 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/e6/083eb61300214590b188616a8ac6ae1ef5730a0974240fb4bec9c17de78b/ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", size = 10896657 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/bd/aacdb8285d10f1b943dbeb818968efca35459afc29f66ae3bd4596fbf954/ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", size = 11228362 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/72/fcb7ad41947f38b4eaa702aca0a361af0e9c2bf671d7fd964480670c297e/ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", size = 8803476 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ea/cae9aeb0f4822c44651c8407baacdb2e5b4dcd7b31a84e1c5df33aa2cc20/ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", size = 9614463 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/76/fbb4bd23dfb48fa7758d35b744413b650a9fd2ddd93bca77e30376864414/ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", size = 8959621 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.41.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.32.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 },
|
||||
]
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# Sequential Thinking MCP Server
|
||||
|
||||
An MCP server implementation that provides a tool for dynamic and reflective problem-solving through a structured thinking process.
|
||||
@@ -78,6 +77,58 @@ Add this to your `claude_desktop_config.json`:
|
||||
}
|
||||
```
|
||||
|
||||
### Usage with VS Code
|
||||
|
||||
For quick installation, click one of the installation buttons below...
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=sequentialthinking&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-sequential-thinking%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=sequentialthinking&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-sequential-thinking%22%5D%7D&quality=insiders)
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=sequentialthinking&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22mcp%2Fsequentialthinking%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=sequentialthinking&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22--rm%22%2C%22-i%22%2C%22mcp%2Fsequentialthinking%22%5D%7D&quality=insiders)
|
||||
|
||||
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open Settings (JSON)`.
|
||||
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
|
||||
|
||||
For NPX installation:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"sequential-thinking": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-sequential-thinking"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Docker installation:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"sequential-thinking": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"mcp/sequentialthinking"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Docker:
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
FROM node:22.12-alpine AS builder
|
||||
|
||||
# Must be entire project because `prepare` script is run during `npm install` and requires all files.
|
||||
COPY src/slack /app
|
||||
COPY tsconfig.json /tsconfig.json
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm npm install
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
|
||||
|
||||
FROM node:22-alpine AS release
|
||||
|
||||
COPY --from=builder /app/dist /app/dist
|
||||
COPY --from=builder /app/package.json /app/package.json
|
||||
COPY --from=builder /app/package-lock.json /app/package-lock.json
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm ci --ignore-scripts --omit-dev
|
||||
|
||||
ENTRYPOINT ["node", "dist/index.js"]
|
||||
@@ -1,166 +0,0 @@
|
||||
# Slack MCP Server
|
||||
|
||||
MCP Server for the Slack API, enabling Claude to interact with Slack workspaces.
|
||||
|
||||
## Tools
|
||||
|
||||
1. `slack_list_channels`
|
||||
- List public or pre-defined channels in the workspace
|
||||
- Optional inputs:
|
||||
- `limit` (number, default: 100, max: 200): Maximum number of channels to return
|
||||
- `cursor` (string): Pagination cursor for next page
|
||||
- Returns: List of channels with their IDs and information
|
||||
|
||||
2. `slack_post_message`
|
||||
- Post a new message to a Slack channel
|
||||
- Required inputs:
|
||||
- `channel_id` (string): The ID of the channel to post to
|
||||
- `text` (string): The message text to post
|
||||
- Returns: Message posting confirmation and timestamp
|
||||
|
||||
3. `slack_reply_to_thread`
|
||||
- Reply to a specific message thread
|
||||
- Required inputs:
|
||||
- `channel_id` (string): The channel containing the thread
|
||||
- `thread_ts` (string): Timestamp of the parent message
|
||||
- `text` (string): The reply text
|
||||
- Returns: Reply confirmation and timestamp
|
||||
|
||||
4. `slack_add_reaction`
|
||||
- Add an emoji reaction to a message
|
||||
- Required inputs:
|
||||
- `channel_id` (string): The channel containing the message
|
||||
- `timestamp` (string): Message timestamp to react to
|
||||
- `reaction` (string): Emoji name without colons
|
||||
- Returns: Reaction confirmation
|
||||
|
||||
5. `slack_get_channel_history`
|
||||
- Get recent messages from a channel
|
||||
- Required inputs:
|
||||
- `channel_id` (string): The channel ID
|
||||
- Optional inputs:
|
||||
- `limit` (number, default: 10): Number of messages to retrieve
|
||||
- Returns: List of messages with their content and metadata
|
||||
|
||||
6. `slack_get_thread_replies`
|
||||
- Get all replies in a message thread
|
||||
- Required inputs:
|
||||
- `channel_id` (string): The channel containing the thread
|
||||
- `thread_ts` (string): Timestamp of the parent message
|
||||
- Returns: List of replies with their content and metadata
|
||||
|
||||
|
||||
7. `slack_get_users`
|
||||
- Get list of workspace users with basic profile information
|
||||
- Optional inputs:
|
||||
- `cursor` (string): Pagination cursor for next page
|
||||
- `limit` (number, default: 100, max: 200): Maximum users to return
|
||||
- Returns: List of users with their basic profiles
|
||||
|
||||
8. `slack_get_user_profile`
|
||||
- Get detailed profile information for a specific user
|
||||
- Required inputs:
|
||||
- `user_id` (string): The user's ID
|
||||
- Returns: Detailed user profile information
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create a Slack App:
|
||||
- Visit the [Slack Apps page](https://api.slack.com/apps)
|
||||
- Click "Create New App"
|
||||
- Choose "From scratch"
|
||||
- Name your app and select your workspace
|
||||
|
||||
2. Configure Bot Token Scopes:
|
||||
Navigate to "OAuth & Permissions" and add these scopes:
|
||||
- `channels:history` - View messages and other content in public channels
|
||||
- `channels:read` - View basic channel information
|
||||
- `chat:write` - Send messages as the app
|
||||
- `reactions:write` - Add emoji reactions to messages
|
||||
- `users:read` - View users and their basic information
|
||||
|
||||
4. Install App to Workspace:
|
||||
- Click "Install to Workspace" and authorize the app
|
||||
- Save the "Bot User OAuth Token" that starts with `xoxb-`
|
||||
|
||||
5. Get your Team ID (starts with a `T`) by following [this guidance](https://slack.com/help/articles/221769328-Locate-your-Slack-URL-or-ID#find-your-workspace-or-org-id)
|
||||
|
||||
### Usage with Claude Desktop
|
||||
|
||||
Add the following to your `claude_desktop_config.json`:
|
||||
|
||||
#### npx
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"slack": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-slack"
|
||||
],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
|
||||
"SLACK_TEAM_ID": "T01234567",
|
||||
"SLACK_CHANNEL_IDS": "C01234567, C76543210"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### docker
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"slack": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"SLACK_BOT_TOKEN",
|
||||
"-e",
|
||||
"SLACK_TEAM_ID",
|
||||
"-e",
|
||||
"SLACK_CHANNEL_IDS",
|
||||
"mcp/slack"
|
||||
],
|
||||
"env": {
|
||||
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
|
||||
"SLACK_TEAM_ID": "T01234567",
|
||||
"SLACK_CHANNEL_IDS": "C01234567, C76543210"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
1. `SLACK_BOT_TOKEN`: Required. The Bot User OAuth Token starting with `xoxb-`.
|
||||
2. `SLACK_TEAM_ID`: Required. Your Slack workspace ID starting with `T`.
|
||||
3. `SLACK_CHANNEL_IDS`: Optional. Comma-separated list of channel IDs to limit channel access (e.g., "C01234567, C76543210"). If not set, all public channels will be listed.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you encounter permission errors, verify that:
|
||||
1. All required scopes are added to your Slack app
|
||||
2. The app is properly installed to your workspace
|
||||
3. The tokens and workspace ID are correctly copied to your configuration
|
||||
4. The app has been added to the channels it needs to access
|
||||
|
||||
## Build
|
||||
|
||||
Docker build:
|
||||
|
||||
```bash
|
||||
docker build -t mcp/slack -f src/slack/Dockerfile .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,582 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequest,
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
// Type definitions for tool arguments
|
||||
interface ListChannelsArgs {
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
interface PostMessageArgs {
|
||||
channel_id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface ReplyToThreadArgs {
|
||||
channel_id: string;
|
||||
thread_ts: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface AddReactionArgs {
|
||||
channel_id: string;
|
||||
timestamp: string;
|
||||
reaction: string;
|
||||
}
|
||||
|
||||
interface GetChannelHistoryArgs {
|
||||
channel_id: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
interface GetThreadRepliesArgs {
|
||||
channel_id: string;
|
||||
thread_ts: string;
|
||||
}
|
||||
|
||||
interface GetUsersArgs {
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
interface GetUserProfileArgs {
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
// Tool definitions
|
||||
const listChannelsTool: Tool = {
|
||||
name: "slack_list_channels",
|
||||
description: "List public or pre-defined channels in the workspace with pagination",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
limit: {
|
||||
type: "number",
|
||||
description:
|
||||
"Maximum number of channels to return (default 100, max 200)",
|
||||
default: 100,
|
||||
},
|
||||
cursor: {
|
||||
type: "string",
|
||||
description: "Pagination cursor for next page of results",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const postMessageTool: Tool = {
|
||||
name: "slack_post_message",
|
||||
description: "Post a new message to a Slack channel",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
channel_id: {
|
||||
type: "string",
|
||||
description: "The ID of the channel to post to",
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
description: "The message text to post",
|
||||
},
|
||||
},
|
||||
required: ["channel_id", "text"],
|
||||
},
|
||||
};
|
||||
|
||||
const replyToThreadTool: Tool = {
|
||||
name: "slack_reply_to_thread",
|
||||
description: "Reply to a specific message thread in Slack",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
channel_id: {
|
||||
type: "string",
|
||||
description: "The ID of the channel containing the thread",
|
||||
},
|
||||
thread_ts: {
|
||||
type: "string",
|
||||
description: "The timestamp of the parent message in the format '1234567890.123456'. Timestamps in the format without the period can be converted by adding the period such that 6 numbers come after it.",
|
||||
},
|
||||
text: {
|
||||
type: "string",
|
||||
description: "The reply text",
|
||||
},
|
||||
},
|
||||
required: ["channel_id", "thread_ts", "text"],
|
||||
},
|
||||
};
|
||||
|
||||
const addReactionTool: Tool = {
|
||||
name: "slack_add_reaction",
|
||||
description: "Add a reaction emoji to a message",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
channel_id: {
|
||||
type: "string",
|
||||
description: "The ID of the channel containing the message",
|
||||
},
|
||||
timestamp: {
|
||||
type: "string",
|
||||
description: "The timestamp of the message to react to",
|
||||
},
|
||||
reaction: {
|
||||
type: "string",
|
||||
description: "The name of the emoji reaction (without ::)",
|
||||
},
|
||||
},
|
||||
required: ["channel_id", "timestamp", "reaction"],
|
||||
},
|
||||
};
|
||||
|
||||
const getChannelHistoryTool: Tool = {
|
||||
name: "slack_get_channel_history",
|
||||
description: "Get recent messages from a channel",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
channel_id: {
|
||||
type: "string",
|
||||
description: "The ID of the channel",
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
description: "Number of messages to retrieve (default 10)",
|
||||
default: 10,
|
||||
},
|
||||
},
|
||||
required: ["channel_id"],
|
||||
},
|
||||
};
|
||||
|
||||
const getThreadRepliesTool: Tool = {
|
||||
name: "slack_get_thread_replies",
|
||||
description: "Get all replies in a message thread",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
channel_id: {
|
||||
type: "string",
|
||||
description: "The ID of the channel containing the thread",
|
||||
},
|
||||
thread_ts: {
|
||||
type: "string",
|
||||
description: "The timestamp of the parent message in the format '1234567890.123456'. Timestamps in the format without the period can be converted by adding the period such that 6 numbers come after it.",
|
||||
},
|
||||
},
|
||||
required: ["channel_id", "thread_ts"],
|
||||
},
|
||||
};
|
||||
|
||||
const getUsersTool: Tool = {
|
||||
name: "slack_get_users",
|
||||
description:
|
||||
"Get a list of all users in the workspace with their basic profile information",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
cursor: {
|
||||
type: "string",
|
||||
description: "Pagination cursor for next page of results",
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
description: "Maximum number of users to return (default 100, max 200)",
|
||||
default: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getUserProfileTool: Tool = {
|
||||
name: "slack_get_user_profile",
|
||||
description: "Get detailed profile information for a specific user",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
user_id: {
|
||||
type: "string",
|
||||
description: "The ID of the user",
|
||||
},
|
||||
},
|
||||
required: ["user_id"],
|
||||
},
|
||||
};
|
||||
|
||||
class SlackClient {
|
||||
private botHeaders: { Authorization: string; "Content-Type": string };
|
||||
|
||||
constructor(botToken: string) {
|
||||
this.botHeaders = {
|
||||
Authorization: `Bearer ${botToken}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
async getChannels(limit: number = 100, cursor?: string): Promise<any> {
|
||||
const predefinedChannelIds = process.env.SLACK_CHANNEL_IDS;
|
||||
if (!predefinedChannelIds) {
|
||||
const params = new URLSearchParams({
|
||||
types: "public_channel",
|
||||
exclude_archived: "true",
|
||||
limit: Math.min(limit, 200).toString(),
|
||||
team_id: process.env.SLACK_TEAM_ID!,
|
||||
});
|
||||
|
||||
if (cursor) {
|
||||
params.append("cursor", cursor);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://slack.com/api/conversations.list?${params}`,
|
||||
{ headers: this.botHeaders },
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const predefinedChannelIdsArray = predefinedChannelIds.split(",").map((id: string) => id.trim());
|
||||
const channels = [];
|
||||
|
||||
for (const channelId of predefinedChannelIdsArray) {
|
||||
const params = new URLSearchParams({
|
||||
channel: channelId,
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`https://slack.com/api/conversations.info?${params}`,
|
||||
{ headers: this.botHeaders }
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.ok && data.channel && !data.channel.is_archived) {
|
||||
channels.push(data.channel);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
channels: channels,
|
||||
response_metadata: { next_cursor: "" },
|
||||
};
|
||||
}
|
||||
|
||||
async postMessage(channel_id: string, text: string): Promise<any> {
|
||||
const response = await fetch("https://slack.com/api/chat.postMessage", {
|
||||
method: "POST",
|
||||
headers: this.botHeaders,
|
||||
body: JSON.stringify({
|
||||
channel: channel_id,
|
||||
text: text,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async postReply(
|
||||
channel_id: string,
|
||||
thread_ts: string,
|
||||
text: string,
|
||||
): Promise<any> {
|
||||
const response = await fetch("https://slack.com/api/chat.postMessage", {
|
||||
method: "POST",
|
||||
headers: this.botHeaders,
|
||||
body: JSON.stringify({
|
||||
channel: channel_id,
|
||||
thread_ts: thread_ts,
|
||||
text: text,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async addReaction(
|
||||
channel_id: string,
|
||||
timestamp: string,
|
||||
reaction: string,
|
||||
): Promise<any> {
|
||||
const response = await fetch("https://slack.com/api/reactions.add", {
|
||||
method: "POST",
|
||||
headers: this.botHeaders,
|
||||
body: JSON.stringify({
|
||||
channel: channel_id,
|
||||
timestamp: timestamp,
|
||||
name: reaction,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getChannelHistory(
|
||||
channel_id: string,
|
||||
limit: number = 10,
|
||||
): Promise<any> {
|
||||
const params = new URLSearchParams({
|
||||
channel: channel_id,
|
||||
limit: limit.toString(),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`https://slack.com/api/conversations.history?${params}`,
|
||||
{ headers: this.botHeaders },
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getThreadReplies(channel_id: string, thread_ts: string): Promise<any> {
|
||||
const params = new URLSearchParams({
|
||||
channel: channel_id,
|
||||
ts: thread_ts,
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`https://slack.com/api/conversations.replies?${params}`,
|
||||
{ headers: this.botHeaders },
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getUsers(limit: number = 100, cursor?: string): Promise<any> {
|
||||
const params = new URLSearchParams({
|
||||
limit: Math.min(limit, 200).toString(),
|
||||
team_id: process.env.SLACK_TEAM_ID!,
|
||||
});
|
||||
|
||||
if (cursor) {
|
||||
params.append("cursor", cursor);
|
||||
}
|
||||
|
||||
const response = await fetch(`https://slack.com/api/users.list?${params}`, {
|
||||
headers: this.botHeaders,
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getUserProfile(user_id: string): Promise<any> {
|
||||
const params = new URLSearchParams({
|
||||
user: user_id,
|
||||
include_labels: "true",
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`https://slack.com/api/users.profile.get?${params}`,
|
||||
{ headers: this.botHeaders },
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const botToken = process.env.SLACK_BOT_TOKEN;
|
||||
const teamId = process.env.SLACK_TEAM_ID;
|
||||
|
||||
if (!botToken || !teamId) {
|
||||
console.error(
|
||||
"Please set SLACK_BOT_TOKEN and SLACK_TEAM_ID environment variables",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.error("Starting Slack MCP Server...");
|
||||
const server = new Server(
|
||||
{
|
||||
name: "Slack MCP Server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const slackClient = new SlackClient(botToken);
|
||||
|
||||
server.setRequestHandler(
|
||||
CallToolRequestSchema,
|
||||
async (request: CallToolRequest) => {
|
||||
console.error("Received CallToolRequest:", request);
|
||||
try {
|
||||
if (!request.params.arguments) {
|
||||
throw new Error("No arguments provided");
|
||||
}
|
||||
|
||||
switch (request.params.name) {
|
||||
case "slack_list_channels": {
|
||||
const args = request.params
|
||||
.arguments as unknown as ListChannelsArgs;
|
||||
const response = await slackClient.getChannels(
|
||||
args.limit,
|
||||
args.cursor,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(response) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "slack_post_message": {
|
||||
const args = request.params.arguments as unknown as PostMessageArgs;
|
||||
if (!args.channel_id || !args.text) {
|
||||
throw new Error(
|
||||
"Missing required arguments: channel_id and text",
|
||||
);
|
||||
}
|
||||
const response = await slackClient.postMessage(
|
||||
args.channel_id,
|
||||
args.text,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(response) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "slack_reply_to_thread": {
|
||||
const args = request.params
|
||||
.arguments as unknown as ReplyToThreadArgs;
|
||||
if (!args.channel_id || !args.thread_ts || !args.text) {
|
||||
throw new Error(
|
||||
"Missing required arguments: channel_id, thread_ts, and text",
|
||||
);
|
||||
}
|
||||
const response = await slackClient.postReply(
|
||||
args.channel_id,
|
||||
args.thread_ts,
|
||||
args.text,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(response) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "slack_add_reaction": {
|
||||
const args = request.params.arguments as unknown as AddReactionArgs;
|
||||
if (!args.channel_id || !args.timestamp || !args.reaction) {
|
||||
throw new Error(
|
||||
"Missing required arguments: channel_id, timestamp, and reaction",
|
||||
);
|
||||
}
|
||||
const response = await slackClient.addReaction(
|
||||
args.channel_id,
|
||||
args.timestamp,
|
||||
args.reaction,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(response) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "slack_get_channel_history": {
|
||||
const args = request.params
|
||||
.arguments as unknown as GetChannelHistoryArgs;
|
||||
if (!args.channel_id) {
|
||||
throw new Error("Missing required argument: channel_id");
|
||||
}
|
||||
const response = await slackClient.getChannelHistory(
|
||||
args.channel_id,
|
||||
args.limit,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(response) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "slack_get_thread_replies": {
|
||||
const args = request.params
|
||||
.arguments as unknown as GetThreadRepliesArgs;
|
||||
if (!args.channel_id || !args.thread_ts) {
|
||||
throw new Error(
|
||||
"Missing required arguments: channel_id and thread_ts",
|
||||
);
|
||||
}
|
||||
const response = await slackClient.getThreadReplies(
|
||||
args.channel_id,
|
||||
args.thread_ts,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(response) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "slack_get_users": {
|
||||
const args = request.params.arguments as unknown as GetUsersArgs;
|
||||
const response = await slackClient.getUsers(
|
||||
args.limit,
|
||||
args.cursor,
|
||||
);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(response) }],
|
||||
};
|
||||
}
|
||||
|
||||
case "slack_get_user_profile": {
|
||||
const args = request.params
|
||||
.arguments as unknown as GetUserProfileArgs;
|
||||
if (!args.user_id) {
|
||||
throw new Error("Missing required argument: user_id");
|
||||
}
|
||||
const response = await slackClient.getUserProfile(args.user_id);
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(response) }],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error executing tool:", error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
console.error("Received ListToolsRequest");
|
||||
return {
|
||||
tools: [
|
||||
listChannelsTool,
|
||||
postMessageTool,
|
||||
replyToThreadTool,
|
||||
addReactionTool,
|
||||
getChannelHistoryTool,
|
||||
getThreadRepliesTool,
|
||||
getUsersTool,
|
||||
getUserProfileTool,
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
console.error("Connecting server to transport...");
|
||||
await server.connect(transport);
|
||||
|
||||
console.error("Slack MCP Server running on stdio");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error in main():", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "@modelcontextprotocol/server-slack",
|
||||
"version": "0.6.2",
|
||||
"description": "MCP server for interacting with Slack",
|
||||
"license": "MIT",
|
||||
"author": "Anthropic, PBC (https://anthropic.com)",
|
||||
"homepage": "https://modelcontextprotocol.io",
|
||||
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-server-slack": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
3.10
|
||||
@@ -1,37 +0,0 @@
|
||||
# Use a Python image with uv pre-installed
|
||||
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS uv
|
||||
|
||||
# Install the project into `/app`
|
||||
WORKDIR /app
|
||||
|
||||
# Enable bytecode compilation
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
# Copy from the cache instead of linking since it's a mounted volume
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Install the project's dependencies using the lockfile and settings
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project --no-dev --no-editable
|
||||
|
||||
# Then, add the rest of the project source code and install it
|
||||
# Installing separately from its dependencies allows optimal layer caching
|
||||
ADD . /app
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-editable
|
||||
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=uv /root/.local /root/.local
|
||||
COPY --from=uv --chown=app:app /app/.venv /app/.venv
|
||||
|
||||
# Place executables in the environment at the front of the path
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# when running the container, add --db-path and a bind mount to the host's db file
|
||||
ENTRYPOINT ["mcp-server-sqlite"]
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
# SQLite MCP Server
|
||||
|
||||
## Overview
|
||||
A Model Context Protocol (MCP) server implementation that provides database interaction and business intelligence capabilities through SQLite. This server enables running SQL queries, analyzing business data, and automatically generating business insight memos.
|
||||
|
||||
## Components
|
||||
|
||||
### Resources
|
||||
The server exposes a single dynamic resource:
|
||||
- `memo://insights`: A continuously updated business insights memo that aggregates discovered insights during analysis
|
||||
- Auto-updates as new insights are discovered via the append-insight tool
|
||||
|
||||
### Prompts
|
||||
The server provides a demonstration prompt:
|
||||
- `mcp-demo`: Interactive prompt that guides users through database operations
|
||||
- Required argument: `topic` - The business domain to analyze
|
||||
- Generates appropriate database schemas and sample data
|
||||
- Guides users through analysis and insight generation
|
||||
- Integrates with the business insights memo
|
||||
|
||||
### Tools
|
||||
The server offers six core tools:
|
||||
|
||||
#### Query Tools
|
||||
- `read_query`
|
||||
- Execute SELECT queries to read data from the database
|
||||
- Input:
|
||||
- `query` (string): The SELECT SQL query to execute
|
||||
- Returns: Query results as array of objects
|
||||
|
||||
- `write_query`
|
||||
- Execute INSERT, UPDATE, or DELETE queries
|
||||
- Input:
|
||||
- `query` (string): The SQL modification query
|
||||
- Returns: `{ affected_rows: number }`
|
||||
|
||||
- `create_table`
|
||||
- Create new tables in the database
|
||||
- Input:
|
||||
- `query` (string): CREATE TABLE SQL statement
|
||||
- Returns: Confirmation of table creation
|
||||
|
||||
#### Schema Tools
|
||||
- `list_tables`
|
||||
- Get a list of all tables in the database
|
||||
- No input required
|
||||
- Returns: Array of table names
|
||||
|
||||
- `describe-table`
|
||||
- View schema information for a specific table
|
||||
- Input:
|
||||
- `table_name` (string): Name of table to describe
|
||||
- Returns: Array of column definitions with names and types
|
||||
|
||||
#### Analysis Tools
|
||||
- `append_insight`
|
||||
- Add new business insights to the memo resource
|
||||
- Input:
|
||||
- `insight` (string): Business insight discovered from data analysis
|
||||
- Returns: Confirmation of insight addition
|
||||
- Triggers update of memo://insights resource
|
||||
|
||||
|
||||
## Usage with Claude Desktop
|
||||
|
||||
### uv
|
||||
|
||||
```bash
|
||||
# Add the server to your claude_desktop_config.json
|
||||
"mcpServers": {
|
||||
"sqlite": {
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"--directory",
|
||||
"parent_of_servers_repo/servers/src/sqlite",
|
||||
"run",
|
||||
"mcp-server-sqlite",
|
||||
"--db-path",
|
||||
"~/test.db"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```json
|
||||
# Add the server to your claude_desktop_config.json
|
||||
"mcpServers": {
|
||||
"sqlite": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-i",
|
||||
"-v",
|
||||
"mcp-test:/mcp",
|
||||
"mcp/sqlite",
|
||||
"--db-path",
|
||||
"/mcp/test.db"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Docker:
|
||||
|
||||
```bash
|
||||
docker build -t mcp/sqlite .
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
|
||||
@@ -1,17 +0,0 @@
|
||||
[project]
|
||||
name = "mcp-server-sqlite"
|
||||
version = "0.6.2"
|
||||
description = "A simple SQLite MCP server"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["mcp>=1.0.0"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = ["pyright>=1.1.389"]
|
||||
|
||||
[project.scripts]
|
||||
mcp-server-sqlite = "mcp_server_sqlite:main"
|
||||
@@ -1,18 +0,0 @@
|
||||
from . import server
|
||||
import asyncio
|
||||
import argparse
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the package."""
|
||||
parser = argparse.ArgumentParser(description='SQLite MCP Server')
|
||||
parser.add_argument('--db-path',
|
||||
default="./sqlite_mcp_server.db",
|
||||
help='Path to SQLite database file')
|
||||
|
||||
args = parser.parse_args()
|
||||
asyncio.run(server.main(args.db_path))
|
||||
|
||||
|
||||
# Optionally expose other important items at package level
|
||||
__all__ = ["main", "server"]
|
||||
@@ -1,382 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import sqlite3
|
||||
import logging
|
||||
from contextlib import closing
|
||||
from pathlib import Path
|
||||
from mcp.server.models import InitializationOptions
|
||||
import mcp.types as types
|
||||
from mcp.server import NotificationOptions, Server
|
||||
import mcp.server.stdio
|
||||
from pydantic import AnyUrl
|
||||
from typing import Any
|
||||
|
||||
# reconfigure UnicodeEncodeError prone default (i.e. windows-1252) to utf-8
|
||||
if sys.platform == "win32" and os.environ.get('PYTHONIOENCODING') is None:
|
||||
sys.stdin.reconfigure(encoding="utf-8")
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
logger = logging.getLogger('mcp_sqlite_server')
|
||||
logger.info("Starting MCP SQLite Server")
|
||||
|
||||
PROMPT_TEMPLATE = """
|
||||
The assistants goal is to walkthrough an informative demo of MCP. To demonstrate the Model Context Protocol (MCP) we will leverage this example server to interact with an SQLite database.
|
||||
It is important that you first explain to the user what is going on. The user has downloaded and installed the SQLite MCP Server and is now ready to use it.
|
||||
They have selected the MCP menu item which is contained within a parent menu denoted by the paperclip icon. Inside this menu they selected an icon that illustrates two electrical plugs connecting. This is the MCP menu.
|
||||
Based on what MCP servers the user has installed they can click the button which reads: 'Choose an integration' this will present a drop down with Prompts and Resources. The user has selected the prompt titled: 'mcp-demo'.
|
||||
This text file is that prompt. The goal of the following instructions is to walk the user through the process of using the 3 core aspects of an MCP server. These are: Prompts, Tools, and Resources.
|
||||
They have already used a prompt and provided a topic. The topic is: {topic}. The user is now ready to begin the demo.
|
||||
Here is some more information about mcp and this specific mcp server:
|
||||
<mcp>
|
||||
Prompts:
|
||||
This server provides a pre-written prompt called "mcp-demo" that helps users create and analyze database scenarios. The prompt accepts a "topic" argument and guides users through creating tables, analyzing data, and generating insights. For example, if a user provides "retail sales" as the topic, the prompt will help create relevant database tables and guide the analysis process. Prompts basically serve as interactive templates that help structure the conversation with the LLM in a useful way.
|
||||
Resources:
|
||||
This server exposes one key resource: "memo://insights", which is a business insights memo that gets automatically updated throughout the analysis process. As users analyze the database and discover insights, the memo resource gets updated in real-time to reflect new findings. Resources act as living documents that provide context to the conversation.
|
||||
Tools:
|
||||
This server provides several SQL-related tools:
|
||||
"read_query": Executes SELECT queries to read data from the database
|
||||
"write_query": Executes INSERT, UPDATE, or DELETE queries to modify data
|
||||
"create_table": Creates new tables in the database
|
||||
"list_tables": Shows all existing tables
|
||||
"describe_table": Shows the schema for a specific table
|
||||
"append_insight": Adds a new business insight to the memo resource
|
||||
</mcp>
|
||||
<demo-instructions>
|
||||
You are an AI assistant tasked with generating a comprehensive business scenario based on a given topic.
|
||||
Your goal is to create a narrative that involves a data-driven business problem, develop a database structure to support it, generate relevant queries, create a dashboard, and provide a final solution.
|
||||
|
||||
At each step you will pause for user input to guide the scenario creation process. Overall ensure the scenario is engaging, informative, and demonstrates the capabilities of the SQLite MCP Server.
|
||||
You should guide the scenario to completion. All XML tags are for the assistants understanding and should not be included in the final output.
|
||||
|
||||
1. The user has chosen the topic: {topic}.
|
||||
|
||||
2. Create a business problem narrative:
|
||||
a. Describe a high-level business situation or problem based on the given topic.
|
||||
b. Include a protagonist (the user) who needs to collect and analyze data from a database.
|
||||
c. Add an external, potentially comedic reason why the data hasn't been prepared yet.
|
||||
d. Mention an approaching deadline and the need to use Claude (you) as a business tool to help.
|
||||
|
||||
3. Setup the data:
|
||||
a. Instead of asking about the data that is required for the scenario, just go ahead and use the tools to create the data. Inform the user you are "Setting up the data".
|
||||
b. Design a set of table schemas that represent the data needed for the business problem.
|
||||
c. Include at least 2-3 tables with appropriate columns and data types.
|
||||
d. Leverage the tools to create the tables in the SQLite database.
|
||||
e. Create INSERT statements to populate each table with relevant synthetic data.
|
||||
f. Ensure the data is diverse and representative of the business problem.
|
||||
g. Include at least 10-15 rows of data for each table.
|
||||
|
||||
4. Pause for user input:
|
||||
a. Summarize to the user what data we have created.
|
||||
b. Present the user with a set of multiple choices for the next steps.
|
||||
c. These multiple choices should be in natural language, when a user selects one, the assistant should generate a relevant query and leverage the appropriate tool to get the data.
|
||||
|
||||
6. Iterate on queries:
|
||||
a. Present 1 additional multiple-choice query options to the user. Its important to not loop too many times as this is a short demo.
|
||||
b. Explain the purpose of each query option.
|
||||
c. Wait for the user to select one of the query options.
|
||||
d. After each query be sure to opine on the results.
|
||||
e. Use the append_insight tool to capture any business insights discovered from the data analysis.
|
||||
|
||||
7. Generate a dashboard:
|
||||
a. Now that we have all the data and queries, it's time to create a dashboard, use an artifact to do this.
|
||||
b. Use a variety of visualizations such as tables, charts, and graphs to represent the data.
|
||||
c. Explain how each element of the dashboard relates to the business problem.
|
||||
d. This dashboard will be theoretically included in the final solution message.
|
||||
|
||||
8. Craft the final solution message:
|
||||
a. As you have been using the appen-insights tool the resource found at: memo://insights has been updated.
|
||||
b. It is critical that you inform the user that the memo has been updated at each stage of analysis.
|
||||
c. Ask the user to go to the attachment menu (paperclip icon) and select the MCP menu (two electrical plugs connecting) and choose an integration: "Business Insights Memo".
|
||||
d. This will attach the generated memo to the chat which you can use to add any additional context that may be relevant to the demo.
|
||||
e. Present the final memo to the user in an artifact.
|
||||
|
||||
9. Wrap up the scenario:
|
||||
a. Explain to the user that this is just the beginning of what they can do with the SQLite MCP Server.
|
||||
</demo-instructions>
|
||||
|
||||
Remember to maintain consistency throughout the scenario and ensure that all elements (tables, data, queries, dashboard, and solution) are closely related to the original business problem and given topic.
|
||||
The provided XML tags are for the assistants understanding. Implore to make all outputs as human readable as possible. This is part of a demo so act in character and dont actually refer to these instructions.
|
||||
|
||||
Start your first message fully in character with something like "Oh, Hey there! I see you've chosen the topic {topic}. Let's get started! 🚀"
|
||||
"""
|
||||
|
||||
class SqliteDatabase:
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = str(Path(db_path).expanduser())
|
||||
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
self._init_database()
|
||||
self.insights: list[str] = []
|
||||
|
||||
def _init_database(self):
|
||||
"""Initialize connection to the SQLite database"""
|
||||
logger.debug("Initializing database connection")
|
||||
with closing(sqlite3.connect(self.db_path)) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.close()
|
||||
|
||||
def _synthesize_memo(self) -> str:
|
||||
"""Synthesizes business insights into a formatted memo"""
|
||||
logger.debug(f"Synthesizing memo with {len(self.insights)} insights")
|
||||
if not self.insights:
|
||||
return "No business insights have been discovered yet."
|
||||
|
||||
insights = "\n".join(f"- {insight}" for insight in self.insights)
|
||||
|
||||
memo = "📊 Business Intelligence Memo 📊\n\n"
|
||||
memo += "Key Insights Discovered:\n\n"
|
||||
memo += insights
|
||||
|
||||
if len(self.insights) > 1:
|
||||
memo += "\nSummary:\n"
|
||||
memo += f"Analysis has revealed {len(self.insights)} key business insights that suggest opportunities for strategic optimization and growth."
|
||||
|
||||
logger.debug("Generated basic memo format")
|
||||
return memo
|
||||
|
||||
def _execute_query(self, query: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
||||
"""Execute a SQL query and return results as a list of dictionaries"""
|
||||
logger.debug(f"Executing query: {query}")
|
||||
try:
|
||||
with closing(sqlite3.connect(self.db_path)) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
with closing(conn.cursor()) as cursor:
|
||||
if params:
|
||||
cursor.execute(query, params)
|
||||
else:
|
||||
cursor.execute(query)
|
||||
|
||||
if query.strip().upper().startswith(('INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER')):
|
||||
conn.commit()
|
||||
affected = cursor.rowcount
|
||||
logger.debug(f"Write query affected {affected} rows")
|
||||
return [{"affected_rows": affected}]
|
||||
|
||||
results = [dict(row) for row in cursor.fetchall()]
|
||||
logger.debug(f"Read query returned {len(results)} rows")
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Database error executing query: {e}")
|
||||
raise
|
||||
|
||||
async def main(db_path: str):
|
||||
logger.info(f"Starting SQLite MCP Server with DB path: {db_path}")
|
||||
|
||||
db = SqliteDatabase(db_path)
|
||||
server = Server("sqlite-manager")
|
||||
|
||||
# Register handlers
|
||||
logger.debug("Registering handlers")
|
||||
|
||||
@server.list_resources()
|
||||
async def handle_list_resources() -> list[types.Resource]:
|
||||
logger.debug("Handling list_resources request")
|
||||
return [
|
||||
types.Resource(
|
||||
uri=AnyUrl("memo://insights"),
|
||||
name="Business Insights Memo",
|
||||
description="A living document of discovered business insights",
|
||||
mimeType="text/plain",
|
||||
)
|
||||
]
|
||||
|
||||
@server.read_resource()
|
||||
async def handle_read_resource(uri: AnyUrl) -> str:
|
||||
logger.debug(f"Handling read_resource request for URI: {uri}")
|
||||
if uri.scheme != "memo":
|
||||
logger.error(f"Unsupported URI scheme: {uri.scheme}")
|
||||
raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
|
||||
|
||||
path = str(uri).replace("memo://", "")
|
||||
if not path or path != "insights":
|
||||
logger.error(f"Unknown resource path: {path}")
|
||||
raise ValueError(f"Unknown resource path: {path}")
|
||||
|
||||
return db._synthesize_memo()
|
||||
|
||||
@server.list_prompts()
|
||||
async def handle_list_prompts() -> list[types.Prompt]:
|
||||
logger.debug("Handling list_prompts request")
|
||||
return [
|
||||
types.Prompt(
|
||||
name="mcp-demo",
|
||||
description="A prompt to seed the database with initial data and demonstrate what you can do with an SQLite MCP Server + Claude",
|
||||
arguments=[
|
||||
types.PromptArgument(
|
||||
name="topic",
|
||||
description="Topic to seed the database with initial data",
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
)
|
||||
]
|
||||
|
||||
@server.get_prompt()
|
||||
async def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult:
|
||||
logger.debug(f"Handling get_prompt request for {name} with args {arguments}")
|
||||
if name != "mcp-demo":
|
||||
logger.error(f"Unknown prompt: {name}")
|
||||
raise ValueError(f"Unknown prompt: {name}")
|
||||
|
||||
if not arguments or "topic" not in arguments:
|
||||
logger.error("Missing required argument: topic")
|
||||
raise ValueError("Missing required argument: topic")
|
||||
|
||||
topic = arguments["topic"]
|
||||
prompt = PROMPT_TEMPLATE.format(topic=topic)
|
||||
|
||||
logger.debug(f"Generated prompt template for topic: {topic}")
|
||||
return types.GetPromptResult(
|
||||
description=f"Demo template for {topic}",
|
||||
messages=[
|
||||
types.PromptMessage(
|
||||
role="user",
|
||||
content=types.TextContent(type="text", text=prompt.strip()),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
@server.list_tools()
|
||||
async def handle_list_tools() -> list[types.Tool]:
|
||||
"""List available tools"""
|
||||
return [
|
||||
types.Tool(
|
||||
name="read_query",
|
||||
description="Execute a SELECT query on the SQLite database",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "SELECT SQL query to execute"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="write_query",
|
||||
description="Execute an INSERT, UPDATE, or DELETE query on the SQLite database",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "SQL query to execute"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="create_table",
|
||||
description="Create a new table in the SQLite database",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "CREATE TABLE SQL statement"},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="list_tables",
|
||||
description="List all tables in the SQLite database",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="describe_table",
|
||||
description="Get the schema information for a specific table",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"table_name": {"type": "string", "description": "Name of the table to describe"},
|
||||
},
|
||||
"required": ["table_name"],
|
||||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="append_insight",
|
||||
description="Add a business insight to the memo",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"insight": {"type": "string", "description": "Business insight discovered from data analysis"},
|
||||
},
|
||||
"required": ["insight"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(
|
||||
name: str, arguments: dict[str, Any] | None
|
||||
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||
"""Handle tool execution requests"""
|
||||
try:
|
||||
if name == "list_tables":
|
||||
results = db._execute_query(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
)
|
||||
return [types.TextContent(type="text", text=str(results))]
|
||||
|
||||
elif name == "describe_table":
|
||||
if not arguments or "table_name" not in arguments:
|
||||
raise ValueError("Missing table_name argument")
|
||||
results = db._execute_query(
|
||||
f"PRAGMA table_info({arguments['table_name']})"
|
||||
)
|
||||
return [types.TextContent(type="text", text=str(results))]
|
||||
|
||||
elif name == "append_insight":
|
||||
if not arguments or "insight" not in arguments:
|
||||
raise ValueError("Missing insight argument")
|
||||
|
||||
db.insights.append(arguments["insight"])
|
||||
_ = db._synthesize_memo()
|
||||
|
||||
# Notify clients that the memo resource has changed
|
||||
await server.request_context.session.send_resource_updated(AnyUrl("memo://insights"))
|
||||
|
||||
return [types.TextContent(type="text", text="Insight added to memo")]
|
||||
|
||||
if not arguments:
|
||||
raise ValueError("Missing arguments")
|
||||
|
||||
if name == "read_query":
|
||||
if not arguments["query"].strip().upper().startswith("SELECT"):
|
||||
raise ValueError("Only SELECT queries are allowed for read_query")
|
||||
results = db._execute_query(arguments["query"])
|
||||
return [types.TextContent(type="text", text=str(results))]
|
||||
|
||||
elif name == "write_query":
|
||||
if arguments["query"].strip().upper().startswith("SELECT"):
|
||||
raise ValueError("SELECT queries are not allowed for write_query")
|
||||
results = db._execute_query(arguments["query"])
|
||||
return [types.TextContent(type="text", text=str(results))]
|
||||
|
||||
elif name == "create_table":
|
||||
if not arguments["query"].strip().upper().startswith("CREATE TABLE"):
|
||||
raise ValueError("Only CREATE TABLE statements are allowed")
|
||||
db._execute_query(arguments["query"])
|
||||
return [types.TextContent(type="text", text="Table created successfully")]
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {name}")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
return [types.TextContent(type="text", text=f"Database error: {str(e)}")]
|
||||
except Exception as e:
|
||||
return [types.TextContent(type="text", text=f"Error: {str(e)}")]
|
||||
|
||||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||
logger.info("Server running with stdio transport")
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name="sqlite",
|
||||
server_version="0.1.0",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
)
|
||||
325
src/sqlite/uv.lock
generated
325
src/sqlite/uv.lock
generated
@@ -1,325 +0,0 @@
|
||||
version = 1
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.6.2.post1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/df/676b7cf674dd1bdc71a64ad393c89879f75e4a0ab8395165b498262ae106/httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", size = 141307 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fb/a19866137577ba60c6d8b69498dc36be479b13ba454f691348ddf428f185/httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc", size = 73551 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx-sse" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-server-sqlite"
|
||||
version = "0.6.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "mcp" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pyright" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "mcp", specifier = ">=1.0.0" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pyright", specifier = ">=1.1.389" }]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/86/a03390cb12cf64e2a8df07c267f3eb8d5035e0f9a04bb20fb79403d2a00e/pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", size = 785401 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/74/da832196702d0c56eb86b75bfa346db9238617e29b0b7ee3b8b4eccfe654/pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e", size = 456364 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.27.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.389"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodeenv" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/4e/9a5ab8745e7606b88c2c7ca223449ac9d82a71fd5e31df47b453f2cb39a1/pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220", size = 21940 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.41.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.32.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/3c/21dba3e7d76138725ef307e3d7ddd29b763119b3aa459d02cc05fefcff75/uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175", size = 77630 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/c1/2d27b0a15826c2b71dcf6e2f5402181ef85acf439617bb2f1453125ce1f3/uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e", size = 63828 },
|
||||
]
|
||||
@@ -45,10 +45,12 @@ Add to your Claude settings:
|
||||
<summary>Using uvx</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"time": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-time"]
|
||||
{
|
||||
"mcpServers": {
|
||||
"time": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -58,10 +60,12 @@ Add to your Claude settings:
|
||||
<summary>Using docker</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"time": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "mcp/time"]
|
||||
{
|
||||
"mcpServers": {
|
||||
"time": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "mcp/time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -71,10 +75,12 @@ Add to your Claude settings:
|
||||
<summary>Using pip installation</summary>
|
||||
|
||||
```json
|
||||
"mcpServers": {
|
||||
"time": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_time"]
|
||||
{
|
||||
"mcpServers": {
|
||||
"time": {
|
||||
"command": "python",
|
||||
"args": ["-m", "mcp_server_time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -110,6 +116,54 @@ Add to your Zed settings.json:
|
||||
```
|
||||
</details>
|
||||
|
||||
### Configure for VS Code
|
||||
|
||||
For quick installation, use one of the one-click install buttons below...
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=time&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-time%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=time&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-time%22%5D%7D&quality=insiders)
|
||||
|
||||
[](https://insiders.vscode.dev/redirect/mcp/install?name=time&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Ftime%22%5D%7D) [](https://insiders.vscode.dev/redirect/mcp/install?name=time&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Ftime%22%5D%7D&quality=insiders)
|
||||
|
||||
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
|
||||
|
||||
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
|
||||
|
||||
> Note that the `mcp` key is needed when using the `mcp.json` file.
|
||||
|
||||
<details>
|
||||
<summary>Using uvx</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"time": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Using Docker</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"servers": {
|
||||
"time": {
|
||||
"command": "docker",
|
||||
"args": ["run", "-i", "--rm", "mcp/time"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
### Customization - System Timezone
|
||||
|
||||
By default, the server automatically detects your system's timezone. You can override this by adding the argument `--local-timezone` to the `args` list in the configuration.
|
||||
|
||||
Reference in New Issue
Block a user