UNPKG

@nestia/core

Version:

Super-fast validation decorators of NestJS

260 lines 13.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.McpAdaptor = void 0; const common_1 = require("@nestjs/common"); /** * MCP (Model Context Protocol) adaptor. * * `McpAdaptor` exposes every method decorated with {@link McpRoute} as an MCP * tool, reachable by LLM clients through a stateless Streamable HTTP endpoint. * * At bootstrap the adaptor walks the {@link NestContainer}, collects every * controller method carrying `"nestia/McpRoute"` metadata, and caches a tool * registry. A fresh MCP server and transport pair is spun up per incoming HTTP * request, following MCP stateless Streamable HTTP mode. This adaptor * intentionally does not manage `Mcp-Session-Id` state. * * Typia-generated JSON Schemas flow through unchanged; the Zod-based high-level * registration API of `McpServer` is bypassed by accessing the low-level * `.server` handler. * * Error mapping follows the MCP specification: * * - Unknown tool name: JSON-RPC `-32601`. * - Typia validation failure: JSON-RPC `-32602` with structured diagnostics. * - Handler throws {@link HttpException}: success response with `isError: true`, * so the LLM can read the message and recover. * - Any other throw: JSON-RPC `-32603`. * * @author wildduck - https://github.com/wildduck2 * @example * ```typescript * import core from "@nestia/core"; * import { NestFactory } from "@nestjs/core"; * * const app = await NestFactory.create(AppModule); * await core.McpAdaptor.upgrade(app, { path: "/mcp" }); * await app.listen(3000); * ```; */ class McpAdaptor { /** * Upgrade a running Nest application with a stateless MCP endpoint. * * Scans the application container for methods decorated with {@link McpRoute}, * then registers a catch-all HTTP route at the configured path. Each incoming * request builds a fresh MCP server + transport on demand, wires the * registered tools into it, and delegates handling. * * Must be called after `NestFactory.create(...)` but before `app.listen(...)` * if you want the MCP endpoint to be reachable alongside your regular HTTP * routes. * * @param app Running Nest application instance. * @param options Transport and identity overrides. */ static upgrade(app_1) { return __awaiter(this, arguments, void 0, function* (app, options = {}) { var _a, _b, _c; var _d, _e, _f, _g, _h; if ("sessioned" in options) throw new Error("McpAdaptor.upgrade() supports stateless Streamable HTTP only; sessioned mode is not implemented."); const tools = []; const container = app.container; for (const module of container.getModules().values()) { for (const wrapper of module.controllers.values()) { const instance = wrapper.instance; if (!instance) continue; const visited = new Set(); for (let proto = Object.getPrototypeOf(instance); proto !== null && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) { for (const key of Object.getOwnPropertyNames(proto)) { if (key === "constructor" || visited.has(key)) continue; visited.add(key); const method = proto[key]; if (typeof method !== "function") continue; const meta = Reflect.getMetadata("nestia/McpRoute", method); if (!meta) continue; const params = (_d = Reflect.getMetadata("nestia/McpRoute/Parameters", proto, key)) !== null && _d !== void 0 ? _d : []; const paramValidator = (_a = params.find((p) => p.category === "params")) === null || _a === void 0 ? void 0 : _a.validate; tools.push({ meta, source: `${(_f = (_e = (_b = wrapper.metatype) === null || _b === void 0 ? void 0 : _b.name) !== null && _e !== void 0 ? _e : (_c = proto.constructor) === null || _c === void 0 ? void 0 : _c.name) !== null && _f !== void 0 ? _f : "UnknownController"}.${String(key)}`, validateArgs: paramValidator, handler: (args) => __awaiter(this, void 0, void 0, function* () { return method.call(instance, args); }), }); } } } } assertUniqueTools(tools); const serverInfo = (_g = options.serverInfo) !== null && _g !== void 0 ? _g : { name: "nestia-mcp", version: "1.0.0", }; const { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, McpServer, StreamableHTTPServerTransport, } = yield loadMcpSdk(); const http = app.getHttpAdapter(); const route = (_h = options.path) !== null && _h !== void 0 ? _h : "/mcp"; http.all(route, (req, res) => __awaiter(this, void 0, void 0, function* () { var _a, _b; // Stateless mode requires a fresh transport per request; sharing one // across clients races on internal initialization and request IDs. const mcp = new McpServer(serverInfo, { capabilities: { tools: {} }, }); const server = mcp.server; server.setRequestHandler(ListToolsRequestSchema, () => __awaiter(this, void 0, void 0, function* () { return ({ tools: tools.map((t) => ({ name: t.meta.name, title: t.meta.title, description: t.meta.description, inputSchema: t.meta.inputSchema, outputSchema: t.meta.outputSchema, annotations: t.meta.annotations, })), }); })); server.setRequestHandler(CallToolRequestSchema, (reqMsg) => __awaiter(this, void 0, void 0, function* () { var _a; const tool = tools.find((t) => t.meta.name === reqMsg.params.name); if (!tool) throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${reqMsg.params.name}`); const args = (_a = reqMsg.params.arguments) !== null && _a !== void 0 ? _a : {}; if (tool.validateArgs) { const err = tool.validateArgs(args); if (err !== null) { const body = err instanceof common_1.BadRequestException ? err.getResponse() : undefined; throw new McpError(ErrorCode.InvalidParams, err.message, { errors: body === null || body === void 0 ? void 0 : body.errors, path: body === null || body === void 0 ? void 0 : body.path, expected: body === null || body === void 0 ? void 0 : body.expected, value: body === null || body === void 0 ? void 0 : body.value, reason: body === null || body === void 0 ? void 0 : body.reason, }); } } try { const result = yield tool.handler(args); if (result === undefined) return { content: [] }; return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result), }, ], }; } catch (e) { if (e instanceof common_1.HttpException) { return { content: [{ type: "text", text: e.message }], isError: true, }; } throw new McpError(ErrorCode.InternalError, e instanceof Error ? e.message : "Internal error"); } })); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true, }); try { yield mcp.connect(transport); yield transport.handleRequest((_a = req.raw) !== null && _a !== void 0 ? _a : req, (_b = res.raw) !== null && _b !== void 0 ? _b : res, req.body); } finally { yield transport.close().catch(() => { }); yield mcp.close().catch(() => { }); } })); }); } } exports.McpAdaptor = McpAdaptor; const assertUniqueTools = (tools) => { var _a; const dict = new Map(); for (const tool of tools) { const array = (_a = dict.get(tool.meta.name)) !== null && _a !== void 0 ? _a : []; array.push(tool); dict.set(tool.meta.name, array); } const duplicated = Array.from(dict.entries()).filter(([, list]) => list.length > 1); if (duplicated.length === 0) return; throw new Error([ "Duplicated MCP tool names are not allowed.", ...duplicated.map(([name, list]) => ` - ${JSON.stringify(name)}: ${list.map((tool) => tool.source).join(", ")}`), ].join("\n")); }; const loadMcpSdk = () => __awaiter(void 0, void 0, void 0, function* () { try { const [server, transport, types] = yield Promise.all([ Promise.resolve().then(() => __importStar(require("@modelcontextprotocol/sdk/server/mcp.js"))), Promise.resolve().then(() => __importStar(require("@modelcontextprotocol/sdk/server/streamableHttp.js"))), Promise.resolve().then(() => __importStar(require("@modelcontextprotocol/sdk/types.js"))), ]); return { McpServer: server.McpServer, StreamableHTTPServerTransport: transport.StreamableHTTPServerTransport, CallToolRequestSchema: types.CallToolRequestSchema, ErrorCode: types.ErrorCode, ListToolsRequestSchema: types.ListToolsRequestSchema, McpError: types.McpError, }; } catch (_a) { throw new Error("McpAdaptor.upgrade() requires @modelcontextprotocol/sdk. Install it before enabling MCP routes."); } }); //# sourceMappingURL=McpAdaptor.js.map