Initialize project with GitMCP router implementation and TypeScript configuration

This commit is contained in:
u60196
2025-10-10 15:09:20 +02:00
commit c9abaed91f
4 changed files with 515 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
function normalizeToGitMcp(urlOrSlug) {
if (!urlOrSlug || typeof urlOrSlug !== 'string') {
throw new Error(`Invalid input: Expected a string URL or repo slug, got: ${typeof urlOrSlug}`);
}
const raw = urlOrSlug.trim();
if (!raw) {
throw new Error('Empty URL or repo slug provided');
}
// If it's already a full GitHub URL, just replace github.com with gitmcp.io
if (raw.startsWith("http") || raw.startsWith("github.com")) {
const url = raw.startsWith("http") ? raw : "https://" + raw;
return url.replace(/github\.com/g, "gitmcp.io");
}
// Handle owner/repo format with optional branch
const [lhs, maybeBranch] = raw.split("#");
const segs = lhs.split("/").filter(Boolean);
if (segs.length !== 2) {
throw new Error(`Could not parse repo from "${urlOrSlug}". Expected "owner/repo" or GitHub URL.`);
}
const [owner, repo] = segs;
const base = `https://gitmcp.io/${owner}/${repo}`;
return maybeBranch ? `${base}/tree/${maybeBranch}` : base;
}
const tools = [
{
name: "fetch_generic_url_content",
description: "Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "GitHub URL (e.g., https://github.com/owner/repo/tree/branch) - required for first connection"
},
url: {
type: "string",
description: "The URL of the document or page to fetch"
}
},
required: ["url"]
}
},
{
name: "fetch_documentation",
description: "Fetch entire documentation file from the connected GitHub repository. Useful for general questions. Always call this tool first if asked about the repository. A usefull file to fetch is readme.md. Requires 'path' parameter with GitHub URL if not already connected.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "GitHub URL (e.g., https://github.com/owner/repo/tree/branch) - required for first connection"
}
},
required: []
}
},
{
name: "search_code",
description: "Search for code within the connected GitHub repository using the GitHub Search API (exact match). Returns matching files for you to query further if relevant. Requires 'path' parameter with GitHub URL if not already connected.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "GitHub URL (e.g., https://github.com/owner/repo/tree/branch) - required for first connection"
},
query: {
type: "string",
description: "The search query to find relevant code files"
},
page: {
type: "number",
description: "Page number to retrieve (starting from 1). Each page contains 30 results."
}
},
required: ["query"]
}
},
{
name: "search_documentation",
description: "Semantically search within the fetched documentation from the connected GitHub repository. Useful for specific queries. Requires 'path' parameter with GitHub URL if not already connected.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "GitHub URL (e.g., https://github.com/owner/repo/tree/branch) - required for first connection"
},
query: {
type: "string",
description: "The search query to find relevant documentation"
}
},
required: ["query"]
}
}
];
class GitMcpRouter {
server;
downstream = null;
currentRepo = null;
constructor() {
this.server = new Server({ name: "gitmcp-router", version: "0.1.0" }, { capabilities: { tools: {}, resources: {} } });
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
// Always expose these common GitMCP tools plus our connector
return {
tools: tools
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { path } = request.params.arguments;
// If we have a path parameter, ensure we're connected to that repository
if (path && (!this.downstream || this.currentRepo !== path)) {
console.log(`Connecting to new repository: ${path}`);
await this.loadRepo(path).catch((e) => {
throw new Error(`Failed to connect to repository: ${e instanceof Error ? e.message : String(e)}`);
});
}
// If no path and no connection, provide helpful error
if (!this.downstream && !path) {
throw new Error(`No repository connected. Please provide a 'path' parameter with a GitHub URL (e.g., "https://github.com/modelcontextprotocol/servers") to connect to a repository first.`);
}
// const toolNames = [
// 'fetch_documentation',
// 'search_documentation',
// 'search_code',
// 'fetch_generic_url_content'
// ]
const toolNames = tools.map(t => t.name);
// const downstreamToolNames = [
// 'fetch_query_documentation',
// 'search_query_documentation',
// 'search_query_code',
// 'fetch_generic_url_content'
// ]
const downstreamToolNames = await this.downstream?.listTools().then((tools) => {
return tools.tools.map(t => t.name);
});
// Map tool names to the actual GitMCP tool names and forward the call
// const toolMapping: Record<string, string> = {
// "fetch_documentation": "fetch_query_documentation",
// "search_code": "search_query_code",
// "search_documentation": "search_query_documentation",
// "fetch_generic_url_content": "fetch_generic_url_content"
// };
const toolMapping = toolNames.reduce((acc, routerToolName) => {
// Find matching downstream tool by checking if downstream tool contains all parts of router tool
const matchingDownstreamTool = downstreamToolNames?.find(downstreamTool => routerToolName.split("_").every(part => downstreamTool.includes(part)));
acc[routerToolName] = matchingDownstreamTool || routerToolName;
return acc;
}, {});
const actualToolName = toolMapping[request.params.name];
if (!actualToolName) {
throw new Error(`Unknown tool: ${request.params.name}`);
}
console.log("Tool mapping:", toolMapping);
// Remove path from arguments when forwarding to downstream
const { path: _, ...forwardArgs } = request.params.arguments;
return await this.downstream.callTool({
name: actualToolName,
arguments: forwardArgs
});
}
catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
});
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
if (!this.downstream)
throw new Error("No repository loaded");
return await this.downstream.listResources();
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (!this.downstream)
throw new Error("No repository loaded");
return await this.downstream.readResource(request.params);
});
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
async loadRepo(githubUrl) {
// Convert GitHub URL directly to GitMCP URL
const gitMcpUrl = normalizeToGitMcp(githubUrl);
if (this.downstream) {
try {
await this.downstream.close();
}
catch { }
}
const transport = new SSEClientTransport(new URL(gitMcpUrl));
this.downstream = new Client({ name: "gitmcp-router-client", version: "0.1.0" });
await this.downstream.connect(transport);
this.currentRepo = githubUrl;
console.error(`Connected to GitMCP: ${githubUrl} -> ${gitMcpUrl}`);
// Debug: List what tools are actually available from downstream
try {
const downstreamTools = await this.downstream.listTools();
console.error(`Available downstream tools: ${JSON.stringify(downstreamTools.tools.map(t => t.name))}`);
}
catch (e) {
console.error(`Error listing downstream tools: ${e}`);
}
}
}
const router = new GitMcpRouter();
router.start().catch((e) => {
console.error(e);
process.exit(1);
});
+20
View File
@@ -0,0 +1,20 @@
{
"name": "g-source",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.16.0",
"eventsource": "^2.0.2",
"undici": "^6.19.8",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.7.1",
"typescript": "^5.6.3"
}
}
+258
View File
@@ -0,0 +1,258 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListToolsRequestSchema,
CallToolRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
Tool
} from "@modelcontextprotocol/sdk/types.js";
function normalizeToGitMcp(urlOrSlug: string): string {
if (!urlOrSlug || typeof urlOrSlug !== 'string') {
throw new Error(`Invalid input: Expected a string URL or repo slug, got: ${typeof urlOrSlug}`);
}
const raw = urlOrSlug.trim();
if (!raw) {
throw new Error('Empty URL or repo slug provided');
}
// If it's already a full GitHub URL, just replace github.com with gitmcp.io
if (raw.startsWith("http") || raw.startsWith("github.com")) {
const url = raw.startsWith("http") ? raw : "https://" + raw;
return url.replace(/github\.com/g, "gitmcp.io");
}
// Handle owner/repo format with optional branch
const [lhs, maybeBranch] = raw.split("#");
const segs = lhs.split("/").filter(Boolean);
if (segs.length !== 2) {
throw new Error(`Could not parse repo from "${urlOrSlug}". Expected "owner/repo" or GitHub URL.`);
}
const [owner, repo] = segs;
const base = `https://gitmcp.io/${owner}/${repo}`;
return maybeBranch ? `${base}/tree/${maybeBranch}` : base;
}
const tools: Tool[] = [
{
name: "fetch_generic_url_content",
description: "Generic tool to fetch content from any absolute URL, respecting robots.txt rules. Use this to retrieve referenced urls (absolute urls) that were mentioned in previously fetched documentation.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "GitHub URL (e.g., https://github.com/owner/repo/tree/branch) - required for first connection"
},
url: {
type: "string",
description: "The URL of the document or page to fetch"
}
},
required: ["url"]
}
},
{
name: "fetch_documentation",
description: "Fetch entire documentation file from the connected GitHub repository. Useful for general questions. Always call this tool first if asked about the repository. A usefull file to fetch is readme.md. Requires 'path' parameter with GitHub URL if not already connected.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "GitHub URL (e.g., https://github.com/owner/repo/tree/branch) - required for first connection"
}
},
required: []
}
},
{
name: "search_code",
description: "Search for code within the connected GitHub repository using the GitHub Search API (exact match). Returns matching files for you to query further if relevant. Requires 'path' parameter with GitHub URL if not already connected.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "GitHub URL (e.g., https://github.com/owner/repo/tree/branch) - required for first connection"
},
query: {
type: "string",
description: "The search query to find relevant code files"
},
page: {
type: "number",
description: "Page number to retrieve (starting from 1). Each page contains 30 results."
}
},
required: ["query"]
}
},
{
name: "search_documentation",
description: "Semantically search within the fetched documentation from the connected GitHub repository. Useful for specific queries. Requires 'path' parameter with GitHub URL if not already connected.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "GitHub URL (e.g., https://github.com/owner/repo/tree/branch) - required for first connection"
},
query: {
type: "string",
description: "The search query to find relevant documentation"
}
},
required: ["query"]
}
}
]
class GitMcpRouter {
private server: Server;
private downstream: Client | null = null;
private currentRepo: string | null = null;
constructor() {
this.server = new Server(
{ name: "gitmcp-router", version: "0.1.0" },
{ capabilities: { tools: {}, resources: {} } }
);
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
// Always expose these common GitMCP tools plus our connector
return {
tools: tools
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { path } = request.params.arguments as { path?: string };
// If we have a path parameter, ensure we're connected to that repository
if (path && (!this.downstream || this.currentRepo !== path)) {
console.log(`Connecting to new repository: ${path}`);
await this.loadRepo(path).catch((e) => {
throw new Error(`Failed to connect to repository: ${e instanceof Error ? e.message : String(e)}`);
});
}
// If no path and no connection, provide helpful error
if (!this.downstream && !path) {
throw new Error(`No repository connected. Please provide a 'path' parameter with a GitHub URL (e.g., "https://github.com/modelcontextprotocol/servers") to connect to a repository first.`);
}
// const toolNames = [
// 'fetch_documentation',
// 'search_documentation',
// 'search_code',
// 'fetch_generic_url_content'
// ]
const toolNames = tools.map(t => t.name);
// const downstreamToolNames = [
// 'fetch_query_documentation',
// 'search_query_documentation',
// 'search_query_code',
// 'fetch_generic_url_content'
// ]
const downstreamToolNames = await this.downstream?.listTools().then(
(tools) => {
return tools.tools.map(t => t.name);
}
);
// Map tool names to the actual GitMCP tool names and forward the call
// const toolMapping: Record<string, string> = {
// "fetch_documentation": "fetch_query_documentation",
// "search_code": "search_query_code",
// "search_documentation": "search_query_documentation",
// "fetch_generic_url_content": "fetch_generic_url_content"
// };
const toolMapping: Record<string, string> = toolNames.reduce<Record<string, string>>((acc, routerToolName) => {
// Find matching downstream tool by checking if downstream tool contains all parts of router tool
const matchingDownstreamTool = downstreamToolNames?.find(downstreamTool => routerToolName.split("_").every(part => downstreamTool.includes(part)));
acc[routerToolName] = matchingDownstreamTool || routerToolName;
return acc;
}, {});
const actualToolName = toolMapping[request.params.name];
if (!actualToolName) {
throw new Error(`Unknown tool: ${request.params.name}`);
}
console.log("Tool mapping:", toolMapping);
// Remove path from arguments when forwarding to downstream
const { path: _, ...forwardArgs } = request.params.arguments as any;
return await this.downstream!.callTool({
name: actualToolName,
arguments: forwardArgs
});
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
});
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
if (!this.downstream) throw new Error("No repository loaded");
return await this.downstream.listResources();
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (!this.downstream) throw new Error("No repository loaded");
return await this.downstream.readResource(request.params);
});
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
private async loadRepo(githubUrl: string) {
// Convert GitHub URL directly to GitMCP URL
const gitMcpUrl = normalizeToGitMcp(githubUrl);
if (this.downstream) {
try { await this.downstream.close(); } catch { }
}
const transport = new SSEClientTransport(new URL(gitMcpUrl));
this.downstream = new Client({ name: "gitmcp-router-client", version: "0.1.0" });
await this.downstream.connect(transport);
this.currentRepo = githubUrl;
console.error(`Connected to GitMCP: ${githubUrl} -> ${gitMcpUrl}`);
// Debug: List what tools are actually available from downstream
try {
const downstreamTools = await this.downstream.listTools();
console.error(`Available downstream tools: ${JSON.stringify(downstreamTools.tools.map(t => t.name))}`);
} catch (e) {
console.error(`Error listing downstream tools: ${e}`);
}
}
}
const router = new GitMcpRouter();
router.start().catch((e) => {
console.error(e);
process.exit(1);
});
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"lib": ["ES2022", "DOM"],
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"types": ["node"]
},
"include": ["src"]
}