@open-mercato/ai-assistant 0.6.5-develop.5337.1.534b781eac → 0.6.5

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 (82) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +14 -2
  3. package/README.md +1 -1
  4. package/dist/modules/ai_assistant/ai-tools/meta-pack.js +3 -1
  5. package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +2 -2
  6. package/dist/modules/ai_assistant/api/mcp-key/route.js +95 -0
  7. package/dist/modules/ai_assistant/api/mcp-key/route.js.map +7 -0
  8. package/dist/modules/ai_assistant/cli.js +14 -6
  9. package/dist/modules/ai_assistant/cli.js.map +2 -2
  10. package/dist/modules/ai_assistant/components/McpConfigDialog.js +1 -1
  11. package/dist/modules/ai_assistant/components/McpConfigDialog.js.map +2 -2
  12. package/dist/modules/ai_assistant/data/entities.js +4 -0
  13. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  14. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +3 -1
  15. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +2 -2
  16. package/dist/modules/ai_assistant/lib/ai-overrides.js.map +1 -1
  17. package/dist/modules/ai_assistant/lib/ai-sdk.js.map +1 -1
  18. package/dist/modules/ai_assistant/lib/attachment-parts.js +10 -3
  19. package/dist/modules/ai_assistant/lib/attachment-parts.js.map +2 -2
  20. package/dist/modules/ai_assistant/lib/chat-config.js.map +1 -1
  21. package/dist/modules/ai_assistant/lib/client-factory.js +0 -8
  22. package/dist/modules/ai_assistant/lib/client-factory.js.map +2 -2
  23. package/dist/modules/ai_assistant/lib/codemode-tools.js +26 -0
  24. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
  25. package/dist/modules/ai_assistant/lib/generated-registry-loader.js +23 -3
  26. package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
  27. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +1 -1
  28. package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +1 -1
  29. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +1 -1
  30. package/dist/modules/ai_assistant/lib/llm-bootstrap.js.map +1 -1
  31. package/dist/modules/ai_assistant/lib/mcp-client.js +2 -4
  32. package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
  33. package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js +71 -0
  34. package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js.map +7 -0
  35. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +1 -30
  36. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
  37. package/dist/modules/ai_assistant/lib/mcp-server.js +29 -23
  38. package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
  39. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +1 -1
  40. package/dist/modules/ai_assistant/lib/task-plan-stream.js.map +1 -1
  41. package/dist/modules/ai_assistant/setup.js +9 -2
  42. package/dist/modules/ai_assistant/setup.js.map +2 -2
  43. package/package.json +11 -12
  44. package/src/modules/ai_assistant/__tests__/cli-mcp-serve-key.test.ts +63 -0
  45. package/src/modules/ai_assistant/__tests__/setup-schedules.test.ts +33 -0
  46. package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +29 -0
  47. package/src/modules/ai_assistant/ai-tools/meta-pack.ts +7 -1
  48. package/src/modules/ai_assistant/api/mcp-key/__tests__/route.test.ts +144 -0
  49. package/src/modules/ai_assistant/api/mcp-key/route.ts +125 -0
  50. package/src/modules/ai_assistant/cli.ts +18 -5
  51. package/src/modules/ai_assistant/components/McpConfigDialog.tsx +5 -1
  52. package/src/modules/ai_assistant/data/__tests__/schema-unique-indexes.test.ts +39 -0
  53. package/src/modules/ai_assistant/data/entities.ts +5 -0
  54. package/src/modules/ai_assistant/lib/__tests__/ai-overrides.test.ts +1 -1
  55. package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +112 -0
  56. package/src/modules/ai_assistant/lib/__tests__/client-factory.test.ts +42 -0
  57. package/src/modules/ai_assistant/lib/__tests__/codemode-tools.test.ts +107 -0
  58. package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +30 -3
  59. package/src/modules/ai_assistant/lib/__tests__/mcp-client.test.ts +20 -1
  60. package/src/modules/ai_assistant/lib/__tests__/mcp-dev-key-resolution.test.ts +92 -0
  61. package/src/modules/ai_assistant/lib/__tests__/mcp-server-auth-fail-closed.test.ts +133 -0
  62. package/src/modules/ai_assistant/lib/__tests__/task-plan-stream.test.ts +1 -1
  63. package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +9 -0
  64. package/src/modules/ai_assistant/lib/ai-overrides.ts +1 -1
  65. package/src/modules/ai_assistant/lib/ai-sdk.ts +1 -1
  66. package/src/modules/ai_assistant/lib/attachment-parts.ts +26 -5
  67. package/src/modules/ai_assistant/lib/chat-config.ts +1 -1
  68. package/src/modules/ai_assistant/lib/client-factory.ts +3 -9
  69. package/src/modules/ai_assistant/lib/codemode-tools.ts +49 -0
  70. package/src/modules/ai_assistant/lib/generated-registry-loader.ts +35 -3
  71. package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +1 -1
  72. package/src/modules/ai_assistant/lib/llm-adapters/google.ts +1 -1
  73. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +1 -1
  74. package/src/modules/ai_assistant/lib/llm-bootstrap.ts +1 -1
  75. package/src/modules/ai_assistant/lib/mcp-client.ts +9 -5
  76. package/src/modules/ai_assistant/lib/mcp-dev-key-resolution.ts +107 -0
  77. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +1 -36
  78. package/src/modules/ai_assistant/lib/mcp-server.ts +47 -33
  79. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +1 -1
  80. package/src/modules/ai_assistant/lib/task-plan-stream.ts +1 -1
  81. package/src/modules/ai_assistant/lib/types.ts +7 -0
  82. package/src/modules/ai_assistant/setup.ts +11 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/generated-registry-loader.ts"],
