GitNexus/gitnexus/src/mcp/server.ts

154 lines
3.9 KiB
TypeScript

/**
* MCP Server
*
* Model Context Protocol server that runs on stdio.
* External AI tools (Cursor, Claude) spawn this process and
* communicate via stdin/stdout using the MCP protocol.
*
* Tools: context, search, cypher, overview, explore, impact, analyze
* Resources: context, clusters, processes, schema, cluster/{name}, process/{name}
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
ListResourceTemplatesRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { GITNEXUS_TOOLS } from './tools.js';
import type { LocalBackend } from './local/local-backend.js';
import { getResourceDefinitions, getResourceTemplates, readResource } from './resources.js';
export async function startMCPServer(backend: LocalBackend): Promise<void> {
const server = new Server(
{
name: 'gitnexus',
version: '0.2.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// Handle list resources request
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const context = backend.context;
if (!context) {
return { resources: [] };
}
const resources = getResourceDefinitions(context.projectName);
return {
resources: resources.map(r => ({
uri: r.uri,
name: r.name,
description: r.description,
mimeType: r.mimeType,
})),
};
});
// Handle list resource templates request (for dynamic resources)
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
const templates = getResourceTemplates();
return {
resourceTemplates: templates.map(t => ({
uriTemplate: t.uriTemplate,
name: t.name,
description: t.description,
mimeType: t.mimeType,
})),
};
});
// Handle read resource request
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
const content = await readResource(uri, backend);
return {
contents: [
{
uri,
mimeType: 'text/yaml',
text: content,
},
],
};
} catch (err: any) {
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: `Error: ${err.message}`,
},
],
};
}
});
// Handle list tools request
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: GITNEXUS_TOOLS.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await backend.callTool(name, args);
return {
content: [
{
type: 'text',
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return {
content: [
{
type: 'text',
text: `Error: ${message}`,
},
],
isError: true,
};
}
});
// Connect to stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
// Handle graceful shutdown
process.on('SIGINT', async () => {
await backend.disconnect();
await server.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
await backend.disconnect();
await server.close();
process.exit(0);
});
}