Back to .md Directory

Model Context Protocol server with GitHub API

_Exported on 12/24/2025 at 14:51:35 GMT+5:30 from Cursor (2.2.43)_

May 2, 2026
0 downloads
0 views
ai mcp cursor
View source

Model Context Protocol server with GitHub API

Exported on 12/24/2025 at 14:51:35 GMT+5:30 from Cursor (2.2.43)


User

I need to build a Model Context Protocol (MCP) server from scratch using TypeScript and Node.js. This server will connect to the GitHub API.

Please guide me through the setup and write the complete code.

Tech Stack Requirements:

  • Language: TypeScript
  • Runtime: Node.js (ES Modules)
  • Libraries:
    • @modelcontextprotocol/sdk (for the server and types)
    • octokit (for GitHub API calls)
    • zod (for input validation)

Project Requirements:

  1. Setup: Help me initialize the package.json and tsconfig.json configured for ES2022 and Node16 module resolution.
  2. Authentication: The server must use a GITHUB_TOKEN from environment variables.
  3. Transport: Use StdioServerTransport to communicate via standard input/output.

Tool Definitions (Endpoints):

Please implement the following two tools:

  1. get_user_repositories

    • Goal: Fetch a list of public repositories for a specific GitHub username.
    • Input: username (string) - e.g., "sushant" or "torvalds".
    • Logic: Use Octokit to list public repos for that user. Return the repo name, description, star count, and HTML URL.
  2. get_repo_readme

    • Goal: Fetch the README.md content of a specific repository.
    • Input: owner (string), repo (string).
    • Logic: Use Octokit to get the README content. Decode the base64 content returned by GitHub and return it as plain text.

Output:

  • Provide the shell commands to install dependencies.
  • Provide the full content for src/index.ts.
  • Provide the build script commands.

Cursor

Here’s everything you need to set up and run the GitHub MCP server.

1) Init & install (run in project root)

  • npm init -y
  • npm pkg set type=module
  • npm install @modelcontextprotocol/sdk octokit zod
  • npm install -D typescript tsx @types/node
  • npm pkg set scripts.build="tsc -p tsconfig.json" scripts.dev="tsx src/index.ts" scripts.start="node dist/index.js"

2) tsconfig.json (ES2022, Node16 resolution)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

3) src/index.ts

import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/transports/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new Server(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.tool(
  "get_user_repositories",
  "List public repositories for a GitHub username.",
  z.object({
    username: z.string().min(1, "username is required")
  }),
  async (input) => {
    const { username } = input;
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(repos, null, 2)
        }
      ]
    };
  }
);

server.tool(
  "get_repo_readme",
  "Fetch README.md content for a repository.",
  z.object({
    owner: z.string().min(1, "owner is required"),
    repo: z.string().min(1, "repo is required")
  }),
  async (input) => {
    const { owner, repo } = input;

    const { data } = await octokit.rest.repos.getReadme({
      owner,
      repo
    });

    const decoded = Buffer.from(data.content, data.encoding as BufferEncoding).toString("utf8");

    return {
      content: [
        {
          type: "text",
          text: decoded
        }
      ]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

4) Build/run

  • Build: npm run build
  • Run compiled: npm start
  • Dev (TS directly): npm run dev

Remember to export GITHUB_TOKEN before running.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}
import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/transports/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new Server(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.tool(
  "get_user_repositories",
  "List public repositories for a GitHub username.",
  z.object({
    username: z.string().min(1, "username is required")
  }),
  async (input) => {
    const { username } = input;
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(repos, null, 2)
        }
      ]
    };
  }
);

server.tool(
  "get_repo_readme",
  "Fetch README.md content for a repository.",
  z.object({
    owner: z.string().min(1, "owner is required"),
    repo: z.string().min(1, "repo is required")
  }),
  async (input) => {
    const { owner, repo } = input;

    const { data } = await octokit.rest.repos.getReadme({
      owner,
      repo
    });

    const decoded = Buffer.from(data.content, data.encoding as BufferEncoding).toString("utf8");

    return {
      content: [
        {
          type: "text",
          text: decoded
        }
      ]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

User

do it on auto


Cursor

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

import { Server } from "@modelcontextprotocol/sdk/server";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/transports/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new Server(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.tool(
  "get_user_repositories",
  "List public repositories for a GitHub username.",
  z.object({
    username: z.string().min(1, "username is required")
  }),
  async (input) => {
    const { username } = input;
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(repos, null, 2)
        }
      ]
    };
  }
);

server.tool(
  "get_repo_readme",
  "Fetch README.md content for a repository.",
  z.object({
    owner: z.string().min(1, "owner is required"),
    repo: z.string().min(1, "repo is required")
  }),
  async (input) => {
    const { owner, repo } = input;
    const { data } = await octokit.rest.repos.getReadme({
      owner,
      repo
    });

    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return {
      content: [
        {
          type: "text",
          text: decoded
        }
      ]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }] };
  }
);

server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({
      owner,
      repo
    });

    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return { content: [{ type: "text", text: decoded }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);


import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }] };
  }
);

server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({
      owner,
      repo
    });

    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return { content: [{ type: "text", text: decoded }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);


MCP GitHub server is set up and built. Key pieces are below.

Code highlights

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({ username: z.string().min(1, "username is required") })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({ username, per_page: 100, sort: "updated" });
    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));
    return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }] };
  }
);
server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({ owner, repo });
    const decoded = Buffer.from(data.content, data.encoding as BufferEncoding).toString("utf8");
    return { content: [{ type: "text", text: decoded }] };
  }
);

What I set up

  • Initialized npm project and set type=module.
  • Installed @modelcontextprotocol/sdk, octokit, zod plus dev deps (typescript, tsx, @types/node).
  • Added scripts: build (tsc -p tsconfig.json), dev (tsx src/index.ts), start (node dist/index.js).
  • Created tsconfig.json targeting ES2022 with Node16 module resolution, rootDir src, outDir dist.
  • Implemented MCP server using McpServer + StdioServerTransport with the two tools.

How to run

  • Set GITHUB_TOKEN in your environment.
  • Dev (TS directly): npm run dev
  • Build: npm run build
  • Run compiled: npm start

Build status: npm run build passes. Note: the SDK requires Node 18+.

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({ username: z.string().min(1, "username is required") })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({ username, per_page: 100, sort: "updated" });
    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));
    return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }] };
  }
);
server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({ owner, repo });
    const decoded = Buffer.from(data.content, data.encoding as BufferEncoding).toString("utf8");
    return { content: [{ type: "text", text: decoded }] };
  }
);

User

I need to build a Model Context Protocol (MCP) server from scratch using TypeScript and Node.js. This server will connect to the GitHub API.

Please guide me through the setup and write the complete code.

Tech Stack Requirements:

  • Language: TypeScript
  • Runtime: Node.js (ES Modules)
  • Libraries:
    • @modelcontextprotocol/sdk (for the server and types)
    • octokit (for GitHub API calls)
    • zod (for input validation)

Project Requirements:

  1. Setup: Help me initialize the package.json and tsconfig.json configured for ES2022 and Node16 module resolution.
  2. Authentication: The server must use a GITHUB_TOKEN from environment variables.
  3. Transport: Use StdioServerTransport to communicate via standard input/output.

Tool Definitions (Endpoints):

Please implement the following two tools:

  1. get_user_repositories

    • Goal: Fetch a list of public repositories for a specific GitHub username.
    • Input: username (string) - e.g., "sushant" or "torvalds".
    • Logic: Use Octokit to list public repos for that user. Return the repo name, description, star count, and HTML URL.
  2. get_repo_readme

    • Goal: Fetch the README.md content of a specific repository.
    • Input: owner (string), repo (string).
    • Logic: Use Octokit to get the README content. Decode the base64 content returned by GitHub and return it as plain text.

Output:

  • Provide the shell commands to install dependencies.
  • Provide the full content for src/index.ts.
  • Provide the build script commands.

Cursor

