Merge pull request #103 from 0xRaduan/add-search-for-github

feat(github): Add GitHub Search to Github MCP
This commit is contained in:
Justin Spahr-Summers
2024-12-05 22:58:00 +00:00
committed by GitHub
5 changed files with 707 additions and 188 deletions

10
package-lock.json generated
View File

@@ -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",

View File

@@ -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,60 @@ 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. `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
11. `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
12. `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

View File

@@ -41,19 +41,32 @@ import {
CreateIssueSchema,
CreatePullRequestSchema,
ForkRepositorySchema,
CreateBranchSchema
} from './schemas.js';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
CreateBranchSchema,
SearchCodeSchema,
SearchIssuesSchema,
SearchUsersSchema,
SearchCodeResponseSchema,
SearchIssuesResponseSchema,
SearchUsersResponseSchema,
type SearchCodeResponse,
type SearchIssuesResponse,
type SearchUsersResponse,
} from "./schemas.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import { z } from "zod";
import type { CallToolRequest } from "@modelcontextprotocol/sdk/types.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 +80,17 @@ async function forkRepository(
repo: string,
organization?: string
): Promise<GitHubFork> {
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 +106,21 @@ async function createBranch(
options: z.infer<typeof CreateBranchOptionsSchema>
): Promise<GitHubReference> {
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 +139,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 +151,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 +185,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 +199,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 +215,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 +241,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 +266,7 @@ async function createOrUpdateFile(
branch: string,
sha?: string
): Promise<GitHubCreateUpdateFileResponse> {
const encodedContent = Buffer.from(content).toString('base64');
const encodedContent = Buffer.from(content).toString("base64");
let currentSha = sha;
if (!currentSha) {
@@ -261,28 +276,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 +315,11 @@ async function createTree(
files: FileOperation[],
baseTree?: string
): Promise<GitHubTree> {
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 +327,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 +358,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 +389,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 +419,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 +434,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 +452,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 +471,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 +486,149 @@ async function createRepository(
return GitHubRepositorySchema.parse(await response.json());
}
async function searchCode(
params: z.infer<typeof SearchCodeSchema>
): Promise<SearchCodeResponse> {
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<typeof SearchIssuesSchema>
): Promise<SearchIssuesResponse> {
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<typeof SearchUsersSchema>
): Promise<SearchUsersResponse> {
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: "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 +641,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 +659,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 +678,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 +732,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 +746,53 @@ 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) }],
};
}
default:
@@ -628,7 +800,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 +822,4 @@ async function runServer() {
runServer().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
});

View File

@@ -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": {

View File

@@ -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,350 @@ 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"),
});
// Export types
export type GitHubAuthor = z.infer<typeof GitHubAuthorSchema>;
export type GitHubFork = z.infer<typeof GitHubForkSchema>;
export type GitHubIssue = z.infer<typeof GitHubIssueSchema>;
export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>;export type GitHubRepository = z.infer<typeof GitHubRepositorySchema>;
export type GitHubPullRequest = z.infer<typeof GitHubPullRequestSchema>;
export type GitHubRepository = z.infer<typeof GitHubRepositorySchema>;
export type GitHubFileContent = z.infer<typeof GitHubFileContentSchema>;
export type GitHubDirectoryContent = z.infer<typeof GitHubDirectoryContentSchema>;
export type GitHubDirectoryContent = z.infer<
typeof GitHubDirectoryContentSchema
>;
export type GitHubContent = z.infer<typeof GitHubContentSchema>;
export type FileOperation = z.infer<typeof FileOperationSchema>;
export type GitHubTree = z.infer<typeof GitHubTreeSchema>;
export type GitHubCommit = z.infer<typeof GitHubCommitSchema>;
export type GitHubReference = z.infer<typeof GitHubReferenceSchema>;
export type CreateRepositoryOptions = z.infer<typeof CreateRepositoryOptionsSchema>;
export type CreateRepositoryOptions = z.infer<
typeof CreateRepositoryOptionsSchema
>;
export type CreateIssueOptions = z.infer<typeof CreateIssueOptionsSchema>;
export type CreatePullRequestOptions = z.infer<typeof CreatePullRequestOptionsSchema>;
export type CreatePullRequestOptions = z.infer<
typeof CreatePullRequestOptionsSchema
>;
export type CreateBranchOptions = z.infer<typeof CreateBranchOptionsSchema>;
export type GitHubCreateUpdateFileResponse = z.infer<typeof GitHubCreateUpdateFileResponseSchema>;
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
export type GitHubCreateUpdateFileResponse = z.infer<
typeof GitHubCreateUpdateFileResponseSchema
>;
export type GitHubSearchResponse = z.infer<typeof GitHubSearchResponseSchema>;
export type SearchCodeItem = z.infer<typeof SearchCodeItemSchema>;
export type SearchCodeResponse = z.infer<typeof SearchCodeResponseSchema>;
export type SearchIssueItem = z.infer<typeof SearchIssueItemSchema>;
export type SearchIssuesResponse = z.infer<typeof SearchIssuesResponseSchema>;
export type SearchUserItem = z.infer<typeof SearchUserItemSchema>;
export type SearchUsersResponse = z.infer<typeof SearchUsersResponseSchema>;