diff --git a/README.md b/README.md
index 822ab053..e5f41461 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ 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.
+-
**[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
-
**[E2B](https://github.com/e2b-dev/mcp-server)** - Run code in secure sandboxes hosted by [E2B](https://e2b.dev)
diff --git a/package-lock.json b/package-lock.json
index b42b4228..b0b19b05 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5287,8 +5287,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 +5311,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/github/README.md b/src/github/README.md
index cfd268a8..9d98b1b6 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,97 @@ 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
+
+## 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..a8341a9d 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,68 @@ 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,
+ GitHubCommitSchema,
+ GitHubContentSchema,
+ GitHubCreateUpdateFileResponseSchema,
+ GitHubForkSchema,
+ GitHubIssueSchema,
+ GitHubPullRequestSchema,
+ GitHubReferenceSchema,
+ GitHubRepositorySchema,
+ GitHubSearchResponseSchema,
+ GitHubTreeSchema,
+ IssueCommentSchema,
+ 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 +81,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 +107,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 +140,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 +152,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 +186,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 +200,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 +216,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 +242,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 +267,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 +277,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 +316,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 +328,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 +359,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 +390,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 +420,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 +435,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 +453,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 +472,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 +487,256 @@ async function createRepository(
return GitHubRepositorySchema.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());
+}
+
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_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),
+ },
+ ],
};
});
@@ -528,8 +749,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 +767,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 +786,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 +840,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 +854,74 @@ 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 };
}
default:
@@ -628,7 +929,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 +951,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..f40da150 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,35 +62,35 @@ 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 GitHubCommitSchema = z.object({
@@ -102,12 +102,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 +120,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 +129,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 +137,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 +146,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 +166,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 +192,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 +210,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 +218,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 +230,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 +255,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 +264,7 @@ export const GitHubPullRequestHeadSchema = z.object({
ref: z.string(),
sha: z.string(),
user: GitHubIssueAssigneeSchema,
- repo: GitHubRepositorySchema
+ repo: GitHubRepositorySchema,
});
export const GitHubPullRequestSchema = z.object({
@@ -285,12 +289,12 @@ export const GitHubPullRequestSchema = z.object({
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 +302,383 @@ 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)")
+ 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 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 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 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);