All done—here are the exact commands, config, and full code you need.

Shell commands (run in project root)

  • npm init -y
  • npm pkg set type=module
  • npm install @modelcontextprotocol/sdk octokit zod
  • npm install -D typescript tsx @types/node
  • npm pkg set scripts.build="tsc -p tsconfig.json" scripts.dev="tsx src/index.ts" scripts.start="node dist/index.js"

tsconfig.json (ES2022, Node16 resolution)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

src/index.ts

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }] };
  }
);

server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({ owner, repo });
    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return { content: [{ type: "text", text: decoded }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Build scripts (already set via npm pkg set)

  • Build: npm run build
  • Dev (TS directly): npm run dev
  • Run compiled: npm start

Reminder: set GITHUB_TOKEN in your environment before running. Node 18+ is required by the SDK.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }] };
  }
);

server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({ owner, repo });
    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return { content: [{ type: "text", text: decoded }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

User

our current arcitecture

I need to build a Model Context Protocol (MCP) server from scratch using TypeScript and Node.js. This server will connect to the GitHub API.

Please guide me through the setup and write the complete code.

Tech Stack Requirements:

  • Language: TypeScript
  • Runtime: Node.js (ES Modules)
  • Libraries:
    • @modelcontextprotocol/sdk (for the server and types)
    • octokit (for GitHub API calls)
    • zod (for input validation)

Project Requirements:

  1. Setup: Help me initialize the package.json and tsconfig.json configured for ES2022 and Node16 module resolution.
  2. Authentication: The server must use a GITHUB_TOKEN from environment variables.
  3. Transport: Use StdioServerTransport to communicate via standard input/output.

Tool Definitions (Endpoints):

Please implement the following two tools:

  1. get_user_repositories

    • Goal: Fetch a list of public repositories for a specific GitHub username.
    • Input: username (string) - e.g., "sushant" or "torvalds".
    • Logic: Use Octokit to list public repos for that user. Return the repo name, description, star count, and HTML URL.
  2. get_repo_readme

    • Goal: Fetch the README.md content of a specific repository.
    • Input: owner (string), repo (string).
    • Logic: Use Octokit to get the README content. Decode the base64 content returned by GitHub and return it as plain text.

Output:

  • Provide the shell commands to install dependencies.
  • Provide the full content for src/index.ts.
  • Provide the build script commands. fix it

Cursor

Here’s the full setup and code you asked for, ready to run.

Shell commands (run in project root)

  • npm init -y
  • npm pkg set type=module
  • npm install @modelcontextprotocol/sdk octokit zod
  • npm install -D typescript tsx @types/node
  • npm pkg set scripts.build="tsc -p tsconfig.json" scripts.dev="tsx src/index.ts" scripts.start="node dist/index.js"

tsconfig.json (ES2022, Node16 resolution)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

src/index.ts

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }] };
  }
);

server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({ owner, repo });
    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return { content: [{ type: "text", text: decoded }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Build/run

  • Build: npm run build
  • Dev (TS directly): npm run dev
  • Run compiled: npm start

Remember to set GITHUB_TOKEN in your environment. The SDK requires Node 18+.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "lib": ["ES2022"],
    "types": ["node"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return { content: [{ type: "text", text: JSON.stringify(repos, null, 2) }] };
  }
);

server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({ owner, repo });
    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return { content: [{ type: "text", text: decoded }] };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

User

not able to run server


Cursor

The server is crashing because GITHUB_TOKEN isn’t set. Set it before running:

PowerShell (current session):

$env:GITHUB_TOKEN = "ghp_your_token_here"
npm run dev   # or npm start after build

To persist for future shells:

setx GITHUB_TOKEN "ghp_your_token_here"

Then open a new terminal and run npm run dev (or npm start after npm run build).

$env:GITHUB_TOKEN = "ghp_your_token_here"
npm run dev   # or npm start after build
setx GITHUB_TOKEN "ghp_your_token_here"

User

create helper funtion and tool


Cursor

import "dotenv/config";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const requireOctokit = () => {
  const githubToken = process.env.GITHUB_TOKEN;
  if (!githubToken) {
    throw new Error("GITHUB_TOKEN is required in the environment.");
  }
  return new Octokit({ auth: githubToken });
};

const textContent = (text: string) => ({
  content: [{ type: "text", text }]
});

const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return textContent(JSON.stringify(repos, null, 2));
  }
);

server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.repos.getReadme({
      owner,
      repo
    });

    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return textContent(decoded);
  }
);

server.registerTool(
  "get_rate_limit",
  {
    description: "Check remaining GitHub API rate limit for the current token."
  },
  async () => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.rateLimit.get();
    const core = data.resources.core;
    const search = data.resources.search;
    const graphql = data.resources.graphql;
    return textContent(
      JSON.stringify(
        {
          core: {
            remaining: core.remaining,
            limit: core.limit,
            reset: new Date(core.reset * 1000).toISOString()
          },
          search: {
            remaining: search.remaining,
            limit: search.limit,
            reset: new Date(search.reset * 1000).toISOString()
          },
          graphql: graphql
            ? {
                remaining: graphql.remaining,
                limit: graphql.limit,
                reset: new Date(graphql.reset * 1000).toISOString()
              }
            : undefined
        },
        null,
        2
      )
    );
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);


import "dotenv/config";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const requireOctokit = () => {
  const githubToken = process.env.GITHUB_TOKEN;
  if (!githubToken) {
    throw new Error("GITHUB_TOKEN is required in the environment.");
  }
  return new Octokit({ auth: githubToken });
};

const textContent = (text: string) => ({
  content: [{ type: "text" as const, text }]
});

const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return textContent(JSON.stringify(repos, null, 2));
  }
);

server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.repos.getReadme({
      owner,
      repo
    });

    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return textContent(decoded);
  }
);

server.registerTool(
  "get_rate_limit",
  {
    description: "Check remaining GitHub API rate limit for the current token."
  },
  async () => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.rateLimit.get();
    const core = data.resources.core;
    const search = data.resources.search;
    const graphql = data.resources.graphql;
    return textContent(
      JSON.stringify(
        {
          core: {
            remaining: core.remaining,
            limit: core.limit,
            reset: new Date(core.reset * 1000).toISOString()
          },
          search: {
            remaining: search.remaining,
            limit: search.limit,
            reset: new Date(search.reset * 1000).toISOString()
          },
          graphql: graphql
            ? {
                remaining: graphql.remaining,
                limit: graphql.limit,
                reset: new Date(graphql.reset * 1000).toISOString()
              }
            : undefined
        },
        null,
        2
      )
    );
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);


I added a helper and a new tool, and enabled .env loading.

What changed

  • Added dotenv load so .env is picked up automatically.
  • Helper requireOctokit() ensures GITHUB_TOKEN is present and returns a configured Octokit.
  • Helper textContent() standardizes MCP text responses.
  • Existing tools now reuse the helper.
  • New tool get_rate_limit reports remaining GitHub API limits for the current token.

Current src/index.ts (key parts)

import "dotenv/config";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const requireOctokit = () => {
  const githubToken = process.env.GITHUB_TOKEN;
  if (!githubToken) throw new Error("GITHUB_TOKEN is required in the environment.");
  return new Octokit({ auth: githubToken });
};

const textContent = (text: string) => ({ content: [{ type: "text" as const, text }] });

const server = new McpServer({ name: "github-mcp-server", version: "0.1.0" }, { capabilities: { tools: {} } });

server.registerTool(
  "get_user_repositories",
  { description: "List public repositories for a GitHub username.", inputSchema: z.object({ username: z.string().min(1) }) },
  async ({ username }) => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.repos.listForUser({ username, per_page: 100, sort: "updated" });
    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));
    return textContent(JSON.stringify(repos, null, 2));
  }
);

