UNPKG

@runbook-docs/mcp-server

Version:
184 lines (163 loc) 5.08 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { resourceHandlers } from './resources/resources'; import { toolHandlers } from './tools/tools'; import { promptHandlers } from './prompts/prompts'; import { McpState } from './state'; import { RequestError } from '@runbook-docs/client'; function kebabToPascal(kebab: string): string { if (!kebab) return ''; return kebab .split('-') .filter(Boolean) .map((s) => s[0].toUpperCase() + s.slice(1).toLowerCase()) .join(''); } function getErrorResponse(e: any) { let text; if (e instanceof RequestError) { text = JSON.stringify(e.attributes); } else { text = e instanceof Error ? e.message : String(e); } return { content: [ { type: 'text', text } ], isError: true }; } export async function buildServer(state: McpState) { const server = new Server( { name: kebabToPascal(state.name), version: '1.7.0' }, { capabilities: { resources: {}, tools: {}, prompts: {} }, instructions: ` This MCP server provides access to Runbook for document management and business workflow automation. Always use Runbook MCP tools (e.g. runbook-create-article, runbook-update-article, runbook-list-articles) to interact with Runbook. Never attempt alternative approaches such as direct HTTP requests or curl commands instead of using these tools. This MCP server connects to the Runbook tenant: ${state.baseUrl}. When multiple Runbook MCP servers are configured, each connects to a different tenant. Always confirm which tenant the user intends to operate on before creating or updating content. If unclear, ask the user to specify the target tenant. ` } ); const resourceHandlersInstance = resourceHandlers(state); const toolHandlersInstance = toolHandlers(state); const promptHandlersInstance = promptHandlers(state); server.setRequestHandler(ListResourcesRequestSchema, async () => { try { return { resources: await resourceHandlersInstance.listResources() }; } catch (e) { return getErrorResponse(e); } }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const { uri } = request.params; try { const content = await resourceHandlersInstance.readResource(uri); return { contents: [ { uri, text: content, mimeType: 'application/json' } ] }; } catch (e) { return getErrorResponse(e); } }); server.setRequestHandler(ListResourceTemplatesRequestSchema, () => { try { return resourceHandlersInstance.listResourceTemplates(); } catch (e) { return getErrorResponse(e); } }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: Object.entries(toolHandlersInstance).map(([name, handler]) => ({ name, description: handler.description, inputSchema: handler.inputSchema, annotations: handler.annotations })) }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const handler = toolHandlersInstance[name]; if (!handler) { throw new Error(`Unknown tool: ${name}`); } try { return await handler.handler(args); } catch (e) { return getErrorResponse(e); } }); server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: Object.entries(promptHandlersInstance).map(([, handler]) => ({ name: handler.name, title: handler.title || handler.name, description: handler.description, arguments: handler.arguments })) }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; const handler = promptHandlersInstance[name]; if (!handler) { throw new Error(`Unknown prompt: ${name}`); } // Simple template replacement let prompt = handler.prompt; if (args) { for (const [key, value] of Object.entries(args)) { const regex = new RegExp(`{{${key}}}`, 'g'); prompt = prompt.replace(regex, String(value)); } // Handle conditional blocks like {{#if runStateUid}} prompt = prompt.replace( /{{#if (\w+)}}([^}]*){{\/if}}/g, (_, condition, content) => { return args[condition] ? content : ''; } ); } return { description: handler.description, messages: [ { role: 'user', content: { type: 'text', text: prompt } } ] }; }); return server; }