UNPKG

@nestia/sdk

Version:

Nestia SDK and Swagger generator

125 lines (114 loc) 4.34 kB
import { METHOD_METADATA, PATH_METADATA } from "@nestjs/common/constants"; import { INestiaProject } from "../structures/INestiaProject"; import { IOperationMetadata } from "../structures/IOperationMetadata"; import { IReflectController } from "../structures/IReflectController"; import { IReflectImport } from "../structures/IReflectImport"; import { IReflectMcpOperation } from "../structures/IReflectMcpOperation"; import { IReflectMcpOperationParameter } from "../structures/IReflectMcpOperationParameter"; import { ImportAnalyzer } from "./ImportAnalyzer"; export namespace ReflectMcpOperationAnalyzer { export interface IProps { project: Omit<INestiaProject, "config">; controller: IReflectController; function: Function; name: string; metadata: IOperationMetadata; } export const analyze = (ctx: IProps): IReflectMcpOperation | null => { const route: | { name: string; title?: string; description?: string; inputSchema: object; outputSchema?: object; annotations?: IReflectMcpOperation.IAnnotations; } | undefined = Reflect.getMetadata("nestia/McpRoute", ctx.function); if (route === undefined) return null; const errors: string[] = []; const hasHttpRoute: boolean = Reflect.getMetadata(PATH_METADATA, ctx.function) !== undefined || Reflect.getMetadata(METHOD_METADATA, ctx.function) !== undefined; const hasWebSocketRoute: boolean = Reflect.getMetadata("nestia/WebSocketRoute", ctx.function) !== undefined; if (hasHttpRoute || hasWebSocketRoute) errors.push( "@McpRoute must not be combined with HTTP or WebSocket route decorators on the same method.", ); const preconfigured: IReflectMcpOperationParameter.IPreconfigured[] = ( (Reflect.getMetadata( "nestia/McpRoute/Parameters", ctx.controller.class.prototype, ctx.name, ) ?? []) as IReflectMcpOperationParameter.IPreconfigured[] ).sort((a, b) => a.index - b.index); if (preconfigured.length > 1) errors.push( "@McpRoute tools may declare at most one @McpRoute.Params() parameter.", ); if (ctx.function.length > 1) errors.push( "@McpRoute tools must have 0 or 1 parameters (the MCP arguments object).", ); const imports: IReflectImport[] = []; const parameters: IReflectMcpOperationParameter[] = preconfigured .map((p) => { const matched: IOperationMetadata.IParameter | undefined = ctx.metadata.parameters.find( (m: IOperationMetadata.IParameter) => p.index === m.index, ); if (matched === undefined) { errors.push( `Unable to find parameter type of the ${p.index} (th) argument.`, ); return null; } if (matched.type === null) { errors.push( `Failed to analyze the parameter type of ${JSON.stringify(matched.name)}.`, ); return null; } imports.push(...matched.imports); return { category: "params" as const, name: matched.name, index: p.index, type: matched.type, imports: matched.imports, description: matched.description, jsDocTags: matched.jsDocTags, }; }) .filter((p): p is IReflectMcpOperationParameter => !!p); if (ctx.metadata.success?.imports?.length) imports.push(...ctx.metadata.success.imports); if (errors.length) { ctx.project.errors.push({ file: ctx.controller.file, class: ctx.controller.class.name, function: ctx.function.name, from: ctx.name, contents: errors, }); return null; } return { protocol: "mcp", name: ctx.name, toolName: route.name, title: route.title ?? null, toolDescription: route.description ?? null, inputSchema: route.inputSchema, outputSchema: route.outputSchema ?? null, annotations: route.annotations ?? null, function: ctx.function, parameters, returnType: ctx.metadata.success?.type ?? null, imports: ImportAnalyzer.merge(imports), description: ctx.metadata.description ?? null, jsDocTags: ctx.metadata.jsDocTags, }; }; }