server.registerTool(
  "get_repo_readme",
  { description: "Fetch README.md content for a repository.", inputSchema: z.object({ owner: z.string().min(1), repo: z.string().min(1) }) },
  async ({ owner, repo }) => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.repos.getReadme({ owner, repo });
    const decoded = Buffer.from(data.content, data.encoding as BufferEncoding).toString("utf8");
    return textContent(decoded);
  }
);

server.registerTool(
  "get_rate_limit",
  { description: "Check remaining GitHub API rate limit for the current token." },
  async () => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.rateLimit.get();
    const core = data.resources.core;
    const search = data.resources.search;
    const graphql = data.resources.graphql;
    return textContent(
      JSON.stringify(
        {
          core: { remaining: core.remaining, limit: core.limit, reset: new Date(core.reset * 1000).toISOString() },
          search: { remaining: search.remaining, limit: search.limit, reset: new Date(search.reset * 1000).toISOString() },
          graphql: graphql
            ? { remaining: graphql.remaining, limit: graphql.limit, reset: new Date(graphql.reset * 1000).toISOString() }
            : undefined
        },
        null,
        2
      )
    );
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

Build status: npm run build passes. To run, ensure .env has GITHUB_TOKEN=... (already present), then npm run dev (or npm start after npm run build).

import "dotenv/config";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

const requireOctokit = () => {
  const githubToken = process.env.GITHUB_TOKEN;
  if (!githubToken) throw new Error("GITHUB_TOKEN is required in the environment.");
  return new Octokit({ auth: githubToken });
};

const textContent = (text: string) => ({ content: [{ type: "text" as const, text }] });

const server = new McpServer({ name: "github-mcp-server", version: "0.1.0" }, { capabilities: { tools: {} } });

server.registerTool(
  "get_user_repositories",
  { description: "List public repositories for a GitHub username.", inputSchema: z.object({ username: z.string().min(1) }) },
  async ({ username }) => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.repos.listForUser({ username, per_page: 100, sort: "updated" });
    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));
    return textContent(JSON.stringify(repos, null, 2));
  }
);

server.registerTool(
  "get_repo_readme",
  { description: "Fetch README.md content for a repository.", inputSchema: z.object({ owner: z.string().min(1), repo: z.string().min(1) }) },
  async ({ owner, repo }) => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.repos.getReadme({ owner, repo });
    const decoded = Buffer.from(data.content, data.encoding as BufferEncoding).toString("utf8");
    return textContent(decoded);
  }
);

server.registerTool(
  "get_rate_limit",
  { description: "Check remaining GitHub API rate limit for the current token." },
  async () => {
    const octokit = requireOctokit();
    const { data } = await octokit.rest.rateLimit.get();
    const core = data.resources.core;
    const search = data.resources.search;
    const graphql = data.resources.graphql;
    return textContent(
      JSON.stringify(
        {
          core: { remaining: core.remaining, limit: core.limit, reset: new Date(core.reset * 1000).toISOString() },
          search: { remaining: search.remaining, limit: search.limit, reset: new Date(search.reset * 1000).toISOString() },
          graphql: graphql
            ? { remaining: graphql.remaining, limit: graphql.limit, reset: new Date(graphql.reset * 1000).toISOString() }
            : undefined
        },
        null,
        2
      )
    );
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);

User

I need to build a Model Context Protocol (MCP) server from scratch using TypeScript and Node.js. This server will connect to the GitHub API.

Please guide me through the setup and write the complete code.

Tech Stack Requirements:

  • Language: TypeScript
  • Runtime: Node.js (ES Modules)
  • Libraries:
    • @modelcontextprotocol/sdk (for the server and types)
    • octokit (for GitHub API calls)
    • zod (for input validation)

Project Requirements:

  1. Setup: Help me initialize the package.json and tsconfig.json configured for ES2022 and Node16 module resolution.
  2. Authentication: The server must use a GITHUB_TOKEN from environment variables.
  3. Transport: Use StdioServerTransport to communicate via standard input/output.

Tool Definitions (Endpoints):

Please implement the following two tools:

  1. get_user_repositories

    • Goal: Fetch a list of public repositories for a specific GitHub username.
    • Input: username (string) - e.g., "sushant" or "torvalds".
    • Logic: Use Octokit to list public repos for that user. Return the repo name, description, star count, and HTML URL.
  2. get_repo_readme

    • Goal: Fetch the README.md content of a specific repository.
    • Input: owner (string), repo (string).
    • Logic: Use Octokit to get the README content. Decode the base64 content returned by GitHub and return it as plain text.

Output:

  • Provide the shell commands to install dependencies.
  • Provide the full content for src/index.ts.
  • Provide the build script commands.

Cursor

Reviewing the current setup and providing the complete code with just the two required tools.

Providing the complete setup with just the two required tools. The project is already configured; here's the summary and the minimal code:

Updating src/index.ts to include only the two required tools, keeping the code clean:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Octokit } from "octokit";

// Authentication: Get GITHUB_TOKEN from environment variables
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
  throw new Error("GITHUB_TOKEN is required in the environment.");
}

const octokit = new Octokit({ auth: githubToken });

// Initialize MCP Server
const server = new McpServer(
  { name: "github-mcp-server", version: "0.1.0" },
  { capabilities: { tools: {} } }
);

// Tool 1: get_user_repositories
// Fetch a list of public repositories for a specific GitHub username
server.registerTool(
  "get_user_repositories",
  {
    description: "List public repositories for a GitHub username.",
    inputSchema: z.object({
      username: z.string().min(1, "username is required")
    })
  },
  async ({ username }) => {
    const { data } = await octokit.rest.repos.listForUser({
      username,
      per_page: 100,
      sort: "updated"
    });

    // Return repo name, description, star count, and HTML URL
    const repos = data.map((repo) => ({
      name: repo.name,
      description: repo.description,
      stars: repo.stargazers_count,
      html_url: repo.html_url
    }));

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(repos, null, 2)
        }
      ]
    };
  }
);

// Tool 2: get_repo_readme
// Fetch the README.md content of a specific repository
server.registerTool(
  "get_repo_readme",
  {
    description: "Fetch README.md content for a repository.",
    inputSchema: z.object({
      owner: z.string().min(1, "owner is required"),
      repo: z.string().min(1, "repo is required")
    })
  },
  async ({ owner, repo }) => {
    const { data } = await octokit.rest.repos.getReadme({
      owner,
      repo
    });

    // Decode the base64 content returned by GitHub and return as plain text
    const decoded = Buffer.from(
      data.content,
      data.encoding as BufferEncoding
    ).toString("utf8");

    return {
      content: [
        {
          type: "text",
          text: decoded
        }
      ]
    };
  }
);

// Connect to transport using StdioServerTransport
const transport = new StdioServerTransport();
await server.connect(transport);


Setup complete. Summary:

Project setup

1. Shell commands (already executed)

npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/sdk octokit zod
npm install -D typescript tsx @types/node
npm pkg set scripts.build="tsc -p tsconfig.json" scripts.dev="tsx src/index.ts" scripts.start="node dist/index.js"

2. Configuration files

package.json — Already configured with:

  • "type": "module" for ES Modules
  • Dependencies: @modelcontextprotocol/sdk, octokit, zod
  • Dev dependencies: typescript, tsx, @types/node
  • Scripts: build, dev, start

tsconfig.json — Already configured with:

  • target: "ES2022"
  • module: "Node16"
  • moduleResolution: "Node16"
  • rootDir: "src", outDir: "dist"

3. Complete src/index.ts

The file now contains:

  • Authentication using GITHUB_TOKEN from environment variables
  • Two tools:
    1. get_user_repositories — Lists public repos for a username
    2. get_repo_readme — Fetches README content for a repo
  • StdioServerTransport for communication

4. Build scripts

  • Build: npm run build (compiles TypeScript to dist/)
  • Dev: npm run dev (runs TypeScript directly with tsx)
  • Start: npm start (runs compiled JavaScript from dist/)

Running the server

  1. Set the environment variable:

    $env:GITHUB_TOKEN = "your_token_here"
    
  2. Run the server:

    npm run dev    # Development mode
    # OR
    npm run build  # Build first
    npm start      # Run compiled version
    

