UNPKG

@graphql-hive/plugin-mcp

Version:
1,404 lines (1,396 loc) 83.6 kB
'use strict'; var node_fs = require('node:fs'); var node_path = require('node:path'); var graphqlFileLoader = require('@graphql-tools/graphql-file-loader'); var load = require('@graphql-tools/load'); var graphql = require('graphql'); var graphqlYoga = require('graphql-yoga'); var sofaApi = require('sofa-api'); function isDescriptionProvider(value) { return typeof value === "object" && value !== null && typeof value.fetchDescription === "function"; } async function resolveBuiltinProvider(name, options) { if (name === "langfuse") { let LangfuseClient; try { const mod = await import('@langfuse/client'); LangfuseClient = mod.LangfuseClient; } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("Cannot find") || message.includes("ERR_MODULE_NOT_FOUND")) { throw new Error( `The "langfuse" provider requires the "@langfuse/client" package. Install it with: npm install @langfuse/client` ); } throw new Error( `Failed to load the "@langfuse/client" package: ${message}` ); } if (typeof LangfuseClient !== "function") { throw new Error( `Failed to resolve the LangfuseClient constructor. Ensure you have a compatible version installed (@langfuse/client ^5.0.0).` ); } const { createLangfuseProvider } = await Promise.resolve().then(function () { return langfuse; }); const { defaults, ...langfuseOptions } = options; if (defaults !== void 0 && (typeof defaults !== "object" || defaults === null || Array.isArray(defaults))) { throw new Error( `Langfuse provider "defaults" must be an object (e.g., { label: "production" }), got ${Array.isArray(defaults) ? "array" : typeof defaults}` ); } try { return createLangfuseProvider( new LangfuseClient(langfuseOptions), defaults ); } catch (err) { throw new Error( `Failed to initialize Langfuse client. Ensure LANGFUSE_SECRET_KEY, LANGFUSE_PUBLIC_KEY, and LANGFUSE_BASE_URL env vars are set. Original error: ${err instanceof Error ? err.message : String(err)}` ); } } throw new Error( `Unknown provider "${name}". Built-in providers: "langfuse". For custom providers, pass a DescriptionProvider object with a fetchDescription() method.` ); } async function resolveProviders(providers) { const registry = {}; for (const [name, entry] of Object.entries(providers)) { if (!entry) continue; registry[name] = isDescriptionProvider(entry) ? entry : await resolveBuiltinProvider(name, entry); } return registry; } async function resolveDescriptions(ctx, tools, providers, options = { isStartup: false }) { const resolved = await Promise.all( tools.map(async (tool) => { const providerConfig = tool.tool?.descriptionProvider; if (!providerConfig) return tool; const provider = providers[providerConfig.type]; if (!provider) { throw new Error( `Unknown description provider type: "${providerConfig.type}" for tool "${tool.name}"` ); } try { const description = await provider.fetchDescription( tool.name, providerConfig, options.context ); return { ...tool, providerDescription: description }; } catch (err) { if (options.isStartup) { throw err; } ctx.log.error( `Description provider failed for tool "${tool.name}": ${err instanceof Error ? err.message : String(err)}` ); return tool; } }) ); return resolved; } async function resolveFieldDescriptions(ctx, tools, providers, options) { const result = /* @__PURE__ */ new Map(); await Promise.all( tools.map(async (tool) => { const properties = tool.input?.schema?.properties; if (!properties) return; const fieldEntries = Object.entries(properties).filter( ([, v]) => v.descriptionProvider ); if (fieldEntries.length === 0) return; const fieldMap = /* @__PURE__ */ new Map(); await Promise.all( fieldEntries.map(async ([fieldName, fieldOverrides]) => { const providerConfig = fieldOverrides.descriptionProvider; const provider = providers[providerConfig.type]; if (!provider) { throw new Error( `Unknown field description provider type: "${providerConfig.type}" for tool "${tool.name}" field "${fieldName}"` ); } try { const description = await provider.fetchDescription( tool.name, providerConfig, options.context ); fieldMap.set(fieldName, description); } catch (err) { ctx.log.error( `Field description provider failed for tool "${tool.name}" field "${fieldName}": ${err instanceof Error ? err.message : String(err)}` ); } }) ); if (fieldMap.size > 0) { result.set(tool.name, fieldMap); } }) ); return result; } const MCP_TOOL_DIRECTIVE = "mcpTool"; const MCP_DESCRIPTION_DIRECTIVE = "mcpDescription"; const MCP_HEADER_DIRECTIVE = "mcpHeader"; function astValueToJs(node) { switch (node.kind) { case graphql.Kind.STRING: return node.value; case graphql.Kind.INT: return parseInt(node.value, 10); case graphql.Kind.FLOAT: return parseFloat(node.value); case graphql.Kind.BOOLEAN: return node.value; case graphql.Kind.NULL: return null; case graphql.Kind.ENUM: return node.value; case graphql.Kind.LIST: return node.values.map(astValueToJs); case graphql.Kind.OBJECT: { const obj = {}; for (const field of node.fields) { obj[field.name.value] = astValueToJs(field.value); } return obj; } case graphql.Kind.VARIABLE: throw new Error( `Variable references ($${node.name.value}) are not supported in @mcpTool meta. Use literal values instead.` ); default: { const _exhaustive = node; throw new Error( `Unexpected AST value node kind: ${_exhaustive.kind}` ); } } } function extractMcpToolDirective(ctx, node) { const directive = node.directives?.find( (d) => d.name.value === MCP_TOOL_DIRECTIVE ); if (!directive) return void 0; const args = {}; let meta; for (const arg of directive.arguments || []) { if (arg.name.value === "meta") { if (arg.value.kind === graphql.Kind.OBJECT) { meta = astValueToJs(arg.value); } else { ctx.log.warn( `@mcpTool directive argument "meta" must be an object literal (got ${arg.value.kind}). The tool will be registered without metadata.` ); } } else if (arg.value.kind === graphql.Kind.STRING) { args[arg.name.value] = arg.value.value; } else { ctx.log.warn( `@mcpTool directive argument "${arg.name.value}" has non-string value (kind: ${arg.value.kind}). Only string literals are supported.` ); } } if (!args["name"]) { ctx.log.warn( `@mcpTool directive found but missing required "name" argument. The directive will be ignored.` ); return void 0; } const result = { name: args["name"] }; if (args["description"]) result.description = args["description"]; if (args["title"]) result.title = args["title"]; if (args["descriptionProvider"]) result.descriptionProvider = args["descriptionProvider"]; if (meta) result.meta = meta; return result; } function getMcpDescriptionProvider(ctx, directives, label) { const directive = directives?.find( (d) => d.name.value === MCP_DESCRIPTION_DIRECTIVE ); if (!directive) return void 0; const providerArg = directive.arguments?.find( (a) => a.name.value === "provider" ); if (!providerArg || providerArg.value.kind !== graphql.Kind.STRING || !providerArg.value.value) { ctx.log.warn( `@mcpDescription on ${label} requires a "provider" string argument (e.g., @mcpDescription(provider: "langfuse:prompt_name")). Ignoring.` ); return void 0; } return providerArg.value.value; } function extractFieldDescriptionProviders(ctx, variables) { let providers; for (const variable of variables) { const value = getMcpDescriptionProvider( ctx, variable.directives, `variable "$${variable.variable.name.value}"` ); if (value) { providers ??= {}; providers[variable.variable.name.value] = value; } } return providers; } function extractHeaderMappings(_, variables) { let mappings; for (const variable of variables) { const directive = variable.directives?.find( (d) => d.name.value === MCP_HEADER_DIRECTIVE ); if (!directive) continue; const nameArg = directive.arguments?.find((a) => a.name.value === "name"); if (!nameArg || nameArg.value.kind !== graphql.Kind.STRING || !nameArg.value.value.trim()) { throw new Error( `@mcpHeader on variable "$${variable.variable.name.value}" requires a non-empty "name" string argument (e.g., @mcpHeader(name: "x-company-id")).` ); } mappings ??= {}; mappings[variable.variable.name.value] = nameArg.value.value; } return mappings; } function extractSelectionDescriptionProviders(ctx, selectionSet, prefix = "") { let providers; for (const selection of selectionSet.selections) { if (selection.kind !== graphql.Kind.FIELD) continue; const fieldName = selection.name.value; const path = prefix ? `${prefix}.${fieldName}` : fieldName; const value = getMcpDescriptionProvider( ctx, selection.directives, `field "${path}"` ); if (value) { providers ??= {}; providers[path] = value; } if (selection.selectionSet) { const nested = extractSelectionDescriptionProviders( ctx, selection.selectionSet, path ); if (nested) { providers ??= {}; Object.assign(providers, nested); } } } return providers; } function stripSelectionDirectives(selectionSet) { return { ...selectionSet, selections: selectionSet.selections.map((selection) => { if (selection.kind !== graphql.Kind.FIELD) return selection; let field = selection; if (field.directives?.some( (d) => d.name.value === MCP_DESCRIPTION_DIRECTIVE )) { field = { ...field, directives: field.directives.filter( (d) => d.name.value !== MCP_DESCRIPTION_DIRECTIVE ) }; } if (field.selectionSet) { field = { ...field, selectionSet: stripSelectionDirectives(field.selectionSet) }; } return field; }) }; } function hasSelectionDirectives(selectionSet) { if (!selectionSet) return false; return selectionSet.selections.some((s) => { if (s.kind !== graphql.Kind.FIELD) return false; if (s.directives?.some((d) => d.name.value === MCP_DESCRIPTION_DIRECTIVE)) return true; return s.selectionSet ? hasSelectionDirectives(s.selectionSet) : false; }); } const MCP_VAR_DIRECTIVES = [MCP_DESCRIPTION_DIRECTIVE, MCP_HEADER_DIRECTIVE]; function stripMcpDirectives(def) { const hasMcpTool = def.directives?.some( (d) => d.name.value === MCP_TOOL_DIRECTIVE ); const hasVarDirective = def.variableDefinitions?.some( (v) => v.directives?.some((d) => MCP_VAR_DIRECTIVES.includes(d.name.value)) ); const hasSelDesc = hasSelectionDirectives(def.selectionSet); if (!hasMcpTool && !hasVarDirective && !hasSelDesc) return def; return { ...def, directives: hasMcpTool ? def.directives?.filter((d) => d.name.value !== MCP_TOOL_DIRECTIVE) : def.directives, variableDefinitions: hasVarDirective ? def.variableDefinitions?.map( (v) => v.directives?.some((d) => MCP_VAR_DIRECTIVES.includes(d.name.value)) ? { ...v, directives: v.directives.filter( (d) => !MCP_VAR_DIRECTIVES.includes(d.name.value) ) } : v ) : def.variableDefinitions, selectionSet: hasSelDesc ? stripSelectionDirectives(def.selectionSet) : def.selectionSet }; } function parseInlineHeaderDirectives(ctx, queryStr) { const doc = graphql.parse(queryStr); const def = doc.definitions.find( (d) => d.kind === graphql.Kind.OPERATION_DEFINITION ); if (!def?.variableDefinitions) return { query: queryStr }; const headerMappings = extractHeaderMappings(ctx, def.variableDefinitions); if (!headerMappings) return { query: queryStr }; const stripped = stripMcpDirectives(def); const strippedDoc = { kind: graphql.Kind.DOCUMENT, definitions: [stripped] }; return { query: graphql.print(strippedDoc), headerMappings }; } function loadOperationsFromDocument(ctx, doc) { const operations = []; for (const def of doc.definitions) { if (def.kind !== graphql.Kind.OPERATION_DEFINITION) continue; if (!def.name) { throw new Error( "anonymous operations are not supported. All MCP operations must be named" ); } const mcpDirective = extractMcpToolDirective(ctx, def); const fieldDescriptionProviders = def.variableDefinitions ? extractFieldDescriptionProviders(ctx, def.variableDefinitions) : void 0; const selectionDescriptionProviders = def.selectionSet ? extractSelectionDescriptionProviders(ctx, def.selectionSet) : void 0; const headerMappings = def.variableDefinitions ? extractHeaderMappings(ctx, def.variableDefinitions) : void 0; const singleDoc = { kind: graphql.Kind.DOCUMENT, definitions: [stripMcpDirectives(def)] }; operations.push({ name: def.name.value, type: def.operation, node: def, document: graphql.print(singleDoc), mcpDirective, fieldDescriptionProviders, selectionDescriptionProviders, headerMappings }); } return operations; } function resolveOperation(operations, operationName, operationType) { return operations.find( (op) => op.name === operationName && op.type === operationType ); } function graphqlTypeToJsonSchema(type, opts) { if (graphql.isNonNullType(type)) { return graphqlTypeToJsonSchema(type.ofType, opts); } if (graphql.isListType(type)) { return { type: "array", items: graphqlTypeToJsonSchema(type.ofType, opts) }; } if (graphql.isInputObjectType(type)) { return sofaApi.buildSchemaObjectFromType(type, opts); } return sofaApi.resolveFieldType(type, opts); } function operationToInputSchema(operationSource, schema) { const document = graphql.parse(operationSource); const operationDef = document.definitions.find( (def) => def.kind === graphql.Kind.OPERATION_DEFINITION ); if (!operationDef || operationDef.kind !== graphql.Kind.OPERATION_DEFINITION) { throw new Error("No operation definition found in document"); } const variables = operationDef.variableDefinitions || []; const properties = {}; const required = []; for (const variable of variables) { const varName = variable.variable.name.value; const varType = graphql.typeFromAST(schema, variable.type); if (!varType) { throw new Error(`Unknown type for variable $${varName}`); } if (variable.type.kind === graphql.Kind.NON_NULL_TYPE) { required.push(varName); } properties[varName] = graphqlTypeToJsonSchema(varType, { customScalars: {} }); } if (operationDef.selectionSet) { const rootType = operationDef.operation === "query" ? schema.getQueryType() : schema.getMutationType(); if (rootType) { for (const selection of operationDef.selectionSet.selections) { if (selection.kind !== graphql.Kind.FIELD) continue; const field = rootType.getFields()[selection.name.value]; if (!field) continue; for (const arg of field.args) { const prop = properties[arg.name]; if (prop && arg.description && !prop.description) { prop.description = arg.description; } } } } } const result = { type: "object", properties }; if (required.length > 0) { result.required = required; } return result; } function getToolDescriptionFromSchema(operationSource, schema) { const document = graphql.parse(operationSource); const operationDef = document.definitions.find( (def) => def.kind === graphql.Kind.OPERATION_DEFINITION ); if (!operationDef || operationDef.kind !== graphql.Kind.OPERATION_DEFINITION) return void 0; const rootSelection = operationDef.selectionSet.selections[0]; if (!rootSelection || rootSelection.kind !== graphql.Kind.FIELD) return void 0; const rootType = operationDef.operation === "query" ? schema.getQueryType() : schema.getMutationType(); if (!rootType) return void 0; const field = rootType.getFields()[rootSelection.name.value]; return field?.description || void 0; } function selectionSetToOutputSchema(document, schema) { const operationDef = document.definitions.find( (def) => def.kind === graphql.Kind.OPERATION_DEFINITION ); if (!operationDef || operationDef.kind !== graphql.Kind.OPERATION_DEFINITION) { throw new Error("No operation definition found in document"); } const rootType = operationDef.operation === "query" ? schema.getQueryType() : schema.getMutationType(); if (!rootType) { throw new Error(`Schema has no ${operationDef.operation} type`); } const rootSelection = operationDef.selectionSet.selections[0]; if (!rootSelection || rootSelection.kind !== graphql.Kind.FIELD) { throw new Error("Expected field selection at operation root"); } const rootField = rootType.getFields()[rootSelection.name.value]; if (!rootField) { throw new Error( `Field ${rootSelection.name.value} not found on ${rootType.name}` ); } const fieldSchema = outputTypeToSchema( rootField.type, rootSelection.selectionSet); return { type: "object", properties: { [rootSelection.name.value]: fieldSchema } }; } function outputTypeToSchema(type, selectionSet, schema) { if (graphql.isNonNullType(type)) { return outputTypeToSchema(type.ofType, selectionSet); } if (graphql.isListType(type)) { return { type: "array", items: outputTypeToSchema(type.ofType, selectionSet) }; } if (graphql.isObjectType(type) && selectionSet) { const properties = {}; for (const selection of selectionSet.selections) { if (selection.kind !== graphql.Kind.FIELD) continue; const fieldName = selection.name.value; if (fieldName === "__typename") continue; const field = type.getFields()[fieldName]; if (!field) continue; const fieldSchema = outputTypeToSchema( field.type, selection.selectionSet); if (field.description) { fieldSchema.description = field.description; } properties[fieldName] = fieldSchema; } return { type: "object", properties }; } return sofaApi.resolveFieldType(type, { customScalars: {} }); } function getByPath(obj, path) { let current = obj; for (const key of path.split(".")) { if (current == null || typeof current !== "object") return void 0; current = current[key]; } return current; } function getSchemaByPath(schema, path) { let current = schema; for (const key of path.split(".")) { if (current.type === "object" && current.properties?.[key]) { current = current.properties[key]; } else if (current.type === "array" && current.items) { const items = current.items; if (items.type === "object" && items.properties?.[key]) { current = items.properties[key]; } else { return void 0; } } else { return void 0; } } return current; } class ToolRegistry { tools = /* @__PURE__ */ new Map(); constructor(ctx, configs, schema) { for (const config of configs) { const query = config.query; let inputSchema = operationToInputSchema(query, schema); let argumentAliases; if (config.headerMappings) { for (const varName of Object.keys(config.headerMappings)) { if (inputSchema.properties?.[varName]) { delete inputSchema.properties[varName]; if (inputSchema.required) { inputSchema.required = inputSchema.required.filter( (r) => r !== varName ); if (inputSchema.required.length === 0) { delete inputSchema.required; } } } else { throw new Error( `Tool "${config.name}": @mcpHeader on variable "$${varName}" but this variable does not exist in the operation. Available variables: ${Object.keys(inputSchema.properties || {}).join(", ")}` ); } } } if (config.input?.schema?.properties) { if (config.headerMappings) { for (const [fieldName, fieldOverrides] of Object.entries( config.input.schema.properties )) { if (config.headerMappings[fieldName]) { throw new Error( `Tool "${config.name}": field "${fieldName}" has both @mcpHeader and input schema overrides. A header-mapped variable is removed from the input schema and cannot have aliases or other overrides.` ); } if (fieldOverrides.alias && config.headerMappings[fieldOverrides.alias]) { throw new Error( `Tool "${config.name}": alias "${fieldOverrides.alias}" for field "${fieldName}" conflicts with @mcpHeader-mapped variable "${fieldOverrides.alias}".` ); } } } const overrides = config.input.schema.properties; const hiddenFields = []; for (const [fieldName, fieldOverrides] of Object.entries(overrides)) { if (!inputSchema.properties?.[fieldName]) { throw new Error( `Tool "${config.name}" has override for field "${fieldName}" but this field does not exist in the operation's variables. Available variables: ${Object.keys(inputSchema.properties || {}).join(", ")}` ); } const { alias, descriptionProvider, hidden, ...schemaOverrides } = fieldOverrides; if (Object.keys(schemaOverrides).length > 0) { Object.assign(inputSchema.properties[fieldName], schemaOverrides); } if (hidden) { hiddenFields.push(alias ?? fieldName); } if (alias !== void 0 && alias !== fieldName) { if (typeof alias !== "string" || alias.trim().length === 0) { throw new Error( `Alias for field "${fieldName}" in tool "${config.name}" must be a non-empty string.` ); } if (inputSchema.properties[alias]) { throw new Error( `Alias "${alias}" for field "${fieldName}" in tool "${config.name}" collides with existing field "${alias}". Choose a different alias name.` ); } argumentAliases ??= {}; if (argumentAliases[alias]) { throw new Error( `Alias "${alias}" is used for both field "${argumentAliases[alias]}" and field "${fieldName}" in tool "${config.name}". Each alias must be unique.` ); } argumentAliases[alias] = fieldName; inputSchema.properties[alias] = inputSchema.properties[fieldName]; delete inputSchema.properties[fieldName]; if (inputSchema.required) { const idx = inputSchema.required.indexOf(fieldName); if (idx !== -1) { inputSchema.required[idx] = alias; } } } } for (const name of hiddenFields) { delete inputSchema.properties?.[name]; if (inputSchema.required) { inputSchema.required = inputSchema.required.filter( (r) => r !== name ); if (inputSchema.required.length === 0) { delete inputSchema.required; } } } } const description = config.tool?.description || config.directiveDescription || getToolDescriptionFromSchema(query, schema) || `Execute ${config.name}`; let outputSchema; try { outputSchema = selectionSetToOutputSchema(graphql.parse(query), schema); } catch (err) { ctx.log.error( `Failed to generate output schema for tool "${config.name}": ${err instanceof Error ? err.message : String(err)}. Tool will be registered without output schema.` ); } const outputPath = config.output?.path; if (outputPath !== void 0) { if (typeof outputPath !== "string" || outputPath.trim().length === 0) { throw new Error( `Tool "${config.name}": output.path must be a non-empty string.` ); } if (outputPath.startsWith(".") || outputPath.endsWith(".") || outputPath.includes("..")) { throw new Error( `Tool "${config.name}": output.path "${outputPath}" is invalid. Use dot-notation like "search.items".` ); } } if (outputPath && outputSchema) { const narrowed = getSchemaByPath(outputSchema, outputPath); if (narrowed) { outputSchema = narrowed; } else { throw new Error( `Tool "${config.name}": output.path "${outputPath}" does not match the output schema. Verify the path matches the GraphQL query's selection set.` ); } } if (config.hooks) { if (config.hooks.preprocess && typeof config.hooks.preprocess !== "function") { throw new Error( `Tool "${config.name}": hooks.preprocess must be a function, got ${typeof config.hooks.preprocess}` ); } if (config.hooks.postprocess && typeof config.hooks.postprocess !== "function") { throw new Error( `Tool "${config.name}": hooks.postprocess must be a function, got ${typeof config.hooks.postprocess}` ); } } const directiveMeta = config.directiveMeta; const configMeta = config.tool?._meta; const _meta = directiveMeta && configMeta ? { ...directiveMeta, ...configMeta } : configMeta || directiveMeta; this.tools.set(config.name, { name: config.name, description, title: config.tool?.title, query, inputSchema, outputSchema, annotations: config.tool?.annotations, icons: config.tool?.icons, execution: config.tool?.execution, _meta, argumentAliases, outputPath, suppressOutputSchema: config.output?.schema === false, contentAnnotations: config.output?.contentAnnotations, hooks: config.hooks, headerMappings: config.headerMappings }); } } getTool(name) { return this.tools.get(name); } getToolNames() { return Array.from(this.tools.keys()); } getMCPTools(options) { return Array.from(this.tools.values()).map((tool) => { const clonedInputSchema = structuredClone(tool.inputSchema); const mcpTool = { name: tool.name, description: tool.description, inputSchema: clonedInputSchema }; if (tool.title) mcpTool.title = tool.title; if (tool.annotations) mcpTool.annotations = tool.annotations; if (tool.icons) mcpTool.icons = tool.icons; if (tool.execution) mcpTool.execution = tool.execution; if (tool._meta) mcpTool._meta = tool._meta; const omitSchema = options?.suppressOutputSchema || tool.suppressOutputSchema || tool.hooks?.preprocess || tool.hooks?.postprocess; if (tool.outputSchema && !omitSchema) mcpTool.outputSchema = structuredClone(tool.outputSchema); return mcpTool; }); } } function dealiasArgs(args, aliases) { if (!aliases) return args; const dealiased = {}; for (const [key, value] of Object.entries(args)) { dealiased[aliases[key] || key] = value; } return dealiased; } async function processExecutionResult(ctx, options) { const { tool } = options; try { let data = options.data; if (tool.outputPath) { const extracted = getByPath(data, tool.outputPath); if (extracted === void 0 && data !== void 0) { ctx.log.error( `output.path "${tool.outputPath}" resolved to undefined for tool "${options.toolName}". Check your output.path configuration.` ); return { jsonrpc: "2.0", id: options.id, result: { content: [ { type: "text", text: JSON.stringify({ error: `output.path "${tool.outputPath}" could not extract data from the result` }) } ], isError: true } }; } data = extracted ?? null; } const hasHooks = !!tool.hooks?.preprocess || !!tool.hooks?.postprocess; let hookProducedResult = false; if (tool.hooks?.postprocess) { try { const hookContext = { toolName: options.toolName, headers: options.headers, query: tool.query }; data = await tool.hooks.postprocess(data, options.args, hookContext); hookProducedResult = true; } catch (hookError) { throw new Error( `postprocess hook failed: ${hookError instanceof Error ? hookError.message : String(hookError)}` ); } } return { jsonrpc: "2.0", id: options.id, result: formatToolCallResult(ctx, data, tool, { hookProducedResult, hasHooks }) }; } catch (error) { ctx.log.error( `tools/call failed for tool "${options.toolName}":`, error instanceof Error ? error.message : error ); return { jsonrpc: "2.0", id: options.id, result: { content: [ { type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) } ], isError: true } }; } } function formatToolCallResult(ctx, result, tool, opts) { const isMCPResult = opts.hookProducedResult && looksLikeMCPResult(result); if (isMCPResult) { return { isError: false, ...result }; } let textValue; let serializationFailed = false; try { textValue = JSON.stringify(result ?? null, null, 2); } catch (err) { serializationFailed = true; ctx.log.error( `Failed to serialize tool result for "${tool.name}":`, err instanceof Error ? err.message : String(err) ); textValue = JSON.stringify({ error: "Result could not be serialized to JSON", detail: err instanceof Error ? err.message : String(err) }); } const textItem = { type: "text", text: textValue }; if (tool.contentAnnotations) textItem["annotations"] = tool.contentAnnotations; const textContent = { content: [textItem], isError: false }; return tool.outputSchema && !opts.hasHooks && !serializationFailed ? { structuredContent: result, ...textContent } : textContent; } function looksLikeMCPResult(value) { if (typeof value !== "object" || value === null) return false; const content = value["content"]; if (!Array.isArray(content) || content.length === 0) return false; return content.every((item) => { if (typeof item !== "object" || item === null) return false; const rec = item; const type = rec["type"]; if (type === "text") return typeof rec["text"] === "string"; if (type === "image") return typeof rec["data"] === "string" && typeof rec["mimeType"] === "string"; if (type === "audio") return typeof rec["data"] === "string" && typeof rec["mimeType"] === "string"; if (type === "resource") { const resource = rec["resource"]; return typeof resource === "object" && resource !== null && !Array.isArray(resource) && typeof resource["uri"] === "string"; } if (type === "resource_link") return typeof rec["uri"] === "string" && typeof rec["name"] === "string"; return false; }); } async function handleMCPRequest(ctx, body, options, providerContext) { const { serverName, serverVersion, protocolVersion = "2025-11-25", registry } = options; const { id, method, params } = body; if (body.jsonrpc !== "2.0") { return { jsonrpc: "2.0", id: id ?? null, error: { code: -32600, message: 'Invalid Request: missing or invalid "jsonrpc" field' } }; } if (!method || typeof method !== "string") { return { jsonrpc: "2.0", id: id ?? null, error: { code: -32600, message: 'Invalid Request: missing or invalid "method" field' } }; } if (id == null && !method.startsWith("notifications/")) { return { jsonrpc: "2.0", id: null, error: { code: -32600, message: 'Invalid Request: missing "id" field' } }; } switch (method) { case "initialize": { const serverInfo = { name: serverName, version: serverVersion }; if (options.serverTitle) serverInfo["title"] = options.serverTitle; if (options.serverDescription) serverInfo["description"] = options.serverDescription; if (options.serverIcons) serverInfo["icons"] = options.serverIcons; if (options.serverWebsiteUrl) serverInfo["websiteUrl"] = options.serverWebsiteUrl; const capabilities = { tools: {} }; const hasResources = options.resources && options.resources.size > 0 || options.resourceTemplates && options.resourceTemplates.length > 0; if (hasResources) { capabilities["resources"] = {}; } const initResult = { protocolVersion, serverInfo, capabilities }; if (options.instructions) initResult["instructions"] = options.instructions; return { jsonrpc: "2.0", id, result: initResult }; } case "tools/list": { const allTools = registry.getMCPTools({ suppressOutputSchema: options.suppressOutputSchema }); if (options.resolveToolDescriptions) { try { const descriptions = await options.resolveToolDescriptions(providerContext); for (const tool of allTools) { if (descriptions.has(tool.name)) { tool.description = descriptions.get(tool.name); } } } catch (err) { ctx.log.error( `Failed to resolve tool descriptions: ${err instanceof Error ? err.message : String(err)}` ); } } if (options.resolveFieldDescriptions) { try { const fieldDescs = await options.resolveFieldDescriptions(providerContext); for (const tool of allTools) { const fields = fieldDescs.get(tool.name); if (fields && tool.inputSchema.properties) { for (const [fieldName, description] of fields) { if (tool.inputSchema.properties[fieldName]) { tool.inputSchema.properties[fieldName].description = description; } else { ctx.log.warn( `Resolved field description for "${fieldName}" on tool "${tool.name}" but no matching input property exists. Available properties: ${Object.keys(tool.inputSchema.properties).join(", ")}` ); } } } } } catch (err) { ctx.log.error( `Failed to resolve field descriptions: ${err instanceof Error ? err.message : String(err)}` ); } } if (options.resolveOutputFieldDescriptions) { try { const outputDescs = await options.resolveOutputFieldDescriptions(providerContext); for (const tool of allTools) { const fields = outputDescs.get(tool.name); if (fields && tool.outputSchema) { for (const [dotPath, description] of fields) { if (!setSchemaDescriptionByPath( tool.outputSchema, dotPath, description )) { ctx.log.warn( `Resolved output field description for "${dotPath}" on tool "${tool.name}" but no matching output property exists.` ); } } } } } catch (err) { ctx.log.error( `Failed to resolve output field descriptions: ${err instanceof Error ? err.message : String(err)}` ); } } if (options.toolsListPageSize !== void 0) { if (!Number.isInteger(options.toolsListPageSize) || options.toolsListPageSize <= 0) { throw new Error( `[MCP] toolsListPageSize must be a positive integer, got ${options.toolsListPageSize}` ); } } const listParams = params; const cursor = listParams?.cursor; let startIndex = 0; if (cursor !== void 0 && cursor !== "") { if (typeof cursor !== "string") { return { jsonrpc: "2.0", id, error: { code: -32602, message: "Invalid cursor: expected a string" } }; } startIndex = parseInt(cursor, 10); if (Number.isNaN(startIndex) || startIndex < 0 || startIndex > 0 && startIndex >= allTools.length) { return { jsonrpc: "2.0", id, error: { code: -32602, message: `Invalid cursor: ${JSON.stringify(cursor)}` } }; } } const pageSize = options.toolsListPageSize ?? allTools.length; const page = allTools.slice(startIndex, startIndex + pageSize); const nextIndex = startIndex + pageSize; const result = { tools: page }; if (nextIndex < allTools.length) { result["nextCursor"] = String(nextIndex); } return { jsonrpc: "2.0", id, result }; } case "resources/list": { const allResources = options.resources ? Array.from(options.resources.values()) : []; const resourceList = allResources.map((r) => { const entry = { uri: r.uri, name: r.name, mimeType: r.mimeType }; if (r.title) entry["title"] = r.title; if (r.size != null) entry["size"] = r.size; if (r.icons) entry["icons"] = r.icons; if (r.annotations) entry["annotations"] = r.annotations; const desc = r.description; if (desc) entry["description"] = desc; return entry; }); if (options.resolveResourceDescriptions) { try { const descriptions = await options.resolveResourceDescriptions(providerContext); for (const resource of resourceList) { const providerDesc = descriptions.get(resource["uri"]); if (providerDesc) { resource["description"] = providerDesc; } } } catch (err) { ctx.log.error( `Failed to resolve resource descriptions (${resourceList.length} resources affected): ${err instanceof Error ? err.message : String(err)}` ); } } if (options.resourcesListPageSize !== void 0) { if (!Number.isInteger(options.resourcesListPageSize) || options.resourcesListPageSize <= 0) { throw new Error( `[MCP] resourcesListPageSize must be a positive integer, got ${options.resourcesListPageSize}` ); } } const listParams = params; const cursor = listParams?.cursor; let startIndex = 0; if (cursor !== void 0 && cursor !== "") { if (typeof cursor !== "string") { return { jsonrpc: "2.0", id, error: { code: -32602, message: "Invalid cursor: expected a string" } }; } startIndex = parseInt(cursor, 10); if (Number.isNaN(startIndex) || startIndex < 0 || startIndex > 0 && startIndex >= resourceList.length) { return { jsonrpc: "2.0", id, error: { code: -32602, message: `Invalid cursor: ${JSON.stringify(cursor)}` } }; } } const pageSize = options.resourcesListPageSize ?? resourceList.length; const page = resourceList.slice(startIndex, startIndex + pageSize); const nextIndex = startIndex + pageSize; const result = { resources: page }; if (nextIndex < resourceList.length) { result["nextCursor"] = String(nextIndex); } return { jsonrpc: "2.0", id, result }; } case "resources/templates/list": { const allTemplates = options.resourceTemplates ?? []; const templateList = allTemplates.map((t) => { const entry = { uriTemplate: t.uriTemplate, name: t.name }; if (t.title) entry["title"] = t.title; if (t.mimeType) entry["mimeType"] = t.mimeType; if (t.icons) entry["icons"] = t.icons; if (t.annotations) entry["annotations"] = t.annotations; const desc = t.description; if (desc) entry["description"] = desc; return entry; }); if (options.resolveTemplateDescriptions) { try { const descriptions = await options.resolveTemplateDescriptions(providerContext); for (const tmpl of templateList) { const providerDesc = descriptions.get( tmpl["uriTemplate"] ); if (providerDesc) { tmpl["description"] = providerDesc; } } } catch (err) { ctx.log.error( `Failed to resolve template descriptions (${templateList.length} templates affected): ${err instanceof Error ? err.message : String(err)}` ); } } return { jsonrpc: "2.0", id, result: { resourceTemplates: templateList } }; } case "resources/read": { const readParams = params ?? {}; if (!readParams.uri) { return { jsonrpc: "2.0", id, error: { code: -32602, message: "Missing required parameter: uri" } }; } const resource = options.resources?.get(readParams.uri); if (resource) { if (resource.blob == null && resource.text == null) { return { jsonrpc: "2.0", id, error: { code: -32603, message: `Resource "${resource.name}" (${resource.uri}) has no content` } }; } const contentItem = { uri: resource.uri, mimeType: resource.mimeType }; if (resource.blob != null) { contentItem["blob"] = resource.blob; } else { contentItem["text"] = resource.text; } return { jsonrpc: "2.0", id, result: { contents: [contentItem] } }; } if (options.resourceTemplates) { for (const tmpl of options.resourceTemplates) { const match = tmpl.pattern.exec(readParams.uri); if (!match) continue; const extractedParams = {}; for (const name of tmpl.paramNames) { extractedParams[name] = match.groups?.[name] ?? ""; } let handlerResult; try { handlerResult = await tmpl.handler(extractedParams); } catch (err) { ctx.log.error( `Resource template handler failed for "${tmpl.uriTemplate}" (uri: ${readParams.uri}):`, err instanceof Error ? err.message : err ); return { jsonrpc: "2.0", id, error: { code: -32002, message: "Resource handler failed", data: { uri: readParams.uri, error: err instanceof Error ? err.message : String(err) } } }; } const mimeType = handlerResult.mimeType ?? tmpl.mimeType ?? "text/plain"; const contentItem = { uri: readParams.uri, mimeType }; if ("blob" in handlerResult && handlerResult.blob != null) { contentItem["blob"] = handlerResult.blob; } else if ("text" in handlerResult && handlerResult.text != null) { contentItem["text"] = handlerResult.text; } else { return { jsonrpc: "2.0", id, error: { code: -32603, message: `Resource template handler for "${tmpl.uriTemplate}" returned neither text nor blob` } }; } return { jsonrpc: "2.0", id, result: { contents: [contentItem] } }; } } return { jsonrpc: "2.0", id, error: { code: -32002, message: "Resource not found", data: { uri: readParams.uri } } }; } case "notifications/initialized": return null; default: return { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } }; } } function setSchemaDescriptionByPath(schema, path, description) { const keys = path.split("."); let current = schema; for (const key of keys) { if (current.type === "object" && current.properties?.[key]) { current = current.properties[key]; } else if (current.type === "array" && current.items) { const items = current.items; if (items.type === "object" && items.properties?.[key]) { current = items.properties[key]; } else { return false; } } else { return false; } } current.description = description; return true; } const VALID_PARAM_NAME = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; function compileUriTemplate(template) { const openCount = (template.match(/\{/g) || []).length; const closeCount = (template.match(/\}/g) || []).length; if (openCount !== closeCount) { throw new Error( `Unbalanced braces in URI template "${template}". Found ${openCount} opening and ${closeCount} closing braces.` ); } const paramNames = []; const escaped = template.replace( /\{([^}]+)\}|([^{]+)/g, (_match, param, literal) => { if (param) { if (!VALID_PARAM_NAME.test(param)) { throw new Error( `Invalid parameter name "{${param}}" in URI template "${template}". Parameter names must be valid identifiers (letters, digits, underscores).` ); } if (paramNames.includes(param)) { throw new Error( `Duplicate parameter name "{${param}}" in URI template "${template}". Each parameter name must be unique.` ); } paramNames.push(param); return `(?<${param}>[^/]+)`; } return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } ); return { pattern: new RegExp(`^${escaped}$`), paramNames }; } function parseDescriptionProviderDirective(value) { const parts = value.split(":"); if (parts.length < 2 || parts.length > 3 || !parts[0] || !parts[1]) { throw new Error( `Invalid descriptionProvider directive format: "${value}". Expected "type:prompt" or "type:prompt:version" (e.g., "langfuse:my_prompt" or "langfuse:my_prompt:3")` ); } const [type, prompt, versionStr] = parts; if (parts.length === 3 && !versionStr) { throw new Error( `Invalid descriptionProvider directive format: "${value}". Trailing colon with no version. Expected "type:prompt" or "type:prompt:version".` ); } const config = { type, prompt }; if (versionStr) { const version = Number(versionStr); if (!Number.isInteger(version) || version < 1) { throw new Error( `Invalid version "${versionStr}" in descriptionProvider directive "${value}". Version must be a positive integer.` ); } config["version"] = version; } return config; } function resolveToolConfigs(ctx, input) { const { tools, operationsSource } = input; let parsedOps; if (operationsSource) { parsedOps = loadOperationsFromDocument(ctx, operationsSource); } const directiveTools = /* @__PURE__ */ new Map(); if (parsedOps) { for (const op of parsedOps) { if (!op.mcpDirective) continue; const toolOverrides = {}; if (op.mcpDirective.title) toolOverrides.title = op.mcpDirective.title; if (op.mcpDirective.descriptionProvider) { toolOverrides.descriptionProvider = parseDescriptionProviderDirective( op.mcpDirective.descriptionProvider ); } let directiveInput; if (op.fieldDescriptionProviders) { const properties = {}; for (const [varName, providerStr] of Object.entries( op.fieldDescriptionProviders )) { properties[varName] = { descriptionProvider: parseDescriptionProviderDirective(providerStr) }; } directiveInput = { schema: { properties } }; } let directiveOutput; if (op.selectionDescriptionProviders) { const descriptionProviders = {}; for (const [path, providerStr] of Object.entries( op.selectionDescriptionProviders )) { descriptionProviders[path] = parseDescriptionProviderDirective(providerStr); }