@runbook-docs/mcp-server
Version:
Runbook Model Context Protocol Server
184 lines (163 loc) • 5.08 kB
text/typescript
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;
}