Build verified: TypeScript compilation succeeds. The server is ready to use.

npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/sdk octokit zod
npm install -D typescript tsx @types/node
npm pkg set scripts.build="tsc -p tsconfig.json" scripts.dev="tsx src/index.ts" scripts.start="node dist/index.js"
   $env:GITHUB_TOKEN = "your_token_here"
   npm run dev    # Development mode
   # OR
   npm run build  # Build first
   npm start      # Run compiled version

User

You are a senior backend engineer.

Build a simple MCP (Model Context Protocol) server in Node.js (TypeScript) that exposes a tool to fetch GitHub repositories for a given user.

Requirements

  1. MCP Protocol

Use JSON-RPC 2.0

Single endpoint: POST /mcp

Support these MCP methods:

tools/list

tools/call

  1. Tool to Implement

Create an MCP tool:

Tool name: getGithubRepos

Description: Fetch public GitHub repositories for a given username.

Input schema:

{ "type": "object", "properties": { "username": { "type": "string" } }, "required": ["username"] }

Behavior:

Call GitHub public REST API https://api.github.com/users/{username}/repos

Return a simplified response:

name

html_url

description

stargazers_count

  1. Folder Structure (keep it simple)

Use this structure exactly:

src/ ├── index.ts ├── server.ts ├── mcp.ts ├── tools/ │ ├── github.ts │ └── index.ts └── services/ └── github.service.ts

  1. Architectural Rules

server.ts → HTTP server only

mcp.ts → MCP router (method dispatch)

tools/ → MCP tool definitions only

services/ → External API logic (GitHub API)

No unnecessary abstractions

Clear separation of concerns

  1. Error Handling

Return JSON-RPC compliant errors

Handle:

Invalid tool name

Missing username

GitHub API errors (404, rate limit)

  1. Tech Constraints

Node.js + TypeScript

Use express

Use native fetch or axios

No authentication required (public GitHub API)

Code should be clean, readable, and production-style

  1. Output Expectations

Generate all required files

Include example JSON-RPC requests for:

tools/list

tools/call

Do NOT include explanations unless necessary

Focus on correct, working implementation

Goal

At the end, I should be able to:

Run the server

Call /mcp

Discover the tool

Fetch GitHub repos via MCP


Cursor

Building a custom MCP server with Express and JSON-RPC 2.0. Creating the required files with the specified structure.

Creating the required files:

export interface GitHubRepo {
  name: string;
  html_url: string;
  description: string | null;
  stargazers_count: number;
}

export class GitHubService {
  private readonly baseUrl = "https://api.github.com";

  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/users/${encodeURIComponent(username)}/repos`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`User '${username}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repos = await response.json();

    return repos.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }
}

import { GitHubService } from "../services/github.service.js";

export interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: {
    type: string;
    properties: Record<string, any>;
    required: string[];
  };
}

export interface ToolCallResult {
  content: Array<{
    type: string;
    text: string;
  }>;
}

export class GithubTool {
  private githubService: GitHubService;

  constructor() {
    this.githubService = new GitHubService();
  }

  getDefinition(): ToolDefinition {
    return {
      name: "getGithubRepos",
      description: "Fetch public GitHub repositories for a given username.",
      inputSchema: {
        type: "object",
        properties: {
          username: {
            type: "string",
          },
        },
        required: ["username"],
      },
    };
  }

  async call(params: { username: string }): Promise<ToolCallResult> {
    if (!params.username) {
      throw new Error("username is required");
    }

    const repos = await this.githubService.getUserRepos(params.username);

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(repos, null, 2),
        },
      ],
    };
  }
}

import { GithubTool } from "./github.js";
import type { ToolDefinition, ToolCallResult } from "./github.js";

export class ToolRegistry {
  private tools: Map<string, GithubTool>;

  constructor() {
    this.tools = new Map();
    this.registerTool(new GithubTool());
  }

  private registerTool(tool: GithubTool): void {
    const definition = tool.getDefinition();
    this.tools.set(definition.name, tool);
  }

  listTools(): ToolDefinition[] {
    return Array.from(this.tools.values()).map((tool) => tool.getDefinition());
  }

  async callTool(name: string, params: any): Promise<ToolCallResult> {
    const tool = this.tools.get(name);
    if (!tool) {
      throw new Error(`Tool '${name}' not found`);
    }
    return tool.call(params);
  }

  hasTool(name: string): boolean {
    return this.tools.has(name);
  }
}

import { ToolRegistry } from "./tools/index.js";

export interface JsonRpcRequest {
  jsonrpc: "2.0";
  id: string | number | null;
  method: string;
  params?: any;
}

export interface JsonRpcResponse {
  jsonrpc: "2.0";
  id: string | number | null;
  result?: any;
  error?: {
    code: number;
    message: string;
    data?: any;
  };
}

export class MCPRouter {
  private toolRegistry: ToolRegistry;

  constructor() {
    this.toolRegistry = new ToolRegistry();
  }

  async handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
    const { method, params, id } = request;

    try {
      let result: any;

      switch (method) {
        case "tools/list":
          result = await this.handleToolsList();
          break;

        case "tools/call":
          result = await this.handleToolsCall(params);
          break;

        default:
          throw {
            code: -32601,
            message: `Method not found: ${method}`,
          };
      }

      return {
        jsonrpc: "2.0",
        id,
        result,
      };
    } catch (error: any) {
      return {
        jsonrpc: "2.0",
        id,
        error: {
          code: error.code || -32000,
          message: error.message || "Internal error",
          data: error.data,
        },
      };
    }
  }

  private async handleToolsList(): Promise<{ tools: any[] }> {
    const tools = this.toolRegistry.listTools();
    return {
      tools: tools.map((tool) => ({
        name: tool.name,
        description: tool.description,
        inputSchema: tool.inputSchema,
      })),
    };
  }

  private async handleToolsCall(params: any): Promise<any> {
    if (!params || typeof params !== "object") {
      throw {
        code: -32602,
        message: "Invalid params",
      };
    }

    const { name, arguments: args } = params;

    if (!name || typeof name !== "string") {
      throw {
        code: -32602,
        message: "Tool name is required",
      };
    }

    if (!this.toolRegistry.hasTool(name)) {
      throw {
        code: -32601,
        message: `Tool '${name}' not found`,
      };
    }

    try {
      const result = await this.toolRegistry.callTool(name, args || {});
      return result;
    } catch (error: any) {
      throw {
        code: -32000,
        message: error.message || "Tool execution failed",
        data: error,
      };
    }
  }
}

