@happyvertical/smrt-core 0.40.59 → 0.40.60
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.
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +75 -31
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +32 -18
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +367 -168
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/tool-schema.d.ts +32 -1
- package/dist/generators/tool-schema.d.ts.map +1 -1
- package/dist/generators/tool-schema.js +108 -36
- package/dist/generators/tool-schema.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +1241 -1
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +1 -0
- package/dist/prebuild/index.js.map +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +1 -0
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +5 -3
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +7 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +4 -4
|
@@ -18,6 +18,7 @@ export interface RuntimeOptions {
|
|
|
18
18
|
name: string;
|
|
19
19
|
description: string;
|
|
20
20
|
inputSchema: Record<string, unknown>;
|
|
21
|
+
outputSchema?: Record<string, unknown>;
|
|
21
22
|
}>;
|
|
22
23
|
/** Internal invocation metadata; never exposed by the MCP tools/list result. */
|
|
23
24
|
customActions?: Record<string, {
|
|
@@ -37,6 +38,13 @@ export interface RuntimeOptions {
|
|
|
37
38
|
* `SMRT_MCP_ALLOW_CROSS_TENANT` env vars (this server has no auth principal).
|
|
38
39
|
*/
|
|
39
40
|
tenantScopedObjects?: string[];
|
|
41
|
+
/**
|
|
42
|
+
* Build-time approved STI discriminators, keyed by the lowercased MCP object
|
|
43
|
+
* prefix. The generated runtime uses this instead of searching an initially
|
|
44
|
+
* empty registry, then loads the approved qualified type through the public
|
|
45
|
+
* collection API.
|
|
46
|
+
*/
|
|
47
|
+
stiTargets?: Record<string, Record<string, string>>;
|
|
40
48
|
}
|
|
41
49
|
/**
|
|
42
50
|
* Generate runtime bootstrap code for MCP server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAStD,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,UAAU,CAAC;IAErB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,wDAAwD;IACxD,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAStD,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,UAAU,CAAC;IAErB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,wDAAwD;IACxD,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,CAAC,CAAC;IACH,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN;QACE,KAAK,EAAE,iBAAiB,CAAC;QACzB,QAAQ,EAAE,OAAO,CAAC;QAClB,sEAAsE;QACtE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,OAAO,CAAC;KACxB,CACF,CAAC;IAEF;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CA8d7E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,GAAE,MAA6B,GACxC,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CA0GR"}
|
|
@@ -12,7 +12,7 @@ function capitalize(str) {
|
|
|
12
12
|
* @returns TypeScript code for server entry point
|
|
13
13
|
*/
|
|
14
14
|
function generateRuntimeBootstrap(options = {}) {
|
|
15
|
-
const { name = "smrt-mcp-server", version = "1.0.0", description = "Auto-generated MCP server from SMRT objects", debug = false, tools = [], customActions = {}, tenantScopedObjects = [] } = options;
|
|
15
|
+
const { name = "smrt-mcp-server", version = "1.0.0", description = "Auto-generated MCP server from SMRT objects", debug = false, tools = [], customActions = {}, tenantScopedObjects = [], stiTargets = {} } = options;
|
|
16
16
|
const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : "[]";
|
|
17
17
|
const tenantScopedSet = Array.from(new Set(tenantScopedObjects.map((n) => n.toLowerCase())));
|
|
18
18
|
const hasTenantScoped = tenantScopedSet.length > 0;
|
|
@@ -28,13 +28,17 @@ ${indent} const offset = args.offset ?? 0;
|
|
|
28
28
|
${indent} const where = args.where ?? {};
|
|
29
29
|
|
|
30
30
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
31
|
-
${indent} persistence: { type: '
|
|
31
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
32
32
|
${indent} ai: aiConfig
|
|
33
33
|
${indent} });
|
|
34
34
|
|
|
35
35
|
${indent} const items = await collection.list({ where, limit, offset });
|
|
36
36
|
${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));
|
|
37
|
-
${indent}
|
|
37
|
+
${indent} const structuredContent = {
|
|
38
|
+
${indent} data: itemsPublic,
|
|
39
|
+
${indent} meta: { total: await collection.count({ where }), limit, offset, count: items.length },
|
|
40
|
+
${indent} };
|
|
41
|
+
${indent} return successResult(structuredContent, JSON.stringify(itemsPublic));
|
|
38
42
|
${indent}}`;
|
|
39
43
|
case "get": return `${indent}case '${tool.name}': {
|
|
40
44
|
${indent} if (!args.id && !args.slug) {
|
|
@@ -42,7 +46,7 @@ ${indent} throw new Error('Either id or slug is required');
|
|
|
42
46
|
${indent} }
|
|
43
47
|
|
|
44
48
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
45
|
-
${indent} persistence: { type: '
|
|
49
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
46
50
|
${indent} ai: aiConfig
|
|
47
51
|
${indent} });
|
|
48
52
|
|
|
@@ -53,18 +57,15 @@ ${indent} if (!item) {
|
|
|
53
57
|
${indent} throw new Error('Object not found');
|
|
54
58
|
${indent} }
|
|
55
59
|
|
|
56
|
-
${indent} return
|
|
60
|
+
${indent} return successResult(item.toPublicJSON(PUBLIC_JSON_OPTIONS));
|
|
57
61
|
${indent}}`;
|
|
58
62
|
case "create": return `${indent}case '${tool.name}': {
|
|
59
|
-
${indent} const collection = await
|
|
60
|
-
${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
|
|
61
|
-
${indent} ai: aiConfig
|
|
62
|
-
${indent} });
|
|
63
|
+
${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget('${objectName}', args, aiConfig);
|
|
63
64
|
|
|
64
|
-
${indent} const newItem = await collection.create(applyWritablePolicy(
|
|
65
|
+
${indent} const newItem = await collection.create(applyWritablePolicy(targetObjectName, args));
|
|
65
66
|
${indent} await newItem.save();
|
|
66
67
|
|
|
67
|
-
${indent} return
|
|
68
|
+
${indent} return successResult(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS));
|
|
68
69
|
${indent}}`;
|
|
69
70
|
case "update": return `${indent}case '${tool.name}': {
|
|
70
71
|
${indent} const { id, ...updateData } = args;
|
|
@@ -73,7 +74,7 @@ ${indent} throw new Error('ID is required for update');
|
|
|
73
74
|
${indent} }
|
|
74
75
|
|
|
75
76
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
76
|
-
${indent} persistence: { type: '
|
|
77
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
77
78
|
${indent} ai: aiConfig
|
|
78
79
|
${indent} });
|
|
79
80
|
|
|
@@ -85,7 +86,7 @@ ${indent} }
|
|
|
85
86
|
${indent} Object.assign(existing, applyWritablePolicy('${capitalize(objectName)}', updateData));
|
|
86
87
|
${indent} await existing.save();
|
|
87
88
|
|
|
88
|
-
${indent} return
|
|
89
|
+
${indent} return successResult(existing.toPublicJSON(PUBLIC_JSON_OPTIONS));
|
|
89
90
|
${indent}}`;
|
|
90
91
|
case "delete": return `${indent}case '${tool.name}': {
|
|
91
92
|
${indent} if (!args.id) {
|
|
@@ -93,7 +94,7 @@ ${indent} throw new Error('ID is required for delete');
|
|
|
93
94
|
${indent} }
|
|
94
95
|
|
|
95
96
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
96
|
-
${indent} persistence: { type: '
|
|
97
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
97
98
|
${indent} ai: aiConfig
|
|
98
99
|
${indent} });
|
|
99
100
|
|
|
@@ -104,7 +105,7 @@ ${indent} }
|
|
|
104
105
|
|
|
105
106
|
${indent} await toDelete.delete();
|
|
106
107
|
|
|
107
|
-
${indent} return
|
|
108
|
+
${indent} return successResult({ success: true, message: 'Object deleted successfully' });
|
|
108
109
|
${indent}}`;
|
|
109
110
|
default: return `${indent}case '${tool.name}': {
|
|
110
111
|
${indent} const actionMeta = CUSTOM_ACTIONS['${tool.name}'] || { scope: 'item', isStatic: false, legacyOptions: true };
|
|
@@ -118,7 +119,7 @@ ${indent} throw new Error('Custom action ${action} is collection-scoped and d
|
|
|
118
119
|
${indent} }
|
|
119
120
|
|
|
120
121
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
121
|
-
${indent} persistence: { type: '
|
|
122
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
122
123
|
${indent} ai: aiConfig
|
|
123
124
|
${indent} });
|
|
124
125
|
|
|
@@ -147,14 +148,15 @@ ${indent} ]);
|
|
|
147
148
|
${indent} const result = await actionMethod.call(target, ...methodArgs);
|
|
148
149
|
${indent} const failure = normalizeCustomActionFailure(result);
|
|
149
150
|
${indent} if (failure) {
|
|
150
|
-
${indent} return
|
|
151
|
-
${indent}
|
|
152
|
-
${indent}
|
|
153
|
-
${indent}
|
|
154
|
-
${indent}
|
|
151
|
+
${indent} return errorResult(
|
|
152
|
+
${indent} { error: failure },
|
|
153
|
+
${indent} JSON.stringify({ error: failure }),
|
|
154
|
+
${indent} { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },
|
|
155
|
+
${indent} );
|
|
155
156
|
${indent} }
|
|
156
157
|
|
|
157
|
-
${indent}
|
|
158
|
+
${indent} const publicResult = toPublicResult(result);
|
|
159
|
+
${indent} return successResult({ data: publicResult }, JSON.stringify(publicResult));
|
|
158
160
|
${indent}}`;
|
|
159
161
|
}
|
|
160
162
|
}).join("\n\n");
|
|
@@ -180,6 +182,8 @@ import {
|
|
|
180
182
|
Server,
|
|
181
183
|
} from '@modelcontextprotocol/server';
|
|
182
184
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
185
|
+
import { existsSync } from 'node:fs';
|
|
186
|
+
import { resolve } from 'node:path';
|
|
183
187
|
import { pathToFileURL } from 'node:url';
|
|
184
188
|
import { ObjectRegistry } from '@happyvertical/smrt-core';
|
|
185
189
|
import { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';
|
|
@@ -194,6 +198,7 @@ const DEBUG = ${debug};
|
|
|
194
198
|
// Static tool definitions (generated at build time)
|
|
195
199
|
const TOOLS = ${toolsCode};
|
|
196
200
|
const CUSTOM_ACTIONS = ${JSON.stringify(customActions)};
|
|
201
|
+
const STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};
|
|
197
202
|
${hasTenantScoped ? `
|
|
198
203
|
// Fail-closed tenant context (#1554): tenant-scoped objects must run inside a
|
|
199
204
|
// tenant. This stdio server has no auth principal, so the tenant is taken from
|
|
@@ -243,6 +248,23 @@ function applyWritablePolicy(objectName: string, data: any): Record<string, any>
|
|
|
243
248
|
return result;
|
|
244
249
|
}
|
|
245
250
|
|
|
251
|
+
/** Resolve an advertised STI discriminator to its registered subtype collection. */
|
|
252
|
+
async function resolveCreateTarget(baseObjectName: string, args: Record<string, any>, aiConfig: any) {
|
|
253
|
+
let objectName = baseObjectName;
|
|
254
|
+
const discriminator = args._meta_type;
|
|
255
|
+
const targets = STI_TARGETS[baseObjectName];
|
|
256
|
+
if (typeof discriminator === 'string' && targets) {
|
|
257
|
+
const target = targets[discriminator];
|
|
258
|
+
if (!target) throw new Error('Unknown STI discriminator: ' + discriminator);
|
|
259
|
+
objectName = target;
|
|
260
|
+
}
|
|
261
|
+
const collection = await ObjectRegistry.getCollection(objectName, {
|
|
262
|
+
persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
263
|
+
ai: aiConfig,
|
|
264
|
+
});
|
|
265
|
+
return { collection, objectName };
|
|
266
|
+
}
|
|
267
|
+
|
|
246
268
|
/**
|
|
247
269
|
* Sensitive-field-safe serialization for custom-action results (#1540).
|
|
248
270
|
* Recurses through arrays and plain objects so nested SmrtObjects are stripped
|
|
@@ -267,6 +289,22 @@ function toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any
|
|
|
267
289
|
return out;
|
|
268
290
|
}
|
|
269
291
|
|
|
292
|
+
function successResult(structuredContent: any, text = JSON.stringify(structuredContent)) {
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: 'text', text }],
|
|
295
|
+
structuredContent,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function errorResult(structuredContent: any, text: string, _meta?: Record<string, any>) {
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: 'text', text }],
|
|
302
|
+
isError: true,
|
|
303
|
+
structuredContent,
|
|
304
|
+
...(_meta ? { _meta } : {}),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
270
308
|
/**
|
|
271
309
|
* Main server startup function
|
|
272
310
|
*/
|
|
@@ -282,6 +320,17 @@ ${hasTenantScoped ? `
|
|
|
282
320
|
// with no interceptor to enforce it.
|
|
283
321
|
enableTenancy();
|
|
284
322
|
` : ""}
|
|
323
|
+
// Register the application package manifest before resolving generated
|
|
324
|
+
// object names. Generated servers are commonly run from the application
|
|
325
|
+
// package itself, which is not a node_modules dependency of its process.
|
|
326
|
+
const localManifestPaths = [
|
|
327
|
+
resolve(process.cwd(), 'dist', 'manifest.json'),
|
|
328
|
+
resolve(process.cwd(), '.smrt', 'manifest.json'),
|
|
329
|
+
].filter(existsSync);
|
|
330
|
+
if (localManifestPaths.length > 0) {
|
|
331
|
+
ObjectRegistry.loadAllManifests({ manifestPaths: localManifestPaths });
|
|
332
|
+
}
|
|
333
|
+
|
|
285
334
|
// Load configuration from environment and .smrt.config files
|
|
286
335
|
const appConfig = await loadConfig();
|
|
287
336
|
const aiConfig = appConfig?.ai || {};
|
|
@@ -355,15 +404,10 @@ ${hasTenantScoped ? `
|
|
|
355
404
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
356
405
|
console.error(\`[MCP] Tool execution failed: \${toolName}\`, error);
|
|
357
406
|
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
text: \`Error executing tool \${toolName}: \${errorMessage}\`,
|
|
363
|
-
},
|
|
364
|
-
],
|
|
365
|
-
isError: true,
|
|
366
|
-
};
|
|
407
|
+
return errorResult(
|
|
408
|
+
{ error: { message: errorMessage } },
|
|
409
|
+
\`Error executing tool \${toolName}: \${errorMessage}\`,
|
|
410
|
+
);
|
|
367
411
|
}
|
|
368
412
|
});
|
|
369
413
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-runtime-template.js","names":[],"sources":["../../src/generators/mcp-runtime-template.ts"],"sourcesContent":["/**\n * Runtime bootstrap template for generated MCP servers\n *\n * This template provides stdio transport integration for SMRT-generated MCP servers.\n * It handles:\n * - Server initialization with @modelcontextprotocol/server v2\n * - Tool registration from MCPGenerator\n * - Stdio transport connection\n * - Error handling and logging\n * - Graceful shutdown\n */\n\nimport type { CustomActionScope } from './custom-action.js';\nimport type { MCPConfig, MCPContext } from './mcp.js';\n\n/**\n * Helper function to capitalize first letter\n */\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\nexport interface RuntimeOptions {\n /** Server name (defaults to package name) */\n name?: string;\n\n /** Server version (defaults to package version) */\n version?: string;\n\n /** Server description */\n description?: string;\n\n /** MCP generator configuration */\n config?: MCPConfig;\n\n /** MCP context (database, AI client, etc.) */\n context?: MCPContext;\n\n /** Enable debug logging */\n debug?: boolean;\n\n /** Static tool definitions (generated at build time) */\n tools?: Array<{\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n }>;\n /** Internal invocation metadata; never exposed by the MCP tools/list result. */\n customActions?: Record<\n string,\n {\n scope: CustomActionScope;\n isStatic: boolean;\n /** Declared method name; tool IDs are lowercased protocol aliases. */\n methodName?: string;\n parameterNames?: string[];\n optionsParameter?: boolean;\n legacyOptions: boolean;\n }\n >;\n\n /**\n * Lowercased simple names of objects that are `@TenantScoped` (#1554). When\n * non-empty, the generated server imports the tenancy fail-closed gate and\n * wraps tenant-scoped tool calls so a stdio invocation cannot read across all\n * tenants. The tenant is sourced from `SMRT_MCP_TENANT_ID` /\n * `SMRT_MCP_ALLOW_CROSS_TENANT` env vars (this server has no auth principal).\n */\n tenantScopedObjects?: string[];\n}\n\n/**\n * Generate runtime bootstrap code for MCP server\n *\n * @param options - Runtime configuration options\n * @returns TypeScript code for server entry point\n */\nexport function generateRuntimeBootstrap(options: RuntimeOptions = {}): string {\n const {\n name = 'smrt-mcp-server',\n version = '1.0.0',\n description = 'Auto-generated MCP server from SMRT objects',\n debug = false,\n tools = [],\n customActions = {},\n tenantScopedObjects = [],\n } = options;\n\n // Generate static tool array as TypeScript code\n const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : '[]';\n\n // Fail-closed tenant context (#1554): only wire the tenancy gate when at\n // least one exposed object is tenant-scoped, so apps without tenancy never\n // get a dangling import.\n const tenantScopedSet = Array.from(\n new Set(tenantScopedObjects.map((n) => n.toLowerCase())),\n );\n const hasTenantScoped = tenantScopedSet.length > 0;\n\n // Generate static switch cases using shared helper\n const generateSwitchCases = (indent: string) => {\n return tools\n .map((tool) => {\n const separator = tool.name.indexOf('_');\n const objectName = tool.name.slice(0, separator);\n const action = tool.name.slice(separator + 1);\n\n switch (action) {\n case 'list':\n return `${indent}case '${tool.name}': {\n${indent} const limit = args.limit ?? 50;\n${indent} const offset = args.offset ?? 0;\n${indent} const where = args.where ?? {};\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const items = await collection.list({ where, limit, offset });\n${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent} return { content: [{ type: 'text', text: JSON.stringify(itemsPublic) }] };\n${indent}}`;\n\n case 'get':\n return `${indent}case '${tool.name}': {\n${indent} if (!args.id && !args.slug) {\n${indent} throw new Error('Either id or slug is required');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const filter = args.id || args.slug;\n${indent} const item = await collection.get(filter);\n\n${indent} if (!item) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };\n${indent}}`;\n\n case 'create':\n return `${indent}case '${tool.name}': {\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const newItem = await collection.create(applyWritablePolicy('${capitalize(objectName)}', args));\n${indent} await newItem.save();\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };\n${indent}}`;\n\n case 'update':\n return `${indent}case '${tool.name}': {\n${indent} const { id, ...updateData } = args;\n${indent} if (!id) {\n${indent} throw new Error('ID is required for update');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const existing = await collection.get(id);\n${indent} if (!existing) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} Object.assign(existing, applyWritablePolicy('${capitalize(objectName)}', updateData));\n${indent} await existing.save();\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(existing.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };\n${indent}}`;\n\n case 'delete':\n return `${indent}case '${tool.name}': {\n${indent} if (!args.id) {\n${indent} throw new Error('ID is required for delete');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const toDelete = await collection.get(args.id);\n${indent} if (!toDelete) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} await toDelete.delete();\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Object deleted successfully' }) }] };\n${indent}}`;\n\n default:\n // Custom action. Its descriptor is deliberately kept separate\n // from TOOLS so MCP clients receive only protocol-defined fields.\n return `${indent}case '${tool.name}': {\n${indent} const actionMeta = CUSTOM_ACTIONS['${tool.name}'] || { scope: 'item', isStatic: false, legacyOptions: true };\n${indent} const { id, options, ...directArgs } = args;\n\n${indent} if (actionMeta.scope === 'item' && !id) {\n${indent} throw new Error('ID is required for custom action ${action}');\n${indent} }\n${indent} if (actionMeta.scope === 'collection' && id) {\n${indent} throw new Error('Custom action ${action} is collection-scoped and does not accept an ID');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const target = actionMeta.scope === 'item'\n${indent} ? await collection.get(id)\n${indent} : actionMeta.isStatic\n${indent} ? ObjectRegistry.getClass('${capitalize(objectName)}')?.constructor\n${indent} : collection;\n${indent} if (!target) {\n${indent} throw new Error(actionMeta.scope === 'item' ? 'Object not found' : 'Custom action target not found');\n${indent} }\n${indent} const actionMethod = target[actionMeta.methodName || '${action}'];\n${indent} if (typeof actionMethod !== 'function') {\n${indent} throw new Error('Method ${action} not found on custom action target');\n${indent} }\n\n${indent} const methodArgs = actionMeta.legacyOptions\n${indent} ? [Object.keys(options ?? {}).length > 0 ? options : directArgs]\n${indent} : actionMeta.optionsParameter\n${indent} ? [options]\n${indent} : (actionMeta.parameterNames || []).map((parameterName) => args[\n${indent} parameterName === 'id'\n${indent} ? 'actionId'\n${indent} : parameterName\n${indent} ]);\n${indent} const result = await actionMethod.call(target, ...methodArgs);\n${indent} const failure = normalizeCustomActionFailure(result);\n${indent} if (failure) {\n${indent} return {\n${indent} content: [{ type: 'text', text: JSON.stringify({ error: failure }) }],\n${indent} isError: true,\n${indent} _meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },\n${indent} };\n${indent} }\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(toPublicResult(result)) }] };\n${indent}}`;\n }\n })\n .join('\\n\\n');\n };\n\n const switchCases = generateSwitchCases(' ');\n\n return `#!/usr/bin/env node\n/**\n * Auto-generated MCP Server\n * Generated by @smrt/core MCPGenerator\n *\n * This server exposes SMRT objects as MCP tools for AI integration.\n *\n * SECURITY (#1540): tool responses exclude @field({ sensitive }) fields and\n * create/update bodies are mass-assignment guarded. This stdio server has NO\n * per-call authentication principal — its trust boundary is the host process /\n * MCP client that launches it. Run it only in a trusted context, or front it\n * with an authenticated gateway. Do not expose it directly to untrusted callers.\n */\n\nimport {\n type CallToolRequest,\n type ListToolsRequest,\n Server,\n} from '@modelcontextprotocol/server';\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\nimport { pathToFileURL } from 'node:url';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';\nimport { loadConfig } from '@happyvertical/smrt-config';\n${hasTenantScoped ? \"import { enableTenancy, runTenantScopedEntryPoint } from '@happyvertical/smrt-tenancy';\\n\" : ''}\n// Server configuration\nconst SERVER_NAME = ${JSON.stringify(name)};\nconst SERVER_VERSION = ${JSON.stringify(version)};\nconst SERVER_DESCRIPTION = ${JSON.stringify(description)};\nconst DEBUG = ${debug};\n\n// Static tool definitions (generated at build time)\nconst TOOLS = ${toolsCode};\nconst CUSTOM_ACTIONS = ${JSON.stringify(customActions)};\n${\n hasTenantScoped\n ? `\n// Fail-closed tenant context (#1554): tenant-scoped objects must run inside a\n// tenant. This stdio server has no auth principal, so the tenant is taken from\n// the environment; without it (and with tenancy enabled) tenant-scoped tools\n// throw rather than reading across all tenants.\nconst TENANT_SCOPED = new Set(${JSON.stringify(tenantScopedSet)});\nconst MCP_TENANT_ID = process.env.SMRT_MCP_TENANT_ID || undefined;\nconst MCP_ALLOW_CROSS_TENANT = process.env.SMRT_MCP_ALLOW_CROSS_TENANT === 'true';\n`\n : ''\n}\nconst PUBLIC_JSON_OPTIONS = {\n permissions: (process.env.SMRT_MCP_PERMISSIONS || '')\n .split(',')\n .map((permission) => permission.trim())\n .filter(Boolean),\n};\n\n/**\n * Mass-assignment guard (#1540): strip framework/server-managed and\n * \\`@field({ readonly: true })\\` fields from create/update bodies, intersecting\n * with the optional \\`@smrt({ api: { writable: [...] } })\\` allowlist.\n */\nfunction applyWritablePolicy(objectName: string, data: any): Record<string, any> {\n if (!data || typeof data !== 'object') return {};\n const serverManaged = new Set([\n 'id', 'tenantId', 'tenant_id',\n 'createdAt', 'created_at', 'updatedAt', 'updated_at',\n ]);\n const readonly = new Set<string>();\n let writable: string[] | null = null;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api as any;\n if (apiConfig && typeof apiConfig === 'object' && Array.isArray(apiConfig.writable)) {\n writable = apiConfig.writable;\n }\n for (const [name, def] of ObjectRegistry.getFields(objectName)) {\n if (def && ((def as any).readonly === true || (def as any)._meta?.readonly === true)) {\n readonly.add(name);\n }\n }\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith('_')) continue;\n if (serverManaged.has(key)) continue;\n if (readonly.has(key)) continue;\n if (writable && !writable.includes(key)) continue;\n result[key] = value;\n }\n return result;\n}\n\n/**\n * Sensitive-field-safe serialization for custom-action results (#1540).\n * Recurses through arrays and plain objects so nested SmrtObjects are stripped\n * too; non-plain instances (Date, etc.) and primitives pass through. Cycle-safe.\n */\nfunction toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.toPublicJSON === 'function') return value.toPublicJSON(PUBLIC_JSON_OPTIONS);\n if (Array.isArray(value)) {\n if (seen.has(value)) return value;\n seen.add(value);\n return value.map((entry: any) => toPublicResult(entry, seen));\n }\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) return value;\n if (seen.has(value)) return value;\n seen.add(value);\n const out: Record<string, any> = {};\n for (const [key, entry] of Object.entries(value)) {\n out[key] = toPublicResult(entry, seen);\n }\n return out;\n}\n\n/**\n * Main server startup function\n */\nexport async function createServer(): Promise<Server> {\n if (DEBUG) {\n console.error(\\`[MCP] Starting server: \\${SERVER_NAME} v\\${SERVER_VERSION}\\`);\n }\n${\n hasTenantScoped\n ? `\n // Fail-closed tenant context (#1554): install the tenancy interceptor so\n // tenant-scoped tools are actually filtered, and so the entry-point gate\n // throws (rather than passing through) when no tenant is supplied. Without\n // this, a tenant set via SMRT_MCP_TENANT_ID would only set async context\n // with no interceptor to enforce it.\n enableTenancy();\n`\n : ''\n}\n // Load configuration from environment and .smrt.config files\n const appConfig = await loadConfig();\n const aiConfig = appConfig?.ai || {};\n\n if (DEBUG) {\n console.error(\\`[MCP] Loaded \\${TOOLS.length} static tools\\`);\n console.error(\\`[MCP] Available tools:\\`, TOOLS.map(t => t.name).join(', '));\n }\n\n // Create MCP server\n const server = new Server(\n {\n name: SERVER_NAME,\n version: SERVER_VERSION,\n },\n {\n capabilities: {\n tools: {},\n },\n }\n );\n\n // Register ListTools handler\n server.setRequestHandler('tools/list', async (_request: ListToolsRequest) => {\n if (DEBUG) {\n console.error(\\`[MCP] ListTools request received\\`);\n }\n\n return {\n tools: TOOLS,\n };\n });\n\n // Register CallTool handler\n server.setRequestHandler('tools/call', async (request: CallToolRequest) => {\n const { name: toolName, arguments: args = {} } = request.params;\n\n if (DEBUG) {\n console.error(\\`[MCP] CallTool request: \\${toolName}\\`);\n console.error(\\`[MCP] Arguments:\\`, JSON.stringify(args, null, 2));\n }\n\n try {\n // Static switch statement for tool execution\n const runToolBody = async () => {\n switch (toolName) {\n${switchCases}\n\n default:\n throw new Error(\\`Unknown tool: \\${toolName}\\`);\n }\n };\n${\n hasTenantScoped\n ? `\n // Fail-closed tenant context for tenant-scoped tools (#1554).\n const [toolObject] = toolName.split('_');\n const result =\n toolObject && TENANT_SCOPED.has(toolObject.toLowerCase())\n ? await runTenantScopedEntryPoint(\n { tenantScoped: true, tenantId: MCP_TENANT_ID, allowCrossTenant: MCP_ALLOW_CROSS_TENANT, surface: 'MCP' },\n runToolBody,\n )\n : await runToolBody();`\n : `\n const result = await runToolBody();`\n}\n\n if (DEBUG) {\n console.error(\\`[MCP] Tool executed successfully: \\${toolName}\\`);\n }\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n console.error(\\`[MCP] Tool execution failed: \\${toolName}\\`, error);\n\n return {\n content: [\n {\n type: 'text',\n text: \\`Error executing tool \\${toolName}: \\${errorMessage}\\`,\n },\n ],\n isError: true,\n };\n }\n });\n\n return server;\n}\n\nasync function main() {\n try {\n const handle = serveStdio(() => createServer(), {\n onerror: (error) => console.error('[MCP] Protocol error:', error),\n });\n const shutdown = async () => {\n if (DEBUG) console.error('[MCP] Shutting down gracefully');\n await handle.close();\n process.exit(0);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n } catch (error) {\n console.error('[MCP] Fatal error during server startup:', error);\n process.exit(1);\n }\n}\n\n// Start only when executed, so adapters and tests may import the factory.\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().catch((error) => {\n console.error('[MCP] Unhandled error:', error);\n process.exit(1);\n });\n}\n`;\n}\n\n/**\n * Generate package.json script for running MCP server\n *\n * @param serverPath - Path to generated server file (relative to package root)\n * @returns Script command for package.json\n */\nexport function generateMCPScript(\n serverPath: string = 'dist/mcp-server.js',\n): string {\n return `node ${serverPath}`;\n}\n\n/**\n * Generate Claude Desktop configuration example\n *\n * @param serverName - Name for the MCP server\n * @param serverPath - Absolute path to server file\n * @returns Configuration object for claude_desktop_config.json\n */\nexport function generateClaudeConfig(\n serverName: string,\n serverPath: string,\n): object {\n return {\n mcpServers: {\n [serverName]: {\n command: 'node',\n args: [serverPath],\n },\n },\n };\n}\n\n/**\n * Generate README documentation for MCP server setup\n *\n * @param serverName - Name of the MCP server\n * @param serverPath - Path to the server file\n * @returns Markdown documentation\n */\nexport function generateMCPDocumentation(\n serverName: string,\n serverPath: string,\n): string {\n return `# MCP Server Setup\n\nThis project includes an auto-generated MCP (Model Context Protocol) server that exposes SMRT objects as tools for AI integration.\n\n## Quick Start\n\n### 1. Build the MCP Server\n\n\\`\\`\\`bash\nnpm run build\n\\`\\`\\`\n\nThis generates the MCP server at: \\`${serverPath}\\`\n\n### 2. Configure Claude Desktop\n\nAdd the following to your Claude Desktop configuration file:\n\n**macOS**: \\`~/.config/Claude/claude_desktop_config.json\\`\n**Windows**: \\`%APPDATA%\\\\Claude\\\\claude_desktop_config.json\\`\n\n\\`\\`\\`json\n{\n \"mcpServers\": {\n \"${serverName}\": {\n \"command\": \"node\",\n \"args\": [\"/absolute/path/to/${serverPath}\"]\n }\n }\n}\n\\`\\`\\`\n\nReplace \\`/absolute/path/to/\\` with the actual absolute path to your project directory.\n\n### 3. Restart Claude Desktop\n\nClose and reopen Claude Desktop to load the new MCP server.\n\n### 4. Test the Integration\n\nIn Claude Code, you can now use the auto-generated tools. For example:\n\n- \\`list_products\\` - List all products\n- \\`get_product\\` - Get a specific product by ID\n- \\`create_product\\` - Create a new product\n- And more...\n\n## Environment Variables\n\nThe MCP server supports optional environment variables:\n\n- \\`DATABASE_URL\\` - Database connection string\n\n**AI Provider Configuration (in priority order):**\n1. **Generic configuration** (supports any provider):\n - \\`SMRT_AI_PROVIDER\\` - Provider name (e.g., 'openai', 'anthropic', 'claude-cli', 'gemini')\n - \\`SMRT_AI_API_KEY\\` - API key for the provider\n - \\`SMRT_AI_MODEL\\` - Model to use (optional)\n\n2. **Provider-specific fallbacks**:\n - \\`OPENAI_API_KEY\\` - OpenAI API key (auto-detects provider as 'openai')\n - \\`ANTHROPIC_API_KEY\\` - Anthropic API key (auto-detects provider as 'anthropic')\n - \\`CLAUDE_API_KEY\\` + \\`CLAUDE_MODEL\\` - Claude CLI provider (defaults to 'sonnet')\n\n**Examples:**\n\\`\\`\\`bash\n# Using generic configuration (recommended)\nexport SMRT_AI_PROVIDER=claude-cli\nexport SMRT_AI_MODEL=sonnet\n\n# Using provider-specific configuration\nexport CLAUDE_API_KEY=your-key\nexport CLAUDE_MODEL=sonnet\n\n# Using OpenAI\nexport OPENAI_API_KEY=your-openai-key\n\\`\\`\\`\n\n## Troubleshooting\n\n### Server Not Appearing in Claude\n\n1. Check that the path in \\`claude_desktop_config.json\\` is absolute\n2. Verify the server file exists at the specified path\n3. Check Claude Desktop logs for errors\n\n### Tools Not Working\n\n1. Ensure your database is accessible (if using one)\n2. Check that SMRT objects are properly decorated with \\`@smrt()\\`\n3. Look for errors in the MCP server output\n\n### Debug Mode\n\nTo enable debug logging, set the \\`DEBUG\\` constant to \\`true\\` in the generated server file.\n\n## Generated Tools\n\nThe following tools are automatically generated from your SMRT objects:\n\n- **CRUD Operations**: \\`list_\\`, \\`get_\\`, \\`create_\\`, \\`update_\\`, \\`delete_\\` for each object type\n- **Custom Actions**: Any custom methods included in the \\`@smrt()\\` decorator configuration\n\nSee the SMRT object definitions for the complete list of available tools and their parameters.\n`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-runtime-template.js","names":[],"sources":["../../src/generators/mcp-runtime-template.ts"],"sourcesContent":["/**\n * Runtime bootstrap template for generated MCP servers\n *\n * This template provides stdio transport integration for SMRT-generated MCP servers.\n * It handles:\n * - Server initialization with @modelcontextprotocol/server v2\n * - Tool registration from MCPGenerator\n * - Stdio transport connection\n * - Error handling and logging\n * - Graceful shutdown\n */\n\nimport type { CustomActionScope } from './custom-action.js';\nimport type { MCPConfig, MCPContext } from './mcp.js';\n\n/**\n * Helper function to capitalize first letter\n */\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\nexport interface RuntimeOptions {\n /** Server name (defaults to package name) */\n name?: string;\n\n /** Server version (defaults to package version) */\n version?: string;\n\n /** Server description */\n description?: string;\n\n /** MCP generator configuration */\n config?: MCPConfig;\n\n /** MCP context (database, AI client, etc.) */\n context?: MCPContext;\n\n /** Enable debug logging */\n debug?: boolean;\n\n /** Static tool definitions (generated at build time) */\n tools?: Array<{\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n outputSchema?: Record<string, unknown>;\n }>;\n /** Internal invocation metadata; never exposed by the MCP tools/list result. */\n customActions?: Record<\n string,\n {\n scope: CustomActionScope;\n isStatic: boolean;\n /** Declared method name; tool IDs are lowercased protocol aliases. */\n methodName?: string;\n parameterNames?: string[];\n optionsParameter?: boolean;\n legacyOptions: boolean;\n }\n >;\n\n /**\n * Lowercased simple names of objects that are `@TenantScoped` (#1554). When\n * non-empty, the generated server imports the tenancy fail-closed gate and\n * wraps tenant-scoped tool calls so a stdio invocation cannot read across all\n * tenants. The tenant is sourced from `SMRT_MCP_TENANT_ID` /\n * `SMRT_MCP_ALLOW_CROSS_TENANT` env vars (this server has no auth principal).\n */\n tenantScopedObjects?: string[];\n\n /**\n * Build-time approved STI discriminators, keyed by the lowercased MCP object\n * prefix. The generated runtime uses this instead of searching an initially\n * empty registry, then loads the approved qualified type through the public\n * collection API.\n */\n stiTargets?: Record<string, Record<string, string>>;\n}\n\n/**\n * Generate runtime bootstrap code for MCP server\n *\n * @param options - Runtime configuration options\n * @returns TypeScript code for server entry point\n */\nexport function generateRuntimeBootstrap(options: RuntimeOptions = {}): string {\n const {\n name = 'smrt-mcp-server',\n version = '1.0.0',\n description = 'Auto-generated MCP server from SMRT objects',\n debug = false,\n tools = [],\n customActions = {},\n tenantScopedObjects = [],\n stiTargets = {},\n } = options;\n\n // Generate static tool array as TypeScript code\n const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : '[]';\n\n // Fail-closed tenant context (#1554): only wire the tenancy gate when at\n // least one exposed object is tenant-scoped, so apps without tenancy never\n // get a dangling import.\n const tenantScopedSet = Array.from(\n new Set(tenantScopedObjects.map((n) => n.toLowerCase())),\n );\n const hasTenantScoped = tenantScopedSet.length > 0;\n\n // Generate static switch cases using shared helper\n const generateSwitchCases = (indent: string) => {\n return tools\n .map((tool) => {\n const separator = tool.name.indexOf('_');\n const objectName = tool.name.slice(0, separator);\n const action = tool.name.slice(separator + 1);\n\n switch (action) {\n case 'list':\n return `${indent}case '${tool.name}': {\n${indent} const limit = args.limit ?? 50;\n${indent} const offset = args.offset ?? 0;\n${indent} const where = args.where ?? {};\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const items = await collection.list({ where, limit, offset });\n${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent} const structuredContent = {\n${indent} data: itemsPublic,\n${indent} meta: { total: await collection.count({ where }), limit, offset, count: items.length },\n${indent} };\n${indent} return successResult(structuredContent, JSON.stringify(itemsPublic));\n${indent}}`;\n\n case 'get':\n return `${indent}case '${tool.name}': {\n${indent} if (!args.id && !args.slug) {\n${indent} throw new Error('Either id or slug is required');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const filter = args.id || args.slug;\n${indent} const item = await collection.get(filter);\n\n${indent} if (!item) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} return successResult(item.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'create':\n return `${indent}case '${tool.name}': {\n${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget('${objectName}', args, aiConfig);\n\n${indent} const newItem = await collection.create(applyWritablePolicy(targetObjectName, args));\n${indent} await newItem.save();\n\n${indent} return successResult(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'update':\n return `${indent}case '${tool.name}': {\n${indent} const { id, ...updateData } = args;\n${indent} if (!id) {\n${indent} throw new Error('ID is required for update');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const existing = await collection.get(id);\n${indent} if (!existing) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} Object.assign(existing, applyWritablePolicy('${capitalize(objectName)}', updateData));\n${indent} await existing.save();\n\n${indent} return successResult(existing.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'delete':\n return `${indent}case '${tool.name}': {\n${indent} if (!args.id) {\n${indent} throw new Error('ID is required for delete');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const toDelete = await collection.get(args.id);\n${indent} if (!toDelete) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} await toDelete.delete();\n\n${indent} return successResult({ success: true, message: 'Object deleted successfully' });\n${indent}}`;\n\n default:\n // Custom action. Its descriptor is deliberately kept separate\n // from TOOLS so MCP clients receive only protocol-defined fields.\n return `${indent}case '${tool.name}': {\n${indent} const actionMeta = CUSTOM_ACTIONS['${tool.name}'] || { scope: 'item', isStatic: false, legacyOptions: true };\n${indent} const { id, options, ...directArgs } = args;\n\n${indent} if (actionMeta.scope === 'item' && !id) {\n${indent} throw new Error('ID is required for custom action ${action}');\n${indent} }\n${indent} if (actionMeta.scope === 'collection' && id) {\n${indent} throw new Error('Custom action ${action} is collection-scoped and does not accept an ID');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const target = actionMeta.scope === 'item'\n${indent} ? await collection.get(id)\n${indent} : actionMeta.isStatic\n${indent} ? ObjectRegistry.getClass('${capitalize(objectName)}')?.constructor\n${indent} : collection;\n${indent} if (!target) {\n${indent} throw new Error(actionMeta.scope === 'item' ? 'Object not found' : 'Custom action target not found');\n${indent} }\n${indent} const actionMethod = target[actionMeta.methodName || '${action}'];\n${indent} if (typeof actionMethod !== 'function') {\n${indent} throw new Error('Method ${action} not found on custom action target');\n${indent} }\n\n${indent} const methodArgs = actionMeta.legacyOptions\n${indent} ? [Object.keys(options ?? {}).length > 0 ? options : directArgs]\n${indent} : actionMeta.optionsParameter\n${indent} ? [options]\n${indent} : (actionMeta.parameterNames || []).map((parameterName) => args[\n${indent} parameterName === 'id'\n${indent} ? 'actionId'\n${indent} : parameterName\n${indent} ]);\n${indent} const result = await actionMethod.call(target, ...methodArgs);\n${indent} const failure = normalizeCustomActionFailure(result);\n${indent} if (failure) {\n${indent} return errorResult(\n${indent} { error: failure },\n${indent} JSON.stringify({ error: failure }),\n${indent} { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },\n${indent} );\n${indent} }\n\n${indent} const publicResult = toPublicResult(result);\n${indent} return successResult({ data: publicResult }, JSON.stringify(publicResult));\n${indent}}`;\n }\n })\n .join('\\n\\n');\n };\n\n const switchCases = generateSwitchCases(' ');\n\n return `#!/usr/bin/env node\n/**\n * Auto-generated MCP Server\n * Generated by @smrt/core MCPGenerator\n *\n * This server exposes SMRT objects as MCP tools for AI integration.\n *\n * SECURITY (#1540): tool responses exclude @field({ sensitive }) fields and\n * create/update bodies are mass-assignment guarded. This stdio server has NO\n * per-call authentication principal — its trust boundary is the host process /\n * MCP client that launches it. Run it only in a trusted context, or front it\n * with an authenticated gateway. Do not expose it directly to untrusted callers.\n */\n\nimport {\n type CallToolRequest,\n type ListToolsRequest,\n Server,\n} from '@modelcontextprotocol/server';\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';\nimport { loadConfig } from '@happyvertical/smrt-config';\n${hasTenantScoped ? \"import { enableTenancy, runTenantScopedEntryPoint } from '@happyvertical/smrt-tenancy';\\n\" : ''}\n// Server configuration\nconst SERVER_NAME = ${JSON.stringify(name)};\nconst SERVER_VERSION = ${JSON.stringify(version)};\nconst SERVER_DESCRIPTION = ${JSON.stringify(description)};\nconst DEBUG = ${debug};\n\n// Static tool definitions (generated at build time)\nconst TOOLS = ${toolsCode};\nconst CUSTOM_ACTIONS = ${JSON.stringify(customActions)};\nconst STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};\n${\n hasTenantScoped\n ? `\n// Fail-closed tenant context (#1554): tenant-scoped objects must run inside a\n// tenant. This stdio server has no auth principal, so the tenant is taken from\n// the environment; without it (and with tenancy enabled) tenant-scoped tools\n// throw rather than reading across all tenants.\nconst TENANT_SCOPED = new Set(${JSON.stringify(tenantScopedSet)});\nconst MCP_TENANT_ID = process.env.SMRT_MCP_TENANT_ID || undefined;\nconst MCP_ALLOW_CROSS_TENANT = process.env.SMRT_MCP_ALLOW_CROSS_TENANT === 'true';\n`\n : ''\n}\nconst PUBLIC_JSON_OPTIONS = {\n permissions: (process.env.SMRT_MCP_PERMISSIONS || '')\n .split(',')\n .map((permission) => permission.trim())\n .filter(Boolean),\n};\n\n/**\n * Mass-assignment guard (#1540): strip framework/server-managed and\n * \\`@field({ readonly: true })\\` fields from create/update bodies, intersecting\n * with the optional \\`@smrt({ api: { writable: [...] } })\\` allowlist.\n */\nfunction applyWritablePolicy(objectName: string, data: any): Record<string, any> {\n if (!data || typeof data !== 'object') return {};\n const serverManaged = new Set([\n 'id', 'tenantId', 'tenant_id',\n 'createdAt', 'created_at', 'updatedAt', 'updated_at',\n ]);\n const readonly = new Set<string>();\n let writable: string[] | null = null;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api as any;\n if (apiConfig && typeof apiConfig === 'object' && Array.isArray(apiConfig.writable)) {\n writable = apiConfig.writable;\n }\n for (const [name, def] of ObjectRegistry.getFields(objectName)) {\n if (def && ((def as any).readonly === true || (def as any)._meta?.readonly === true)) {\n readonly.add(name);\n }\n }\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith('_')) continue;\n if (serverManaged.has(key)) continue;\n if (readonly.has(key)) continue;\n if (writable && !writable.includes(key)) continue;\n result[key] = value;\n }\n return result;\n}\n\n/** Resolve an advertised STI discriminator to its registered subtype collection. */\nasync function resolveCreateTarget(baseObjectName: string, args: Record<string, any>, aiConfig: any) {\n let objectName = baseObjectName;\n const discriminator = args._meta_type;\n const targets = STI_TARGETS[baseObjectName];\n if (typeof discriminator === 'string' && targets) {\n const target = targets[discriminator];\n if (!target) throw new Error('Unknown STI discriminator: ' + discriminator);\n objectName = target;\n }\n const collection = await ObjectRegistry.getCollection(objectName, {\n persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n ai: aiConfig,\n });\n return { collection, objectName };\n}\n\n/**\n * Sensitive-field-safe serialization for custom-action results (#1540).\n * Recurses through arrays and plain objects so nested SmrtObjects are stripped\n * too; non-plain instances (Date, etc.) and primitives pass through. Cycle-safe.\n */\nfunction toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.toPublicJSON === 'function') return value.toPublicJSON(PUBLIC_JSON_OPTIONS);\n if (Array.isArray(value)) {\n if (seen.has(value)) return value;\n seen.add(value);\n return value.map((entry: any) => toPublicResult(entry, seen));\n }\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) return value;\n if (seen.has(value)) return value;\n seen.add(value);\n const out: Record<string, any> = {};\n for (const [key, entry] of Object.entries(value)) {\n out[key] = toPublicResult(entry, seen);\n }\n return out;\n}\n\nfunction successResult(structuredContent: any, text = JSON.stringify(structuredContent)) {\n return {\n content: [{ type: 'text', text }],\n structuredContent,\n };\n}\n\nfunction errorResult(structuredContent: any, text: string, _meta?: Record<string, any>) {\n return {\n content: [{ type: 'text', text }],\n isError: true,\n structuredContent,\n ...(_meta ? { _meta } : {}),\n };\n}\n\n/**\n * Main server startup function\n */\nexport async function createServer(): Promise<Server> {\n if (DEBUG) {\n console.error(\\`[MCP] Starting server: \\${SERVER_NAME} v\\${SERVER_VERSION}\\`);\n }\n${\n hasTenantScoped\n ? `\n // Fail-closed tenant context (#1554): install the tenancy interceptor so\n // tenant-scoped tools are actually filtered, and so the entry-point gate\n // throws (rather than passing through) when no tenant is supplied. Without\n // this, a tenant set via SMRT_MCP_TENANT_ID would only set async context\n // with no interceptor to enforce it.\n enableTenancy();\n`\n : ''\n}\n // Register the application package manifest before resolving generated\n // object names. Generated servers are commonly run from the application\n // package itself, which is not a node_modules dependency of its process.\n const localManifestPaths = [\n resolve(process.cwd(), 'dist', 'manifest.json'),\n resolve(process.cwd(), '.smrt', 'manifest.json'),\n ].filter(existsSync);\n if (localManifestPaths.length > 0) {\n ObjectRegistry.loadAllManifests({ manifestPaths: localManifestPaths });\n }\n\n // Load configuration from environment and .smrt.config files\n const appConfig = await loadConfig();\n const aiConfig = appConfig?.ai || {};\n\n if (DEBUG) {\n console.error(\\`[MCP] Loaded \\${TOOLS.length} static tools\\`);\n console.error(\\`[MCP] Available tools:\\`, TOOLS.map(t => t.name).join(', '));\n }\n\n // Create MCP server\n const server = new Server(\n {\n name: SERVER_NAME,\n version: SERVER_VERSION,\n },\n {\n capabilities: {\n tools: {},\n },\n }\n );\n\n // Register ListTools handler\n server.setRequestHandler('tools/list', async (_request: ListToolsRequest) => {\n if (DEBUG) {\n console.error(\\`[MCP] ListTools request received\\`);\n }\n\n return {\n tools: TOOLS,\n };\n });\n\n // Register CallTool handler\n server.setRequestHandler('tools/call', async (request: CallToolRequest) => {\n const { name: toolName, arguments: args = {} } = request.params;\n\n if (DEBUG) {\n console.error(\\`[MCP] CallTool request: \\${toolName}\\`);\n console.error(\\`[MCP] Arguments:\\`, JSON.stringify(args, null, 2));\n }\n\n try {\n // Static switch statement for tool execution\n const runToolBody = async () => {\n switch (toolName) {\n${switchCases}\n\n default:\n throw new Error(\\`Unknown tool: \\${toolName}\\`);\n }\n };\n${\n hasTenantScoped\n ? `\n // Fail-closed tenant context for tenant-scoped tools (#1554).\n const [toolObject] = toolName.split('_');\n const result =\n toolObject && TENANT_SCOPED.has(toolObject.toLowerCase())\n ? await runTenantScopedEntryPoint(\n { tenantScoped: true, tenantId: MCP_TENANT_ID, allowCrossTenant: MCP_ALLOW_CROSS_TENANT, surface: 'MCP' },\n runToolBody,\n )\n : await runToolBody();`\n : `\n const result = await runToolBody();`\n}\n\n if (DEBUG) {\n console.error(\\`[MCP] Tool executed successfully: \\${toolName}\\`);\n }\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n console.error(\\`[MCP] Tool execution failed: \\${toolName}\\`, error);\n\n return errorResult(\n { error: { message: errorMessage } },\n \\`Error executing tool \\${toolName}: \\${errorMessage}\\`,\n );\n }\n });\n\n return server;\n}\n\nasync function main() {\n try {\n const handle = serveStdio(() => createServer(), {\n onerror: (error) => console.error('[MCP] Protocol error:', error),\n });\n const shutdown = async () => {\n if (DEBUG) console.error('[MCP] Shutting down gracefully');\n await handle.close();\n process.exit(0);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n } catch (error) {\n console.error('[MCP] Fatal error during server startup:', error);\n process.exit(1);\n }\n}\n\n// Start only when executed, so adapters and tests may import the factory.\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().catch((error) => {\n console.error('[MCP] Unhandled error:', error);\n process.exit(1);\n });\n}\n`;\n}\n\n/**\n * Generate package.json script for running MCP server\n *\n * @param serverPath - Path to generated server file (relative to package root)\n * @returns Script command for package.json\n */\nexport function generateMCPScript(\n serverPath: string = 'dist/mcp-server.js',\n): string {\n return `node ${serverPath}`;\n}\n\n/**\n * Generate Claude Desktop configuration example\n *\n * @param serverName - Name for the MCP server\n * @param serverPath - Absolute path to server file\n * @returns Configuration object for claude_desktop_config.json\n */\nexport function generateClaudeConfig(\n serverName: string,\n serverPath: string,\n): object {\n return {\n mcpServers: {\n [serverName]: {\n command: 'node',\n args: [serverPath],\n },\n },\n };\n}\n\n/**\n * Generate README documentation for MCP server setup\n *\n * @param serverName - Name of the MCP server\n * @param serverPath - Path to the server file\n * @returns Markdown documentation\n */\nexport function generateMCPDocumentation(\n serverName: string,\n serverPath: string,\n): string {\n return `# MCP Server Setup\n\nThis project includes an auto-generated MCP (Model Context Protocol) server that exposes SMRT objects as tools for AI integration.\n\n## Quick Start\n\n### 1. Build the MCP Server\n\n\\`\\`\\`bash\nnpm run build\n\\`\\`\\`\n\nThis generates the MCP server at: \\`${serverPath}\\`\n\n### 2. Configure Claude Desktop\n\nAdd the following to your Claude Desktop configuration file:\n\n**macOS**: \\`~/.config/Claude/claude_desktop_config.json\\`\n**Windows**: \\`%APPDATA%\\\\Claude\\\\claude_desktop_config.json\\`\n\n\\`\\`\\`json\n{\n \"mcpServers\": {\n \"${serverName}\": {\n \"command\": \"node\",\n \"args\": [\"/absolute/path/to/${serverPath}\"]\n }\n }\n}\n\\`\\`\\`\n\nReplace \\`/absolute/path/to/\\` with the actual absolute path to your project directory.\n\n### 3. Restart Claude Desktop\n\nClose and reopen Claude Desktop to load the new MCP server.\n\n### 4. Test the Integration\n\nIn Claude Code, you can now use the auto-generated tools. For example:\n\n- \\`list_products\\` - List all products\n- \\`get_product\\` - Get a specific product by ID\n- \\`create_product\\` - Create a new product\n- And more...\n\n## Environment Variables\n\nThe MCP server supports optional environment variables:\n\n- \\`DATABASE_URL\\` - Database connection string\n\n**AI Provider Configuration (in priority order):**\n1. **Generic configuration** (supports any provider):\n - \\`SMRT_AI_PROVIDER\\` - Provider name (e.g., 'openai', 'anthropic', 'claude-cli', 'gemini')\n - \\`SMRT_AI_API_KEY\\` - API key for the provider\n - \\`SMRT_AI_MODEL\\` - Model to use (optional)\n\n2. **Provider-specific fallbacks**:\n - \\`OPENAI_API_KEY\\` - OpenAI API key (auto-detects provider as 'openai')\n - \\`ANTHROPIC_API_KEY\\` - Anthropic API key (auto-detects provider as 'anthropic')\n - \\`CLAUDE_API_KEY\\` + \\`CLAUDE_MODEL\\` - Claude CLI provider (defaults to 'sonnet')\n\n**Examples:**\n\\`\\`\\`bash\n# Using generic configuration (recommended)\nexport SMRT_AI_PROVIDER=claude-cli\nexport SMRT_AI_MODEL=sonnet\n\n# Using provider-specific configuration\nexport CLAUDE_API_KEY=your-key\nexport CLAUDE_MODEL=sonnet\n\n# Using OpenAI\nexport OPENAI_API_KEY=your-openai-key\n\\`\\`\\`\n\n## Troubleshooting\n\n### Server Not Appearing in Claude\n\n1. Check that the path in \\`claude_desktop_config.json\\` is absolute\n2. Verify the server file exists at the specified path\n3. Check Claude Desktop logs for errors\n\n### Tools Not Working\n\n1. Ensure your database is accessible (if using one)\n2. Check that SMRT objects are properly decorated with \\`@smrt()\\`\n3. Look for errors in the MCP server output\n\n### Debug Mode\n\nTo enable debug logging, set the \\`DEBUG\\` constant to \\`true\\` in the generated server file.\n\n## Generated Tools\n\nThe following tools are automatically generated from your SMRT objects:\n\n- **CRUD Operations**: \\`list_\\`, \\`get_\\`, \\`create_\\`, \\`update_\\`, \\`delete_\\` for each object type\n- **Custom Actions**: Any custom methods included in the \\`@smrt()\\` decorator configuration\n\nSee the SMRT object definitions for the complete list of available tools and their parameters.\n`;\n}\n"],"mappings":"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"}
|
package/dist/generators/mcp.d.ts
CHANGED
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* MCP (Model Context Protocol) server generator for smrt objects
|
|
3
|
-
*
|
|
4
|
-
* Exposes smrt objects as AI tools for Claude, GPT, and other AI models
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* A JSON Schema fragment for an MCP tool input property. The shape varies by
|
|
8
|
-
* field kind (string/number/object/…), so values are `unknown`; this is only
|
|
9
|
-
* ever serialized into the generated tool definitions, never read back.
|
|
10
|
-
*/
|
|
11
|
-
type McpJsonSchema = Record<string, unknown>;
|
|
1
|
+
import { ToolJsonSchema } from './tool-schema.js';
|
|
12
2
|
/**
|
|
13
3
|
* Runtime tool-call arguments. They arrive as untyped JSON from the MCP client,
|
|
14
4
|
* so individual keys are narrowed (`as`) at each action's boundary.
|
|
@@ -49,11 +39,9 @@ export interface MCPContext {
|
|
|
49
39
|
export interface MCPTool {
|
|
50
40
|
name: string;
|
|
51
41
|
description: string;
|
|
52
|
-
inputSchema:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
required?: string[];
|
|
56
|
-
};
|
|
42
|
+
inputSchema: ToolJsonSchema;
|
|
43
|
+
/** Public result schema for tools/call structuredContent. */
|
|
44
|
+
outputSchema: ToolJsonSchema;
|
|
57
45
|
}
|
|
58
46
|
export interface MCPRequest {
|
|
59
47
|
method: string;
|
|
@@ -69,6 +57,8 @@ export interface MCPResponse {
|
|
|
69
57
|
}>;
|
|
70
58
|
isError?: boolean;
|
|
71
59
|
_meta?: Record<string, unknown>;
|
|
60
|
+
/** Machine-readable projection matching the tool's declared outputSchema. */
|
|
61
|
+
structuredContent?: unknown;
|
|
72
62
|
}
|
|
73
63
|
/**
|
|
74
64
|
* Generate MCP server from smrt objects
|
|
@@ -101,14 +91,31 @@ export declare class MCPGenerator {
|
|
|
101
91
|
* Validate that a custom method exists on a class
|
|
102
92
|
*/
|
|
103
93
|
private validateCustomMethod;
|
|
94
|
+
/** Normalize registry fields for the transport-neutral schema emitter. */
|
|
95
|
+
private toToolFields;
|
|
104
96
|
/**
|
|
105
|
-
*
|
|
97
|
+
* Add the optional STI discriminator branches to write schemas. The legacy
|
|
98
|
+
* branch preserves existing base-class creates that let SMRT pick the base
|
|
99
|
+
* type; an explicit `_meta_type` selects a known child collection below.
|
|
106
100
|
*/
|
|
107
|
-
private
|
|
101
|
+
private buildInputSchema;
|
|
102
|
+
/**
|
|
103
|
+
* Public output schemas follow the actual `toPublicJSON()` boundary: known
|
|
104
|
+
* non-sensitive fields are described, while `additionalProperties` keeps
|
|
105
|
+
* framework/system fields and application transforms honest.
|
|
106
|
+
*/
|
|
107
|
+
private buildOutputSchema;
|
|
108
|
+
private buildPublicItemSchema;
|
|
109
|
+
private buildFieldSchemaProperties;
|
|
110
|
+
private getStiVariants;
|
|
108
111
|
/**
|
|
109
112
|
* Handle MCP tool calls
|
|
110
113
|
*/
|
|
111
114
|
handleToolCall(request: MCPRequest): Promise<MCPResponse>;
|
|
115
|
+
/** Convert runtime values to the JSON values MCP structuredContent permits. */
|
|
116
|
+
private toJsonValue;
|
|
117
|
+
/** Build the MCP-required object root without changing legacy text payloads. */
|
|
118
|
+
private toStructuredContent;
|
|
112
119
|
/**
|
|
113
120
|
* Get or create collection for an object
|
|
114
121
|
*/
|
|
@@ -215,6 +222,13 @@ export declare class MCPGenerator {
|
|
|
215
222
|
modular?: boolean;
|
|
216
223
|
}): Promise<void>;
|
|
217
224
|
private runtimeCustomActions;
|
|
225
|
+
/**
|
|
226
|
+
* Emit only the STI discriminator targets advertised by create-tool schemas.
|
|
227
|
+
* Generated processes start with an empty registry, so resolving the
|
|
228
|
+
* qualified target through `getCollection()` both validates the declaration
|
|
229
|
+
* and lets the public registry loader register the selected subtype.
|
|
230
|
+
*/
|
|
231
|
+
private runtimeStiTargets;
|
|
218
232
|
/**
|
|
219
233
|
* Generate modular MCP server structure
|
|
220
234
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/generators/mcp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/generators/mcp.ts"],"names":[],"mappings":"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"}
|