@nestia/sdk 11.2.1 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/assets/bundle/distribute/package.json +1 -1
  2. package/lib/NestiaSdkApplication.js +17 -2
  3. package/lib/NestiaSdkApplication.js.map +1 -1
  4. package/lib/NestiaSwaggerComposer.js +2 -0
  5. package/lib/NestiaSwaggerComposer.js.map +1 -1
  6. package/lib/analyses/AccessorAnalyzer.d.ts +4 -1
  7. package/lib/analyses/AccessorAnalyzer.js.map +1 -1
  8. package/lib/analyses/ReflectControllerAnalyzer.js +3 -2
  9. package/lib/analyses/ReflectControllerAnalyzer.js.map +1 -1
  10. package/lib/analyses/ReflectMcpOperationAnalyzer.d.ts +20 -0
  11. package/lib/analyses/ReflectMcpOperationAnalyzer.js +84 -0
  12. package/lib/analyses/ReflectMcpOperationAnalyzer.js.map +1 -0
  13. package/lib/analyses/TypedMcpRouteAnalyzer.d.ts +15 -0
  14. package/lib/analyses/TypedMcpRouteAnalyzer.js +40 -0
  15. package/lib/analyses/TypedMcpRouteAnalyzer.js.map +1 -0
  16. package/lib/generates/SdkGenerator.js +48 -0
  17. package/lib/generates/SdkGenerator.js.map +1 -1
  18. package/lib/generates/internal/ImportDictionary.d.ts +1 -0
  19. package/lib/generates/internal/ImportDictionary.js +5 -4
  20. package/lib/generates/internal/ImportDictionary.js.map +1 -1
  21. package/lib/generates/internal/SdkDistributionComposer.d.ts +1 -0
  22. package/lib/generates/internal/SdkDistributionComposer.js +8 -1
  23. package/lib/generates/internal/SdkDistributionComposer.js.map +1 -1
  24. package/lib/generates/internal/SdkFileProgrammer.js +4 -1
  25. package/lib/generates/internal/SdkFileProgrammer.js.map +1 -1
  26. package/lib/generates/internal/SdkMcpRouteProgrammer.d.ts +25 -0
  27. package/lib/generates/internal/SdkMcpRouteProgrammer.js +192 -0
  28. package/lib/generates/internal/SdkMcpRouteProgrammer.js.map +1 -0
  29. package/lib/generates/internal/SdkRouteDirectory.d.ts +2 -1
  30. package/lib/generates/internal/SdkRouteDirectory.js.map +1 -1
  31. package/lib/structures/IReflectController.d.ts +2 -1
  32. package/lib/structures/IReflectMcpOperation.d.ts +37 -0
  33. package/lib/structures/IReflectMcpOperation.js +3 -0
  34. package/lib/structures/IReflectMcpOperation.js.map +1 -0
  35. package/lib/structures/IReflectMcpOperationParameter.d.ts +21 -0
  36. package/lib/structures/IReflectMcpOperationParameter.js +3 -0
  37. package/lib/structures/IReflectMcpOperationParameter.js.map +1 -0
  38. package/lib/structures/ITypedApplication.d.ts +2 -1
  39. package/lib/structures/ITypedMcpRoute.d.ts +33 -0
  40. package/lib/structures/ITypedMcpRoute.js +3 -0
  41. package/lib/structures/ITypedMcpRoute.js.map +1 -0
  42. package/package.json +5 -4
  43. package/src/NestiaSdkApplication.ts +23 -3
  44. package/src/NestiaSwaggerComposer.ts +1 -0
  45. package/src/analyses/AccessorAnalyzer.ts +7 -10
  46. package/src/analyses/ReflectControllerAnalyzer.ts +8 -1
  47. package/src/analyses/ReflectMcpOperationAnalyzer.ts +124 -0
  48. package/src/analyses/TypedMcpRouteAnalyzer.ts +40 -0
  49. package/src/generates/SdkGenerator.ts +55 -0
  50. package/src/generates/internal/ImportDictionary.ts +10 -8
  51. package/src/generates/internal/SdkDistributionComposer.ts +6 -0
  52. package/src/generates/internal/SdkFileProgrammer.ts +6 -2
  53. package/src/generates/internal/SdkMcpRouteProgrammer.ts +469 -0
  54. package/src/generates/internal/SdkRouteDirectory.ts +4 -1
  55. package/src/structures/IReflectController.ts +4 -1
  56. package/src/structures/IReflectMcpOperation.ts +40 -0
  57. package/src/structures/IReflectMcpOperationParameter.ts +29 -0
  58. package/src/structures/ITypedApplication.ts +2 -1
  59. package/src/structures/ITypedMcpRoute.ts +35 -0