import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /health - Health check`);
    });
  }
}

import { HTTPServer } from "./server.js";

const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;

const server = new HTTPServer(PORT);
server.start();

export interface GitHubRepo {
  name: string;
  html_url: string;
  description: string | null;
  stargazers_count: number;
}

export class GitHubService {
  private readonly baseUrl = "https://api.github.com";

  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/users/${encodeURIComponent(username)}/repos`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`User '${username}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repos: any[] = await response.json();

    return repos.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }
}


export interface GitHubRepo {
  name: string;
  html_url: string;
  description: string | null;
  stargazers_count: number;
}

export class GitHubService {
  private readonly baseUrl = "https://api.github.com";

  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/users/${encodeURIComponent(username)}/repos`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`User '${username}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repos = (await response.json()) as any[];

    return repos.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }
}


Creating example JSON-RPC requests:

{
  "tools_list_request": {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  },
  "tools_list_response": {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "tools": [
        {
          "name": "getGithubRepos",
          "description": "Fetch public GitHub repositories for a given username.",
          "inputSchema": {
            "type": "object",
            "properties": {
              "username": {
                "type": "string"
              }
            },
            "required": ["username"]
          }
        }
      ]
    }
  },
  "tools_call_request": {
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "getGithubRepos",
      "arguments": {
        "username": "torvalds"
      }
    }
  },
  "tools_call_response": {
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
      "content": [
        {
          "type": "text",
          "text": "[\n  {\n    \"name\": \"linux\",\n    \"html_url\": \"https://github.com/torvalds/linux\",\n    \"description\": \"Linux kernel source tree\",\n    \"stargazers_count\": 123456\n  }\n]"
        }
      ]
    }
  },
  "error_example": {
    "jsonrpc": "2.0",
    "id": 3,
    "error": {
      "code": -32601,
      "message": "Tool 'invalidTool' not found"
    }
  }
}

export interface GitHubRepo {
  name: string;
  html_url: string;
  description: string | null;
  stargazers_count: number;
}

export class GitHubService {
  private readonly baseUrl = "https://api.github.com";

  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/users/${encodeURIComponent(username)}/repos`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`User '${username}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repos = (await response.json()) as any[];

    return repos.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }
}


Verifying the folder structure:

Project Structure

src/
 ├── index.ts
 ├── server.ts
 ├── mcp.ts
 ├── tools/
 │    ├── github.ts
 │    └── index.ts
 └── services/
      └── github.service.ts

Run the Server

npm run dev    # Development mode
# OR
npm run build  # Build
npm start      # Production mode

Server runs on http://localhost:3000

Example JSON-RPC Requests

tools/list:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}

tools/call:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "getGithubRepos",
    "arguments": {
      "username": "torvalds"
    }
  }
}

Send POST requests to http://localhost:3000/mcp with these JSON-RPC payloads.

All files are implemented with error handling, JSON-RPC 2.0 compliance, and clean separation of concerns. Build verified.

src/
 ├── index.ts
 ├── server.ts
 ├── mcp.ts
 ├── tools/
 │    ├── github.ts
 │    └── index.ts
 └── services/
      └── github.service.ts
npm run dev    # Development mode
# OR
npm run build  # Build
npm start      # Production mode
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list"
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "getGithubRepos",
    "arguments": {
      "username": "torvalds"
    }
  }
}

User

create get endpoint to get any github repo via macp server


Cursor

Adding a GET endpoint to fetch any GitHub repository. Updating the service and server:

export interface GitHubRepo {
  name: string;
  html_url: string;
  description: string | null;
  stargazers_count: number;
}

export interface GitHubRepoDetails extends GitHubRepo {
  full_name: string;
  owner: {
    login: string;
    html_url: string;
  };
  language: string | null;
  forks_count: number;
  open_issues_count: number;
  created_at: string;
  updated_at: string;
  default_branch: string;
}

export class GitHubService {
  private readonly baseUrl = "https://api.github.com";

  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/users/${encodeURIComponent(username)}/repos`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`User '${username}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repos = (await response.json()) as any[];

    return repos.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }
}


export interface GitHubRepo {
  name: string;
  html_url: string;
  description: string | null;
  stargazers_count: number;
}

export interface GitHubRepoDetails extends GitHubRepo {
  full_name: string;
  owner: {
    login: string;
    html_url: string;
  };
  language: string | null;
  forks_count: number;
  open_issues_count: number;
  created_at: string;
  updated_at: string;
  default_branch: string;
}

export class GitHubService {
  private readonly baseUrl = "https://api.github.com";

  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/users/${encodeURIComponent(username)}/repos`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`User '${username}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repos = (await response.json()) as any[];

    return repos.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }

  async getRepo(owner: string, repo: string): Promise<GitHubRepoDetails> {
    const url = `${this.baseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`Repository '${owner}/${repo}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repoData = (await response.json()) as any;

    return {
      name: repoData.name,
      html_url: repoData.html_url,
      description: repoData.description,
      stargazers_count: repoData.stargazers_count,
      full_name: repoData.full_name,
      owner: {
        login: repoData.owner.login,
        html_url: repoData.owner.html_url,
      },
      language: repoData.language,
      forks_count: repoData.forks_count,
      open_issues_count: repoData.open_issues_count,
      created_at: repoData.created_at,
      updated_at: repoData.updated_at,
      default_branch: repoData.default_branch,
    };
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /repos/:owner/:repo - Get GitHub repository`);
      console.log(`GET /health - Health check`);
    });
  }
}


Added a GET endpoint to fetch any GitHub repository. Summary:

New GET Endpoint

Endpoint: GET /repos/:owner/:repo

Example Usage:

GET http://localhost:3000/repos/torvalds/linux
GET http://localhost:3000/repos/microsoft/vscode

What Was Added

  1. Enhanced GitHub Service (src/services/github.service.ts):

    • Added getRepo(owner, repo) method
    • Returns detailed repository information including:
      • Basic info (name, description, stars)
      • Owner details
      • Language, forks, issues
      • Timestamps and default branch
  2. New REST Endpoint (src/server.ts):

    • GET /repos/:owner/:repo endpoint
    • Uses the GitHub service directly
    • Handles errors (404, 429, 500)
    • Returns JSON response

Response Format

{
  "name": "linux",
  "html_url": "https://github.com/torvalds/linux",
  "description": "Linux kernel source tree",
  "stargazers_count": 123456,
  "full_name": "torvalds/linux",
  "owner": {
    "login": "torvalds",
    "html_url": "https://github.com/torvalds"
  },
  "language": "C",
  "forks_count": 45678,
  "open_issues_count": 123,
  "created_at": "2011-09-04T22:48:12Z",
  "updated_at": "2024-01-15T10:30:00Z",
  "default_branch": "master"
}

Build verified. The endpoint is ready to use.

GET http://localhost:3000/repos/torvalds/linux
GET http://localhost:3000/repos/microsoft/vscode
{
  "name": "linux",
  "html_url": "https://github.com/torvalds/linux",
  "description": "Linux kernel source tree",
  "stargazers_count": 123456,
  "full_name": "torvalds/linux",
  "owner": {
    "login": "torvalds",
    "html_url": "https://github.com/torvalds"
  },
  "language": "C",
  "forks_count": 45678,
  "open_issues_count": 123,
  "created_at": "2011-09-04T22:48:12Z",
  "updated_at": "2024-01-15T10:30:00Z",
  "default_branch": "master"
}

User

Cannot GET /repos/torvalds/linux

http://localhost:3000/repos/torvalds/linux


Cursor

Checking the server code to diagnose the routing issue:

Checking if the server is running and verifying the route is registered correctly:

Route is in the compiled code. Rebuilding and checking if the server needs a restart:

Adding a catch-all route for debugging and verifying the route order:

import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /repos/:owner/:repo",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /repos/:owner/:repo - Get GitHub repository`);
      console.log(`GET /health - Health check`);
    });
  }
}


Route is in the code. Restart the server to load the changes:

  1. Stop the current server (Ctrl+C if running in terminal)
  2. Restart it:
    npm run dev
    
    or if using the compiled version:
    npm run build
    npm start
    

The route /repos/:owner/:repo is registered. After restarting, test:

  • http://localhost:3000/repos/torvalds/linux
  • http://localhost:3000/health (to verify the server is running)

If it still doesn't work after restart, checking for a potential route registration issue:

   npm run dev
   npm run build
   npm start

Route is correctly defined. Restart the server to load the changes. Adding a simple test to verify the route is working:

The route is correctly defined. Restart the server to load the changes.

