@open-mercato/ai-assistant 0.6.5-develop.5382.1.f542de69af → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +12 -0
- package/dist/modules/ai_assistant/cli.js +14 -6
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities.js +4 -0
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/lib/attachment-parts.js +10 -3
- package/dist/modules/ai_assistant/lib/attachment-parts.js.map +2 -2
- package/dist/modules/ai_assistant/lib/client-factory.js +0 -8
- package/dist/modules/ai_assistant/lib/client-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/codemode-tools.js +26 -0
- package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js +23 -3
- package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-client.js +2 -4
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js +71 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-key-resolution.js.map +7 -0
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +1 -30
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-server.js +29 -23
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
- package/dist/modules/ai_assistant/setup.js +9 -2
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/package.json +11 -12
- package/src/modules/ai_assistant/__tests__/cli-mcp-serve-key.test.ts +63 -0
- package/src/modules/ai_assistant/__tests__/setup-schedules.test.ts +33 -0
- package/src/modules/ai_assistant/cli.ts +18 -5
- package/src/modules/ai_assistant/data/__tests__/schema-unique-indexes.test.ts +39 -0
- package/src/modules/ai_assistant/data/entities.ts +5 -0
- package/src/modules/ai_assistant/lib/__tests__/attachment-parts.test.ts +112 -0
- package/src/modules/ai_assistant/lib/__tests__/client-factory.test.ts +42 -0
- package/src/modules/ai_assistant/lib/__tests__/codemode-tools.test.ts +107 -0
- package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +30 -3
- package/src/modules/ai_assistant/lib/__tests__/mcp-client.test.ts +20 -1
- package/src/modules/ai_assistant/lib/__tests__/mcp-dev-key-resolution.test.ts +92 -0
- package/src/modules/ai_assistant/lib/__tests__/mcp-server-auth-fail-closed.test.ts +133 -0
- package/src/modules/ai_assistant/lib/attachment-parts.ts +26 -5
- package/src/modules/ai_assistant/lib/client-factory.ts +3 -9
- package/src/modules/ai_assistant/lib/codemode-tools.ts +49 -0
- package/src/modules/ai_assistant/lib/generated-registry-loader.ts +35 -3
- package/src/modules/ai_assistant/lib/mcp-client.ts +9 -5
- package/src/modules/ai_assistant/lib/mcp-dev-key-resolution.ts +107 -0
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +1 -36
- package/src/modules/ai_assistant/lib/mcp-server.ts +47 -33
- package/src/modules/ai_assistant/lib/types.ts +7 -0
- package/src/modules/ai_assistant/setup.ts +11 -2
|
@@ -25,16 +25,14 @@ class McpClient {
|
|
|
25
25
|
const args = options.args ?? [
|
|
26
26
|
"mercato",
|
|
27
27
|
"ai_assistant",
|
|
28
|
-
"mcp:serve"
|
|
29
|
-
"--api-key",
|
|
30
|
-
options.apiKeySecret
|
|
28
|
+
"mcp:serve"
|
|
31
29
|
];
|
|
32
30
|
const cwd = options.cwd ?? process.cwd();
|
|
33
31
|
const transport = new StdioClientTransport({
|
|
34
32
|
command,
|
|
35
33
|
args,
|
|
36
34
|
cwd,
|
|
37
|
-
env: process.env,
|
|
35
|
+
env: { ...process.env, OPEN_MERCATO_API_KEY: options.apiKeySecret },
|
|
38
36
|
stderr: "pipe"
|
|
39
37
|
});
|
|
40
38
|
transport.stderr?.on("data", (data) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/mcp-client.ts"],
|
|
4
|
-
"sourcesContent": ["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { McpClientInterface, ToolInfo, ToolResult } from './types'\n\n/**\n * Options for stdio transport.\n */\nexport type StdioClientOptions = {\n transport: 'stdio'\n
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AACrC,SAAS,qCAAqC;
|
|
4
|
+
"sourcesContent": ["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { McpClientInterface, ToolInfo, ToolResult } from './types'\n\n/**\n * Options for stdio transport.\n */\nexport type StdioClientOptions = {\n transport: 'stdio'\n /**\n * API key secret. Delivered to the spawned server via the\n * `OPEN_MERCATO_API_KEY` environment variable, never as a command-line\n * argument (argv is world-readable via `ps`/`/proc/<pid>/cmdline`).\n */\n apiKeySecret: string\n /** Command to run (default: 'yarn') */\n command?: string\n /** Arguments for the command (default: mercato ai_assistant mcp:serve, no secret on argv) */\n args?: string[]\n /** Working directory (default: process.cwd()) */\n cwd?: string\n}\n\n/**\n * Options for HTTP transport.\n */\nexport type HttpClientOptions = {\n transport: 'http'\n /** API key secret (sent via x-api-key header) */\n apiKeySecret: string\n /** MCP server URL (e.g., 'http://localhost:3001/mcp') */\n url: string\n}\n\n/**\n * Combined options for McpClient.\n */\nexport type McpClientOptions = StdioClientOptions | HttpClientOptions\n\n/**\n * MCP protocol client for connecting to MCP servers.\n *\n * Supports two transport modes:\n * - stdio: Spawns server as subprocess\n * - http: Connects to HTTP server\n */\nexport class McpClient implements McpClientInterface {\n private client: Client\n private transport: StdioClientTransport | StreamableHTTPClientTransport\n private apiKeySecret: string\n\n private constructor(\n client: Client,\n transport: StdioClientTransport | StreamableHTTPClientTransport,\n apiKeySecret: string\n ) {\n this.client = client\n this.transport = transport\n this.apiKeySecret = apiKeySecret\n }\n\n /**\n * Connect to an MCP server via the specified transport.\n */\n static async connect(options: McpClientOptions): Promise<McpClient> {\n if (options.transport === 'stdio') {\n return McpClient.connectStdio(options)\n } else {\n return McpClient.connectHttp(options)\n }\n }\n\n /**\n * Connect via stdio transport (spawn subprocess).\n */\n private static async connectStdio(options: StdioClientOptions): Promise<McpClient> {\n const command = options.command ?? 'yarn'\n // The API key is passed via OPEN_MERCATO_API_KEY in the child env (below),\n // never on argv \u2014 command-line arguments are readable by any local user.\n const args = options.args ?? [\n 'mercato',\n 'ai_assistant',\n 'mcp:serve',\n ]\n const cwd = options.cwd ?? process.cwd()\n\n const transport = new StdioClientTransport({\n command,\n args,\n cwd,\n env: { ...process.env, OPEN_MERCATO_API_KEY: options.apiKeySecret } as Record<string, string>,\n stderr: 'pipe',\n })\n transport.stderr?.on('data', (data) => {\n const message = data.toString().trim()\n if (message) {\n console.error(`[MCP Client] ${message}`)\n }\n })\n\n const client = new Client(\n { name: 'open-mercato-client', version: '0.1.0' },\n { capabilities: {} }\n )\n\n await client.connect(transport)\n\n return new McpClient(client, transport, options.apiKeySecret)\n }\n\n /**\n * Connect via HTTP transport.\n */\n private static async connectHttp(options: HttpClientOptions): Promise<McpClient> {\n const transport = new StreamableHTTPClientTransport(\n new URL(options.url),\n {\n requestInit: {\n headers: {\n 'x-api-key': options.apiKeySecret,\n },\n },\n }\n )\n\n const client = new Client(\n { name: 'open-mercato-client', version: '0.1.0' },\n { capabilities: {} }\n )\n\n await client.connect(transport)\n\n return new McpClient(client, transport, options.apiKeySecret)\n }\n\n /**\n * List available tools from the server.\n */\n async listTools(): Promise<ToolInfo[]> {\n const response = await this.client.listTools()\n\n return response.tools.map((tool) => ({\n name: tool.name,\n description: tool.description ?? '',\n inputSchema: (tool.inputSchema ?? {}) as Record<string, unknown>,\n }))\n }\n\n /**\n * Call a tool on the server.\n */\n async callTool(name: string, args: unknown): Promise<ToolResult> {\n try {\n const response = await this.client.callTool({\n name,\n arguments: args as Record<string, unknown>,\n })\n\n // Parse content from response\n const content = response.content\n if (!Array.isArray(content) || content.length === 0) {\n return { success: true, result: null }\n }\n\n const firstContent = content[0]\n if (firstContent.type === 'text') {\n try {\n const parsed = JSON.parse(firstContent.text)\n\n // Check if it's an error response\n if (response.isError || parsed.error) {\n return {\n success: false,\n error: parsed.error ?? 'Unknown error',\n }\n }\n\n return { success: true, result: parsed }\n } catch {\n // Not JSON, return as-is\n return { success: true, result: firstContent.text }\n }\n }\n\n return { success: true, result: content }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return { success: false, error: message }\n }\n }\n\n /**\n * Close the client and release resources.\n */\n async close(): Promise<void> {\n try {\n await this.client.close()\n } catch {\n // Ignore close errors\n }\n\n try {\n await this.transport.close()\n } catch {\n // Ignore close errors\n }\n\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { resolve, dirname } from "node:path";
|
|
2
|
+
const MCP_CONFIG_FILENAME = ".mcp.json";
|
|
3
|
+
const PROJECT_ROOT_MARKERS = [".git", "yarn.lock", "pnpm-lock.yaml", "package-lock.json"];
|
|
4
|
+
const log = (message, ...args) => {
|
|
5
|
+
console.error(`[MCP Dev] ${message}`, ...args);
|
|
6
|
+
};
|
|
7
|
+
function findProjectRoot(startDir, exists) {
|
|
8
|
+
let dir = resolve(startDir);
|
|
9
|
+
while (true) {
|
|
10
|
+
for (const marker of PROJECT_ROOT_MARKERS) {
|
|
11
|
+
if (exists(resolve(dir, marker))) return dir;
|
|
12
|
+
}
|
|
13
|
+
const parent = dirname(dir);
|
|
14
|
+
if (parent === dir) return resolve(startDir);
|
|
15
|
+
dir = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function checkMcpConfigPermissions(configPath, fileStat, currentUid, warn) {
|
|
19
|
+
if (currentUid === null) return { ok: true };
|
|
20
|
+
if (fileStat.uid !== currentUid) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: `refusing to read ${configPath}: it is owned by uid ${fileStat.uid}, not the current user (uid ${currentUid})`
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if ((fileStat.mode & 63) !== 0) {
|
|
27
|
+
warn(
|
|
28
|
+
`${configPath} is accessible to group/other (mode ${(fileStat.mode & 511).toString(8)}); it contains a live API key \u2014 restrict it with: chmod 600 ${configPath}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
33
|
+
async function getApiKeyFromMcpJson() {
|
|
34
|
+
const envKey = process.env.OPEN_MERCATO_API_KEY;
|
|
35
|
+
if (envKey && envKey.trim().length > 0) {
|
|
36
|
+
return envKey.trim();
|
|
37
|
+
}
|
|
38
|
+
const { readFile, stat } = await import("node:fs/promises");
|
|
39
|
+
const { existsSync } = await import("node:fs");
|
|
40
|
+
try {
|
|
41
|
+
const projectRoot = findProjectRoot(process.cwd(), existsSync);
|
|
42
|
+
const mcpJsonPath = resolve(projectRoot, MCP_CONFIG_FILENAME);
|
|
43
|
+
if (!existsSync(mcpJsonPath)) {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
const fileStat = await stat(mcpJsonPath);
|
|
47
|
+
const currentUid = typeof process.getuid === "function" ? process.getuid() : null;
|
|
48
|
+
const permissionCheck = checkMcpConfigPermissions(
|
|
49
|
+
mcpJsonPath,
|
|
50
|
+
{ uid: fileStat.uid, mode: fileStat.mode },
|
|
51
|
+
currentUid,
|
|
52
|
+
(message) => log(`Warning: ${message}`)
|
|
53
|
+
);
|
|
54
|
+
if (!permissionCheck.ok) {
|
|
55
|
+
log(`Error: ${permissionCheck.reason}`);
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
const content = await readFile(mcpJsonPath, "utf-8");
|
|
59
|
+
const config = JSON.parse(content);
|
|
60
|
+
const serverConfig = config?.mcpServers?.["open-mercato"];
|
|
61
|
+
return serverConfig?.headers?.["x-api-key"];
|
|
62
|
+
} catch {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
checkMcpConfigPermissions,
|
|
68
|
+
findProjectRoot,
|
|
69
|
+
getApiKeyFromMcpJson
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=mcp-dev-key-resolution.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/mcp-dev-key-resolution.ts"],
|
|
4
|
+
"sourcesContent": ["import { resolve, dirname } from 'node:path'\n\nconst MCP_CONFIG_FILENAME = '.mcp.json'\n\n// Markers that identify a project/repo root. The lookup stops at the first\n// ancestor that has one of these, so the secret is only ever read from the\n// project root \u2014 never from an unbounded walk up to the filesystem root.\nconst PROJECT_ROOT_MARKERS = ['.git', 'yarn.lock', 'pnpm-lock.yaml', 'package-lock.json'] as const\n\nconst log = (message: string, ...args: unknown[]) => {\n console.error(`[MCP Dev] ${message}`, ...args)\n}\n\ntype PermissionStat = { uid: number; mode: number }\n\n/**\n * Resolve the project root by walking up from `startDir` to the nearest\n * ancestor containing a project-root marker. Falls back to `startDir` (never\n * the filesystem root) when no marker is found, so a planted `.mcp.json` in a\n * writable ancestor such as `/tmp` can never be picked up.\n */\nexport function findProjectRoot(startDir: string, exists: (path: string) => boolean): string {\n let dir = resolve(startDir)\n while (true) {\n for (const marker of PROJECT_ROOT_MARKERS) {\n if (exists(resolve(dir, marker))) return dir\n }\n const parent = dirname(dir)\n if (parent === dir) return resolve(startDir)\n dir = parent\n }\n}\n\n/**\n * Validate the ownership and permissions of `.mcp.json` before its secret is\n * read. Refuses when the file is owned by another user (a planted/shadowed\n * config) and warns loudly when it is accessible to group/other (the key is a\n * live secret). POSIX-only: when `currentUid` is null (e.g. Windows) the checks\n * are skipped.\n */\nexport function checkMcpConfigPermissions(\n configPath: string,\n fileStat: PermissionStat,\n currentUid: number | null,\n warn: (message: string) => void,\n): { ok: boolean; reason?: string } {\n if (currentUid === null) return { ok: true }\n if (fileStat.uid !== currentUid) {\n return {\n ok: false,\n reason: `refusing to read ${configPath}: it is owned by uid ${fileStat.uid}, not the current user (uid ${currentUid})`,\n }\n }\n if ((fileStat.mode & 0o077) !== 0) {\n warn(\n `${configPath} is accessible to group/other (mode ${(fileStat.mode & 0o777).toString(8)}); it contains a live API key \u2014 restrict it with: chmod 600 ${configPath}`,\n )\n }\n return { ok: true }\n}\n\n/**\n * Resolve the dev MCP server's API key.\n *\n * Priority: `OPEN_MERCATO_API_KEY` env var, then `headers.x-api-key` from a\n * single `.mcp.json` read from the detected project root. The lookup is bounded\n * to the project root (it never walks up to the filesystem root), and the file\n * is ownership/permission-checked before its secret is read.\n */\nexport async function getApiKeyFromMcpJson(): Promise<string | undefined> {\n const envKey = process.env.OPEN_MERCATO_API_KEY\n if (envKey && envKey.trim().length > 0) {\n return envKey.trim()\n }\n\n const { readFile, stat } = await import('node:fs/promises')\n const { existsSync } = await import('node:fs')\n\n try {\n const projectRoot = findProjectRoot(process.cwd(), existsSync)\n const mcpJsonPath = resolve(projectRoot, MCP_CONFIG_FILENAME)\n if (!existsSync(mcpJsonPath)) {\n return undefined\n }\n\n const fileStat = await stat(mcpJsonPath)\n const currentUid = typeof process.getuid === 'function' ? process.getuid() : null\n const permissionCheck = checkMcpConfigPermissions(\n mcpJsonPath,\n { uid: fileStat.uid, mode: fileStat.mode },\n currentUid,\n (message) => log(`Warning: ${message}`),\n )\n if (!permissionCheck.ok) {\n log(`Error: ${permissionCheck.reason}`)\n return undefined\n }\n\n const content = await readFile(mcpJsonPath, 'utf-8')\n const config = JSON.parse(content)\n const serverConfig = config?.mcpServers?.['open-mercato']\n\n return serverConfig?.headers?.['x-api-key']\n } catch {\n return undefined\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS,eAAe;AAEjC,MAAM,sBAAsB;AAK5B,MAAM,uBAAuB,CAAC,QAAQ,aAAa,kBAAkB,mBAAmB;AAExF,MAAM,MAAM,CAAC,YAAoB,SAAoB;AACnD,UAAQ,MAAM,aAAa,OAAO,IAAI,GAAG,IAAI;AAC/C;AAUO,SAAS,gBAAgB,UAAkB,QAA2C;AAC3F,MAAI,MAAM,QAAQ,QAAQ;AAC1B,SAAO,MAAM;AACX,eAAW,UAAU,sBAAsB;AACzC,UAAI,OAAO,QAAQ,KAAK,MAAM,CAAC,EAAG,QAAO;AAAA,IAC3C;AACA,UAAM,SAAS,QAAQ,GAAG;AAC1B,QAAI,WAAW,IAAK,QAAO,QAAQ,QAAQ;AAC3C,UAAM;AAAA,EACR;AACF;AASO,SAAS,0BACd,YACA,UACA,YACA,MACkC;AAClC,MAAI,eAAe,KAAM,QAAO,EAAE,IAAI,KAAK;AAC3C,MAAI,SAAS,QAAQ,YAAY;AAC/B,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,oBAAoB,UAAU,wBAAwB,SAAS,GAAG,+BAA+B,UAAU;AAAA,IACrH;AAAA,EACF;AACA,OAAK,SAAS,OAAO,QAAW,GAAG;AACjC;AAAA,MACE,GAAG,UAAU,wCAAwC,SAAS,OAAO,KAAO,SAAS,CAAC,CAAC,oEAA+D,UAAU;AAAA,IAClK;AAAA,EACF;AACA,SAAO,EAAE,IAAI,KAAK;AACpB;AAUA,eAAsB,uBAAoD;AACxE,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,UAAU,OAAO,KAAK,EAAE,SAAS,GAAG;AACtC,WAAO,OAAO,KAAK;AAAA,EACrB;AAEA,QAAM,EAAE,UAAU,KAAK,IAAI,MAAM,OAAO,kBAAkB;AAC1D,QAAM,EAAE,WAAW,IAAI,MAAM,OAAO,SAAS;AAE7C,MAAI;AACF,UAAM,cAAc,gBAAgB,QAAQ,IAAI,GAAG,UAAU;AAC7D,UAAM,cAAc,QAAQ,aAAa,mBAAmB;AAC5D,QAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,MAAM,KAAK,WAAW;AACvC,UAAM,aAAa,OAAO,QAAQ,WAAW,aAAa,QAAQ,OAAO,IAAI;AAC7E,UAAM,kBAAkB;AAAA,MACtB;AAAA,MACA,EAAE,KAAK,SAAS,KAAK,MAAM,SAAS,KAAK;AAAA,MACzC;AAAA,MACA,CAAC,YAAY,IAAI,YAAY,OAAO,EAAE;AAAA,IACxC;AACA,QAAI,CAAC,gBAAgB,IAAI;AACvB,UAAI,UAAU,gBAAgB,MAAM,EAAE;AACtC,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,MAAM,SAAS,aAAa,OAAO;AACnD,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,UAAM,eAAe,QAAQ,aAAa,cAAc;AAExD,WAAO,cAAc,UAAU,WAAW;AAAA,EAC5C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -7,40 +7,11 @@ import { executeTool } from "./tool-executor.js";
|
|
|
7
7
|
import { loadAllModuleTools, indexToolsForSearch } from "./tool-loader.js";
|
|
8
8
|
import { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from "./auth.js";
|
|
9
9
|
import { jsonSchemaToZod } from "./schema-utils.js";
|
|
10
|
+
import { getApiKeyFromMcpJson } from "./mcp-dev-key-resolution.js";
|
|
10
11
|
const DEFAULT_PORT = 3001;
|
|
11
12
|
const log = (message, ...args) => {
|
|
12
13
|
console.error(`[MCP Dev] ${message}`, ...args);
|
|
13
14
|
};
|
|
14
|
-
async function getApiKeyFromMcpJson() {
|
|
15
|
-
const { readFile, access } = await import("node:fs/promises");
|
|
16
|
-
const { resolve, dirname } = await import("node:path");
|
|
17
|
-
const findMcpJson = async (startDir) => {
|
|
18
|
-
let dir = startDir;
|
|
19
|
-
const root = resolve("/");
|
|
20
|
-
while (dir !== root) {
|
|
21
|
-
const candidate = resolve(dir, ".mcp.json");
|
|
22
|
-
try {
|
|
23
|
-
await access(candidate);
|
|
24
|
-
return candidate;
|
|
25
|
-
} catch {
|
|
26
|
-
dir = dirname(dir);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
};
|
|
31
|
-
try {
|
|
32
|
-
const mcpJsonPath = await findMcpJson(process.cwd());
|
|
33
|
-
if (!mcpJsonPath) {
|
|
34
|
-
return void 0;
|
|
35
|
-
}
|
|
36
|
-
const content = await readFile(mcpJsonPath, "utf-8");
|
|
37
|
-
const config = JSON.parse(content);
|
|
38
|
-
const serverConfig = config?.mcpServers?.["open-mercato"];
|
|
39
|
-
return serverConfig?.headers?.["x-api-key"];
|
|
40
|
-
} catch {
|
|
41
|
-
return void 0;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
15
|
const MAX_BODY_SIZE = 1 * 1024 * 1024;
|
|
45
16
|
async function parseJsonBody(req) {
|
|
46
17
|
return new Promise((resolve, reject) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/mcp-dev-server.ts"],
|
|
4
|
-
"sourcesContent": ["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { z, type ZodType } from 'zod'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'\nimport { jsonSchemaToZod } from './schema-utils'\nimport type { McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nconst DEFAULT_PORT = 3001\n\nconst log = (message: string, ...args: unknown[]) => {\n console.error(`[MCP Dev] ${message}`, ...args)\n}\n\nasync function getApiKeyFromMcpJson(): Promise<string | undefined> {\n const { readFile, access } = await import('node:fs/promises')\n const { resolve, dirname } = await import('node:path')\n\n // Search for .mcp.json starting from cwd and walking up to project root\n const findMcpJson = async (startDir: string): Promise<string | null> => {\n let dir = startDir\n const root = resolve('/')\n while (dir !== root) {\n const candidate = resolve(dir, '.mcp.json')\n try {\n await access(candidate)\n return candidate\n } catch {\n dir = dirname(dir)\n }\n }\n return null\n }\n\n try {\n const mcpJsonPath = await findMcpJson(process.cwd())\n if (!mcpJsonPath) {\n return undefined\n }\n\n const content = await readFile(mcpJsonPath, 'utf-8')\n const config = JSON.parse(content)\n const serverConfig = config?.mcpServers?.['open-mercato']\n\n return serverConfig?.headers?.['x-api-key']\n } catch {\n return undefined\n }\n}\n\n/**\n * Maximum request body size (1MB).\n */\nconst MAX_BODY_SIZE = 1 * 1024 * 1024\n\n/**\n * Parse JSON body from request with size limit.\n */\nasync function parseJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let totalSize = 0\n\n req.on('data', (chunk: Buffer) => {\n totalSize += chunk.length\n if (totalSize > MAX_BODY_SIZE) {\n req.destroy()\n reject(new Error('Request payload too large'))\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n const body = Buffer.concat(chunks).toString('utf-8')\n resolve(body ? JSON.parse(body) : undefined)\n } catch (error) {\n reject(error)\n }\n })\n req.on('error', reject)\n })\n}\n\n/**\n * Create MCP server with tools pre-authenticated for dev use.\n * No session tokens required - uses API key authentication directly.\n */\nfunction createDevMcpServer(\n toolContext: McpToolContext,\n authFeatures: string[],\n isSuperAdmin: boolean,\n debug: boolean\n): McpServer {\n const server = new McpServer(\n { name: 'open-mercato-mcp-dev', version: '0.1.0' },\n { capabilities: { tools: {} } }\n )\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n // Filter tools based on API key permissions\n const rbacService = toolContext.container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, authFeatures, isSuperAdmin, rbacService)\n )\n\n if (debug) {\n log(`Registering ${accessibleTools.length}/${tools.length} tools (filtered by API key permissions)`)\n }\n\n for (const tool of accessibleTools) {\n if (debug) {\n log(`Registering tool: ${tool.name}`)\n }\n\n // Convert Zod schema to safe schema without Date types\n let safeSchema: ZodType | undefined\n if (tool.inputSchema) {\n try {\n const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>\n const converted = jsonSchemaToZod(jsonSchema)\n safeSchema = (converted as z.ZodObject<any>).passthrough()\n } catch (error) {\n if (debug) {\n log(`Skipping tool ${tool.name} - schema conversion failed:`, error instanceof Error ? error.message : error)\n }\n continue\n }\n } else {\n safeSchema = z.object({}).passthrough()\n }\n\n try {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: safeSchema,\n },\n async (args: unknown) => {\n const toolArgs = (args ?? {}) as Record<string, unknown>\n\n if (debug) {\n log(`Calling tool: ${tool.name}`, JSON.stringify(toolArgs))\n }\n\n const result = await executeTool(tool.name, toolArgs, toolContext)\n\n if (!result.success) {\n log(`Tool error: ${result.error}`)\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n }\n )\n } catch (error) {\n if (debug) {\n log(`Skipping tool ${tool.name} - registration failed:`, error instanceof Error ? error.message : error)\n }\n continue\n }\n }\n\n return server\n}\n\n/**\n * Development MCP server for Claude Code integration.\n *\n * This server uses HTTP transport and authenticates via the\n * x-api-key header configured in .mcp.json file.\n *\n * Usage:\n * yarn mcp:dev\n *\n * Configure in .mcp.json for Claude Code with HTTP transport.\n */\nexport async function runMcpDevServer(): Promise<void> {\n const apiKey = await getApiKeyFromMcpJson()\n const port = parseInt(process.env.MCP_DEV_PORT ?? '', 10) || DEFAULT_PORT\n const debug = process.env.MCP_DEBUG === 'true'\n\n if (!apiKey) {\n log('Error: API key not found in .mcp.json')\n log('')\n log('To get an API key:')\n log(' 1. Log into Open Mercato as an admin')\n log(' 2. Go to Settings > API Keys')\n log(' 3. Create a new key with the required permissions')\n log('')\n log('Then configure in .mcp.json:')\n log(' {')\n log(' \"mcpServers\": {')\n log(' \"open-mercato\": {')\n log(' \"type\": \"http\",')\n log(' \"url\": \"http://localhost:3001/mcp\",')\n log(' \"headers\": {')\n log(' \"x-api-key\": \"omk_your_api_key_here\"')\n log(' }')\n log(' }')\n log(' }')\n log(' }')\n process.exit(1)\n }\n\n log('Starting development MCP HTTP server...')\n\n // Create DI container\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n\n // Authenticate the API key upfront\n log('Authenticating API key...')\n const authResult = await authenticateMcpRequest(apiKey, container)\n\n if (!authResult.success) {\n log(`Authentication failed: ${authResult.error}`)\n process.exit(1)\n }\n\n log(`Authenticated as: ${authResult.keyName}`)\n log(`Tenant: ${authResult.tenantId ?? '(global)'}`)\n log(`Organization: ${authResult.organizationId ?? '(none)'}`)\n log(`Super admin: ${authResult.isSuperAdmin}`)\n log(`Features: ${authResult.features.length > 0 ? authResult.features.join(', ') : '(none)'}`)\n\n // Load tools\n log('Loading tools...')\n await loadAllModuleTools()\n\n // Generate and cache entity graph\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n\n log('Generating entity relationship graph...')\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n log(`Entity graph: ${graph.nodes.length} entities, ${graph.edges.length} relationships`)\n } catch (error) {\n log('Entity graph generation skipped:', error instanceof Error ? error.message : error)\n }\n\n // Pre-cache rich OpenAPI spec for Code Mode search tool (prefers runtime module registry over static JSON)\n try {\n const { loadRichOpenApiSpec } = await import('./api-endpoint-index')\n const spec = await loadRichOpenApiSpec()\n if (spec) {\n log('Rich OpenAPI spec cached for Code Mode (with requestBody schemas)')\n } else {\n log('OpenAPI spec not available')\n }\n } catch (error) {\n log('OpenAPI spec caching skipped:', error instanceof Error ? error.message : error)\n }\n\n // Index tools and entity schemas for search (if search service available)\n try {\n const searchService = container.resolve('searchService') as SearchService\n await indexToolsForSearch(searchService)\n\n // Index entity schemas for hybrid search\n try {\n const { getCachedEntityGraph } = await import('./entity-graph')\n const { indexEntitiesForSearch } = await import('./entity-index')\n const graph = getCachedEntityGraph()\n if (graph) {\n const { count } = await indexEntitiesForSearch(searchService, graph)\n if (count > 0) {\n log(`Indexed ${count} entity schemas for discovery`)\n }\n }\n } catch (entityError) {\n log('Entity schema indexing skipped:', entityError instanceof Error ? entityError.message : entityError)\n }\n } catch {\n log('Search indexing skipped (search service not available)')\n }\n\n // Generate a stable session ID for dev mode (enables session memory / caching)\n const { randomBytes } = await import('node:crypto')\n const devSessionId = 'dev_' + randomBytes(8).toString('hex')\n log(`Session ID: ${devSessionId} (stable for this server instance)`)\n\n // Create tool context from auth result\n const toolContext: McpToolContext = {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n userId: authResult.userId,\n container,\n userFeatures: authResult.features,\n isSuperAdmin: authResult.isSuperAdmin,\n apiKeySecret: apiKey,\n sessionId: devSessionId,\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '/', `http://localhost:${port}`)\n\n // Health check endpoint\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({\n status: 'ok',\n mode: 'development',\n tools: getToolRegistry().listToolNames().length,\n tenant: authResult.tenantId,\n timestamp: new Date().toISOString(),\n }))\n return\n }\n\n if (url.pathname !== '/mcp') {\n res.writeHead(404, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Not found' }))\n return\n }\n\n // Extract and validate API key from header\n const headers: Record<string, string | undefined> = {}\n for (const [key, value] of Object.entries(req.headers)) {\n headers[key] = Array.isArray(value) ? value[0] : value\n }\n\n const providedApiKey = extractApiKeyFromHeaders(headers)\n if (!providedApiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'API key required (x-api-key header)' }))\n return\n }\n\n // Validate against the configured API key\n if (providedApiKey !== apiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Invalid API key' }))\n return\n }\n\n if (debug) {\n log(`Authenticated request (${req.method})`)\n }\n\n try {\n // Create stateless transport\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: req.method === 'POST',\n })\n\n // Create server with pre-authenticated context (no session tokens needed)\n const mcpServer = createDevMcpServer(toolContext, authResult.features, authResult.isSuperAdmin, debug)\n\n // Connect server to transport\n await mcpServer.connect(transport)\n\n // Handle the request\n if (req.method === 'POST') {\n const body = await parseJsonBody(req)\n await transport.handleRequest(req, res, body)\n } else {\n await transport.handleRequest(req, res)\n }\n\n // Cleanup after response finishes\n res.on('finish', () => {\n transport.close()\n mcpServer.close()\n if (debug) {\n log(`Request completed, cleaned up`)\n }\n })\n } catch (error) {\n log('Error handling request:', error)\n if (!res.headersSent) {\n if (error instanceof Error && error.message === 'Request payload too large') {\n res.writeHead(413, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Request payload too large (max 1MB)' }))\n return\n }\n\n res.writeHead(500, { 'Content-Type': 'application/json' })\n res.end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,\n },\n id: null,\n })\n )\n }\n }\n })\n\n const toolCount = getToolRegistry().listToolNames().length\n\n log(`Tools registered: ${toolCount}`)\n log(`Endpoint: http://localhost:${port}/mcp`)\n log(`Health: http://localhost:${port}/health`)\n log(`Mode: Development (API key auth, no session tokens)`)\n\n return new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n log(`Server listening on port ${port}`)\n log('Ready for Claude Code connections')\n })\n\n const shutdown = async () => {\n log('Shutting down...')\n httpServer.close(() => {\n log('Server closed')\n resolve()\n })\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n })\n}\n"],
|
|
5
|
-
"mappings": "AAAA,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;
|
|
4
|
+
"sourcesContent": ["import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport { z, type ZodType } from 'zod'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, extractApiKeyFromHeaders, hasRequiredFeatures } from './auth'\nimport { jsonSchemaToZod } from './schema-utils'\nimport { getApiKeyFromMcpJson } from './mcp-dev-key-resolution'\nimport type { McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\nconst DEFAULT_PORT = 3001\n\nconst log = (message: string, ...args: unknown[]) => {\n console.error(`[MCP Dev] ${message}`, ...args)\n}\n\n/**\n * Maximum request body size (1MB).\n */\nconst MAX_BODY_SIZE = 1 * 1024 * 1024\n\n/**\n * Parse JSON body from request with size limit.\n */\nasync function parseJsonBody(req: IncomingMessage): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let totalSize = 0\n\n req.on('data', (chunk: Buffer) => {\n totalSize += chunk.length\n if (totalSize > MAX_BODY_SIZE) {\n req.destroy()\n reject(new Error('Request payload too large'))\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n const body = Buffer.concat(chunks).toString('utf-8')\n resolve(body ? JSON.parse(body) : undefined)\n } catch (error) {\n reject(error)\n }\n })\n req.on('error', reject)\n })\n}\n\n/**\n * Create MCP server with tools pre-authenticated for dev use.\n * No session tokens required - uses API key authentication directly.\n */\nfunction createDevMcpServer(\n toolContext: McpToolContext,\n authFeatures: string[],\n isSuperAdmin: boolean,\n debug: boolean\n): McpServer {\n const server = new McpServer(\n { name: 'open-mercato-mcp-dev', version: '0.1.0' },\n { capabilities: { tools: {} } }\n )\n\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n // Filter tools based on API key permissions\n const rbacService = toolContext.container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, authFeatures, isSuperAdmin, rbacService)\n )\n\n if (debug) {\n log(`Registering ${accessibleTools.length}/${tools.length} tools (filtered by API key permissions)`)\n }\n\n for (const tool of accessibleTools) {\n if (debug) {\n log(`Registering tool: ${tool.name}`)\n }\n\n // Convert Zod schema to safe schema without Date types\n let safeSchema: ZodType | undefined\n if (tool.inputSchema) {\n try {\n const jsonSchema = z.toJSONSchema(tool.inputSchema, { unrepresentable: 'any' }) as Record<string, unknown>\n const converted = jsonSchemaToZod(jsonSchema)\n safeSchema = (converted as z.ZodObject<any>).passthrough()\n } catch (error) {\n if (debug) {\n log(`Skipping tool ${tool.name} - schema conversion failed:`, error instanceof Error ? error.message : error)\n }\n continue\n }\n } else {\n safeSchema = z.object({}).passthrough()\n }\n\n try {\n server.registerTool(\n tool.name,\n {\n description: tool.description,\n inputSchema: safeSchema,\n },\n async (args: unknown) => {\n const toolArgs = (args ?? {}) as Record<string, unknown>\n\n if (debug) {\n log(`Calling tool: ${tool.name}`, JSON.stringify(toolArgs))\n }\n\n const result = await executeTool(tool.name, toolArgs, toolContext)\n\n if (!result.success) {\n log(`Tool error: ${result.error}`)\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ error: result.error, code: result.errorCode }),\n },\n ],\n isError: true,\n }\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n }\n )\n } catch (error) {\n if (debug) {\n log(`Skipping tool ${tool.name} - registration failed:`, error instanceof Error ? error.message : error)\n }\n continue\n }\n }\n\n return server\n}\n\n/**\n * Development MCP server for Claude Code integration.\n *\n * This server uses HTTP transport and authenticates via the\n * x-api-key header configured in .mcp.json file.\n *\n * Usage:\n * yarn mcp:dev\n *\n * Configure in .mcp.json for Claude Code with HTTP transport.\n */\nexport async function runMcpDevServer(): Promise<void> {\n const apiKey = await getApiKeyFromMcpJson()\n const port = parseInt(process.env.MCP_DEV_PORT ?? '', 10) || DEFAULT_PORT\n const debug = process.env.MCP_DEBUG === 'true'\n\n if (!apiKey) {\n log('Error: API key not found in .mcp.json')\n log('')\n log('To get an API key:')\n log(' 1. Log into Open Mercato as an admin')\n log(' 2. Go to Settings > API Keys')\n log(' 3. Create a new key with the required permissions')\n log('')\n log('Then configure in .mcp.json:')\n log(' {')\n log(' \"mcpServers\": {')\n log(' \"open-mercato\": {')\n log(' \"type\": \"http\",')\n log(' \"url\": \"http://localhost:3001/mcp\",')\n log(' \"headers\": {')\n log(' \"x-api-key\": \"omk_your_api_key_here\"')\n log(' }')\n log(' }')\n log(' }')\n log(' }')\n process.exit(1)\n }\n\n log('Starting development MCP HTTP server...')\n\n // Create DI container\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n\n // Authenticate the API key upfront\n log('Authenticating API key...')\n const authResult = await authenticateMcpRequest(apiKey, container)\n\n if (!authResult.success) {\n log(`Authentication failed: ${authResult.error}`)\n process.exit(1)\n }\n\n log(`Authenticated as: ${authResult.keyName}`)\n log(`Tenant: ${authResult.tenantId ?? '(global)'}`)\n log(`Organization: ${authResult.organizationId ?? '(none)'}`)\n log(`Super admin: ${authResult.isSuperAdmin}`)\n log(`Features: ${authResult.features.length > 0 ? authResult.features.join(', ') : '(none)'}`)\n\n // Load tools\n log('Loading tools...')\n await loadAllModuleTools()\n\n // Generate and cache entity graph\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n\n log('Generating entity relationship graph...')\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n log(`Entity graph: ${graph.nodes.length} entities, ${graph.edges.length} relationships`)\n } catch (error) {\n log('Entity graph generation skipped:', error instanceof Error ? error.message : error)\n }\n\n // Pre-cache rich OpenAPI spec for Code Mode search tool (prefers runtime module registry over static JSON)\n try {\n const { loadRichOpenApiSpec } = await import('./api-endpoint-index')\n const spec = await loadRichOpenApiSpec()\n if (spec) {\n log('Rich OpenAPI spec cached for Code Mode (with requestBody schemas)')\n } else {\n log('OpenAPI spec not available')\n }\n } catch (error) {\n log('OpenAPI spec caching skipped:', error instanceof Error ? error.message : error)\n }\n\n // Index tools and entity schemas for search (if search service available)\n try {\n const searchService = container.resolve('searchService') as SearchService\n await indexToolsForSearch(searchService)\n\n // Index entity schemas for hybrid search\n try {\n const { getCachedEntityGraph } = await import('./entity-graph')\n const { indexEntitiesForSearch } = await import('./entity-index')\n const graph = getCachedEntityGraph()\n if (graph) {\n const { count } = await indexEntitiesForSearch(searchService, graph)\n if (count > 0) {\n log(`Indexed ${count} entity schemas for discovery`)\n }\n }\n } catch (entityError) {\n log('Entity schema indexing skipped:', entityError instanceof Error ? entityError.message : entityError)\n }\n } catch {\n log('Search indexing skipped (search service not available)')\n }\n\n // Generate a stable session ID for dev mode (enables session memory / caching)\n const { randomBytes } = await import('node:crypto')\n const devSessionId = 'dev_' + randomBytes(8).toString('hex')\n log(`Session ID: ${devSessionId} (stable for this server instance)`)\n\n // Create tool context from auth result\n const toolContext: McpToolContext = {\n tenantId: authResult.tenantId,\n organizationId: authResult.organizationId,\n userId: authResult.userId,\n container,\n userFeatures: authResult.features,\n isSuperAdmin: authResult.isSuperAdmin,\n apiKeySecret: apiKey,\n sessionId: devSessionId,\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || '/', `http://localhost:${port}`)\n\n // Health check endpoint\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({\n status: 'ok',\n mode: 'development',\n tools: getToolRegistry().listToolNames().length,\n tenant: authResult.tenantId,\n timestamp: new Date().toISOString(),\n }))\n return\n }\n\n if (url.pathname !== '/mcp') {\n res.writeHead(404, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Not found' }))\n return\n }\n\n // Extract and validate API key from header\n const headers: Record<string, string | undefined> = {}\n for (const [key, value] of Object.entries(req.headers)) {\n headers[key] = Array.isArray(value) ? value[0] : value\n }\n\n const providedApiKey = extractApiKeyFromHeaders(headers)\n if (!providedApiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'API key required (x-api-key header)' }))\n return\n }\n\n // Validate against the configured API key\n if (providedApiKey !== apiKey) {\n res.writeHead(401, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Invalid API key' }))\n return\n }\n\n if (debug) {\n log(`Authenticated request (${req.method})`)\n }\n\n try {\n // Create stateless transport\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: req.method === 'POST',\n })\n\n // Create server with pre-authenticated context (no session tokens needed)\n const mcpServer = createDevMcpServer(toolContext, authResult.features, authResult.isSuperAdmin, debug)\n\n // Connect server to transport\n await mcpServer.connect(transport)\n\n // Handle the request\n if (req.method === 'POST') {\n const body = await parseJsonBody(req)\n await transport.handleRequest(req, res, body)\n } else {\n await transport.handleRequest(req, res)\n }\n\n // Cleanup after response finishes\n res.on('finish', () => {\n transport.close()\n mcpServer.close()\n if (debug) {\n log(`Request completed, cleaned up`)\n }\n })\n } catch (error) {\n log('Error handling request:', error)\n if (!res.headersSent) {\n if (error instanceof Error && error.message === 'Request payload too large') {\n res.writeHead(413, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify({ error: 'Request payload too large (max 1MB)' }))\n return\n }\n\n res.writeHead(500, { 'Content-Type': 'application/json' })\n res.end(\n JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: `Internal server error: ${error instanceof Error ? error.message : String(error)}`,\n },\n id: null,\n })\n )\n }\n }\n })\n\n const toolCount = getToolRegistry().listToolNames().length\n\n log(`Tools registered: ${toolCount}`)\n log(`Endpoint: http://localhost:${port}/mcp`)\n log(`Health: http://localhost:${port}/health`)\n log(`Mode: Development (API key auth, no session tokens)`)\n\n return new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n log(`Server listening on port ${port}`)\n log('Ready for Claude Code connections')\n })\n\n const shutdown = async () => {\n log('Shutting down...')\n httpServer.close(() => {\n log('Server closed')\n resolve()\n })\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -10,7 +10,8 @@ import { executeTool } from "./tool-executor.js";
|
|
|
10
10
|
import { loadAllModuleTools, indexToolsForSearch } from "./tool-loader.js";
|
|
11
11
|
import { authenticateMcpRequest, hasRequiredFeatures } from "./auth.js";
|
|
12
12
|
async function createMcpServer(options) {
|
|
13
|
-
const { config, container, context,
|
|
13
|
+
const { config, container, context, allowUnauthenticatedSuperadmin } = options;
|
|
14
|
+
const apiKeySecret = typeof options.apiKeySecret === "string" && options.apiKeySecret.trim().length > 0 ? options.apiKeySecret : void 0;
|
|
14
15
|
let tenantId = null;
|
|
15
16
|
let organizationId = null;
|
|
16
17
|
let userId = null;
|
|
@@ -27,29 +28,34 @@ async function createMcpServer(options) {
|
|
|
27
28
|
userFeatures = authResult.features;
|
|
28
29
|
isSuperAdmin = authResult.isSuperAdmin;
|
|
29
30
|
console.error(`[MCP Server] Authenticated via API key: ${authResult.keyName}`);
|
|
30
|
-
} else if (context) {
|
|
31
|
+
} else if (context && context.userId) {
|
|
31
32
|
tenantId = context.tenantId;
|
|
32
33
|
organizationId = context.organizationId;
|
|
33
34
|
userId = context.userId;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
try {
|
|
36
|
+
const rbacService = container.resolve("rbacService");
|
|
37
|
+
const acl = await rbacService.loadAcl(userId, {
|
|
38
|
+
tenantId,
|
|
39
|
+
organizationId
|
|
40
|
+
});
|
|
41
|
+
userFeatures = acl.features;
|
|
42
|
+
isSuperAdmin = acl.isSuperAdmin;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("[MCP Server] Failed to load user ACL:", error);
|
|
45
|
+
}
|
|
46
|
+
} else if (allowUnauthenticatedSuperadmin) {
|
|
47
|
+
if (context) {
|
|
48
|
+
tenantId = context.tenantId;
|
|
49
|
+
organizationId = context.organizationId;
|
|
49
50
|
}
|
|
50
|
-
} else {
|
|
51
51
|
isSuperAdmin = true;
|
|
52
|
-
console.error(
|
|
52
|
+
console.error(
|
|
53
|
+
"[MCP Server] WARNING: allowUnauthenticatedSuperadmin is enabled \u2014 running with UNAUTHENTICATED SUPERADMIN access and no per-user ACL. Do not use this outside local development/testing."
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"[internal] MCP server refused to start: no authentication provided. Supply a valid apiKeySecret, a context with a non-empty userId, or explicitly set allowUnauthenticatedSuperadmin: true for local development."
|
|
58
|
+
);
|
|
53
59
|
}
|
|
54
60
|
const toolContext = {
|
|
55
61
|
tenantId,
|
|
@@ -142,14 +148,14 @@ async function runMcpServer(options) {
|
|
|
142
148
|
const transport = new StdioServerTransport();
|
|
143
149
|
const toolCount = getToolRegistry().listToolNames().length;
|
|
144
150
|
console.error(`[MCP Server] Starting ${options.config.name} v${options.config.version}`);
|
|
145
|
-
if (options.apiKeySecret) {
|
|
151
|
+
if (options.apiKeySecret && options.apiKeySecret.trim().length > 0) {
|
|
146
152
|
console.error(`[MCP Server] Authentication: API key`);
|
|
147
|
-
} else if (options.context) {
|
|
153
|
+
} else if (options.context && options.context.userId) {
|
|
148
154
|
console.error(`[MCP Server] Tenant: ${options.context.tenantId ?? "(none)"}`);
|
|
149
155
|
console.error(`[MCP Server] Organization: ${options.context.organizationId ?? "(none)"}`);
|
|
150
|
-
console.error(`[MCP Server] User: ${options.context.userId
|
|
156
|
+
console.error(`[MCP Server] User: ${options.context.userId}`);
|
|
151
157
|
} else {
|
|
152
|
-
console.error(`[MCP Server] Authentication: none (superadmin
|
|
158
|
+
console.error(`[MCP Server] Authentication: none (unauthenticated superadmin opt-in)`);
|
|
153
159
|
}
|
|
154
160
|
console.error(`[MCP Server] Tools registered: ${toolCount}`);
|
|
155
161
|
await server.connect(transport);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/mcp-server.ts"],
|
|
4
|
-
"sourcesContent": ["import { Server } from '@modelcontextprotocol/sdk/server/index.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {\n ListToolsRequestSchema,\n CallToolRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, hasRequiredFeatures } from './auth'\nimport type { McpServerOptions, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\n/**\n * Create and configure an MCP server instance.\n */\nexport async function createMcpServer(options: McpServerOptions): Promise<Server> {\n const { config, container, context,
|
|
5
|
-
"mappings": "AAAA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAChC,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB,2BAA2B;AACxD,SAAS,wBAAwB,2BAA2B;AAQ5D,eAAsB,gBAAgB,SAA4C;AAChF,QAAM,EAAE,QAAQ,WAAW,SAAS,aAAa,
|
|
4
|
+
"sourcesContent": ["import { Server } from '@modelcontextprotocol/sdk/server/index.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {\n ListToolsRequestSchema,\n CallToolRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js'\nimport { zodToJsonSchema } from 'zod-to-json-schema'\nimport { getToolRegistry } from './tool-registry'\nimport { executeTool } from './tool-executor'\nimport { loadAllModuleTools, indexToolsForSearch } from './tool-loader'\nimport { authenticateMcpRequest, hasRequiredFeatures } from './auth'\nimport type { McpServerOptions, McpToolContext } from './types'\nimport type { SearchService } from '@open-mercato/search/service'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\n\n/**\n * Create and configure an MCP server instance.\n */\nexport async function createMcpServer(options: McpServerOptions): Promise<Server> {\n const { config, container, context, allowUnauthenticatedSuperadmin } = options\n\n // Treat empty / whitespace-only secrets as missing so a blank api key cannot\n // fall through into an unauthenticated branch.\n const apiKeySecret =\n typeof options.apiKeySecret === 'string' && options.apiKeySecret.trim().length > 0\n ? options.apiKeySecret\n : undefined\n\n let tenantId: string | null = null\n let organizationId: string | null = null\n let userId: string | null = null\n let userFeatures: string[] = []\n let isSuperAdmin = false\n\n // API key authentication takes precedence\n if (apiKeySecret) {\n const authResult = await authenticateMcpRequest(apiKeySecret, container)\n if (!authResult.success) {\n throw new Error(`API key authentication failed: ${authResult.error}`)\n }\n tenantId = authResult.tenantId\n organizationId = authResult.organizationId\n userId = authResult.userId\n userFeatures = authResult.features\n isSuperAdmin = authResult.isSuperAdmin\n console.error(`[MCP Server] Authenticated via API key: ${authResult.keyName}`)\n } else if (context && context.userId) {\n // Manual context with a real user \u2014 load that user's ACL.\n tenantId = context.tenantId\n organizationId = context.organizationId\n userId = context.userId\n\n try {\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n }>\n }\n const acl = await rbacService.loadAcl(userId, {\n tenantId,\n organizationId,\n })\n userFeatures = acl.features\n isSuperAdmin = acl.isSuperAdmin\n } catch (error) {\n console.error('[MCP Server] Failed to load user ACL:', error)\n }\n } else if (allowUnauthenticatedSuperadmin) {\n // Explicit, loud dev/testing opt-in. Without a user there is no ACL to load,\n // so the server runs as superadmin with no tenant scoping beyond whatever the\n // caller pinned via `context`. NEVER enable this in production.\n if (context) {\n tenantId = context.tenantId\n organizationId = context.organizationId\n }\n isSuperAdmin = true\n console.error(\n '[MCP Server] WARNING: allowUnauthenticatedSuperadmin is enabled \u2014 running with UNAUTHENTICATED SUPERADMIN access and no per-user ACL. Do not use this outside local development/testing.'\n )\n } else {\n // Fail closed: refuse to start rather than silently escalating to superadmin.\n throw new Error(\n '[internal] MCP server refused to start: no authentication provided. Supply a valid apiKeySecret, a context with a non-empty userId, or explicitly set allowUnauthenticatedSuperadmin: true for local development.'\n )\n }\n\n const toolContext: McpToolContext = {\n tenantId,\n organizationId,\n userId,\n container,\n userFeatures,\n isSuperAdmin,\n apiKeySecret,\n }\n\n const server = new Server(\n { name: config.name, version: config.version },\n { capabilities: { tools: {} } }\n )\n\n // List tools handler\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n const registry = getToolRegistry()\n const tools = Array.from(registry.getTools().values())\n\n // Filter tools based on user permissions\n const rbacService = container.resolve<RbacService>('rbacService')\n const accessibleTools = tools.filter((tool) =>\n hasRequiredFeatures(tool.requiredFeatures, userFeatures, isSuperAdmin, rbacService)\n )\n\n if (config.debug) {\n console.error(\n `[MCP Server] Listing ${accessibleTools.length}/${tools.length} tools (filtered by ACL)`\n )\n }\n\n return {\n tools: accessibleTools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n // Cast to any for Zod v4 compatibility with zod-to-json-schema\n inputSchema: zodToJsonSchema(tool.inputSchema as any) as Record<string, unknown>,\n })),\n }\n })\n\n // Call tool handler\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params\n\n if (config.debug) {\n console.error(`[MCP Server] Calling tool: ${name}`, JSON.stringify(args))\n }\n\n const result = await executeTool(name, args ?? {}, toolContext)\n\n if (!result.success) {\n return {\n content: [\n {\n type: 'text',\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',\n text: JSON.stringify(result.result, null, 2),\n },\n ],\n }\n })\n\n return server\n}\n\n/**\n * Run MCP server with stdio transport.\n * This keeps the process running until terminated.\n *\n * Supports two authentication modes:\n * 1. API key: Provide `apiKeySecret` option\n * 2. Manual context: Provide `context` with tenant/org/user\n */\nexport async function runMcpServer(options: McpServerOptions): Promise<void> {\n // Generate entity graph for Code Mode search tool\n try {\n const { extractEntityGraph, cacheEntityGraph } = await import('./entity-graph')\n const { getOrm } = await import('@open-mercato/shared/lib/db/mikro')\n const orm = await getOrm()\n const graph = await extractEntityGraph(orm)\n cacheEntityGraph(graph)\n console.error(`[MCP Server] Entity graph: ${graph.nodes.length} entities`)\n } catch (error) {\n console.error('[MCP Server] Entity graph skipped:', error instanceof Error ? error.message : error)\n }\n\n // Pre-cache raw OpenAPI spec for Code Mode search tool\n try {\n const { getRawOpenApiSpec } = await import('./api-endpoint-index')\n await getRawOpenApiSpec()\n console.error('[MCP Server] Raw OpenAPI spec cached for Code Mode')\n } catch (error) {\n console.error('[MCP Server] Raw OpenAPI spec caching skipped:', error instanceof Error ? error.message : error)\n }\n\n // Load tools from all modules before starting\n await loadAllModuleTools()\n\n // Index tools for hybrid search discovery (if search service available)\n try {\n const searchService = options.container.resolve('searchService') as SearchService\n await indexToolsForSearch(searchService)\n } catch (error) {\n // Search service might not be configured - discovery will use fallback\n console.error('[MCP Server] Search indexing skipped (search service not available)')\n }\n\n const server = await createMcpServer(options)\n const transport = new StdioServerTransport()\n\n const toolCount = getToolRegistry().listToolNames().length\n\n console.error(`[MCP Server] Starting ${options.config.name} v${options.config.version}`)\n\n if (options.apiKeySecret && options.apiKeySecret.trim().length > 0) {\n console.error(`[MCP Server] Authentication: API key`)\n } else if (options.context && options.context.userId) {\n console.error(`[MCP Server] Tenant: ${options.context.tenantId ?? '(none)'}`)\n console.error(`[MCP Server] Organization: ${options.context.organizationId ?? '(none)'}`)\n console.error(`[MCP Server] User: ${options.context.userId}`)\n } else {\n console.error(`[MCP Server] Authentication: none (unauthenticated superadmin opt-in)`)\n }\n\n console.error(`[MCP Server] Tools registered: ${toolCount}`)\n\n await server.connect(transport)\n\n console.error('[MCP Server] Connected and ready for requests')\n\n // Handle shutdown gracefully\n const shutdown = async () => {\n console.error('[MCP Server] Shutting down...')\n await server.close()\n process.exit(0)\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const PENDING_ACTION_CLEANUP_SCHEDULE_KEY = "ai_assistant:pending-action-cleanup";
|
|
3
|
+
const TOKEN_USAGE_PRUNE_SCHEDULE_KEY = "ai_assistant:token-usage-prune";
|
|
4
|
+
function stableScheduleUuid(stableKey) {
|
|
5
|
+
const hex = createHash("sha256").update(stableKey).digest("hex");
|
|
6
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
7
|
+
}
|
|
8
|
+
const PENDING_ACTION_CLEANUP_SCHEDULE_ID = stableScheduleUuid(PENDING_ACTION_CLEANUP_SCHEDULE_KEY);
|
|
9
|
+
const TOKEN_USAGE_PRUNE_SCHEDULE_ID = stableScheduleUuid(TOKEN_USAGE_PRUNE_SCHEDULE_KEY);
|
|
3
10
|
async function ensurePendingActionCleanupSchedule(container) {
|
|
4
11
|
if (!container) return;
|
|
5
12
|
let schedulerService;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/ai_assistant/setup.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nconst
|
|
5
|
-
"mappings": "AAEA,MAAM,qCAAqC;
|
|
4
|
+
"sourcesContent": ["import { createHash } from 'node:crypto'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nconst PENDING_ACTION_CLEANUP_SCHEDULE_KEY = 'ai_assistant:pending-action-cleanup'\nconst TOKEN_USAGE_PRUNE_SCHEDULE_KEY = 'ai_assistant:token-usage-prune'\n\nfunction stableScheduleUuid(stableKey: string): string {\n const hex = createHash('sha256').update(stableKey).digest('hex')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\nconst PENDING_ACTION_CLEANUP_SCHEDULE_ID = stableScheduleUuid(PENDING_ACTION_CLEANUP_SCHEDULE_KEY)\nconst TOKEN_USAGE_PRUNE_SCHEDULE_ID = stableScheduleUuid(TOKEN_USAGE_PRUNE_SCHEDULE_KEY)\n\n/**\n * System-scoped recurring schedule: every 5 minutes, enqueue a job to the\n * `ai-pending-action-cleanup` queue so the worker can sweep rows whose TTL\n * elapsed without any confirm/cancel activity (Step 5.12). The schedule id\n * is stable and `scheduler.register()` is an upsert, so calling this from\n * every tenant bootstrap stays idempotent.\n */\nasync function ensurePendingActionCleanupSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: PENDING_ACTION_CLEANUP_SCHEDULE_ID,\n name: 'AI pending-action cleanup',\n description:\n 'Sweep pending AI mutation approvals whose TTL elapsed without confirm/cancel and flip them to expired.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '5m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-pending-action-cleanup',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register pending-action cleanup schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\n/**\n * System-scoped daily schedule: enqueue a job to the `ai-token-usage-prune`\n * queue to prune events older than the retention window and reconcile the\n * daily rollup session counts.\n *\n * Phase 6.4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nasync function ensureTokenUsagePruneSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: TOKEN_USAGE_PRUNE_SCHEDULE_ID,\n name: 'AI token-usage prune',\n description:\n 'Delete ai_token_usage_events rows older than AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS (default 90) and reconcile session_count on the daily rollup.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '24h',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-token-usage-prune',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register token-usage prune schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: [\n 'ai_assistant.view',\n 'ai_assistant.settings.manage',\n 'ai_assistant.conversations.manage',\n 'ai_assistant.conversations.share',\n 'ai_assistant.mcp.serve',\n 'ai_assistant.tools.list',\n 'ai_assistant.mcp_servers.view',\n 'ai_assistant.mcp_servers.manage',\n ],\n employee: ['ai_assistant.view', 'ai_assistant.conversations.share'],\n },\n\n async seedDefaults({ container }) {\n await ensurePendingActionCleanupSchedule(container)\n await ensureTokenUsagePruneSchedule(container)\n },\n}\n\nexport default setup\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AAG3B,MAAM,sCAAsC;AAC5C,MAAM,iCAAiC;AAEvC,SAAS,mBAAmB,WAA2B;AACrD,QAAM,MAAM,WAAW,QAAQ,EAAE,OAAO,SAAS,EAAE,OAAO,KAAK;AAC/D,SAAO,GAAG,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC,IAAI,IAAI,MAAM,IAAI,EAAE,CAAC;AAC9G;AAEA,MAAM,qCAAqC,mBAAmB,mCAAmC;AACjG,MAAM,gCAAgC,mBAAmB,8BAA8B;AASvF,eAAe,mCACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AASA,eAAe,8BACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU,CAAC,qBAAqB,kCAAkC;AAAA,EACpE;AAAA,EAEA,MAAM,aAAa,EAAE,UAAU,GAAG;AAChC,UAAM,mCAAmC,SAAS;AAClD,UAAM,8BAA8B,SAAS;AAAA,EAC/C;AACF;AAEA,IAAO,gBAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.6.5
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -85,11 +85,11 @@
|
|
|
85
85
|
}
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@ai-sdk/anthropic": "^3.0.
|
|
89
|
-
"@ai-sdk/google": "^3.0.
|
|
90
|
-
"@ai-sdk/openai": "^3.0.
|
|
88
|
+
"@ai-sdk/anthropic": "^3.0.84",
|
|
89
|
+
"@ai-sdk/google": "^3.0.82",
|
|
90
|
+
"@ai-sdk/openai": "^3.0.71",
|
|
91
91
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
92
|
-
"ai": "^6.0.
|
|
92
|
+
"ai": "^6.0.205",
|
|
93
93
|
"cmdk": "^1.0.0",
|
|
94
94
|
"framer-motion": "^12.40.0",
|
|
95
95
|
"isolated-vm": "^7.0.0",
|
|
@@ -98,16 +98,16 @@
|
|
|
98
98
|
"zod-to-json-schema": "^3.25.2"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
|
-
"@open-mercato/shared": "0.6.5
|
|
102
|
-
"@open-mercato/ui": "0.6.5
|
|
101
|
+
"@open-mercato/shared": "0.6.5",
|
|
102
|
+
"@open-mercato/ui": "0.6.5",
|
|
103
103
|
"react": "^19.0.0",
|
|
104
104
|
"react-dom": "^19.0.0",
|
|
105
105
|
"zod": ">=3.23.0"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@open-mercato/cli": "0.6.5
|
|
109
|
-
"@open-mercato/shared": "0.6.5
|
|
110
|
-
"@open-mercato/ui": "0.6.5
|
|
108
|
+
"@open-mercato/cli": "0.6.5",
|
|
109
|
+
"@open-mercato/shared": "0.6.5",
|
|
110
|
+
"@open-mercato/ui": "0.6.5",
|
|
111
111
|
"@types/react": "^19.2.17",
|
|
112
112
|
"@types/react-dom": "^19.2.3",
|
|
113
113
|
"react": "19.2.7",
|
|
@@ -122,6 +122,5 @@
|
|
|
122
122
|
"type": "git",
|
|
123
123
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
124
124
|
"directory": "packages/ai-assistant"
|
|
125
|
-
}
|
|
126
|
-
"stableVersion": "0.6.4"
|
|
125
|
+
}
|
|
127
126
|
}
|