@happyvertical/smrt-core 0.40.49 ā 0.40.50
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/agents/generators.md +32 -0
- package/dist/generators/cli.d.ts +11 -3
- package/dist/generators/cli.d.ts.map +1 -1
- package/dist/generators/cli.js +54 -11
- package/dist/generators/cli.js.map +1 -1
- package/dist/generators/custom-action.d.ts +68 -0
- package/dist/generators/custom-action.d.ts.map +1 -0
- package/dist/generators/custom-action.js +128 -0
- package/dist/generators/custom-action.js.map +1 -0
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -1
- package/dist/generators/mcp-runtime-template.d.ts +11 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +40 -12
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +6 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +157 -67
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/tool-schema.d.ts +3 -1
- package/dist/generators/tool-schema.d.ts.map +1 -1
- package/dist/generators/tool-schema.js +8 -17
- package/dist/generators/tool-schema.js.map +1 -1
- package/dist/generators.js +2 -1
- package/dist/index.js +2 -1
- package/dist/manifest/static-manifest.js +2 -2
- 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 +405 -581
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +10 -4
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/smrt-knowledge.json +6 -7
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +6 -7
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +2 -2
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +29 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +7 -1
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/dist/vite-plugin.js +2 -2
- package/package.json +4 -5
- package/dist/__tests__/fixtures/advisor-test-classes.d.ts +0 -37
- package/dist/__tests__/fixtures/advisor-test-classes.d.ts.map +0 -1
- package/dist/mcp-advisor/index.d.ts +0 -499
- package/dist/mcp-advisor/index.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/add-ai-methods.d.ts +0 -6
- package/dist/mcp-advisor/tools/add-ai-methods.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/configure-decorators.d.ts +0 -6
- package/dist/mcp-advisor/tools/configure-decorators.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/generate-collection.d.ts +0 -6
- package/dist/mcp-advisor/tools/generate-collection.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/generate-field-definitions.d.ts +0 -6
- package/dist/mcp-advisor/tools/generate-field-definitions.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/generate-smrt-class.d.ts +0 -6
- package/dist/mcp-advisor/tools/generate-smrt-class.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/get-object-config.d.ts +0 -6
- package/dist/mcp-advisor/tools/get-object-config.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/get-object-schema.d.ts +0 -10
- package/dist/mcp-advisor/tools/get-object-schema.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/list-registered-objects.d.ts +0 -9
- package/dist/mcp-advisor/tools/list-registered-objects.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +0 -10
- package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/preview-mcp-tools.d.ts +0 -9
- package/dist/mcp-advisor/tools/preview-mcp-tools.d.ts.map +0 -1
- package/dist/mcp-advisor/tools/validate-smrt-object.d.ts +0 -6
- package/dist/mcp-advisor/tools/validate-smrt-object.d.ts.map +0 -1
- package/dist/mcp-advisor/types.d.ts +0 -213
- package/dist/mcp-advisor/types.d.ts.map +0 -1
package/dist/generators/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, customActionParameterInputName, normalizeCustomActionFailure, resolveCustomActionMetadata } from "./custom-action.js";
|
|
1
2
|
import { runWithTenantGate, setTenantEntryPointRunner } from "./tenant-gate.js";
|
|
2
3
|
import { CLIGenerator, getCLIHandler, setupCLI } from "./cli.js";
|
|
3
4
|
import { PRIVATE_READ_CACHE_CONTROL, canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized } from "./conditional-get.js";
|
|
@@ -5,4 +6,4 @@ import { DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EV
|
|
|
5
6
|
import { MCPGenerator } from "./mcp.js";
|
|
6
7
|
import { APIGenerator, computeRuntimeWebManifestHash, createRestServer, startRestServer } from "./rest.js";
|
|
7
8
|
import { generateOpenAPISpec, setupSwaggerUI } from "./swagger.js";
|
|
8
|
-
export { APIGenerator, CLIGenerator, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, MCPGenerator, PRIVATE_READ_CACHE_CONTROL, buildChangeEventStream, canonicalReadRepresentation, changeEventSubscribersAtCapacity, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, createRestServer, eventStreamCapacityExceededResponse, generateOpenAPISpec, getCLIHandler, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, normalizeEventsMaxSubscribers, resolveReadCacheControl, resolveTenantEtagDiscriminator, runWithTenantGate, setTenantEntryPointRunner, setupCLI, setupSwaggerUI, signalVisibleToTenant, startRestServer, tryReserveChangeEventSubscriberSlot, versionConditionalResponse, warnIfSharedCacheNeutralized };
|
|
9
|
+
export { APIGenerator, CLIGenerator, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, MCPGenerator, PRIVATE_READ_CACHE_CONTROL, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, canonicalReadRepresentation, changeEventSubscribersAtCapacity, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, createRestServer, customActionParameterInputName, eventStreamCapacityExceededResponse, generateOpenAPISpec, getCLIHandler, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, normalizeCustomActionFailure, normalizeEventsMaxSubscribers, resolveCustomActionMetadata, resolveReadCacheControl, resolveTenantEtagDiscriminator, runWithTenantGate, setTenantEntryPointRunner, setupCLI, setupSwaggerUI, signalVisibleToTenant, startRestServer, tryReserveChangeEventSubscriberSlot, versionConditionalResponse, warnIfSharedCacheNeutralized };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CustomActionScope } from './custom-action.js';
|
|
1
2
|
import { MCPConfig, MCPContext } from './mcp.js';
|
|
2
3
|
export interface RuntimeOptions {
|
|
3
4
|
/** Server name (defaults to package name) */
|
|
@@ -18,6 +19,16 @@ export interface RuntimeOptions {
|
|
|
18
19
|
description: string;
|
|
19
20
|
inputSchema: Record<string, unknown>;
|
|
20
21
|
}>;
|
|
22
|
+
/** Internal invocation metadata; never exposed by the MCP tools/list result. */
|
|
23
|
+
customActions?: Record<string, {
|
|
24
|
+
scope: CustomActionScope;
|
|
25
|
+
isStatic: boolean;
|
|
26
|
+
/** Declared method name; tool IDs are lowercased protocol aliases. */
|
|
27
|
+
methodName?: string;
|
|
28
|
+
parameterNames?: string[];
|
|
29
|
+
optionsParameter?: boolean;
|
|
30
|
+
legacyOptions: boolean;
|
|
31
|
+
}>;
|
|
21
32
|
/**
|
|
22
33
|
* Lowercased simple names of objects that are `@TenantScoped` (#1554). When
|
|
23
34
|
* non-empty, the generated server imports the tenancy fail-closed gate and
|
|
@@ -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,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;KACtC,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;KACtC,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;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CA6b7E;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,13 +12,15 @@ 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 = [], 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 = [] } = 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;
|
|
19
19
|
const generateSwitchCases = (indent) => {
|
|
20
20
|
return tools.map((tool) => {
|
|
21
|
-
const
|
|
21
|
+
const separator = tool.name.indexOf("_");
|
|
22
|
+
const objectName = tool.name.slice(0, separator);
|
|
23
|
+
const action = tool.name.slice(separator + 1);
|
|
22
24
|
switch (action) {
|
|
23
25
|
case "list": return `${indent}case '${tool.name}': {
|
|
24
26
|
${indent} const limit = args.limit ?? 50;
|
|
@@ -105,28 +107,52 @@ ${indent} await toDelete.delete();
|
|
|
105
107
|
${indent} return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Object deleted successfully' }) }] };
|
|
106
108
|
${indent}}`;
|
|
107
109
|
default: return `${indent}case '${tool.name}': {
|
|
108
|
-
${indent} const {
|
|
110
|
+
${indent} const actionMeta = CUSTOM_ACTIONS['${tool.name}'] || { scope: 'item', isStatic: false, legacyOptions: true };
|
|
111
|
+
${indent} const { id, options, ...directArgs } = args;
|
|
109
112
|
|
|
110
|
-
${indent} if (!id) {
|
|
113
|
+
${indent} if (actionMeta.scope === 'item' && !id) {
|
|
111
114
|
${indent} throw new Error('ID is required for custom action ${action}');
|
|
112
115
|
${indent} }
|
|
116
|
+
${indent} if (actionMeta.scope === 'collection' && id) {
|
|
117
|
+
${indent} throw new Error('Custom action ${action} is collection-scoped and does not accept an ID');
|
|
118
|
+
${indent} }
|
|
113
119
|
|
|
114
120
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
115
121
|
${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
|
|
116
122
|
${indent} ai: aiConfig
|
|
117
123
|
${indent} });
|
|
118
124
|
|
|
119
|
-
${indent} const
|
|
120
|
-
${indent}
|
|
121
|
-
${indent}
|
|
125
|
+
${indent} const target = actionMeta.scope === 'item'
|
|
126
|
+
${indent} ? await collection.get(id)
|
|
127
|
+
${indent} : actionMeta.isStatic
|
|
128
|
+
${indent} ? ObjectRegistry.getClass('${capitalize(objectName)}')?.constructor
|
|
129
|
+
${indent} : collection;
|
|
130
|
+
${indent} if (!target) {
|
|
131
|
+
${indent} throw new Error(actionMeta.scope === 'item' ? 'Object not found' : 'Custom action target not found');
|
|
122
132
|
${indent} }
|
|
123
|
-
|
|
124
|
-
${indent} if (typeof
|
|
125
|
-
${indent} throw new Error('Method ${action} not found on
|
|
133
|
+
${indent} const actionMethod = target[actionMeta.methodName || '${action}'];
|
|
134
|
+
${indent} if (typeof actionMethod !== 'function') {
|
|
135
|
+
${indent} throw new Error('Method ${action} not found on custom action target');
|
|
126
136
|
${indent} }
|
|
127
137
|
|
|
128
|
-
${indent} const methodArgs =
|
|
129
|
-
${indent}
|
|
138
|
+
${indent} const methodArgs = actionMeta.legacyOptions
|
|
139
|
+
${indent} ? [Object.keys(options ?? {}).length > 0 ? options : directArgs]
|
|
140
|
+
${indent} : actionMeta.optionsParameter
|
|
141
|
+
${indent} ? [options]
|
|
142
|
+
${indent} : (actionMeta.parameterNames || []).map((parameterName) => args[
|
|
143
|
+
${indent} parameterName === 'id'
|
|
144
|
+
${indent} ? 'actionId'
|
|
145
|
+
${indent} : parameterName
|
|
146
|
+
${indent} ]);
|
|
147
|
+
${indent} const result = await actionMethod.call(target, ...methodArgs);
|
|
148
|
+
${indent} const failure = normalizeCustomActionFailure(result);
|
|
149
|
+
${indent} if (failure) {
|
|
150
|
+
${indent} return {
|
|
151
|
+
${indent} content: [{ type: 'text', text: JSON.stringify({ error: failure }) }],
|
|
152
|
+
${indent} isError: true,
|
|
153
|
+
${indent} _meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },
|
|
154
|
+
${indent} };
|
|
155
|
+
${indent} }
|
|
130
156
|
|
|
131
157
|
${indent} return { content: [{ type: 'text', text: JSON.stringify(toPublicResult(result)) }] };
|
|
132
158
|
${indent}}`;
|
|
@@ -157,6 +183,7 @@ import {
|
|
|
157
183
|
type ListToolsRequest,
|
|
158
184
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
159
185
|
import { ObjectRegistry } from '@happyvertical/smrt-core';
|
|
186
|
+
import { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';
|
|
160
187
|
import { config } from '@happyvertical/smrt-config';
|
|
161
188
|
${hasTenantScoped ? "import { enableTenancy, runTenantScopedEntryPoint } from '@happyvertical/smrt-tenancy';\n" : ""}
|
|
162
189
|
// Server configuration
|
|
@@ -167,6 +194,7 @@ const DEBUG = ${debug};
|
|
|
167
194
|
|
|
168
195
|
// Static tool definitions (generated at build time)
|
|
169
196
|
const TOOLS = ${toolsCode};
|
|
197
|
+
const CUSTOM_ACTIONS = ${JSON.stringify(customActions)};
|
|
170
198
|
${hasTenantScoped ? `
|
|
171
199
|
// Fail-closed tenant context (#1554): tenant-scoped objects must run inside a
|
|
172
200
|
// tenant. This stdio server has no auth principal, so the tenant is taken from
|
|
@@ -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/sdk\n * - Tool registration from MCPGenerator\n * - Stdio transport connection\n * - Error handling and logging\n * - Graceful shutdown\n */\n\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\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 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 [objectName, action] = tool.name.split('_');\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\n return `${indent}case '${tool.name}': {\n${indent} const { id, options = {}, ...directArgs } = args;\n\n${indent} if (!id) {\n${indent} throw new Error('ID is required for custom action ${action}');\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 object = await collection.get(id);\n${indent} if (!object) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} if (typeof object['${action}'] !== 'function') {\n${indent} throw new Error('Method ${action} not found on object');\n${indent} }\n\n${indent} const methodArgs = Object.keys(options).length > 0 ? options : directArgs;\n${indent} const result = await object['${action}'](methodArgs);\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 { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n type CallToolRequest,\n type ListToolsRequest,\n} from '@modelcontextprotocol/sdk/types.js';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { config } 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};\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 */\nasync function main() {\n try {\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 config.load();\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(ListToolsRequestSchema, 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(CallToolRequestSchema, 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 // Setup stdio transport\n const transport = new StdioServerTransport();\n\n // Connect server to transport\n await server.connect(transport);\n\n if (DEBUG) {\n console.error(\\`[MCP] Server connected via stdio transport\\`);\n console.error(\\`[MCP] Ready to receive requests\\`);\n }\n\n // Handle graceful shutdown\n process.on('SIGINT', async () => {\n if (DEBUG) {\n console.error(\\`[MCP] Received SIGINT, shutting down gracefully\\`);\n }\n await server.close();\n process.exit(0);\n });\n\n process.on('SIGTERM', async () => {\n if (DEBUG) {\n console.error(\\`[MCP] Received SIGTERM, shutting down gracefully\\`);\n }\n await server.close();\n process.exit(0);\n });\n } catch (error) {\n console.error('[MCP] Fatal error during server startup:', error);\n process.exit(1);\n }\n}\n\n// Start the server\nmain().catch((error) => {\n console.error('[MCP] Unhandled error:', error);\n process.exit(1);\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":";;;;AAiBA,SAAS,WAAW,KAAqB;CACvC,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,IAAI,MAAM,CAAC;AAClD;;;;;;;AA4CA,SAAgB,yBAAyB,UAA0B,CAAC,GAAW;CAC7E,MAAM,EACJ,OAAO,mBACP,UAAU,SACV,cAAc,+CACd,QAAQ,OACR,QAAQ,CAAC,GACT,sBAAsB,CAAC,MACrB;CAGJ,MAAM,YAAY,MAAM,SAAS,IAAI,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI;CAKtE,MAAM,kBAAkB,MAAM,KAC5B,IAAI,IAAI,oBAAoB,KAAK,MAAM,EAAE,YAAY,CAAC,CAAC,CACzD;CACA,MAAM,kBAAkB,gBAAgB,SAAS;CAGjD,MAAM,uBAAuB,WAAmB;EAC9C,OAAO,MACJ,KAAK,SAAS;GACb,MAAM,CAAC,YAAY,UAAU,KAAK,KAAK,MAAM,GAAG;GAEhD,QAAQ,QAAR;IACE,KAAK,QACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,2DAA2D,WAAW,UAAU,EAAE;EACzF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;IAEC,KAAK,OACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,2DAA2D,WAAW,UAAU,EAAE;EACzF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;IAEC,KAAK,UACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO,2DAA2D,WAAW,UAAU,EAAE;EACzF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,iEAAiE,WAAW,UAAU,EAAE;EAC/F,OAAO;;EAEP,OAAO;EACP,OAAO;IAEC,KAAK,UACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,2DAA2D,WAAW,UAAU,EAAE;EACzF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,iDAAiD,WAAW,UAAU,EAAE;EAC/E,OAAO;;EAEP,OAAO;EACP,OAAO;IAEC,KAAK,UACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,2DAA2D,WAAW,UAAU,EAAE;EACzF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;;EAEP,OAAO;EACP,OAAO;IAEC,SAEE,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO;;EAEP,OAAO;EACP,OAAO,wDAAwD,OAAO;EACtE,OAAO;;EAEP,OAAO,2DAA2D,WAAW,UAAU,EAAE;EACzF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,uBAAuB,OAAO;EACrC,OAAO,8BAA8B,OAAO;EAC5C,OAAO;;EAEP,OAAO;EACP,OAAO,iCAAiC,OAAO;;EAE/C,OAAO;EACP,OAAO;GACD;EACF,CAAC,CAAC,CACD,KAAK,MAAM;CAChB;CAEA,MAAM,cAAc,oBAAoB,YAAY;CAEpD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;EAwBP,kBAAkB,8FAA8F,GAAG;;sBAE/F,KAAK,UAAU,IAAI,EAAE;yBAClB,KAAK,UAAU,OAAO,EAAE;6BACpB,KAAK,UAAU,WAAW,EAAE;gBACzC,MAAM;;;gBAGN,UAAU;EAExB,kBACI;;;;;gCAK0B,KAAK,UAAU,eAAe,EAAE;;;IAI1D,GACL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0EC,kBACI;;;;;;;IAQA,GACL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+CC,YAAY;;;;;;EAOZ,kBACI;;;;;;;;;sCAUA;6CAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DD;;;;;;;AAQA,SAAgB,kBACd,aAAqB,sBACb;CACR,OAAO,QAAQ;AACjB;;;;;;;;AASA,SAAgB,qBACd,YACA,YACQ;CACR,OAAO,EACL,YAAY,GACT,aAAa;EACZ,SAAS;EACT,MAAM,CAAC,UAAU;CACnB,EACF,EACF;AACF;;;;;;;;AASA,SAAgB,yBACd,YACA,YACQ;CACR,OAAO;;;;;;;;;;;;sCAY6B,WAAW;;;;;;;;;;;;OAY1C,WAAW;;oCAEkB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+E/C"}
|
|
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/sdk\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 { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n type CallToolRequest,\n type ListToolsRequest,\n} from '@modelcontextprotocol/sdk/types.js';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';\nimport { config } 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 */\nasync function main() {\n try {\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 config.load();\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(ListToolsRequestSchema, 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(CallToolRequestSchema, 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 // Setup stdio transport\n const transport = new StdioServerTransport();\n\n // Connect server to transport\n await server.connect(transport);\n\n if (DEBUG) {\n console.error(\\`[MCP] Server connected via stdio transport\\`);\n console.error(\\`[MCP] Ready to receive requests\\`);\n }\n\n // Handle graceful shutdown\n process.on('SIGINT', async () => {\n if (DEBUG) {\n console.error(\\`[MCP] Received SIGINT, shutting down gracefully\\`);\n }\n await server.close();\n process.exit(0);\n });\n\n process.on('SIGTERM', async () => {\n if (DEBUG) {\n console.error(\\`[MCP] Received SIGTERM, shutting down gracefully\\`);\n }\n await server.close();\n process.exit(0);\n });\n } catch (error) {\n console.error('[MCP] Fatal error during server startup:', error);\n process.exit(1);\n }\n}\n\n// Start the server\nmain().catch((error) => {\n console.error('[MCP] Unhandled error:', error);\n process.exit(1);\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
|
@@ -67,6 +67,8 @@ export interface MCPResponse {
|
|
|
67
67
|
type: 'text';
|
|
68
68
|
text: string;
|
|
69
69
|
}>;
|
|
70
|
+
isError?: boolean;
|
|
71
|
+
_meta?: Record<string, unknown>;
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
72
74
|
* Generate MCP server from smrt objects
|
|
@@ -92,6 +94,9 @@ export declare class MCPGenerator {
|
|
|
92
94
|
* Generate tools for a specific object
|
|
93
95
|
*/
|
|
94
96
|
private generateObjectTools;
|
|
97
|
+
private buildCustomActionTool;
|
|
98
|
+
private resolveCustomActionMetadata;
|
|
99
|
+
private hasCollectionReceiver;
|
|
95
100
|
/**
|
|
96
101
|
* Validate that a custom method exists on a class
|
|
97
102
|
*/
|
|
@@ -209,6 +214,7 @@ export declare class MCPGenerator {
|
|
|
209
214
|
/** Generate modular directory structure (tools/, handlers/, config.ts) */
|
|
210
215
|
modular?: boolean;
|
|
211
216
|
}): Promise<void>;
|
|
217
|
+
private runtimeCustomActions;
|
|
212
218
|
/**
|
|
213
219
|
* Generate modular MCP server structure
|
|
214
220
|
*
|
|
@@ -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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"}
|
package/dist/generators/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, customActionParameterInputName, normalizeCustomActionFailure, resolveCustomActionMetadata } from "./custom-action.js";
|
|
1
2
|
import { ObjectRegistry } from "../registry.js";
|
|
2
3
|
import { SmrtCollection } from "../collection.js";
|
|
3
4
|
import { runWithTenantGate } from "./tenant-gate.js";
|
|
@@ -10,6 +11,26 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
10
11
|
*
|
|
11
12
|
* Exposes smrt objects as AI tools for Claude, GPT, and other AI models
|
|
12
13
|
*/
|
|
14
|
+
var CustomActionFailureError = class extends Error {
|
|
15
|
+
failure;
|
|
16
|
+
constructor(failure) {
|
|
17
|
+
super(failure.message);
|
|
18
|
+
this.failure = failure;
|
|
19
|
+
this.name = "CustomActionFailureError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* MCP tool identifiers are lowercase for stable protocol vocabulary, while
|
|
24
|
+
* JavaScript method names retain their declared casing. Resolve a tool suffix
|
|
25
|
+
* back to the registry's canonical method name before inspecting metadata or
|
|
26
|
+
* invoking it.
|
|
27
|
+
*/
|
|
28
|
+
function resolveCustomActionMethod(methods, toolAction) {
|
|
29
|
+
const direct = methods.get(toolAction);
|
|
30
|
+
if (direct) return [toolAction, direct];
|
|
31
|
+
for (const [methodName, method] of methods) if (methodName.toLowerCase() === toolAction.toLowerCase()) return [methodName, method];
|
|
32
|
+
return [toolAction, void 0];
|
|
33
|
+
}
|
|
13
34
|
/**
|
|
14
35
|
* Generate MCP server from smrt objects
|
|
15
36
|
*/
|
|
@@ -191,54 +212,35 @@ var MCPGenerator = class {
|
|
|
191
212
|
}
|
|
192
213
|
const methodDef = methods.get(methodName);
|
|
193
214
|
if (methodDef && !methodDef.isPublic) continue;
|
|
194
|
-
|
|
195
|
-
tools.push({
|
|
196
|
-
name: toolName,
|
|
197
|
-
description: `Execute ${methodName} action on ${objectName}`,
|
|
198
|
-
inputSchema: {
|
|
199
|
-
type: "object",
|
|
200
|
-
properties: {
|
|
201
|
-
id: {
|
|
202
|
-
type: "string",
|
|
203
|
-
description: "ID of the object to execute action on"
|
|
204
|
-
},
|
|
205
|
-
options: {
|
|
206
|
-
type: "object",
|
|
207
|
-
description: "Additional options for the custom action",
|
|
208
|
-
additionalProperties: true
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
required: ["id"]
|
|
212
|
-
}
|
|
213
|
-
});
|
|
215
|
+
tools.push(this.buildCustomActionTool(objectName, lowerName, methodName, methodDef, this.hasCollectionReceiver(classInfo)));
|
|
214
216
|
}
|
|
215
217
|
else for (const [methodName, methodDef] of methods) {
|
|
216
218
|
if (!methodDef.isPublic) continue;
|
|
217
219
|
if (excluded.includes(methodName)) continue;
|
|
218
|
-
|
|
219
|
-
tools.push({
|
|
220
|
-
name: toolName,
|
|
221
|
-
description: `Execute ${methodName} action on ${objectName}`,
|
|
222
|
-
inputSchema: {
|
|
223
|
-
type: "object",
|
|
224
|
-
properties: {
|
|
225
|
-
id: {
|
|
226
|
-
type: "string",
|
|
227
|
-
description: "ID of the object to execute action on"
|
|
228
|
-
},
|
|
229
|
-
options: {
|
|
230
|
-
type: "object",
|
|
231
|
-
description: "Additional options for the custom action",
|
|
232
|
-
additionalProperties: true
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
required: ["id"]
|
|
236
|
-
}
|
|
237
|
-
});
|
|
220
|
+
tools.push(this.buildCustomActionTool(objectName, lowerName, methodName, methodDef, this.hasCollectionReceiver(classInfo)));
|
|
238
221
|
}
|
|
239
222
|
}
|
|
240
223
|
return tools;
|
|
241
224
|
}
|
|
225
|
+
buildCustomActionTool(objectName, lowerName, methodName, methodDef, collectionReceiver = false) {
|
|
226
|
+
const metadata = this.resolveCustomActionMetadata(objectName, methodName, methodDef, collectionReceiver);
|
|
227
|
+
return {
|
|
228
|
+
name: `${lowerName}_${methodName}`.toLowerCase(),
|
|
229
|
+
description: `Execute ${methodName} action on ${objectName}`,
|
|
230
|
+
inputSchema: buildCustomActionInputSchema(metadata)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
resolveCustomActionMetadata(objectName, action, method, collectionReceiver = false) {
|
|
234
|
+
return resolveCustomActionMetadata({
|
|
235
|
+
actionName: action,
|
|
236
|
+
method,
|
|
237
|
+
apiConfig: ObjectRegistry.getConfig(objectName).api,
|
|
238
|
+
...collectionReceiver ? { defaultScope: "collection" } : {}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
hasCollectionReceiver(classInfo) {
|
|
242
|
+
return !!classInfo && classInfo.constructor.prototype instanceof SmrtCollection;
|
|
243
|
+
}
|
|
242
244
|
/**
|
|
243
245
|
* Validate that a custom method exists on a class
|
|
244
246
|
*/
|
|
@@ -322,6 +324,14 @@ var MCPGenerator = class {
|
|
|
322
324
|
text: JSON.stringify(result, null, 2)
|
|
323
325
|
}] };
|
|
324
326
|
} catch (error) {
|
|
327
|
+
if (error instanceof CustomActionFailureError) return {
|
|
328
|
+
content: [{
|
|
329
|
+
type: "text",
|
|
330
|
+
text: JSON.stringify({ error: error.failure })
|
|
331
|
+
}],
|
|
332
|
+
isError: true,
|
|
333
|
+
_meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: error.failure }
|
|
334
|
+
};
|
|
325
335
|
return { content: [{
|
|
326
336
|
type: "text",
|
|
327
337
|
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
@@ -535,7 +545,7 @@ var MCPGenerator = class {
|
|
|
535
545
|
};
|
|
536
546
|
}
|
|
537
547
|
default: {
|
|
538
|
-
const result = await this.executeCustomAction(collection, action, args);
|
|
548
|
+
const result = await this.executeCustomAction(collection, action, args, objectName);
|
|
539
549
|
return this.toPublicData(result);
|
|
540
550
|
}
|
|
541
551
|
}
|
|
@@ -543,26 +553,43 @@ var MCPGenerator = class {
|
|
|
543
553
|
/**
|
|
544
554
|
* Execute a custom action on a collection/object
|
|
545
555
|
*/
|
|
546
|
-
async executeCustomAction(collection, action, args) {
|
|
547
|
-
const
|
|
548
|
-
const
|
|
556
|
+
async executeCustomAction(collection, action, args, objectName) {
|
|
557
|
+
const id = args.id;
|
|
558
|
+
const [methodName, methodDef] = objectName ? resolveCustomActionMethod(await ObjectRegistry.getAllMethods(objectName), action) : [action, void 0];
|
|
559
|
+
const metadata = this.resolveCustomActionMetadata(objectName ?? "", methodName, methodDef, objectName ? this.hasCollectionReceiver(ObjectRegistry.getClass(objectName)) : false);
|
|
560
|
+
const methodArgs = buildCustomActionInvocationArgs(metadata, args);
|
|
549
561
|
try {
|
|
562
|
+
if (methodDef && metadata.idRequired && !id) throw new Error(`ID is required for custom action '${action}'`);
|
|
563
|
+
if (!metadata.idRequired && id && methodDef) throw new Error(`Custom action '${action}' is collection-scoped and does not accept an ID`);
|
|
550
564
|
if (id) {
|
|
551
565
|
const object = await collection.get(id);
|
|
552
566
|
if (!object) throw new Error("Object not found");
|
|
553
|
-
const objectMethod = object[
|
|
567
|
+
const objectMethod = object[methodName];
|
|
554
568
|
if (typeof objectMethod === "function") {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
569
|
+
const result = await objectMethod.call(object, ...methodArgs);
|
|
570
|
+
const failure = normalizeCustomActionFailure(result);
|
|
571
|
+
if (failure) throw new CustomActionFailureError(failure);
|
|
572
|
+
return result;
|
|
573
|
+
} else throw new Error(`Method '${methodName}' not found on object instance`);
|
|
574
|
+
} else if (metadata.isStatic && objectName) {
|
|
575
|
+
const classInfo = ObjectRegistry.getClass(objectName);
|
|
576
|
+
const classMethod = (classInfo?.constructor)?.[methodName];
|
|
577
|
+
if (typeof classMethod !== "function") throw new Error(`Static method '${methodName}' not found on ${objectName}`);
|
|
578
|
+
const result = await classMethod.call(classInfo?.constructor, ...methodArgs);
|
|
579
|
+
const failure = normalizeCustomActionFailure(result);
|
|
580
|
+
if (failure) throw new CustomActionFailureError(failure);
|
|
581
|
+
return result;
|
|
558
582
|
} else {
|
|
559
|
-
const collectionMethod = collection[
|
|
583
|
+
const collectionMethod = collection[methodName];
|
|
560
584
|
if (typeof collectionMethod === "function") {
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
585
|
+
const result = await collectionMethod.call(collection, ...methodArgs);
|
|
586
|
+
const failure = normalizeCustomActionFailure(result);
|
|
587
|
+
if (failure) throw new CustomActionFailureError(failure);
|
|
588
|
+
return result;
|
|
589
|
+
} else throw new Error(`Method '${methodName}' not found on collection. For object-specific actions, provide an 'id' parameter.`);
|
|
564
590
|
}
|
|
565
591
|
} catch (error) {
|
|
592
|
+
if (error instanceof CustomActionFailureError) throw error;
|
|
566
593
|
throw new Error(`Failed to execute custom action '${action}': ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
567
594
|
}
|
|
568
595
|
}
|
|
@@ -619,6 +646,7 @@ var MCPGenerator = class {
|
|
|
619
646
|
context: this.context,
|
|
620
647
|
debug,
|
|
621
648
|
tools,
|
|
649
|
+
customActions: await this.runtimeCustomActions(tools),
|
|
622
650
|
tenantScopedObjects: await this.tenantScopedObjectNames(tools)
|
|
623
651
|
}), "utf-8");
|
|
624
652
|
console.log(`ā
Generated MCP server: ${resolvedPath}`);
|
|
@@ -639,6 +667,41 @@ var MCPGenerator = class {
|
|
|
639
667
|
console.log(`\nš Add this to your package.json scripts:`);
|
|
640
668
|
console.log(` "mcp": "${mcpScript}"\n`);
|
|
641
669
|
}
|
|
670
|
+
async runtimeCustomActions(tools) {
|
|
671
|
+
const metadata = {};
|
|
672
|
+
const crudActions = /* @__PURE__ */ new Set([
|
|
673
|
+
"list",
|
|
674
|
+
"get",
|
|
675
|
+
"create",
|
|
676
|
+
"update",
|
|
677
|
+
"delete"
|
|
678
|
+
]);
|
|
679
|
+
const classes = ObjectRegistry.getAllClasses();
|
|
680
|
+
for (const tool of tools) {
|
|
681
|
+
const separator = tool.name.indexOf("_");
|
|
682
|
+
if (separator === -1) continue;
|
|
683
|
+
const objectPrefix = tool.name.slice(0, separator);
|
|
684
|
+
const action = tool.name.slice(separator + 1);
|
|
685
|
+
if (crudActions.has(action)) continue;
|
|
686
|
+
const matched = Array.from(classes.entries()).find(([key, info]) => (info.name || key).toLowerCase() === objectPrefix);
|
|
687
|
+
if (!matched) continue;
|
|
688
|
+
const [key, classInfo] = matched;
|
|
689
|
+
const objectName = classInfo.name || key;
|
|
690
|
+
const [methodName, method] = resolveCustomActionMethod(await ObjectRegistry.getAllMethods(objectName), action);
|
|
691
|
+
const resolved = this.resolveCustomActionMetadata(objectName, methodName, method, this.hasCollectionReceiver(classInfo));
|
|
692
|
+
metadata[tool.name] = {
|
|
693
|
+
scope: resolved.scope,
|
|
694
|
+
isStatic: resolved.isStatic,
|
|
695
|
+
methodName,
|
|
696
|
+
...resolved.parameters ? {
|
|
697
|
+
parameterNames: resolved.parameters.map((parameter) => parameter.name),
|
|
698
|
+
optionsParameter: resolved.parameters.length === 1 && resolved.parameters[0]?.name === "options"
|
|
699
|
+
} : {},
|
|
700
|
+
legacyOptions: !resolved.parameters
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
return metadata;
|
|
704
|
+
}
|
|
642
705
|
/**
|
|
643
706
|
* Generate modular MCP server structure
|
|
644
707
|
*
|
|
@@ -707,8 +770,10 @@ export const tools: Array<{
|
|
|
707
770
|
async generateToolSwitchCases(indent = " ") {
|
|
708
771
|
const tools = await this.generateTools();
|
|
709
772
|
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
710
|
-
return tools.map((tool) => {
|
|
711
|
-
const
|
|
773
|
+
return (await Promise.all(tools.map(async (tool) => {
|
|
774
|
+
const separator = tool.name.indexOf("_");
|
|
775
|
+
const objectName = tool.name.slice(0, separator);
|
|
776
|
+
const action = tool.name.slice(separator + 1);
|
|
712
777
|
switch (action) {
|
|
713
778
|
case "list": return `${indent}case '${tool.name}': {
|
|
714
779
|
${indent} const limit = args.limit ?? 50;
|
|
@@ -794,34 +859,59 @@ ${indent} await toDelete.delete();
|
|
|
794
859
|
|
|
795
860
|
${indent} return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Object deleted successfully' }) }] };
|
|
796
861
|
${indent}}`;
|
|
797
|
-
default:
|
|
798
|
-
|
|
862
|
+
default: {
|
|
863
|
+
const matched = Array.from(ObjectRegistry.getAllClasses().entries()).find(([key, info]) => (info.name || key).toLowerCase() === objectName.toLowerCase());
|
|
864
|
+
if (!matched) throw new Error(`Unable to resolve custom-action target for tool '${tool.name}'`);
|
|
865
|
+
const [classKey, classInfo] = matched;
|
|
866
|
+
const registeredName = classInfo.name || classKey;
|
|
867
|
+
const [methodName, method] = resolveCustomActionMethod(await ObjectRegistry.getAllMethods(registeredName), action);
|
|
868
|
+
const metadata = this.resolveCustomActionMetadata(registeredName, methodName, method, this.hasCollectionReceiver(classInfo));
|
|
869
|
+
const methodArgs = !metadata.parameters ? "Object.keys(options ?? {}).length > 0 ? options : directArgs" : metadata.parameters.length === 1 && metadata.parameters[0]?.name === "options" ? "options" : `[${metadata.parameters.map((parameter) => `args[${JSON.stringify(customActionParameterInputName(metadata, parameter.name))}]`).join(", ")}]`;
|
|
870
|
+
return `${indent}case '${tool.name}': {
|
|
871
|
+
${indent} const { id, options, ...directArgs } = args;
|
|
799
872
|
|
|
800
|
-
${indent} if (!id) {
|
|
873
|
+
${indent} if (${JSON.stringify(metadata.scope)} === 'item' && !id) {
|
|
801
874
|
${indent} throw new Error('ID is required for custom action ${action}');
|
|
802
875
|
${indent} }
|
|
876
|
+
${indent} if (${JSON.stringify(metadata.scope)} === 'collection' && id) {
|
|
877
|
+
${indent} throw new Error('Custom action ${action} is collection-scoped and does not accept an ID');
|
|
878
|
+
${indent} }
|
|
803
879
|
|
|
804
|
-
${indent} const collection = await ObjectRegistry.getCollection(
|
|
880
|
+
${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(registeredName)}, {
|
|
805
881
|
${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
|
|
806
882
|
${indent} ai: aiConfig
|
|
807
883
|
${indent} });
|
|
808
884
|
|
|
809
|
-
${indent} const
|
|
810
|
-
${indent}
|
|
811
|
-
${indent}
|
|
885
|
+
${indent} const target = ${JSON.stringify(metadata.scope)} === 'item'
|
|
886
|
+
${indent} ? await collection.get(id)
|
|
887
|
+
${indent} : ${metadata.isStatic}
|
|
888
|
+
${indent} ? ObjectRegistry.getClass(${JSON.stringify(registeredName)})?.constructor
|
|
889
|
+
${indent} : collection;
|
|
890
|
+
${indent} if (!target) {
|
|
891
|
+
${indent} throw new Error(${JSON.stringify(metadata.scope === "item" ? "Object not found" : "Custom action target not found")});
|
|
812
892
|
${indent} }
|
|
813
893
|
|
|
814
|
-
${indent}
|
|
815
|
-
${indent}
|
|
894
|
+
${indent} const actionMethod = target[${JSON.stringify(methodName)}];
|
|
895
|
+
${indent} if (typeof actionMethod !== 'function') {
|
|
896
|
+
${indent} throw new Error('Method ${methodName} not found on custom action target');
|
|
816
897
|
${indent} }
|
|
817
898
|
|
|
818
|
-
${indent} const methodArgs =
|
|
819
|
-
${indent} const result = await
|
|
899
|
+
${indent} const methodArgs = ${methodArgs.startsWith("[") ? methodArgs : `[${methodArgs}]`};
|
|
900
|
+
${indent} const result = await actionMethod.call(target, ...methodArgs);
|
|
901
|
+
${indent} const failure = normalizeCustomActionFailure(result);
|
|
902
|
+
${indent} if (failure) {
|
|
903
|
+
${indent} return {
|
|
904
|
+
${indent} content: [{ type: 'text', text: JSON.stringify({ error: failure }) }],
|
|
905
|
+
${indent} isError: true,
|
|
906
|
+
${indent} _meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },
|
|
907
|
+
${indent} };
|
|
908
|
+
${indent} }
|
|
820
909
|
|
|
821
910
|
${indent} return { content: [{ type: 'text', text: JSON.stringify(toPublicResult(result)) }] };
|
|
822
911
|
${indent}}`;
|
|
912
|
+
}
|
|
823
913
|
}
|
|
824
|
-
}).join("\n\n");
|
|
914
|
+
}))).join("\n\n");
|
|
825
915
|
}
|
|
826
916
|
/**
|
|
827
917
|
* Generate handlers file for modular server
|
|
@@ -845,7 +935,7 @@ ${indent}}`;
|
|
|
845
935
|
* principal) and tenancy is enabled so the interceptor enforces filtering.
|
|
846
936
|
*/
|
|
847
937
|
|
|
848
|
-
import { ObjectRegistry } from '@happyvertical/smrt-core';
|
|
938
|
+
import { ObjectRegistry, normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';
|
|
849
939
|
${hasTenantScoped ? "import { enableTenancy, runTenantScopedEntryPoint } from '@happyvertical/smrt-tenancy';\n" : ""}${hasTenantScoped ? `
|
|
850
940
|
// Install the tenancy interceptor so tenant-scoped tools are filtered and the
|
|
851
941
|
// gate fail-closes when no tenant is supplied (#1554).
|