@open-mercato/ai-assistant 0.6.5-develop.5187.1.82e5532561 → 0.6.5-develop.5200.1.871eca3402

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.
@@ -1,13 +1,11 @@
1
- import { spawn } from "node:child_process";
2
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
2
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
4
  class McpClient {
6
- constructor(client, transport, apiKeySecret, childProcess) {
5
+ constructor(client, transport, apiKeySecret) {
7
6
  this.client = client;
8
7
  this.transport = transport;
9
8
  this.apiKeySecret = apiKeySecret;
10
- this.childProcess = childProcess;
11
9
  }
12
10
  /**
13
11
  * Connect to an MCP server via the specified transport.
@@ -32,29 +30,25 @@ class McpClient {
32
30
  options.apiKeySecret
33
31
  ];
34
32
  const cwd = options.cwd ?? process.cwd();
35
- const childProcess = spawn(command, args, {
33
+ const transport = new StdioClientTransport({
34
+ command,
35
+ args,
36
36
  cwd,
37
- stdio: ["pipe", "pipe", "pipe"],
38
- env: { ...process.env }
37
+ env: process.env,
38
+ stderr: "pipe"
39
39
  });
40
- childProcess.stderr?.on("data", (data) => {
40
+ transport.stderr?.on("data", (data) => {
41
41
  const message = data.toString().trim();
42
42
  if (message) {
43
43
  console.error(`[MCP Client] ${message}`);
44
44
  }
45
45
  });
46
- const transport = new StdioClientTransport({
47
- command,
48
- args,
49
- cwd,
50
- env: process.env
51
- });
52
46
  const client = new Client(
53
47
  { name: "open-mercato-client", version: "0.1.0" },
54
48
  { capabilities: {} }
55
49
  );
56
50
  await client.connect(transport);
57
- return new McpClient(client, transport, options.apiKeySecret, childProcess);
51
+ return new McpClient(client, transport, options.apiKeySecret);
58
52
  }
59
53
  /**
60
54
  * Connect via HTTP transport.
@@ -134,10 +128,6 @@ class McpClient {
134
128
  await this.transport.close();
135
129
  } catch {
136
130
  }
137
- if (this.childProcess) {
138
- this.childProcess.kill();
139
- this.childProcess = void 0;
140
- }
141
131
  }
142
132
  }
143
133
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/mcp-client.ts"],
4
- "sourcesContent": ["import { spawn, type ChildProcess } from 'node:child_process'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { McpClientInterface, ToolInfo, ToolResult } from './types'\n\n/**\n * Options for stdio transport.\n */\nexport type StdioClientOptions = {\n transport: 'stdio'\n /** API key secret (passed to server via --api-key) */\n apiKeySecret: string\n /** Command to run (default: 'yarn') */\n command?: string\n /** Arguments for the command (default: mercato mcp:serve with api-key) */\n args?: string[]\n /** Working directory (default: process.cwd()) */\n cwd?: string\n}\n\n/**\n * Options for HTTP transport.\n */\nexport type HttpClientOptions = {\n transport: 'http'\n /** API key secret (sent via x-api-key header) */\n apiKeySecret: string\n /** MCP server URL (e.g., 'http://localhost:3001/mcp') */\n url: string\n}\n\n/**\n * Combined options for McpClient.\n */\nexport type McpClientOptions = StdioClientOptions | HttpClientOptions\n\n/**\n * MCP protocol client for connecting to MCP servers.\n *\n * Supports two transport modes:\n * - stdio: Spawns server as subprocess\n * - http: Connects to HTTP server\n */\nexport class McpClient implements McpClientInterface {\n private client: Client\n private transport: StdioClientTransport | StreamableHTTPClientTransport\n private childProcess?: ChildProcess\n private apiKeySecret: string\n\n private constructor(\n client: Client,\n transport: StdioClientTransport | StreamableHTTPClientTransport,\n apiKeySecret: string,\n childProcess?: ChildProcess\n ) {\n this.client = client\n this.transport = transport\n this.apiKeySecret = apiKeySecret\n this.childProcess = childProcess\n }\n\n /**\n * Connect to an MCP server via the specified transport.\n */\n static async connect(options: McpClientOptions): Promise<McpClient> {\n if (options.transport === 'stdio') {\n return McpClient.connectStdio(options)\n } else {\n return McpClient.connectHttp(options)\n }\n }\n\n /**\n * Connect via stdio transport (spawn subprocess).\n */\n private static async connectStdio(options: StdioClientOptions): Promise<McpClient> {\n const command = options.command ?? 'yarn'\n const args = options.args ?? [\n 'mercato',\n 'ai_assistant',\n 'mcp:serve',\n '--api-key',\n options.apiKeySecret,\n ]\n const cwd = options.cwd ?? process.cwd()\n\n const childProcess = spawn(command, args, {\n cwd,\n stdio: ['pipe', 'pipe', 'pipe'],\n env: { ...process.env },\n })\n\n // Forward stderr for debugging\n childProcess.stderr?.on('data', (data) => {\n const message = data.toString().trim()\n if (message) {\n console.error(`[MCP Client] ${message}`)\n }\n })\n\n const transport = new StdioClientTransport({\n command,\n args,\n cwd,\n env: process.env as Record<string, string>,\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, childProcess)\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 if (this.childProcess) {\n this.childProcess.kill()\n this.childProcess = undefined\n }\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { McpClientInterface, ToolInfo, ToolResult } from './types'\n\n/**\n * Options for stdio transport.\n */\nexport type StdioClientOptions = {\n transport: 'stdio'\n /** API key secret (passed to server via --api-key) */\n apiKeySecret: string\n /** Command to run (default: 'yarn') */\n command?: string\n /** Arguments for the command (default: mercato mcp:serve with api-key) */\n args?: string[]\n /** Working directory (default: process.cwd()) */\n cwd?: string\n}\n\n/**\n * Options for HTTP transport.\n */\nexport type HttpClientOptions = {\n transport: 'http'\n /** API key secret (sent via x-api-key header) */\n apiKeySecret: string\n /** MCP server URL (e.g., 'http://localhost:3001/mcp') */\n url: string\n}\n\n/**\n * Combined options for McpClient.\n */\nexport type McpClientOptions = StdioClientOptions | HttpClientOptions\n\n/**\n * MCP protocol client for connecting to MCP servers.\n *\n * Supports two transport modes:\n * - stdio: Spawns server as subprocess\n * - http: Connects to HTTP server\n */\nexport class McpClient implements McpClientInterface {\n private client: Client\n private transport: StdioClientTransport | StreamableHTTPClientTransport\n private apiKeySecret: string\n\n private constructor(\n client: Client,\n transport: StdioClientTransport | StreamableHTTPClientTransport,\n apiKeySecret: string\n ) {\n this.client = client\n this.transport = transport\n this.apiKeySecret = apiKeySecret\n }\n\n /**\n * Connect to an MCP server via the specified transport.\n */\n static async connect(options: McpClientOptions): Promise<McpClient> {\n if (options.transport === 'stdio') {\n return McpClient.connectStdio(options)\n } else {\n return McpClient.connectHttp(options)\n }\n }\n\n /**\n * Connect via stdio transport (spawn subprocess).\n */\n private static async connectStdio(options: StdioClientOptions): Promise<McpClient> {\n const command = options.command ?? 'yarn'\n const args = options.args ?? [\n 'mercato',\n 'ai_assistant',\n 'mcp:serve',\n '--api-key',\n options.apiKeySecret,\n ]\n const cwd = options.cwd ?? process.cwd()\n\n const transport = new StdioClientTransport({\n command,\n args,\n cwd,\n env: process.env as Record<string, string>,\n stderr: 'pipe',\n })\n transport.stderr?.on('data', (data) => {\n const message = data.toString().trim()\n if (message) {\n console.error(`[MCP Client] ${message}`)\n }\n })\n\n const client = new Client(\n { name: 'open-mercato-client', version: '0.1.0' },\n { capabilities: {} }\n )\n\n await client.connect(transport)\n\n return new McpClient(client, transport, options.apiKeySecret)\n }\n\n /**\n * Connect via HTTP transport.\n */\n private static async connectHttp(options: HttpClientOptions): Promise<McpClient> {\n const transport = new StreamableHTTPClientTransport(\n new URL(options.url),\n {\n requestInit: {\n headers: {\n 'x-api-key': options.apiKeySecret,\n },\n },\n }\n )\n\n const client = new Client(\n { name: 'open-mercato-client', version: '0.1.0' },\n { capabilities: {} }\n )\n\n await client.connect(transport)\n\n return new McpClient(client, transport, options.apiKeySecret)\n }\n\n /**\n * List available tools from the server.\n */\n async listTools(): Promise<ToolInfo[]> {\n const response = await this.client.listTools()\n\n return response.tools.map((tool) => ({\n name: tool.name,\n description: tool.description ?? '',\n inputSchema: (tool.inputSchema ?? {}) as Record<string, unknown>,\n }))\n }\n\n /**\n * Call a tool on the server.\n */\n async callTool(name: string, args: unknown): Promise<ToolResult> {\n try {\n const response = await this.client.callTool({\n name,\n arguments: args as Record<string, unknown>,\n })\n\n // Parse content from response\n const content = response.content\n if (!Array.isArray(content) || content.length === 0) {\n return { success: true, result: null }\n }\n\n const firstContent = content[0]\n if (firstContent.type === 'text') {\n try {\n const parsed = JSON.parse(firstContent.text)\n\n // Check if it's an error response\n if (response.isError || parsed.error) {\n return {\n success: false,\n error: parsed.error ?? 'Unknown error',\n }\n }\n\n return { success: true, result: parsed }\n } catch {\n // Not JSON, return as-is\n return { success: true, result: firstContent.text }\n }\n }\n\n return { success: true, result: content }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return { success: false, error: message }\n }\n }\n\n /**\n * Close the client and release resources.\n */\n async close(): Promise<void> {\n try {\n await this.client.close()\n } catch {\n // Ignore close errors\n }\n\n try {\n await this.transport.close()\n } catch {\n // Ignore close errors\n }\n\n }\n}\n"],
5
+ "mappings": "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",
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-develop.5187.1.82e5532561",
3
+ "version": "0.6.5-develop.5200.1.871eca3402",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.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-develop.5187.1.82e5532561",
102
- "@open-mercato/ui": "0.6.5-develop.5187.1.82e5532561",
101
+ "@open-mercato/shared": "0.6.5-develop.5200.1.871eca3402",
102
+ "@open-mercato/ui": "0.6.5-develop.5200.1.871eca3402",
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-develop.5187.1.82e5532561",
109
- "@open-mercato/shared": "0.6.5-develop.5187.1.82e5532561",
110
- "@open-mercato/ui": "0.6.5-develop.5187.1.82e5532561",
108
+ "@open-mercato/cli": "0.6.5-develop.5200.1.871eca3402",
109
+ "@open-mercato/shared": "0.6.5-develop.5200.1.871eca3402",
110
+ "@open-mercato/ui": "0.6.5-develop.5200.1.871eca3402",
111
111
  "@types/react": "^19.2.17",
112
112
  "@types/react-dom": "^19.2.3",
113
113
  "react": "19.2.7",
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { McpClient } from '../mcp-client'
5
+
6
+ describe('McpClient stdio integration', () => {
7
+ it('starts exactly one SDK-managed server process for one stdio client connection', async () => {
8
+ const previousSpawnLog = process.env.MCP_SPAWN_LOG
9
+ const logPath = path.join(os.tmpdir(), `open-mercato-mcp-spawn-${Date.now()}.log`)
10
+ process.env.MCP_SPAWN_LOG = logPath
11
+
12
+ const serverCode = `
13
+ import fs from 'node:fs';
14
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
17
+ fs.appendFileSync(process.env.MCP_SPAWN_LOG, String(process.pid) + '\\n');
18
+ const server = new Server(
19
+ { name: 'open-mercato-spawn-proof', version: '1.0.0' },
20
+ { capabilities: { tools: {} } }
21
+ );
22
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [] }));
23
+ await server.connect(new StdioServerTransport());
24
+ `
25
+
26
+ try {
27
+ const client = await McpClient.connect({
28
+ transport: 'stdio',
29
+ apiKeySecret: 'test-secret',
30
+ command: process.execPath,
31
+ args: ['--input-type=module', '-e', serverCode],
32
+ cwd: process.cwd(),
33
+ })
34
+
35
+ try {
36
+ await expect(client.listTools()).resolves.toEqual([])
37
+ } finally {
38
+ await client.close()
39
+ }
40
+
41
+ await new Promise((resolve) => setTimeout(resolve, 250))
42
+ const pids = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean)
43
+ expect(pids).toHaveLength(1)
44
+ } finally {
45
+ if (previousSpawnLog === undefined) {
46
+ delete process.env.MCP_SPAWN_LOG
47
+ } else {
48
+ process.env.MCP_SPAWN_LOG = previousSpawnLog
49
+ }
50
+ fs.rmSync(logPath, { force: true })
51
+ }
52
+ })
53
+ })
@@ -0,0 +1,74 @@
1
+ import { EventEmitter } from 'node:events'
2
+ import { spawn } from 'node:child_process'
3
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
4
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
5
+ import { McpClient } from '../mcp-client'
6
+
7
+ const mockClientConnect = jest.fn(async () => undefined)
8
+ const mockClientClose = jest.fn(async () => undefined)
9
+ const mockClientListTools = jest.fn(async () => ({ tools: [] }))
10
+ const mockTransportClose = jest.fn(async () => undefined)
11
+ const mockTransportStderr = new EventEmitter()
12
+ const mockManualChildKill = jest.fn()
13
+
14
+ jest.mock('node:child_process', () => ({
15
+ spawn: jest.fn(() => ({
16
+ stderr: new EventEmitter(),
17
+ kill: mockManualChildKill,
18
+ })),
19
+ }))
20
+
21
+ jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({
22
+ Client: jest.fn().mockImplementation(() => ({
23
+ connect: mockClientConnect,
24
+ close: mockClientClose,
25
+ listTools: mockClientListTools,
26
+ })),
27
+ }))
28
+
29
+ jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
30
+ StdioClientTransport: jest.fn().mockImplementation(() => ({
31
+ close: mockTransportClose,
32
+ stderr: mockTransportStderr,
33
+ })),
34
+ }))
35
+
36
+ jest.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({
37
+ StreamableHTTPClientTransport: jest.fn().mockImplementation(() => ({
38
+ close: jest.fn(async () => undefined),
39
+ })),
40
+ }))
41
+
42
+ describe('McpClient stdio transport', () => {
43
+ beforeEach(() => {
44
+ jest.clearAllMocks()
45
+ })
46
+
47
+ it('delegates process spawning to StdioClientTransport without a manual duplicate spawn', async () => {
48
+ const client = await McpClient.connect({
49
+ transport: 'stdio',
50
+ apiKeySecret: 'test-secret',
51
+ command: 'node',
52
+ args: ['server.js'],
53
+ cwd: '/tmp/open-mercato',
54
+ })
55
+
56
+ expect(Client).toHaveBeenCalledTimes(1)
57
+ expect(StdioClientTransport).toHaveBeenCalledWith({
58
+ command: 'node',
59
+ args: ['server.js'],
60
+ cwd: '/tmp/open-mercato',
61
+ env: process.env,
62
+ stderr: 'pipe',
63
+ })
64
+ expect(mockClientConnect).toHaveBeenCalledTimes(1)
65
+ expect(mockClientConnect).toHaveBeenCalledWith(expect.any(Object))
66
+ expect(spawn).not.toHaveBeenCalled()
67
+
68
+ await client.close()
69
+
70
+ expect(mockClientClose).toHaveBeenCalledTimes(1)
71
+ expect(mockTransportClose).toHaveBeenCalledTimes(1)
72
+ expect(mockManualChildKill).not.toHaveBeenCalled()
73
+ })
74
+ })
@@ -1,4 +1,3 @@
1
- import { spawn, type ChildProcess } from 'node:child_process'
2
1
  import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3