Steps to fix:

  1. Stop the current server (press Ctrl+C in the terminal where it's running)

  2. Rebuild and restart:

    npm run build
    npm start
    

    Or for development mode:

    npm run dev
    
  3. Test the endpoint:

    • http://localhost:3000/repos/torvalds/linux
    • http://localhost:3000/health (to verify server is running)

The route /repos/:owner/:repo is registered at line 70. A catch-all 404 handler was added to show available routes if a route isn't found.

After restarting, the endpoint should work. If you still see "Cannot GET", check the server console output—it should show:

GET /repos/:owner/:repo - Get GitHub repository

This confirms the route is registered.

   npm run build
   npm start
   npm run dev
   npm run build
   npm start
   npm run dev
GET /repos/:owner/:repo - Get GitHub repository

User

http://localhost:3000/repos/torvalds/linux with this endpoin can i get any repo like any username


Cursor

Yes. The endpoint GET /repos/:owner/:repo works for any GitHub repository. Replace :owner and :repo with any GitHub username/organization and repository name.

Examples:

http://localhost:3000/repos/torvalds/linux
http://localhost:3000/repos/microsoft/vscode
http://localhost:3000/repos/facebook/react
http://localhost:3000/repos/vercel/next.js
http://localhost:3000/repos/nodejs/node
http://localhost:3000/repos/your-username/your-repo

How it works:

The route uses Express path parameters:

  • :owner = GitHub username or organization name
  • :repo = Repository name

So http://localhost:3000/repos/torvalds/linux becomes:

  • owner = "torvalds"
  • repo = "linux"

The server extracts these from the URL and calls the GitHub API to fetch that repository's data.

Try it:

You can test with any public GitHub repository:

  • http://localhost:3000/repos/microsoft/vscode
  • http://localhost:3000/repos/facebook/react
  • http://localhost:3000/repos/your-username/your-repo-name

The endpoint returns repository details including name, description, stars, forks, language, and more.

http://localhost:3000/repos/torvalds/linux
http://localhost:3000/repos/microsoft/vscode
http://localhost:3000/repos/facebook/react
http://localhost:3000/repos/vercel/next.js
http://localhost:3000/repos/nodejs/node
http://localhost:3000/repos/your-username/your-repo

User

how is mcp helping in this


Cursor

Explaining how MCP is used in this setup:

How MCP helps in this setup:

  1. Standardized AI/LLM integration

    • MCP provides a standard protocol for AI assistants (like Claude, ChatGPT) to discover and use tools
    • The POST /mcp endpoint handles MCP protocol requests
  2. Tool discovery (tools/list)

    • AI models can query available tools:
    POST /mcp
    {
      "jsonrpc": "2.0",
      "id": 1,
      "method": "tools/list"
    }
    
    • Returns tool definitions with schemas so the AI knows what parameters are needed
  3. Tool execution (tools/call)

    • AI models can call tools with validated parameters:
    POST /mcp
    {
      "jsonrpc": "2.0",
      "id": 2,
      "method": "tools/call",
      "params": {
        "name": "getGithubRepos",
        "arguments": {
          "username": "torvalds"
        }
      }
    }
    
  4. Benefits over REST

    • Self-describing tools: schemas tell the AI what's available
    • Standardized protocol: works with any MCP-compatible AI
    • Structured responses: consistent format for AI processing
    • Error handling: JSON-RPC error codes
  5. Current architecture

    AI Assistant → POST /mcp (tools/call) → MCP Router → Tool Registry → GitHub Service → GitHub API
    
    Browser/App → GET /repos/:owner/:repo → GitHub Service → GitHub API
    

The REST endpoint (GET /repos) is a convenience for direct HTTP calls. MCP enables AI assistants to:

  • Discover available GitHub tools
  • Understand required parameters from schemas
  • Call tools programmatically
  • Get structured, consistent responses

This makes your server compatible with AI assistants that support MCP, allowing them to fetch GitHub data through a standardized interface.

   POST /mcp
   {
     "jsonrpc": "2.0",
     "id": 1,
     "method": "tools/list"
   }
   POST /mcp
   {
     "jsonrpc": "2.0",
     "id": 2,
     "method": "tools/call",
     "params": {
       "name": "getGithubRepos",
       "arguments": {
         "username": "torvalds"
       }
     }
   }
   AI Assistant → POST /mcp (tools/call) → MCP Router → Tool Registry → GitHub Service → GitHub API
   
   Browser/App → GET /repos/:owner/:repo → GitHub Service → GitHub API

User

like i want to fetch all repo available on githubmane


Cursor

Adding a tool and endpoint to search all GitHub repositories (not just by username). Implementing:

Adding search functionality to find all repositories on GitHub:

export interface GitHubRepo {
  name: string;
  html_url: string;
  description: string | null;
  stargazers_count: number;
}

export interface GitHubRepoDetails extends GitHubRepo {
  full_name: string;
  owner: {
    login: string;
    html_url: string;
  };
  language: string | null;
  forks_count: number;
  open_issues_count: number;
  created_at: string;
  updated_at: string;
  default_branch: string;
}

export class GitHubService {
  private readonly baseUrl = "https://api.github.com";

  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/users/${encodeURIComponent(username)}/repos`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`User '${username}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repos = (await response.json()) as any[];

    return repos.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }

  async getRepo(owner: string, repo: string): Promise<GitHubRepoDetails> {
    const url = `${this.baseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`Repository '${owner}/${repo}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repoData = (await response.json()) as any;

    return {
      name: repoData.name,
      html_url: repoData.html_url,
      description: repoData.description,
      stargazers_count: repoData.stargazers_count,
      full_name: repoData.full_name,
      owner: {
        login: repoData.owner.login,
        html_url: repoData.owner.html_url,
      },
      language: repoData.language,
      forks_count: repoData.forks_count,
      open_issues_count: repoData.open_issues_count,
      created_at: repoData.created_at,
      updated_at: repoData.updated_at,
      default_branch: repoData.default_branch,
    };
  }

  async searchRepos(query: string, sort: string = "stars", order: string = "desc", perPage: number = 30): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&order=${order}&per_page=${perPage}`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      if (response.status === 422) {
        throw new Error("Invalid search query");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const data = (await response.json()) as any;

    return data.items.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }
}


import { GitHubService } from "../services/github.service.js";
import type { ToolDefinition, ToolCallResult } from "./github.js";

export class SearchReposTool {
  private githubService: GitHubService;

  constructor() {
    this.githubService = new GitHubService();
  }

  getDefinition(): ToolDefinition {
    return {
      name: "searchGithubRepos",
      description: "Search all public GitHub repositories by query (language, topic, name, etc.).",
      inputSchema: {
        type: "object",
        properties: {
          query: {
            type: "string",
            description: "Search query (e.g., 'language:javascript', 'topic:react', 'nodejs', etc.)",
          },
          sort: {
            type: "string",
            enum: ["stars", "forks", "help-wanted-issues", "updated"],
            description: "Sort results by: stars, forks, help-wanted-issues, or updated",
            default: "stars",
          },
          order: {
            type: "string",
            enum: ["desc", "asc"],
            description: "Sort order: desc (descending) or asc (ascending)",
            default: "desc",
          },
          perPage: {
            type: "number",
            description: "Number of results per page (max 100)",
            default: 30,
            minimum: 1,
            maximum: 100,
          },
        },
        required: ["query"],
      },
    };
  }

  async call(params: { query: string; sort?: string; order?: string; perPage?: number }): Promise<ToolCallResult> {
    if (!params.query) {
      throw new Error("query is required");
    }

    const repos = await this.githubService.searchRepos(
      params.query,
      params.sort || "stars",
      params.order || "desc",
      params.perPage || 30
    );

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(repos, null, 2),
        },
      ],
    };
  }
}

import { GithubTool } from "./github.js";
import { SearchReposTool } from "./search.js";
import type { ToolDefinition, ToolCallResult } from "./github.js";

interface Tool {
  getDefinition(): ToolDefinition;
  call(params: any): Promise<ToolCallResult>;
}

export class ToolRegistry {
  private tools: Map<string, Tool>;

  constructor() {
    this.tools = new Map();
    this.registerTool(new GithubTool());
    this.registerTool(new SearchReposTool());
  }

  private registerTool(tool: Tool): void {
    const definition = tool.getDefinition();
    this.tools.set(definition.name, tool);
  }

  listTools(): ToolDefinition[] {
    return Array.from(this.tools.values()).map((tool) => tool.getDefinition());
  }

  async callTool(name: string, params: any): Promise<ToolCallResult> {
    const tool = this.tools.get(name);
    if (!tool) {
      throw new Error(`Tool '${name}' not found`);
    }
    return tool.call(params);
  }

  hasTool(name: string): boolean {
    return this.tools.has(name);
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /repos/:owner/:repo",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /repos/:owner/:repo - Get GitHub repository`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /repos/:owner/:repo",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /repos/:owner/:repo - Get GitHub repository`);
      console.log(`GET /search/repos?q=query - Search all GitHub repositories`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /repos/:owner/:repo",
          "GET /search/repos?q=query",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /repos/:owner/:repo - Get GitHub repository`);
      console.log(`GET /search/repos?q=query - Search all GitHub repositories`);
      console.log(`GET /health - Health check`);
    });
  }
}


Added search functionality to fetch repositories from GitHub. Here's what's available:

New Features

1. REST Endpoint - Search All Repositories

GET /search/repos?q=query&sort=stars&order=desc&per_page=30

Examples:

http://localhost:3000/search/repos?q=javascript
http://localhost:3000/search/repos?q=language:python
http://localhost:3000/search/repos?q=topic:react
http://localhost:3000/search/repos?q=nodejs+stars:>1000
http://localhost:3000/search/repos?q=machine+learning&sort=stars&order=desc&per_page=50

2. MCP Tool - searchGithubRepos

Available via the MCP protocol at POST /mcp:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "searchGithubRepos",
    "arguments": {
      "query": "language:javascript",
      "sort": "stars",
      "order": "desc",
      "perPage": 30
    }
  }
}

Search Query Examples

  • language:javascript - Repositories in JavaScript
  • topic:react - Repositories with "react" topic
  • nodejs - Repositories with "nodejs" in name/description
  • stars:>1000 - Repositories with more than 1000 stars
  • machine learning - General search
  • user:torvalds - Repositories by specific user

Available Tools

  1. getGithubRepos - Get repos for a specific username
  2. searchGithubRepos - Search all GitHub repositories

Restart your server to use the new search functionality.

GET /search/repos?q=query&sort=stars&order=desc&per_page=30
http://localhost:3000/search/repos?q=javascript
http://localhost:3000/search/repos?q=language:python
http://localhost:3000/search/repos?q=topic:react
http://localhost:3000/search/repos?q=nodejs+stars:>1000
http://localhost:3000/search/repos?q=machine+learning&sort=stars&order=desc&per_page=50
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "searchGithubRepos",
    "arguments": {
      "query": "language:javascript",
      "sort": "stars",
      "order": "desc",
      "perPage": 30
    }
  }
}

User

endpoint should work like it can fetch all github repos get by giveving only username


Cursor

Adding a REST endpoint that accepts a username and returns all their repositories:

import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/users/:username/repos", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /repos/:owner/:repo",
          "GET /search/repos?q=query",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /repos/:owner/:repo - Get GitHub repository`);
      console.log(`GET /search/repos?q=query - Search all GitHub repositories`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/users/:username/repos", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /users/:username/repos",
          "GET /repos/:owner/:repo",
          "GET /search/repos?q=query",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /repos/:owner/:repo - Get GitHub repository`);
      console.log(`GET /search/repos?q=query - Search all GitHub repositories`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/users/:username/repos", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /users/:username/repos",
          "GET /repos/:owner/:repo",
          "GET /search/repos?q=query",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /users/:username/repos - Get all repositories for a user`);
      console.log(`GET /repos/:owner/:repo - Get specific GitHub repository`);
      console.log(`GET /search/repos?q=query - Search all GitHub repositories`);
      console.log(`GET /health - Health check`);
    });
  }
}