4
- "sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\n/**\n * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without\n * hardcoding the workspace layout. Searches upward from this module's compiled\n * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then\n * falls back to cwd-relative lookups (standalone apps run from the app dir).\n */\nexport function findGeneratedFile(fileName: string): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n\n if (here) {\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n }\n // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone\n // app whose root holds `.mercato/generated`).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import a generated registry file on the fly. Resolves the `@/`\n * alias to the app root, transpiles TS \u2192 ESM, and emits a sibling `.mjs` we can\n * `import()` from Node. Cached on mtime so repeat calls in the same process\n * don't recompile.\n *\n * Transpile-only (no bundling): the generated registries declare an array\n * literal whose entries are static `import(\"\u2026\")` arrow functions \u2014 we want\n * those `import()` strings to stay as runtime imports so Node resolves them\n * lazily through the workspace's normal module resolution. Eagerly bundling\n * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime\n * (e.g. `next/server` package-exports map).\n */\nexport async function compileAndImportGenerated(tsPath: string): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n if (!fs.existsSync(tsPath)) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n const esbuild = await import('esbuild')\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n const aliasRewritten = rewriteGeneratedAliasImports(tsSource, appRoot)\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n\n return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>\n}\n\n/**\n * Rewrite `@/...` path-alias imports (both `from \"@/x\"` and dynamic\n * `import(\"@/x\")`) in generated-registry source to absolute `file://` URLs\n * rooted at `appRoot`. The `@/` alias is a Next.js bundler convention; outside\n * the bundler Node treats `@/...` as a bare package specifier and throws\n * `ERR_MODULE_NOT_FOUND`. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(source: string, appRoot: string): string {\n const resolveAlias = (relativePath: string): string => {\n const target = path.join(appRoot, relativePath)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return pathToFileURL(candidate).href\n }\n return source\n .replace(/from\\s+[\"']@\\/([^\"']+)[\"']/g, (_match, relativePath: string) => {\n return `from ${JSON.stringify(resolveAlias(relativePath))}`\n })\n .replace(/import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g, (_match, relativePath: string) => {\n return `import(${JSON.stringify(resolveAlias(relativePath))})`\n })\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many module tools are \"API-backed\" \u2014 their handlers\n * delegate to `createAiApiOperationRunner`, which fails closed with\n * \"No API route manifest registered\" unless the manifest is present. In the\n * Next.js app this is wired at bootstrap, but the standalone MCP servers\n * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.\n *\n * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so\n * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the\n * number of registered routes (0 when the generated file is absent).\n */\nexport async function ensureApiRouteManifestsRegistered(): Promise<number> {\n const registry = await import('@open-mercato/shared/modules/registry')\n // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the\n // existing manifest untouched so we never interfere with the in-app agents\n // framework, which registers it at bootstrap with its own override pipeline.\n const existing = registry.getApiRouteManifests()\n if (existing.length > 0) return existing.length\n\n const tsPath = findGeneratedFile('api-routes.generated.ts')\n if (!tsPath) return 0\n try {\n const mod = await compileAndImportGenerated(tsPath)\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) return 0\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n return apiRoutes.length\n } catch (error) {\n console.warn(\n '[MCP Tools] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n return 0\n }\n}\n"],
5
- "mappings": "AAgBA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,eAAe,qBAAqB;AAQtC,SAAS,kBAAkB,UAAiC;AACjE,QAAM,QAAQ,MAAM;AAClB,QAAI;AACF,aAAO,cAAc,YAAY,GAAG;AAAA,IACtC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,MAAI,MAAM;AACR,QAAI,SAAS,KAAK,QAAQ,IAAI;AAC9B,aAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,YAAM,YAAY,KAAK,KAAK,QAAQ,QAAQ,WAAW,YAAY,aAAa,QAAQ;AACxF,UAAI,GAAG,WAAW,SAAS,EAAG,QAAO;AACrC,YAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,UAAI,SAAS,OAAQ;AACrB,eAAS;AAAA,IACX;AAAA,EACF;AAGA,QAAM,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,QAAQ,WAAW,YAAY,aAAa,QAAQ;AAChG,MAAI,GAAG,WAAW,OAAO,EAAG,QAAO;AACnC,QAAM,gBAAgB,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,aAAa,QAAQ;AACnF,MAAI,GAAG,WAAW,aAAa,EAAG,QAAO;AACzC,SAAO;AACT;AAeA,eAAsB,0BAA0B,QAAkD;AAChG,QAAM,SAAS,OAAO,QAAQ,SAAS,MAAM;AAE7C,QAAM,UAAU,KAAK,QAAQ,KAAK,QAAQ,KAAK,QAAQ,MAAM,CAAC,CAAC;AAE/D,MAAI,CAAC,GAAG,WAAW,MAAM,GAAG;AAC1B,UAAM,IAAI,MAAM,6BAA6B,MAAM,EAAE;AAAA,EACvD;AAEA,QAAM,WAAW,GAAG,WAAW,MAAM;AACrC,QAAM,eACJ,CAAC,YAAY,GAAG,SAAS,MAAM,EAAE,UAAU,GAAG,SAAS,MAAM,EAAE;AAEjE,MAAI,cAAc;AAChB,UAAM,UAAU,MAAM,OAAO,SAAS;AACtC,UAAM,WAAW,GAAG,aAAa,QAAQ,OAAO;AAChD,UAAM,iBAAiB,6BAA6B,UAAU,OAAO;AACrE,UAAM,SAAS,MAAM,QAAQ,UAAU,gBAAgB;AAAA,MACrD,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,YAAY;AAAA,IACd,CAAC;AACD,OAAG,cAAc,QAAQ,OAAO,IAAI;AAAA,EACtC;AAEA,SAAQ,MAAM,OAAO,cAAc,MAAM,EAAE;AAC7C;AASO,SAAS,6BAA6B,QAAgB,SAAyB;AACpF,QAAM,eAAe,CAAC,iBAAiC;AACrD,UAAM,SAAS,KAAK,KAAK,SAAS,YAAY;AAC9C,UAAM,YAAY,GAAG,WAAW,MAAM,IAClC,SACA,GAAG,WAAW,SAAS,KAAK,IAC1B,SAAS,QACT;AACN,WAAO,cAAc,SAAS,EAAE;AAAA,EAClC;AACA,SAAO,OACJ,QAAQ,+BAA+B,CAAC,QAAQ,iBAAyB;AACxE,WAAO,QAAQ,KAAK,UAAU,aAAa,YAAY,CAAC,CAAC;AAAA,EAC3D,CAAC,EACA,QAAQ,2CAA2C,CAAC,QAAQ,iBAAyB;AACpF,WAAO,UAAU,KAAK,UAAU,aAAa,YAAY,CAAC,CAAC;AAAA,EAC7D,CAAC;AACL;AAcA,eAAsB,oCAAqD;AACzE,QAAM,WAAW,MAAM,OAAO,uCAAuC;AAIrE,QAAM,WAAW,SAAS,qBAAqB;AAC/C,MAAI,SAAS,SAAS,EAAG,QAAO,SAAS;AAEzC,QAAM,SAAS,kBAAkB,yBAAyB;AAC1D,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI;AACF,UAAM,MAAM,MAAM,0BAA0B,MAAM;AAClD,UAAM,YAAa,IAAgC;AACnD,QAAI,CAAC,MAAM,QAAQ,SAAS,EAAG,QAAO;AACtC,aAAS;AAAA,MACP;AAAA,IACF;AACA,WAAO,UAAU;AAAA,EACnB,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AACA,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\n/**\n * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without\n * hardcoding the workspace layout. Searches upward from this module's compiled\n * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then\n * falls back to cwd-relative lookups (standalone apps run from the app dir).\n */\nexport function findGeneratedFile(fileName: string): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n\n if (here) {\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n }\n // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone\n // app whose root holds `.mercato/generated`).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import a generated registry file on the fly. Resolves the `@/`\n * alias to the app root, transpiles TS \u2192 ESM, and emits a sibling `.mjs` we can\n * `import()` from Node. Cached on mtime so repeat calls in the same process\n * don't recompile.\n *\n * Transpile-only (no bundling): the generated registries declare an array\n * literal whose entries are static `import(\"\u2026\")` arrow functions \u2014 we want\n * those `import()` strings to stay as runtime imports so Node resolves them\n * lazily through the workspace's normal module resolution. Eagerly bundling\n * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime\n * (e.g. `next/server` package-exports map).\n */\nexport async function compileAndImportGenerated(tsPath: string): Promise<Record<string, unknown>> {\n const jsPath = tsPath.replace(/\\.ts$/, '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n if (!fs.existsSync(tsPath)) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n const esbuild = await import('esbuild')\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n const aliasRewritten = rewriteGeneratedAliasImports(tsSource, appRoot)\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n\n return (await import(pathToFileURL(jsPath).href)) as Record<string, unknown>\n}\n\nconst UNSAFE_JS_STRING_CHAR_ESCAPES: Record<number, string> = {\n 0x3c: '\\\\u003C', // < \u2014 HTML/script-tag breakout\n 0x3e: '\\\\u003E', // > \u2014 HTML/script-tag breakout\n 0x2028: '\\\\u2028', // line separator \u2014 string content but a statement terminator pre-ES2019\n 0x2029: '\\\\u2029', // paragraph separator \u2014 same\n}\n\n/**\n * Escape characters that `JSON.stringify` leaves intact but which can still\n * break out of (or alter the meaning of) the JavaScript string literal that the\n * stringified value is embedded into \u2014 notably `<`/`>` (HTML/script-tag\n * breakout) and the U+2028 / U+2029 line separators (valid string content but\n * statement terminators in pre-ES2019 parsers). Apply this on top of\n * `JSON.stringify` so the emitted import source stays well-formed regardless of\n * the resolved path. Exported for unit testing.\n */\nexport function escapeUnsafeJsStringChars(value: string): string {\n return value.replace(\n /[<>\\u2028\\u2029]/g,\n (char) => UNSAFE_JS_STRING_CHAR_ESCAPES[char.charCodeAt(0)],\n )\n}\n\n/**\n * Stringify a resolved path into a JavaScript string literal that is safe to\n * embed in generated source: `JSON.stringify` handles quoting/standard escapes,\n * and `escapeUnsafeJsStringChars` neutralizes the characters it leaves intact.\n */\nfunction toSafeJsStringLiteral(value: string): string {\n return escapeUnsafeJsStringChars(JSON.stringify(value))\n}\n\n/**\n * Rewrite `@/...` path-alias imports (both `from \"@/x\"` and dynamic\n * `import(\"@/x\")`) in generated-registry source to absolute `file://` URLs\n * rooted at `appRoot`. The `@/` alias is a Next.js bundler convention; outside\n * the bundler Node treats `@/...` as a bare package specifier and throws\n * `ERR_MODULE_NOT_FOUND`. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(source: string, appRoot: string): string {\n const resolveAlias = (relativePath: string): string => {\n const target = path.join(appRoot, relativePath)\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return toSafeJsStringLiteral(pathToFileURL(candidate).href)\n }\n return source\n .replace(/from\\s+[\"']@\\/([^\"']+)[\"']/g, (_match, relativePath: string) => {\n return `from ${resolveAlias(relativePath)}`\n })\n .replace(/import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g, (_match, relativePath: string) => {\n return `import(${resolveAlias(relativePath)})`\n })\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many module tools are \"API-backed\" \u2014 their handlers\n * delegate to `createAiApiOperationRunner`, which fails closed with\n * \"No API route manifest registered\" unless the manifest is present. In the\n * Next.js app this is wired at bootstrap, but the standalone MCP servers\n * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.\n *\n * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so\n * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the\n * number of registered routes (0 when the generated file is absent).\n */\nexport async function ensureApiRouteManifestsRegistered(): Promise<number> {\n const registry = await import('@open-mercato/shared/modules/registry')\n // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the\n // existing manifest untouched so we never interfere with the in-app agents\n // framework, which registers it at bootstrap with its own override pipeline.\n const existing = registry.getApiRouteManifests()\n if (existing.length > 0) return existing.length\n\n const tsPath = findGeneratedFile('api-routes.generated.ts')\n if (!tsPath) return 0\n try {\n const mod = await compileAndImportGenerated(tsPath)\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) return 0\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n return apiRoutes.length\n } catch (error) {\n console.warn(\n '[MCP Tools] Could not register api-routes manifest:',\n error instanceof Error ? error.message : error,\n )\n return 0\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/lib/llm-adapters/anthropic.ts"],
4
- "sourcesContent": ["/**\n * AnthropicAdapter \u2014 implements the LlmProvider port for Anthropic's\n * Messages API (Claude Haiku, Sonnet, Opus).\n *\n * Wraps `createAnthropic({ apiKey })` from `@ai-sdk/anthropic` and exposes\n * a curated model list for the AI Assistant UI dropdown.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createAnthropic } from '@ai-sdk/anthropic'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\nconst DEFAULT_MODEL = 'claude-haiku-4-5-20251001'\n\nconst DEFAULT_MODELS: readonly LlmModelInfo[] = [\n {\n id: 'claude-haiku-4-5-20251001',\n name: 'Claude Haiku 4.5',\n contextWindow: 200000,\n tags: ['budget'],\n },\n {\n id: 'claude-sonnet-4-6-20260107',\n name: 'Claude Sonnet 4.6',\n contextWindow: 200000,\n tags: ['flagship'],\n },\n {\n id: 'claude-opus-4-6-20260107',\n name: 'Claude Opus 4.6',\n contextWindow: 1000000,\n tags: ['flagship', 'reasoning'],\n },\n] as const\n\n/**\n * Factory returning a fresh `AnthropicAdapter` instance. The adapter is\n * stateless \u2014 caller is free to reuse the returned object.\n *\n * `createModel` accepts an optional `baseURL` from {@link LlmCreateModelOptions}\n * and forwards it to `createAnthropic({ apiKey, baseURL })`.\n *\n * baseURL only works for Anthropic Messages-protocol relays (Cloudflare AI\n * Gateway in Anthropic mode, Helicone proxy). For OpenAI-format gateways,\n * use the OpenAI / OpenRouter presets.\n */\nexport function createAnthropicAdapter(): LlmProvider {\n const envKeys = ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'] as const\n\n function resolveApiKey(env?: EnvLookup): string | null {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n }\n\n return {\n id: 'anthropic',\n name: 'Anthropic',\n envKeys,\n defaultModel: DEFAULT_MODEL,\n defaultModels: DEFAULT_MODELS,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n const anthropic = createAnthropic({\n apiKey: options.apiKey,\n ...(options.baseURL ? { baseURL: options.baseURL } : {}),\n })\n return anthropic(options.modelId)\n },\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * AnthropicAdapter \u2014 implements the LlmProvider port for Anthropic's\n * Messages API (Claude Haiku, Sonnet, Opus).\n *\n * Wraps `createAnthropic({ apiKey })` from `@ai-sdk/anthropic` and exposes\n * a curated model list for the AI Assistant UI dropdown.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createAnthropic } from '@ai-sdk/anthropic'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\nconst DEFAULT_MODEL = 'claude-haiku-4-5-20251001'\n\nconst DEFAULT_MODELS: readonly LlmModelInfo[] = [\n {\n id: 'claude-haiku-4-5-20251001',\n name: 'Claude Haiku 4.5',\n contextWindow: 200000,\n tags: ['budget'],\n },\n {\n id: 'claude-sonnet-4-6-20260107',\n name: 'Claude Sonnet 4.6',\n contextWindow: 200000,\n tags: ['flagship'],\n },\n {\n id: 'claude-opus-4-6-20260107',\n name: 'Claude Opus 4.6',\n contextWindow: 1000000,\n tags: ['flagship', 'reasoning'],\n },\n] as const\n\n/**\n * Factory returning a fresh `AnthropicAdapter` instance. The adapter is\n * stateless \u2014 caller is free to reuse the returned object.\n *\n * `createModel` accepts an optional `baseURL` from {@link LlmCreateModelOptions}\n * and forwards it to `createAnthropic({ apiKey, baseURL })`.\n *\n * baseURL only works for Anthropic Messages-protocol relays (Cloudflare AI\n * Gateway in Anthropic mode, Helicone proxy). For OpenAI-format gateways,\n * use the OpenAI / OpenRouter presets.\n */\nexport function createAnthropicAdapter(): LlmProvider {\n const envKeys = ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'] as const\n\n function resolveApiKey(env?: EnvLookup): string | null {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n }\n\n return {\n id: 'anthropic',\n name: 'Anthropic',\n envKeys,\n defaultModel: DEFAULT_MODEL,\n defaultModels: DEFAULT_MODELS,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n const anthropic = createAnthropic({\n apiKey: options.apiKey,\n ...(options.baseURL ? { baseURL: options.baseURL } : {}),\n })\n return anthropic(options.modelId)\n },\n }\n}\n"],
5
5
  "mappings": "AAWA,SAAS,uBAAuB;AAQhC,MAAM,gBAAgB;AAEtB,MAAM,iBAA0C;AAAA,EAC9C;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,QAAQ;AAAA,EACjB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,UAAU;AAAA,EACnB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,YAAY,WAAW;AAAA,EAChC;AACF;AAaO,SAAS,yBAAsC;AACpD,QAAM,UAAU,CAAC,qBAAqB,4BAA4B;AAElE,WAAS,cAAc,KAAgC;AACrD,UAAM,SAAS,OAAO,QAAQ;AAC9B,eAAW,OAAO,SAAS;AACzB,YAAM,QAAQ,OAAO,GAAG;AACxB,UAAI,OAAO,UAAU,UAAU;AAC7B,cAAM,UAAU,MAAM,KAAK;AAC3B,YAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,MACjC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN;AAAA,IACA,cAAc;AAAA,IACd,eAAe;AAAA,IAEf,aAAa,KAA0B;AACrC,aAAO,cAAc,GAAG,MAAM;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,oBAAoB,KAAyB;AAC3C,YAAM,SAAS,OAAO,QAAQ;AAC9B,iBAAW,OAAO,SAAS;AACzB,cAAM,QAAQ,OAAO,GAAG;AACxB,YAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,QAAQ,CAAC;AAAA,IAClB;AAAA,IAEA,YAAY,SAAyC;AACnD,YAAM,YAAY,gBAAgB;AAAA,QAChC,QAAQ,QAAQ;AAAA,QAChB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,MACxD,CAAC;AACD,aAAO,UAAU,QAAQ,OAAO;AAAA,IAClC;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/lib/llm-adapters/google.ts"],