2
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
4
3
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
@@ -45,19 +44,16 @@ export type McpClientOptions = StdioClientOptions | HttpClientOptions
45
44
  export class McpClient implements McpClientInterface {
46
45
  private client: Client
47
46
  private transport: StdioClientTransport | StreamableHTTPClientTransport
48
- private childProcess?: ChildProcess
49
47
  private apiKeySecret: string
50
48
 
51
49
  private constructor(
52
50
  client: Client,
53
51
  transport: StdioClientTransport | StreamableHTTPClientTransport,
54
- apiKeySecret: string,
55
- childProcess?: ChildProcess
52
+ apiKeySecret: string
56
53
  ) {
57
54
  this.client = client
58
55
  this.transport = transport
59
56
  this.apiKeySecret = apiKeySecret
60
- this.childProcess = childProcess
61
57
  }
62
58
 
63
59
  /**
@@ -85,27 +81,20 @@ export class McpClient implements McpClientInterface {
85
81
  ]
86
82
  const cwd = options.cwd ?? process.cwd()
87
83
 
88
- const childProcess = spawn(command, args, {
84
+ const transport = new StdioClientTransport({
85
+ command,
86
+ args,
89
87
  cwd,
90
- stdio: ['pipe', 'pipe', 'pipe'],
91
- env: { ...process.env },
88
+ env: process.env as Record<string, string>,
89
+ stderr: 'pipe',
92
90
  })
93
-
94
- // Forward stderr for debugging
95
- childProcess.stderr?.on('data', (data) => {
91
+ transport.stderr?.on('data', (data) => {
96
92
  const message = data.toString().trim()
97
93
  if (message) {
98
94
  console.error(`[MCP Client] ${message}`)
99
95
  }
100
96
  })
101
97
 
102
- const transport = new StdioClientTransport({
103
- command,
104
- args,
105
- cwd,
106
- env: process.env as Record<string, string>,
107
- })
108
-
109
98
  const client = new Client(
110
99
  { name: 'open-mercato-client', version: '0.1.0' },
111
100
  { capabilities: {} }
@@ -113,7 +102,7 @@ export class McpClient implements McpClientInterface {
113
102
 
114
103
  await client.connect(transport)
115
104
 
116
- return new McpClient(client, transport, options.apiKeySecret, childProcess)
105
+ return new McpClient(client, transport, options.apiKeySecret)
117
106
  }
118
107
 
119
108
  /**
@@ -213,9 +202,5 @@ export class McpClient implements McpClientInterface {
213
202
  // Ignore close errors
214
203
  }
215
204
 
216
- if (this.childProcess) {
217
- this.childProcess.kill()
218
- this.childProcess = undefined
219
- }
220
205
  }
221
206
  }