UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

1,369 lines (1,360 loc) 66.5 kB
import { CallToolRequestSchema, CompleteRequestSchema, DEFAULT_NEGOTIATED_PROTOCOL_VERSION, ErrorCode, GetPromptRequestSchema, JSONRPCMessageSchema, ListPromptsRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, SUPPORTED_PROTOCOL_VERSIONS, Server, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, getLiteralValue, getObjectShape, getParseErrorMessage, getSchemaDescription, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, isSchemaOptional, normalizeObjectSchema, objectFromShape, playgroundRoomToTenant, safeParseAsync, toJsonSchemaCompat } from "./chunk-2NJRCGWJ.js"; import { gatewayPublicUrl, getRegisteredRoom, listRegisteredRooms, roomMcpPathForUrl } from "./chunk-YC5I32PS.js"; // src/server/cors.ts var DEFAULT_METHODS = "GET, POST, PUT, DELETE, OPTIONS"; var DEFAULT_HEADERS = "Content-Type, Authorization, mcp-session-id, mcp-protocol-version, Last-Event-ID, Accept, X-Trellis-Lane, X-Trellis-Tenant, X-Trellis-Transport"; function corsEnabledForConfig(apiKey) { return Boolean(process.env.TRELLIS_CORS_ORIGINS) || Boolean(apiKey); } function allowedOrigins() { const raw = process.env.TRELLIS_CORS_ORIGINS?.trim(); if (!raw || raw === "*") return "*"; return raw.split(",").map((s) => s.trim()).filter(Boolean); } function corsHeaders(req) { const origins = allowedOrigins(); const requestOrigin = req.headers.get("Origin"); let allowOrigin = "*"; if (origins !== "*") { if (requestOrigin && origins.includes(requestOrigin)) { allowOrigin = requestOrigin; } else if (origins.length === 1) { allowOrigin = origins[0]; } else { allowOrigin = origins[0] ?? "*"; } } const requested = req.headers.get("Access-Control-Request-Headers"); const allowHeaders = requested?.trim() || DEFAULT_HEADERS; return { "Access-Control-Allow-Origin": allowOrigin, "Access-Control-Allow-Methods": DEFAULT_METHODS, "Access-Control-Allow-Headers": allowHeaders, Vary: "Origin, Access-Control-Request-Headers" }; } function withCors(req, res) { const headers = new Headers(res.headers); for (const [key, value] of Object.entries(corsHeaders(req))) { headers.set(key, value); } return new Response(res.body, { status: res.status, statusText: res.statusText, headers }); } function corsPreflightResponse(req) { return new Response(null, { status: 204, headers: corsHeaders(req) }); } // node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/completable.js var COMPLETABLE_SYMBOL = Symbol.for("mcp.completable"); function isCompletable(schema) { return !!schema && typeof schema === "object" && COMPLETABLE_SYMBOL in schema; } function getCompleter(schema) { const meta = schema[COMPLETABLE_SYMBOL]; return meta?.complete; } var McpZodTypeKind; (function(McpZodTypeKind2) { McpZodTypeKind2["Completable"] = "McpCompletable"; })(McpZodTypeKind || (McpZodTypeKind = {})); // node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/toolNameValidation.js var TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/; function validateToolName(name) { const warnings = []; if (name.length === 0) { return { isValid: false, warnings: ["Tool name cannot be empty"] }; } if (name.length > 128) { return { isValid: false, warnings: [`Tool name exceeds maximum length of 128 characters (current: ${name.length})`] }; } if (name.includes(" ")) { warnings.push("Tool name contains spaces, which may cause parsing issues"); } if (name.includes(",")) { warnings.push("Tool name contains commas, which may cause parsing issues"); } if (name.startsWith("-") || name.endsWith("-")) { warnings.push("Tool name starts or ends with a dash, which may cause parsing issues in some contexts"); } if (name.startsWith(".") || name.endsWith(".")) { warnings.push("Tool name starts or ends with a dot, which may cause parsing issues in some contexts"); } if (!TOOL_NAME_REGEX.test(name)) { const invalidChars = name.split("").filter((char) => !/[A-Za-z0-9._-]/.test(char)).filter((char, index, arr) => arr.indexOf(char) === index); warnings.push(`Tool name contains invalid characters: ${invalidChars.map((c) => `"${c}"`).join(", ")}`, "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)"); return { isValid: false, warnings }; } return { isValid: true, warnings }; } function issueToolNameWarning(name, warnings) { if (warnings.length > 0) { console.warn(`Tool name validation warning for "${name}":`); for (const warning of warnings) { console.warn(` - ${warning}`); } console.warn("Tool registration will proceed, but this may cause compatibility issues."); console.warn("Consider updating the tool name to conform to the MCP tool naming standard."); console.warn("See SEP: Specify Format for Tool Names (https://github.com/modelcontextprotocol/modelcontextprotocol/issues/986) for more details."); } } function validateAndWarnToolName(name) { const result = validateToolName(name); issueToolNameWarning(name, result.warnings); return result.isValid; } // node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/mcp-server.js var ExperimentalMcpServerTasks = class { constructor(_mcpServer) { this._mcpServer = _mcpServer; } registerToolTask(name, config, handler) { const execution = { taskSupport: "required", ...config.execution }; if (execution.taskSupport === "forbidden") { throw new Error(`Cannot register task-based tool '${name}' with taskSupport 'forbidden'. Use registerTool() instead.`); } const mcpServerInternal = this._mcpServer; return mcpServerInternal._createRegisteredTool(name, config.title, config.description, config.inputSchema, config.outputSchema, config.annotations, execution, config._meta, handler); } }; // node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js import { ZodOptional } from "zod"; var McpServer = class { constructor(serverInfo, options) { this._registeredResources = {}; this._registeredResourceTemplates = {}; this._registeredTools = {}; this._registeredPrompts = {}; this._toolHandlersInitialized = false; this._completionHandlerInitialized = false; this._resourceHandlersInitialized = false; this._promptHandlersInitialized = false; this.server = new Server(serverInfo, options); } /** * Access experimental features. * * WARNING: These APIs are experimental and may change without notice. * * @experimental */ get experimental() { if (!this._experimental) { this._experimental = { tasks: new ExperimentalMcpServerTasks(this) }; } return this._experimental; } /** * Attaches to the given transport, starts it, and starts listening for messages. * * The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward. */ async connect(transport) { return await this.server.connect(transport); } /** * Closes the connection. */ async close() { await this.server.close(); } setToolRequestHandlers() { if (this._toolHandlersInitialized) { return; } this.server.assertCanSetRequestHandler(getMethodValue(ListToolsRequestSchema)); this.server.assertCanSetRequestHandler(getMethodValue(CallToolRequestSchema)); this.server.registerCapabilities({ tools: { listChanged: true } }); this.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: Object.entries(this._registeredTools).filter(([, tool]) => tool.enabled).map(([name, tool]) => { const toolDefinition = { name, title: tool.title, description: tool.description, inputSchema: (() => { const obj = normalizeObjectSchema(tool.inputSchema); return obj ? toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: "input" }) : EMPTY_OBJECT_JSON_SCHEMA; })(), annotations: tool.annotations, execution: tool.execution, _meta: tool._meta }; if (tool.outputSchema) { const obj = normalizeObjectSchema(tool.outputSchema); if (obj) { toolDefinition.outputSchema = toJsonSchemaCompat(obj, { strictUnions: true, pipeStrategy: "output" }); } } return toolDefinition; }) })); this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { try { const tool = this._registeredTools[request.params.name]; if (!tool) { throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`); } if (!tool.enabled) { throw new McpError(ErrorCode.InvalidParams, `Tool ${request.params.name} disabled`); } const isTaskRequest = !!request.params.task; const taskSupport = tool.execution?.taskSupport; const isTaskHandler = "createTask" in tool.handler; if ((taskSupport === "required" || taskSupport === "optional") && !isTaskHandler) { throw new McpError(ErrorCode.InternalError, `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`); } if (taskSupport === "required" && !isTaskRequest) { throw new McpError(ErrorCode.MethodNotFound, `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`); } if (taskSupport === "optional" && !isTaskRequest && isTaskHandler) { return await this.handleAutomaticTaskPolling(tool, request, extra); } const args = await this.validateToolInput(tool, request.params.arguments, request.params.name); const result = await this.executeToolHandler(tool, args, extra); if (isTaskRequest) { return result; } await this.validateToolOutput(tool, result, request.params.name); return result; } catch (error) { if (error instanceof McpError) { if (error.code === ErrorCode.UrlElicitationRequired) { throw error; } } return this.createToolError(error instanceof Error ? error.message : String(error)); } }); this._toolHandlersInitialized = true; } /** * Creates a tool error result. * * @param errorMessage - The error message. * @returns The tool error result. */ createToolError(errorMessage) { return { content: [ { type: "text", text: errorMessage } ], isError: true }; } /** * Validates tool input arguments against the tool's input schema. */ async validateToolInput(tool, args, toolName) { if (!tool.inputSchema) { return void 0; } const inputObj = normalizeObjectSchema(tool.inputSchema); const schemaToParse = inputObj ?? tool.inputSchema; const parseResult = await safeParseAsync(schemaToParse, args); if (!parseResult.success) { const error = "error" in parseResult ? parseResult.error : "Unknown error"; const errorMessage = getParseErrorMessage(error); throw new McpError(ErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${errorMessage}`); } return parseResult.data; } /** * Validates tool output against the tool's output schema. */ async validateToolOutput(tool, result, toolName) { if (!tool.outputSchema) { return; } if (!("content" in result)) { return; } if (result.isError) { return; } if (!result.structuredContent) { throw new McpError(ErrorCode.InvalidParams, `Output validation error: Tool ${toolName} has an output schema but no structured content was provided`); } const outputObj = normalizeObjectSchema(tool.outputSchema); const parseResult = await safeParseAsync(outputObj, result.structuredContent); if (!parseResult.success) { const error = "error" in parseResult ? parseResult.error : "Unknown error"; const errorMessage = getParseErrorMessage(error); throw new McpError(ErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${errorMessage}`); } } /** * Executes a tool handler (either regular or task-based). */ async executeToolHandler(tool, args, extra) { const handler = tool.handler; const isTaskHandler = "createTask" in handler; if (isTaskHandler) { if (!extra.taskStore) { throw new Error("No task store provided."); } const taskExtra = { ...extra, taskStore: extra.taskStore }; if (tool.inputSchema) { const typedHandler = handler; return await Promise.resolve(typedHandler.createTask(args, taskExtra)); } else { const typedHandler = handler; return await Promise.resolve(typedHandler.createTask(taskExtra)); } } if (tool.inputSchema) { const typedHandler = handler; return await Promise.resolve(typedHandler(args, extra)); } else { const typedHandler = handler; return await Promise.resolve(typedHandler(extra)); } } /** * Handles automatic task polling for tools with taskSupport 'optional'. */ async handleAutomaticTaskPolling(tool, request, extra) { if (!extra.taskStore) { throw new Error("No task store provided for task-capable tool."); } const args = await this.validateToolInput(tool, request.params.arguments, request.params.name); const handler = tool.handler; const taskExtra = { ...extra, taskStore: extra.taskStore }; const createTaskResult = args ? await Promise.resolve(handler.createTask(args, taskExtra)) : ( // eslint-disable-next-line @typescript-eslint/no-explicit-any await Promise.resolve(handler.createTask(taskExtra)) ); const taskId = createTaskResult.task.taskId; let task = createTaskResult.task; const pollInterval = task.pollInterval ?? 5e3; while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") { await new Promise((resolve) => setTimeout(resolve, pollInterval)); const updatedTask = await extra.taskStore.getTask(taskId); if (!updatedTask) { throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`); } task = updatedTask; } return await extra.taskStore.getTaskResult(taskId); } setCompletionRequestHandler() { if (this._completionHandlerInitialized) { return; } this.server.assertCanSetRequestHandler(getMethodValue(CompleteRequestSchema)); this.server.registerCapabilities({ completions: {} }); this.server.setRequestHandler(CompleteRequestSchema, async (request) => { switch (request.params.ref.type) { case "ref/prompt": assertCompleteRequestPrompt(request); return this.handlePromptCompletion(request, request.params.ref); case "ref/resource": assertCompleteRequestResourceTemplate(request); return this.handleResourceCompletion(request, request.params.ref); default: throw new McpError(ErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`); } }); this._completionHandlerInitialized = true; } async handlePromptCompletion(request, ref) { const prompt = this._registeredPrompts[ref.name]; if (!prompt) { throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} not found`); } if (!prompt.enabled) { throw new McpError(ErrorCode.InvalidParams, `Prompt ${ref.name} disabled`); } if (!prompt.argsSchema) { return EMPTY_COMPLETION_RESULT; } const promptShape = getObjectShape(prompt.argsSchema); const field = promptShape?.[request.params.argument.name]; if (!isCompletable(field)) { return EMPTY_COMPLETION_RESULT; } const completer = getCompleter(field); if (!completer) { return EMPTY_COMPLETION_RESULT; } const suggestions = await completer(request.params.argument.value, request.params.context); return createCompletionResult(suggestions); } async handleResourceCompletion(request, ref) { const template = Object.values(this._registeredResourceTemplates).find((t) => t.resourceTemplate.uriTemplate.toString() === ref.uri); if (!template) { if (this._registeredResources[ref.uri]) { return EMPTY_COMPLETION_RESULT; } throw new McpError(ErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} not found`); } const completer = template.resourceTemplate.completeCallback(request.params.argument.name); if (!completer) { return EMPTY_COMPLETION_RESULT; } const suggestions = await completer(request.params.argument.value, request.params.context); return createCompletionResult(suggestions); } setResourceRequestHandlers() { if (this._resourceHandlersInitialized) { return; } this.server.assertCanSetRequestHandler(getMethodValue(ListResourcesRequestSchema)); this.server.assertCanSetRequestHandler(getMethodValue(ListResourceTemplatesRequestSchema)); this.server.assertCanSetRequestHandler(getMethodValue(ReadResourceRequestSchema)); this.server.registerCapabilities({ resources: { listChanged: true } }); this.server.setRequestHandler(ListResourcesRequestSchema, async (request, extra) => { const resources = Object.entries(this._registeredResources).filter(([_, resource]) => resource.enabled).map(([uri, resource]) => ({ uri, name: resource.name, ...resource.metadata })); const templateResources = []; for (const template of Object.values(this._registeredResourceTemplates)) { if (!template.resourceTemplate.listCallback) { continue; } const result = await template.resourceTemplate.listCallback(extra); for (const resource of result.resources) { templateResources.push({ ...template.metadata, // the defined resource metadata should override the template metadata if present ...resource }); } } return { resources: [...resources, ...templateResources] }; }); this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { const resourceTemplates = Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({ name, uriTemplate: template.resourceTemplate.uriTemplate.toString(), ...template.metadata })); return { resourceTemplates }; }); this.server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => { const uri = new URL(request.params.uri); const resource = this._registeredResources[uri.toString()]; if (resource) { if (!resource.enabled) { throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} disabled`); } return resource.readCallback(uri, extra); } for (const template of Object.values(this._registeredResourceTemplates)) { const variables = template.resourceTemplate.uriTemplate.match(uri.toString()); if (variables) { return template.readCallback(uri, variables, extra); } } throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`); }); this._resourceHandlersInitialized = true; } setPromptRequestHandlers() { if (this._promptHandlersInitialized) { return; } this.server.assertCanSetRequestHandler(getMethodValue(ListPromptsRequestSchema)); this.server.assertCanSetRequestHandler(getMethodValue(GetPromptRequestSchema)); this.server.registerCapabilities({ prompts: { listChanged: true } }); this.server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: Object.entries(this._registeredPrompts).filter(([, prompt]) => prompt.enabled).map(([name, prompt]) => { return { name, title: prompt.title, description: prompt.description, arguments: prompt.argsSchema ? promptArgumentsFromSchema(prompt.argsSchema) : void 0 }; }) })); this.server.setRequestHandler(GetPromptRequestSchema, async (request, extra) => { const prompt = this._registeredPrompts[request.params.name]; if (!prompt) { throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} not found`); } if (!prompt.enabled) { throw new McpError(ErrorCode.InvalidParams, `Prompt ${request.params.name} disabled`); } if (prompt.argsSchema) { const argsObj = normalizeObjectSchema(prompt.argsSchema); const parseResult = await safeParseAsync(argsObj, request.params.arguments); if (!parseResult.success) { const error = "error" in parseResult ? parseResult.error : "Unknown error"; const errorMessage = getParseErrorMessage(error); throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for prompt ${request.params.name}: ${errorMessage}`); } const args = parseResult.data; const cb = prompt.callback; return await Promise.resolve(cb(args, extra)); } else { const cb = prompt.callback; return await Promise.resolve(cb(extra)); } }); this._promptHandlersInitialized = true; } resource(name, uriOrTemplate, ...rest) { let metadata; if (typeof rest[0] === "object") { metadata = rest.shift(); } const readCallback = rest[0]; if (typeof uriOrTemplate === "string") { if (this._registeredResources[uriOrTemplate]) { throw new Error(`Resource ${uriOrTemplate} is already registered`); } const registeredResource = this._createRegisteredResource(name, void 0, uriOrTemplate, metadata, readCallback); this.setResourceRequestHandlers(); this.sendResourceListChanged(); return registeredResource; } else { if (this._registeredResourceTemplates[name]) { throw new Error(`Resource template ${name} is already registered`); } const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, void 0, uriOrTemplate, metadata, readCallback); this.setResourceRequestHandlers(); this.sendResourceListChanged(); return registeredResourceTemplate; } } registerResource(name, uriOrTemplate, config, readCallback) { if (typeof uriOrTemplate === "string") { if (this._registeredResources[uriOrTemplate]) { throw new Error(`Resource ${uriOrTemplate} is already registered`); } const registeredResource = this._createRegisteredResource(name, config.title, uriOrTemplate, config, readCallback); this.setResourceRequestHandlers(); this.sendResourceListChanged(); return registeredResource; } else { if (this._registeredResourceTemplates[name]) { throw new Error(`Resource template ${name} is already registered`); } const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config.title, uriOrTemplate, config, readCallback); this.setResourceRequestHandlers(); this.sendResourceListChanged(); return registeredResourceTemplate; } } _createRegisteredResource(name, title, uri, metadata, readCallback) { const registeredResource = { name, title, metadata, readCallback, enabled: true, disable: () => registeredResource.update({ enabled: false }), enable: () => registeredResource.update({ enabled: true }), remove: () => registeredResource.update({ uri: null }), update: (updates) => { if (typeof updates.uri !== "undefined" && updates.uri !== uri) { delete this._registeredResources[uri]; if (updates.uri) this._registeredResources[updates.uri] = registeredResource; } if (typeof updates.name !== "undefined") registeredResource.name = updates.name; if (typeof updates.title !== "undefined") registeredResource.title = updates.title; if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata; if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback; if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled; this.sendResourceListChanged(); } }; this._registeredResources[uri] = registeredResource; return registeredResource; } _createRegisteredResourceTemplate(name, title, template, metadata, readCallback) { const registeredResourceTemplate = { resourceTemplate: template, title, metadata, readCallback, enabled: true, disable: () => registeredResourceTemplate.update({ enabled: false }), enable: () => registeredResourceTemplate.update({ enabled: true }), remove: () => registeredResourceTemplate.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { delete this._registeredResourceTemplates[name]; if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate; } if (typeof updates.title !== "undefined") registeredResourceTemplate.title = updates.title; if (typeof updates.template !== "undefined") registeredResourceTemplate.resourceTemplate = updates.template; if (typeof updates.metadata !== "undefined") registeredResourceTemplate.metadata = updates.metadata; if (typeof updates.callback !== "undefined") registeredResourceTemplate.readCallback = updates.callback; if (typeof updates.enabled !== "undefined") registeredResourceTemplate.enabled = updates.enabled; this.sendResourceListChanged(); } }; this._registeredResourceTemplates[name] = registeredResourceTemplate; const variableNames = template.uriTemplate.variableNames; const hasCompleter = Array.isArray(variableNames) && variableNames.some((v) => !!template.completeCallback(v)); if (hasCompleter) { this.setCompletionRequestHandler(); } return registeredResourceTemplate; } _createRegisteredPrompt(name, title, description, argsSchema, callback) { const registeredPrompt = { title, description, argsSchema: argsSchema === void 0 ? void 0 : objectFromShape(argsSchema), callback, enabled: true, disable: () => registeredPrompt.update({ enabled: false }), enable: () => registeredPrompt.update({ enabled: true }), remove: () => registeredPrompt.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { delete this._registeredPrompts[name]; if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt; } if (typeof updates.title !== "undefined") registeredPrompt.title = updates.title; if (typeof updates.description !== "undefined") registeredPrompt.description = updates.description; if (typeof updates.argsSchema !== "undefined") registeredPrompt.argsSchema = objectFromShape(updates.argsSchema); if (typeof updates.callback !== "undefined") registeredPrompt.callback = updates.callback; if (typeof updates.enabled !== "undefined") registeredPrompt.enabled = updates.enabled; this.sendPromptListChanged(); } }; this._registeredPrompts[name] = registeredPrompt; if (argsSchema) { const hasCompletable = Object.values(argsSchema).some((field) => { const inner = field instanceof ZodOptional ? field._def?.innerType : field; return isCompletable(inner); }); if (hasCompletable) { this.setCompletionRequestHandler(); } } return registeredPrompt; } _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, execution, _meta, handler) { validateAndWarnToolName(name); const registeredTool = { title, description, inputSchema: getZodSchemaObject(inputSchema), outputSchema: getZodSchemaObject(outputSchema), annotations, execution, _meta, handler, enabled: true, disable: () => registeredTool.update({ enabled: false }), enable: () => registeredTool.update({ enabled: true }), remove: () => registeredTool.update({ name: null }), update: (updates) => { if (typeof updates.name !== "undefined" && updates.name !== name) { if (typeof updates.name === "string") { validateAndWarnToolName(updates.name); } delete this._registeredTools[name]; if (updates.name) this._registeredTools[updates.name] = registeredTool; } if (typeof updates.title !== "undefined") registeredTool.title = updates.title; if (typeof updates.description !== "undefined") registeredTool.description = updates.description; if (typeof updates.paramsSchema !== "undefined") registeredTool.inputSchema = objectFromShape(updates.paramsSchema); if (typeof updates.outputSchema !== "undefined") registeredTool.outputSchema = objectFromShape(updates.outputSchema); if (typeof updates.callback !== "undefined") registeredTool.handler = updates.callback; if (typeof updates.annotations !== "undefined") registeredTool.annotations = updates.annotations; if (typeof updates._meta !== "undefined") registeredTool._meta = updates._meta; if (typeof updates.enabled !== "undefined") registeredTool.enabled = updates.enabled; this.sendToolListChanged(); } }; this._registeredTools[name] = registeredTool; this.setToolRequestHandlers(); this.sendToolListChanged(); return registeredTool; } /** * tool() implementation. Parses arguments passed to overrides defined above. */ tool(name, ...rest) { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); } let description; let inputSchema; let outputSchema; let annotations; if (typeof rest[0] === "string") { description = rest.shift(); } if (rest.length > 1) { const firstArg = rest[0]; if (isZodRawShapeCompat(firstArg)) { inputSchema = rest.shift(); if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !isZodRawShapeCompat(rest[0])) { annotations = rest.shift(); } } else if (typeof firstArg === "object" && firstArg !== null) { if (Object.values(firstArg).some((v) => typeof v === "object" && v !== null)) { throw new Error(`Tool ${name} expected a Zod schema or ToolAnnotations, but received an unrecognized object`); } annotations = rest.shift(); } } const callback = rest[0]; return this._createRegisteredTool(name, void 0, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, void 0, callback); } /** * Registers a tool with a config object and callback. */ registerTool(name, config, cb) { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); } const { title, description, inputSchema, outputSchema, annotations, _meta } = config; return this._createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, _meta, cb); } prompt(name, ...rest) { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); } let description; if (typeof rest[0] === "string") { description = rest.shift(); } let argsSchema; if (rest.length > 1) { argsSchema = rest.shift(); } const cb = rest[0]; const registeredPrompt = this._createRegisteredPrompt(name, void 0, description, argsSchema, cb); this.setPromptRequestHandlers(); this.sendPromptListChanged(); return registeredPrompt; } /** * Registers a prompt with a config object and callback. */ registerPrompt(name, config, cb) { if (this._registeredPrompts[name]) { throw new Error(`Prompt ${name} is already registered`); } const { title, description, argsSchema } = config; const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema, cb); this.setPromptRequestHandlers(); this.sendPromptListChanged(); return registeredPrompt; } /** * Checks if the server is connected to a transport. * @returns True if the server is connected */ isConnected() { return this.server.transport !== void 0; } /** * Sends a logging message to the client, if connected. * Note: You only need to send the parameters object, not the entire JSON RPC message * @see LoggingMessageNotification * @param params * @param sessionId optional for stateless and backward compatibility */ async sendLoggingMessage(params, sessionId) { return this.server.sendLoggingMessage(params, sessionId); } /** * Sends a resource list changed event to the client, if connected. */ sendResourceListChanged() { if (this.isConnected()) { this.server.sendResourceListChanged(); } } /** * Sends a tool list changed event to the client, if connected. */ sendToolListChanged() { if (this.isConnected()) { this.server.sendToolListChanged(); } } /** * Sends a prompt list changed event to the client, if connected. */ sendPromptListChanged() { if (this.isConnected()) { this.server.sendPromptListChanged(); } } }; var EMPTY_OBJECT_JSON_SCHEMA = { type: "object", properties: {} }; function isZodTypeLike(value) { return value !== null && typeof value === "object" && "parse" in value && typeof value.parse === "function" && "safeParse" in value && typeof value.safeParse === "function"; } function isZodSchemaInstance(obj) { return "_def" in obj || "_zod" in obj || isZodTypeLike(obj); } function isZodRawShapeCompat(obj) { if (typeof obj !== "object" || obj === null) { return false; } if (isZodSchemaInstance(obj)) { return false; } if (Object.keys(obj).length === 0) { return true; } return Object.values(obj).some(isZodTypeLike); } function getZodSchemaObject(schema) { if (!schema) { return void 0; } if (isZodRawShapeCompat(schema)) { return objectFromShape(schema); } if (!isZodSchemaInstance(schema)) { throw new Error("inputSchema must be a Zod schema or raw shape, received an unrecognized object"); } return schema; } function promptArgumentsFromSchema(schema) { const shape = getObjectShape(schema); if (!shape) return []; return Object.entries(shape).map(([name, field]) => { const description = getSchemaDescription(field); const isOptional = isSchemaOptional(field); return { name, description, required: !isOptional }; }); } function getMethodValue(schema) { const shape = getObjectShape(schema); const methodSchema = shape?.method; if (!methodSchema) { throw new Error("Schema is missing a method literal"); } const value = getLiteralValue(methodSchema); if (typeof value === "string") { return value; } throw new Error("Schema method literal must be a string"); } function createCompletionResult(suggestions) { return { completion: { values: suggestions.slice(0, 100), total: suggestions.length, hasMore: suggestions.length > 100 } }; } var EMPTY_COMPLETION_RESULT = { completion: { values: [], hasMore: false } }; // src/mcp/discovery.ts import { z } from "zod"; function text(content) { return { content: [{ type: "text", text: content }] }; } function jsonText(data) { return text(JSON.stringify(data, null, 2)); } function createDiscoveryMcpServer(ctx = {}) { const server = new McpServer({ name: "trellis-mcp-gateway", version: "0.1.0" }); server.registerTool( "list_rooms", { description: "List Trellis rooms known to this gateway (deployed Sprites + local config).", inputSchema: {} }, async () => { const rooms = listRegisteredRooms(ctx).map( ({ name, url, mcpUrl, source, active, deployedAt }) => ({ name, url, mcpUrl, source, active, deployedAt }) ); return jsonText({ gateway: gatewayPublicUrl(ctx), count: rooms.length, rooms }); } ); server.registerTool( "get_room", { description: "Get details for a room by name or URL.", inputSchema: { name: z.string().describe("Room name or base URL") } }, async ({ name }) => { const room = getRegisteredRoom(name, ctx); if (!room) return text(`Room not found: ${name}`); return jsonText(room); } ); server.registerTool( "connect_room", { description: "Return MCP client configuration to connect an agent to a room graph.", inputSchema: { name: z.string().describe("Room name or base URL"), client: z.string().optional().describe("Client hint: cursor | claude | generic"), playgroundRoom: z.string().optional().describe( "Playground ?room= slug \u2014 adds embed-{slug} tenant to bridge / headers" ) } }, async ({ name, client, playgroundRoom }) => { const room = getRegisteredRoom(name, ctx); if (!room) return text(`Room not found: ${name}`); const tenantId = playgroundRoom?.trim() ? playgroundRoomToTenant(playgroundRoom) : void 0; const authHeader = room.apiKey ? { Authorization: `Bearer ${room.apiKey}` } : void 0; const tenantHeader = tenantId ? { "X-Trellis-Tenant": tenantId } : void 0; const httpHeaders = { ...authHeader, ...tenantHeader }; const bridgeTenantArgs = tenantId ? playgroundRoom?.trim().startsWith("embed-") ? ["--tenant", tenantId] : ["--playground-room", playgroundRoom.trim()] : []; const cursor = { mcpServers: { "trellis-room": { url: room.mcpUrl, ...Object.keys(httpHeaders).length ? { headers: httpHeaders } : {} } } }; const claude = room.apiKey ? { mcpServers: { "trellis-room": { command: "npx", args: [ "trellis", "mcp", "bridge", "--room", room.url, "--api-key", room.apiKey, ...bridgeTenantArgs ] } } } : { mcpServers: { "trellis-room": { command: "npx", args: [ "trellis", "mcp", "bridge", "--room", room.url, ...bridgeTenantArgs ] } } }; const oauth = ctx.origin != null ? { loginUrl: `${ctx.origin}/auth/oauth/google`, tokenUse: "Authorization: Bearer <jwt>", protectedResource: `${ctx.origin}/.well-known/oauth-protected-resource` } : void 0; const configs = { cursor, claude, generic: { url: room.mcpUrl, ...Object.keys(httpHeaders).length ? { headers: httpHeaders } : {} } }; return jsonText({ room: { name: room.name, url: room.url, mcpUrl: room.mcpUrl }, ...tenantId ? { playground: { roomSlug: playgroundRoom.trim(), tenantId, urlHint: `https://playground.trellis.computer/?room=${encodeURIComponent(playgroundRoom.trim())}`, mcpToolHint: "Pass room or tenantId on each tool call, or set X-Trellis-Tenant header / bridge --playground-room" } } : {}, oauth, config: client && configs[client] ? configs[client] : { cursor, claude, generic: configs.generic } }); } ); return server; } // node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js var WebStandardStreamableHTTPServerTransport = class { constructor(options = {}) { this._started = false; this._hasHandledRequest = false; this._streamMapping = /* @__PURE__ */ new Map(); this._requestToStreamMapping = /* @__PURE__ */ new Map(); this._requestResponseMap = /* @__PURE__ */ new Map(); this._initialized = false; this._enableJsonResponse = false; this._standaloneSseStreamId = "_GET_stream"; this.sessionIdGenerator = options.sessionIdGenerator; this._enableJsonResponse = options.enableJsonResponse ?? false; this._eventStore = options.eventStore; this._onsessioninitialized = options.onsessioninitialized; this._onsessionclosed = options.onsessionclosed; this._allowedHosts = options.allowedHosts; this._allowedOrigins = options.allowedOrigins; this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false; this._retryInterval = options.retryInterval; } /** * Starts the transport. This is required by the Transport interface but is a no-op * for the Streamable HTTP transport as connections are managed per-request. */ async start() { if (this._started) { throw new Error("Transport already started"); } this._started = true; } /** * Helper to create a JSON error response */ createJsonErrorResponse(status, code, message, options) { const error = { code, message }; if (options?.data !== void 0) { error.data = options.data; } return new Response(JSON.stringify({ jsonrpc: "2.0", error, id: null }), { status, headers: { "Content-Type": "application/json", ...options?.headers } }); } /** * Validates request headers for DNS rebinding protection. * @returns Error response if validation fails, undefined if validation passes. */ validateRequestHeaders(req) { if (!this._enableDnsRebindingProtection) { return void 0; } if (this._allowedHosts && this._allowedHosts.length > 0) { const hostHeader = req.headers.get("host"); if (!hostHeader || !this._allowedHosts.includes(hostHeader)) { const error = `Invalid Host header: ${hostHeader}`; this.onerror?.(new Error(error)); return this.createJsonErrorResponse(403, -32e3, error); } } if (this._allowedOrigins && this._allowedOrigins.length > 0) { const originHeader = req.headers.get("origin"); if (originHeader && !this._allowedOrigins.includes(originHeader)) { const error = `Invalid Origin header: ${originHeader}`; this.onerror?.(new Error(error)); return this.createJsonErrorResponse(403, -32e3, error); } } return void 0; } /** * Handles an incoming HTTP request, whether GET, POST, or DELETE * Returns a Response object (Web Standard) */ async handleRequest(req, options) { if (!this.sessionIdGenerator && this._hasHandledRequest) { throw new Error("Stateless transport cannot be reused across requests. Create a new transport per request."); } this._hasHandledRequest = true; const validationError = this.validateRequestHeaders(req); if (validationError) { return validationError; } switch (req.method) { case "POST": return this.handlePostRequest(req, options); case "GET": return this.handleGetRequest(req); case "DELETE": return this.handleDeleteRequest(req); default: return this.handleUnsupportedRequest(); } } /** * Writes a priming event to establish resumption capability. * Only sends if eventStore is configured (opt-in for resumability) and * the client's protocol version supports empty SSE data (>= 2025-11-25). */ async writePrimingEvent(controller, encoder, streamId, protocolVersion) { if (!this._eventStore) { return; } if (protocolVersion < "2025-11-25") { return; } const primingEventId = await this._eventStore.storeEvent(streamId, {}); let primingEvent = `id: ${primingEventId} data: `; if (this._retryInterval !== void 0) { primingEvent = `id: ${primingEventId} retry: ${this._retryInterval} data: `; } controller.enqueue(encoder.encode(primingEvent)); } /** * Handles GET requests for SSE stream */ async handleGetRequest(req) { const acceptHeader = req.headers.get("accept"); if (!acceptHeader?.includes("text/event-stream")) { this.onerror?.(new Error("Not Acceptable: Client must accept text/event-stream")); return this.createJsonErrorResponse(406, -32e3, "Not Acceptable: Client must accept text/event-stream"); } const sessionError = this.validateSession(req); if (sessionError) { return sessionError; } const protocolError = this.validateProtocolVersion(req); if (protocolError) { return protocolError; } if (this._eventStore) { const lastEventId = req.headers.get("last-event-id"); if (lastEventId) { return this.replayEvents(lastEventId); } } if (this._streamMapping.get(this._standaloneSseStreamId) !== void 0) { this.onerror?.(new Error("Conflict: Only one SSE stream is allowed per session")); return this.createJsonErrorResponse(409, -32e3, "Conflict: Only one SSE stream is allowed per session"); } const encoder = new TextEncoder(); let streamController; const readable = new ReadableStream({ start: (controller) => { streamController = controller; }, cancel: () => { this._streamMapping.delete(this._standaloneSseStreamId); } }); const headers = { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive" }; if (this.sessionId !== void 0) { headers["mcp-session-id"] = this.sessionId; } this._streamMapping.set(this._standaloneSseStreamId, { controller: streamController, encoder, cleanup: () => { this._streamMapping.delete(this._standaloneSseStreamId); try { streamController.close(); } catch { } } }); return new Response(readable, { headers }); } /** * Replays events that would have been sent after the specified event ID * Only used when resumability is enabled */ async replayEvents(lastEventId) { if (!this._eventStore) { this.onerror?.(new Error("Event store not configured")); return this.createJsonErrorResponse(400, -32e3, "Event store not configured"); } try { let streamId; if (this._eventStore.getStreamIdForEventId) { streamId = await this._eventStore.getStreamIdForEventId(lastEventId); if (!streamId) { this.onerror?.(new Error("Invalid event ID format")); return this.createJsonErrorResponse(400, -32e3, "Invalid event ID format"); } if (this._streamMapping.get(streamId) !== void 0) { this.onerror?.(new Error("Conflict: Stream already has an active connection")); return this.createJsonErrorResponse(409, -32e3, "Conflict: Stream already has an active connection"); } } const headers = { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive" }; if (this.sessionId !== void 0) { headers["mcp-session-id"] = this.sessionId; } const encoder = new TextEncoder(); let streamController; const readable = new ReadableStream({ start: (controller) => { streamController = controller; }, cancel: () => { } }); const replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, { send: async (eventId, message) => { const success = this.writeSSEEvent(streamController, encoder, message, eventId); if (!success) { this.onerror?.(new Error("Failed replay events")); try { streamController.close(); } catch { } } } }); this._streamMapping.set(replayedStreamId, { controller: streamController, encoder, cleanup: () => { this._streamMapping.delete(replayedStreamId); try { streamController.close(); } catch { } } }); return new Response(readable, { headers }); } catch (error) { this.onerror?.(error); return this.createJsonErrorResponse(500, -32e3, "E