@happyvertical/smrt-core 0.40.60 → 0.40.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +21 -0
  2. package/dist/generated-client-runtime.d.ts +6 -5
  3. package/dist/generated-client-runtime.d.ts.map +1 -1
  4. package/dist/generated-client-runtime.js +49 -14
  5. package/dist/generated-client-runtime.js.map +1 -1
  6. package/dist/generators/index.d.ts +3 -2
  7. package/dist/generators/index.d.ts.map +1 -1
  8. package/dist/generators/index.js +3 -2
  9. package/dist/generators/mcp-runtime-template.d.ts +10 -0
  10. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  11. package/dist/generators/mcp-runtime-template.js +238 -4
  12. package/dist/generators/mcp-runtime-template.js.map +1 -1
  13. package/dist/generators/mcp.d.ts +93 -0
  14. package/dist/generators/mcp.d.ts.map +1 -1
  15. package/dist/generators/mcp.js +166 -11
  16. package/dist/generators/mcp.js.map +1 -1
  17. package/dist/generators/rest.d.ts.map +1 -1
  18. package/dist/generators/rest.js +3 -0
  19. package/dist/generators/rest.js.map +1 -1
  20. package/dist/generators/typed-http-error.d.ts +16 -0
  21. package/dist/generators/typed-http-error.d.ts.map +1 -0
  22. package/dist/generators/typed-http-error.js +17 -0
  23. package/dist/generators/typed-http-error.js.map +1 -0
  24. package/dist/generators.js +3 -2
  25. package/dist/index.js +3 -2
  26. package/dist/manifest/static-manifest.js +1 -1
  27. package/dist/manifest/static-manifest.js.map +1 -1
  28. package/dist/manifest/store.js +1 -1
  29. package/dist/manifest.json +1 -1
  30. package/dist/prebuild/index.d.ts.map +1 -1
  31. package/dist/prebuild/index.js +13 -1
  32. package/dist/prebuild/index.js.map +1 -1
  33. package/dist/registry/types.d.ts +23 -0
  34. package/dist/registry/types.d.ts.map +1 -1
  35. package/dist/smrt-knowledge.json +3 -3
  36. package/dist/system/compatibility.d.ts.map +1 -1
  37. package/dist/system/compatibility.js +6 -0
  38. package/dist/system/compatibility.js.map +1 -1
  39. package/dist/vite-plugin/index.d.ts +2 -0
  40. package/dist/vite-plugin/index.d.ts.map +1 -1
  41. package/dist/vite-plugin/index.js +24 -11
  42. package/dist/vite-plugin/index.js.map +1 -1
  43. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  44. package/dist/vite-plugin/sveltekit-generator.js +94 -26
  45. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  46. package/dist/vite-plugin/web-collections.d.ts +1 -0
  47. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  48. package/dist/vite-plugin/web-collections.js +17 -2
  49. package/dist/vite-plugin/web-collections.js.map +1 -1
  50. package/package.json +5 -5
  51. package/dist/manifest/test-manifest-loader.d.ts +0 -3
  52. package/dist/manifest/test-manifest-loader.d.ts.map +0 -1
  53. package/dist/manifest/test-manifest-stub.d.ts +0 -4
  54. package/dist/manifest/test-manifest-stub.d.ts.map +0 -1
  55. package/dist/manifest/test-manifest-stub.js +0 -77056
  56. package/dist/manifest/test-manifest-stub.js.map +0 -1
