@graphql-hive/plugin-mcp
Version:
1,407 lines (1,401 loc) • 83.2 kB
JavaScript
import { statSync, readFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { loadDocumentsSync } from '@graphql-tools/load';
import { Kind, print, parse, typeFromAST, isNonNullType, isListType, isInputObjectType, isObjectType, concatAST } from 'graphql';
import { isAsyncIterable } from 'graphql-yoga';
import { buildSchemaObjectFromType, resolveFieldType } from '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 Kind.STRING:
return node.value;
case Kind.INT:
return parseInt(node.value, 10);
case Kind.FLOAT:
return parseFloat(node.value);
case Kind.BOOLEAN:
return node.value;
case Kind.NULL:
return null;
case Kind.ENUM:
return node.value;
case Kind.LIST:
return node.values.map(astValueToJs);
case Kind.OBJECT: {
const obj = {};
for (const field of node.fields) {
obj[field.name.value] = astValueToJs(field.value);
}
return obj;
}
case 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 === 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 === 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 !== 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 !== 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 !== 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 !== 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 !== 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 = parse(queryStr);
const def = doc.definitions.find(
(d) => d.kind === 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: Kind.DOCUMENT,
definitions: [stripped]
};
return { query: print(strippedDoc), headerMappings };
}
function loadOperationsFromDocument(ctx, doc) {
const operations = [];
for (const def of doc.definitions) {
if (def.kind !== 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: Kind.DOCUMENT,
definitions: [stripMcpDirectives(def)]
};
operations.push({
name: def.name.value,
type: def.operation,
node: def,
document: 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 (isNonNullType(type)) {
return graphqlTypeToJsonSchema(type.ofType, opts);
}
if (isListType(type)) {
return {
type: "array",
items: graphqlTypeToJsonSchema(type.ofType, opts)
};
}
if (isInputObjectType(type)) {
return buildSchemaObjectFromType(type, opts);
}
return resolveFieldType(type, opts);
}
function operationToInputSchema(operationSource, schema) {
const document = parse(operationSource);
const operationDef = document.definitions.find(
(def) => def.kind === Kind.OPERATION_DEFINITION
);
if (!operationDef || operationDef.kind !== 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 = typeFromAST(schema, variable.type);
if (!varType) {
throw new Error(`Unknown type for variable $${varName}`);
}
if (variable.type.kind === 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 !== 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 = parse(operationSource);
const operationDef = document.definitions.find(
(def) => def.kind === Kind.OPERATION_DEFINITION
);
if (!operationDef || operationDef.kind !== Kind.OPERATION_DEFINITION)
return void 0;
const rootSelection = operationDef.selectionSet.selections[0];
if (!rootSelection || rootSelection.kind !== 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 === Kind.OPERATION_DEFINITION
);
if (!operationDef || operationDef.kind !== 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 !== 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 (isNonNullType(type)) {
return outputTypeToSchema(type.ofType, selectionSet);
}
if (isListType(type)) {
return {
type: "array",
items: outputTypeToSchema(type.ofType, selectionSet)
};
}
if (isObjectType(type) && selectionSet) {
const properties = {};
for (const selection of selectionSet.selections) {
if (selection.kind !== 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 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(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);
}
directiveOutput = { descriptionProviders };
}
directiveTools.set(op.mcpDirective.name, {
name: op.mcpDirective.name,
query: op.document,
directiveDescription: op.mcpDirective.desc