diff --git a/README.md b/README.md
index 12c0b1bd..aa03df7d 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ These servers aim to demonstrate MCP features and the Typescript and Python SDK.
- **[PostgreSQL](src/postgres)** - Read-only database access with schema inspection
- **[Puppeteer](src/puppeteer)** - Browser automation and web scraping
- **[Sentry](src/sentry)** - Retrieving and analyzing issues from Sentry.io
-- **[Sequential Thinking](src/sequential-thinking)** - Dynamic and reflective problem-solving through thought sequences
+- **[Sequential Thinking](src/sequentialthinking)** - Dynamic and reflective problem-solving through thought sequences
- **[Slack](src/slack)** - Channel management and messaging capabilities
- **[Sqlite](src/sqlite)** - Database interaction and business intelligence capabilities
@@ -34,8 +34,11 @@ These servers aim to demonstrate MCP features and the Typescript and Python SDK.
Official integrations are maintained by companies building production ready MCP servers for their platforms.
+-
**[Axiom](https://github.com/axiomhq/mcp-server-axiom)** - Query and analyze your Axiom logs, traces, and all other event data in natural language
+-
**[Browserbase](https://github.com/browserbase/mcp-server-browserbase)** - Automate browser interactions in the cloud (e.g. web navigation, data extraction, form filling, and more)
-
**[Cloudflare](https://github.com/cloudflare/mcp-server-cloudflare)** - Deploy, configure & interrogate your resources on the Cloudflare developer platform (e.g. Workers/KV/R2/D1)
- **[Raygun](https://github.com/MindscapeHQ/mcp-server-raygun)** - Interact with your crash reporting and real using monitoring data on your Raygun account
+- **[Obsidian Markdown Notes](https://github.com/calclavia/mcp-obsidian)** - Read and search through your Obsidian vault or any directory containing Markdown notes
-
**[E2B](https://github.com/e2b-dev/mcp-server)** - Run code in secure sandboxes hosted by [E2B](https://e2b.dev)
- **[Neon](https://github.com/neondatabase/mcp-server-neon)** - Interact with the Neon serverless Postgres platform
-
**[Tinybird](https://github.com/tinybirdco/mcp-tinybird)** - Interact with Tinybird serverless ClickHouse platform
@@ -49,30 +52,42 @@ A growing set of community-developed and maintained servers demonstrates various
> **Note:** Community servers are **untested** and should be used at **your own risk**. They are not affiliated with or endorsed by Anthropic.
- **[MCP Installer](https://github.com/anaisbetts/mcp-installer)** - This server is a server that installs other MCP servers for you.
-- **[Spotify MCP](https://github.com/varunneal/spotify-mcp)** - This MCP allows an LLM to play and use Spotify.
+- **[Spotify](https://github.com/varunneal/spotify-mcp)** - This MCP allows an LLM to play and use Spotify.
- **[Inoyu](https://github.com/sergehuber/inoyu-mcp-unomi-server)** - Interact with an Apache Unomi CDP customer data platform to retrieve and update customer profiles
+- **[Snowflake](https://github.com/datawiz168/mcp-snowflake-service)** - This MCP server enables LLMs to interact with Snowflake databases, allowing for secure and controlled data operations.
- **[MySQL](https://github.com/designcomputer/mysql_mcp_server)** - MySQL database integration with configurable access controls and schema inspection
+- **[MSSQL](https://github.com/aekanun2020/mcp-server/)** - MSSQL database integration with configurable access controls and schema inspection
- **[BigQuery](https://github.com/LucasHild/mcp-server-bigquery)** (by LucasHild) - This server enables LLMs to inspect database schemas and execute queries on BigQuery.
- **[BigQuery](https://github.com/ergut/mcp-bigquery-server)** (by ergut) - Server implementation for Google BigQuery integration that enables direct BigQuery database access and querying capabilities
- **[Todoist](https://github.com/abhiz123/todoist-mcp-server)** - Interact with Todoist to manage your tasks.
+- **[Tavily search](https://github.com/RamXX/mcp-tavily")** - An MCP server for Tavily's search & news API, with explicit site inclusions/exclusions
- **[Linear](https://github.com/jerhadf/linear-mcp-server)** - Allows LLM to interact with Linear's API for project management, including searching, creating, and updating issues.
-- **[Playwright MCP](https://github.com/executeautomation/mcp-playwright)** - This MCP Server will help you run browser automation and webscraping using Playwright
+- **[Playwright](https://github.com/executeautomation/mcp-playwright)** - This MCP Server will help you run browser automation and webscraping using Playwright
- **[AWS](https://github.com/rishikavikondala/mcp-server-aws)** - Perform operations on your AWS resources using an LLM
- **[LlamaCloud](https://github.com/run-llama/mcp-server-llamacloud)** (by marcusschiesser) - Integrate the data stored in a managed index on [LlamaCloud](https://cloud.llamaindex.ai/)
- **[Any Chat Completions](https://github.com/pyroprompts/any-chat-completions-mcp)** - Interact with any OpenAI SDK Compatible Chat Completions API like OpenAI, Perplexity, Groq, xAI and many more.
- **[Windows CLI](https://github.com/SimonB97/win-cli-mcp-server)** - MCP server for secure command-line interactions on Windows systems, enabling controlled access to PowerShell, CMD, and Git Bash shells.
- **[OpenRPC](https://github.com/shanejonas/openrpc-mpc-server)** - Interact with and discover JSON-RPC APIs via [OpenRPC](https://open-rpc.org).
-- **[ChatSum](https://github.com/mcpservers/mcp-server-chatsum)** - Query and Summarize wechat messages with LLM.
+- **[FireCrawl](https://github.com/vrknetha/mcp-server-firecrawl)** - Advanced web scraping with JavaScript rendering, PDF support, and smart rate limiting
+- **[AlphaVantage](https://github.com/calvernaz/alphavantage)** - MCP server for stock market data API [AlphaVantage](https://www.alphavantage.co)
+- **[Docker](https://github.com/ckreiling/mcp-server-docker)** - Integrate with Docker to manage containers, images, volumes, and networks.
+- **[Kubernetes](https://github.com/Flux159/mcp-server-kubernetes)** - Connect to Kubernetes cluster and manage pods, deployments, and services.
+- **[OpenAPI](https://github.com/snaggle-ai/openapi-mcp-server)** - Interact with [OpenAPI](https://www.openapis.org/) APIs.
+- **[Pandoc](https://github.com/vivekVells/mcp-pandoc)** - MCP server for seamless document format conversion using Pandoc, supporting Markdown, HTML, and plain text, with other formats like PDF, csv and docx in development.
+- **[ChatSum](https://github.com/mcpso/mcp-server-chatsum)** - Query and Summarize chat messages with LLM.
+
## 📚 Resources
Additional resources on MCP.
-- **[Awesome MCP Servers by punkpeye](https://github.com/punkpeye/awesome-mcp-servers)** - A curated list of MCP servers by **[Frank Fiegel](https://github.com/punkpeye)**
-- **[Awesome MCP Servers by wong2](https://github.com/wong2/awesome-mcp-servers)** - A curated list of MCP servers by **[wong2](https://github.com/wong2)**
+- **[Awesome MCP Servers by punkpeye](https://github.com/punkpeye/awesome-mcp-servers)** (**[website](https://glama.ai/mcp/servers)**) - A curated list of MCP servers by **[Frank Fiegel](https://github.com/punkpeye)**
+- **[Awesome MCP Servers by wong2](https://github.com/wong2/awesome-mcp-servers)** (**[website](https://mcpservers.org)**) - A curated list of MCP servers by **[wong2](https://github.com/wong2)**
- **[Awesome MCP Servers by appcypher](https://github.com/appcypher/awesome-mcp-servers)** - A curated list of MCP servers by **[Stephen Akinyemi](https://github.com/appcypher)**
+- **[Discord Server](https://glama.ai/mcp/discord)** – A community discord server dedicated to MCP by **[Frank Fiegel](https://github.com/punkpeye)**
- **[mcp-get](https://mcp-get.com)** - Command line tool for installing and managing MCP servers by **[Michael Latman](https://github.com/michaellatman)**
- **[mcp-cli](https://github.com/wong2/mcp-cli)** - A CLI inspector for the Model Context Protocol by **[wong2](https://github.com/wong2)**
+- **[r/mcp](https://www.reddit.com/r/mcp)** – A Reddit community dedicated to MCP by **[Frank Fiegel](https://github.com/punkpeye)**
## 🚀 Getting Started
diff --git a/package-lock.json b/package-lock.json
index b42b4228..fa9ec691 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1523,6 +1523,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/diff": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz",
+ "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==",
+ "dev": true
+ },
"node_modules/@types/express": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
@@ -2258,6 +2264,14 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg=="
},
+ "node_modules/diff": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+ "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/dotenv": {
"version": "16.4.6",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.6.tgz",
@@ -4983,6 +4997,7 @@
}
},
"src/aws-kb-retrieval-server": {
+ "name": "@modelcontextprotocol/server-aws-kb-retrieval",
"version": "0.6.2",
"license": "MIT",
"dependencies": {
@@ -5167,7 +5182,8 @@
"version": "0.6.2",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/sdk": "1.0.1",
+ "@modelcontextprotocol/sdk": "0.5.0",
+ "diff": "^5.1.0",
"glob": "^10.3.10",
"zod-to-json-schema": "^3.23.5"
},
@@ -5175,6 +5191,7 @@
"mcp-server-filesystem": "dist/index.js"
},
"devDependencies": {
+ "@types/diff": "^5.0.9",
"@types/node": "^20.11.0",
"shx": "^0.3.4",
"typescript": "^5.3.3"
@@ -5287,8 +5304,10 @@
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1",
+ "@types/node": "^20.11.0",
"@types/node-fetch": "^2.6.12",
"node-fetch": "^3.3.2",
+ "zod": "^3.22.4",
"zod-to-json-schema": "^3.23.5"
},
"bin": {
@@ -5309,6 +5328,14 @@
"zod": "^3.23.8"
}
},
+ "src/github/node_modules/@types/node": {
+ "version": "20.17.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz",
+ "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
"src/github/node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
diff --git a/src/filesystem/README.md b/src/filesystem/README.md
index c2950cd5..37bc290f 100644
--- a/src/filesystem/README.md
+++ b/src/filesystem/README.md
@@ -36,6 +36,30 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio
- `path` (string): File location
- `content` (string): File content
+- **edit_file**
+ - Make selective edits using advanced pattern matching and formatting
+ - Features:
+ - Line-based and multi-line content matching
+ - Whitespace normalization with indentation preservation
+ - Fuzzy matching with confidence scoring
+ - Multiple simultaneous edits with correct positioning
+ - Indentation style detection and preservation
+ - Git-style diff output with context
+ - Preview changes with dry run mode
+ - Failed match debugging with confidence scores
+ - Inputs:
+ - `path` (string): File to edit
+ - `edits` (array): List of edit operations
+ - `oldText` (string): Text to search for (can be substring)
+ - `newText` (string): Text to replace with
+ - `dryRun` (boolean): Preview changes without applying (default: false)
+ - `options` (object): Optional formatting settings
+ - `preserveIndentation` (boolean): Keep existing indentation (default: true)
+ - `normalizeWhitespace` (boolean): Normalize spaces while preserving structure (default: true)
+ - `partialMatch` (boolean): Enable fuzzy matching (default: true)
+ - Returns detailed diff and match information for dry runs, otherwise applies changes
+ - Best Practice: Always use dryRun first to preview changes before applying them
+
- **create_directory**
- Create new directory or ensure it exists
- Input: `path` (string)
@@ -98,4 +122,4 @@ Add this to your `claude_desktop_config.json`:
## 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 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.
\ No newline at end of file
diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts
index b4c4e92d..23d989d0 100644
--- a/src/filesystem/index.ts
+++ b/src/filesystem/index.ts
@@ -12,6 +12,7 @@ import path from "path";
import os from 'os';
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
+import { diffLines, createTwoFilesPatch } from 'diff';
// Command line argument parsing
const args = process.argv.slice(2);
@@ -106,6 +107,17 @@ const WriteFileArgsSchema = z.object({
content: z.string(),
});
+const EditOperation = z.object({
+ oldText: z.string().describe('Text to search for - must match exactly'),
+ newText: z.string().describe('Text to replace with')
+});
+
+const EditFileArgsSchema = z.object({
+ path: z.string(),
+ edits: z.array(EditOperation),
+ dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
+});
+
const CreateDirectoryArgsSchema = z.object({
path: z.string(),
});
@@ -202,6 +214,104 @@ async function searchFiles(
return results;
}
+// file editing and diffing utilities
+function normalizeLineEndings(text: string): string {
+ return text.replace(/\r\n/g, '\n');
+}
+
+function createUnifiedDiff(originalContent: string, newContent: string, filepath: string = 'file'): string {
+ // Ensure consistent line endings for diff
+ const normalizedOriginal = normalizeLineEndings(originalContent);
+ const normalizedNew = normalizeLineEndings(newContent);
+
+ return createTwoFilesPatch(
+ filepath,
+ filepath,
+ normalizedOriginal,
+ normalizedNew,
+ 'original',
+ 'modified'
+ );
+}
+
+async function applyFileEdits(
+ filePath: string,
+ edits: Array<{oldText: string, newText: string}>,
+ dryRun = false
+): Promise {
+ // Read file content and normalize line endings
+ const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
+
+ // Apply edits sequentially
+ let modifiedContent = content;
+ for (const edit of edits) {
+ const normalizedOld = normalizeLineEndings(edit.oldText);
+ const normalizedNew = normalizeLineEndings(edit.newText);
+
+ // If exact match exists, use it
+ if (modifiedContent.includes(normalizedOld)) {
+ modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
+ continue;
+ }
+
+ // Otherwise, try line-by-line matching with flexibility for whitespace
+ const oldLines = normalizedOld.split('\n');
+ const contentLines = modifiedContent.split('\n');
+ let matchFound = false;
+
+ for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
+ const potentialMatch = contentLines.slice(i, i + oldLines.length);
+
+ // Compare lines with normalized whitespace
+ const isMatch = oldLines.every((oldLine, j) => {
+ const contentLine = potentialMatch[j];
+ return oldLine.trim() === contentLine.trim();
+ });
+
+ if (isMatch) {
+ // Preserve original indentation of first line
+ const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
+ const newLines = normalizedNew.split('\n').map((line, j) => {
+ if (j === 0) return originalIndent + line.trimStart();
+ // For subsequent lines, try to preserve relative indentation
+ const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || '';
+ const newIndent = line.match(/^\s*/)?.[0] || '';
+ if (oldIndent && newIndent) {
+ const relativeIndent = newIndent.length - oldIndent.length;
+ return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();
+ }
+ return line;
+ });
+
+ contentLines.splice(i, oldLines.length, ...newLines);
+ modifiedContent = contentLines.join('\n');
+ matchFound = true;
+ break;
+ }
+ }
+
+ if (!matchFound) {
+ throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
+ }
+ }
+
+ // Create unified diff
+ const diff = createUnifiedDiff(content, modifiedContent, filePath);
+
+ // Format diff with appropriate number of backticks
+ let numBackticks = 3;
+ while (diff.includes('`'.repeat(numBackticks))) {
+ numBackticks++;
+ }
+ const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
+
+ if (!dryRun) {
+ await fs.writeFile(filePath, modifiedContent, 'utf-8');
+ }
+
+ return formattedDiff;
+}
+
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
@@ -233,6 +343,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
"Handles text content with proper encoding. Only works within allowed directories.",
inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput,
},
+ {
+ name: "edit_file",
+ description:
+ "Make line-based edits to a text file. Each edit replaces exact line sequences " +
+ "with new content. Returns a git-style diff showing the changes made. " +
+ "Only works within allowed directories.",
+ inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput,
+ },
{
name: "create_directory",
description:
@@ -346,6 +464,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
}
+ case "edit_file": {
+ const parsed = EditFileArgsSchema.safeParse(args);
+ if (!parsed.success) {
+ throw new Error(`Invalid arguments for edit_file: ${parsed.error}`);
+ }
+ const validPath = await validatePath(parsed.data.path);
+ const result = await applyFileEdits(validPath, parsed.data.edits, parsed.data.dryRun);
+ return {
+ content: [{ type: "text", text: result }],
+ };
+ }
+
case "create_directory": {
const parsed = CreateDirectoryArgsSchema.safeParse(args);
if (!parsed.success) {
diff --git a/src/filesystem/package.json b/src/filesystem/package.json
index e6e2f43d..bb9797ce 100644
--- a/src/filesystem/package.json
+++ b/src/filesystem/package.json
@@ -19,11 +19,13 @@
"watch": "tsc --watch"
},
"dependencies": {
- "@modelcontextprotocol/sdk": "1.0.1",
+ "@modelcontextprotocol/sdk": "0.5.0",
+ "diff": "^5.1.0",
"glob": "^10.3.10",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
+ "@types/diff": "^5.0.9",
"@types/node": "^20.11.0",
"shx": "^0.3.4",
"typescript": "^5.3.3"
diff --git a/src/git/README.md b/src/git/README.md
index caf01294..e7956f1f 100644
--- a/src/git/README.md
+++ b/src/git/README.md
@@ -26,34 +26,41 @@ Please note that mcp-server-git is currently in early development. The functiona
- `repo_path` (string): Path to Git repository
- Returns: Diff output of staged changes
-4. `git_commit`
+4. `git_diff`
+ - Shows differences between branches or commits
+ - Inputs:
+ - `repo_path` (string): Path to Git repository
+ - `target` (string): Target branch or commit to compare with
+ - Returns: Diff output comparing current state with target
+
+5. `git_commit`
- Records changes to the repository
- Inputs:
- `repo_path` (string): Path to Git repository
- `message` (string): Commit message
- Returns: Confirmation with new commit hash
-5. `git_add`
+6. `git_add`
- Adds file contents to the staging area
- Inputs:
- `repo_path` (string): Path to Git repository
- `files` (string[]): Array of file paths to stage
- Returns: Confirmation of staged files
-6. `git_reset`
+7. `git_reset`
- Unstages all staged changes
- Input:
- `repo_path` (string): Path to Git repository
- Returns: Confirmation of reset operation
-7. `git_log`
+8. `git_log`
- Shows the commit logs
- Inputs:
- `repo_path` (string): Path to Git repository
- `max_count` (number, optional): Maximum number of commits to show (default: 10)
- Returns: Array of commit entries with hash, author, date, and message
-8. `git_create_branch`
+9. `git_create_branch`
- Creates a new branch
- Inputs:
- `repo_path` (string): Path to Git repository
diff --git a/src/git/src/mcp_server_git/server.py b/src/git/src/mcp_server_git/server.py
index 02fae584..fd802eaa 100644
--- a/src/git/src/mcp_server_git/server.py
+++ b/src/git/src/mcp_server_git/server.py
@@ -24,6 +24,10 @@ class GitDiffUnstaged(BaseModel):
class GitDiffStaged(BaseModel):
repo_path: str
+class GitDiff(BaseModel):
+ repo_path: str
+ target: str
+
class GitCommit(BaseModel):
repo_path: str
message: str
@@ -48,6 +52,7 @@ class GitTools(str, Enum):
STATUS = "git_status"
DIFF_UNSTAGED = "git_diff_unstaged"
DIFF_STAGED = "git_diff_staged"
+ DIFF = "git_diff"
COMMIT = "git_commit"
ADD = "git_add"
RESET = "git_reset"
@@ -63,6 +68,9 @@ def git_diff_unstaged(repo: git.Repo) -> str:
def git_diff_staged(repo: git.Repo) -> str:
return repo.git.diff("--cached")
+def git_diff(repo: git.Repo, target: str) -> str:
+ return repo.git.diff(target)
+
def git_commit(repo: git.Repo, message: str) -> str:
commit = repo.index.commit(message)
return f"Changes committed successfully with hash {commit.hexsha}"
@@ -127,6 +135,11 @@ async def serve(repository: Path | None) -> None:
description="Shows changes that are staged for commit",
inputSchema=GitDiffStaged.schema(),
),
+ Tool(
+ name=GitTools.DIFF,
+ description="Shows differences between branches or commits",
+ inputSchema=GitDiff.schema(),
+ ),
Tool(
name=GitTools.COMMIT,
description="Records changes to the repository",
@@ -210,6 +223,13 @@ async def serve(repository: Path | None) -> None:
text=f"Staged changes:\n{diff}"
)]
+ case GitTools.DIFF:
+ diff = git_diff(repo, arguments["target"])
+ return [TextContent(
+ type="text",
+ text=f"Diff with {arguments['target']}:\n{diff}"
+ )]
+
case GitTools.COMMIT:
result = git_commit(repo, arguments["message"])
return [TextContent(
diff --git a/src/github/README.md b/src/github/README.md
index cfd268a8..1898f9e1 100644
--- a/src/github/README.md
+++ b/src/github/README.md
@@ -1,6 +1,6 @@
# GitHub MCP Server
-MCP Server for the GitHub API, enabling file operations, repository management, and more.
+MCP Server for the GitHub API, enabling file operations, repository management, search functionality, and more.
### Features
@@ -8,6 +8,7 @@ MCP Server for the GitHub API, enabling file operations, repository management,
- **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
@@ -102,6 +103,115 @@ MCP Server for the GitHub API, enabling file operations, repository management,
- `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
+
+## 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
diff --git a/src/github/index.ts b/src/github/index.ts
index 800bce83..3759e8b5 100644
--- a/src/github/index.ts
+++ b/src/github/index.ts
@@ -1,5 +1,4 @@
#!/usr/bin/env node
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
@@ -7,53 +6,72 @@ import {
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";
-import {
- GitHubForkSchema,
- GitHubReferenceSchema,
- GitHubRepositorySchema,
- GitHubIssueSchema,
- GitHubPullRequestSchema,
- GitHubContentSchema,
- GitHubCreateUpdateFileResponseSchema,
- GitHubSearchResponseSchema,
- GitHubTreeSchema,
- GitHubCommitSchema,
- CreateRepositoryOptionsSchema,
- CreateIssueOptionsSchema,
- CreatePullRequestOptionsSchema,
- CreateBranchOptionsSchema,
- type GitHubFork,
- type GitHubReference,
- type GitHubRepository,
- type GitHubIssue,
- type GitHubPullRequest,
- type GitHubContent,
- type GitHubCreateUpdateFileResponse,
- type GitHubSearchResponse,
- type GitHubTree,
- type GitHubCommit,
- type FileOperation,
- CreateOrUpdateFileSchema,
- SearchRepositoriesSchema,
- CreateRepositorySchema,
- GetFileContentsSchema,
- PushFilesSchema,
- CreateIssueSchema,
- CreatePullRequestSchema,
- ForkRepositorySchema,
- CreateBranchSchema
-} from './schemas.js';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
+import {
+ CreateBranchOptionsSchema,
+ CreateBranchSchema,
+ CreateIssueOptionsSchema,
+ CreateIssueSchema,
+ CreateOrUpdateFileSchema,
+ CreatePullRequestOptionsSchema,
+ CreatePullRequestSchema,
+ CreateRepositoryOptionsSchema,
+ CreateRepositorySchema,
+ ForkRepositorySchema,
+ GetFileContentsSchema,
+ GetIssueSchema,
+ GitHubCommitSchema,
+ GitHubContentSchema,
+ GitHubCreateUpdateFileResponseSchema,
+ GitHubForkSchema,
+ GitHubIssueSchema,
+ GitHubListCommits,
+ GitHubListCommitsSchema,
+ GitHubPullRequestSchema,
+ GitHubReferenceSchema,
+ GitHubRepositorySchema,
+ GitHubSearchResponseSchema,
+ GitHubTreeSchema,
+ IssueCommentSchema,
+ ListCommitsSchema,
+ ListIssuesOptionsSchema,
+ PushFilesSchema,
+ SearchCodeResponseSchema,
+ SearchCodeSchema,
+ SearchIssuesResponseSchema,
+ SearchIssuesSchema,
+ SearchRepositoriesSchema,
+ SearchUsersResponseSchema,
+ SearchUsersSchema,
+ UpdateIssueOptionsSchema,
+ type FileOperation,
+ type GitHubCommit,
+ type GitHubContent,
+ type GitHubCreateUpdateFileResponse,
+ type GitHubFork,
+ type GitHubIssue,
+ type GitHubPullRequest,
+ type GitHubReference,
+ type GitHubRepository,
+ type GitHubSearchResponse,
+ type GitHubTree,
+ type SearchCodeResponse,
+ type SearchIssuesResponse,
+ type SearchUsersResponse
+} from './schemas.js';
-const server = new Server({
- name: "github-mcp-server",
- version: "0.1.0",
-}, {
- capabilities: {
- tools: {}
+const server = new Server(
+ {
+ name: "github-mcp-server",
+ version: "0.1.0",
+ },
+ {
+ capabilities: {
+ tools: {},
+ },
}
-});
+);
const GITHUB_PERSONAL_ACCESS_TOKEN = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
@@ -67,17 +85,17 @@ async function forkRepository(
repo: string,
organization?: string
): Promise {
- const url = organization
+ const url = organization
? `https://api.github.com/repos/${owner}/${repo}/forks?organization=${organization}`
: `https://api.github.com/repos/${owner}/${repo}/forks`;
const response = await fetch(url, {
method: "POST",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "github-mcp-server"
- }
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
});
if (!response.ok) {
@@ -93,21 +111,21 @@ async function createBranch(
options: z.infer
): Promise {
const fullRef = `refs/heads/${options.ref}`;
-
+
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/git/refs`,
{
method: "POST",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
body: JSON.stringify({
ref: fullRef,
- sha: options.sha
- })
+ sha: options.sha,
+ }),
}
);
@@ -126,10 +144,10 @@ async function getDefaultBranchSHA(
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/main`,
{
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "github-mcp-server"
- }
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
}
);
@@ -138,15 +156,17 @@ async function getDefaultBranchSHA(
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/master`,
{
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "github-mcp-server"
- }
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
}
);
if (!masterResponse.ok) {
- throw new Error("Could not find default branch (tried 'main' and 'master')");
+ throw new Error(
+ "Could not find default branch (tried 'main' and 'master')"
+ );
}
const data = GitHubReferenceSchema.parse(await masterResponse.json());
@@ -170,10 +190,10 @@ async function getFileContents(
const response = await fetch(url, {
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "github-mcp-server"
- }
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
});
if (!response.ok) {
@@ -184,7 +204,7 @@ async function getFileContents(
// If it's a file, decode the content
if (!Array.isArray(data) && data.content) {
- data.content = Buffer.from(data.content, 'base64').toString('utf8');
+ data.content = Buffer.from(data.content, "base64").toString("utf8");
}
return data;
@@ -200,12 +220,12 @@ async function createIssue(
{
method: "POST",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
- body: JSON.stringify(options)
+ body: JSON.stringify(options),
}
);
@@ -226,12 +246,12 @@ async function createPullRequest(
{
method: "POST",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
- body: JSON.stringify(options)
+ body: JSON.stringify(options),
}
);
@@ -251,7 +271,7 @@ async function createOrUpdateFile(
branch: string,
sha?: string
): Promise {
- const encodedContent = Buffer.from(content).toString('base64');
+ const encodedContent = Buffer.from(content).toString("base64");
let currentSha = sha;
if (!currentSha) {
@@ -261,28 +281,30 @@ async function createOrUpdateFile(
currentSha = existingFile.sha;
}
} catch (error) {
- console.error('Note: File does not exist in branch, will create new file');
+ 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 } : {})
+ ...(currentSha ? { sha: currentSha } : {}),
};
const response = await fetch(url, {
method: "PUT",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
- body: JSON.stringify(body)
+ body: JSON.stringify(body),
});
if (!response.ok) {
@@ -298,11 +320,11 @@ async function createTree(
files: FileOperation[],
baseTree?: string
): Promise {
- const tree = files.map(file => ({
+ const tree = files.map((file) => ({
path: file.path,
- mode: '100644' as const,
- type: 'blob' as const,
- content: file.content
+ mode: "100644" as const,
+ type: "blob" as const,
+ content: file.content,
}));
const response = await fetch(
@@ -310,15 +332,15 @@ async function createTree(
{
method: "POST",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
body: JSON.stringify({
tree,
- base_tree: baseTree
- })
+ base_tree: baseTree,
+ }),
}
);
@@ -341,16 +363,16 @@ async function createCommit(
{
method: "POST",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
body: JSON.stringify({
message,
tree,
- parents
- })
+ parents,
+ }),
}
);
@@ -372,15 +394,15 @@ async function updateReference(
{
method: "PATCH",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
body: JSON.stringify({
sha,
- force: true
- })
+ force: true,
+ }),
}
);
@@ -402,10 +424,10 @@ async function pushFiles(
`https://api.github.com/repos/${owner}/${repo}/git/refs/heads/${branch}`,
{
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "github-mcp-server"
- }
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
}
);
@@ -417,7 +439,9 @@ async function pushFiles(
const commitSha = ref.object.sha;
const tree = await createTree(owner, repo, files, commitSha);
- const commit = await createCommit(owner, repo, message, tree.sha, [commitSha]);
+ const commit = await createCommit(owner, repo, message, tree.sha, [
+ commitSha,
+ ]);
return await updateReference(owner, repo, `heads/${branch}`, commit.sha);
}
@@ -433,10 +457,10 @@ async function searchRepositories(
const response = await fetch(url.toString(), {
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "github-mcp-server"
- }
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
});
if (!response.ok) {
@@ -452,12 +476,12 @@ async function createRepository(
const response = await fetch("https://api.github.com/user/repos", {
method: "POST",
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
"User-Agent": "github-mcp-server",
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
- body: JSON.stringify(options)
+ body: JSON.stringify(options),
});
if (!response.ok) {
@@ -467,55 +491,323 @@ async function createRepository(
return GitHubRepositorySchema.parse(await response.json());
}
+async function listCommits(
+ owner: string,
+ repo: string,
+ page: number = 1,
+ perPage: number = 30,
+ sha?: string,
+): Promise {
+ const url = new URL(`https://api.github.com/repos/${owner}/${repo}/commits`);
+ url.searchParams.append("page", page.toString());
+ url.searchParams.append("per_page", perPage.toString());
+ if (sha) {
+ url.searchParams.append("sha", sha);
+ }
+
+ const response = await fetch(
+ url.toString(),
+ {
+ method: "GET",
+ headers: {
+ "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ "Accept": "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ "Content-Type": "application/json"
+ },
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.statusText}`);
+ }
+
+ return GitHubListCommitsSchema.parse(await response.json());
+}
+
+async function listIssues(
+ owner: string,
+ repo: string,
+ options: Omit, 'owner' | 'repo'>
+): Promise {
+ const url = new URL(`https://api.github.com/repos/${owner}/${repo}/issues`);
+
+ // Add query parameters
+ if (options.state) url.searchParams.append('state', options.state);
+ if (options.labels) url.searchParams.append('labels', options.labels.join(','));
+ if (options.sort) url.searchParams.append('sort', options.sort);
+ if (options.direction) url.searchParams.append('direction', options.direction);
+ if (options.since) url.searchParams.append('since', options.since);
+ if (options.page) url.searchParams.append('page', options.page.toString());
+ if (options.per_page) url.searchParams.append('per_page', options.per_page.toString());
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ "Accept": "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server"
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.statusText}`);
+ }
+
+ return z.array(GitHubIssueSchema).parse(await response.json());
+}
+
+async function updateIssue(
+ owner: string,
+ repo: string,
+ issueNumber: number,
+ options: Omit, 'owner' | 'repo' | 'issue_number'>
+): Promise {
+ const response = await fetch(
+ `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
+ {
+ method: "PATCH",
+ headers: {
+ "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ "Accept": "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ title: options.title,
+ body: options.body,
+ state: options.state,
+ labels: options.labels,
+ assignees: options.assignees,
+ milestone: options.milestone
+ })
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.statusText}`);
+ }
+
+ return GitHubIssueSchema.parse(await response.json());
+}
+
+async function addIssueComment(
+ owner: string,
+ repo: string,
+ issueNumber: number,
+ body: string
+): Promise> {
+ const response = await fetch(
+ `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
+ {
+ method: "POST",
+ headers: {
+ "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ "Accept": "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ body })
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.statusText}`);
+ }
+
+ return IssueCommentSchema.parse(await response.json());
+}
+
+async function searchCode(
+ params: z.infer
+): Promise {
+ const url = new URL("https://api.github.com/search/code");
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined && value !== null) {
+ url.searchParams.append(key, value.toString());
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.statusText}`);
+ }
+
+ return SearchCodeResponseSchema.parse(await response.json());
+}
+
+async function searchIssues(
+ params: z.infer
+): Promise {
+ const url = new URL("https://api.github.com/search/issues");
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined && value !== null) {
+ url.searchParams.append(key, value.toString());
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.statusText}`);
+ }
+
+ return SearchIssuesResponseSchema.parse(await response.json());
+}
+
+async function searchUsers(
+ params: z.infer
+): Promise {
+ const url = new URL("https://api.github.com/search/users");
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined && value !== null) {
+ url.searchParams.append(key, value.toString());
+ }
+ });
+
+ const response = await fetch(url.toString(), {
+ headers: {
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.statusText}`);
+ }
+
+ return SearchUsersResponseSchema.parse(await response.json());
+}
+
+async function getIssue(
+ owner: string,
+ repo: string,
+ issueNumber: number
+): Promise {
+ const response = await fetch(
+ `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
+ {
+ headers: {
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
+ }
+);
+
+ if (!response.ok) {
+ throw new Error(`Github API error: ${response.statusText}`);
+ }
+
+ return GitHubIssueSchema.parse(await response.json());
+}
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create_or_update_file",
description: "Create or update a single file in a GitHub repository",
- inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema)
+ inputSchema: zodToJsonSchema(CreateOrUpdateFileSchema),
},
{
name: "search_repositories",
description: "Search for GitHub repositories",
- inputSchema: zodToJsonSchema(SearchRepositoriesSchema)
+ inputSchema: zodToJsonSchema(SearchRepositoriesSchema),
},
{
name: "create_repository",
description: "Create a new GitHub repository in your account",
- inputSchema: zodToJsonSchema(CreateRepositorySchema)
+ inputSchema: zodToJsonSchema(CreateRepositorySchema),
},
{
name: "get_file_contents",
- description: "Get the contents of a file or directory from a GitHub repository",
- inputSchema: zodToJsonSchema(GetFileContentsSchema)
+ description:
+ "Get the contents of a file or directory from a GitHub repository",
+ inputSchema: zodToJsonSchema(GetFileContentsSchema),
},
{
name: "push_files",
- description: "Push multiple files to a GitHub repository in a single commit",
- inputSchema: zodToJsonSchema(PushFilesSchema)
+ description:
+ "Push multiple files to a GitHub repository in a single commit",
+ inputSchema: zodToJsonSchema(PushFilesSchema),
},
{
name: "create_issue",
description: "Create a new issue in a GitHub repository",
- inputSchema: zodToJsonSchema(CreateIssueSchema)
+ inputSchema: zodToJsonSchema(CreateIssueSchema),
},
{
name: "create_pull_request",
description: "Create a new pull request in a GitHub repository",
- inputSchema: zodToJsonSchema(CreatePullRequestSchema)
+ inputSchema: zodToJsonSchema(CreatePullRequestSchema),
},
{
name: "fork_repository",
- description: "Fork a GitHub repository to your account or specified organization",
- inputSchema: zodToJsonSchema(ForkRepositorySchema)
+ description:
+ "Fork a GitHub repository to your account or specified organization",
+ inputSchema: zodToJsonSchema(ForkRepositorySchema),
},
{
name: "create_branch",
description: "Create a new branch in a GitHub repository",
- inputSchema: zodToJsonSchema(CreateBranchSchema)
+ inputSchema: zodToJsonSchema(CreateBranchSchema),
+ },
+ {
+ name: "list_commits",
+ description: "Get list of commits of a branch in a GitHub repository",
+ inputSchema: zodToJsonSchema(ListCommitsSchema)
+ },
+ {
+ name: "list_issues",
+ description: "List issues in a GitHub repository with filtering options",
+ inputSchema: zodToJsonSchema(ListIssuesOptionsSchema)
+ },
+ {
+ name: "update_issue",
+ description: "Update an existing issue in a GitHub repository",
+ inputSchema: zodToJsonSchema(UpdateIssueOptionsSchema)
+ },
+ {
+ name: "add_issue_comment",
+ description: "Add a comment to an existing issue",
+ inputSchema: zodToJsonSchema(IssueCommentSchema)
+ },
+ {
+ name: "search_code",
+ description: "Search for code across GitHub repositories",
+ inputSchema: zodToJsonSchema(SearchCodeSchema),
+ },
+ {
+ name: "search_issues",
+ description:
+ "Search for issues and pull requests across GitHub repositories",
+ inputSchema: zodToJsonSchema(SearchIssuesSchema),
+ },
+ {
+ name: "search_users",
+ description: "Search for users on GitHub",
+ inputSchema: zodToJsonSchema(SearchUsersSchema),
+ },
+ {
+ name: "get_issue",
+ description: "Get details of a specific issue in a GitHub repository.",
+ inputSchema: zodToJsonSchema(GetIssueSchema)
}
- ]
+ ],
};
});
@@ -528,8 +820,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "fork_repository": {
const args = ForkRepositorySchema.parse(request.params.arguments);
- const fork = await forkRepository(args.owner, args.repo, args.organization);
- return { content: [{ type: "text", text: JSON.stringify(fork, null, 2) }] };
+ const fork = await forkRepository(
+ args.owner,
+ args.repo,
+ args.organization
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(fork, null, 2) }],
+ };
}
case "create_branch": {
@@ -540,10 +838,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
`https://api.github.com/repos/${args.owner}/${args.repo}/git/refs/heads/${args.from_branch}`,
{
headers: {
- "Authorization": `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
- "Accept": "application/vnd.github.v3+json",
- "User-Agent": "github-mcp-server"
- }
+ Authorization: `token ${GITHUB_PERSONAL_ACCESS_TOKEN}`,
+ Accept: "application/vnd.github.v3+json",
+ "User-Agent": "github-mcp-server",
+ },
}
);
@@ -559,28 +857,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const branch = await createBranch(args.owner, args.repo, {
ref: args.branch,
- sha
+ sha,
});
- return { content: [{ type: "text", text: JSON.stringify(branch, null, 2) }] };
+ return {
+ content: [{ type: "text", text: JSON.stringify(branch, null, 2) }],
+ };
}
case "search_repositories": {
const args = SearchRepositoriesSchema.parse(request.params.arguments);
- const results = await searchRepositories(args.query, args.page, args.perPage);
- return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
+ const results = await searchRepositories(
+ args.query,
+ args.page,
+ args.perPage
+ );
+ 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) }] };
+ 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.owner, args.repo, args.path, args.branch);
- return { content: [{ type: "text", text: JSON.stringify(contents, null, 2) }] };
+ const contents = await getFileContents(
+ args.owner,
+ args.repo,
+ args.path,
+ args.branch
+ );
+ return {
+ content: [{ type: "text", text: JSON.stringify(contents, null, 2) }],
+ };
}
case "create_or_update_file": {
@@ -594,7 +911,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
args.branch,
args.sha
);
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
+ return {
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
+ };
}
case "push_files": {
@@ -606,21 +925,90 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
args.files,
args.message
);
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
+ return {
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
+ };
}
case "create_issue": {
const args = CreateIssueSchema.parse(request.params.arguments);
const { owner, repo, ...options } = args;
const issue = await createIssue(owner, repo, options);
- return { content: [{ type: "text", text: JSON.stringify(issue, null, 2) }] };
+ return {
+ content: [{ type: "text", text: JSON.stringify(issue, null, 2) }],
+ };
}
case "create_pull_request": {
const args = CreatePullRequestSchema.parse(request.params.arguments);
const { owner, repo, ...options } = args;
const pullRequest = await createPullRequest(owner, repo, options);
- return { content: [{ type: "text", text: JSON.stringify(pullRequest, null, 2) }] };
+ return {
+ content: [
+ { type: "text", text: JSON.stringify(pullRequest, null, 2) },
+ ],
+ };
+ }
+
+ case "search_code": {
+ const args = SearchCodeSchema.parse(request.params.arguments);
+ const results = await searchCode(args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
+ };
+ }
+
+ case "search_issues": {
+ const args = SearchIssuesSchema.parse(request.params.arguments);
+ const results = await searchIssues(args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
+ };
+ }
+
+ case "search_users": {
+ const args = SearchUsersSchema.parse(request.params.arguments);
+ const results = await searchUsers(args);
+ return {
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
+ };
+ }
+
+ case "list_issues": {
+ const args = ListIssuesOptionsSchema.parse(request.params.arguments);
+ const { owner, repo, ...options } = args;
+ const issues = await listIssues(owner, repo, options);
+ return { toolResult: issues };
+ }
+
+ case "update_issue": {
+ const args = UpdateIssueOptionsSchema.parse(request.params.arguments);
+ const { owner, repo, issue_number, ...options } = args;
+ const issue = await updateIssue(owner, repo, issue_number, options);
+ return { toolResult: issue };
+ }
+
+ case "add_issue_comment": {
+ const args = IssueCommentSchema.parse(request.params.arguments);
+ const { owner, repo, issue_number, body } = args;
+ const comment = await addIssueComment(owner, repo, issue_number, body);
+ return { toolResult: comment };
+ }
+
+ case "list_commits": {
+ const args = ListCommitsSchema.parse(request.params.arguments);
+ const results = await 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 = z.object({
+ owner: z.string(),
+ repo: z.string(),
+ issue_number: z.number()
+ }).parse(request.params.arguments);
+ const issue = await getIssue(args.owner, args.repo, args.issue_number);
+ return { toolResult: issue };
}
default:
@@ -628,7 +1016,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
} catch (error) {
if (error instanceof z.ZodError) {
- throw new Error(`Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`);
+ throw new Error(
+ `Invalid arguments: ${error.errors
+ .map(
+ (e: z.ZodError["errors"][number]) =>
+ `${e.path.join(".")}: ${e.message}`
+ )
+ .join(", ")}`
+ );
}
throw error;
}
@@ -643,4 +1038,4 @@ async function runServer() {
runServer().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
-});
\ No newline at end of file
+});
diff --git a/src/github/package.json b/src/github/package.json
index a9dc0b33..0fc2aaeb 100644
--- a/src/github/package.json
+++ b/src/github/package.json
@@ -20,8 +20,10 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1",
+ "@types/node": "^20.11.0",
"@types/node-fetch": "^2.6.12",
"node-fetch": "^3.3.2",
+ "zod": "^3.22.4",
"zod-to-json-schema": "^3.23.5"
},
"devDependencies": {
diff --git a/src/github/schemas.ts b/src/github/schemas.ts
index 213458eb..cefdc1d1 100644
--- a/src/github/schemas.ts
+++ b/src/github/schemas.ts
@@ -1,10 +1,10 @@
-import { z } from 'zod';
+import { z } from "zod";
// Base schemas for common types
export const GitHubAuthorSchema = z.object({
name: z.string(),
email: z.string(),
- date: z.string()
+ date: z.string(),
});
// Repository related schemas
@@ -15,7 +15,7 @@ export const GitHubOwnerSchema = z.object({
avatar_url: z.string(),
url: z.string(),
html_url: z.string(),
- type: z.string()
+ type: z.string(),
});
export const GitHubRepositorySchema = z.object({
@@ -35,7 +35,7 @@ export const GitHubRepositorySchema = z.object({
git_url: z.string(),
ssh_url: z.string(),
clone_url: z.string(),
- default_branch: z.string()
+ default_branch: z.string(),
});
// File content schemas
@@ -50,7 +50,7 @@ export const GitHubFileContentSchema = z.object({
url: z.string(),
git_url: z.string(),
html_url: z.string(),
- download_url: z.string()
+ download_url: z.string(),
});
export const GitHubDirectoryContentSchema = z.object({
@@ -62,37 +62,56 @@ export const GitHubDirectoryContentSchema = z.object({
url: z.string(),
git_url: z.string(),
html_url: z.string(),
- download_url: z.string().nullable()
+ download_url: z.string().nullable(),
});
export const GitHubContentSchema = z.union([
GitHubFileContentSchema,
- z.array(GitHubDirectoryContentSchema)
+ z.array(GitHubDirectoryContentSchema),
]);
// Operation schemas
export const FileOperationSchema = z.object({
path: z.string(),
- content: z.string()
+ content: z.string(),
});
// Tree and commit schemas
export const GitHubTreeEntrySchema = z.object({
path: z.string(),
- mode: z.enum(['100644', '100755', '040000', '160000', '120000']),
- type: z.enum(['blob', 'tree', 'commit']),
+ mode: z.enum(["100644", "100755", "040000", "160000", "120000"]),
+ type: z.enum(["blob", "tree", "commit"]),
size: z.number().optional(),
sha: z.string(),
- url: z.string()
+ url: z.string(),
});
export const GitHubTreeSchema = z.object({
sha: z.string(),
url: z.string(),
tree: z.array(GitHubTreeEntrySchema),
- truncated: z.boolean()
+ truncated: z.boolean(),
});
+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 GitHubCommitSchema = z.object({
sha: z.string(),
node_id: z.string(),
@@ -102,12 +121,14 @@ export const GitHubCommitSchema = z.object({
message: z.string(),
tree: z.object({
sha: z.string(),
- url: z.string()
+ url: z.string(),
}),
- parents: z.array(z.object({
- sha: z.string(),
- url: z.string()
- }))
+ parents: z.array(
+ z.object({
+ sha: z.string(),
+ url: z.string(),
+ })
+ ),
});
// Reference schema
@@ -118,8 +139,8 @@ export const GitHubReferenceSchema = z.object({
object: z.object({
sha: z.string(),
type: z.string(),
- url: z.string()
- })
+ url: z.string(),
+ }),
});
// Input schemas for operations
@@ -127,7 +148,7 @@ export const CreateRepositoryOptionsSchema = z.object({
name: z.string(),
description: z.string().optional(),
private: z.boolean().optional(),
- auto_init: z.boolean().optional()
+ auto_init: z.boolean().optional(),
});
export const CreateIssueOptionsSchema = z.object({
@@ -135,7 +156,7 @@ export const CreateIssueOptionsSchema = z.object({
body: z.string().optional(),
assignees: z.array(z.string()).optional(),
milestone: z.number().optional(),
- labels: z.array(z.string()).optional()
+ labels: z.array(z.string()).optional(),
});
export const CreatePullRequestOptionsSchema = z.object({
@@ -144,12 +165,12 @@ export const CreatePullRequestOptionsSchema = z.object({
head: z.string(),
base: z.string(),
maintainer_can_modify: z.boolean().optional(),
- draft: z.boolean().optional()
+ draft: z.boolean().optional(),
});
export const CreateBranchOptionsSchema = z.object({
ref: z.string(),
- sha: z.string()
+ sha: z.string(),
});
// Response schemas for operations
@@ -164,21 +185,23 @@ export const GitHubCreateUpdateFileResponseSchema = z.object({
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()
- }))
- })
+ }),
+ parents: z.array(
+ z.object({
+ sha: z.string(),
+ url: z.string(),
+ html_url: z.string(),
+ })
+ ),
+ }),
});
export const GitHubSearchResponseSchema = z.object({
total_count: z.number(),
incomplete_results: z.boolean(),
- items: z.array(GitHubRepositorySchema)
+ items: z.array(GitHubRepositorySchema),
});
// Fork related schemas
@@ -188,14 +211,14 @@ export const GitHubForkParentSchema = z.object({
owner: z.object({
login: z.string(),
id: z.number(),
- avatar_url: z.string()
+ avatar_url: z.string(),
}),
- html_url: z.string()
+ html_url: z.string(),
});
export const GitHubForkSchema = GitHubRepositorySchema.extend({
parent: GitHubForkParentSchema,
- source: GitHubForkParentSchema
+ source: GitHubForkParentSchema,
});
// Issue related schemas
@@ -206,7 +229,7 @@ export const GitHubLabelSchema = z.object({
name: z.string(),
color: z.string(),
default: z.boolean(),
- description: z.string().optional()
+ description: z.string().optional(),
});
export const GitHubIssueAssigneeSchema = z.object({
@@ -214,7 +237,7 @@ export const GitHubIssueAssigneeSchema = z.object({
id: z.number(),
avatar_url: z.string(),
url: z.string(),
- html_url: z.string()
+ html_url: z.string(),
});
export const GitHubMilestoneSchema = z.object({
@@ -226,7 +249,7 @@ export const GitHubMilestoneSchema = z.object({
number: z.number(),
title: z.string(),
description: z.string(),
- state: z.string()
+ state: z.string(),
});
export const GitHubIssueSchema = z.object({
@@ -251,7 +274,7 @@ export const GitHubIssueSchema = z.object({
created_at: z.string(),
updated_at: z.string(),
closed_at: z.string().nullable(),
- body: z.string()
+ body: z.string(),
});
// Pull Request related schemas
@@ -260,7 +283,7 @@ export const GitHubPullRequestHeadSchema = z.object({
ref: z.string(),
sha: z.string(),
user: GitHubIssueAssigneeSchema,
- repo: GitHubRepositorySchema
+ repo: GitHubRepositorySchema,
});
export const GitHubPullRequestSchema = z.object({
@@ -281,16 +304,16 @@ export const GitHubPullRequestSchema = z.object({
updated_at: z.string(),
closed_at: z.string().nullable(),
merged_at: z.string().nullable(),
- merge_commit_sha: z.string(),
+ merge_commit_sha: z.string().nullable(),
assignee: GitHubIssueAssigneeSchema.nullable(),
assignees: z.array(GitHubIssueAssigneeSchema),
head: GitHubPullRequestHeadSchema,
- base: GitHubPullRequestHeadSchema
+ base: GitHubPullRequestHeadSchema,
});
const RepoParamsSchema = z.object({
owner: z.string().describe("Repository owner (username or organization)"),
- repo: z.string().describe("Repository name")
+ repo: z.string().describe("Repository name"),
});
export const CreateOrUpdateFileSchema = RepoParamsSchema.extend({
@@ -298,81 +321,399 @@ export const CreateOrUpdateFileSchema = RepoParamsSchema.extend({
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)")
+ sha: z
+ .string()
+ .optional()
+ .describe(
+ "SHA of the file being replaced (required when updating existing files)"
+ ),
});
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 ListCommitsSchema = z.object({
+ owner: z.string().describe("Repository owner (username or organization)"),
+ repo: z.string().describe("Repository name"),
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)")
+ perPage: z.number().optional().describe("Number of results per page (default: 30, max: 100)"),
+ sha: z.string().optional()
+ .describe("SHA of the file being replaced (required when updating existing files)")
});
export const CreateRepositorySchema = 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")
+ private: z
+ .boolean()
+ .optional()
+ .describe("Whether the repository should be private"),
+ autoInit: z.boolean().optional().describe("Initialize with README.md"),
});
export const GetFileContentsSchema = RepoParamsSchema.extend({
path: z.string().describe("Path to the file or directory"),
- branch: z.string().optional().describe("Branch to get contents from")
+ branch: z.string().optional().describe("Branch to get contents from"),
});
export const PushFilesSchema = RepoParamsSchema.extend({
branch: z.string().describe("Branch to push to (e.g., 'main' or 'master')"),
- files: z.array(z.object({
- path: z.string().describe("Path where to create the file"),
- content: z.string().describe("Content of the file")
- })).describe("Array of files to push"),
- message: z.string().describe("Commit message")
+ files: z
+ .array(
+ z.object({
+ path: z.string().describe("Path where to create the file"),
+ content: z.string().describe("Content of the file"),
+ })
+ )
+ .describe("Array of files to push"),
+ message: z.string().describe("Commit message"),
});
export const CreateIssueSchema = RepoParamsSchema.extend({
title: z.string().describe("Issue title"),
body: z.string().optional().describe("Issue body/description"),
- assignees: z.array(z.string()).optional().describe("Array of usernames to assign"),
+ assignees: z
+ .array(z.string())
+ .optional()
+ .describe("Array of usernames to assign"),
labels: z.array(z.string()).optional().describe("Array of label names"),
- milestone: z.number().optional().describe("Milestone number to assign")
+ milestone: z.number().optional().describe("Milestone number to assign"),
});
export const CreatePullRequestSchema = RepoParamsSchema.extend({
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")
+ 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 ForkRepositorySchema = RepoParamsSchema.extend({
- organization: z.string().optional()
- .describe("Optional: organization to fork to (defaults to your personal account)")
+ organization: z
+ .string()
+ .optional()
+ .describe(
+ "Optional: organization to fork to (defaults to your personal account)"
+ ),
});
export const CreateBranchSchema = RepoParamsSchema.extend({
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)")
+ from_branch: z
+ .string()
+ .optional()
+ .describe(
+ "Optional: source branch to create from (defaults to the repository's default branch)"
+ ),
+});
+
+/**
+ * Response schema for a code search result item
+ * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-code
+ */
+export const SearchCodeItemSchema = z.object({
+ name: z.string().describe("The name of the file"),
+ path: z.string().describe("The path to the file in the repository"),
+ sha: z.string().describe("The SHA hash of the file"),
+ url: z.string().describe("The API URL for this file"),
+ git_url: z.string().describe("The Git URL for this file"),
+ html_url: z.string().describe("The HTML URL to view this file on GitHub"),
+ repository: GitHubRepositorySchema.describe(
+ "The repository where this file was found"
+ ),
+ score: z.number().describe("The search result score"),
+});
+
+/**
+ * Response schema for code search results
+ */
+export const SearchCodeResponseSchema = z.object({
+ total_count: z.number().describe("Total number of matching results"),
+ incomplete_results: z
+ .boolean()
+ .describe("Whether the results are incomplete"),
+ items: z.array(SearchCodeItemSchema).describe("The search results"),
+});
+
+/**
+ * Response schema for an issue search result item
+ * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests
+ */
+export const SearchIssueItemSchema = z.object({
+ url: z.string().describe("The API URL for this issue"),
+ repository_url: z
+ .string()
+ .describe("The API URL for the repository where this issue was found"),
+ labels_url: z.string().describe("The API URL for the labels of this issue"),
+ comments_url: z.string().describe("The API URL for comments of this issue"),
+ events_url: z.string().describe("The API URL for events of this issue"),
+ html_url: z.string().describe("The HTML URL to view this issue on GitHub"),
+ id: z.number().describe("The ID of this issue"),
+ node_id: z.string().describe("The Node ID of this issue"),
+ number: z.number().describe("The number of this issue"),
+ title: z.string().describe("The title of this issue"),
+ user: GitHubIssueAssigneeSchema.describe("The user who created this issue"),
+ labels: z.array(GitHubLabelSchema).describe("The labels of this issue"),
+ state: z.string().describe("The state of this issue"),
+ locked: z.boolean().describe("Whether this issue is locked"),
+ assignee: GitHubIssueAssigneeSchema.nullable().describe(
+ "The assignee of this issue"
+ ),
+ assignees: z
+ .array(GitHubIssueAssigneeSchema)
+ .describe("The assignees of this issue"),
+ comments: z.number().describe("The number of comments on this issue"),
+ created_at: z.string().describe("The creation time of this issue"),
+ updated_at: z.string().describe("The last update time of this issue"),
+ closed_at: z.string().nullable().describe("The closure time of this issue"),
+ body: z.string().describe("The body of this issue"),
+ score: z.number().describe("The search result score"),
+ pull_request: z
+ .object({
+ url: z.string().describe("The API URL for this pull request"),
+ html_url: z.string().describe("The HTML URL to view this pull request"),
+ diff_url: z.string().describe("The URL to view the diff"),
+ patch_url: z.string().describe("The URL to view the patch"),
+ })
+ .optional()
+ .describe("Pull request details if this is a PR"),
+});
+
+/**
+ * Response schema for issue search results
+ */
+export const SearchIssuesResponseSchema = z.object({
+ total_count: z.number().describe("Total number of matching results"),
+ incomplete_results: z
+ .boolean()
+ .describe("Whether the results are incomplete"),
+ items: z.array(SearchIssueItemSchema).describe("The search results"),
+});
+
+/**
+ * Response schema for a user search result item
+ * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-users
+ */
+export const SearchUserItemSchema = z.object({
+ login: z.string().describe("The username of the user"),
+ id: z.number().describe("The ID of the user"),
+ node_id: z.string().describe("The Node ID of the user"),
+ avatar_url: z.string().describe("The avatar URL of the user"),
+ gravatar_id: z.string().describe("The Gravatar ID of the user"),
+ url: z.string().describe("The API URL for this user"),
+ html_url: z.string().describe("The HTML URL to view this user on GitHub"),
+ followers_url: z.string().describe("The API URL for followers of this user"),
+ following_url: z.string().describe("The API URL for following of this user"),
+ gists_url: z.string().describe("The API URL for gists of this user"),
+ starred_url: z
+ .string()
+ .describe("The API URL for starred repositories of this user"),
+ subscriptions_url: z
+ .string()
+ .describe("The API URL for subscriptions of this user"),
+ organizations_url: z
+ .string()
+ .describe("The API URL for organizations of this user"),
+ repos_url: z.string().describe("The API URL for repositories of this user"),
+ events_url: z.string().describe("The API URL for events of this user"),
+ received_events_url: z
+ .string()
+ .describe("The API URL for received events of this user"),
+ type: z.string().describe("The type of this user"),
+ site_admin: z.boolean().describe("Whether this user is a site administrator"),
+ score: z.number().describe("The search result score"),
+});
+
+/**
+ * Response schema for user search results
+ */
+export const SearchUsersResponseSchema = z.object({
+ total_count: z.number().describe("Total number of matching results"),
+ incomplete_results: z
+ .boolean()
+ .describe("Whether the results are incomplete"),
+ items: z.array(SearchUserItemSchema).describe("The search results"),
+});
+
+/**
+ * Input schema for code search
+ * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-code--parameters
+ */
+export const SearchCodeSchema = z.object({
+ q: z
+ .string()
+ .describe(
+ "Search query. See GitHub code search syntax: https://docs.github.com/en/search-github/searching-on-github/searching-code"
+ ),
+ order: z
+ .enum(["asc", "desc"])
+ .optional()
+ .describe("Sort order (asc or desc)"),
+ per_page: z
+ .number()
+ .min(1)
+ .max(100)
+ .optional()
+ .describe("Results per page (max 100)"),
+ page: z.number().min(1).optional().describe("Page number"),
+});
+
+/**
+ * Input schema for issues search
+ * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-issues-and-pull-requests--parameters
+ */
+export const SearchIssuesSchema = z.object({
+ q: z
+ .string()
+ .describe(
+ "Search query. See GitHub issues search syntax: https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests"
+ ),
+ sort: z
+ .enum([
+ "comments",
+ "reactions",
+ "reactions-+1",
+ "reactions--1",
+ "reactions-smile",
+ "reactions-thinking_face",
+ "reactions-heart",
+ "reactions-tada",
+ "interactions",
+ "created",
+ "updated",
+ ])
+ .optional()
+ .describe("Sort field"),
+ order: z
+ .enum(["asc", "desc"])
+ .optional()
+ .describe("Sort order (asc or desc)"),
+ per_page: z
+ .number()
+ .min(1)
+ .max(100)
+ .optional()
+ .describe("Results per page (max 100)"),
+ page: z.number().min(1).optional().describe("Page number"),
+});
+
+/**
+ * Input schema for users search
+ * @see https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28#search-users--parameters
+ */
+export const SearchUsersSchema = z.object({
+ q: z
+ .string()
+ .describe(
+ "Search query. See GitHub users search syntax: https://docs.github.com/en/search-github/searching-on-github/searching-users"
+ ),
+ sort: z
+ .enum(["followers", "repositories", "joined"])
+ .optional()
+ .describe("Sort field"),
+ order: z
+ .enum(["asc", "desc"])
+ .optional()
+ .describe("Sort order (asc or desc)"),
+ per_page: z
+ .number()
+ .min(1)
+ .max(100)
+ .optional()
+ .describe("Results per page (max 100)"),
+ page: z.number().min(1).optional().describe("Page number"),
+});
+
+// Add these schema definitions for issue management
+
+export const ListIssuesOptionsSchema = z.object({
+ owner: z.string(),
+ repo: z.string(),
+ state: z.enum(['open', 'closed', 'all']).optional(),
+ labels: z.array(z.string()).optional(),
+ sort: z.enum(['created', 'updated', 'comments']).optional(),
+ direction: z.enum(['asc', 'desc']).optional(),
+ since: z.string().optional(), // ISO 8601 timestamp
+ page: z.number().optional(),
+ per_page: z.number().optional()
+});
+
+export const UpdateIssueOptionsSchema = z.object({
+ owner: z.string(),
+ repo: z.string(),
+ issue_number: z.number(),
+ title: z.string().optional(),
+ body: z.string().optional(),
+ state: z.enum(['open', 'closed']).optional(),
+ labels: z.array(z.string()).optional(),
+ assignees: z.array(z.string()).optional(),
+ milestone: z.number().optional()
+});
+
+export const IssueCommentSchema = z.object({
+ owner: z.string(),
+ repo: z.string(),
+ issue_number: z.number(),
+ body: z.string()
+});
+
+export const GetIssueSchema = z.object({
+ owner: z.string().describe("Repository owner (username or organization)"),
+ repo: z.string().describe("Repository name"),
+ issue_number: z.number().describe("Issue number")
});
// Export types
export type GitHubAuthor = z.infer;
export type GitHubFork = z.infer;
export type GitHubIssue = z.infer;
-export type GitHubPullRequest = z.infer;export type GitHubRepository = z.infer;
+export type GitHubPullRequest = z.infer;
+export type GitHubRepository = z.infer;
export type GitHubFileContent = z.infer;
-export type GitHubDirectoryContent = z.infer;
+export type GitHubDirectoryContent = z.infer<
+ typeof GitHubDirectoryContentSchema
+>;
export type GitHubContent = z.infer;
export type FileOperation = z.infer;
export type GitHubTree = z.infer;
export type GitHubCommit = z.infer;
+export type GitHubListCommits = z.infer;
export type GitHubReference = z.infer;
-export type CreateRepositoryOptions = z.infer;
+export type CreateRepositoryOptions = z.infer<
+ typeof CreateRepositoryOptionsSchema
+>;
export type CreateIssueOptions = z.infer;
-export type CreatePullRequestOptions = z.infer;
+export type CreatePullRequestOptions = z.infer<
+ typeof CreatePullRequestOptionsSchema
+>;
export type CreateBranchOptions = z.infer;
-export type GitHubCreateUpdateFileResponse = z.infer;
-export type GitHubSearchResponse = z.infer;
\ No newline at end of file
+export type GitHubCreateUpdateFileResponse = z.infer<
+ typeof GitHubCreateUpdateFileResponseSchema
+>;
+export type GitHubSearchResponse = z.infer;
+export type SearchCodeItem = z.infer;
+export type SearchCodeResponse = z.infer;
+export type SearchIssueItem = z.infer;
+export type SearchIssuesResponse = z.infer;
+export type SearchUserItem = z.infer;
+export type SearchUsersResponse = z.infer;
diff --git a/src/puppeteer/index.ts b/src/puppeteer/index.ts
index d8da6cae..5cae2eb0 100644
--- a/src/puppeteer/index.ts
+++ b/src/puppeteer/index.ts
@@ -124,6 +124,15 @@ async function ensureBrowser() {
return page!;
}
+declare global {
+ interface Window {
+ mcpHelper: {
+ logs: string[],
+ originalConsole: Partial,
+ }
+ }
+}
+
async function handleToolCall(name: string, args: any): Promise {
const page = await ensureBrowser();
@@ -263,32 +272,34 @@ async function handleToolCall(name: string, args: any): Promise
case "puppeteer_evaluate":
try {
- const result = await page.evaluate((script) => {
- const logs: string[] = [];
- const originalConsole = { ...console };
+ await page.evaluate(() => {
+ window.mcpHelper = {
+ logs: [],
+ originalConsole: { ...console },
+ };
['log', 'info', 'warn', 'error'].forEach(method => {
(console as any)[method] = (...args: any[]) => {
- logs.push(`[${method}] ${args.join(' ')}`);
- (originalConsole as any)[method](...args);
+ window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
+ (window.mcpHelper.originalConsole as any)[method](...args);
};
- });
+ } );
+ } );
- try {
- const result = eval(script);
- Object.assign(console, originalConsole);
- return { result, logs };
- } catch (error) {
- Object.assign(console, originalConsole);
- throw error;
- }
- }, args.script);
+ 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.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`,
+ text: `Execution result:\n${JSON.stringify(result, null, 2)}\n\nConsole output:\n${logs.join('\n')}`,
},
],
isError: false,
@@ -387,4 +398,4 @@ async function runServer() {
await server.connect(transport);
}
-runServer().catch(console.error);
\ No newline at end of file
+runServer().catch(console.error);
diff --git a/src/sentry/README.md b/src/sentry/README.md
index 34a1feb5..aae44568 100644
--- a/src/sentry/README.md
+++ b/src/sentry/README.md
@@ -6,7 +6,7 @@ A Model Context Protocol server for retrieving and analyzing issues from Sentry.
### Tools
-1. `get-sentry-issue`
+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
diff --git a/src/sentry/src/mcp_server_sentry/server.py b/src/sentry/src/mcp_server_sentry/server.py
index 1760311c..9f885bbc 100644
--- a/src/sentry/src/mcp_server_sentry/server.py
+++ b/src/sentry/src/mcp_server_sentry/server.py
@@ -223,7 +223,7 @@ async def serve(auth_token: str) -> Server:
async def handle_list_tools() -> list[types.Tool]:
return [
types.Tool(
- name="get-sentry-issue",
+ 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
@@ -247,7 +247,7 @@ async def serve(auth_token: str) -> Server:
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
- if name != "get-sentry-issue":
+ if name != "get_sentry_issue":
raise ValueError(f"Unknown tool: {name}")
if not arguments or "issue_id_or_url" not in arguments:
diff --git a/src/sqlite/README.md b/src/sqlite/README.md
index 5f211c41..2b2bea80 100644
--- a/src/sqlite/README.md
+++ b/src/sqlite/README.md
@@ -22,26 +22,26 @@ The server provides a demonstration prompt:
The server offers six core tools:
#### Query Tools
-- `read-query`
+- `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`
+- `write_query`
- Execute INSERT, UPDATE, or DELETE queries
- Input:
- `query` (string): The SQL modification query
- Returns: `{ affected_rows: number }`
-- `create-table`
+- `create_table`
- Create new tables in the database
- Input:
- `query` (string): CREATE TABLE SQL statement
- Returns: Confirmation of table creation
#### Schema Tools
-- `list-tables`
+- `list_tables`
- Get a list of all tables in the database
- No input required
- Returns: Array of table names
@@ -53,7 +53,7 @@ The server offers six core tools:
- Returns: Array of column definitions with names and types
#### Analysis Tools
-- `append-insight`
+- `append_insight`
- Add new business insights to the memo resource
- Input:
- `insight` (string): Business insight discovered from data analysis
diff --git a/src/sqlite/src/mcp_server_sqlite/server.py b/src/sqlite/src/mcp_server_sqlite/server.py
index 1b97a6a4..05cd117b 100644
--- a/src/sqlite/src/mcp_server_sqlite/server.py
+++ b/src/sqlite/src/mcp_server_sqlite/server.py
@@ -27,12 +27,12 @@ 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
+"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
You are an AI assistant tasked with generating a comprehensive business scenario based on a given topic.
@@ -68,7 +68,7 @@ a. Present 1 additional multiple-choice query options to the user. Its important
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.
+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.
@@ -233,7 +233,7 @@ async def main(db_path: str):
"""List available tools"""
return [
types.Tool(
- name="read-query",
+ name="read_query",
description="Execute a SELECT query on the SQLite database",
inputSchema={
"type": "object",
@@ -244,7 +244,7 @@ async def main(db_path: str):
},
),
types.Tool(
- name="write-query",
+ name="write_query",
description="Execute an INSERT, UPDATE, or DELETE query on the SQLite database",
inputSchema={
"type": "object",
@@ -255,7 +255,7 @@ async def main(db_path: str):
},
),
types.Tool(
- name="create-table",
+ name="create_table",
description="Create a new table in the SQLite database",
inputSchema={
"type": "object",
@@ -266,7 +266,7 @@ async def main(db_path: str):
},
),
types.Tool(
- name="list-tables",
+ name="list_tables",
description="List all tables in the SQLite database",
inputSchema={
"type": "object",
@@ -274,7 +274,7 @@ async def main(db_path: str):
},
),
types.Tool(
- name="describe-table",
+ name="describe_table",
description="Get the schema information for a specific table",
inputSchema={
"type": "object",
@@ -285,7 +285,7 @@ async def main(db_path: str):
},
),
types.Tool(
- name="append-insight",
+ name="append_insight",
description="Add a business insight to the memo",
inputSchema={
"type": "object",
@@ -303,13 +303,13 @@ async def main(db_path: str):
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
"""Handle tool execution requests"""
try:
- if name == "list-tables":
+ 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":
+ elif name == "describe_table":
if not arguments or "table_name" not in arguments:
raise ValueError("Missing table_name argument")
results = db._execute_query(
@@ -317,7 +317,7 @@ async def main(db_path: str):
)
return [types.TextContent(type="text", text=str(results))]
- elif name == "append-insight":
+ elif name == "append_insight":
if not arguments or "insight" not in arguments:
raise ValueError("Missing insight argument")
@@ -332,19 +332,19 @@ async def main(db_path: str):
if not arguments:
raise ValueError("Missing arguments")
- if name == "read-query":
+ if name == "read_query":
if not arguments["query"].strip().upper().startswith("SELECT"):
- raise ValueError("Only SELECT queries are allowed for read-query")
+ 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":
+ elif name == "write_query":
if arguments["query"].strip().upper().startswith("SELECT"):
- raise ValueError("SELECT queries are not allowed for write-query")
+ 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":
+ 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"])