4
- "sourcesContent": ["/**\n * GoogleAdapter \u2014 implements the LlmProvider port for Google's Generative\n * AI API (Gemini Flash, Pro).\n *\n * Wraps `createGoogleGenerativeAI({ apiKey })` from `@ai-sdk/google`.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createGoogleGenerativeAI } from '@ai-sdk/google'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\nconst DEFAULT_MODEL = 'gemini-3-flash'\n\nconst DEFAULT_MODELS: readonly LlmModelInfo[] = [\n {\n id: 'gemini-3-flash',\n name: 'Gemini 3 Flash',\n contextWindow: 1048576,\n tags: ['budget'],\n },\n {\n id: 'gemini-3-pro',\n name: 'Gemini 3 Pro',\n contextWindow: 1048576,\n tags: ['flagship'],\n },\n] as const\n\nexport function createGoogleAdapter(): LlmProvider {\n const envKeys = ['GOOGLE_GENERATIVE_AI_API_KEY', 'OPENCODE_GOOGLE_API_KEY'] as const\n\n function resolveApiKey(env?: EnvLookup): string | null {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n }\n\n return {\n id: 'google',\n name: 'Google',\n envKeys,\n defaultModel: DEFAULT_MODEL,\n defaultModels: DEFAULT_MODELS,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n const google = createGoogleGenerativeAI({\n apiKey: options.apiKey,\n ...(options.baseURL ? { baseURL: options.baseURL } : {}),\n })\n return google(options.modelId)\n },\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * GoogleAdapter \u2014 implements the LlmProvider port for Google's Generative\n * AI API (Gemini Flash, Pro).\n *\n * Wraps `createGoogleGenerativeAI({ apiKey })` from `@ai-sdk/google`.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createGoogleGenerativeAI } from '@ai-sdk/google'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\nconst DEFAULT_MODEL = 'gemini-3-flash'\n\nconst DEFAULT_MODELS: readonly LlmModelInfo[] = [\n {\n id: 'gemini-3-flash',\n name: 'Gemini 3 Flash',\n contextWindow: 1048576,\n tags: ['budget'],\n },\n {\n id: 'gemini-3-pro',\n name: 'Gemini 3 Pro',\n contextWindow: 1048576,\n tags: ['flagship'],\n },\n] as const\n\nexport function createGoogleAdapter(): LlmProvider {\n const envKeys = ['GOOGLE_GENERATIVE_AI_API_KEY', 'OPENCODE_GOOGLE_API_KEY'] as const\n\n function resolveApiKey(env?: EnvLookup): string | null {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n }\n\n return {\n id: 'google',\n name: 'Google',\n envKeys,\n defaultModel: DEFAULT_MODEL,\n defaultModels: DEFAULT_MODELS,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n const google = createGoogleGenerativeAI({\n apiKey: options.apiKey,\n ...(options.baseURL ? { baseURL: options.baseURL } : {}),\n })\n return google(options.modelId)\n },\n }\n}\n"],
5
5
  "mappings": "AAUA,SAAS,gCAAgC;AAQzC,MAAM,gBAAgB;AAEtB,MAAM,iBAA0C;AAAA,EAC9C;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,QAAQ;AAAA,EACjB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,UAAU;AAAA,EACnB;AACF;AAEO,SAAS,sBAAmC;AACjD,QAAM,UAAU,CAAC,gCAAgC,yBAAyB;AAE1E,WAAS,cAAc,KAAgC;AACrD,UAAM,SAAS,OAAO,QAAQ;AAC9B,eAAW,OAAO,SAAS;AACzB,YAAM,QAAQ,OAAO,GAAG;AACxB,UAAI,OAAO,UAAU,UAAU;AAC7B,cAAM,UAAU,MAAM,KAAK;AAC3B,YAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,MACjC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN;AAAA,IACA,cAAc;AAAA,IACd,eAAe;AAAA,IAEf,aAAa,KAA0B;AACrC,aAAO,cAAc,GAAG,MAAM;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,oBAAoB,KAAyB;AAC3C,YAAM,SAAS,OAAO,QAAQ;AAC9B,iBAAW,OAAO,SAAS;AACzB,cAAM,QAAQ,OAAO,GAAG;AACxB,YAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,QAAQ,CAAC;AAAA,IAClB;AAAA,IAEA,YAAY,SAAyC;AACnD,YAAM,SAAS,yBAAyB;AAAA,QACtC,QAAQ,QAAQ;AAAA,QAChB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,MACxD,CAAC;AACD,aAAO,OAAO,QAAQ,OAAO;AAAA,IAC/B;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/lib/llm-adapters/openai.ts"],
4
- "sourcesContent": ["/**\n * OpenAIAdapter \u2014 implements the LlmProvider port for the OpenAI\n * chat-completions protocol.\n *\n * This single adapter serves OpenAI itself and every OpenAI-compatible\n * backend (DeepInfra, Groq, Together, Fireworks, Azure OpenAI, LiteLLM,\n * Ollama, LocalAI, vLLM, \u2026). Vendor-specific configuration \u2014 endpoint\n * URL, available models, env var conventions \u2014 lives in\n * `./openai-compatible-presets.ts` as plain data, not code.\n *\n * The factory {@link createOpenAICompatibleProvider} takes a preset and\n * returns a fully-configured `LlmProvider` that internally calls\n * `createOpenAI({ apiKey, baseURL })` from `@ai-sdk/openai`.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see ./openai-compatible-presets.ts\n * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createOpenAI } from '@ai-sdk/openai'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\n/**\n * Configuration for a single OpenAI-compatible provider instance.\n *\n * Built-in presets live in {@link ./openai-compatible-presets.ts}.\n * Downstream applications may construct custom presets at bootstrap time\n * and register them with `llmProviderRegistry.register(\n * createOpenAICompatibleProvider(customPreset),\n * )`.\n */\nexport interface OpenAICompatiblePreset {\n /** Stable id (e.g. `openai`, `deepinfra`, `groq`, `together`). */\n id: string\n /** Human-readable display name. */\n name: string\n /**\n * Upstream base URL. Leave `undefined` to use the AI SDK default\n * (`https://api.openai.com/v1`). Required for DeepInfra, Groq, etc.\n */\n baseURL?: string\n /**\n * Env var names where the adapter looks for the API key, in priority\n * order. Each preset declares its own keys so unrelated presets never\n * accidentally share credentials (e.g. DeepInfra uses\n * `DEEPINFRA_API_KEY`, not `OPENAI_API_KEY`).\n */\n envKeys: readonly string[]\n /** Default model id used when the caller does not specify one. */\n defaultModel: string\n /** Curated model catalog shown in the UI dropdown. */\n defaultModels: readonly LlmModelInfo[]\n /**\n * Optional env var names for overriding the base URL at runtime.\n * Primarily used by presets that rely on a user-supplied URL\n * (Azure deployment, self-hosted LiteLLM, Ollama on a custom port).\n * The first non-empty value wins and overrides {@link baseURL}.\n */\n baseURLEnvKeys?: readonly string[]\n}\n\nfunction readFirstNonEmpty(\n env: EnvLookup,\n keys: readonly string[],\n): string | null {\n for (const key of keys) {\n const value = env[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n}\n\n/**\n * Builds a `LlmProvider` instance bound to a specific OpenAI-compatible\n * preset. The returned object is stateless and can be registered directly\n * with `llmProviderRegistry.register(...)`.\n */\nexport function createOpenAICompatibleProvider(\n preset: OpenAICompatiblePreset,\n): LlmProvider {\n if (!preset.id || preset.id.length === 0) {\n throw new Error('[OpenAIAdapter] Preset must have a non-empty id')\n }\n if (!preset.envKeys || preset.envKeys.length === 0) {\n throw new Error(\n `[OpenAIAdapter] Preset \"${preset.id}\" must declare at least one env key`,\n )\n }\n\n function resolveApiKey(env?: EnvLookup): string | null {\n return readFirstNonEmpty(env ?? process.env, preset.envKeys)\n }\n\n function resolveBaseURL(env?: EnvLookup): string | undefined {\n const lookup = env ?? process.env\n if (preset.baseURLEnvKeys && preset.baseURLEnvKeys.length > 0) {\n const override = readFirstNonEmpty(lookup, preset.baseURLEnvKeys)\n if (override) return override\n }\n return preset.baseURL\n }\n\n return {\n id: preset.id,\n name: preset.name,\n envKeys: preset.envKeys,\n defaultModel: preset.defaultModel,\n defaultModels: preset.defaultModels,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of preset.envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return preset.envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n // Per-request baseURL override wins over preset/env defaults.\n const baseURL = options.baseURL ?? resolveBaseURL()\n const openai = createOpenAI({\n apiKey: options.apiKey,\n ...(baseURL ? { baseURL } : {}),\n })\n return openai(options.modelId)\n },\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * OpenAIAdapter \u2014 implements the LlmProvider port for the OpenAI\n * chat-completions protocol.\n *\n * This single adapter serves OpenAI itself and every OpenAI-compatible\n * backend (DeepInfra, Groq, Together, Fireworks, Azure OpenAI, LiteLLM,\n * Ollama, LocalAI, vLLM, \u2026). Vendor-specific configuration \u2014 endpoint\n * URL, available models, env var conventions \u2014 lives in\n * `./openai-compatible-presets.ts` as plain data, not code.\n *\n * The factory {@link createOpenAICompatibleProvider} takes a preset and\n * returns a fully-configured `LlmProvider` that internally calls\n * `createOpenAI({ apiKey, baseURL })` from `@ai-sdk/openai`.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see ./openai-compatible-presets.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createOpenAI } from '@ai-sdk/openai'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\n/**\n * Configuration for a single OpenAI-compatible provider instance.\n *\n * Built-in presets live in {@link ./openai-compatible-presets.ts}.\n * Downstream applications may construct custom presets at bootstrap time\n * and register them with `llmProviderRegistry.register(\n * createOpenAICompatibleProvider(customPreset),\n * )`.\n */\nexport interface OpenAICompatiblePreset {\n /** Stable id (e.g. `openai`, `deepinfra`, `groq`, `together`). */\n id: string\n /** Human-readable display name. */\n name: string\n /**\n * Upstream base URL. Leave `undefined` to use the AI SDK default\n * (`https://api.openai.com/v1`). Required for DeepInfra, Groq, etc.\n */\n baseURL?: string\n /**\n * Env var names where the adapter looks for the API key, in priority\n * order. Each preset declares its own keys so unrelated presets never\n * accidentally share credentials (e.g. DeepInfra uses\n * `DEEPINFRA_API_KEY`, not `OPENAI_API_KEY`).\n */\n envKeys: readonly string[]\n /** Default model id used when the caller does not specify one. */\n defaultModel: string\n /** Curated model catalog shown in the UI dropdown. */\n defaultModels: readonly LlmModelInfo[]\n /**\n * Optional env var names for overriding the base URL at runtime.\n * Primarily used by presets that rely on a user-supplied URL\n * (Azure deployment, self-hosted LiteLLM, Ollama on a custom port).\n * The first non-empty value wins and overrides {@link baseURL}.\n */\n baseURLEnvKeys?: readonly string[]\n}\n\nfunction readFirstNonEmpty(\n env: EnvLookup,\n keys: readonly string[],\n): string | null {\n for (const key of keys) {\n const value = env[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n}\n\n/**\n * Builds a `LlmProvider` instance bound to a specific OpenAI-compatible\n * preset. The returned object is stateless and can be registered directly\n * with `llmProviderRegistry.register(...)`.\n */\nexport function createOpenAICompatibleProvider(\n preset: OpenAICompatiblePreset,\n): LlmProvider {\n if (!preset.id || preset.id.length === 0) {\n throw new Error('[OpenAIAdapter] Preset must have a non-empty id')\n }\n if (!preset.envKeys || preset.envKeys.length === 0) {\n throw new Error(\n `[OpenAIAdapter] Preset \"${preset.id}\" must declare at least one env key`,\n )\n }\n\n function resolveApiKey(env?: EnvLookup): string | null {\n return readFirstNonEmpty(env ?? process.env, preset.envKeys)\n }\n\n function resolveBaseURL(env?: EnvLookup): string | undefined {\n const lookup = env ?? process.env\n if (preset.baseURLEnvKeys && preset.baseURLEnvKeys.length > 0) {\n const override = readFirstNonEmpty(lookup, preset.baseURLEnvKeys)\n if (override) return override\n }\n return preset.baseURL\n }\n\n return {\n id: preset.id,\n name: preset.name,\n envKeys: preset.envKeys,\n defaultModel: preset.defaultModel,\n defaultModels: preset.defaultModels,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of preset.envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return preset.envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n // Per-request baseURL override wins over preset/env defaults.\n const baseURL = options.baseURL ?? resolveBaseURL()\n const openai = createOpenAI({\n apiKey: options.apiKey,\n ...(baseURL ? { baseURL } : {}),\n })\n return openai(options.modelId)\n },\n }\n}\n"],
5
5
  "mappings": "AAmBA,SAAS,oBAAoB;AA+C7B,SAAS,kBACP,KACA,MACe;AACf,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,IAAI,GAAG;AACrB,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM,UAAU,MAAM,KAAK;AAC3B,UAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,+BACd,QACa;AACb,MAAI,CAAC,OAAO,MAAM,OAAO,GAAG,WAAW,GAAG;AACxC,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,MAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,WAAW,GAAG;AAClD,UAAM,IAAI;AAAA,MACR,2BAA2B,OAAO,EAAE;AAAA,IACtC;AAAA,EACF;AAEA,WAAS,cAAc,KAAgC;AACrD,WAAO,kBAAkB,OAAO,QAAQ,KAAK,OAAO,OAAO;AAAA,EAC7D;AAEA,WAAS,eAAe,KAAqC;AAC3D,UAAM,SAAS,OAAO,QAAQ;AAC9B,QAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC7D,YAAM,WAAW,kBAAkB,QAAQ,OAAO,cAAc;AAChE,UAAI,SAAU,QAAO;AAAA,IACvB;AACA,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AAAA,IACL,IAAI,OAAO;AAAA,IACX,MAAM,OAAO;AAAA,IACb,SAAS,OAAO;AAAA,IAChB,cAAc,OAAO;AAAA,IACrB,eAAe,OAAO;AAAA,IAEtB,aAAa,KAA0B;AACrC,aAAO,cAAc,GAAG,MAAM;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,oBAAoB,KAAyB;AAC3C,YAAM,SAAS,OAAO,QAAQ;AAC9B,iBAAW,OAAO,OAAO,SAAS;AAChC,cAAM,QAAQ,OAAO,GAAG;AACxB,YAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,OAAO,QAAQ,CAAC;AAAA,IACzB;AAAA,IAEA,YAAY,SAAyC;AAEnD,YAAM,UAAU,QAAQ,WAAW,eAAe;AAClD,YAAM,SAAS,aAAa;AAAA,QAC1B,QAAQ,QAAQ;AAAA,QAChB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC/B,CAAC;AACD,aAAO,OAAO,QAAQ,OAAO;AAAA,IAC/B;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/llm-bootstrap.ts"],