@@ -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 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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"}
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 /** Cache hint emitted for deploy-static tools/list results. */\n toolListCacheHint?: {\n ttlMs: number;\n cacheScope: 'private' | 'public';\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 /** Task-enabled item custom actions. Never emitted in tools/list. */\n taskActions?: Record<\n string,\n {\n objectName: string;\n objectType: string;\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 taskActions = {},\n tenantScopedObjects = [],\n stiTargets = {},\n toolListCacheHint = { ttlMs: 86_400_000, cacheScope: 'private' },\n } = options;\n\n // Generate static tool array as TypeScript code\n const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : '[]';\n const hasTaskActions = Object.keys(taskActions).length > 0;\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 ${hasTaskActions ? 'specTypeSchemas,' : ''}\n} from '@modelcontextprotocol/server';\nimport { ${hasTaskActions ? 'StdioServerTransport, ' : ''}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${hasTaskActions ? \"import { McpTaskStore, TaskRunner } from '@happyvertical/smrt-jobs';\\n\" : ''}\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 TOOL_LIST_CACHE_HINT = ${JSON.stringify(toolListCacheHint)};\nconst CUSTOM_ACTIONS = ${JSON.stringify(customActions)};\nconst TASK_ACTIONS = ${JSON.stringify(taskActions)};\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 hasTaskActions\n ? `\n// The bundled SDK validates tools/call responses against its older result\n// codec, which does not yet know CreateTaskResult. Intercept the extension at\n// the transport boundary, then pass every non-task message untouched to the\n// SDK server. This keeps ordinary MCP behaviour and version negotiation owned\n// by the SDK while making the extension available today.\nconst MCP_TASKS_EXTENSION = 'io.modelcontextprotocol/tasks';\nlet taskRuntime: Promise<{ store: McpTaskStore; runner: TaskRunner }> | undefined;\n\nfunction clientSupportsTasks(message: any): boolean {\n return message?.params?._meta?.['io.modelcontextprotocol/clientCapabilities']\n ?.extensions?.[MCP_TASKS_EXTENSION] !== undefined;\n}\n\n/** Validate the 2026 request envelope before task dispatch mutates a job. */\nfunction taskEnvelopeError(message: any): { code: number; message: string; data?: any } | undefined {\n const meta = message?.params?._meta;\n if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {\n return {\n code: -32602,\n message: 'Request is missing the required _meta envelope for protocol revision 2026-07-28',\n };\n }\n const protocolVersion = meta['io.modelcontextprotocol/protocolVersion'];\n if (protocolVersion !== '2026-07-28') {\n return typeof protocolVersion === 'string'\n ? {\n code: -32022,\n message: 'Unsupported protocol version: ' + protocolVersion,\n data: { supported: ['2026-07-28'], requested: protocolVersion },\n }\n : {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/protocolVersion must be a string',\n };\n }\n if (!specTypeSchemas.ClientCapabilities.safeParse(\n meta['io.modelcontextprotocol/clientCapabilities'],\n ).success) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/clientCapabilities is invalid',\n };\n }\n if (\n meta['io.modelcontextprotocol/clientInfo'] !== undefined &&\n !specTypeSchemas.Implementation.safeParse(\n meta['io.modelcontextprotocol/clientInfo'],\n ).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/clientInfo is invalid',\n };\n }\n if (\n meta['io.modelcontextprotocol/logLevel'] !== undefined &&\n !specTypeSchemas.LoggingLevel.safeParse(\n meta['io.modelcontextprotocol/logLevel'],\n ).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/logLevel is invalid',\n };\n }\n if (\n meta.progressToken !== undefined &&\n !specTypeSchemas.ProgressToken.safeParse(meta.progressToken).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: progressToken is invalid',\n };\n }\n return undefined;\n}\n\nfunction jsonRpcError(id: any, code: number, message: string, data?: any) {\n return { jsonrpc: '2.0', id, error: { code, message, ...(data === undefined ? {} : { data }) } };\n}\n\nasync function getTaskRuntime() {\n if (!taskRuntime) {\n taskRuntime = (async () => {\n const firstAction = Object.values(TASK_ACTIONS)[0] as { objectName: string } | undefined;\n if (!firstAction) throw new Error('No task-enabled MCP action is configured');\n const collection = await ObjectRegistry.getCollection(firstAction.objectName, {\n persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n });\n const db = collection.db;\n const store = await McpTaskStore.create(db, { ownerId: process.env.SMRT_MCP_TENANT_ID || null });\n const runner = new TaskRunner({ queues: ['mcp-tasks'] });\n await runner.initialize(db);\n await runner.start();\n return { store, runner };\n })();\n }\n return taskRuntime;\n}\n\nfunction taskInvocationArgs(actionMeta: any, args: Record<string, any>): any[] {\n const { id: _id, options, ...directArgs } = args;\n if (actionMeta.legacyOptions) {\n return [Object.keys(options ?? {}).length > 0 ? options : directArgs];\n }\n if (actionMeta.optionsParameter) return [options];\n const parameterNames = actionMeta.parameterNames || [];\n const invocationParameterNames = parameterNames.at(-1) === 'context'\n ? parameterNames.slice(0, -1)\n : parameterNames;\n return invocationParameterNames.map((parameterName: string) =>\n args[parameterName === 'id' ? 'actionId' : parameterName],\n );\n}\n\nasync function handleTaskExtensionMessage(message: any): Promise<any | null> {\n if (!message || typeof message !== 'object' || message.id === undefined) return null;\n const method = message.method;\n const params = message.params ?? {};\n const action = method === 'tools/call' ? TASK_ACTIONS[params.name] : undefined;\n const isTaskMethod = action || ['tasks/get', 'tasks/update', 'tasks/cancel'].includes(method);\n if (!isTaskMethod) return null;\n // A task-enabled tool retains its normal synchronous behaviour until its\n // client explicitly opts into Tasks. Do not impose the task envelope on\n // legacy calls that will be passed untouched to the SDK.\n if (!clientSupportsTasks(message) && method === 'tools/call') return null;\n const envelopeError = taskEnvelopeError(message);\n if (envelopeError) {\n return jsonRpcError(message.id, envelopeError.code, envelopeError.message, envelopeError.data);\n }\n if (!clientSupportsTasks(message)) {\n // A task-only method cannot fall back to a legacy response shape. A task\n // tool can still run normally when the client did not opt in, so let the\n // SDK handle that direct tools/call path.\n if (method === 'tools/call') return null;\n return jsonRpcError(message.id, -32021, 'Missing required client capability', {\n requiredCapabilities: { extensions: { [MCP_TASKS_EXTENSION]: {} } },\n });\n }\n try {\n const { store } = await getTaskRuntime();\n if (action) {\n if (typeof params.arguments?.id !== 'string') {\n return jsonRpcError(message.id, -32602, 'Task-enabled custom actions require an id');\n }\n const actionMeta = CUSTOM_ACTIONS[params.name];\n const task = await store.createTask({\n objectType: action.objectType,\n objectId: params.arguments.id,\n method: actionMeta.methodName || params.name.slice(params.name.indexOf('_') + 1),\n invocationArgs: taskInvocationArgs(actionMeta, params.arguments),\n tenantId: ${hasTenantScoped ? 'MCP_TENANT_ID ?? null' : 'null'},\n });\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'task', content: [], structuredContent: {}, ...task } };\n }\n if (typeof params.taskId !== 'string') {\n return jsonRpcError(message.id, -32602, 'taskId is required');\n }\n if (method === 'tasks/get') {\n const task = await store.getTask(params.taskId);\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete', ...task } };\n }\n if (method === 'tasks/update') {\n await store.updateTask(\n params.taskId,\n params.inputResponses && typeof params.inputResponses === 'object'\n ? params.inputResponses\n : {},\n );\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete' } };\n }\n await store.cancelTask(params.taskId);\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete' } };\n } catch (error) {\n const messageText = error instanceof Error ? error.message : 'Task operation failed';\n return jsonRpcError(message.id, -32602, messageText);\n }\n}\n\nclass McpTaskExtensionTransport {\n onclose?: () => void;\n onerror?: (error: Error) => void;\n onmessage?: (message: any) => void;\n\n constructor(private readonly wire: StdioServerTransport) {}\n\n async start() {\n this.wire.onclose = () => this.onclose?.();\n this.wire.onerror = (error) => this.onerror?.(error);\n this.wire.onmessage = (message) => {\n void (async () => {\n const response = await handleTaskExtensionMessage(message);\n if (response) await this.wire.send(response);\n else this.onmessage?.(message);\n })().catch((error) => this.onerror?.(error instanceof Error ? error : new Error(String(error))));\n };\n await this.wire.start();\n }\n\n close() { return this.wire.close(); }\n send(message: any) { return this.wire.send(message); }\n}\n`\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 // A manifest supplies schemas and tool metadata, while an executable\n // custom action also needs the application's actual class constructor.\n // Consumer builds generate this registration module; load it after the\n // manifest so decorators enrich the already-known metadata.\n const localRegisterPath = process.env.SMRT_MCP_REGISTER_PATH\n || resolve(process.cwd(), '.smrt', 'register.js');\n if (existsSync(localRegisterPath)) {\n await import(pathToFileURL(localRegisterPath).href);\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 ${hasTaskActions ? \"extensions: { 'io.modelcontextprotocol/tasks': {} },\" : ''}\n },\n cacheHints: {\n 'tools/list': TOOL_LIST_CACHE_HINT,\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].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0),\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 // Initialize the registry before accepting an intercepted task call. The\n // SDK would normally invoke this factory for the first regular message,\n // but task calls can be the first message on a stdio connection.\n const server = await createServer();\n const transport = ${hasTaskActions ? 'new McpTaskExtensionTransport(new StdioServerTransport())' : 'undefined'};\n const handle = serveStdio(() => server, {\n ...(transport ? { transport } : {}),\n onerror: (error) => console.error('[MCP] Protocol error:', error),\n });\n const shutdown = async () => {\n if (DEBUG) console.error('[MCP] Shutting down gracefully');\n ${hasTaskActions ? 'if (taskRuntime) {\\n const { runner } = await taskRuntime;\\n await runner.stop();\\n }' : ''}\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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"}
@@ -8,11 +8,47 @@ export interface MCPConfig {
8
8
  name?: string;
9
9
  version?: string;
10
10
  description?: string;
11
+ /**
12
+ * Cache policy for generated MCP protocol results.
13
+ *
14
+ * Generated catalog results are private by default. A public tool catalog
15
+ * needs both an explicit public scope and an explicit assertion that the
16
+ * entire catalog is global and unauthenticated; tenant-scoped catalogs are
17
+ * always forced back to private.
18
+ */
19
+ cache?: {
20
+ toolsList?: MCPToolListCacheOptions;
21
+ };
11
22
  server?: {
12
23
  name: string;
13
24
  version: string;
14
25
  };
15
26
  }
27
+ export declare const MCP_STABLE_CATALOG_TTL_MS = 86400000;
28
+ export interface MCPToolListCacheOptions {
29
+ /** Cache lifetime in milliseconds. Defaults to one day for a deploy-static catalog. */
30
+ ttlMs?: number;
31
+ /** Requested cache visibility. Defaults to private. */
32
+ cacheScope?: 'private' | 'public';
33
+ /**
34
+ * Explicitly attest that every listed tool is global and unauthenticated.
35
+ * This must accompany `cacheScope: 'public'`; tenant-scoped tool sets cannot
36
+ * opt in regardless of this assertion.
37
+ */
38
+ publicCatalog?: true;
39
+ }
40
+ export interface MCPToolListCacheHint {
41
+ ttlMs: number;
42
+ cacheScope: 'private' | 'public';
43
+ }
44
+ /**
45
+ * Resolve the generated tools/list cache policy at generation time.
46
+ *
47
+ * A shared cache may otherwise serve one tenant's tool catalog to another, so
48
+ * public caching is deliberately double opt-in and unavailable when a
49
+ * generated server exposes any tenant-scoped object.
50
+ */
51
+ export declare function resolveMCPToolListCacheHint(options: MCPToolListCacheOptions | undefined, hasTenantScopedTools: boolean): MCPToolListCacheHint;
16
52
  export interface MCPContext {
17
53
  db?: unknown;
18
54
  ai?: unknown;
@@ -35,6 +71,32 @@ export interface MCPContext {
35
71
  * or this flag, tenant-scoped tool calls fail closed when tenancy is enabled.
36
72
  */
37
73
  allowCrossTenant?: boolean;
74
+ /**
75
+ * Optional durable backing store for the MCP Tasks extension. Core owns
76
+ * task eligibility and action argument projection; jobs-backed runtimes own
77
+ * persistence so `@happyvertical/smrt-core` stays independent of jobs.
78
+ */
79
+ taskStore?: MCPTaskStore;
80
+ }
81
+ /** Minimal durable task-store contract implemented by `@smrt-jobs`. */
82
+ export interface MCPTaskStore {
83
+ createTask(input: {
84
+ objectType: string;
85
+ objectId: string;
86
+ method: string;
87
+ invocationArgs: unknown[];
88
+ tenantId?: string | null;
89
+ }): Promise<MCPTask>;
90
+ }
91
+ /** Flat MCP Tasks extension projection shared by generated and app transports. */
92
+ export interface MCPTask {
93
+ taskId: string;
94
+ status: 'working' | 'input_required' | 'completed' | 'cancelled' | 'failed';
95
+ createdAt: string;
96
+ lastUpdatedAt: string;
97
+ ttlMs: number;
98
+ pollIntervalMs?: number;
99
+ statusMessage?: string;
38
100
  }
39
101
  export interface MCPTool {
40
102
  name: string;
@@ -43,6 +105,8 @@ export interface MCPTool {
43
105
  /** Public result schema for tools/call structuredContent. */
44
106
  outputSchema: ToolJsonSchema;
45
107
  }
108
+ /** Return a copied, canonical tool sequence for byte-stable tools/list output. */
109
+ export declare function sortMCPTools<T extends Pick<MCPTool, 'name'>>(tools: T[]): T[];
46
110
  export interface MCPRequest {
47
111
  method: string;
48
112
  params: {
@@ -59,6 +123,15 @@ export interface MCPResponse {
59
123
  _meta?: Record<string, unknown>;
60
124
  /** Machine-readable projection matching the tool's declared outputSchema. */
61
125
  structuredContent?: unknown;
126
+ /** Set only for a Tasks extension CreateTaskResult. */
127
+ resultType?: 'task' | 'complete';
128
+ taskId?: string;
129
+ status?: MCPTask['status'];
130
+ createdAt?: string;
131
+ lastUpdatedAt?: string;
132
+ ttlMs?: number;
133
+ pollIntervalMs?: number;
134
+ statusMessage?: string;
62
135
  }
63
136
  /**
64
137
  * Generate MCP server from smrt objects
@@ -112,6 +185,15 @@ export declare class MCPGenerator {
112
185
  * Handle MCP tool calls
113
186
  */
114
187
  handleToolCall(request: MCPRequest): Promise<MCPResponse>;
188
+ /** Whether a visible tool has explicitly opted into durable task execution. */
189
+ supportsTaskTool(name: string): Promise<boolean>;
190
+ /**
191
+ * Create a durable MCP task for an explicitly enabled item custom action.
192
+ * The caller is responsible for checking the client's extension capability
193
+ * before exposing this result on the wire.
194
+ */
195
+ createTask(request: MCPRequest): Promise<MCPResponse>;
196
+ private resolveTaskAction;
115
197
  /** Convert runtime values to the JSON values MCP structuredContent permits. */
116
198
  private toJsonValue;
117
199
  /** Build the MCP-required object root without changing legacy text payloads. */
@@ -161,6 +243,15 @@ export declare class MCPGenerator {
161
243
  * absent there is also nothing to enforce, so emitting no gate is correct.
162
244
  */
163
245
  private tenantScopedObjectNames;
246
+ /**
247
+ * Whether a catalog contains a tenant-scoped class for cache isolation.
248
+ *
249
+ * Unlike the emitted runtime tenant gate, cache visibility must also fail
250
+ * closed for core-declared `@smrt({ tenantScoped })` models when the optional
251
+ * tenancy package is not installed. The registry covers that form, while
252
+ * `tenantScopedObjectNames()` covers the tenancy-owned decorator form.
253
+ */
254
+ private hasTenantScopedTools;
164
255
  /**
165
256
  * Execute a resolved MCP action (CRUD or custom) against a collection. Always
166
257
  * invoked inside the tenant gate established by {@link executeAction}.
@@ -222,6 +313,8 @@ export declare class MCPGenerator {
222
313
  modular?: boolean;
223
314
  }): Promise<void>;
224
315
  private runtimeCustomActions;
316
+ /** Emit only task-enabled item custom actions for the generated runtime. */
317
+ private runtimeTaskActions;
225
318
  /**
226
319
  * Emit only the STI discriminator targets advertised by create-tool schemas.
227
320
  * Generated processes start with an empty registry, so resolving the
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/generators/mcp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/generators/mcp.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,27 @@ import { mkdir, writeFile } from "node:fs/promises";
12
12
  *
13
13
  * Exposes smrt objects as AI tools for Claude, GPT, and other AI models
14
14
  */
15
+ var MCP_STABLE_CATALOG_TTL_MS = 864e5;
16
+ /**
17
+ * Resolve the generated tools/list cache policy at generation time.
18
+ *
19
+ * A shared cache may otherwise serve one tenant's tool catalog to another, so
20
+ * public caching is deliberately double opt-in and unavailable when a
21
+ * generated server exposes any tenant-scoped object.
22
+ */
23
+ function resolveMCPToolListCacheHint(options, hasTenantScopedTools) {
24
+ const ttlMs = options?.ttlMs ?? 864e5;
25
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 0) throw new RangeError("MCP tools/list cache ttlMs must be a non-negative safe integer.");
26
+ if (options?.cacheScope !== void 0 && options.cacheScope !== "private" && options.cacheScope !== "public") throw new RangeError("MCP tools/list cacheScope must be 'private' or 'public'.");
27
+ return {
28
+ ttlMs,
29
+ cacheScope: !hasTenantScopedTools && options?.cacheScope === "public" && options.publicCatalog === true ? "public" : "private"
30
+ };
31
+ }
32
+ /** Return a copied, canonical tool sequence for byte-stable tools/list output. */
33
+ function sortMCPTools(tools) {
34
+ return [...tools].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
35
+ }
15
36
  var CustomActionFailureError = class extends Error {
16
37
  failure;
17
38
  constructor(failure) {
@@ -32,6 +53,29 @@ function resolveCustomActionMethod(methods, toolAction) {
32
53
  for (const [methodName, method] of methods) if (methodName.toLowerCase() === toolAction.toLowerCase()) return [methodName, method];
33
54
  return [toolAction, void 0];
34
55
  }
56
+ /** Preserve a declared method's case when a runtime-only class has no manifest. */
57
+ function resolveRuntimeMethodName(classConstructor, action) {
58
+ let prototype = classConstructor?.prototype;
59
+ while (prototype && prototype !== Object.prototype) {
60
+ const match = Object.getOwnPropertyNames(prototype).find((name) => name.toLowerCase() === action.toLowerCase());
61
+ if (match) return match;
62
+ prototype = Object.getPrototypeOf(prototype);
63
+ }
64
+ return action;
65
+ }
66
+ /**
67
+ * Background task methods receive `JobExecutionContext` from TaskRunner, not
68
+ * from untrusted MCP arguments. Keep that conventional trailing parameter out
69
+ * of the persisted positional call so the runner can append its live context.
70
+ */
71
+ function buildTaskActionInvocationArgs(metadata, args) {
72
+ const parameters = metadata.parameters;
73
+ if (parameters?.at(-1)?.name === "context") return buildCustomActionInvocationArgs({
74
+ ...metadata,
75
+ parameters: parameters.slice(0, -1)
76
+ }, args);
77
+ return buildCustomActionInvocationArgs(metadata, args);
78
+ }
35
79
  /**
36
80
  * Generate MCP server from smrt objects
37
81
  */
@@ -84,7 +128,7 @@ var MCPGenerator = class {
84
128
  const objectTools = await this.generateObjectTools(simpleName, shouldInclude);
85
129
  tools.push(...objectTools);
86
130
  }
87
- return tools;
131
+ return sortMCPTools(tools);
88
132
  }
89
133
  /**
90
134
  * Generate tools for a specific object
@@ -438,6 +482,63 @@ var MCPGenerator = class {
438
482
  };
439
483
  }
440
484
  }
485
+ /** Whether a visible tool has explicitly opted into durable task execution. */
486
+ async supportsTaskTool(name) {
487
+ return await this.resolveTaskAction(name, { id: "__mcp_task_probe__" }) !== null;
488
+ }
489
+ /**
490
+ * Create a durable MCP task for an explicitly enabled item custom action.
491
+ * The caller is responsible for checking the client's extension capability
492
+ * before exposing this result on the wire.
493
+ */
494
+ async createTask(request) {
495
+ if (!this.context.taskStore) throw new Error("MCP Tasks is enabled but no durable task store is configured");
496
+ const resolved = await this.resolveTaskAction(request.params.name, request.params.arguments);
497
+ if (!resolved) throw new Error(`MCP task execution is not enabled for tool: ${request.params.name}`);
498
+ return {
499
+ content: [],
500
+ structuredContent: {},
501
+ resultType: "task",
502
+ ...await this.context.taskStore.createTask({
503
+ objectType: resolved.objectType,
504
+ objectId: resolved.objectId,
505
+ method: resolved.methodName,
506
+ invocationArgs: resolved.invocationArgs,
507
+ tenantId: this.context.tenantId ?? null
508
+ })
509
+ };
510
+ }
511
+ async resolveTaskAction(toolName, args) {
512
+ const separator = toolName.indexOf("_");
513
+ if (separator <= 0) return null;
514
+ const objectPrefix = toolName.slice(0, separator);
515
+ const action = toolName.slice(separator + 1);
516
+ if ([
517
+ "list",
518
+ "get",
519
+ "create",
520
+ "update",
521
+ "delete"
522
+ ].includes(action)) return null;
523
+ const classEntry = Array.from(ObjectRegistry.getAllClasses().entries()).find(([key, info]) => (info.name || key).toLowerCase() === objectPrefix.toLowerCase());
524
+ if (!classEntry) return null;
525
+ const [key, classInfo] = classEntry;
526
+ const objectName = classInfo.name || key;
527
+ const mcpConfig = ObjectRegistry.getConfig(objectName).mcp;
528
+ const configuredTasks = typeof mcpConfig === "object" ? mcpConfig.tasks : void 0;
529
+ if (configuredTasks !== true && (!Array.isArray(configuredTasks) || !configuredTasks.some((method) => method.toLowerCase() === action.toLowerCase()))) return null;
530
+ if (!(await this.generateTools()).some((tool) => tool.name === toolName)) return null;
531
+ const [resolvedMethodName, method] = resolveCustomActionMethod(await ObjectRegistry.getAllMethods(objectName), action);
532
+ const methodName = method ? resolvedMethodName : resolveRuntimeMethodName(classInfo.constructor, action);
533
+ const metadata = this.resolveCustomActionMetadata(objectName, methodName, method, this.hasCollectionReceiver(classInfo));
534
+ if (!metadata.idRequired || metadata.isStatic || typeof args.id !== "string") return null;
535
+ return {
536
+ objectType: classInfo.qualifiedName || objectName,
537
+ objectId: args.id,
538
+ methodName,
539
+ invocationArgs: buildTaskActionInvocationArgs(metadata, args)
540
+ };
541
+ }
441
542
  /** Convert runtime values to the JSON values MCP structuredContent permits. */
442
543
  toJsonValue(value) {
443
544
  const serialized = JSON.stringify(value);
@@ -609,6 +710,29 @@ var MCPGenerator = class {
609
710
  return Array.from(scoped);
610
711
  }
611
712
  /**
713
+ * Whether a catalog contains a tenant-scoped class for cache isolation.
714
+ *
715
+ * Unlike the emitted runtime tenant gate, cache visibility must also fail
716
+ * closed for core-declared `@smrt({ tenantScoped })` models when the optional
717
+ * tenancy package is not installed. The registry covers that form, while
718
+ * `tenantScopedObjectNames()` covers the tenancy-owned decorator form.
719
+ */
720
+ async hasTenantScopedTools(tools) {
721
+ if ((await this.tenantScopedObjectNames(tools)).length > 0) return true;
722
+ for (const tool of tools) {
723
+ const [objectName] = tool.name.split("_");
724
+ if (!objectName) continue;
725
+ for (const [key, info] of ObjectRegistry.getAllClasses()) {
726
+ const simpleName = info.name || key;
727
+ if (simpleName.toLowerCase() === objectName.toLowerCase()) {
728
+ if (ObjectRegistry.isTenantScoped(simpleName)) return true;
729
+ break;
730
+ }
731
+ }
732
+ }
733
+ return false;
734
+ }
735
+ /**
612
736
  * Execute a resolved MCP action (CRUD or custom) against a collection. Always
613
737
  * invoked inside the tenant gate established by {@link executeAction}.
614
738
  */
@@ -765,6 +889,8 @@ var MCPGenerator = class {
765
889
  if (modular) await this.generateModularServer(outputDir, serverName, serverVersion, debug);
766
890
  else {
767
891
  const tools = await this.generateTools();
892
+ const tenantScopedObjects = await this.tenantScopedObjectNames(tools);
893
+ const hasTenantScopedTools = tenantScopedObjects.length > 0 || await this.hasTenantScopedTools(tools);
768
894
  await writeFile(resolvedPath, generateRuntimeBootstrap({
769
895
  name: serverName,
770
896
  version: serverVersion,
@@ -774,8 +900,10 @@ var MCPGenerator = class {
774
900
  debug,
775
901
  tools,
776
902
  customActions: await this.runtimeCustomActions(tools),
777
- tenantScopedObjects: await this.tenantScopedObjectNames(tools),
778
- stiTargets: this.runtimeStiTargets(tools)
903
+ taskActions: await this.runtimeTaskActions(tools),
904
+ tenantScopedObjects,
905
+ stiTargets: this.runtimeStiTargets(tools),
906
+ toolListCacheHint: resolveMCPToolListCacheHint(this.config.cache?.toolsList, hasTenantScopedTools)
779
907
  }), "utf-8");
780
908
  console.log(`✅ Generated MCP server: ${resolvedPath}`);
781
909
  }
@@ -830,6 +958,26 @@ var MCPGenerator = class {
830
958
  }
831
959
  return metadata;
832
960
  }
961
+ /** Emit only task-enabled item custom actions for the generated runtime. */
962
+ async runtimeTaskActions(tools) {
963
+ const actions = {};
964
+ const classes = ObjectRegistry.getAllClasses();
965
+ for (const tool of tools) {
966
+ if (!await this.supportsTaskTool(tool.name)) continue;
967
+ const separator = tool.name.indexOf("_");
968
+ if (separator <= 0) continue;
969
+ const objectPrefix = tool.name.slice(0, separator).toLowerCase();
970
+ const matched = Array.from(classes.entries()).find(([key, info]) => (info.name || key).toLowerCase() === objectPrefix);
971
+ if (!matched) continue;
972
+ const [key, classInfo] = matched;
973
+ const objectName = classInfo.name || key;
974
+ actions[tool.name] = {
975
+ objectName,
976
+ objectType: classInfo.qualifiedName || objectName
977
+ };
978
+ }
979
+ return actions;
980
+ }
833
981
  /**
834
982
  * Emit only the STI discriminator targets advertised by create-tool schemas.
835
983
  * Generated processes start with an empty registry, so resolving the
@@ -871,15 +1019,17 @@ var MCPGenerator = class {
871
1019
  const configPath = resolve(outputDir, "config.ts");
872
1020
  await writeFile(configPath, this.generateConfigFile(serverName, serverVersion, debug), "utf-8");
873
1021
  console.log(`✅ Generated config: ${configPath}`);
1022
+ const generatedTools = await this.generateTools();
874
1023
  const toolsPath = resolve(toolsDir, "index.ts");
875
- await writeFile(toolsPath, await this.generateToolsFile(), "utf-8");
1024
+ await writeFile(toolsPath, this.generateToolsFile(generatedTools), "utf-8");
876
1025
  console.log(`✅ Generated tools: ${toolsPath}`);
877
1026
  const handlersPath = resolve(handlersDir, "index.ts");
878
- const tenantScopedObjects = await this.tenantScopedObjectNames(await this.generateTools());
1027
+ const tenantScopedObjects = await this.tenantScopedObjectNames(generatedTools);
1028
+ const hasTenantScopedTools = tenantScopedObjects.length > 0 || await this.hasTenantScopedTools(generatedTools);
879
1029
  await writeFile(handlersPath, await this.generateHandlersFile(tenantScopedObjects), "utf-8");
880
1030
  console.log(`✅ Generated handlers: ${handlersPath}`);
881
1031
  const indexPath = resolve(outputDir, "index.js");
882
- await writeFile(indexPath, this.generateModularIndex(), "utf-8");
1032
+ await writeFile(indexPath, this.generateModularIndex(resolveMCPToolListCacheHint(this.config.cache?.toolsList, hasTenantScopedTools)), "utf-8");
883
1033
  console.log(`✅ Generated MCP server: ${indexPath}`);
884
1034
  }
885
1035
  /**
@@ -900,8 +1050,7 @@ export const DEBUG = ${debug};
900
1050
  /**
901
1051
  * Generate tools definitions file for modular server
902
1052
  */
903
- async generateToolsFile() {
904
- const tools = await this.generateTools();
1053
+ generateToolsFile(tools) {
905
1054
  return `/**
906
1055
  * MCP Tools Definitions
907
1056
  * Auto-generated from SMRT objects
@@ -1099,6 +1248,7 @@ const TENANT_SCOPED = new Set(${JSON.stringify(tenantScopedSet)});
1099
1248
  const MCP_TENANT_ID = process.env.SMRT_MCP_TENANT_ID || undefined;
1100
1249
  const MCP_ALLOW_CROSS_TENANT = process.env.SMRT_MCP_ALLOW_CROSS_TENANT === 'true';
1101
1250
  ` : ""}
1251
+
1102
1252
  const PUBLIC_JSON_OPTIONS = {
1103
1253
  permissions: (process.env.SMRT_MCP_PERMISSIONS || '')
1104
1254
  .split(',')
@@ -1243,7 +1393,7 @@ ${hasTenantScoped ? `
1243
1393
  /**
1244
1394
  * Generate modular index file (main entry point)
1245
1395
  */
1246
- generateModularIndex() {
1396
+ generateModularIndex(toolListCacheHint) {
1247
1397
  return `#!/usr/bin/env node
1248
1398
  /**
1249
1399
  * Auto-generated MCP Server
@@ -1266,6 +1416,8 @@ import { SERVER_NAME, SERVER_VERSION, DEBUG } from './config.js';
1266
1416
  import { tools } from './tools/index.js';
1267
1417
  import { handleToolCall } from './handlers/index.js';
1268
1418
 
1419
+ const TOOL_LIST_CACHE_HINT = ${JSON.stringify(toolListCacheHint)};
1420
+
1269
1421
  /**
1270
1422
  * Main server startup function
1271
1423
  */
@@ -1300,6 +1452,9 @@ export async function createServer() {
1300
1452
  capabilities: {
1301
1453
  tools: {},
1302
1454
  },
1455
+ cacheHints: {
1456
+ 'tools/list': TOOL_LIST_CACHE_HINT,
1457
+ },
1303
1458
  }
1304
1459
  );
1305
1460
 
@@ -1310,7 +1465,7 @@ export async function createServer() {
1310
1465
  }
1311
1466
 
1312
1467
  return {
1313
- tools: tools.map(tool => ({
1468
+ tools: [...tools].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0).map(tool => ({
1314
1469
  name: tool.name,
1315
1470
  description: tool.description,
1316
1471
  inputSchema: tool.inputSchema,
@@ -1362,6 +1517,6 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
1362
1517
  }
1363
1518
  };
1364
1519
  //#endregion
1365
- export { MCPGenerator };
1520
+ export { MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, resolveMCPToolListCacheHint, sortMCPTools };
1366
1521
 
1367
1522
  //# sourceMappingURL=mcp.js.map