@open-mercato/ai-assistant 0.10.1-develop.7741.1.61a2652671 → 0.10.1-develop.7748.1.d745ba61aa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/ai_assistant/cli.js +7 -1
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/lib/http-server.js +7 -2
- package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +16 -3
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-host-resolution.js +18 -0
- package/dist/modules/ai_assistant/lib/mcp-host-resolution.js.map +7 -0
- package/package.json +6 -6
- package/src/modules/ai_assistant/cli.ts +6 -0
- package/src/modules/ai_assistant/lib/__tests__/mcp-dev-server.test.ts +20 -0
- package/src/modules/ai_assistant/lib/__tests__/mcp-host-resolution.test.ts +37 -0
- package/src/modules/ai_assistant/lib/http-server.ts +16 -2
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +28 -4
- package/src/modules/ai_assistant/lib/mcp-host-resolution.ts +32 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:ai-assistant] found
|
|
1
|
+
[build:ai-assistant] found 248 entry points
|
|
2
2
|
[build:ai-assistant] built successfully
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
2
2
|
import { resetServerLoggerCache } from "@open-mercato/shared/lib/logger";
|
|
3
3
|
import { installNextSubpathResolveHook } from "./lib/next-subpath-resolve-hook.js";
|
|
4
|
+
import { resolveMcpHost } from "./lib/mcp-host-resolution.js";
|
|
4
5
|
async function ensureBootstrap() {
|
|
5
6
|
installNextSubpathResolveHook();
|
|
6
7
|
try {
|
|
@@ -109,6 +110,10 @@ const mcpServeHttp = {
|
|
|
109
110
|
const portArg = parseInt(String(args.port ?? ""), 10);
|
|
110
111
|
const port = !portArg || isNaN(portArg) ? MCP_DEFAULT_PORT : portArg;
|
|
111
112
|
const debug = args.debug === true || args.debug === "true";
|
|
113
|
+
const host = resolveMcpHost(
|
|
114
|
+
typeof args.host === "string" ? args.host : void 0,
|
|
115
|
+
process.env.MCP_HTTP_HOST
|
|
116
|
+
);
|
|
112
117
|
await ensureBootstrap();
|
|
113
118
|
const container = await createRequestContainer();
|
|
114
119
|
const { runMcpHttpServer } = await import("./lib/http-server.js");
|
|
@@ -119,7 +124,8 @@ const mcpServeHttp = {
|
|
|
119
124
|
debug
|
|
120
125
|
},
|
|
121
126
|
container,
|
|
122
|
-
port
|
|
127
|
+
port,
|
|
128
|
+
host
|
|
123
129
|
});
|
|
124
130
|
}
|
|
125
131
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/ai_assistant/cli.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resetServerLoggerCache } from '@open-mercato/shared/lib/logger'\nimport { installNextSubpathResolveHook } from './lib/next-subpath-resolve-hook'\n/**\n * Ensure app bootstrap is called before creating DI container.\n * Uses the shared generated-bootstrap loader so the command works both from\n * the monorepo app and from standalone apps installed through npm packages.\n */\nasync function ensureBootstrap(): Promise<void> {\n // These commands run outside Next.js, but the API route modules the\n // operation runner imports on a tool call say `from 'next/server'`, which\n // plain Node cannot resolve (#6118). Install the retry-with-`.js` hook before\n // anything below can pull a route module in.\n installNextSubpathResolveHook()\n\n // First check if DI is already available\n try {\n const { getDiRegistrars } = await import('@open-mercato/shared/lib/di/container')\n getDiRegistrars()\n return // DI already available\n } catch {\n // DI not available, need to bootstrap\n }\n\n try {\n const { bootstrapFromAppRoot } = await import('@open-mercato/shared/lib/bootstrap/dynamicLoader')\n await bootstrapFromAppRoot()\n } catch (error) {\n console.error('[MCP] Bootstrap failed:', error instanceof Error ? error.message : error)\n // Continue - some contexts may not have bootstrap available\n }\n}\n\nfunction parseArgs(rest: string[]): Record<string, string | boolean> {\n const args: Record<string, string | boolean> = {}\n for (let i = 0; i < rest.length; i++) {\n const arg = rest[i]\n if (!arg?.startsWith('--')) continue\n\n const [key, value] = arg.replace(/^--/, '').split('=')\n if (value !== undefined) {\n args[key] = value\n } else if (rest[i + 1] && !rest[i + 1]!.startsWith('--')) {\n args[key] = rest[i + 1]!\n i++\n } else {\n args[key] = true\n }\n }\n return args\n}\n\nconst mcpServe: ModuleCli = {\n command: 'mcp:serve',\n async run(rest) {\n // stdout carries the MCP JSON-RPC protocol (StdioServerTransport) \u2014 route all\n // facade logs to stderr and rebuild any pino root created earlier in this process.\n process.env.OM_LOG_DESTINATION = 'stderr'\n resetServerLoggerCache()\n const args = parseArgs(rest)\n // Prefer the OPEN_MERCATO_API_KEY env var so the secret never has to be\n // placed on the command line (argv is world-readable via ps / /proc).\n // The --api-key flag stays supported for backward compatibility.\n const apiKey = String(args['api-key'] ?? args.apiKey ?? '') || process.env.OPEN_MERCATO_API_KEY || null\n const tenantId = String(args.tenant ?? args.tenantId ?? '') || null\n const organizationId = String(args.org ?? args.organizationId ?? '') || null\n const userId = String(args.user ?? args.userId ?? '') || null\n const debug = args.debug === true || args.debug === 'true'\n const allowUnauthenticatedSuperadmin =\n args['allow-unauthenticated-superadmin'] === true ||\n args['allow-unauthenticated-superadmin'] === 'true'\n\n // Either API key or tenant is required\n if (!apiKey && !tenantId) {\n console.error('Usage: mercato ai_assistant mcp:serve [options]')\n console.error('')\n console.error('Authentication (choose one):')\n console.error(' OPEN_MERCATO_API_KEY env API key secret (recommended \u2014 keeps the secret off argv)')\n console.error(' --api-key <secret> API key secret for authentication (visible in process listings)')\n console.error(' --tenant <id> Tenant ID (for manual context)')\n console.error('')\n console.error('Options (with --tenant):')\n console.error(' --org <id> Organization ID (optional)')\n console.error(' --user <id> User ID for ACL (required unless --allow-unauthenticated-superadmin is set)')\n console.error('')\n console.error('Common options:')\n console.error(' --debug Enable debug logging')\n console.error(' --allow-unauthenticated-superadmin')\n console.error(' DEV/TEST ONLY: run as superadmin with no per-user ACL when')\n console.error(' no --user (and no --api-key) is supplied. Never use in production.')\n console.error('')\n console.error('Examples:')\n console.error(' OPEN_MERCATO_API_KEY=omk_xxxx.yyyy... mercato ai_assistant mcp:serve')\n console.error(' mercato ai_assistant mcp:serve --api-key omk_xxxx.yyyy...')\n console.error(' mercato ai_assistant mcp:serve --tenant 123e4567-e89b-12d3-a456-426614174000 --user <user-id>')\n console.error(' mercato ai_assistant mcp:serve --tenant 123e4567-e89b-12d3-a456-426614174000 --allow-unauthenticated-superadmin')\n return\n }\n\n await ensureBootstrap()\n const container = await createRequestContainer()\n\n const { runMcpServer } = await import('./lib/mcp-server')\n\n if (apiKey) {\n await runMcpServer({\n config: {\n name: 'open-mercato-mcp',\n version: '0.1.0',\n debug,\n },\n container,\n apiKeySecret: apiKey,\n })\n } else {\n await runMcpServer({\n config: {\n name: 'open-mercato-mcp',\n version: '0.1.0',\n debug,\n },\n container,\n context: {\n tenantId,\n organizationId,\n userId,\n },\n allowUnauthenticatedSuperadmin,\n })\n }\n },\n}\n\nconst MCP_DEFAULT_PORT = 3001\n\nconst mcpServeHttp: ModuleCli = {\n command: 'mcp:serve-http',\n async run(rest) {\n const args = parseArgs(rest)\n const portArg = parseInt(String(args.port ?? ''), 10)\n const port = !portArg || isNaN(portArg) ? MCP_DEFAULT_PORT : portArg\n const debug = args.debug === true || args.debug === 'true'\n\n await ensureBootstrap()\n const container = await createRequestContainer()\n\n const { runMcpHttpServer } = await import('./lib/http-server')\n\n await runMcpHttpServer({\n config: {\n name: 'open-mercato-mcp',\n version: '0.1.0',\n debug,\n },\n container,\n port,\n })\n },\n}\n\nconst mcpDev: ModuleCli = {\n command: 'mcp:dev',\n async run() {\n await ensureBootstrap()\n const { runMcpDevServer } = await import('./lib/mcp-dev-server')\n await runMcpDevServer()\n },\n}\n\nconst mcpEnsureApiKey: ModuleCli = {\n command: 'mcp:ensure-api-key',\n async run(rest) {\n const args = parseArgs(rest)\n // A bare `--file` (no value) parses to boolean true \u2014 treat it as missing\n // rather than provisioning into a file literally named \"true\".\n const fileArg = typeof args.file === 'string' ? args.file.trim() : ''\n const filePath =\n fileArg || process.env.MCP_SERVER_API_KEY_FILE?.trim() || process.env.MCP_API_KEY_FILE?.trim() || ''\n if (!filePath) {\n console.error('Usage: mercato ai_assistant mcp:ensure-api-key --file <path> [options]')\n console.error('')\n console.error('Ensures a valid MCP server API key exists in the database and that its')\n console.error('plaintext secret is stored in <path> (the idempotency anchor). The secret')\n console.error('is never printed; consumers such as the OpenCode container read the file.')\n console.error('')\n console.error('Options:')\n console.error(' --file <path> Secret file path (or MCP_SERVER_API_KEY_FILE env)')\n console.error(' --name <name> API key name (default __mcp_server__)')\n console.error(' --email <email> Owner user email (default OM_INIT_SUPERADMIN_EMAIL, then superadmin@acme.com)')\n console.error(' --rotate Force rotation even when the file secret is still valid')\n process.exitCode = 1\n return\n }\n\n const keyName = typeof args.name === 'string' && args.name.length > 0 ? args.name : undefined\n const ownerEmail = typeof args.email === 'string' && args.email.length > 0 ? args.email : undefined\n const rotate = args.rotate === true || args.rotate === 'true'\n\n await ensureBootstrap()\n const container = await createRequestContainer()\n const em = container.resolve<import('@mikro-orm/postgresql').EntityManager>('em')\n const rbac = container.resolve('rbacService') as import('@open-mercato/core/modules/auth/services/rbacService').RbacService\n\n const { ensureMcpApiKey } = await import('./lib/mcp-ensure-api-key')\n const result = await ensureMcpApiKey({ em, filePath, keyName, ownerEmail, rotate, rbac })\n const message = result.status === 'valid' ? 'Existing key is valid' : 'New key created'\n console.log(`[mcp:ensure-api-key] ${message} (id=${result.keyId}, prefix=${result.keyPrefix})`)\n },\n}\n\nconst listTools: ModuleCli = {\n command: 'mcp:list-tools',\n async run(rest) {\n const args = parseArgs(rest)\n const verbose = args.verbose === true || args.verbose === 'true'\n\n // Ensure bootstrap runs so modules are registered for API discovery\n await ensureBootstrap()\n\n const { loadAllModuleTools } = await import('./lib/tool-loader')\n await loadAllModuleTools()\n\n const { getToolRegistry } = await import('./lib/tool-registry')\n const registry = getToolRegistry()\n const toolNames = registry.listToolNames()\n\n if (toolNames.length === 0) {\n console.log('\\nNo MCP tools registered.')\n console.log('Tools can be registered by modules using registerMcpTool().\\n')\n return\n }\n\n console.log(`\\nRegistered MCP Tools (${toolNames.length}):\\n`)\n\n // Group tools by module\n const byModule = new Map<string, string[]>()\n for (const name of toolNames) {\n const [module] = name.split('.')\n const list = byModule.get(module) ?? []\n list.push(name)\n byModule.set(module, list)\n }\n\n // Sort modules alphabetically\n const sortedModules = Array.from(byModule.keys()).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n\n for (const module of sortedModules) {\n const tools = byModule.get(module)!\n console.log(`${module} (${tools.length} tools):`)\n\n for (const name of tools.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))) {\n const tool = registry.getTool(name)\n if (!tool) continue\n\n if (verbose) {\n console.log(` ${name}`)\n console.log(` ${tool.description}`)\n if (tool.requiredFeatures?.length) {\n console.log(` Requires: ${tool.requiredFeatures.join(', ')}`)\n }\n } else {\n console.log(` - ${name}`)\n }\n }\n console.log('')\n }\n },\n}\n\nconst entityGraph: ModuleCli = {\n command: 'entity-graph',\n async run(rest) {\n const args = parseArgs(rest)\n const format = String(args.format ?? 'triples') as 'json' | 'triples'\n const entity = args.entity ? String(args.entity) : undefined\n const module = args.module ? String(args.module) : undefined\n\n await ensureBootstrap()\n\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n const { extractEntityGraph, formatGraphAsTriples, filterGraphByEntity, filterGraphByModule } = await import(\n './lib/entity-graph'\n )\n\n console.log('[Entity Graph] Extracting from MikroORM metadata...')\n\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n\n // Apply filters\n let edges = graph.edges\n\n if (entity) {\n edges = filterGraphByEntity(graph, entity)\n console.log(`[Entity Graph] Filtered by entity: ${entity}`)\n }\n\n if (module) {\n const filteredGraph = { ...graph, edges }\n edges = filterGraphByModule(filteredGraph, module)\n console.log(`[Entity Graph] Filtered by module: ${module}`)\n }\n\n const filteredGraph = { ...graph, edges }\n\n if (format === 'json') {\n console.log(JSON.stringify(filteredGraph, null, 2))\n } else {\n const triples = formatGraphAsTriples(filteredGraph)\n console.log('')\n for (const triple of triples) {\n console.log(triple)\n }\n }\n\n console.log(`\\n[Entity Graph] ${graph.nodes.length} entities, ${edges.length} relationships`)\n },\n}\n\nconst runPendingActionCleanup: ModuleCli = {\n command: 'run-pending-action-cleanup',\n async run() {\n await ensureBootstrap()\n const container = await createRequestContainer()\n\n const { runPendingActionCleanup: runCleanup } = await import(\n './workers/ai-pending-action-cleanup'\n )\n\n const em = container.resolve<import('@mikro-orm/postgresql').EntityManager>('em')\n const summary = await runCleanup({ em })\n\n console.log('[ai-pending-action-cleanup] Sweep complete:', summary)\n },\n}\n\nconst testTools: ModuleCli = {\n command: 'test-tools',\n async run(rest) {\n const args = parseArgs(rest)\n const json = args.json === true || args.json === 'true'\n const moduleFilter =\n typeof args.module === 'string' && args.module.length > 0 ? args.module : null\n const includeMutations = args['no-mutations'] !== true && args['no-mutations'] !== 'true'\n const tenantId =\n typeof args.tenant === 'string' && args.tenant.length > 0 ? args.tenant : null\n const organizationId =\n typeof args.org === 'string' && args.org.length > 0 ? args.org : null\n\n await ensureBootstrap()\n const { runToolTests } = await import('./lib/tool-test-runner')\n const report = await runToolTests({\n tenantId,\n organizationId,\n moduleFilter,\n includeMutations,\n })\n\n if (json) {\n // Wrap in markers so a Playwright spec (or any caller) can extract the\n // JSON payload without being thrown off by bootstrap log lines emitted\n // to stdout by other modules during DI container creation.\n console.log('---TOOL_TEST_REPORT_BEGIN---')\n console.log(JSON.stringify(report))\n console.log('---TOOL_TEST_REPORT_END---')\n } else {\n console.log('')\n console.log(\n `AI tool test report \u2014 tenant=${report.tenantId ?? '<none>'} org=${\n report.organizationId ?? '<none>'\n }`,\n )\n console.log(\n `total=${report.total} pass=${report.passed} fail=${report.failed} skip=${report.skipped}`,\n )\n const byModule = new Map<string, typeof report.records>()\n for (const record of report.records) {\n const list = byModule.get(record.module) ?? []\n list.push(record)\n byModule.set(record.module, list)\n }\n const sortedModules = Array.from(byModule.keys()).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n for (const moduleId of sortedModules) {\n const list = byModule.get(moduleId)!\n console.log('')\n console.log(`${moduleId} (${list.length}):`)\n for (const record of list) {\n const marker =\n record.status === 'pass' ? '\u2713' : record.status === 'fail' ? '\u2717' : '\u00B7'\n const reason = record.reason ? ` \u2014 ${record.reason}` : ''\n const mutation = record.isMutation ? ' [mutation]' : ''\n console.log(\n ` ${marker} ${record.tool}${mutation} (${record.durationMs}ms)${reason}`,\n )\n }\n }\n console.log('')\n }\n\n if (report.failed > 0) {\n process.exitCode = 1\n }\n },\n}\n\nconst runTokenUsagePrune: ModuleCli = {\n command: 'run-token-usage-prune',\n async run() {\n await ensureBootstrap()\n const container = await createRequestContainer()\n\n const { runTokenUsagePrune: runPrune } = await import(\n './workers/ai-token-usage-prune'\n )\n\n const em = container.resolve<import('@mikro-orm/postgresql').EntityManager>('em')\n const summary = await runPrune({ em })\n\n console.log('[ai-token-usage-prune] Prune complete:', summary)\n },\n}\n\nexport default [\n mcpServe,\n mcpServeHttp,\n mcpDev,\n mcpEnsureApiKey,\n listTools,\n entityGraph,\n runPendingActionCleanup,\n runTokenUsagePrune,\n testTools,\n]\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AACvC,SAAS,qCAAqC;
|
|
4
|
+
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { resetServerLoggerCache } from '@open-mercato/shared/lib/logger'\nimport { installNextSubpathResolveHook } from './lib/next-subpath-resolve-hook'\nimport { resolveMcpHost } from './lib/mcp-host-resolution'\n/**\n * Ensure app bootstrap is called before creating DI container.\n * Uses the shared generated-bootstrap loader so the command works both from\n * the monorepo app and from standalone apps installed through npm packages.\n */\nasync function ensureBootstrap(): Promise<void> {\n // These commands run outside Next.js, but the API route modules the\n // operation runner imports on a tool call say `from 'next/server'`, which\n // plain Node cannot resolve (#6118). Install the retry-with-`.js` hook before\n // anything below can pull a route module in.\n installNextSubpathResolveHook()\n\n // First check if DI is already available\n try {\n const { getDiRegistrars } = await import('@open-mercato/shared/lib/di/container')\n getDiRegistrars()\n return // DI already available\n } catch {\n // DI not available, need to bootstrap\n }\n\n try {\n const { bootstrapFromAppRoot } = await import('@open-mercato/shared/lib/bootstrap/dynamicLoader')\n await bootstrapFromAppRoot()\n } catch (error) {\n console.error('[MCP] Bootstrap failed:', error instanceof Error ? error.message : error)\n // Continue - some contexts may not have bootstrap available\n }\n}\n\nfunction parseArgs(rest: string[]): Record<string, string | boolean> {\n const args: Record<string, string | boolean> = {}\n for (let i = 0; i < rest.length; i++) {\n const arg = rest[i]\n if (!arg?.startsWith('--')) continue\n\n const [key, value] = arg.replace(/^--/, '').split('=')\n if (value !== undefined) {\n args[key] = value\n } else if (rest[i + 1] && !rest[i + 1]!.startsWith('--')) {\n args[key] = rest[i + 1]!\n i++\n } else {\n args[key] = true\n }\n }\n return args\n}\n\nconst mcpServe: ModuleCli = {\n command: 'mcp:serve',\n async run(rest) {\n // stdout carries the MCP JSON-RPC protocol (StdioServerTransport) \u2014 route all\n // facade logs to stderr and rebuild any pino root created earlier in this process.\n process.env.OM_LOG_DESTINATION = 'stderr'\n resetServerLoggerCache()\n const args = parseArgs(rest)\n // Prefer the OPEN_MERCATO_API_KEY env var so the secret never has to be\n // placed on the command line (argv is world-readable via ps / /proc).\n // The --api-key flag stays supported for backward compatibility.\n const apiKey = String(args['api-key'] ?? args.apiKey ?? '') || process.env.OPEN_MERCATO_API_KEY || null\n const tenantId = String(args.tenant ?? args.tenantId ?? '') || null\n const organizationId = String(args.org ?? args.organizationId ?? '') || null\n const userId = String(args.user ?? args.userId ?? '') || null\n const debug = args.debug === true || args.debug === 'true'\n const allowUnauthenticatedSuperadmin =\n args['allow-unauthenticated-superadmin'] === true ||\n args['allow-unauthenticated-superadmin'] === 'true'\n\n // Either API key or tenant is required\n if (!apiKey && !tenantId) {\n console.error('Usage: mercato ai_assistant mcp:serve [options]')\n console.error('')\n console.error('Authentication (choose one):')\n console.error(' OPEN_MERCATO_API_KEY env API key secret (recommended \u2014 keeps the secret off argv)')\n console.error(' --api-key <secret> API key secret for authentication (visible in process listings)')\n console.error(' --tenant <id> Tenant ID (for manual context)')\n console.error('')\n console.error('Options (with --tenant):')\n console.error(' --org <id> Organization ID (optional)')\n console.error(' --user <id> User ID for ACL (required unless --allow-unauthenticated-superadmin is set)')\n console.error('')\n console.error('Common options:')\n console.error(' --debug Enable debug logging')\n console.error(' --allow-unauthenticated-superadmin')\n console.error(' DEV/TEST ONLY: run as superadmin with no per-user ACL when')\n console.error(' no --user (and no --api-key) is supplied. Never use in production.')\n console.error('')\n console.error('Examples:')\n console.error(' OPEN_MERCATO_API_KEY=omk_xxxx.yyyy... mercato ai_assistant mcp:serve')\n console.error(' mercato ai_assistant mcp:serve --api-key omk_xxxx.yyyy...')\n console.error(' mercato ai_assistant mcp:serve --tenant 123e4567-e89b-12d3-a456-426614174000 --user <user-id>')\n console.error(' mercato ai_assistant mcp:serve --tenant 123e4567-e89b-12d3-a456-426614174000 --allow-unauthenticated-superadmin')\n return\n }\n\n await ensureBootstrap()\n const container = await createRequestContainer()\n\n const { runMcpServer } = await import('./lib/mcp-server')\n\n if (apiKey) {\n await runMcpServer({\n config: {\n name: 'open-mercato-mcp',\n version: '0.1.0',\n debug,\n },\n container,\n apiKeySecret: apiKey,\n })\n } else {\n await runMcpServer({\n config: {\n name: 'open-mercato-mcp',\n version: '0.1.0',\n debug,\n },\n container,\n context: {\n tenantId,\n organizationId,\n userId,\n },\n allowUnauthenticatedSuperadmin,\n })\n }\n },\n}\n\nconst MCP_DEFAULT_PORT = 3001\n\nconst mcpServeHttp: ModuleCli = {\n command: 'mcp:serve-http',\n async run(rest) {\n const args = parseArgs(rest)\n const portArg = parseInt(String(args.port ?? ''), 10)\n const port = !portArg || isNaN(portArg) ? MCP_DEFAULT_PORT : portArg\n const debug = args.debug === true || args.debug === 'true'\n const host = resolveMcpHost(\n typeof args.host === 'string' ? args.host : undefined,\n process.env.MCP_HTTP_HOST,\n )\n\n await ensureBootstrap()\n const container = await createRequestContainer()\n\n const { runMcpHttpServer } = await import('./lib/http-server')\n\n await runMcpHttpServer({\n config: {\n name: 'open-mercato-mcp',\n version: '0.1.0',\n debug,\n },\n container,\n port,\n host,\n })\n },\n}\n\nconst mcpDev: ModuleCli = {\n command: 'mcp:dev',\n async run() {\n await ensureBootstrap()\n const { runMcpDevServer } = await import('./lib/mcp-dev-server')\n await runMcpDevServer()\n },\n}\n\nconst mcpEnsureApiKey: ModuleCli = {\n command: 'mcp:ensure-api-key',\n async run(rest) {\n const args = parseArgs(rest)\n // A bare `--file` (no value) parses to boolean true \u2014 treat it as missing\n // rather than provisioning into a file literally named \"true\".\n const fileArg = typeof args.file === 'string' ? args.file.trim() : ''\n const filePath =\n fileArg || process.env.MCP_SERVER_API_KEY_FILE?.trim() || process.env.MCP_API_KEY_FILE?.trim() || ''\n if (!filePath) {\n console.error('Usage: mercato ai_assistant mcp:ensure-api-key --file <path> [options]')\n console.error('')\n console.error('Ensures a valid MCP server API key exists in the database and that its')\n console.error('plaintext secret is stored in <path> (the idempotency anchor). The secret')\n console.error('is never printed; consumers such as the OpenCode container read the file.')\n console.error('')\n console.error('Options:')\n console.error(' --file <path> Secret file path (or MCP_SERVER_API_KEY_FILE env)')\n console.error(' --name <name> API key name (default __mcp_server__)')\n console.error(' --email <email> Owner user email (default OM_INIT_SUPERADMIN_EMAIL, then superadmin@acme.com)')\n console.error(' --rotate Force rotation even when the file secret is still valid')\n process.exitCode = 1\n return\n }\n\n const keyName = typeof args.name === 'string' && args.name.length > 0 ? args.name : undefined\n const ownerEmail = typeof args.email === 'string' && args.email.length > 0 ? args.email : undefined\n const rotate = args.rotate === true || args.rotate === 'true'\n\n await ensureBootstrap()\n const container = await createRequestContainer()\n const em = container.resolve<import('@mikro-orm/postgresql').EntityManager>('em')\n const rbac = container.resolve('rbacService') as import('@open-mercato/core/modules/auth/services/rbacService').RbacService\n\n const { ensureMcpApiKey } = await import('./lib/mcp-ensure-api-key')\n const result = await ensureMcpApiKey({ em, filePath, keyName, ownerEmail, rotate, rbac })\n const message = result.status === 'valid' ? 'Existing key is valid' : 'New key created'\n console.log(`[mcp:ensure-api-key] ${message} (id=${result.keyId}, prefix=${result.keyPrefix})`)\n },\n}\n\nconst listTools: ModuleCli = {\n command: 'mcp:list-tools',\n async run(rest) {\n const args = parseArgs(rest)\n const verbose = args.verbose === true || args.verbose === 'true'\n\n // Ensure bootstrap runs so modules are registered for API discovery\n await ensureBootstrap()\n\n const { loadAllModuleTools } = await import('./lib/tool-loader')\n await loadAllModuleTools()\n\n const { getToolRegistry } = await import('./lib/tool-registry')\n const registry = getToolRegistry()\n const toolNames = registry.listToolNames()\n\n if (toolNames.length === 0) {\n console.log('\\nNo MCP tools registered.')\n console.log('Tools can be registered by modules using registerMcpTool().\\n')\n return\n }\n\n console.log(`\\nRegistered MCP Tools (${toolNames.length}):\\n`)\n\n // Group tools by module\n const byModule = new Map<string, string[]>()\n for (const name of toolNames) {\n const [module] = name.split('.')\n const list = byModule.get(module) ?? []\n list.push(name)\n byModule.set(module, list)\n }\n\n // Sort modules alphabetically\n const sortedModules = Array.from(byModule.keys()).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n\n for (const module of sortedModules) {\n const tools = byModule.get(module)!\n console.log(`${module} (${tools.length} tools):`)\n\n for (const name of tools.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))) {\n const tool = registry.getTool(name)\n if (!tool) continue\n\n if (verbose) {\n console.log(` ${name}`)\n console.log(` ${tool.description}`)\n if (tool.requiredFeatures?.length) {\n console.log(` Requires: ${tool.requiredFeatures.join(', ')}`)\n }\n } else {\n console.log(` - ${name}`)\n }\n }\n console.log('')\n }\n },\n}\n\nconst entityGraph: ModuleCli = {\n command: 'entity-graph',\n async run(rest) {\n const args = parseArgs(rest)\n const format = String(args.format ?? 'triples') as 'json' | 'triples'\n const entity = args.entity ? String(args.entity) : undefined\n const module = args.module ? String(args.module) : undefined\n\n await ensureBootstrap()\n\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n const { extractEntityGraph, formatGraphAsTriples, filterGraphByEntity, filterGraphByModule } = await import(\n './lib/entity-graph'\n )\n\n console.log('[Entity Graph] Extracting from MikroORM metadata...')\n\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n\n // Apply filters\n let edges = graph.edges\n\n if (entity) {\n edges = filterGraphByEntity(graph, entity)\n console.log(`[Entity Graph] Filtered by entity: ${entity}`)\n }\n\n if (module) {\n const filteredGraph = { ...graph, edges }\n edges = filterGraphByModule(filteredGraph, module)\n console.log(`[Entity Graph] Filtered by module: ${module}`)\n }\n\n const filteredGraph = { ...graph, edges }\n\n if (format === 'json') {\n console.log(JSON.stringify(filteredGraph, null, 2))\n } else {\n const triples = formatGraphAsTriples(filteredGraph)\n console.log('')\n for (const triple of triples) {\n console.log(triple)\n }\n }\n\n console.log(`\\n[Entity Graph] ${graph.nodes.length} entities, ${edges.length} relationships`)\n },\n}\n\nconst runPendingActionCleanup: ModuleCli = {\n command: 'run-pending-action-cleanup',\n async run() {\n await ensureBootstrap()\n const container = await createRequestContainer()\n\n const { runPendingActionCleanup: runCleanup } = await import(\n './workers/ai-pending-action-cleanup'\n )\n\n const em = container.resolve<import('@mikro-orm/postgresql').EntityManager>('em')\n const summary = await runCleanup({ em })\n\n console.log('[ai-pending-action-cleanup] Sweep complete:', summary)\n },\n}\n\nconst testTools: ModuleCli = {\n command: 'test-tools',\n async run(rest) {\n const args = parseArgs(rest)\n const json = args.json === true || args.json === 'true'\n const moduleFilter =\n typeof args.module === 'string' && args.module.length > 0 ? args.module : null\n const includeMutations = args['no-mutations'] !== true && args['no-mutations'] !== 'true'\n const tenantId =\n typeof args.tenant === 'string' && args.tenant.length > 0 ? args.tenant : null\n const organizationId =\n typeof args.org === 'string' && args.org.length > 0 ? args.org : null\n\n await ensureBootstrap()\n const { runToolTests } = await import('./lib/tool-test-runner')\n const report = await runToolTests({\n tenantId,\n organizationId,\n moduleFilter,\n includeMutations,\n })\n\n if (json) {\n // Wrap in markers so a Playwright spec (or any caller) can extract the\n // JSON payload without being thrown off by bootstrap log lines emitted\n // to stdout by other modules during DI container creation.\n console.log('---TOOL_TEST_REPORT_BEGIN---')\n console.log(JSON.stringify(report))\n console.log('---TOOL_TEST_REPORT_END---')\n } else {\n console.log('')\n console.log(\n `AI tool test report \u2014 tenant=${report.tenantId ?? '<none>'} org=${\n report.organizationId ?? '<none>'\n }`,\n )\n console.log(\n `total=${report.total} pass=${report.passed} fail=${report.failed} skip=${report.skipped}`,\n )\n const byModule = new Map<string, typeof report.records>()\n for (const record of report.records) {\n const list = byModule.get(record.module) ?? []\n list.push(record)\n byModule.set(record.module, list)\n }\n const sortedModules = Array.from(byModule.keys()).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n for (const moduleId of sortedModules) {\n const list = byModule.get(moduleId)!\n console.log('')\n console.log(`${moduleId} (${list.length}):`)\n for (const record of list) {\n const marker =\n record.status === 'pass' ? '\u2713' : record.status === 'fail' ? '\u2717' : '\u00B7'\n const reason = record.reason ? ` \u2014 ${record.reason}` : ''\n const mutation = record.isMutation ? ' [mutation]' : ''\n console.log(\n ` ${marker} ${record.tool}${mutation} (${record.durationMs}ms)${reason}`,\n )\n }\n }\n console.log('')\n }\n\n if (report.failed > 0) {\n process.exitCode = 1\n }\n },\n}\n\nconst runTokenUsagePrune: ModuleCli = {\n command: 'run-token-usage-prune',\n async run() {\n await ensureBootstrap()\n const container = await createRequestContainer()\n\n const { runTokenUsagePrune: runPrune } = await import(\n './workers/ai-token-usage-prune'\n )\n\n const em = container.resolve<import('@mikro-orm/postgresql').EntityManager>('em')\n const summary = await runPrune({ em })\n\n console.log('[ai-token-usage-prune] Prune complete:', summary)\n },\n}\n\nexport default [\n mcpServe,\n mcpServeHttp,\n mcpDev,\n mcpEnsureApiKey,\n listTools,\n entityGraph,\n runPendingActionCleanup,\n runTokenUsagePrune,\n testTools,\n]\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["filteredGraph"]
|
|
7
7
|
}
|
|
@@ -11,6 +11,7 @@ import { jsonSchemaToZod } from "./schema-utils.js";
|
|
|
11
11
|
import { buildMcpToolAnnotations } from "./mcp-tool-annotations.js";
|
|
12
12
|
import { redactSecretForLog, deriveApiKeySessionId } from "./log-redaction.js";
|
|
13
13
|
import { findApiKeyBySecret, findSessionApiKeyWithSecret } from "@open-mercato/core/modules/api_keys/services/apiKeyService";
|
|
14
|
+
import { isLoopbackHost, resolveMcpHost } from "./mcp-host-resolution.js";
|
|
14
15
|
const logger = createLogger("ai_assistant").child({ component: "mcp-http" });
|
|
15
16
|
async function resolveSessionContext(sessionToken, baseContext, debug) {
|
|
16
17
|
try {
|
|
@@ -295,6 +296,7 @@ async function parseJsonBody(req) {
|
|
|
295
296
|
}
|
|
296
297
|
async function runMcpHttpServer(options) {
|
|
297
298
|
const { config, container, port } = options;
|
|
299
|
+
const host = resolveMcpHost(options.host, void 0);
|
|
298
300
|
await loadAllModuleTools();
|
|
299
301
|
try {
|
|
300
302
|
const { extractEntityGraph, cacheEntityGraph } = await import("./entity-graph.js");
|
|
@@ -437,9 +439,12 @@ async function runMcpHttpServer(options) {
|
|
|
437
439
|
logger.info("Mode: stateless (new server per request)");
|
|
438
440
|
logger.info("Server auth: API key validated against database (x-api-key header)");
|
|
439
441
|
logger.info("User auth: session token (_sessionToken) preferred, falls back to API key roles");
|
|
442
|
+
if (!isLoopbackHost(host)) {
|
|
443
|
+
logger.warn("Binding beyond loopback \u2014 ensure a firewall and a strong, rotated MCP_SERVER_API_KEY", { host });
|
|
444
|
+
}
|
|
440
445
|
return new Promise((resolve) => {
|
|
441
|
-
httpServer.listen(port, () => {
|
|
442
|
-
logger.info("Server listening", { port });
|
|
446
|
+
httpServer.listen(port, host, () => {
|
|
447
|
+
logger.info("Server listening", { host, port });
|
|
443
448
|
});
|
|
444
449
|
const shutdown = async () => {
|
|
445
450
|
logger.info("Shutting down");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/http-server.ts"],
|
|
4
|
-
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { 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 type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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, toSafeZodSchema } from './schema-utils'\nimport { buildMcpToolAnnotations } from './mcp-tool-annotations'\nimport { redactSecretForLog, deriveApiKeySessionId } from './log-redaction'\nimport type { McpServerConfig, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { ApiKey } from '@open-mercato/core/modules/api_keys/data/entities'\nimport { findApiKeyBySecret, findSessionApiKeyWithSecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\n\nconst logger = createLogger('ai_assistant').child({ component: 'mcp-http' })\n\n/**\n * Options for the HTTP MCP server.\n */\nexport type McpHttpServerOptions = {\n config: McpServerConfig\n container: AwilixContainer\n port: number\n}\n\n/**\n * Resolve user context from session token.\n * Returns null if session token is invalid or expired.\n * Includes the decrypted API key secret for making authenticated API calls.\n */\nasync function resolveSessionContext(\n sessionToken: string,\n baseContext: McpToolContext,\n debug?: boolean\n): Promise<McpToolContext | null> {\n try {\n const em = baseContext.container.resolve<EntityManager>('em')\n const rbacService = baseContext.container.resolve<RbacService>('rbacService')\n\n // Look up ephemeral key by session token with decrypted secret\n const sessionResult = await findSessionApiKeyWithSecret(em, sessionToken)\n if (!sessionResult) {\n if (debug) {\n logger.debug('Session token not found, expired, or secret unavailable', { sessionToken: redactSecretForLog(sessionToken) })\n }\n return null\n }\n\n const { key: sessionKey, secret: sessionSecret } = sessionResult\n\n // Load ACL for the session user\n const userId = sessionKey.sessionUserId || sessionKey.createdBy\n if (!userId) {\n if (debug) {\n logger.debug('Session key has no associated user')\n }\n return null\n }\n\n const acl = await rbacService.loadAcl(`api_key:${sessionKey.id}`, {\n tenantId: sessionKey.tenantId ?? null,\n organizationId: sessionKey.organizationId ?? null,\n })\n\n if (debug) {\n logger.debug('Session context resolved', {\n userId,\n tenantId: sessionKey.tenantId,\n organizationId: sessionKey.organizationId,\n features: acl.features.length,\n isSuperAdmin: acl.isSuperAdmin,\n hasSessionSecret: !!sessionSecret,\n })\n }\n\n return {\n tenantId: sessionKey.tenantId ?? null,\n organizationId: sessionKey.organizationId ?? null,\n userId,\n container: baseContext.container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n // Use the decrypted session secret for API calls (not the MCP server key)\n apiKeySecret: sessionSecret,\n }\n } catch (error) {\n if (debug) {\n logger.debug('Error resolving session context', { err: error })\n }\n return null\n }\n}\n\n/**\n * Resolve user context from the server-level API key (header-based auth fallback).\n * Used when no session token is provided \u2014 loads the API key's ACL for RBAC.\n */\nasync function resolveApiKeyContext(\n apiKeyRecord: ApiKey,\n baseContext: McpToolContext,\n debug?: boolean\n): Promise<McpToolContext | null> {\n try {\n const rbacService = baseContext.container.resolve<RbacService>('rbacService')\n const userId = apiKeyRecord.sessionUserId ?? apiKeyRecord.createdBy\n if (!userId) {\n if (debug) {\n logger.debug('API key has no associated user')\n }\n return null\n }\n\n const acl = await rbacService.loadAcl(`api_key:${apiKeyRecord.id}`, {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n })\n\n if (debug) {\n logger.debug('API key context resolved', {\n userId,\n tenantId: apiKeyRecord.tenantId,\n organizationId: apiKeyRecord.organizationId,\n features: acl.features.length,\n isSuperAdmin: acl.isSuperAdmin,\n })\n }\n\n return {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n userId,\n container: baseContext.container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n apiKeySecret: baseContext.apiKeySecret,\n }\n } catch (error) {\n if (debug) {\n logger.debug('Error resolving API key context', { err: error })\n }\n return null\n }\n}\n\n/**\n * Create a stateless MCP server instance for a single request.\n * Tools are registered without pre-filtering - permission checks happen at execution time\n * based on the session token provided in each tool call.\n */\nfunction createMcpServerForRequest(\n config: McpServerConfig,\n toolContext: McpToolContext,\n apiKeyRecord: ApiKey\n): McpServer {\n const server = new McpServer(\n { name: config.name, version: config.version },\n { capabilities: { tools: {} } }\n )\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n if (config.debug) {\n logger.debug('Registering tools (ACL checked per-call via session token)', { toolCount: tools.length })\n }\n\n // Register ALL tools - permission checks happen at execution time via session token\n for (const tool of tools) {\n if (config.debug) {\n logger.debug('Registering tool', { toolName: tool.name })\n }\n\n // Convert Zod schema to a \"safe\" schema without Date types\n // This uses JSON Schema round-trip to avoid issues with MCP SDK's internal conversion\n // Also inject _sessionToken as an optional parameter so the AI knows to pass it\n let safeSchema: ZodType | undefined\n if (tool.inputSchema) {\n try {\n // Convert to JSON Schema first\n const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>\n\n // Inject _sessionToken into the JSON schema properties\n const properties = (jsonSchema.properties ?? {}) as Record<string, unknown>\n properties._sessionToken = {\n type: 'string',\n description: 'Session authorization token. If omitted, the server API key roles are used instead.',\n }\n jsonSchema.properties = properties\n\n // Convert back to Zod with passthrough to allow extra properties\n const converted = jsonSchemaToZod(jsonSchema)\n // Use type assertion since we know it's an object schema (we added properties above)\n safeSchema = (converted as z.ZodObject<any>).passthrough()\n } catch (error) {\n if (config.debug) {\n logger.debug('Skipping tool: schema conversion failed', { toolName: tool.name, err: error })\n }\n continue\n }\n } else {\n // If no schema, create one with just _sessionToken\n safeSchema = z.object({\n _sessionToken: z\n .string()\n .optional()\n .describe('Session authorization token (REQUIRED for all tool calls)'),\n })\n }\n\n // Wrap in try/catch to handle any remaining edge cases\n try {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: safeSchema,\n annotations: buildMcpToolAnnotations(tool),\n },\n async (args: unknown) => {\n const toolArgs = (args ?? {}) as Record<string, unknown>\n\n // Extract session token from args\n const sessionToken = toolArgs._sessionToken as string | undefined\n delete toolArgs._sessionToken // Remove before passing to tool handler\n\n // Always log tool calls for debugging\n logger.debug('Tool call received', {\n toolName: tool.name,\n hasSessionToken: !!sessionToken,\n argKeys: Object.keys(toolArgs ?? {}).join(','),\n })\n\n // Resolve user context from session token\n let effectiveContext = toolContext\n if (sessionToken) {\n const sessionContext = await resolveSessionContext(sessionToken, toolContext, config.debug)\n if (sessionContext) {\n // Session context includes the decrypted API key secret + session ID for memory layer\n effectiveContext = { ...sessionContext, sessionId: sessionToken }\n } else {\n // Session token expired - return user-friendly error for AI to relay\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: 'Your chat session has expired. Please close and reopen the chat window to continue.',\n code: 'SESSION_EXPIRED',\n }),\n },\n ],\n isError: true,\n }\n }\n } else {\n // No session token \u2014 fall back to header API key auth\n const apiKeyContext = await resolveApiKeyContext(apiKeyRecord, toolContext, config.debug)\n if (apiKeyContext) {\n effectiveContext = apiKeyContext\n } else if (!effectiveContext.userId && effectiveContext.userFeatures.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: 'Authentication failed: provide a session token (_sessionToken) or a valid API key with assigned roles',\n code: 'UNAUTHORIZED',\n }),\n },\n ],\n isError: true,\n }\n }\n\n // Derive a fallback sessionId from the API key so all tool calls\n // within the same MCP connection share a session memory cache\n if (!effectiveContext.sessionId && effectiveContext.apiKeySecret) {\n effectiveContext = {\n ...effectiveContext,\n sessionId: deriveApiKeySessionId(effectiveContext.apiKeySecret),\n }\n }\n }\n\n // Check if user has required permissions for this tool\n if (tool.requiredFeatures?.length) {\n const rbacService = effectiveContext.container.resolve<RbacService>('rbacService')\n const hasAccess = hasRequiredFeatures(\n tool.requiredFeatures,\n effectiveContext.userFeatures,\n effectiveContext.isSuperAdmin,\n rbacService\n )\n if (!hasAccess) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: `Insufficient permissions for tool \"${tool.name}\". Required: ${tool.requiredFeatures.join(', ')}`,\n code: 'UNAUTHORIZED',\n }),\n },\n ],\n isError: true,\n }\n }\n }\n\n try {\n const result = await executeTool(tool.name, toolArgs, effectiveContext)\n\n if (!result.success) {\n logger.error('Tool call failed', { toolName: tool.name, err: result.error, code: result.errorCode })\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 logger.debug('Tool call succeeded', { toolName: tool.name })\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n } catch (err) {\n logger.error('Tool call threw', { toolName: tool.name, err })\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: err instanceof Error ? err.message : 'Unknown error', code: 'EXCEPTION' }),\n },\n ],\n isError: true,\n }\n }\n }\n )\n } catch (error) {\n // Skip tools with schemas that can't be registered\n if (config.debug) {\n logger.debug('Skipping tool: registration failed', { toolName: tool.name, err: error })\n }\n continue\n }\n }\n\n return server\n}\n\n/**\n * Maximum request body size (1MB).\n * Prevents memory exhaustion from oversized payloads.\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 * Run MCP server with HTTP transport (stateless mode).\n *\n * Each request creates a new MCP server instance and transport.\n * The server authenticates requests using API keys from the x-api-key header.\n */\nexport async function runMcpHttpServer(options: McpHttpServerOptions): Promise<void> {\n const { config, container, port } = options\n\n await loadAllModuleTools()\n\n // Generate and cache entity graph for understand_entity tool\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n logger.info('Entity graph generated', { entities: graph.nodes.length, relationships: graph.edges.length })\n } catch (error) {\n logger.warn('Entity graph generation skipped', { err: 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 logger.info('Rich OpenAPI spec cached for Code Mode (with requestBody schemas)')\n } else {\n logger.warn('OpenAPI spec not available')\n }\n } catch (error) {\n logger.warn('OpenAPI spec caching skipped', { err: error })\n }\n\n // Index tools and entity schemas for hybrid search discovery (if search service available)\n try {\n const searchService = container.resolve('searchService') as SearchService\n\n // Index MCP tools\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 logger.info('Indexed entity schemas for hybrid search', { count })\n }\n }\n } catch (entityError) {\n logger.warn('Entity schema indexing skipped', { err: entityError })\n }\n } catch (error) {\n // Search service might not be configured - discovery will use fallback\n logger.warn('Search indexing skipped (search service not available)', { err: error })\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 tools: getToolRegistry().listToolNames().length,\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 logger.debug('Request received', { method: req.method, path: url.pathname })\n\n // Extract headers\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 // Server-level authentication via database lookup\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 API key against database (prefix lookup + bcrypt verify + expiry check)\n const em = container.resolve<EntityManager>('em')\n const apiKeyRecord = await findApiKeyBySecret(em, providedApiKey)\n if (!apiKeyRecord) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Invalid or expired API key' }))\n return\n }\n\n if (config.debug) {\n logger.debug('Server-level auth passed', { method: req.method, keyPrefix: apiKeyRecord.keyPrefix })\n }\n\n // Create base tool context using API key's tenant/org scope\n // Session tokens can override with user-specific permissions\n const toolContext: McpToolContext = {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n userId: apiKeyRecord.createdBy ?? null,\n container,\n userFeatures: [],\n isSuperAdmin: false,\n apiKeySecret: providedApiKey,\n }\n\n try {\n // Create stateless transport (no session ID generator = stateless)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: req.method === 'POST',\n })\n\n // Create new server for this request\n const mcpServer = createMcpServerForRequest(config, toolContext, apiKeyRecord)\n\n if (config.debug) {\n // Check registered tools on the server\n const registeredTools = (mcpServer as any)._registeredTools || {}\n logger.debug('Registered tools in McpServer', { toolNames: Object.keys(registeredTools).join(',') })\n logger.debug('Tool handlers initialized', { initialized: (mcpServer as any)._toolHandlersInitialized })\n }\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 (config.debug) {\n logger.debug('Request completed, cleaned up')\n }\n })\n } catch (error) {\n logger.error('Error handling request', { err: error })\n if (!res.headersSent) {\n // Handle payload too large error\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 logger.info('Starting MCP HTTP server', { name: config.name, version: config.version })\n logger.info('Endpoint ready', { endpoint: `http://localhost:${port}/mcp` })\n logger.info('Health endpoint ready', { endpoint: `http://localhost:${port}/health` })\n logger.info('Tools registered', { toolCount })\n logger.info('Mode: stateless (new server per request)')\n logger.info('Server auth: API key validated against database (x-api-key header)')\n logger.info('User auth: session token (_sessionToken) preferred, falls back to API key roles')\n\n // Return a Promise that keeps the process alive until shutdown\n return new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n logger.info('Server listening', { port })\n })\n\n const shutdown = async () => {\n logger.info('Shutting down')\n httpServer.close(() => {\n logger.info('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 { createLogger } from '@open-mercato/shared/lib/logger'\nimport { 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 type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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, toSafeZodSchema } from './schema-utils'\nimport { buildMcpToolAnnotations } from './mcp-tool-annotations'\nimport { redactSecretForLog, deriveApiKeySessionId } from './log-redaction'\nimport type { McpServerConfig, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport type { ApiKey } from '@open-mercato/core/modules/api_keys/data/entities'\nimport { findApiKeyBySecret, findSessionApiKeyWithSecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { isLoopbackHost, resolveMcpHost } from './mcp-host-resolution'\n\nconst logger = createLogger('ai_assistant').child({ component: 'mcp-http' })\n\n/**\n * Options for the HTTP MCP server.\n */\nexport type McpHttpServerOptions = {\n config: McpServerConfig\n container: AwilixContainer\n port: number\n /**\n * Bind address. Defaults to loopback (`127.0.0.1`) \u2014 the only intended client is OpenCode\n * running in Docker, which reaches host loopback via `host.docker.internal` on Docker Desktop.\n * On native Linux `host-gateway` is the bridge IP, so `yarn dev` passes a wider host there\n * (see `resolveDevMcpHost` in `scripts/dev-mcp.mjs`). Omitting a host to `net.Server.listen` binds every interface\n * (`0.0.0.0`/`::`), which exposes the server (and its shared API key) off-host; pass this\n * explicitly to opt into that, paired with a firewall and a strong, rotated key.\n */\n host?: string\n}\n\n/**\n * Resolve user context from session token.\n * Returns null if session token is invalid or expired.\n * Includes the decrypted API key secret for making authenticated API calls.\n */\nasync function resolveSessionContext(\n sessionToken: string,\n baseContext: McpToolContext,\n debug?: boolean\n): Promise<McpToolContext | null> {\n try {\n const em = baseContext.container.resolve<EntityManager>('em')\n const rbacService = baseContext.container.resolve<RbacService>('rbacService')\n\n // Look up ephemeral key by session token with decrypted secret\n const sessionResult = await findSessionApiKeyWithSecret(em, sessionToken)\n if (!sessionResult) {\n if (debug) {\n logger.debug('Session token not found, expired, or secret unavailable', { sessionToken: redactSecretForLog(sessionToken) })\n }\n return null\n }\n\n const { key: sessionKey, secret: sessionSecret } = sessionResult\n\n // Load ACL for the session user\n const userId = sessionKey.sessionUserId || sessionKey.createdBy\n if (!userId) {\n if (debug) {\n logger.debug('Session key has no associated user')\n }\n return null\n }\n\n const acl = await rbacService.loadAcl(`api_key:${sessionKey.id}`, {\n tenantId: sessionKey.tenantId ?? null,\n organizationId: sessionKey.organizationId ?? null,\n })\n\n if (debug) {\n logger.debug('Session context resolved', {\n userId,\n tenantId: sessionKey.tenantId,\n organizationId: sessionKey.organizationId,\n features: acl.features.length,\n isSuperAdmin: acl.isSuperAdmin,\n hasSessionSecret: !!sessionSecret,\n })\n }\n\n return {\n tenantId: sessionKey.tenantId ?? null,\n organizationId: sessionKey.organizationId ?? null,\n userId,\n container: baseContext.container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n // Use the decrypted session secret for API calls (not the MCP server key)\n apiKeySecret: sessionSecret,\n }\n } catch (error) {\n if (debug) {\n logger.debug('Error resolving session context', { err: error })\n }\n return null\n }\n}\n\n/**\n * Resolve user context from the server-level API key (header-based auth fallback).\n * Used when no session token is provided \u2014 loads the API key's ACL for RBAC.\n */\nasync function resolveApiKeyContext(\n apiKeyRecord: ApiKey,\n baseContext: McpToolContext,\n debug?: boolean\n): Promise<McpToolContext | null> {\n try {\n const rbacService = baseContext.container.resolve<RbacService>('rbacService')\n const userId = apiKeyRecord.sessionUserId ?? apiKeyRecord.createdBy\n if (!userId) {\n if (debug) {\n logger.debug('API key has no associated user')\n }\n return null\n }\n\n const acl = await rbacService.loadAcl(`api_key:${apiKeyRecord.id}`, {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n })\n\n if (debug) {\n logger.debug('API key context resolved', {\n userId,\n tenantId: apiKeyRecord.tenantId,\n organizationId: apiKeyRecord.organizationId,\n features: acl.features.length,\n isSuperAdmin: acl.isSuperAdmin,\n })\n }\n\n return {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n userId,\n container: baseContext.container,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n apiKeySecret: baseContext.apiKeySecret,\n }\n } catch (error) {\n if (debug) {\n logger.debug('Error resolving API key context', { err: error })\n }\n return null\n }\n}\n\n/**\n * Create a stateless MCP server instance for a single request.\n * Tools are registered without pre-filtering - permission checks happen at execution time\n * based on the session token provided in each tool call.\n */\nfunction createMcpServerForRequest(\n config: McpServerConfig,\n toolContext: McpToolContext,\n apiKeyRecord: ApiKey\n): McpServer {\n const server = new McpServer(\n { name: config.name, version: config.version },\n { capabilities: { tools: {} } }\n )\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n if (config.debug) {\n logger.debug('Registering tools (ACL checked per-call via session token)', { toolCount: tools.length })\n }\n\n // Register ALL tools - permission checks happen at execution time via session token\n for (const tool of tools) {\n if (config.debug) {\n logger.debug('Registering tool', { toolName: tool.name })\n }\n\n // Convert Zod schema to a \"safe\" schema without Date types\n // This uses JSON Schema round-trip to avoid issues with MCP SDK's internal conversion\n // Also inject _sessionToken as an optional parameter so the AI knows to pass it\n let safeSchema: ZodType | undefined\n if (tool.inputSchema) {\n try {\n // Convert to JSON Schema first\n const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>\n\n // Inject _sessionToken into the JSON schema properties\n const properties = (jsonSchema.properties ?? {}) as Record<string, unknown>\n properties._sessionToken = {\n type: 'string',\n description: 'Session authorization token. If omitted, the server API key roles are used instead.',\n }\n jsonSchema.properties = properties\n\n // Convert back to Zod with passthrough to allow extra properties\n const converted = jsonSchemaToZod(jsonSchema)\n // Use type assertion since we know it's an object schema (we added properties above)\n safeSchema = (converted as z.ZodObject<any>).passthrough()\n } catch (error) {\n if (config.debug) {\n logger.debug('Skipping tool: schema conversion failed', { toolName: tool.name, err: error })\n }\n continue\n }\n } else {\n // If no schema, create one with just _sessionToken\n safeSchema = z.object({\n _sessionToken: z\n .string()\n .optional()\n .describe('Session authorization token (REQUIRED for all tool calls)'),\n })\n }\n\n // Wrap in try/catch to handle any remaining edge cases\n try {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: safeSchema,\n annotations: buildMcpToolAnnotations(tool),\n },\n async (args: unknown) => {\n const toolArgs = (args ?? {}) as Record<string, unknown>\n\n // Extract session token from args\n const sessionToken = toolArgs._sessionToken as string | undefined\n delete toolArgs._sessionToken // Remove before passing to tool handler\n\n // Always log tool calls for debugging\n logger.debug('Tool call received', {\n toolName: tool.name,\n hasSessionToken: !!sessionToken,\n argKeys: Object.keys(toolArgs ?? {}).join(','),\n })\n\n // Resolve user context from session token\n let effectiveContext = toolContext\n if (sessionToken) {\n const sessionContext = await resolveSessionContext(sessionToken, toolContext, config.debug)\n if (sessionContext) {\n // Session context includes the decrypted API key secret + session ID for memory layer\n effectiveContext = { ...sessionContext, sessionId: sessionToken }\n } else {\n // Session token expired - return user-friendly error for AI to relay\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: 'Your chat session has expired. Please close and reopen the chat window to continue.',\n code: 'SESSION_EXPIRED',\n }),\n },\n ],\n isError: true,\n }\n }\n } else {\n // No session token \u2014 fall back to header API key auth\n const apiKeyContext = await resolveApiKeyContext(apiKeyRecord, toolContext, config.debug)\n if (apiKeyContext) {\n effectiveContext = apiKeyContext\n } else if (!effectiveContext.userId && effectiveContext.userFeatures.length === 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: 'Authentication failed: provide a session token (_sessionToken) or a valid API key with assigned roles',\n code: 'UNAUTHORIZED',\n }),\n },\n ],\n isError: true,\n }\n }\n\n // Derive a fallback sessionId from the API key so all tool calls\n // within the same MCP connection share a session memory cache\n if (!effectiveContext.sessionId && effectiveContext.apiKeySecret) {\n effectiveContext = {\n ...effectiveContext,\n sessionId: deriveApiKeySessionId(effectiveContext.apiKeySecret),\n }\n }\n }\n\n // Check if user has required permissions for this tool\n if (tool.requiredFeatures?.length) {\n const rbacService = effectiveContext.container.resolve<RbacService>('rbacService')\n const hasAccess = hasRequiredFeatures(\n tool.requiredFeatures,\n effectiveContext.userFeatures,\n effectiveContext.isSuperAdmin,\n rbacService\n )\n if (!hasAccess) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: `Insufficient permissions for tool \"${tool.name}\". Required: ${tool.requiredFeatures.join(', ')}`,\n code: 'UNAUTHORIZED',\n }),\n },\n ],\n isError: true,\n }\n }\n }\n\n try {\n const result = await executeTool(tool.name, toolArgs, effectiveContext)\n\n if (!result.success) {\n logger.error('Tool call failed', { toolName: tool.name, err: result.error, code: result.errorCode })\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 logger.debug('Tool call succeeded', { toolName: tool.name })\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n } catch (err) {\n logger.error('Tool call threw', { toolName: tool.name, err })\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: err instanceof Error ? err.message : 'Unknown error', code: 'EXCEPTION' }),\n },\n ],\n isError: true,\n }\n }\n }\n )\n } catch (error) {\n // Skip tools with schemas that can't be registered\n if (config.debug) {\n logger.debug('Skipping tool: registration failed', { toolName: tool.name, err: error })\n }\n continue\n }\n }\n\n return server\n}\n\n/**\n * Maximum request body size (1MB).\n * Prevents memory exhaustion from oversized payloads.\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 * Run MCP server with HTTP transport (stateless mode).\n *\n * Each request creates a new MCP server instance and transport.\n * The server authenticates requests using API keys from the x-api-key header.\n */\nexport async function runMcpHttpServer(options: McpHttpServerOptions): Promise<void> {\n const { config, container, port } = options\n const host = resolveMcpHost(options.host, undefined)\n\n await loadAllModuleTools()\n\n // Generate and cache entity graph for understand_entity tool\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n logger.info('Entity graph generated', { entities: graph.nodes.length, relationships: graph.edges.length })\n } catch (error) {\n logger.warn('Entity graph generation skipped', { err: 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 logger.info('Rich OpenAPI spec cached for Code Mode (with requestBody schemas)')\n } else {\n logger.warn('OpenAPI spec not available')\n }\n } catch (error) {\n logger.warn('OpenAPI spec caching skipped', { err: error })\n }\n\n // Index tools and entity schemas for hybrid search discovery (if search service available)\n try {\n const searchService = container.resolve('searchService') as SearchService\n\n // Index MCP tools\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 logger.info('Indexed entity schemas for hybrid search', { count })\n }\n }\n } catch (entityError) {\n logger.warn('Entity schema indexing skipped', { err: entityError })\n }\n } catch (error) {\n // Search service might not be configured - discovery will use fallback\n logger.warn('Search indexing skipped (search service not available)', { err: error })\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 tools: getToolRegistry().listToolNames().length,\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 logger.debug('Request received', { method: req.method, path: url.pathname })\n\n // Extract headers\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 // Server-level authentication via database lookup\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 API key against database (prefix lookup + bcrypt verify + expiry check)\n const em = container.resolve<EntityManager>('em')\n const apiKeyRecord = await findApiKeyBySecret(em, providedApiKey)\n if (!apiKeyRecord) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Invalid or expired API key' }))\n return\n }\n\n if (config.debug) {\n logger.debug('Server-level auth passed', { method: req.method, keyPrefix: apiKeyRecord.keyPrefix })\n }\n\n // Create base tool context using API key's tenant/org scope\n // Session tokens can override with user-specific permissions\n const toolContext: McpToolContext = {\n tenantId: apiKeyRecord.tenantId ?? null,\n organizationId: apiKeyRecord.organizationId ?? null,\n userId: apiKeyRecord.createdBy ?? null,\n container,\n userFeatures: [],\n isSuperAdmin: false,\n apiKeySecret: providedApiKey,\n }\n\n try {\n // Create stateless transport (no session ID generator = stateless)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: req.method === 'POST',\n })\n\n // Create new server for this request\n const mcpServer = createMcpServerForRequest(config, toolContext, apiKeyRecord)\n\n if (config.debug) {\n // Check registered tools on the server\n const registeredTools = (mcpServer as any)._registeredTools || {}\n logger.debug('Registered tools in McpServer', { toolNames: Object.keys(registeredTools).join(',') })\n logger.debug('Tool handlers initialized', { initialized: (mcpServer as any)._toolHandlersInitialized })\n }\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 (config.debug) {\n logger.debug('Request completed, cleaned up')\n }\n })\n } catch (error) {\n logger.error('Error handling request', { err: error })\n if (!res.headersSent) {\n // Handle payload too large error\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 logger.info('Starting MCP HTTP server', { name: config.name, version: config.version })\n logger.info('Endpoint ready', { endpoint: `http://localhost:${port}/mcp` })\n logger.info('Health endpoint ready', { endpoint: `http://localhost:${port}/health` })\n logger.info('Tools registered', { toolCount })\n logger.info('Mode: stateless (new server per request)')\n logger.info('Server auth: API key validated against database (x-api-key header)')\n logger.info('User auth: session token (_sessionToken) preferred, falls back to API key roles')\n if (!isLoopbackHost(host)) {\n logger.warn('Binding beyond loopback \u2014 ensure a firewall and a strong, rotated MCP_SERVER_API_KEY', { host })\n }\n\n // Return a Promise that keeps the process alive until shutdown\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n logger.info('Server listening', { host, port })\n })\n\n const shutdown = async () => {\n logger.info('Shutting down')\n httpServer.close(() => {\n logger.info('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
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -10,11 +11,18 @@ import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures }
|
|
|
10
11
|
import { jsonSchemaToZod } from "./schema-utils.js";
|
|
11
12
|
import { buildMcpToolAnnotations } from "./mcp-tool-annotations.js";
|
|
12
13
|
import { getApiKeyFromMcpJson } from "./mcp-dev-key-resolution.js";
|
|
14
|
+
import { isLoopbackHost, resolveMcpHost } from "./mcp-host-resolution.js";
|
|
13
15
|
const logger = createLogger("ai_assistant");
|
|
14
16
|
const DEFAULT_PORT = 3001;
|
|
15
17
|
const log = (message, ...args) => {
|
|
16
18
|
logger.info(message, args.length > 0 ? { details: args.map((arg) => String(arg)).join(" ") } : void 0);
|
|
17
19
|
};
|
|
20
|
+
function isMatchingApiKey(expected, provided) {
|
|
21
|
+
const expectedBuffer = Buffer.from(expected);
|
|
22
|
+
const providedBuffer = Buffer.from(provided);
|
|
23
|
+
if (expectedBuffer.length !== providedBuffer.length) return false;
|
|
24
|
+
return timingSafeEqual(expectedBuffer, providedBuffer);
|
|
25
|
+
}
|
|
18
26
|
const MAX_BODY_SIZE = 1 * 1024 * 1024;
|
|
19
27
|
async function parseJsonBody(req) {
|
|
20
28
|
return new Promise((resolve, reject) => {
|
|
@@ -121,6 +129,7 @@ function createDevMcpServer(toolContext, authFeatures, isSuperAdmin, debug) {
|
|
|
121
129
|
async function runMcpDevServer() {
|
|
122
130
|
const apiKey = await getApiKeyFromMcpJson();
|
|
123
131
|
const port = parseInt(process.env.MCP_DEV_PORT ?? "", 10) || DEFAULT_PORT;
|
|
132
|
+
const host = resolveMcpHost(void 0, process.env.MCP_DEV_HOST);
|
|
124
133
|
const debug = process.env.MCP_DEBUG === "true";
|
|
125
134
|
if (!apiKey) {
|
|
126
135
|
log("Error: API key not found in .mcp.json");
|
|
@@ -242,7 +251,7 @@ async function runMcpDevServer() {
|
|
|
242
251
|
res.end(JSON.stringify({ error: "API key required (x-api-key header)" }));
|
|
243
252
|
return;
|
|
244
253
|
}
|
|
245
|
-
if (providedApiKey
|
|
254
|
+
if (!isMatchingApiKey(apiKey, providedApiKey)) {
|
|
246
255
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
247
256
|
res.end(JSON.stringify({ error: "Invalid API key" }));
|
|
248
257
|
return;
|
|
@@ -297,9 +306,12 @@ async function runMcpDevServer() {
|
|
|
297
306
|
log(`Endpoint: http://localhost:${port}/mcp`);
|
|
298
307
|
log(`Health: http://localhost:${port}/health`);
|
|
299
308
|
log(`Mode: Development (API key auth, no session tokens)`);
|
|
309
|
+
if (!isLoopbackHost(host)) {
|
|
310
|
+
log(`WARNING: binding to ${host} instead of loopback \u2014 this server grants admin-equivalent access to any caller who passes the single API-key check`);
|
|
311
|
+
}
|
|
300
312
|
return new Promise((resolve) => {
|
|
301
|
-
httpServer.listen(port, () => {
|
|
302
|
-
log(`Server listening on
|
|
313
|
+
httpServer.listen(port, host, () => {
|
|
314
|
+
log(`Server listening on ${host}:${port}`);
|
|
303
315
|
log("Ready for Claude Code connections");
|
|
304
316
|
});
|
|
305
317
|
const shutdown = async () => {
|
|
@@ -314,6 +326,7 @@ async function runMcpDevServer() {
|
|
|
314
326
|
});
|
|
315
327
|
}
|
|
316
328
|
export {
|
|
329
|
+
isMatchingApiKey,
|
|
317
330
|
runMcpDevServer
|
|
318
331
|
};
|
|
319
332
|
//# sourceMappingURL=mcp-dev-server.js.map
|
|
@@ -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 { createLogger } from '@open-mercato/shared/lib/logger'\nimport { 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 { buildMcpToolAnnotations } from './mcp-tool-annotations'\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 logger = createLogger('ai_assistant')\n\nconst DEFAULT_PORT = 3001\n\nconst log = (message: string, ...args: unknown[]) => {\n logger.info(message, args.length > 0 ? { details: args.map((arg) => String(arg)).join(' ') } : undefined)\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 annotations: buildMcpToolAnnotations(tool),\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": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAA+D;AACxE,SAAS,iBAAiB;AAC1B,SAAS,qCAAqC;AAC9C,SAAS,SAAuB;AAChC,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB,2BAA2B;AACxD,SAAS,wBAAwB,0BAA0B,2BAA2B;AACtF,SAAS,uBAAuB;AAChC,SAAS,+BAA+B;AACxC,SAAS,4BAA4B;
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { timingSafeEqual } from 'node:crypto'\nimport { 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 { buildMcpToolAnnotations } from './mcp-tool-annotations'\nimport { getApiKeyFromMcpJson } from './mcp-dev-key-resolution'\nimport { isLoopbackHost, resolveMcpHost } from './mcp-host-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 logger = createLogger('ai_assistant')\n\nconst DEFAULT_PORT = 3001\n\nconst log = (message: string, ...args: unknown[]) => {\n logger.info(message, args.length > 0 ? { details: args.map((arg) => String(arg)).join(' ') } : undefined)\n}\n\n/**\n * Constant-time API-key comparison, length-guarded (`timingSafeEqual` throws on a length\n * mismatch rather than reporting `false`). A plain `!==` short-circuits at the first\n * differing byte, leaking a timing side channel an attacker can use to recover the key\n * one byte at a time \u2014 AGENTS.md mandates constant-time comparison for this class of check.\n */\nexport function isMatchingApiKey(expected: string, provided: string): boolean {\n const expectedBuffer = Buffer.from(expected)\n const providedBuffer = Buffer.from(provided)\n if (expectedBuffer.length !== providedBuffer.length) return false\n return timingSafeEqual(expectedBuffer, providedBuffer)\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 annotations: buildMcpToolAnnotations(tool),\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 // Loopback by default: this server authenticates once at startup and reuses a single,\n // superadmin-equivalent context for every request for the life of the process (no\n // per-request ACL re-resolution), so exposing it beyond loopback turns a leaked/brute-forced\n // API key into admin-equivalent access to tenant data. Opt in explicitly if the dev port\n // genuinely needs to be reachable off-host (e.g. a Docker bridge).\n const host = resolveMcpHost(undefined, process.env.MCP_DEV_HOST)\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 (constant-time; see isMatchingApiKey)\n if (!isMatchingApiKey(apiKey, providedApiKey)) {\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 if (!isLoopbackHost(host)) {\n log(`WARNING: binding to ${host} instead of loopback \u2014 this server grants admin-equivalent access to any caller who passes the single API-key check`)\n }\n\n return new Promise<void>((resolve) => {\n httpServer.listen(port, host, () => {\n log(`Server listening on ${host}:${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
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const DEFAULT_MCP_HOST = "127.0.0.1";
|
|
2
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
3
|
+
function isLoopbackHost(host) {
|
|
4
|
+
return LOOPBACK_HOSTS.has(host.trim().toLowerCase());
|
|
5
|
+
}
|
|
6
|
+
function resolveMcpHost(explicitHost, envHost) {
|
|
7
|
+
const explicit = explicitHost?.trim();
|
|
8
|
+
if (explicit) return explicit;
|
|
9
|
+
const fromEnv = envHost?.trim();
|
|
10
|
+
if (fromEnv) return fromEnv;
|
|
11
|
+
return DEFAULT_MCP_HOST;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
DEFAULT_MCP_HOST,
|
|
15
|
+
isLoopbackHost,
|
|
16
|
+
resolveMcpHost
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=mcp-host-resolution.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/mcp-host-resolution.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared bind-address resolution for the MCP servers (issue #2659, #2670). Loopback by\n * default; an explicit CLI flag or env var opts into a wider bind. Pulled out as pure\n * functions so the precedence and the loopback-alias set are unit-testable without\n * starting a listener.\n */\n\nexport const DEFAULT_MCP_HOST = '127.0.0.1'\n\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]'])\n\n/**\n * True for every hostname that already means \"this machine only\" \u2014 used to skip the\n * \"binding beyond loopback\" warning for a caller who wrote `localhost`/`::1` instead of\n * the canonical `127.0.0.1`, which is not the wider-exposure case that warning is for.\n */\nexport function isLoopbackHost(host: string): boolean {\n return LOOPBACK_HOSTS.has(host.trim().toLowerCase())\n}\n\n/**\n * Precedence: an explicit value (CLI flag or constructor option) wins, then the env var,\n * then the loopback default. Both inputs are trimmed; an empty string is treated as unset,\n * consistent with how the rest of the CLI reads optional string args/env vars.\n */\nexport function resolveMcpHost(explicitHost: string | undefined, envHost: string | undefined): string {\n const explicit = explicitHost?.trim()\n if (explicit) return explicit\n const fromEnv = envHost?.trim()\n if (fromEnv) return fromEnv\n return DEFAULT_MCP_HOST\n}\n"],
|
|
5
|
+
"mappings": "AAOO,MAAM,mBAAmB;AAEhC,MAAM,iBAAiB,oBAAI,IAAI,CAAC,aAAa,aAAa,OAAO,OAAO,CAAC;AAOlE,SAAS,eAAe,MAAuB;AACpD,SAAO,eAAe,IAAI,KAAK,KAAK,EAAE,YAAY,CAAC;AACrD;AAOO,SAAS,eAAe,cAAkC,SAAqC;AACpG,QAAM,WAAW,cAAc,KAAK;AACpC,MAAI,SAAU,QAAO;AACrB,QAAM,UAAU,SAAS,KAAK;AAC9B,MAAI,QAAS,QAAO;AACpB,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7748.1.d745ba61aa",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -100,16 +100,16 @@
|
|
|
100
100
|
"zod-to-json-schema": "^3.25.2"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
104
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
103
|
+
"@open-mercato/shared": "0.10.1-develop.7748.1.d745ba61aa",
|
|
104
|
+
"@open-mercato/ui": "0.10.1-develop.7748.1.d745ba61aa",
|
|
105
105
|
"react": "^19.0.0",
|
|
106
106
|
"react-dom": "^19.0.0",
|
|
107
107
|
"zod": ">=3.23.0"
|
|
108
108
|
},
|
|
109
109
|
"devDependencies": {
|
|
110
|
-
"@open-mercato/cli": "0.10.1-develop.
|
|
111
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
112
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
110
|
+
"@open-mercato/cli": "0.10.1-develop.7748.1.d745ba61aa",
|
|
111
|
+
"@open-mercato/shared": "0.10.1-develop.7748.1.d745ba61aa",
|
|
112
|
+
"@open-mercato/ui": "0.10.1-develop.7748.1.d745ba61aa",
|
|
113
113
|
"@types/react": "^19.2.18",
|
|
114
114
|
"@types/react-dom": "^19.2.5",
|
|
115
115
|
"react": "19.2.8",
|
|
@@ -2,6 +2,7 @@ import type { ModuleCli } from '@open-mercato/shared/modules/registry'
|
|
|
2
2
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
3
|
import { resetServerLoggerCache } from '@open-mercato/shared/lib/logger'
|
|
4
4
|
import { installNextSubpathResolveHook } from './lib/next-subpath-resolve-hook'
|
|
5
|
+
import { resolveMcpHost } from './lib/mcp-host-resolution'
|
|
5
6
|
/**
|
|
6
7
|
* Ensure app bootstrap is called before creating DI container.
|
|
7
8
|
* Uses the shared generated-bootstrap loader so the command works both from
|
|
@@ -141,6 +142,10 @@ const mcpServeHttp: ModuleCli = {
|
|
|
141
142
|
const portArg = parseInt(String(args.port ?? ''), 10)
|
|
142
143
|
const port = !portArg || isNaN(portArg) ? MCP_DEFAULT_PORT : portArg
|
|
143
144
|
const debug = args.debug === true || args.debug === 'true'
|
|
145
|
+
const host = resolveMcpHost(
|
|
146
|
+
typeof args.host === 'string' ? args.host : undefined,
|
|
147
|
+
process.env.MCP_HTTP_HOST,
|
|
148
|
+
)
|
|
144
149
|
|
|
145
150
|
await ensureBootstrap()
|
|
146
151
|
const container = await createRequestContainer()
|
|
@@ -155,6 +160,7 @@ const mcpServeHttp: ModuleCli = {
|
|
|
155
160
|
},
|
|
156
161
|
container,
|
|
157
162
|
port,
|
|
163
|
+
host,
|
|
158
164
|
})
|
|
159
165
|
},
|
|
160
166
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { isMatchingApiKey } from '../mcp-dev-server'
|
|
2
|
+
|
|
3
|
+
describe('isMatchingApiKey', () => {
|
|
4
|
+
test('accepts an exact match', () => {
|
|
5
|
+
expect(isMatchingApiKey('omk_secret-key', 'omk_secret-key')).toBe(true)
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
test('rejects a mismatched key of the same length', () => {
|
|
9
|
+
expect(isMatchingApiKey('omk_secret-key', 'omk_secret-kex')).toBe(false)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('rejects a shorter or longer provided value without throwing', () => {
|
|
13
|
+
expect(isMatchingApiKey('omk_secret-key', 'omk_secret-ke')).toBe(false)
|
|
14
|
+
expect(isMatchingApiKey('omk_secret-key', 'omk_secret-key-extra')).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('rejects an empty provided value', () => {
|
|
18
|
+
expect(isMatchingApiKey('omk_secret-key', '')).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DEFAULT_MCP_HOST, isLoopbackHost, resolveMcpHost } from '../mcp-host-resolution'
|
|
2
|
+
|
|
3
|
+
describe('resolveMcpHost', () => {
|
|
4
|
+
test('defaults to loopback when nothing is set', () => {
|
|
5
|
+
expect(resolveMcpHost(undefined, undefined)).toBe(DEFAULT_MCP_HOST)
|
|
6
|
+
expect(resolveMcpHost('', '')).toBe(DEFAULT_MCP_HOST)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('an explicit host wins over the env var', () => {
|
|
10
|
+
expect(resolveMcpHost('0.0.0.0', '10.0.0.5')).toBe('0.0.0.0')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('the env var is used when no explicit host is given', () => {
|
|
14
|
+
expect(resolveMcpHost(undefined, '0.0.0.0')).toBe('0.0.0.0')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('trims whitespace and treats a blank value as unset', () => {
|
|
18
|
+
expect(resolveMcpHost(' ', ' 0.0.0.0 ')).toBe('0.0.0.0')
|
|
19
|
+
expect(resolveMcpHost(' 10.0.0.5 ', undefined)).toBe('10.0.0.5')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('isLoopbackHost', () => {
|
|
24
|
+
test('recognizes every loopback alias', () => {
|
|
25
|
+
expect(isLoopbackHost('127.0.0.1')).toBe(true)
|
|
26
|
+
expect(isLoopbackHost('localhost')).toBe(true)
|
|
27
|
+
expect(isLoopbackHost('::1')).toBe(true)
|
|
28
|
+
expect(isLoopbackHost('[::1]')).toBe(true)
|
|
29
|
+
expect(isLoopbackHost('LOCALHOST')).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('rejects a real bind-everywhere or LAN address', () => {
|
|
33
|
+
expect(isLoopbackHost('0.0.0.0')).toBe(false)
|
|
34
|
+
expect(isLoopbackHost('10.0.0.5')).toBe(false)
|
|
35
|
+
expect(isLoopbackHost('host.docker.internal')).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -17,6 +17,7 @@ import type { SearchService } from '@open-mercato/search/service'
|
|
|
17
17
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
18
18
|
import type { ApiKey } from '@open-mercato/core/modules/api_keys/data/entities'
|
|
19
19
|
import { findApiKeyBySecret, findSessionApiKeyWithSecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'
|
|
20
|
+
import { isLoopbackHost, resolveMcpHost } from './mcp-host-resolution'
|
|
20
21
|
|
|
21
22
|
const logger = createLogger('ai_assistant').child({ component: 'mcp-http' })
|
|
22
23
|
|
|
@@ -27,6 +28,15 @@ export type McpHttpServerOptions = {
|
|
|
27
28
|
config: McpServerConfig
|
|
28
29
|
container: AwilixContainer
|
|
29
30
|
port: number
|
|
31
|
+
/**
|
|
32
|
+
* Bind address. Defaults to loopback (`127.0.0.1`) — the only intended client is OpenCode
|
|
33
|
+
* running in Docker, which reaches host loopback via `host.docker.internal` on Docker Desktop.
|
|
34
|
+
* On native Linux `host-gateway` is the bridge IP, so `yarn dev` passes a wider host there
|
|
35
|
+
* (see `resolveDevMcpHost` in `scripts/dev-mcp.mjs`). Omitting a host to `net.Server.listen` binds every interface
|
|
36
|
+
* (`0.0.0.0`/`::`), which exposes the server (and its shared API key) off-host; pass this
|
|
37
|
+
* explicitly to opt into that, paired with a firewall and a strong, rotated key.
|
|
38
|
+
*/
|
|
39
|
+
host?: string
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
/**
|
|
@@ -407,6 +417,7 @@ async function parseJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
|
407
417
|
*/
|
|
408
418
|
export async function runMcpHttpServer(options: McpHttpServerOptions): Promise<void> {
|
|
409
419
|
const { config, container, port } = options
|
|
420
|
+
const host = resolveMcpHost(options.host, undefined)
|
|
410
421
|
|
|
411
422
|
await loadAllModuleTools()
|
|
412
423
|
|
|
@@ -593,11 +604,14 @@ export async function runMcpHttpServer(options: McpHttpServerOptions): Promise<v
|
|
|
593
604
|
logger.info('Mode: stateless (new server per request)')
|
|
594
605
|
logger.info('Server auth: API key validated against database (x-api-key header)')
|
|
595
606
|
logger.info('User auth: session token (_sessionToken) preferred, falls back to API key roles')
|
|
607
|
+
if (!isLoopbackHost(host)) {
|
|
608
|
+
logger.warn('Binding beyond loopback — ensure a firewall and a strong, rotated MCP_SERVER_API_KEY', { host })
|
|
609
|
+
}
|
|
596
610
|
|
|
597
611
|
// Return a Promise that keeps the process alive until shutdown
|
|
598
612
|
return new Promise<void>((resolve) => {
|
|
599
|
-
httpServer.listen(port, () => {
|
|
600
|
-
logger.info('Server listening', { port })
|
|
613
|
+
httpServer.listen(port, host, () => {
|
|
614
|
+
logger.info('Server listening', { host, port })
|
|
601
615
|
})
|
|
602
616
|
|
|
603
617
|
const shutdown = async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
2
3
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
3
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
4
5
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
@@ -10,6 +11,7 @@ import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures }
|
|
|
10
11
|
import { jsonSchemaToZod } from './schema-utils'
|
|
11
12
|
import { buildMcpToolAnnotations } from './mcp-tool-annotations'
|
|
12
13
|
import { getApiKeyFromMcpJson } from './mcp-dev-key-resolution'
|
|
14
|
+
import { isLoopbackHost, resolveMcpHost } from './mcp-host-resolution'
|
|
13
15
|
import type { McpToolContext } from './types'
|
|
14
16
|
import type { SearchService } from '@open-mercato/search/service'
|
|
15
17
|
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
@@ -22,6 +24,19 @@ const log = (message: string, ...args: unknown[]) => {
|
|
|
22
24
|
logger.info(message, args.length > 0 ? { details: args.map((arg) => String(arg)).join(' ') } : undefined)
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Constant-time API-key comparison, length-guarded (`timingSafeEqual` throws on a length
|
|
29
|
+
* mismatch rather than reporting `false`). A plain `!==` short-circuits at the first
|
|
30
|
+
* differing byte, leaking a timing side channel an attacker can use to recover the key
|
|
31
|
+
* one byte at a time — AGENTS.md mandates constant-time comparison for this class of check.
|
|
32
|
+
*/
|
|
33
|
+
export function isMatchingApiKey(expected: string, provided: string): boolean {
|
|
34
|
+
const expectedBuffer = Buffer.from(expected)
|
|
35
|
+
const providedBuffer = Buffer.from(provided)
|
|
36
|
+
if (expectedBuffer.length !== providedBuffer.length) return false
|
|
37
|
+
return timingSafeEqual(expectedBuffer, providedBuffer)
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
/**
|
|
26
41
|
* Maximum request body size (1MB).
|
|
27
42
|
*/
|
|
@@ -171,6 +186,12 @@ function createDevMcpServer(
|
|
|
171
186
|
export async function runMcpDevServer(): Promise<void> {
|
|
172
187
|
const apiKey = await getApiKeyFromMcpJson()
|
|
173
188
|
const port = parseInt(process.env.MCP_DEV_PORT ?? '', 10) || DEFAULT_PORT
|
|
189
|
+
// Loopback by default: this server authenticates once at startup and reuses a single,
|
|
190
|
+
// superadmin-equivalent context for every request for the life of the process (no
|
|
191
|
+
// per-request ACL re-resolution), so exposing it beyond loopback turns a leaked/brute-forced
|
|
192
|
+
// API key into admin-equivalent access to tenant data. Opt in explicitly if the dev port
|
|
193
|
+
// genuinely needs to be reachable off-host (e.g. a Docker bridge).
|
|
194
|
+
const host = resolveMcpHost(undefined, process.env.MCP_DEV_HOST)
|
|
174
195
|
const debug = process.env.MCP_DEBUG === 'true'
|
|
175
196
|
|
|
176
197
|
if (!apiKey) {
|
|
@@ -323,8 +344,8 @@ export async function runMcpDevServer(): Promise<void> {
|
|
|
323
344
|
return
|
|
324
345
|
}
|
|
325
346
|
|
|
326
|
-
// Validate against the configured API key
|
|
327
|
-
if (providedApiKey
|
|
347
|
+
// Validate against the configured API key (constant-time; see isMatchingApiKey)
|
|
348
|
+
if (!isMatchingApiKey(apiKey, providedApiKey)) {
|
|
328
349
|
res.writeHead(401, { 'Content-Type': 'application/json' })
|
|
329
350
|
res.end(JSON.stringify({ error: 'Invalid API key' }))
|
|
330
351
|
return
|
|
@@ -393,10 +414,13 @@ export async function runMcpDevServer(): Promise<void> {
|
|
|
393
414
|
log(`Endpoint: http://localhost:${port}/mcp`)
|
|
394
415
|
log(`Health: http://localhost:${port}/health`)
|
|
395
416
|
log(`Mode: Development (API key auth, no session tokens)`)
|
|
417
|
+
if (!isLoopbackHost(host)) {
|
|
418
|
+
log(`WARNING: binding to ${host} instead of loopback — this server grants admin-equivalent access to any caller who passes the single API-key check`)
|
|
419
|
+
}
|
|
396
420
|
|
|
397
421
|
return new Promise<void>((resolve) => {
|
|
398
|
-
httpServer.listen(port, () => {
|
|
399
|
-
log(`Server listening on
|
|
422
|
+
httpServer.listen(port, host, () => {
|
|
423
|
+
log(`Server listening on ${host}:${port}`)
|
|
400
424
|
log('Ready for Claude Code connections')
|
|
401
425
|
})
|
|
402
426
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared bind-address resolution for the MCP servers (issue #2659, #2670). Loopback by
|
|
3
|
+
* default; an explicit CLI flag or env var opts into a wider bind. Pulled out as pure
|
|
4
|
+
* functions so the precedence and the loopback-alias set are unit-testable without
|
|
5
|
+
* starting a listener.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_MCP_HOST = '127.0.0.1'
|
|
9
|
+
|
|
10
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]'])
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* True for every hostname that already means "this machine only" — used to skip the
|
|
14
|
+
* "binding beyond loopback" warning for a caller who wrote `localhost`/`::1` instead of
|
|
15
|
+
* the canonical `127.0.0.1`, which is not the wider-exposure case that warning is for.
|
|
16
|
+
*/
|
|
17
|
+
export function isLoopbackHost(host: string): boolean {
|
|
18
|
+
return LOOPBACK_HOSTS.has(host.trim().toLowerCase())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Precedence: an explicit value (CLI flag or constructor option) wins, then the env var,
|
|
23
|
+
* then the loopback default. Both inputs are trimmed; an empty string is treated as unset,
|
|
24
|
+
* consistent with how the rest of the CLI reads optional string args/env vars.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveMcpHost(explicitHost: string | undefined, envHost: string | undefined): string {
|
|
27
|
+
const explicit = explicitHost?.trim()
|
|
28
|
+
if (explicit) return explicit
|
|
29
|
+
const fromEnv = envHost?.trim()
|
|
30
|
+
if (fromEnv) return fromEnv
|
|
31
|
+
return DEFAULT_MCP_HOST
|
|
32
|
+
}
|