4
- "sourcesContent": ["/**\n * LLM provider bootstrap \u2014 registers built-in adapters and OpenAI-compatible\n * presets with the shared `llmProviderRegistry` singleton.\n *\n * This runs at module load via the side-effect import in `./ai-sdk.ts`.\n * Safe to call multiple times \u2014 each registration is idempotent (replaces\n * existing by id), so Next.js hot-reload does not duplicate providers.\n *\n * Adapter registration is wrapped in individual try/catch blocks. A\n * failing import (e.g. missing peer dependency) skips that adapter with a\n * `console.warn` but leaves the rest of the registry working.\n *\n * @see packages/shared/src/lib/ai/llm-provider-registry.ts\n * @see .ai/specs/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { createAnthropicAdapter } from './llm-adapters/anthropic'\nimport { createGoogleAdapter } from './llm-adapters/google'\nimport { createOpenAICompatibleProvider } from './llm-adapters/openai'\nimport { OPENAI_COMPATIBLE_PRESETS } from './openai-compatible-presets'\n\nlet bootstrapped = false\n\n/**\n * Registers all built-in LLM providers with the shared singleton.\n * Idempotent \u2014 the second and subsequent calls are no-ops (unless the\n * registry was reset by a test, in which case registration runs again).\n */\nexport function registerBuiltInLlmProviders(): void {\n if (bootstrapped && llmProviderRegistry.list().length > 0) {\n return\n }\n\n // Native protocol adapters.\n try {\n llmProviderRegistry.register(createAnthropicAdapter())\n } catch (error) {\n console.warn(\n '[LlmBootstrap] Failed to register Anthropic adapter:',\n error instanceof Error ? error.message : error,\n )\n }\n\n try {\n llmProviderRegistry.register(createGoogleAdapter())\n } catch (error) {\n console.warn(\n '[LlmBootstrap] Failed to register Google adapter:',\n error instanceof Error ? error.message : error,\n )\n }\n\n // OpenAI-compatible presets \u2014 all share one protocol adapter under the\n // hood but appear as separate providers in the registry.\n for (const preset of OPENAI_COMPATIBLE_PRESETS) {\n try {\n llmProviderRegistry.register(createOpenAICompatibleProvider(preset))\n } catch (error) {\n console.warn(\n `[LlmBootstrap] Failed to register OpenAI-compatible preset \"${preset.id}\":`,\n error instanceof Error ? error.message : error,\n )\n }\n }\n\n bootstrapped = true\n}\n\n/**\n * Resets the bootstrap state. Intended for tests that call\n * `llmProviderRegistry.reset()` and then want a fresh bootstrap run.\n */\nexport function resetLlmBootstrapState(): void {\n bootstrapped = false\n}\n\n// Auto-bootstrap on module load so any consumer importing from\n// `@open-mercato/ai-assistant/lib/llm-bootstrap` triggers registration.\nregisterBuiltInLlmProviders()\n"],
4
+ "sourcesContent": ["/**\n * LLM provider bootstrap \u2014 registers built-in adapters and OpenAI-compatible\n * presets with the shared `llmProviderRegistry` singleton.\n *\n * This runs at module load via the side-effect import in `./ai-sdk.ts`.\n * Safe to call multiple times \u2014 each registration is idempotent (replaces\n * existing by id), so Next.js hot-reload does not duplicate providers.\n *\n * Adapter registration is wrapped in individual try/catch blocks. A\n * failing import (e.g. missing peer dependency) skips that adapter with a\n * `console.warn` but leaves the rest of the registry working.\n *\n * @see packages/shared/src/lib/ai/llm-provider-registry.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { createAnthropicAdapter } from './llm-adapters/anthropic'\nimport { createGoogleAdapter } from './llm-adapters/google'\nimport { createOpenAICompatibleProvider } from './llm-adapters/openai'\nimport { OPENAI_COMPATIBLE_PRESETS } from './openai-compatible-presets'\n\nlet bootstrapped = false\n\n/**\n * Registers all built-in LLM providers with the shared singleton.\n * Idempotent \u2014 the second and subsequent calls are no-ops (unless the\n * registry was reset by a test, in which case registration runs again).\n */\nexport function registerBuiltInLlmProviders(): void {\n if (bootstrapped && llmProviderRegistry.list().length > 0) {\n return\n }\n\n // Native protocol adapters.\n try {\n llmProviderRegistry.register(createAnthropicAdapter())\n } catch (error) {\n console.warn(\n '[LlmBootstrap] Failed to register Anthropic adapter:',\n error instanceof Error ? error.message : error,\n )\n }\n\n try {\n llmProviderRegistry.register(createGoogleAdapter())\n } catch (error) {\n console.warn(\n '[LlmBootstrap] Failed to register Google adapter:',\n error instanceof Error ? error.message : error,\n )\n }\n\n // OpenAI-compatible presets \u2014 all share one protocol adapter under the\n // hood but appear as separate providers in the registry.\n for (const preset of OPENAI_COMPATIBLE_PRESETS) {\n try {\n llmProviderRegistry.register(createOpenAICompatibleProvider(preset))\n } catch (error) {\n console.warn(\n `[LlmBootstrap] Failed to register OpenAI-compatible preset \"${preset.id}\":`,\n error instanceof Error ? error.message : error,\n )\n }\n }\n\n bootstrapped = true\n}\n\n/**\n * Resets the bootstrap state. Intended for tests that call\n * `llmProviderRegistry.reset()` and then want a fresh bootstrap run.\n */\nexport function resetLlmBootstrapState(): void {\n bootstrapped = false\n}\n\n// Auto-bootstrap on module load so any consumer importing from\n// `@open-mercato/ai-assistant/lib/llm-bootstrap` triggers registration.\nregisterBuiltInLlmProviders()\n"],
5
5
  "mappings": "AAgBA,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,sCAAsC;AAC/C,SAAS,iCAAiC;AAE1C,IAAI,eAAe;AAOZ,SAAS,8BAAoC;AAClD,MAAI,gBAAgB,oBAAoB,KAAK,EAAE,SAAS,GAAG;AACzD;AAAA,EACF;AAGA,MAAI;AACF,wBAAoB,SAAS,uBAAuB,CAAC;AAAA,EACvD,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AAEA,MAAI;AACF,wBAAoB,SAAS,oBAAoB,CAAC;AAAA,EACpD,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AAIA,aAAW,UAAU,2BAA2B;AAC9C,QAAI;AACF,0BAAoB,SAAS,+BAA+B,MAAM,CAAC;AAAA,IACrE,SAAS,OAAO;AACd,cAAQ;AAAA,QACN,+DAA+D,OAAO,EAAE;AAAA,QACxE,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAEA,iBAAe;AACjB;AAMO,SAAS,yBAA+B;AAC7C,iBAAe;AACjB;AAIA,4BAA4B;",
6
6
  "names": []
7
7
  }
