UNPKG

@smartbear/mcp

Version:

MCP server for interacting SmartBear Products

240 lines (239 loc) 10.1 kB
import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js"; import { ZodAny, ZodArray, ZodBoolean, ZodDefault, ZodEnum, ZodIntersection, ZodLiteral, ZodNumber, ZodObject, ZodOptional, ZodRecord, ZodString, ZodUnion, } from "zod"; import Bugsnag from "../common/bugsnag.js"; import { CacheService } from "./cache.js"; import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "./info.js"; import { ToolError } from "./types.js"; export class SmartBearMcpServer extends McpServer { cache; constructor() { super({ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION, }, { capabilities: { resources: { listChanged: true }, // Server supports dynamic resource lists tools: { listChanged: true }, // Server supports dynamic tool lists sampling: {}, // Server supports sampling requests to Host elicitation: {}, // Server supports eliciting input from the user logging: {}, // Server supports logging messages prompts: {}, // Server supports sending prompts to Host }, }); this.cache = new CacheService(); } getCache() { return this.cache; } addClient(client) { client.registerTools((params, cb) => { const toolName = `${client.toolPrefix}_${params.title.replace(/\s+/g, "_").toLowerCase()}`; const toolTitle = `${client.name}: ${params.title}`; return super.registerTool(toolName, { title: toolTitle, description: this.getDescription(params), inputSchema: this.getInputSchema(params), outputSchema: this.getOutputSchema(params), annotations: this.getAnnotations(toolTitle, params), }, async (args, extra) => { try { const result = await cb(args, extra); if (result) { this.validateCallbackResult(result, params); this.addStructuredContentAsText(result); } return result; } catch (e) { // ToolErrors should not be reported to BugSnag if (e instanceof ToolError) { return { isError: true, content: [ { type: "text", text: `Error executing ${toolTitle}: ${e.message}`, }, ], }; } else { Bugsnag.notify(e, (event) => { event.addMetadata("app", { tool: toolName }); event.unhandled = true; }); } throw e; } }); }, (params, options) => { return this.server.elicitInput(params, options); }); if (client.registerResources) { client.registerResources((name, path, cb) => { const url = `${client.toolPrefix}://${name}/${path}`; return super.registerResource(name, new ResourceTemplate(url, { list: undefined, }), {}, async (url, variables, extra) => { try { return await cb(url, variables, extra); } catch (e) { Bugsnag.notify(e, (event) => { event.addMetadata("app", { resource: name, url: url }); event.unhandled = true; }); throw e; } }); }); } if (client.registerPrompts) { client.registerPrompts((name, config, cb) => { return super.registerPrompt(name, config, cb); }); } } validateCallbackResult(result, params) { if (result.isError) { return; } if (params.outputSchema && !result.structuredContent) { throw new Error(`The result of the tool '${params.title}' must include 'structuredContent'`); } } addStructuredContentAsText(result) { if (result.structuredContent && !result.content?.length) { result.content = [ { type: "text", text: JSON.stringify(result.structuredContent), }, ]; } } getAnnotations(toolTitle, params) { const annotations = { title: toolTitle, readOnlyHint: params.readOnly ?? true, destructiveHint: params.destructive ?? false, idempotentHint: params.idempotent ?? true, openWorldHint: params.openWorld ?? false, }; return annotations; } getInputSchema(params) { const args = {}; for (const param of params.parameters ?? []) { args[param.name] = param.type; if (param.description) { args[param.name] = args[param.name].describe(param.description); } if (!param.required) { args[param.name] = args[param.name].optional(); } } return { ...args, ...this.schemaToRawShape(params.inputSchema) }; } schemaToRawShape(schema) { if (schema) { if (schema instanceof ZodObject) { return schema.shape; } if (schema instanceof ZodIntersection) { const leftShape = this.schemaToRawShape(schema._def.left); const rightShape = this.schemaToRawShape(schema._def.right); return { ...leftShape, ...rightShape }; } } return undefined; } getOutputSchema(params) { return this.schemaToRawShape(params.outputSchema); } getDescription(params) { const { summary, useCases, examples, parameters, inputSchema, hints, outputDescription, } = params; let description = summary; // Parameters if available otherwise use inputSchema if ((parameters ?? []).length > 0) { description += `\n\n**Parameters:**\n${parameters ?.map((p) => `- ${p.name} (${this.getReadableTypeName(p.type)})${p.required ? " *required*" : ""}` + `${p.description ? `: ${p.description}` : ""}` + `${p.examples ? ` (e.g. ${p.examples.join(", ")})` : ""}` + `${p.constraints ? `\n - ${p.constraints.join("\n - ")}` : ""}`) .join("\n")}`; } if (inputSchema && inputSchema instanceof ZodObject) { description += "\n\n**Parameters:**\n"; description += Object.keys(inputSchema.shape) .map((key) => this.formatParameterDescription(key, inputSchema.shape[key])) .join("\n"); } if (outputDescription) { description += `\n\n**Output Description:** ${outputDescription}`; } // Use Cases if (useCases && useCases.length > 0) { description += `\n\n**Use Cases:** ${useCases.map((uc, i) => `${i + 1}. ${uc}`).join(" ")}`; } // Examples if (examples && examples.length > 0) { description += `\n\n**Examples:**\n` + examples .map((ex, idx) => `${idx + 1}. ${ex.description}\n\`\`\`json\n${JSON.stringify(ex.parameters, null, 2)}\n\`\`\`${ex.expectedOutput ? `\nExpected Output: ${ex.expectedOutput}` : ""}`) .join("\n\n"); } // Hints if (hints && hints.length > 0) { description += `\n\n**Hints:** ${hints.map((hint, i) => `${i + 1}. ${hint}`).join(" ")}`; } return description.trim(); } formatParameterDescription(key, field, description = null, isOptional = false, defaultValue = null) { description = description ?? (field.description || null); if (field instanceof ZodOptional) { field = field.unwrap(); return this.formatParameterDescription(key, field, description, true, defaultValue); } if (field instanceof ZodDefault) { defaultValue = JSON.stringify(field._def.defaultValue()); field = field.removeDefault(); return this.formatParameterDescription(key, field, description, true, defaultValue); } return (`- ${key} (${this.getReadableTypeName(field)})` + `${isOptional ? "" : " *required*"}` + `${description ? `: ${description}` : ""}` + `${defaultValue ? ` (default: ${defaultValue})` : ""}`); } getReadableTypeName(zodType) { if (zodType instanceof ZodOptional) { return this.getReadableTypeName(zodType.unwrap()); } if (zodType instanceof ZodDefault) { return this.getReadableTypeName(zodType.removeDefault()); } if (zodType instanceof ZodRecord) { return `record<${this.getReadableTypeName(zodType.keySchema)}, ${this.getReadableTypeName(zodType.valueSchema)}>`; } if (zodType instanceof ZodString) return "string"; if (zodType instanceof ZodNumber) return "number"; if (zodType instanceof ZodBoolean) return "boolean"; if (zodType instanceof ZodArray) return "array"; if (zodType instanceof ZodObject) return "object"; if (zodType instanceof ZodEnum) return "enum"; if (zodType instanceof ZodLiteral) return "literal"; if (zodType instanceof ZodUnion) return "union"; if (zodType instanceof ZodAny) return "any"; return "any"; } }