Added an endpoint to fetch all repositories for a username.

New Endpoint

GET /users/:username/repos

Usage Examples:

http://localhost:3000/users/torvalds/repos
http://localhost:3000/users/microsoft/repos
http://localhost:3000/users/facebook/repos
http://localhost:3000/users/your-username/repos

Response Format:

{
  "username": "torvalds",
  "total": 5,
  "repositories": [
    {
      "name": "linux",
      "html_url": "https://github.com/torvalds/linux",
      "description": "Linux kernel source tree",
      "stargazers_count": 123456
    },
    {
      "name": "subsurface",
      "html_url": "https://github.com/torvalds/subsurface",
      "description": "Subsurface divelog program",
      "stargazers_count": 5432
    }
    // ... more repos
  ]
}

All Available Endpoints:

  1. GET /users/:username/repos - Get all repos for a username
  2. GET /repos/:owner/:repo - Get a specific repository
  3. GET /search/repos?q=query - Search all GitHub repositories
  4. POST /mcp - MCP protocol endpoint

Restart your server and use /users/:username/repos with any GitHub username.

http://localhost:3000/users/torvalds/repos
http://localhost:3000/users/microsoft/repos
http://localhost:3000/users/facebook/repos
http://localhost:3000/users/your-username/repos
{
  "username": "torvalds",
  "total": 5,
  "repositories": [
    {
      "name": "linux",
      "html_url": "https://github.com/torvalds/linux",
      "description": "Linux kernel source tree",
      "stargazers_count": 123456
    },
    {
      "name": "subsurface",
      "html_url": "https://github.com/torvalds/subsurface",
      "description": "Subsurface divelog program",
      "stargazers_count": 5432
    }
    // ... more repos
  ]
}

User

http://localhost:3000/repos/sushant6095 why its not able to get my repos { "error": "Cannot GET /repos/sushant6095", "availableRoutes": [ "POST /mcp", "GET /users/:username/repos", "GET /repos/:owner/:repo", "GET /search/repos?q=query", "GET /health" ] }


Cursor

The route /repos/sushant6095 doesn't match any endpoint. Use /users/sushant6095/repos instead. Adding a simpler route /repos/:username for convenience:

Adding a simpler route /repos/:username that works with just a username. Placing it before the two-parameter route to avoid conflicts:

import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/users/:username/repos", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:username", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /users/:username/repos",
          "GET /repos/:owner/:repo",
          "GET /search/repos?q=query",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /users/:username/repos - Get all repositories for a user`);
      console.log(`GET /repos/:owner/:repo - Get specific GitHub repository`);
      console.log(`GET /search/repos?q=query - Search all GitHub repositories`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/users/:username/repos", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:username", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /users/:username/repos",
          "GET /repos/:username",
          "GET /repos/:owner/:repo",
          "GET /search/repos?q=query",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /users/:username/repos - Get all repositories for a user`);
      console.log(`GET /repos/:owner/:repo - Get specific GitHub repository`);
      console.log(`GET /search/repos?q=query - Search all GitHub repositories`);
      console.log(`GET /health - Health check`);
    });
  }
}


import express, { Request, Response } from "express";
import { MCPRouter, JsonRpcRequest } from "./mcp.js";
import { GitHubService } from "./services/github.service.js";

export class HTTPServer {
  private app: express.Application;
  private mcpRouter: MCPRouter;
  private githubService: GitHubService;
  private port: number;

  constructor(port: number = 3000) {
    this.app = express();
    this.mcpRouter = new MCPRouter();
    this.githubService = new GitHubService();
    this.port = port;
    this.setupMiddleware();
    this.setupRoutes();
  }

  private setupMiddleware(): void {
    this.app.use(express.json());
  }

