@open-mercato/ai-assistant 0.6.5-develop.5169.1.d0671533ca → 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.
- package/dist/modules/ai_assistant/lib/mcp-client.js +8 -18
- package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
- package/package.json +6 -6
- package/src/modules/ai_assistant/lib/__tests__/mcp-client.integration.test.ts +53 -0
- package/src/modules/ai_assistant/lib/__tests__/mcp-client.test.ts +74 -0
- package/src/modules/ai_assistant/lib/mcp-client.ts +8 -23
|
@@ -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
|
|
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
|
|
33
|
+
const transport = new StdioClientTransport({
|
|
34
|
+
command,
|
|
35
|
+
args,
|
|
36
36
|
cwd,
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
env: process.env,
|
|
38
|
+
stderr: "pipe"
|
|
39
39
|
});
|
|
40
|
-
|
|
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
|
|
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 {
|
|
5
|
-
"mappings": "AAAA,SAAS,
|
|
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.
|
|
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.
|
|
102
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
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.
|
|
109
|
-
"@open-mercato/shared": "0.6.5-develop.
|
|
110
|
-
"@open-mercato/ui": "0.6.5-develop.
|
|
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
|
|
84
|
+
const transport = new StdioClientTransport({
|
|
85
|
+
command,
|
|
86
|
+
args,
|
|
89
87
|
cwd,
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
}
|