@@ -0,0 +1,124 @@
1
+ import { INestiaProject } from "../structures/INestiaProject";
2
+ import { IReflectController } from "../structures/IReflectController";
3
+ import { IReflectImport } from "../structures/IReflectImport";
4
+ import { IReflectMcpOperation } from "../structures/IReflectMcpOperation";
5
+ import { IReflectMcpOperationParameter } from "../structures/IReflectMcpOperationParameter";
6
+ import { IOperationMetadata } from "../transformers/IOperationMetadata";
7
+ import { ImportAnalyzer } from "./ImportAnalyzer";
8
+
9
+ /**
10
+ * Reflects controller methods carrying `"nestia/McpRoute"` metadata into
11
+ * {@link IReflectMcpOperation} structures consumed by the SDK generator.
12
+ *
13
+ * @author wildduck - https://github.com/wildduck2
14
+ */
15
+ export namespace ReflectMcpOperationAnalyzer {
16
+ export interface IProps {
17
+ project: Omit<INestiaProject, "config">;
18
+ controller: IReflectController;
19
+ function: Function;
20
+ name: string;
21
+ metadata: IOperationMetadata;
22
+ }
23
+
24
+ export const analyze = (ctx: IProps): IReflectMcpOperation | null => {
25
+ const route:
26
+ | {
27
+ name: string;
28
+ title?: string;
29
+ description?: string;
30
+ inputSchema: object;
31
+ outputSchema?: object;
32
+ annotations?: IReflectMcpOperation.IAnnotations;
33
+ }
34
+ | undefined = Reflect.getMetadata("nestia/McpRoute", ctx.function);
35
+ if (route === undefined) return null;
36
+
37
+ const errors: string[] = [];
38
+
39
+ const preconfigured: IReflectMcpOperationParameter.IPreconfigured[] = (
40
+ (Reflect.getMetadata(
41
+ "nestia/McpRoute/Parameters",
42
+ ctx.controller.class.prototype,
43
+ ctx.name,
44
+ ) ?? []) as IReflectMcpOperationParameter.IPreconfigured[]
45
+ ).sort((a, b) => a.index - b.index);
46
+
47
+ if (preconfigured.length !== 1)
48
+ errors.push(
49
+ "@McpRoute tools must declare exactly one @McpRoute.Params() parameter.",
50
+ );
51
+ if (ctx.metadata.parameters.length !== 1)
52
+ errors.push(
53
+ "@McpRoute tools must have exactly one parameter (the MCP arguments object).",
54
+ );
55
+ else if (preconfigured[0]?.index !== 0)
56
+ errors.push(
57
+ "@McpRoute tools must decorate their only parameter with @McpRoute.Params().",
58
+ );
59
+
60
+ const imports: IReflectImport[] = [];
61
+ const parameters: IReflectMcpOperationParameter[] = preconfigured
62
+ .map((p) => {
63
+ const matched: IOperationMetadata.IParameter | undefined =
64
+ ctx.metadata.parameters.find((m) => p.index === m.index);
65
+ if (matched === undefined) {
66
+ errors.push(
67
+ `Unable to find parameter type of the ${p.index} (th) argument.`,
68
+ );
69
+ return null;
70
+ }
71
+ if (matched.type === null) {
72
+ errors.push(
73
+ `Failed to analyze the parameter type of ${JSON.stringify(matched.name)}.`,
74
+ );
75
+ return null;
76
+ }
77
+ imports.push(...matched.imports);
78
+ return {
79
+ category: "params" as const,
80
+ name: matched.name,
81
+ index: p.index,
82
+ type: matched.type,
83
+ imports: matched.imports,
84
+ description: matched.description,
85
+ jsDocTags: matched.jsDocTags,
86
+ };
87
+ })
88
+ .filter((p): p is IReflectMcpOperationParameter => !!p);
89
+
90
+ if (ctx.metadata.success?.imports?.length)
91
+ imports.push(...ctx.metadata.success.imports);
92
+
93
+ if (errors.length) {
94
+ ctx.project.errors.push({
95
+ file: ctx.controller.file,
96
+ class: ctx.controller.class.name,
97
+ function: ctx.function.name,
98
+ from: ctx.name,
99
+ contents: errors,
100
+ });
101
+ return null;
102
+ }
103
+ // Prefer the decorator's explicit config. Fall back to the JSDoc-derived
104
+ // description captured by SdkOperationTransformer, so users who only
105
+ // document their method with a JSDoc comment still get it surfaced on
106
+ // the `tools/list` wire + the generated SDK metadata.
107
+ return {
108
+ protocol: "mcp",
109
+ name: ctx.name,
110
+ toolName: route.name,
111
+ title: route.title ?? null,
112
+ toolDescription: route.description ?? ctx.metadata.description ?? null,
113
+ inputSchema: route.inputSchema,
114
+ outputSchema: route.outputSchema ?? null,
115
+ annotations: route.annotations ?? null,
116
+ function: ctx.function,
117
+ parameters,
118
+ returnType: ctx.metadata.success?.type ?? null,
119
+ imports: ImportAnalyzer.merge(imports),
120
+ description: ctx.metadata.description ?? null,
121
+ jsDocTags: ctx.metadata.jsDocTags,
122
+ };
123
+ };
124
+ }
@@ -0,0 +1,40 @@
1
+ import { IReflectController } from "../structures/IReflectController";
2
+ import { IReflectMcpOperation } from "../structures/IReflectMcpOperation";
3
+ import { ITypedMcpRoute } from "../structures/ITypedMcpRoute";
4
+
5
+ /**
6
+ * Builds {@link ITypedMcpRoute} entries from a reflected MCP operation,
7
+ * assigning a stable `["mcp", toolName]` accessor for SDK file emission.
8
+ *
9
+ * @author wildduck - https://github.com/wildduck2
10
+ */
11
+ export namespace TypedMcpRouteAnalyzer {
12
+ export const analyze = (props: {
13
+ controller: IReflectController;
14
+ operation: IReflectMcpOperation;
15
+ }): ITypedMcpRoute[] => [
16
+ {
17
+ protocol: "mcp",
18
+ controller: props.controller,
19
+ name: props.operation.name,
20
+ toolName: props.operation.toolName,
21
+ title: props.operation.title,
22
+ toolDescription: props.operation.toolDescription,
23
+ accessor: accessor(props.operation.toolName),
24
+ function: props.operation.function,
25
+ input: props.operation.parameters[0] ?? null,
26
+ returnType: props.operation.returnType,
27
+ inputSchema: props.operation.inputSchema,
28
+ outputSchema: props.operation.outputSchema,
29
+ annotations: props.operation.annotations,
30
+ imports: props.operation.imports,
31
+ description: props.operation.description,
32
+ jsDocTags: props.operation.jsDocTags,
33
+ },
34
+ ];
35
+
36
+ const accessor = (toolName: string): string[] => {
37
+ const safe = toolName.replace(/[^A-Za-z0-9_$]/g, "_");
38
+ return ["mcp", safe];
39
+ };
40
+ }
@@ -7,6 +7,7 @@ import { IReflectOperationError } from "../structures/IReflectOperationError";
7
7
  import { IReflectType } from "../structures/IReflectType";