@@ -25,16 +25,14 @@ class McpClient {
25
25
  const args = options.args ?? [
26
26
  "mercato",
27
27
  "ai_assistant",
28
- "mcp:serve",
29
- "--api-key",
30
- options.apiKeySecret
28
+ "mcp:serve"
31
29
  ];
32
30
  const cwd = options.cwd ?? process.cwd();
33
31
  const transport = new StdioClientTransport({
34
32
  command,
35
33
  args,
36
34
  cwd,
37
- env: process.env,
35
+ env: { ...process.env, OPEN_MERCATO_API_KEY: options.apiKeySecret },
38
36
  stderr: "pipe"
39
37
  });
40
38
  transport.stderr?.on("data", (data) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/mcp-client.ts"],
4
- "sourcesContent": ["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { McpClientInterface, ToolInfo, ToolResult } from './types'\n\n/**\n * Options for stdio transport.\n */\nexport type StdioClientOptions = {\n transport: 'stdio'\n /** API key secret (passed to server via --api-key) */\n apiKeySecret: string\n /** Command to run (default: 'yarn') */\n command?: string\n /** Arguments for the command (default: mercato mcp:serve with api-key) */\n args?: string[]\n /** Working directory (default: process.cwd()) */\n cwd?: string\n}\n\n/**\n * Options for HTTP transport.\n */\nexport type HttpClientOptions = {\n transport: 'http'\n /** API key secret (sent via x-api-key header) */\n apiKeySecret: string\n /** MCP server URL (e.g., 'http://localhost:3001/mcp') */\n url: string\n}\n\n/**\n * Combined options for McpClient.\n */\nexport type McpClientOptions = StdioClientOptions | HttpClientOptions\n\n/**\n * MCP protocol client for connecting to MCP servers.\n *\n * Supports two transport modes:\n * - stdio: Spawns server as subprocess\n * - http: Connects to HTTP server\n */\nexport class McpClient implements McpClientInterface {\n private client: Client\n private transport: StdioClientTransport | StreamableHTTPClientTransport\n private apiKeySecret: string\n\n private constructor(\n client: Client,\n transport: StdioClientTransport | StreamableHTTPClientTransport,\n apiKeySecret: string\n ) {\n this.client = client\n this.transport = transport\n this.apiKeySecret = apiKeySecret\n }\n\n /**\n * Connect to an MCP server via the specified transport.\n */\n static async connect(options: McpClientOptions): Promise<McpClient> {\n if (options.transport === 'stdio') {\n return McpClient.connectStdio(options)\n } else {\n return McpClient.connectHttp(options)\n }\n }\n\n /**\n * Connect via stdio transport (spawn subprocess).\n */\n private static async connectStdio(options: StdioClientOptions): Promise<McpClient> {\n const command = options.command ?? 'yarn'\n const args = options.args ?? [\n 'mercato',\n 'ai_assistant',\n 'mcp:serve',\n '--api-key',\n options.apiKeySecret,\n ]\n const cwd = options.cwd ?? process.cwd()\n\n const transport = new StdioClientTransport({\n command,\n args,\n cwd,\n env: process.env as Record<string, string>,\n stderr: 'pipe',\n })\n transport.stderr?.on('data', (data) => {\n const message = data.toString().trim()\n if (message) {\n console.error(`[MCP Client] ${message}`)\n }\n })\n\n const client = new Client(\n { name: 'open-mercato-client', version: '0.1.0' },\n { capabilities: {} }\n )\n\n await client.connect(transport)\n\n return new McpClient(client, transport, options.apiKeySecret)\n }\n\n /**\n * Connect via HTTP transport.\n */\n private static async connectHttp(options: HttpClientOptions): Promise<McpClient> {\n const transport = new StreamableHTTPClientTransport(\n new URL(options.url),\n {\n requestInit: {\n headers: {\n 'x-api-key': options.apiKeySecret,\n },\n },\n }\n )\n\n const client = new Client(\n { name: 'open-mercato-client', version: '0.1.0' },\n { capabilities: {} }\n )\n\n await client.connect(transport)\n\n return new McpClient(client, transport, options.apiKeySecret)\n }\n\n /**\n * List available tools from the server.\n */\n async listTools(): Promise<ToolInfo[]> {\n const response = await this.client.listTools()\n\n return response.tools.map((tool) => ({\n name: tool.name,\n description: tool.description ?? '',\n inputSchema: (tool.inputSchema ?? {}) as Record<string, unknown>,\n }))\n }\n\n /**\n * Call a tool on the server.\n */\n async callTool(name: string, args: unknown): Promise<ToolResult> {\n try {\n const response = await this.client.callTool({\n name,\n arguments: args as Record<string, unknown>,\n })\n\n // Parse content from response\n const content = response.content\n if (!Array.isArray(content) || content.length === 0) {\n return { success: true, result: null }\n }\n\n const firstContent = content[0]\n if (firstContent.type === 'text') {\n try {\n const parsed = JSON.parse(firstContent.text)\n\n // Check if it's an error response\n if (response.isError || parsed.error) {\n return {\n success: false,\n error: parsed.error ?? 'Unknown error',\n }\n }\n\n return { success: true, result: parsed }\n } catch {\n // Not JSON, return as-is\n return { success: true, result: firstContent.text }\n }\n }\n\n return { success: true, result: content }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return { success: false, error: message }\n }\n }\n\n /**\n * Close the client and release resources.\n */\n async close(): Promise<void> {\n try {\n await this.client.close()\n } catch {\n // Ignore close errors\n }\n\n try {\n await this.transport.close()\n } catch {\n // Ignore close errors\n }\n\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AACrC,SAAS,qCAAqC;AAyCvC,MAAM,UAAwC;AAAA,EAK3C,YACN,QACA,WACA,cACA;AACA,SAAK,SAAS;AACd,SAAK,YAAY;AACjB,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,QAAQ,SAA+C;AAClE,QAAI,QAAQ,cAAc,SAAS;AACjC,aAAO,UAAU,aAAa,OAAO;AAAA,IACvC,OAAO;AACL,aAAO,UAAU,YAAY,OAAO;AAAA,IACtC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAqB,aAAa,SAAiD;AACjF,UAAM,UAAU,QAAQ,WAAW;AACnC,UAAM,OAAO,QAAQ,QAAQ;AAAA,MAC3B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,IACV;AACA,UAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AAEvC,UAAM,YAAY,IAAI,qBAAqB;AAAA,MACzC;AAAA,MACA;AAAA,MACA;AAAA,MACA,KAAK,QAAQ;AAAA,MACb,QAAQ;AAAA,IACV,CAAC;AACD,cAAU,QAAQ,GAAG,QAAQ,CAAC,SAAS;AACrC,YAAM,UAAU,KAAK,SAAS,EAAE,KAAK;AACrC,UAAI,SAAS;AACX,gBAAQ,MAAM,gBAAgB,OAAO,EAAE;AAAA,MACzC;AAAA,IACF,CAAC;AAED,UAAM,SAAS,IAAI;AAAA,MACjB,EAAE,MAAM,uBAAuB,SAAS,QAAQ;AAAA,MAChD,EAAE,cAAc,CAAC,EAAE;AAAA,IACrB;AAEA,UAAM,OAAO,QAAQ,SAAS;AAE9B,WAAO,IAAI,UAAU,QAAQ,WAAW,QAAQ,YAAY;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA,EAKA,aAAqB,YAAY,SAAgD;AAC/E,UAAM,YAAY,IAAI;AAAA,MACpB,IAAI,IAAI,QAAQ,GAAG;AAAA,MACnB;AAAA,QACE,aAAa;AAAA,UACX,SAAS;AAAA,YACP,aAAa,QAAQ;AAAA,UACvB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,UAAM,SAAS,IAAI;AAAA,MACjB,EAAE,MAAM,uBAAuB,SAAS,QAAQ;AAAA,MAChD,EAAE,cAAc,CAAC,EAAE;AAAA,IACrB;AAEA,UAAM,OAAO,QAAQ,SAAS;AAE9B,WAAO,IAAI,UAAU,QAAQ,WAAW,QAAQ,YAAY;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAiC;AACrC,UAAM,WAAW,MAAM,KAAK,OAAO,UAAU;AAE7C,WAAO,SAAS,MAAM,IAAI,CAAC,UAAU;AAAA,MACnC,MAAM,KAAK;AAAA,MACX,aAAa,KAAK,eAAe;AAAA,MACjC,aAAc,KAAK,eAAe,CAAC;AAAA,IACrC,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAS,MAAc,MAAoC;AAC/D,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OAAO,SAAS;AAAA,QAC1C;AAAA,QACA,WAAW;AAAA,MACb,CAAC;AAGD,YAAM,UAAU,SAAS;AACzB,UAAI,CAAC,MAAM,QAAQ,OAAO,KAAK,QAAQ,WAAW,GAAG;AACnD,eAAO,EAAE,SAAS,MAAM,QAAQ,KAAK;AAAA,MACvC;AAEA,YAAM,eAAe,QAAQ,CAAC;AAC9B,UAAI,aAAa,SAAS,QAAQ;AAChC,YAAI;AACF,gBAAM,SAAS,KAAK,MAAM,aAAa,IAAI;AAG3C,cAAI,SAAS,WAAW,OAAO,OAAO;AACpC,mBAAO;AAAA,cACL,SAAS;AAAA,cACT,OAAO,OAAO,SAAS;AAAA,YACzB;AAAA,UACF;AAEA,iBAAO,EAAE,SAAS,MAAM,QAAQ,OAAO;AAAA,QACzC,QAAQ;AAEN,iBAAO,EAAE,SAAS,MAAM,QAAQ,aAAa,KAAK;AAAA,QACpD;AAAA,MACF;AAEA,aAAO,EAAE,SAAS,MAAM,QAAQ,QAAQ;AAAA,IAC1C,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,aAAO,EAAE,SAAS,OAAO,OAAO,QAAQ;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,QAAI;AACF,YAAM,KAAK,OAAO,MAAM;AAAA,IAC1B,QAAQ;AAAA,IAER;AAEA,QAAI;AACF,YAAM,KAAK,UAAU,MAAM;AAAA,IAC7B,QAAQ;AAAA,IAER;AAAA,EAEF;AACF;",
4
+ "sourcesContent": ["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { McpClientInterface, ToolInfo, ToolResult } from './types'\n\n/**\n * Options for stdio transport.\n */\nexport type StdioClientOptions = {\n transport: 'stdio'\n /**\n * API key secret. Delivered to the spawned server via the\n * `OPEN_MERCATO_API_KEY` environment variable, never as a command-line\n * argument (argv is world-readable via `ps`/`/proc/<pid>/cmdline`).\n */\n apiKeySecret: string\n /** Command to run (default: 'yarn') */\n command?: string\n /** Arguments for the command (default: mercato ai_assistant mcp:serve, no secret on argv) */\n args?: string[]\n /** Working directory (default: process.cwd()) */\n cwd?: string\n}\n\n/**\n * Options for HTTP transport.\n */\nexport type HttpClientOptions = {\n transport: 'http'\n /** API key secret (sent via x-api-key header) */\n apiKeySecret: string\n /** MCP server URL (e.g., 'http://localhost:3001/mcp') */\n url: string\n}\n\n/**\n * Combined options for McpClient.\n */\nexport type McpClientOptions = StdioClientOptions | HttpClientOptions\n\n/**\n * MCP protocol client for connecting to MCP servers.\n *\n * Supports two transport modes:\n * - stdio: Spawns server as subprocess\n * - http: Connects to HTTP server\n */\nexport class McpClient implements McpClientInterface {\n private client: Client\n private transport: StdioClientTransport | StreamableHTTPClientTransport\n private apiKeySecret: string\n\n private constructor(\n client: Client,\n transport: StdioClientTransport | StreamableHTTPClientTransport,\n apiKeySecret: string\n ) {\n this.client = client\n this.transport = transport\n this.apiKeySecret = apiKeySecret\n }\n\n /**\n * Connect to an MCP server via the specified transport.\n */\n static async connect(options: McpClientOptions): Promise<McpClient> {\n if (options.transport === 'stdio') {\n return McpClient.connectStdio(options)\n } else {\n return McpClient.connectHttp(options)\n }\n }\n\n /**\n * Connect via stdio transport (spawn subprocess).\n */\n private static async connectStdio(options: StdioClientOptions): Promise<McpClient> {\n const command = options.command ?? 'yarn'\n // The API key is passed via OPEN_MERCATO_API_KEY in the child env (below),\n // never on argv \u2014 command-line arguments are readable by any local user.\n const args = options.args ?? [\n 'mercato',\n 'ai_assistant',\n 'mcp:serve',\n ]\n const cwd = options.cwd ?? process.cwd()\n\n const transport = new StdioClientTransport({\n command,\n args,\n cwd,\n env: { ...process.env, OPEN_MERCATO_API_KEY: options.apiKeySecret } as Record<string, string>,\n stderr: 'pipe',\n })\n transport.stderr?.on('data', (data) => {\n const message = data.toString().trim()\n if (message) {\n console.error(`[MCP Client] ${message}`)\n }\n })\n\n const client = new Client(\n { name: 'open-mercato-client', version: '0.1.0' },\n { capabilities: {} }\n )\n\n await client.connect(transport)\n\n return new McpClient(client, transport, options.apiKeySecret)\n }\n\n /**\n * Connect via HTTP transport.\n */\n private static async connectHttp(options: HttpClientOptions): Promise<McpClient> {\n const transport = new StreamableHTTPClientTransport(\n new URL(options.url),\n {\n requestInit: {\n headers: {\n 'x-api-key': options.apiKeySecret,\n },\n },\n }\n )\n\n const client = new Client(\n { name: 'open-mercato-client', version: '0.1.0' },\n { capabilities: {} }\n )\n\n await client.connect(transport)\n\n return new McpClient(client, transport, options.apiKeySecret)\n }\n\n /**\n * List available tools from the server.\n */\n async listTools(): Promise<ToolInfo[]> {\n const response = await this.client.listTools()\n\n return response.tools.map((tool) => ({\n name: tool.name,\n description: tool.description ?? '',\n inputSchema: (tool.inputSchema ?? {}) as Record<string, unknown>,\n }))\n }\n\n /**\n * Call a tool on the server.\n */\n async callTool(name: string, args: unknown): Promise<ToolResult> {\n try {\n const response = await this.client.callTool({\n name,\n arguments: args as Record<string, unknown>,\n })\n\n // Parse content from response\n const content = response.content\n if (!Array.isArray(content) || content.length === 0) {\n return { success: true, result: null }\n }\n\n const firstContent = content[0]\n if (firstContent.type === 'text') {\n try {\n const parsed = JSON.parse(firstContent.text)\n\n // Check if it's an error response\n if (response.isError || parsed.error) {\n return {\n success: false,\n error: parsed.error ?? 'Unknown error',\n }\n }\n\n return { success: true, result: parsed }\n } catch {\n // Not JSON, return as-is\n return { success: true, result: firstContent.text }\n }\n }\n\n return { success: true, result: content }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return { success: false, error: message }\n }\n }\n\n /**\n * Close the client and release resources.\n */\n async close(): Promise<void> {\n try {\n await this.client.close()\n } catch {\n // Ignore close errors\n }\n\n try {\n await this.transport.close()\n } catch {\n // Ignore close errors\n }\n\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,71 @@
1
+ import { resolve, dirname } from "node:path";
2
+ const MCP_CONFIG_FILENAME = ".mcp.json";
3
+ const PROJECT_ROOT_MARKERS = [".git", "yarn.lock", "pnpm-lock.yaml", "package-lock.json"];
4
+ const log = (message, ...args) => {
5
+ console.error(`[MCP Dev] ${message}`, ...args);
6
+ };
7
+ function findProjectRoot(startDir, exists) {
8
+ let dir = resolve(startDir);
9
+ while (true) {
10
+ for (const marker of PROJECT_ROOT_MARKERS) {
11
+ if (exists(resolve(dir, marker))) return dir;
12
+ }
13
+ const parent = dirname(dir);
14
+ if (parent === dir) return resolve(startDir);
15
+ dir = parent;
16
+ }
17
+ }
18
+ function checkMcpConfigPermissions(configPath, fileStat, currentUid, warn) {
19
+ if (currentUid === null) return { ok: true };
20
+ if (fileStat.uid !== currentUid) {
21
+ return {
22
+ ok: false,
23
+ reason: `refusing to read ${configPath}: it is owned by uid ${fileStat.uid}, not the current user (uid ${currentUid})`
24
+ };
25
+ }
26
+ if ((fileStat.mode & 63) !== 0) {
27
+ warn(
28
+ `${configPath} is accessible to group/other (mode ${(fileStat.mode & 511).toString(8)}); it contains a live API key \u2014 restrict it with: chmod 600 ${configPath}`
29
+ );
30
+ }
31
+ return { ok: true };
32
+ }
33
+ async function getApiKeyFromMcpJson() {
34
+ const envKey = process.env.OPEN_MERCATO_API_KEY;
35
+ if (envKey && envKey.trim().length > 0) {
36
+ return envKey.trim();
37
+ }
38
+ const { readFile, stat } = await import("node:fs/promises");
39
+ const { existsSync } = await import("node:fs");
40
+ try {
41
+ const projectRoot = findProjectRoot(process.cwd(), existsSync);
42
+ const mcpJsonPath = resolve(projectRoot, MCP_CONFIG_FILENAME);
43
+ if (!existsSync(mcpJsonPath)) {
44
+ return void 0;
45
+ }
46
+ const fileStat = await stat(mcpJsonPath);
47
+ const currentUid = typeof process.getuid === "function" ? process.getuid() : null;
48
+ const permissionCheck = checkMcpConfigPermissions(
49
+ mcpJsonPath,
50
+ { uid: fileStat.uid, mode: fileStat.mode },
51
+ currentUid,
52
+ (message) => log(`Warning: ${message}`)
53
+ );
54
+ if (!permissionCheck.ok) {
55
+ log(`Error: ${permissionCheck.reason}`);
56
+ return void 0;
57
+ }
58
+ const content = await readFile(mcpJsonPath, "utf-8");
59
+ const config = JSON.parse(content);
60
+ const serverConfig = config?.mcpServers?.["open-mercato"];
61
+ return serverConfig?.headers?.["x-api-key"];
62
+ } catch {
63
+ return void 0;
64
+ }
65
+ }
66
+ export {
67
+ checkMcpConfigPermissions,
68
+ findProjectRoot,
69
+ getApiKeyFromMcpJson
70
+ };
71
+ //# sourceMappingURL=mcp-dev-key-resolution.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/lib/mcp-dev-key-resolution.ts"],
4
+ "sourcesContent": ["import { resolve, dirname } from 'node:path'\n\nconst MCP_CONFIG_FILENAME = '.mcp.json'\n\n// Markers that identify a project/repo root. The lookup stops at the first\n// ancestor that has one of these, so the secret is only ever read from the\n// project root \u2014 never from an unbounded walk up to the filesystem root.\nconst PROJECT_ROOT_MARKERS = ['.git', 'yarn.lock', 'pnpm-lock.yaml', 'package-lock.json'] as const\n\nconst log = (message: string, ...args: unknown[]) => {\n console.error(`[MCP Dev] ${message}`, ...args)\n}\n\ntype PermissionStat = { uid: number; mode: number }\n\n/**\n * Resolve the project root by walking up from `startDir` to the nearest\n * ancestor containing a project-root marker. Falls back to `startDir` (never\n * the filesystem root) when no marker is found, so a planted `.mcp.json` in a\n * writable ancestor such as `/tmp` can never be picked up.\n */\nexport function findProjectRoot(startDir: string, exists: (path: string) => boolean): string {\n let dir = resolve(startDir)\n while (true) {\n for (const marker of PROJECT_ROOT_MARKERS) {\n if (exists(resolve(dir, marker))) return dir\n }\n const parent = dirname(dir)\n if (parent === dir) return resolve(startDir)\n dir = parent\n }\n}\n\n/**\n * Validate the ownership and permissions of `.mcp.json` before its secret is\n * read. Refuses when the file is owned by another user (a planted/shadowed\n * config) and warns loudly when it is accessible to group/other (the key is a\n * live secret). POSIX-only: when `currentUid` is null (e.g. Windows) the checks\n * are skipped.\n */\nexport function checkMcpConfigPermissions(\n configPath: string,\n fileStat: PermissionStat,\n currentUid: number | null,\n warn: (message: string) => void,\n): { ok: boolean; reason?: string } {\n if (currentUid === null) return { ok: true }\n if (fileStat.uid !== currentUid) {\n return {\n ok: false,\n reason: `refusing to read ${configPath}: it is owned by uid ${fileStat.uid}, not the current user (uid ${currentUid})`,\n }\n }\n if ((fileStat.mode & 0o077) !== 0) {\n warn(\n `${configPath} is accessible to group/other (mode ${(fileStat.mode & 0o777).toString(8)}); it contains a live API key \u2014 restrict it with: chmod 600 ${configPath}`,\n )\n }\n return { ok: true }\n}\n\n/**\n * Resolve the dev MCP server's API key.\n *\n * Priority: `OPEN_MERCATO_API_KEY` env var, then `headers.x-api-key` from a\n * single `.mcp.json` read from the detected project root. The lookup is bounded\n * to the project root (it never walks up to the filesystem root), and the file\n * is ownership/permission-checked before its secret is read.\n */\nexport async function getApiKeyFromMcpJson(): Promise<string | undefined> {\n const envKey = process.env.OPEN_MERCATO_API_KEY\n if (envKey && envKey.trim().length > 0) {\n return envKey.trim()\n }\n\n const { readFile, stat } = await import('node:fs/promises')\n const { existsSync } = await import('node:fs')\n\n try {\n const projectRoot = findProjectRoot(process.cwd(), existsSync)\n const mcpJsonPath = resolve(projectRoot, MCP_CONFIG_FILENAME)\n if (!existsSync(mcpJsonPath)) {\n return undefined\n }\n\n const fileStat = await stat(mcpJsonPath)\n const currentUid = typeof process.getuid === 'function' ? process.getuid() : null\n const permissionCheck = checkMcpConfigPermissions(\n mcpJsonPath,\n { uid: fileStat.uid, mode: fileStat.mode },\n currentUid,\n (message) => log(`Warning: ${message}`),\n )\n if (!permissionCheck.ok) {\n log(`Error: ${permissionCheck.reason}`)\n return undefined\n }\n\n const content = await readFile(mcpJsonPath, 'utf-8')\n const config = JSON.parse(content)\n const serverConfig = config?.mcpServers?.['open-mercato']\n\n return serverConfig?.headers?.['x-api-key']\n } catch {\n return undefined\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,SAAS,eAAe;AAEjC,MAAM,sBAAsB;AAK5B,MAAM,uBAAuB,CAAC,QAAQ,aAAa,kBAAkB,mBAAmB;AAExF,MAAM,MAAM,CAAC,YAAoB,SAAoB;AACnD,UAAQ,MAAM,aAAa,OAAO,IAAI,GAAG,IAAI;AAC/C;AAUO,SAAS,gBAAgB,UAAkB,QAA2C;AAC3F,MAAI,MAAM,QAAQ,QAAQ;AAC1B,SAAO,MAAM;AACX,eAAW,UAAU,sBAAsB;AACzC,UAAI,OAAO,QAAQ,KAAK,MAAM,CAAC,EAAG,QAAO;AAAA,IAC3C;AACA,UAAM,SAAS,QAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK,QAAO,QAAQ,QAAQ;AAC3C,UAAM;AAAA,EACR;AACF;AASO,SAAS,0BACd,YACA,UACA,YACA,MACkC;AAClC,MAAI,eAAe,KAAM,QAAO,EAAE,IAAI,KAAK;AAC3C,MAAI,SAAS,QAAQ,YAAY;AAC/B,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,oBAAoB,UAAU,wBAAwB,SAAS,GAAG,+BAA+B,UAAU;AAAA,IACrH;AAAA,EACF;AACA,OAAK,SAAS,OAAO,QAAW,GAAG;AACjC;AAAA,MACE,GAAG,UAAU,wCAAwC,SAAS,OAAO,KAAO,SAAS,CAAC,CAAC,oEAA+D,UAAU;AAAA,IAClK;AAAA,EACF;AACA,SAAO,EAAE,IAAI,KAAK;AACpB;AAUA,eAAsB,uBAAoD;AACxE,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,UAAU,OAAO,KAAK,EAAE,SAAS,GAAG;AACtC,WAAO,OAAO,KAAK;AAAA,EACrB;AAEA,QAAM,EAAE,UAAU,KAAK,IAAI,MAAM,OAAO,kBAAkB;AAC1D,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,SAAS;AAE7C,MAAI;AACF,UAAM,cAAc,gBAAgB,QAAQ,IAAI,GAAG,UAAU;AAC7D,UAAM,cAAc,QAAQ,aAAa,mBAAmB;AAC5D,QAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,MAAM,KAAK,WAAW;AACvC,UAAM,aAAa,OAAO,QAAQ,WAAW,aAAa,QAAQ,OAAO,IAAI;AAC7E,UAAM,kBAAkB;AAAA,MACtB;AAAA,MACA,EAAE,KAAK,SAAS,KAAK,MAAM,SAAS,KAAK;AAAA,MACzC;AAAA,MACA,CAAC,YAAY,IAAI,YAAY,OAAO,EAAE;AAAA,IACxC;AACA,QAAI,CAAC,gBAAgB,IAAI;AACvB,UAAI,UAAU,gBAAgB,MAAM,EAAE;AACtC,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,UAAM,eAAe,QAAQ,aAAa,cAAc;AAExD,WAAO,cAAc,UAAU,WAAW;AAAA,EAC5C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -7,40 +7,11 @@ import { executeTool } from "./tool-executor.js";
7
7
  import { loadAllModuleTools, indexToolsForSearch } from "./tool-loader.js";
8
8
  import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from "./auth.js";
9
9
  import { jsonSchemaToZod } from "./schema-utils.js";
10
+ import { getApiKeyFromMcpJson } from "./mcp-dev-key-resolution.js";
10
11
  const DEFAULT_PORT = 3001;
11
12
  const log = (message, ...args) => {
12
13
  console.error(`[MCP Dev] ${message}`, ...args);
13
14
  };
14
- async function getApiKeyFromMcpJson() {
15
- const { readFile, access } = await import("node:fs/promises");
16
- const { resolve, dirname } = await import("node:path");
17
- const findMcpJson = async (startDir) => {
18
- let dir = startDir;
19
- const root = resolve("/");
20
- while (dir !== root) {
21
- const candidate = resolve(dir, ".mcp.json");
22
- try {
23
- await access(candidate);
24
- return candidate;
25
- } catch {
26
- dir = dirname(dir);
27
- }
28
- }
29
- return null;
30
- };
31
- try {
32
- const mcpJsonPath = await findMcpJson(process.cwd());
33
- if (!mcpJsonPath) {
34
- return void 0;
35
- }
36
- const content = await readFile(mcpJsonPath, "utf-8");
37
- const config = JSON.parse(content);
38
- const serverConfig = config?.mcpServers?.["open-mercato"];
39
- return serverConfig?.headers?.["x-api-key"];
40
- } catch {
41
- return void 0;
42
- }
43
- }
44
15
  const MAX_BODY_SIZE = 1 * 1024 * 1024;
45
16
  async function parseJsonBody(req) {
46
17
  return new Promise((resolve, reject) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/mcp-dev-server.ts"],
4
- "sourcesContent": ["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { z, type ZodType } from 'zod'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'\nimport { jsonSchemaToZod } from './schema-utils'\nimport type { McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nconst DEFAULT_PORT = 3001\n\nconst log = (message: string, ...args: unknown[]) => {\n console.error(`[MCP Dev] ${message}`, ...args)\n}\n\nasync function getApiKeyFromMcpJson(): Promise<string | undefined> {\n const { readFile, access } = await import('node:fs/promises')\n const { resolve, dirname } = await import('node:path')\n\n // Search for .mcp.json starting from cwd and walking up to project root\n const findMcpJson = async (startDir: string): Promise<string | null> => {\n let dir = startDir\n const root = resolve('/')\n while (dir !== root) {\n const candidate = resolve(dir, '.mcp.json')\n try {\n await access(candidate)\n return candidate\n } catch {\n dir = dirname(dir)\n }\n }\n return null\n }\n\n try {\n const mcpJsonPath = await findMcpJson(process.cwd())\n if (!mcpJsonPath) {\n return undefined\n }\n\n const content = await readFile(mcpJsonPath, 'utf-8')\n const config = JSON.parse(content)\n const serverConfig = config?.mcpServers?.['open-mercato']\n\n return serverConfig?.headers?.['x-api-key']\n } catch {\n return undefined\n }\n}\n\n/**\n * Maximum request body size (1MB).\n */\nconst MAX_BODY_SIZE = 1 * 1024 * 1024\n\n/**\n * Parse JSON body from request with size limit.\n */\nasync function parseJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let totalSize = 0\n\n req.on('data', (chunk: Buffer) => {\n totalSize += chunk.length\n if (totalSize > MAX_BODY_SIZE) {\n req.destroy()\n reject(new Error('Request payload too large'))\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n const body = Buffer.concat(chunks).toString('utf-8')\n resolve(body ? JSON.parse(body) : undefined)\n } catch (error) {\n reject(error)\n }\n })\n req.on('error', reject)\n })\n}\n\n/**\n * Create MCP server with tools pre-authenticated for dev use.\n * No session tokens required - uses API key authentication directly.\n */\nfunction createDevMcpServer(\n toolContext: McpToolContext,\n authFeatures: string[],\n isSuperAdmin: boolean,\n debug: boolean\n): McpServer {\n const server = new McpServer(\n { name: 'open-mercato-mcp-dev', version: '0.1.0' },\n { capabilities: { tools: {} } }\n )\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n // Filter tools based on API key permissions\n const rbacService = toolContext.container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, authFeatures, isSuperAdmin, rbacService)\n )\n\n if (debug) {\n log(`Registering ${accessibleTools.length}/${tools.length} tools (filtered by API key permissions)`)\n }\n\n for (const tool of accessibleTools) {\n if (debug) {\n log(`Registering tool: ${tool.name}`)\n }\n\n // Convert Zod schema to safe schema without Date types\n let safeSchema: ZodType | undefined\n if (tool.inputSchema) {\n try {\n const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>\n const converted = jsonSchemaToZod(jsonSchema)\n safeSchema = (converted as z.ZodObject<any>).passthrough()\n } catch (error) {\n if (debug) {\n log(`Skipping tool ${tool.name} - schema conversion failed:`, error instanceof Error ? error.message : error)\n }\n continue\n }\n } else {\n safeSchema = z.object({}).passthrough()\n }\n\n try {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: safeSchema,\n },\n async (args: unknown) => {\n const toolArgs = (args ?? {}) as Record<string, unknown>\n\n if (debug) {\n log(`Calling tool: ${tool.name}`, JSON.stringify(toolArgs))\n }\n\n const result = await executeTool(tool.name, toolArgs, toolContext)\n\n if (!result.success) {\n log(`Tool error: ${result.error}`)\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n }\n )\n } catch (error) {\n if (debug) {\n log(`Skipping tool ${tool.name} - registration failed:`, error instanceof Error ? error.message : error)\n }\n continue\n }\n }\n\n return server\n}\n\n/**\n * Development MCP server for Claude Code integration.\n *\n * This server uses HTTP transport and authenticates via the\n * x-api-key header configured in .mcp.json file.\n *\n * Usage:\n * yarn mcp:dev\n *\n * Configure in .mcp.json for Claude Code with HTTP transport.\n */\nexport async function runMcpDevServer(): Promise<void> {\n const apiKey = await getApiKeyFromMcpJson()\n const port = parseInt(process.env.MCP_DEV_PORT ?? '', 10) || DEFAULT_PORT\n const debug = process.env.MCP_DEBUG === 'true'\n\n if (!apiKey) {\n log('Error: API key not found in .mcp.json')\n log('')\n log('To get an API key:')\n log(' 1. Log into Open Mercato as an admin')\n log(' 2. Go to Settings > API Keys')\n log(' 3. Create a new key with the required permissions')\n log('')\n log('Then configure in .mcp.json:')\n log(' {')\n log(' \"mcpServers\": {')\n log(' \"open-mercato\": {')\n log(' \"type\": \"http\",')\n log(' \"url\": \"http://localhost:3001/mcp\",')\n log(' \"headers\": {')\n log(' \"x-api-key\": \"omk_your_api_key_here\"')\n log(' }')\n log(' }')\n log(' }')\n log(' }')\n process.exit(1)\n }\n\n log('Starting development MCP HTTP server...')\n\n // Create DI container\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n\n // Authenticate the API key upfront\n log('Authenticating API key...')\n const authResult = await authenticateMcpRequest(apiKey, container)\n\n if (!authResult.success) {\n log(`Authentication failed: ${authResult.error}`)\n process.exit(1)\n }\n\n log(`Authenticated as: ${authResult.keyName}`)\n log(`Tenant: ${authResult.tenantId ?? '(global)'}`)\n log(`Organization: ${authResult.organizationId ?? '(none)'}`)\n log(`Super admin: ${authResult.isSuperAdmin}`)\n log(`Features: ${authResult.features.length > 0 ? authResult.features.join(', ') : '(none)'}`)\n\n // Load tools\n log('Loading tools...')\n await loadAllModuleTools()\n\n // Generate and cache entity graph\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n\n log('Generating entity relationship graph...')\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n log(`Entity graph: ${graph.nodes.length} entities, ${graph.edges.length} relationships`)\n } catch (error) {\n log('Entity graph generation skipped:', error instanceof Error ? error.message : error)\n }\n\n // Pre-cache rich OpenAPI spec for Code Mode search tool (prefers runtime module registry over static JSON)\n try {\n const { loadRichOpenApiSpec } = await import('./api-endpoint-index')\n const spec = await loadRichOpenApiSpec()\n if (spec) {\n log('Rich OpenAPI spec cached for Code Mode (with requestBody schemas)')\n } else {\n log('OpenAPI spec not available')\n }\n } catch (error) {\n log('OpenAPI spec caching skipped:', error instanceof Error ? error.message : error)\n }\n\n // Index tools and entity schemas for search (if search service available)\n try {\n const searchService = container.resolve('searchService') as SearchService\n await indexToolsForSearch(searchService)\n\n // Index entity schemas for hybrid search\n try {\n const { getCachedEntityGraph } = await import('./entity-graph')\n const { indexEntitiesForSearch } = await import('./entity-index')\n const graph = getCachedEntityGraph()\n if (graph) {\n const { count } = await indexEntitiesForSearch(searchService, graph)\n if (count > 0) {\n log(`Indexed ${count} entity schemas for discovery`)\n }\n }\n } catch (entityError) {\n log('Entity schema indexing skipped:', entityError instanceof Error ? entityError.message : entityError)\n }\n } catch {\n log('Search indexing skipped (search service not available)')\n }\n\n // Generate a stable session ID for dev mode (enables session memory / caching)\n const { randomBytes } = await import('node:crypto')\n const devSessionId = 'dev_' + randomBytes(8).toString('hex')\n log(`Session ID: ${devSessionId} (stable for this server instance)`)\n\n // Create tool context from auth result\n const toolContext: McpToolContext = {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n userId: authResult.userId,\n container,\n userFeatures: authResult.features,\n isSuperAdmin: authResult.isSuperAdmin,\n apiKeySecret: apiKey,\n sessionId: devSessionId,\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '/', `http://localhost:${port}`)\n\n // Health check endpoint\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({\n status: 'ok',\n mode: 'development',\n tools: getToolRegistry().listToolNames().length,\n tenant: authResult.tenantId,\n timestamp: new Date().toISOString(),\n }))\n return\n }\n\n if (url.pathname !== '/mcp') {\n res.writeHead(404, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Not found' }))\n return\n }\n\n // Extract and validate API key from header\n const headers: Record<string, string | undefined> = {}\n for (const [key, value] of Object.entries(req.headers)) {\n headers[key] = Array.isArray(value) ? value[0] : value\n }\n\n const providedApiKey = extractApiKeyFromHeaders(headers)\n if (!providedApiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'API key required (x-api-key header)' }))\n return\n }\n\n // Validate against the configured API key\n if (providedApiKey !== apiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Invalid API key' }))\n return\n }\n\n if (debug) {\n log(`Authenticated request (${req.method})`)\n }\n\n try {\n // Create stateless transport\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: req.method === 'POST',\n })\n\n // Create server with pre-authenticated context (no session tokens needed)\n const mcpServer = createDevMcpServer(toolContext, authResult.features, authResult.isSuperAdmin, debug)\n\n // Connect server to transport\n await mcpServer.connect(transport)\n\n // Handle the request\n if (req.method === 'POST') {\n const body = await parseJsonBody(req)\n await transport.handleRequest(req, res, body)\n } else {\n await transport.handleRequest(req, res)\n }\n\n // Cleanup after response finishes\n res.on('finish', () => {\n transport.close()\n mcpServer.close()\n if (debug) {\n log(`Request completed, cleaned up`)\n }\n })\n } catch (error) {\n log('Error handling request:', error)\n if (!res.headersSent) {\n if (error instanceof Error && error.message === 'Request payload too large') {\n res.writeHead(413, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Request payload too large (max 1MB)' }))\n return\n }\n\n res.writeHead(500, { 'Content-Type': 'application/json' })\n res.end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,\n },\n id: null,\n })\n )\n }\n }\n })\n\n const toolCount = getToolRegistry().listToolNames().length\n\n log(`Tools registered: ${toolCount}`)\n log(`Endpoint: http://localhost:${port}/mcp`)\n log(`Health: http://localhost:${port}/health`)\n log(`Mode: Development (API key auth, no session tokens)`)\n\n return new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n log(`Server listening on port ${port}`)\n log('Ready for Claude Code connections')\n })\n\n const shutdown = async () => {\n log('Shutting down...')\n httpServer.close(() => {\n log('Server closed')\n resolve()\n })\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { z, type ZodType } from 'zod'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'\nimport { jsonSchemaToZod } from './schema-utils'\nimport { getApiKeyFromMcpJson } from './mcp-dev-key-resolution'\nimport type { McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nconst DEFAULT_PORT = 3001\n\nconst log = (message: string, ...args: unknown[]) => {\n console.error(`[MCP Dev] ${message}`, ...args)\n}\n\n/**\n * Maximum request body size (1MB).\n */\nconst MAX_BODY_SIZE = 1 * 1024 * 1024\n\n/**\n * Parse JSON body from request with size limit.\n */\nasync function parseJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let totalSize = 0\n\n req.on('data', (chunk: Buffer) => {\n totalSize += chunk.length\n if (totalSize > MAX_BODY_SIZE) {\n req.destroy()\n reject(new Error('Request payload too large'))\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n const body = Buffer.concat(chunks).toString('utf-8')\n resolve(body ? JSON.parse(body) : undefined)\n } catch (error) {\n reject(error)\n }\n })\n req.on('error', reject)\n })\n}\n\n/**\n * Create MCP server with tools pre-authenticated for dev use.\n * No session tokens required - uses API key authentication directly.\n */\nfunction createDevMcpServer(\n toolContext: McpToolContext,\n authFeatures: string[],\n isSuperAdmin: boolean,\n debug: boolean\n): McpServer {\n const server = new McpServer(\n { name: 'open-mercato-mcp-dev', version: '0.1.0' },\n { capabilities: { tools: {} } }\n )\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n // Filter tools based on API key permissions\n const rbacService = toolContext.container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, authFeatures, isSuperAdmin, rbacService)\n )\n\n if (debug) {\n log(`Registering ${accessibleTools.length}/${tools.length} tools (filtered by API key permissions)`)\n }\n\n for (const tool of accessibleTools) {\n if (debug) {\n log(`Registering tool: ${tool.name}`)\n }\n\n // Convert Zod schema to safe schema without Date types\n let safeSchema: ZodType | undefined\n if (tool.inputSchema) {\n try {\n const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>\n const converted = jsonSchemaToZod(jsonSchema)\n safeSchema = (converted as z.ZodObject<any>).passthrough()\n } catch (error) {\n if (debug) {\n log(`Skipping tool ${tool.name} - schema conversion failed:`, error instanceof Error ? error.message : error)\n }\n continue\n }\n } else {\n safeSchema = z.object({}).passthrough()\n }\n\n try {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: safeSchema,\n },\n async (args: unknown) => {\n const toolArgs = (args ?? {}) as Record<string, unknown>\n\n if (debug) {\n log(`Calling tool: ${tool.name}`, JSON.stringify(toolArgs))\n }\n\n const result = await executeTool(tool.name, toolArgs, toolContext)\n\n if (!result.success) {\n log(`Tool error: ${result.error}`)\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n }\n )\n } catch (error) {\n if (debug) {\n log(`Skipping tool ${tool.name} - registration failed:`, error instanceof Error ? error.message : error)\n }\n continue\n }\n }\n\n return server\n}\n\n/**\n * Development MCP server for Claude Code integration.\n *\n * This server uses HTTP transport and authenticates via the\n * x-api-key header configured in .mcp.json file.\n *\n * Usage:\n * yarn mcp:dev\n *\n * Configure in .mcp.json for Claude Code with HTTP transport.\n */\nexport async function runMcpDevServer(): Promise<void> {\n const apiKey = await getApiKeyFromMcpJson()\n const port = parseInt(process.env.MCP_DEV_PORT ?? '', 10) || DEFAULT_PORT\n const debug = process.env.MCP_DEBUG === 'true'\n\n if (!apiKey) {\n log('Error: API key not found in .mcp.json')\n log('')\n log('To get an API key:')\n log(' 1. Log into Open Mercato as an admin')\n log(' 2. Go to Settings > API Keys')\n log(' 3. Create a new key with the required permissions')\n log('')\n log('Then configure in .mcp.json:')\n log(' {')\n log(' \"mcpServers\": {')\n log(' \"open-mercato\": {')\n log(' \"type\": \"http\",')\n log(' \"url\": \"http://localhost:3001/mcp\",')\n log(' \"headers\": {')\n log(' \"x-api-key\": \"omk_your_api_key_here\"')\n log(' }')\n log(' }')\n log(' }')\n log(' }')\n process.exit(1)\n }\n\n log('Starting development MCP HTTP server...')\n\n // Create DI container\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n\n // Authenticate the API key upfront\n log('Authenticating API key...')\n const authResult = await authenticateMcpRequest(apiKey, container)\n\n if (!authResult.success) {\n log(`Authentication failed: ${authResult.error}`)\n process.exit(1)\n }\n\n log(`Authenticated as: ${authResult.keyName}`)\n log(`Tenant: ${authResult.tenantId ?? '(global)'}`)\n log(`Organization: ${authResult.organizationId ?? '(none)'}`)\n log(`Super admin: ${authResult.isSuperAdmin}`)\n log(`Features: ${authResult.features.length > 0 ? authResult.features.join(', ') : '(none)'}`)\n\n // Load tools\n log('Loading tools...')\n await loadAllModuleTools()\n\n // Generate and cache entity graph\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n\n log('Generating entity relationship graph...')\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n log(`Entity graph: ${graph.nodes.length} entities, ${graph.edges.length} relationships`)\n } catch (error) {\n log('Entity graph generation skipped:', error instanceof Error ? error.message : error)\n }\n\n // Pre-cache rich OpenAPI spec for Code Mode search tool (prefers runtime module registry over static JSON)\n try {\n const { loadRichOpenApiSpec } = await import('./api-endpoint-index')\n const spec = await loadRichOpenApiSpec()\n if (spec) {\n log('Rich OpenAPI spec cached for Code Mode (with requestBody schemas)')\n } else {\n log('OpenAPI spec not available')\n }\n } catch (error) {\n log('OpenAPI spec caching skipped:', error instanceof Error ? error.message : error)\n }\n\n // Index tools and entity schemas for search (if search service available)\n try {\n const searchService = container.resolve('searchService') as SearchService\n await indexToolsForSearch(searchService)\n\n // Index entity schemas for hybrid search\n try {\n const { getCachedEntityGraph } = await import('./entity-graph')\n const { indexEntitiesForSearch } = await import('./entity-index')\n const graph = getCachedEntityGraph()\n if (graph) {\n const { count } = await indexEntitiesForSearch(searchService, graph)\n if (count > 0) {\n log(`Indexed ${count} entity schemas for discovery`)\n }\n }\n } catch (entityError) {\n log('Entity schema indexing skipped:', entityError instanceof Error ? entityError.message : entityError)\n }\n } catch {\n log('Search indexing skipped (search service not available)')\n }\n\n // Generate a stable session ID for dev mode (enables session memory / caching)\n const { randomBytes } = await import('node:crypto')\n const devSessionId = 'dev_' + randomBytes(8).toString('hex')\n log(`Session ID: ${devSessionId} (stable for this server instance)`)\n\n // Create tool context from auth result\n const toolContext: McpToolContext = {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n userId: authResult.userId,\n container,\n userFeatures: authResult.features,\n isSuperAdmin: authResult.isSuperAdmin,\n apiKeySecret: apiKey,\n sessionId: devSessionId,\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '/', `http://localhost:${port}`)\n\n // Health check endpoint\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({\n status: 'ok',\n mode: 'development',\n tools: getToolRegistry().listToolNames().length,\n tenant: authResult.tenantId,\n timestamp: new Date().toISOString(),\n }))\n return\n }\n\n if (url.pathname !== '/mcp') {\n res.writeHead(404, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Not found' }))\n return\n }\n\n // Extract and validate API key from header\n const headers: Record<string, string | undefined> = {}\n for (const [key, value] of Object.entries(req.headers)) {\n headers[key] = Array.isArray(value) ? value[0] : value\n }\n\n const providedApiKey = extractApiKeyFromHeaders(headers)\n if (!providedApiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'API key required (x-api-key header)' }))\n return\n }\n\n // Validate against the configured API key\n if (providedApiKey !== apiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Invalid API key' }))\n return\n }\n\n if (debug) {\n log(`Authenticated request (${req.method})`)\n }\n\n try {\n // Create stateless transport\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: req.method === 'POST',\n })\n\n // Create server with pre-authenticated context (no session tokens needed)\n const mcpServer = createDevMcpServer(toolContext, authResult.features, authResult.isSuperAdmin, debug)\n\n // Connect server to transport\n await mcpServer.connect(transport)\n\n // Handle the request\n if (req.method === 'POST') {\n const body = await parseJsonBody(req)\n await transport.handleRequest(req, res, body)\n } else {\n await transport.handleRequest(req, res)\n }\n\n // Cleanup after response finishes\n res.on('finish', () => {\n transport.close()\n mcpServer.close()\n if (debug) {\n log(`Request completed, cleaned up`)\n }\n })\n } catch (error) {\n log('Error handling request:', error)\n if (!res.headersSent) {\n if (error instanceof Error && error.message === 'Request payload too large') {\n res.writeHead(413, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Request payload too large (max 1MB)' }))\n return\n }\n\n res.writeHead(500, { 'Content-Type': 'application/json' })\n res.end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,\n },\n id: null,\n })\n )\n }\n }\n })\n\n const toolCount = getToolRegistry().listToolNames().length\n\n log(`Tools registered: ${toolCount}`)\n log(`Endpoint: http://localhost:${port}/mcp`)\n log(`Health: http://localhost:${port}/health`)\n log(`Mode: Development (API key auth, no session tokens)`)\n\n return new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n log(`Server listening on port ${port}`)\n log('Ready for Claude Code connections')\n })\n\n const shutdown = async () => {\n log('Shutting down...')\n httpServer.close(() => {\n log('Server closed')\n resolve()\n })\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -10,7 +10,8 @@ import { executeTool } from "./tool-executor.js";
10
10
  import { loadAllModuleTools, indexToolsForSearch } from "./tool-loader.js";
11
11
  import { authenticateMcpRequest, hasRequiredFeatures } from "./auth.js";
12
12
  async function createMcpServer(options) {
13
- const { config, container, context, apiKeySecret } = options;
13
+ const { config, container, context, allowUnauthenticatedSuperadmin } = options;
14
+ const apiKeySecret = typeof options.apiKeySecret === "string" && options.apiKeySecret.trim().length > 0 ? options.apiKeySecret : void 0;
14
15
  let tenantId = null;
15
16
  let organizationId = null;
16
17
  let userId = null;
@@ -27,29 +28,34 @@ async function createMcpServer(options) {
27
28
  userFeatures = authResult.features;
28
29
  isSuperAdmin = authResult.isSuperAdmin;
29
30
  console.error(`[MCP Server] Authenticated via API key: ${authResult.keyName}`);
30
- } else if (context) {
31
+ } else if (context && context.userId) {
31
32
  tenantId = context.tenantId;
32
33
  organizationId = context.organizationId;
33
34
  userId = context.userId;
34
- if (userId) {
35
- try {
36
- const rbacService = container.resolve("rbacService");
37
- const acl = await rbacService.loadAcl(userId, {
38
- tenantId,
39
- organizationId
40
- });
41
- userFeatures = acl.features;
42
- isSuperAdmin = acl.isSuperAdmin;
43
- } catch (error) {
44
- console.error("[MCP Server] Failed to load user ACL:", error);
45
- }
46
- } else {
47
- isSuperAdmin = true;
48
- console.error("[MCP Server] No user specified, running with superadmin access");
35
+ try {
36
+ const rbacService = container.resolve("rbacService");
37
+ const acl = await rbacService.loadAcl(userId, {
38
+ tenantId,
39
+ organizationId
40
+ });
41
+ userFeatures = acl.features;
42
+ isSuperAdmin = acl.isSuperAdmin;
43
+ } catch (error) {
44
+ console.error("[MCP Server] Failed to load user ACL:", error);
45
+ }
46
+ } else if (allowUnauthenticatedSuperadmin) {
47
+ if (context) {
48
+ tenantId = context.tenantId;
49
+ organizationId = context.organizationId;
49
50
  }
50
- } else {
51
51
  isSuperAdmin = true;
52
- console.error("[MCP Server] No auth context, running with superadmin access");
52
+ console.error(
53
+ "[MCP Server] WARNING: allowUnauthenticatedSuperadmin is enabled \u2014 running with UNAUTHENTICATED SUPERADMIN access and no per-user ACL. Do not use this outside local development/testing."
54
+ );
55
+ } else {
56
+ throw new Error(
57
+ "[internal] MCP server refused to start: no authentication provided. Supply a valid apiKeySecret, a context with a non-empty userId, or explicitly set allowUnauthenticatedSuperadmin: true for local development."
58
+ );
53
59
  }
54
60
  const toolContext = {
55
61
  tenantId,
@@ -142,14 +148,14 @@ async function runMcpServer(options) {
142
148
  const transport = new StdioServerTransport();
143
149
  const toolCount = getToolRegistry().listToolNames().length;
144
150
  console.error(`[MCP Server] Starting ${options.config.name} v${options.config.version}`);
145
- if (options.apiKeySecret) {
151
+ if (options.apiKeySecret && options.apiKeySecret.trim().length > 0) {
146
152
  console.error(`[MCP Server] Authentication: API key`);
147
- } else if (options.context) {
153
+ } else if (options.context && options.context.userId) {
148
154
  console.error(`[MCP Server] Tenant: ${options.context.tenantId ?? "(none)"}`);
149
155
  console.error(`[MCP Server] Organization: ${options.context.organizationId ?? "(none)"}`);
150
- console.error(`[MCP Server] User: ${options.context.userId ?? "(superadmin)"}`);
156
+ console.error(`[MCP Server] User: ${options.context.userId}`);
151
157
  } else {
152
- console.error(`[MCP Server] Authentication: none (superadmin mode)`);
158
+ console.error(`[MCP Server] Authentication: none (unauthenticated superadmin opt-in)`);
153
159
  }
154
160
  console.error(`[MCP Server] Tools registered: ${toolCount}`);
155
161
  await server.connect(transport);