# Directory Structure ``` ├── .dockerignore ├── .env.example ├── .github │ └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── eslint.config.js ├── package.json ├── pnpm-lock.yaml ├── prettier.config.mjs ├── README.md ├── rolldown.config.ts ├── src │ ├── app.ts │ ├── common │ │ └── github-schema.ts │ ├── constants.ts │ ├── env.ts │ ├── tools │ │ ├── get-issue.ts │ │ ├── get-pull-request.ts │ │ ├── list-repositories-issues.ts │ │ ├── list-repositories-pull-requests.ts │ │ ├── search-code.ts │ │ ├── search-commits.ts │ │ ├── search-issues.ts │ │ ├── search-labels.ts │ │ ├── search-repositories.ts │ │ ├── search-topics.ts │ │ └── search-users.ts │ └── utils │ └── gh-fetch.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules/ .env .env.production dist/ ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` node_modules .git .gitignore *.md dist .env .env.production .env.example ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` NODE_ENV=development GITHUB_PERSONAL_ACCESS_TOKEN=your_personal_github_access_token ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Github MCP Server A [Model Context Protocol](https://github.com/modelcontextprotocol) Server for Github. Provides integration with Github through MCP, allowing LLMs to interact with it. [Github REST Api Docs](https://docs.github.com/en/rest) ## Installation ### Manual Installation 1. Create or get access token for your Github Account: [Guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) 2. Add server config to Claude Desktop: - MacOS: ~/Library/Application Support/Claude/claude_desktop_config.json - Windows: [Check this Guide](https://gist.github.com/feveromo/7a340d7795fca1ccd535a5802b976e1f) ```json { "mcpServers": { "github": { "command": "npx", "args": ["-y", "github-mcp-server"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "your_personal_github_access_token" } } } } ``` ## Components ### Tools 1. `search_repositories`: Search GitHub for a repository. - Required inputs: - `query` (string): The query to search for repository. - `page` (number, default: 30, max: 100): Page number for pagination. - `per_page` (number, default: 30, max: 100): Number of results per page. 2. `search_issues`: Search issues from a repository. - Required inputs: - `query` (string): The query to search for repository. - `page` (number, default: 1): Page number for pagination. - `per_page` (number, default: 30, max: 100): Number of results per page. - `order` (optional string, default: `desc`): Sort of order (`asc` or `desc`). - `sort` (optional string, default: `best match`): Sort field (can be one of: `comments`, `reactions`, `reactions-+1`, `reactions--1`, `reactions-smile`, `reactions-thinking_face`, `reactions-heart`, `reactions-tada`, `interactions`, `created` or `updated`). 3. `search_commits`: Search commits from a repository. - Required inputs: - `query` (string): The query to search for repository. - `page` (number, default: 1): Page number for pagination. - `per_page` (number, default: 30, max: 100): Number of results per page. - `order` (optional string, default: `desc`): Sort of order (`asc` or `desc`). - `sort` (optional string, default: `best match`): Sort field (can be one of: `committer-date` or `author-date`). 4. `search_code`: Search code from a repository. - Required inputs: - `query` (string): The query to search for repository. - `page` (number, default: 1): Page number for pagination. - `per_page` (number, default: 30, max: 100): Number of results per page. 5. `search_users`: Search users from a repository. - Required inputs: - `query` (string): The query to search for repository. - `page` (number, default: 1): Page number for pagination. - `per_page` (number, default: 30, max: 100): Number of results per page. - `order` (optional string, default: `desc`): Sort of order (`asc` or `desc`). - `sort` (optional string, default: `best match`): Sort field (can be one of: `followers`, `repositories` or `joined`). 6. `search_topics`: Search topics. - Required inputs: - `query` (string): The query to search for repository. - `page` (number, default: 1): Page number for pagination. - `per_page` (number, default: 30, max: 100): Number of results per page. 7. `search_labels`: Search labels in a repository. - Required inputs: - `query` (string): The query to search for repository. - `page` (number, default: 1): Page number for pagination. - `per_page` (number, default: 30, max: 100): Number of results per page. - `order` (optional string, default: `desc`): Sort of order (`asc` or `desc`). - `sort` (optional string, default: `best match`): Sort field (can be one of: `created` or `updated`). 8. `list_repositories_issues`: List issues from a repository. - Required inputs: - `owner` (string): The owner of the repository. - `repo` (string): The repository name. - `page` (optional number, default: 1): Page number for pagination. - `per_page` (optional number, default: 30, max: 100): Number of results per page. - `direction` (optional string, default: `desc`): Direction of sort (`asc` or `desc`). - `sort` (optional string, default: `created`): Sort field (can be one of: `created`, `comments` or `updated`). - `since` (optional string): Results last updated after the given time (ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.). - `labels` (optional string): Comma separated label names. Example: bug,ui,@high. - `milestone` (optional string): Milestone number. - `assignee` (optional string): Name of assignee user (`*` for all). - `creator` (optional string): The user that created the issue. (`*` for all). - `mentioned` (optional string): A user that's mentioned in the issue. 9. `get_issue`: Get an issue from a repository. - Required inputs: - `owner` (string): The owner of the repository. - `repo` (string): The repository name. - `issue_number` (number): The issue number. 10. `list_repositories_pull_requests`: List pull requests from a repository. - Required inputs: - `owner` (string): The owner of the repository. - `repo` (string): The repository name. - `page` (optional number, default: 1): Page number for pagination. - `per_page` (optional number, default: 30, max: 100): Number of results per page. - `direction` (optional string, default: `desc`): Direction of sort (`asc` or `desc`). - `sort` (optional string, default: `created`): Sort field (can be one of: `created`, `popularity`, `long-running` or `updated`). - `head` (optional string): Filter pulls by head user or head organization and branch name in the format of user:ref-name or organization:ref-name (For example: github:new-script-format or octocat:test-branch). - `base` (optional string): Filter pulls by base branch name. (For example: gh-pages). 11. `get_pull_request`: Get a pull request from a repository. - Required inputs: - `owner` (string): The owner of the repository. - `repo` (string): The repository name. - `pull_request_number` (number): The pull request number. ## Usage examples Some example prompts you can use to interact with Github: 1. "modelcontextprotocol" → execute the `search_repositories` tool to find repositories where modelcontextprotocol mentioned. 2. "What is the 739 issue on modelcontextprotocol servers repo" → execute the `get_issue` tool to find 739 issue from modelcontextprotocol servers repo. 3. "What is the 717 PR on modelcontextprotocol servers repo" → execute the `get_pull_request` tool to find 717 PR from modelcontextprotocol servers repo. ## Development 1. Install dependencies: ```shell pnpm install ``` 2. Configure Github Access token in `.env`: ```shell GITHUB_PERSONAL_ACCESS_TOKEN=<your_personal_github_access_token> ``` 3. Run locally with watch: ```shell pnpm dev ``` 4. Build the server: ```shell pnpm build ``` 5. Local debugging with inspector: ```shell pnpm inspector ``` ``` -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- ``` /** * @type {import('prettier').Config} */ const config = { arrowParens: "always", printWidth: 80, singleQuote: false, jsxSingleQuote: false, semi: true, trailingComma: "all", tabWidth: 2, }; export default config; ``` -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- ```typescript import { getUserAgent } from "universal-user-agent"; export const VERSION = "0.0.1"; export const GITHUB_API_BASE_URL = "https://api.github.com"; export const USER_AGENT = `modelcontextprotocol/servers/github/v${VERSION} ${getUserAgent()}`; ``` -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- ```javascript import js from "@eslint/js"; import tseslint from "typescript-eslint"; import neverThrowPlugin from "eslint-plugin-neverthrow"; export default tseslint.config( { ignores: ["dist"] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], plugins: { neverthrow: neverThrowPlugin, }, languageOptions: { ecmaVersion: 2022, }, }, ); ``` -------------------------------------------------------------------------------- /rolldown.config.ts: -------------------------------------------------------------------------------- ```typescript import { defineConfig } from "rolldown"; import { minify } from "rollup-plugin-swc3"; export default defineConfig({ input: "src/app.ts", platform: "node", treeshake: true, output: { file: "dist/app.js", }, define: { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), "process.env.GITHUB_PERSONAL_ACCESS_TOKEN": JSON.stringify( process.env.GITHUB_PERSONAL_ACCESS_TOKEN, ), }, plugins: [ minify({ module: true, mangle: {}, compress: {}, }), ], }); ``` -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- ```typescript import { createEnv } from "@t3-oss/env-core"; import { z } from "zod"; export const env = createEnv({ emptyStringAsUndefined: true, clientPrefix: "PUBLIC_", client: {}, shared: { NODE_ENV: z.enum(["development", "production"]), }, server: { GITHUB_PERSONAL_ACCESS_TOKEN: z .string({ required_error: "GITHUB_PERSONAL_ACCESS_TOKEN is required", }) .min(1, "GITHUB_PERSONAL_ACCESS_TOKEN is required"), }, runtimeEnv: { NODE_ENV: process.env.NODE_ENV, GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_PERSONAL_ACCESS_TOKEN, }, }); ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "skipLibCheck": true, "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, // Some stricter flags (disabled by default) "noFallthroughCasesInSwitch": true, "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile FROM node:22.12-bullseye-slim AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable WORKDIR /app FROM base AS prod-deps COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile FROM base AS build RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile COPY . . ENV NODE_ENV="production" ARG GITHUB_PERSONAL_ACCESS_TOKEN ENV GITHUB_PERSONAL_ACCESS_TOKEN=$GITHUB_PERSONAL_ACCESS_TOKEN RUN pnpm run build FROM base COPY --from=prod-deps /app/node_modules /node_modules COPY --from=build /app/dist /dist ENTRYPOINT [ "node", "dist/app.js" ] ``` -------------------------------------------------------------------------------- /src/utils/gh-fetch.ts: -------------------------------------------------------------------------------- ```typescript import { USER_AGENT } from "../constants.js"; import { env } from "../env.js"; import { err, ok } from "neverthrow"; export async function $github(url: string, options?: RequestInit) { const defaultHeaders = { Authorization: `Bearer ${env.GITHUB_PERSONAL_ACCESS_TOKEN}`, Accept: "application/vnd.github.v3+json", "Content-Type": "application/json", "User-Agent": USER_AGENT, }; const headers = Object.assign(defaultHeaders, options?.headers ?? {}); try { const response = await fetch(url, { headers, ...options, }); if (!response.ok) { return err(new Error(`Failed to fetch ${url}: ${response.statusText}`)); } return ok(response); } catch (error) { return err(new Error(`Failed to fetch ${url}: ${error}`)); } } export async function $githubJson(url: string, options?: RequestInit) { try { const response = await $github(url, options); if (response.isErr()) return err(response.error); const json = await response.value.json(); return ok(json); } catch (error) { return err(new Error(`Failed to fetch ${url}: ${error}`)); } } ``` -------------------------------------------------------------------------------- /src/tools/get-issue.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/issues/issues#get-an-issue get issue API docs export const getIssueInputSchema = z.object({ owner: z.string().describe("The owner of the repository"), repo: z.string().describe("The repository name"), issue_number: z.number().describe("The issue number"), }); export const GET_ISSUE_TOOL: Tool = { name: "get_issue", description: "Get an issue from a repository", inputSchema: zodToJsonSchema(getIssueInputSchema) as Tool["inputSchema"], }; export type GetIssueInput = z.output<typeof getIssueInputSchema>; export async function getIssue(input: GetIssueInput) { const url = new URL( `/repos/${input.owner}/${input.repo}/issues/${input.issue_number}`, GITHUB_API_BASE_URL, ); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); try { // const data = gitHubSearchResponseSchema.parse(json.value); return ok(json.value); } catch (error) { return err( new Error(`Failed to parse github get issue response: ${error}`), ); } } ``` -------------------------------------------------------------------------------- /src/tools/get-pull-request.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request get pull request API docs export const getPullRequestInputSchema = z.object({ owner: z.string().describe("The owner of the repository"), repo: z.string().describe("The repository name"), pull_request_number: z.number().describe("The pull request number"), }); export const GET_PULL_REQUEST_TOOL: Tool = { name: "get_pull_request", description: "Get a pull request from a repository", inputSchema: zodToJsonSchema( getPullRequestInputSchema, ) as Tool["inputSchema"], }; export type GetPullRequestInput = z.output<typeof getPullRequestInputSchema>; export async function getPullRequest(input: GetPullRequestInput) { const url = new URL( `/repos/${input.owner}/${input.repo}/pulls/${input.pull_request_number}`, GITHUB_API_BASE_URL, ); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); try { return ok(json.value); } catch (error) { return err( new Error(`Failed to parse github get pull request response: ${error}`), ); } } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "github-mcp-server", "version": "1.0.0", "author": "Paras Solanki", "repository": { "type": "git", "url": "https://github.com/ParasSolanki/github-mcp-server.git" }, "license": "MIT", "description": "A model context protocol server for GitHub API.", "type": "module", "keywords": [ "github", "mcp", "server", "mcp", "api", "model context protocol" ], "packageManager": "[email protected]", "bin": { "github-mcp-server": "dist/app.js" }, "files": [ "dist" ], "scripts": { "dev": "dotenvx run -- tsx watch src/app.ts", "build": "dotenvx run -f .env.production -- rolldown -c rolldown.config.ts", "inspector": "pnpm build && npx @modelcontextprotocol/inspector dist/app.js", "lint": "eslint .", "format": "prettier '**/*.{cjs,mjs,ts,tsx,md,json}' --ignore-path ./.gitignore --ignore-unknown --write", "format:check": "prettier '**/*.{cjs,mjs,ts,tsx,md,json}' --ignore-path ./.gitignore --ignore-unknown --check" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "@t3-oss/env-core": "^0.12.0", "neverthrow": "^8.2.0", "universal-user-agent": "^7.0.2", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.3" }, "devDependencies": { "@dotenvx/dotenvx": "^1.38.3", "@eslint/js": "^9.21.0", "@types/node": "^22.13.8", "eslint-plugin-neverthrow": "^1.1.4", "prettier": "^3.5.3", "rolldown": "1.0.0-beta.3", "rollup-plugin-swc3": "^0.12.1", "tsx": "^4.19.3", "typescript": "^5.7.2", "typescript-eslint": "^8.25.0" } } ``` -------------------------------------------------------------------------------- /src/tools/search-code.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/search/search#search-code search code API docs export const searchCodeInputSchema = z.object({ query: z .string() .describe( "The query to search for (see Github search query syntax). The query contains one or more search keywords and qualifiers. Qualifiers allow you to limit your search to specific areas of GitHub. The REST API supports the same qualifiers as the web interface for GitHub. ", ), page: z.number().describe("Page number for pagination (default: 1)"), per_page: z .number() .describe("Number of results per page (default: 30, max: 100)"), }); export const SEARCH_CODE_TOOL: Tool = { name: "search_code", description: "Search code from a repository", inputSchema: zodToJsonSchema(searchCodeInputSchema) as Tool["inputSchema"], }; export type SearchCodeInput = z.output<typeof searchCodeInputSchema>; export async function searchCode(input: SearchCodeInput) { const url = new URL(`/search/code`, GITHUB_API_BASE_URL); url.searchParams.set("q", input.query); if (input.page) url.searchParams.set("page", input.page.toString()); if (input.per_page) url.searchParams.set("per_page", input.per_page.toString()); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); return ok(json.value); } ``` -------------------------------------------------------------------------------- /src/tools/search-topics.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/search/search#search-topics search topics API docs export const searchTopicsInputSchema = z.object({ query: z .string() .describe( "The query to search for (see Github search query syntax). The query contains one or more search keywords and qualifiers. Qualifiers allow you to limit your search to specific areas of GitHub. The REST API supports the same qualifiers as the web interface for GitHub. ", ), page: z.number().describe("Page number for pagination (default: 1)"), per_page: z .number() .describe("Number of results per page (default: 30, max: 100)"), }); export const SEARCH_TOPICS_TOOL: Tool = { name: "search_topics", description: "Search topics", inputSchema: zodToJsonSchema(searchTopicsInputSchema) as Tool["inputSchema"], }; export type SearchTopicsInput = z.output<typeof searchTopicsInputSchema>; export async function searchTopics(input: SearchTopicsInput) { const url = new URL(`/search/topics`, GITHUB_API_BASE_URL); url.searchParams.set("q", input.query); if (input.page) url.searchParams.set("page", input.page.toString()); if (input.per_page) url.searchParams.set("per_page", input.per_page.toString()); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); return ok(json.value); } ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: pull_request: branches: ["*"] merge_group: jobs: prettier: runs-on: ubuntu-latest name: Run prettier timeout-minutes: 5 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v3 name: Install pnpm id: pnpm-install with: run_install: false - name: Get pnpm store directory id: pnpm-cache run: | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - run: pnpm format:check lint: runs-on: ubuntu-latest name: Run ESLint timeout-minutes: 5 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 22 - uses: pnpm/action-setup@v3 name: Install pnpm id: pnpm-install with: run_install: false - name: Get pnpm store directory id: pnpm-cache run: | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - run: pnpm lint ``` -------------------------------------------------------------------------------- /src/tools/search-labels.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/search/search#search-labels search labels API docs export const searchLabelsInputSchema = z.object({ query: z .string() .describe( "The search keywords. This endpoint does not accept qualifiers in the query. (see Github search query syntax)", ), page: z.number().describe("Page number for pagination (default: 1)"), per_page: z .number() .describe("Number of results per page (default: 30, max: 100)"), sort: z .enum(["created", "updated"]) .optional() .describe( "Sorts the results of your query by when the label was created or updated. (default: best match)", ), order: z .enum(["asc", "desc"]) .optional() .describe( "Determines whether the first search result returned is the highest number of matches (desc) or lowest number of matches (asc). This parameter is ignored unless you provide sort. (default: desc)", ), }); export const SEARCH_LABELS_TOOL: Tool = { name: "search_labels", description: "Search labels in a repository", inputSchema: zodToJsonSchema(searchLabelsInputSchema) as Tool["inputSchema"], }; export type SearchLabelsInput = z.output<typeof searchLabelsInputSchema>; export async function searchLabels(input: SearchLabelsInput) { const url = new URL(`/search/labels`, GITHUB_API_BASE_URL); url.searchParams.set("q", input.query); if (input.page) url.searchParams.set("page", input.page.toString()); if (input.per_page) url.searchParams.set("per_page", input.per_page.toString()); if (input.sort) url.searchParams.set("sort", input.sort); if (input.order) url.searchParams.set("order", input.order); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); return ok(json.value); } ``` -------------------------------------------------------------------------------- /src/tools/search-commits.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/search/search#search-commits search commits API docs export const searchCommitsInputSchema = z.object({ query: z .string() .describe( "The query to search for (see Github search query syntax). The query contains one or more search keywords and qualifiers. Qualifiers allow you to limit your search to specific areas of GitHub. The REST API supports the same qualifiers as the web interface for GitHub. ", ), page: z.number().describe("Page number for pagination (default: 1)"), per_page: z .number() .describe("Number of results per page (default: 30, max: 100)"), sort: z .enum(["committer-date", "author-date"]) .optional() .describe( "Sorts the results of your query by author-date or committer-date. (default: best match)", ), order: z .enum(["asc", "desc"]) .optional() .describe( "Determines whether the first search result returned is the highest number of matches (desc) or lowest number of matches (asc). This parameter is ignored unless you provide sort. (default: desc)", ), }); export const SEARCH_COMMITS_TOOL: Tool = { name: "search_commits", description: "Search commits from a repository", inputSchema: zodToJsonSchema(searchCommitsInputSchema) as Tool["inputSchema"], }; export type SearchCommitsInput = z.output<typeof searchCommitsInputSchema>; export async function searchCommits(input: SearchCommitsInput) { const url = new URL(`/search/commits`, GITHUB_API_BASE_URL); url.searchParams.set("q", input.query); if (input.page) url.searchParams.set("page", input.page.toString()); if (input.per_page) url.searchParams.set("per_page", input.per_page.toString()); if (input.sort) url.searchParams.set("sort", input.sort); if (input.order) url.searchParams.set("order", input.order); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); return ok(json.value); } ``` -------------------------------------------------------------------------------- /src/tools/search-users.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/search/search#search-users search users API docs export const searchUsersInputSchema = z.object({ query: z .string() .describe( "The query to search for (see Github search query syntax). The query contains one or more search keywords and qualifiers. Qualifiers allow you to limit your search to specific areas of GitHub. The REST API supports the same qualifiers as the web interface for GitHub. ", ), page: z.number().describe("Page number for pagination (default: 1)"), per_page: z .number() .describe("Number of results per page (default: 30, max: 100)"), sort: z .enum(["followers", "repositories", "joined"]) .optional() .describe( "Sorts the results of your query by number of followers or repositories, or when the person joined GitHub. Default: best match", ), order: z .enum(["asc", "desc"]) .optional() .describe( "Determines whether the first search result returned is the highest number of matches (desc) or lowest number of matches (asc). This parameter is ignored unless you provide sort. (default: desc).", ), }); export const SEARCH_USERS_TOOL: Tool = { name: "search_users", description: "Search users from a repository", inputSchema: zodToJsonSchema(searchUsersInputSchema) as Tool["inputSchema"], }; export type SearchUsersInput = z.output<typeof searchUsersInputSchema>; export async function searchUsers(input: SearchUsersInput) { const url = new URL(`/search/users`, GITHUB_API_BASE_URL); url.searchParams.set("q", input.query); if (input.page) url.searchParams.set("page", input.page.toString()); if (input.per_page) url.searchParams.set("per_page", input.per_page.toString()); if (input.sort) url.searchParams.set("sort", input.sort); if (input.order) url.searchParams.set("order", input.order); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); return ok(json.value); } ``` -------------------------------------------------------------------------------- /src/common/github-schema.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; export const gitHubAuthorSchema = z.object({ name: z.string().describe("The name of the author"), email: z.string().describe("The email of the author"), date: z.string().describe("The date of the commit"), }); export const gitHubOwnerSchema = z.object({ login: z.string().describe("The login name of the owner"), id: z.number().describe("The ID of the owner"), node_id: z.string().describe("The node ID of the owner"), avatar_url: z.string().describe("The avatar URL of the owner"), url: z.string().describe("The URL of the owner"), html_url: z.string().describe("The HTML URL of the owner"), type: z.string().describe("The type of the owner"), }); export const gitHubRepositorySchema = z.object({ id: z.number().describe("The ID of the repository"), node_id: z.string().describe("The node ID of the repository"), name: z.string().describe("The name of the repository"), full_name: z.string().describe("The full name of the repository"), private: z.boolean().describe("Whether the repository is private"), owner: gitHubOwnerSchema, html_url: z.string().describe("The HTML URL of the repository"), description: z .string() .nullable() .describe("The description of the repository"), fork: z.boolean().describe("Whether the repository is a fork"), url: z.string().describe("The URL of the repository"), created_at: z.string().describe("The date the repository was created"), updated_at: z.string().describe("The date the repository was updated"), pushed_at: z.string().describe("The date the repository was pushed"), git_url: z.string().describe("The Git URL of the repository"), ssh_url: z.string().describe("The SSH URL of the repository"), clone_url: z.string().describe("The clone URL of the repository"), default_branch: z.string().describe("The default branch of the repository"), }); export const gitHubSearchResponseSchema = z.object({ total_count: z.number().describe("The total number of results"), incomplete_results: z .boolean() .describe("Whether the results are incomplete"), items: z.array(gitHubRepositorySchema).describe("The repositories"), }); export type GitHubAuthor = z.infer<typeof gitHubAuthorSchema>; export type GitHubRepository = z.infer<typeof gitHubRepositorySchema>; export type GitHubSearchResponse = z.infer<typeof gitHubSearchResponseSchema>; ``` -------------------------------------------------------------------------------- /src/tools/search-repositories.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { gitHubSearchResponseSchema } from "../common/github-schema.js"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import { zodToJsonSchema } from "zod-to-json-schema"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; // @see https://docs.github.com/en/rest/search/search#search-repositories search repositories API docs export const searchRepositoriesInputSchema = z.object({ query: z .string() .describe( "The query contains one or more search keywords and qualifiers. Qualifiers allow you to limit your search to specific areas of GitHub. The REST API supports the same qualifiers as the web interface for GitHub. (see Github search query syntax)", ), page: z.number().describe("Page number for pagination (default: 1)"), per_page: z .number() .describe("Number of results per page (default: 30, max: 100)"), sort: z .enum(["stars", "forks", "help-wanted-issues", "updated"]) .optional() .describe( "Sorts the results of your query by the number of stars, forks, or help-wanted-issues or how recently the items were updated. Default: best match", ), order: z .enum(["asc", "desc"]) .optional() .describe( "Determines whether the first search result returned is the highest number of matches (desc) or lowest number of matches (asc). This parameter is ignored unless you provide sort. (default: desc)", ), }); export const SEARCH_REPOSITORIES_TOOL: Tool = { name: "search_repositories", description: "Search GitHub for a repository", inputSchema: zodToJsonSchema( searchRepositoriesInputSchema, ) as Tool["inputSchema"], }; export type SearchRepositoriesInput = z.output< typeof searchRepositoriesInputSchema >; export async function searchRepositories(input: SearchRepositoriesInput) { const url = new URL("/search/repositories", GITHUB_API_BASE_URL); url.searchParams.set("q", input.query); url.searchParams.set("page", input.page.toString()); url.searchParams.set("per_page", input.per_page.toString()); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); try { const data = gitHubSearchResponseSchema.parse(json.value); return ok(data); } catch (error) { return err( new Error( `Failed to parse github search repositories response: ${error}`, ), ); } } ``` -------------------------------------------------------------------------------- /src/tools/search-issues.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.js"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; export const searchIssuesInputSchema = z.object({ query: z .string() .describe( "The query to search for (see Github search query syntax). The query contains one or more search keywords and qualifiers. Qualifiers allow you to limit your search to specific areas of GitHub. The REST API supports the same qualifiers as the web interface for GitHub. ", ), page: z.number().describe("Page number for pagination (default: 1)"), per_page: z .number() .describe("Number of results per page (default: 30, max: 100)"), sort: z .enum([ "comments", "reactions", "reactions-+1", "reactions--1", "reactions-smile", "reactions-thinking_face", "reactions-heart", "reactions-tada", "interactions", "created", "updated", ]) .optional() .describe( "Sorts the results of your query by the number of comments, reactions, reactions-+1, reactions--1, reactions-smile, reactions-thinking_face, reactions-heart, reactions-tada, or interactions. You can also sort results by how recently the items were created or updated. Default: best match", ), order: z .enum(["asc", "desc"]) .optional() .describe( "Determines whether the first search result returned is the highest number of matches (desc) or lowest number of matches (asc). This parameter is ignored unless you provide sort. (default: desc)", ), }); export const SEARCH_ISSUES_TOOL: Tool = { name: "search_issues", description: "Search issues from a repository", inputSchema: zodToJsonSchema(searchIssuesInputSchema) as Tool["inputSchema"], }; export type SearchIssuesInput = z.output<typeof searchIssuesInputSchema>; export async function searchIssues(input: SearchIssuesInput) { const url = new URL(`/search/issues`, GITHUB_API_BASE_URL); url.searchParams.set("q", input.query); if (input.page) url.searchParams.set("page", input.page.toString()); if (input.per_page) url.searchParams.set("per_page", input.per_page.toString()); if (input.sort) url.searchParams.set("sort", input.sort); if (input.order) url.searchParams.set("order", input.order); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); return ok(json.value); } ``` -------------------------------------------------------------------------------- /src/tools/list-repositories-pull-requests.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.ts"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/pulls/pulls#list-pull-requests list pull requests API docs export const listRepositoriesPullRequestsInputSchema = z.object({ owner: z.string().describe("The owner of the repository"), repo: z.string().describe("The repository name"), state: z .enum(["open", "closed", "all"]) .describe("The state of the pull requests to list (default: open)"), page: z .number() .optional() .describe("The page number for pagination (default: 1)"), per_page: z .number() .optional() .describe("The number of results per page (default: 30, max: 100)."), sort: z .enum(["created", "updated", "popularity", "long-running"]) .optional() .describe( "What to sort results by. popularity will sort by the number of comments. long-running will sort by date created and will limit the results to pull requests that have been open for more than a month and have had activity within the past month. (default: created)", ), direction: z .enum(["asc", "desc"]) .optional() .describe( "The direction of the sort. Default: desc when sort is created or sort is not specified, otherwise asc", ), head: z .string() .optional() .describe( "Filter pulls by head user or head organization and branch name in the format of user:ref-name or organization:ref-name. For example: github:new-script-format or octocat:test-branch", ), base: z .string() .optional() .describe("Filter pulls by base branch name. Example: gh-pages."), }); export const LIST_REPOSITORIES_PULL_REQUESTS_TOOL: Tool = { name: "list_repositories_pull_requests", description: "List pull requests from a repository", inputSchema: zodToJsonSchema( listRepositoriesPullRequestsInputSchema, ) as Tool["inputSchema"], }; export type ListRepositoriesPullRequestsInput = z.output< typeof listRepositoriesPullRequestsInputSchema >; export async function listRepositoriesPullRequests( input: ListRepositoriesPullRequestsInput, ) { const url = new URL( `/repos/${input.owner}/${input.repo}/pulls`, GITHUB_API_BASE_URL, ); if (input.state) url.searchParams.set("state", input.state); if (input.page) url.searchParams.set("page", input.page.toString()); if (input.per_page) url.searchParams.set("per_page", input.per_page.toString()); if (input.sort) url.searchParams.set("sort", input.sort); if (input.direction) url.searchParams.set("direction", input.direction); if (input.head) url.searchParams.set("head", input.head); if (input.base) url.searchParams.set("base", input.base); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); return ok(json.value); } ``` -------------------------------------------------------------------------------- /src/tools/list-repositories-issues.ts: -------------------------------------------------------------------------------- ```typescript import { z } from "zod"; import { GITHUB_API_BASE_URL } from "../constants.ts"; import { $githubJson } from "../utils/gh-fetch.ts"; import { err, ok } from "neverthrow"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { zodToJsonSchema } from "zod-to-json-schema"; // @see https://docs.github.com/en/rest/issues/issues#list-repository-issues list repository issues API docs export const listRepositoriesIssuesInputSchema = z.object({ owner: z.string().describe("The owner of the repository"), repo: z.string().describe("The repository name"), state: z .enum(["open", "closed", "all"]) .describe("The state of the issues to list (default: open)"), page: z .number() .optional() .describe("The page number for pagination (default: 1)"), per_page: z .number() .optional() .describe("The number of results per page (default: 30, max: 100)."), sort: z .enum(["created", "updated", "comments"]) .optional() .describe("The sort order of the issues, (default: created)"), direction: z .enum(["asc", "desc"]) .optional() .describe("The direction of the sort order (default: desc)"), since: z .string() .optional() .describe( "Only show results that were last updated after the given time. This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.", ), labels: z .string() .optional() .describe("A list of comma separated label names. Example: bug,ui,@high"), milestone: z .string() .optional() .describe( "If an integer is passed, it should refer to a milestone by its number field. If the string * is passed, issues with any milestone are accepted. If the string none is passed, issues without milestones are returned.", ), assignee: z .string() .optional() .describe( "Can be the name of a user. Pass in none for issues with no assigned user, and * for issues assigned to any user.", ), creator: z.string().optional().describe("The user that created the issue."), mentioned: z .string() .optional() .describe("A user that's mentioned in the issue."), }); export const LIST_REPOSITORIES_ISSUES_TOOL: Tool = { name: "list_repositories_issues", description: "List issues from a repository", inputSchema: zodToJsonSchema( listRepositoriesIssuesInputSchema, ) as Tool["inputSchema"], }; export type ListRepositoriesIssuesInput = z.output< typeof listRepositoriesIssuesInputSchema >; export async function listRepositoriesIssues( input: ListRepositoriesIssuesInput, ) { const url = new URL( `/repos/${input.owner}/${input.repo}/issues`, GITHUB_API_BASE_URL, ); if (input.state) url.searchParams.set("state", input.state); if (input.page) url.searchParams.set("page", input.page.toString()); if (input.per_page) url.searchParams.set("per_page", input.per_page.toString()); if (input.sort) url.searchParams.set("sort", input.sort); if (input.direction) url.searchParams.set("direction", input.direction); if (input.since) url.searchParams.set("since", input.since); if (input.labels) url.searchParams.set("labels", input.labels); if (input.milestone) url.searchParams.set("milestone", input.milestone); if (input.assignee) url.searchParams.set("assignee", input.assignee); if (input.creator) url.searchParams.set("creator", input.creator); if (input.mentioned) url.searchParams.set("mentioned", input.mentioned); const json = await $githubJson(url.toString()); if (json.isErr()) return err(json.error); return ok(json.value); } ``` -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { SEARCH_REPOSITORIES_TOOL, searchRepositories, searchRepositoriesInputSchema, } from "./tools/search-repositories.js"; import { VERSION } from "./constants.js"; import { GET_ISSUE_TOOL, getIssue, getIssueInputSchema, } from "./tools/get-issue.ts"; import { GET_PULL_REQUEST_TOOL, getPullRequest, getPullRequestInputSchema, } from "./tools/get-pull-request.ts"; import { LIST_REPOSITORIES_ISSUES_TOOL, listRepositoriesIssues, listRepositoriesIssuesInputSchema, } from "./tools/list-repositories-issues.ts"; import { LIST_REPOSITORIES_PULL_REQUESTS_TOOL, listRepositoriesPullRequests, listRepositoriesPullRequestsInputSchema, } from "./tools/list-repositories-pull-requests.ts"; import { SEARCH_ISSUES_TOOL, searchIssues, searchIssuesInputSchema, } from "./tools/search-issues.ts"; import { SEARCH_CODE_TOOL, searchCode, searchCodeInputSchema, } from "./tools/search-code.ts"; import { SEARCH_USERS_TOOL, searchUsers, searchUsersInputSchema, } from "./tools/search-users.ts"; import { SEARCH_COMMITS_TOOL, searchCommits, searchCommitsInputSchema, } from "./tools/search-commits.ts"; import { SEARCH_TOPICS_TOOL, searchTopics, searchTopicsInputSchema, } from "./tools/search-topics.ts"; import { SEARCH_LABELS_TOOL, searchLabels, searchLabelsInputSchema, } from "./tools/search-labels.ts"; const server = new Server( { name: "Github MCP Server", version: VERSION }, { capabilities: { tools: {} } }, ); export const tools = [ // search SEARCH_REPOSITORIES_TOOL, SEARCH_ISSUES_TOOL, SEARCH_CODE_TOOL, SEARCH_USERS_TOOL, SEARCH_COMMITS_TOOL, SEARCH_TOPICS_TOOL, SEARCH_LABELS_TOOL, // issues GET_ISSUE_TOOL, LIST_REPOSITORIES_ISSUES_TOOL, // pull requests LIST_REPOSITORIES_PULL_REQUESTS_TOOL, GET_PULL_REQUEST_TOOL, ] satisfies Tool[]; server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const name = request.params.name; const args = request.params.arguments; if (!args) throw new Error("No arguments provided"); if (name === SEARCH_REPOSITORIES_TOOL.name) { const input = searchRepositoriesInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await searchRepositories(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === GET_ISSUE_TOOL.name) { const input = getIssueInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await getIssue(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === GET_PULL_REQUEST_TOOL.name) { const input = getPullRequestInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await getPullRequest(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === LIST_REPOSITORIES_ISSUES_TOOL.name) { const input = listRepositoriesIssuesInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await listRepositoriesIssues(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === LIST_REPOSITORIES_PULL_REQUESTS_TOOL.name) { const input = listRepositoriesPullRequestsInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await listRepositoriesPullRequests(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === SEARCH_ISSUES_TOOL.name) { const input = searchIssuesInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await searchIssues(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === SEARCH_CODE_TOOL.name) { const input = searchCodeInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await searchCode(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === SEARCH_USERS_TOOL.name) { const input = searchUsersInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await searchUsers(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === SEARCH_COMMITS_TOOL.name) { const input = searchCommitsInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await searchCommits(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === SEARCH_TOPICS_TOOL.name) { const input = searchTopicsInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await searchTopics(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } if (name === SEARCH_LABELS_TOOL.name) { const input = searchLabelsInputSchema.safeParse(args); if (!input.success) { return { isError: true, content: [{ type: "text", text: "Invalid input" }], }; } const result = await searchLabels(input.data); if (result.isErr()) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } return { content: [ { type: "text", text: JSON.stringify(result.value, null, 2) }, ], }; } throw new Error(`Unknown tool: ${name}`); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return { isError: true, content: [{ type: "text", text: "An error occurred" }], }; } }); async function run() { const transport = new StdioServerTransport(); await server.connect(transport); } run().catch((error) => { console.error("Fatal error in run()", error); process.exit(1); }); ```