  private setupRoutes(): void {
    this.app.post("/mcp", async (req: Request, res: Response) => {
      try {
        const request: JsonRpcRequest = req.body;

        if (!request.jsonrpc || request.jsonrpc !== "2.0") {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: jsonrpc must be '2.0'",
            },
          });
        }

        if (!request.method) {
          return res.status(400).json({
            jsonrpc: "2.0",
            id: request.id || null,
            error: {
              code: -32600,
              message: "Invalid Request: method is required",
            },
          });
        }

        const response = await this.mcpRouter.handleRequest(request);
        res.json(response);
      } catch (error: any) {
        res.status(500).json({
          jsonrpc: "2.0",
          id: req.body.id || null,
          error: {
            code: -32603,
            message: "Internal error",
            data: error.message,
          },
        });
      }
    });

    this.app.get("/health", (req: Request, res: Response) => {
      res.json({ status: "ok" });
    });

    this.app.get("/users/:username/repos", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:username", async (req: Request, res: Response) => {
      try {
        const { username } = req.params;

        if (!username) {
          return res.status(400).json({
            error: "Username parameter is required",
          });
        }

        const repos = await this.githubService.getUserRepos(username);

        res.json({
          username,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/repos/:owner/:repo", async (req: Request, res: Response) => {
      try {
        const { owner, repo } = req.params;

        if (!owner || !repo) {
          return res.status(400).json({
            error: "Owner and repo parameters are required",
          });
        }

        const repoData = await this.githubService.getRepo(owner, repo);
        res.json(repoData);
      } catch (error: any) {
        if (error.message.includes("not found")) {
          return res.status(404).json({
            error: error.message,
          });
        }
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.get("/search/repos", async (req: Request, res: Response) => {
      try {
        const { q, sort, order, per_page } = req.query;

        if (!q || typeof q !== "string") {
          return res.status(400).json({
            error: "Query parameter 'q' is required",
          });
        }

        const repos = await this.githubService.searchRepos(
          q,
          (sort as string) || "stars",
          (order as string) || "desc",
          per_page ? parseInt(per_page as string, 10) : 30
        );

        res.json({
          query: q,
          total: repos.length,
          repositories: repos,
        });
      } catch (error: any) {
        if (error.message.includes("rate limit")) {
          return res.status(429).json({
            error: error.message,
          });
        }
        if (error.message.includes("Invalid search query")) {
          return res.status(422).json({
            error: error.message,
          });
        }
        res.status(500).json({
          error: "Internal server error",
          message: error.message,
        });
      }
    });

    this.app.use((req: Request, res: Response) => {
      res.status(404).json({
        error: `Cannot ${req.method} ${req.path}`,
        availableRoutes: [
          "POST /mcp",
          "GET /users/:username/repos",
          "GET /repos/:username",
          "GET /repos/:owner/:repo",
          "GET /search/repos?q=query",
          "GET /health",
        ],
      });
    });
  }

  start(): void {
    this.app.listen(this.port, () => {
      console.log(`MCP Server running on http://localhost:${this.port}`);
      console.log(`POST /mcp - MCP endpoint`);
      console.log(`GET /users/:username/repos - Get all repositories for a user`);
      console.log(`GET /repos/:username - Get all repositories for a user (short form)`);
      console.log(`GET /repos/:owner/:repo - Get specific GitHub repository`);
      console.log(`GET /search/repos?q=query - Search all GitHub repositories`);
      console.log(`GET /health - Health check`);
    });
  }
}


Added the route /repos/:username. Restart your server and use:

Now you can use:

http://localhost:3000/repos/sushant6095

This will return all repositories for the username sushant6095.

Available endpoints for getting user repos:

  1. GET /repos/:username - Short form (what you wanted)

    • Example: http://localhost:3000/repos/sushant6095
  2. GET /users/:username/repos - Alternative form

    • Example: http://localhost:3000/users/sushant6095/repos

Both endpoints return the same data:

{
  "username": "sushant6095",
  "total": 10,
  "repositories": [
    {
      "name": "repo-name",
      "html_url": "https://github.com/sushant6095/repo-name",
      "description": "Repo description",
      "stargazers_count": 5
    }
    // ... more repos
  ]
}

Restart your server and try http://localhost:3000/repos/sushant6095 again. It should work now.

http://localhost:3000/repos/sushant6095
{
  "username": "sushant6095",
  "total": 10,
  "repositories": [
    {
      "name": "repo-name",
      "html_url": "https://github.com/sushant6095/repo-name",
      "description": "Repo description",
      "stargazers_count": 5
    }
    // ... more repos
  ]
}

User

should fetch all the repos


Cursor

Updating the service to fetch all repositories across all pages (handle pagination):

Updating the service to fetch all repositories across all pages:

export interface GitHubRepo {
  name: string;
  html_url: string;
  description: string | null;
  stargazers_count: number;
}

export interface GitHubRepoDetails extends GitHubRepo {
  full_name: string;
  owner: {
    login: string;
    html_url: string;
  };
  language: string | null;
  forks_count: number;
  open_issues_count: number;
  created_at: string;
  updated_at: string;
  default_branch: string;
}

export class GitHubService {
  private readonly baseUrl = "https://api.github.com";

  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    const allRepos: GitHubRepo[] = [];
    let page = 1;
    const perPage = 100; // Maximum items per page
    let hasMore = true;

    while (hasMore) {
      const url = `${this.baseUrl}/users/${encodeURIComponent(username)}/repos?page=${page}&per_page=${perPage}&sort=updated`;
      
      const response = await fetch(url, {
        headers: {
          Accept: "application/vnd.github.v3+json",
        },
      });

      if (!response.ok) {
        if (response.status === 404) {
          throw new Error(`User '${username}' not found`);
        }
        if (response.status === 403) {
          throw new Error("GitHub API rate limit exceeded");
        }
        throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
      }

      const repos = (await response.json()) as any[];

      if (repos.length === 0) {
        hasMore = false;
      } else {
        const mappedRepos = repos.map((repo: any) => ({
          name: repo.name,
          html_url: repo.html_url,
          description: repo.description,
          stargazers_count: repo.stargazers_count,
        }));

        allRepos.push(...mappedRepos);

        // Check if there are more pages by checking the Link header
        const linkHeader = response.headers.get("link");
        if (linkHeader && linkHeader.includes('rel="next"')) {
          page++;
        } else {
          hasMore = false;
        }
      }
    }

    return allRepos;
  }

  async getRepo(owner: string, repo: string): Promise<GitHubRepoDetails> {
    const url = `${this.baseUrl}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 404) {
        throw new Error(`Repository '${owner}/${repo}' not found`);
      }
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const repoData = (await response.json()) as any;

    return {
      name: repoData.name,
      html_url: repoData.html_url,
      description: repoData.description,
      stargazers_count: repoData.stargazers_count,
      full_name: repoData.full_name,
      owner: {
        login: repoData.owner.login,
        html_url: repoData.owner.html_url,
      },
      language: repoData.language,
      forks_count: repoData.forks_count,
      open_issues_count: repoData.open_issues_count,
      created_at: repoData.created_at,
      updated_at: repoData.updated_at,
      default_branch: repoData.default_branch,
    };
  }

  async searchRepos(query: string, sort: string = "stars", order: string = "desc", perPage: number = 30): Promise<GitHubRepo[]> {
    const url = `${this.baseUrl}/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&order=${order}&per_page=${perPage}`;
    
    const response = await fetch(url, {
      headers: {
        Accept: "application/vnd.github.v3+json",
      },
    });

    if (!response.ok) {
      if (response.status === 403) {
        throw new Error("GitHub API rate limit exceeded");
      }
      if (response.status === 422) {
        throw new Error("Invalid search query");
      }
      throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
    }

    const data = (await response.json()) as any;

    return data.items.map((repo: any) => ({
      name: repo.name,
      html_url: repo.html_url,
      description: repo.description,
      stargazers_count: repo.stargazers_count,
    }));
  }
}


Updated the service to fetch all repositories across all pages. Changes:

What Changed:

  1. Pagination handling: Fetches all pages automatically
  2. Maximum items per page: Uses 100 items per page (GitHub's maximum)
  3. Link header detection: Checks for more pages using the Link header
  4. Complete results: Combines all pages into a single array

How it works:

  • Starts at page 1 with 100 items per page
  • Fetches each page sequentially
  • Checks the Link header to see if there's a next page
  • Continues until all pages are fetched
  • Returns all repositories in one array

Example:

When you call:

http://localhost:3000/repos/sushant6095

It will now return ALL repositories, not just the first 30. If you have 150 repositories, it will fetch all 150 across 2 pages.

Restart your server and try again. It should now fetch all your repositories.

http://localhost:3000/repos/sushant6095

Related Documents