8
8
  import { ITypedApplication } from "../structures/ITypedApplication";
9
9
  import { ITypedHttpRoute } from "../structures/ITypedHttpRoute";
10
+ import { ITypedMcpRoute } from "../structures/ITypedMcpRoute";
10
11
  import { CloneGenerator } from "./CloneGenerator";
11
12
  import { SdkDistributionComposer } from "./internal/SdkDistributionComposer";
12
13
  import { SdkFileProgrammer } from "./internal/SdkFileProgrammer";
@@ -55,6 +56,7 @@ export namespace SdkGenerator {
55
56
  if (app.project.config.distribute !== undefined)
56
57
  await SdkDistributionComposer.compose({
57
58
  config: app.project.config,
59
+ mcp: app.routes.some((r) => r.protocol === "mcp"),
58
60
  websocket: app.routes.some((r) => r.protocol === "websocket"),
59
61
  });
60
62
  };
@@ -63,6 +65,8 @@ export namespace SdkGenerator {
63
65
  app: ITypedApplication,
64
66
  ): IReflectOperationError[] => {
65
67
  const errors: IReflectOperationError[] = [];
68
+ validateMcpDuplicates(errors)(app.routes);
69
+ validateMcpAccessors(errors)(app.routes);
66
70
  if (app.project.config.clone === true) return errors;
67
71
  for (const route of app.routes)
68
72
  if (route.protocol === "http")
@@ -74,6 +78,57 @@ export namespace SdkGenerator {
74
78
  return errors;
75
79
  };
76
80
 
81
+ const validateMcpDuplicates =
82
+ (errors: IReflectOperationError[]) =>
83
+ (routes: ITypedApplication["routes"]): void => {
84
+ const dict: Map<string, ITypedMcpRoute[]> = new Map();
85
+ for (const route of routes)
86
+ if (route.protocol === "mcp") {
87
+ const array = dict.get(route.toolName) ?? [];
88
+ array.push(route);
89
+ dict.set(route.toolName, array);
90
+ }
91
+
92
+ for (const [toolName, list] of dict)
93
+ if (list.length > 1)
94
+ for (const route of list)
95
+ errors.push({
96
+ file: route.controller.file,
97
+ class: route.controller.class.name,
98
+ function: route.function.name || route.name,
99
+ from: `@McpRoute(${JSON.stringify(toolName)})`,
100
+ contents: [
101
+ `Duplicate MCP tool name ${JSON.stringify(toolName)} is not allowed.`,
102
+ ],
103
+ });
104
+ };
105
+
106
+ const validateMcpAccessors =
107
+ (errors: IReflectOperationError[]) =>
108
+ (routes: ITypedApplication["routes"]): void => {
109
+ const dict: Map<string, ITypedMcpRoute[]> = new Map();
110
+ for (const route of routes)
111
+ if (route.protocol === "mcp") {
112
+ const accessor = route.accessor.join(".");
113
+ const array = dict.get(accessor) ?? [];
114
+ array.push(route);
115
+ dict.set(accessor, array);
116
+ }
117
+
118
+ for (const [accessor, list] of dict)
119
+ if (list.length > 1)
120
+ for (const route of list)
121
+ errors.push({
122
+ file: route.controller.file,
123
+ class: route.controller.class.name,
124
+ function: route.function.name || route.name,
125
+ from: `@McpRoute(${JSON.stringify(route.toolName)})`,
126
+ contents: [
127
+ `MCP tool name ${JSON.stringify(route.toolName)} conflicts on generated SDK accessor "api.functional.${accessor}".`,
128
+ ],
129
+ });
130
+ };
131
+
77
132
  const validateImplicit = (props: {
78
133
  config: INestiaConfig;
79
134
  errors: IReflectOperationError[];
@@ -1,5 +1,5 @@
1
1
  import path from "path";
2
- import { HashMap, TreeSet, hash } from "tstl";
2
+ import { HashMap, TreeMap, hash } from "tstl";
3
3
  import ts from "typescript";
4
4
 
5
5
  import { ImportAnalyzer } from "../../analyses/ImportAnalyzer";
@@ -78,10 +78,11 @@ export class ImportDictionary {
78
78
  };
79
79
  const value: ICompositeValue = this.components_.take(key, () => ({
80
80
  ...key,
81
- elements: new TreeSet<string>(),
81
+ elements: new TreeMap<string, string | null>(),
82
82
  }));
83
- if (props.type === "element") value.elements.insert(props.name);
84
- return props.name;
83
+ if (props.type === "element")
84
+ value.elements.set(props.name, props.alias ?? null);
85
+ return props.type === "element" ? (props.alias ?? props.name) : props.name;
85
86
  }
86
87
 
87
88
  public toStatements(outDir: string): ts.Statement[] {
@@ -149,11 +150,11 @@ export class ImportDictionary {
149
150
  c.default !== null ? ts.factory.createIdentifier(c.default) : undefined,
150
151
  c.elements.size() !== 0
151
152
  ? ts.factory.createNamedImports(
152
- Array.from(c.elements).map((elem) =>
153
+ Array.from(c.elements).map(({ first: name, second: alias }) =>
153
154
  ts.factory.createImportSpecifier(
154
155
  false,
155
- undefined,
156
- ts.factory.createIdentifier(elem),
156
+ alias !== null ? ts.factory.createIdentifier(name) : undefined,
157
+ ts.factory.createIdentifier(alias ?? name),
157
158
  ),
158
159
  ),
159
160
  )
@@ -166,6 +167,7 @@ export namespace ImportDictionary {
166
167
  type: "default" | "element" | "asterisk";
167
168
  file: string;
168
169
  name: string;
170
+ alias?: string;
169
171
  declaration: boolean;
170
172
  }
171
173
  }
@@ -177,7 +179,7 @@ interface ICompositeKey {
177
179
  default: string | null;
178
180
  }
179
181
  interface ICompositeValue extends ICompositeKey {
180
- elements: TreeSet<string>;
182
+ elements: TreeMap<string, string | null>;
181
183
  }
182
184
 
183
185
  const NODE_MODULES = "node_modules/";
@@ -8,6 +8,7 @@ import { INestiaConfig } from "../../INestiaConfig";
8
8
  export namespace SdkDistributionComposer {
9
9
  export const compose = async (props: {
10
10
  config: INestiaConfig;
11
+ mcp: boolean;
11
12
  websocket: boolean;
12
13
  }) => {
13
14
  if (!fs.existsSync(props.config.distribute!))
@@ -34,6 +35,10 @@ export namespace SdkDistributionComposer {
34
35
  execute("npm install --save-dev rimraf");
35
36
  execute(`npm install --save @nestia/fetcher@${v.version}`);
36
37
  execute(`npm install --save typia@${v.typia}`);
38
+ if (props.mcp)
39
+ execute(
40
+ `npm install --save @modelcontextprotocol/sdk@${v["@modelcontextprotocol/sdk"]}`,
41
+ );
37
42
  if (props.websocket) execute(`npm install --save tgrid@${v.tgrid}`);
38
43
  execute("npx typia setup --manager npm");
39
44
 
@@ -96,6 +101,7 @@ export namespace SdkDistributionComposer {
96
101
  }
97
102
 
98
103
  interface IDependencies {
104
+ "@modelcontextprotocol/sdk": string;
99
105
  version: string;
100
106
  typia: string;
101
107
  tgrid: string;
@@ -4,11 +4,13 @@ import ts from "typescript";
4
4
  import { INestiaProject } from "../../structures/INestiaProject";
5
5
  import { ITypedApplication } from "../../structures/ITypedApplication";
6
6
  import { ITypedHttpRoute } from "../../structures/ITypedHttpRoute";
7
+ import { ITypedMcpRoute } from "../../structures/ITypedMcpRoute";
7
8
  import { ITypedWebSocketRoute } from "../../structures/ITypedWebSocketRoute";
8
9
  import { MapUtil } from "../../utils/MapUtil";
9
10
  import { FilePrinter } from "./FilePrinter";
10
11
  import { ImportDictionary } from "./ImportDictionary";
11
12
  import { SdkHttpRouteProgrammer } from "./SdkHttpRouteProgrammer";
13
+ import { SdkMcpRouteProgrammer } from "./SdkMcpRouteProgrammer";
12
14
  import { SdkRouteDirectory } from "./SdkRouteDirectory";
13
15
  import { SdkWebSocketRouteProgrammer } from "./SdkWebSocketRouteProgrammer";
14
16
 
@@ -27,7 +29,7 @@ export namespace SdkFileProgrammer {
27
29
 
28
30
  const emplace =
29
31
  (directory: SdkRouteDirectory) =>
30
- (route: ITypedHttpRoute | ITypedWebSocketRoute): void => {
32
+ (route: ITypedHttpRoute | ITypedWebSocketRoute | ITypedMcpRoute): void => {
31
33
  // OPEN DIRECTORIES
32
34
  for (const key of route.accessor.slice(0, -1)) {
33
35
  directory = MapUtil.take(
@@ -80,7 +82,9 @@ export namespace SdkFileProgrammer {
80
82
  statements.push(
81
83
  ...(route.protocol === "http"
82
84
  ? SdkHttpRouteProgrammer.write(project)(importer)(route)
83
- : SdkWebSocketRouteProgrammer.write(project)(importer)(route)),
85
+ : route.protocol === "websocket"
86
+ ? SdkWebSocketRouteProgrammer.write(project)(importer)(route)
87
+ : SdkMcpRouteProgrammer.write(project)(importer)(route)),
84
88
  );
85
89
  if (i !== directory.routes.length - 1)
86
90
  statements.push(FilePrinter.enter());