@mcp-web/client 0.1.0 → 0.1.1
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/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/standalone.js +7 -4
- package/package.json +11 -3
- package/esbuild.standalone.mjs +0 -17
- package/src/client.test.ts +0 -259
- package/src/client.ts +0 -783
- package/src/index.ts +0 -44
- package/src/schemas.ts +0 -38
- package/src/types.ts +0 -14
- package/src/utils.ts +0 -5
- package/tsconfig.json +0 -20
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAOA,YAAY,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,mBAAmB,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
import { MCPWebClient } from './client.js';
|
|
3
5
|
import { MCPWebClientConfigSchema } from './schemas.js';
|
|
4
6
|
// Export for programmatic use
|
|
5
7
|
export { MCPWebClient } from './client.js';
|
|
6
8
|
// Only run as CLI if this is the main module in Node.js
|
|
7
9
|
// Guard against running in Deno or when bundled
|
|
10
|
+
// Uses realpathSync on both sides to handle symlinks (e.g. npx, pnpm)
|
|
8
11
|
// @ts-expect-error - Deno global exists in Deno runtime
|
|
9
12
|
const isDeno = typeof Deno !== 'undefined';
|
|
10
13
|
const isNodeCLI = !isDeno &&
|
|
11
14
|
typeof process !== 'undefined' &&
|
|
12
|
-
process.argv &&
|
|
13
|
-
import.meta.url ===
|
|
15
|
+
process.argv?.[1] &&
|
|
16
|
+
realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
14
17
|
if (isNodeCLI) {
|
|
15
18
|
// Handle graceful shutdown
|
|
16
19
|
process.on('SIGINT', () => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAGxD,8BAA8B;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,wDAAwD;AACxD,gDAAgD;AAChD,wDAAwD;AACxD,MAAM,MAAM,GAAG,OAAO,IAAI,KAAK,WAAW,CAAC;AAC3C,MAAM,SAAS,GAAG,CAAC,MAAM;IACvB,OAAO,OAAO,KAAK,WAAW;IAC9B,OAAO,CAAC,IAAI;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAGxD,8BAA8B;AAC9B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,wDAAwD;AACxD,gDAAgD;AAChD,sEAAsE;AACtE,wDAAwD;AACxD,MAAM,MAAM,GAAG,OAAO,IAAI,KAAK,WAAW,CAAC;AAC3C,MAAM,SAAS,GAAG,CAAC,MAAM;IACvB,OAAO,OAAO,KAAK,WAAW;IAC9B,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACjB,YAAY,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAEjF,IAAI,SAAS,EAAE,CAAC;IACd,2BAA2B;IAC3B,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACzB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC;QAC5C,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;QACrC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU;QACjC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpG,CAAC,CAAC;IAEH,mBAAmB;IACnB,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC;IACxC,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;QAC3B,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAC;QAC3D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/standalone.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
5
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -6790,6 +6789,10 @@ var require_dist = __commonJS({
|
|
|
6790
6789
|
}
|
|
6791
6790
|
});
|
|
6792
6791
|
|
|
6792
|
+
// src/index.ts
|
|
6793
|
+
import { realpathSync } from "node:fs";
|
|
6794
|
+
import { fileURLToPath } from "node:url";
|
|
6795
|
+
|
|
6793
6796
|
// ../../node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/external.js
|
|
6794
6797
|
var external_exports = {};
|
|
6795
6798
|
__export(external_exports, {
|
|
@@ -19361,8 +19364,8 @@ var McpWebConfigSchema = external_exports.object({
|
|
|
19361
19364
|
description: external_exports.string().min(1).describe("The description of the server. This should describe the web app you want the AI App to control."),
|
|
19362
19365
|
/** The bridge server address as host:port (e.g., 'localhost:3001' or 'bridge.example.com'). Protocol is determined automatically based on page context. */
|
|
19363
19366
|
bridgeUrl: external_exports.string().optional().default("localhost:3001").transform((url2) => url2.replace(/^(wss?|https?):\/\//, "")).describe("The bridge server address as host:port (e.g., 'localhost:3001' or 'bridge.example.com'). Protocol is determined automatically."),
|
|
19364
|
-
/** Either a URL or a data URI
|
|
19365
|
-
icon: external_exports.string().optional().describe('Either a URL or a data URI
|
|
19367
|
+
/** Either a URL or a data URI (e.g., "data:image/svg+xml;base64,..." or "data:image/png;base64,..."). This is shown in the AI App. */
|
|
19368
|
+
icon: external_exports.string().optional().describe('Either a URL or a data URI (e.g., "data:image/svg+xml;base64,..." or "data:image/png;base64,..."). This is shown in the AI App.'),
|
|
19366
19369
|
/** The agent server address with optional path (e.g., 'localhost:3000' or 'localhost:3000/api/v1/query'). Protocol is determined automatically. Required for query support. */
|
|
19367
19370
|
agentUrl: external_exports.string().optional().transform((url2) => url2?.replace(/^(wss?|https?):\/\//, "")).describe("The agent server address with optional path (e.g., 'localhost:3000' or 'localhost:3000/api/v1/query'). Protocol is determined automatically."),
|
|
19368
19371
|
/** Authentication token for the agent. If not provided, will use auto-generated token. */
|
|
@@ -27200,7 +27203,7 @@ var MCPWebClient = class _MCPWebClient {
|
|
|
27200
27203
|
|
|
27201
27204
|
// src/index.ts
|
|
27202
27205
|
var isDeno = typeof Deno !== "undefined";
|
|
27203
|
-
var isNodeCLI = !isDeno && typeof process !== "undefined" && process.argv && import.meta.url ===
|
|
27206
|
+
var isNodeCLI = !isDeno && typeof process !== "undefined" && process.argv?.[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
27204
27207
|
if (isNodeCLI) {
|
|
27205
27208
|
process.on("SIGINT", () => {
|
|
27206
27209
|
console.error("Shutting down MCP Bridge Client...");
|
package/package.json
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-web/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"@mcp-web/client": "./dist/
|
|
7
|
+
"@mcp-web/client": "./dist/standalone.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/"
|
|
11
|
+
],
|
|
9
12
|
"exports": {
|
|
10
13
|
".": "./dist/index.js"
|
|
11
14
|
},
|
|
12
15
|
"dependencies": {
|
|
13
16
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
14
17
|
"zod": "~4.1.12",
|
|
15
|
-
"@mcp-web/types": "0.1.
|
|
18
|
+
"@mcp-web/types": "0.1.1"
|
|
16
19
|
},
|
|
17
20
|
"devDependencies": {
|
|
18
21
|
"@types/node": "^25.0.9",
|
|
19
22
|
"esbuild": "^0.24.2",
|
|
20
23
|
"typescript": "~5.9.3"
|
|
21
24
|
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/flekschas/mcp-web",
|
|
28
|
+
"directory": "packages/client"
|
|
29
|
+
},
|
|
22
30
|
"scripts": {
|
|
23
31
|
"dev": "tsc --watch",
|
|
24
32
|
"build": "tsc && node esbuild.standalone.mjs",
|
package/esbuild.standalone.mjs
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { build } from 'esbuild';
|
|
2
|
-
|
|
3
|
-
await build({
|
|
4
|
-
entryPoints: ['src/index.ts'],
|
|
5
|
-
bundle: true,
|
|
6
|
-
platform: 'node',
|
|
7
|
-
target: 'node22',
|
|
8
|
-
format: 'esm',
|
|
9
|
-
outfile: 'dist/standalone.js',
|
|
10
|
-
banner: {
|
|
11
|
-
js: '#!/usr/bin/env node',
|
|
12
|
-
},
|
|
13
|
-
minify: false,
|
|
14
|
-
sourcemap: false,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
console.log('✓ Built standalone bundle: dist/standalone.js');
|
package/src/client.test.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test';
|
|
2
|
-
import { ClientNotConextualizedErrorCode, QueryDoneErrorCode } from '@mcp-web/types';
|
|
3
|
-
import { MCPWebClient } from './client.js';
|
|
4
|
-
|
|
5
|
-
// Store original fetch for tests that need to mock it
|
|
6
|
-
const originalFetch = globalThis.fetch;
|
|
7
|
-
|
|
8
|
-
test('MCPWebClient initializes with config', () => {
|
|
9
|
-
const client = new MCPWebClient({
|
|
10
|
-
serverUrl: 'http://localhost:3002',
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
expect(client).toBeDefined();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('contextualize creates new client instance with query', () => {
|
|
17
|
-
const client = new MCPWebClient({
|
|
18
|
-
serverUrl: 'http://localhost:3002',
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const query = {
|
|
22
|
-
uuid: 'test-query-123',
|
|
23
|
-
prompt: 'Test prompt',
|
|
24
|
-
context: [],
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const contextClient = client.contextualize(query);
|
|
28
|
-
|
|
29
|
-
expect(contextClient).toBeDefined();
|
|
30
|
-
expect(contextClient !== client).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('callTool validates restrictTools', async () => {
|
|
34
|
-
const client = new MCPWebClient({
|
|
35
|
-
serverUrl: 'http://localhost:3002',
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const query = {
|
|
39
|
-
uuid: 'test-query',
|
|
40
|
-
prompt: 'test',
|
|
41
|
-
context: [],
|
|
42
|
-
tools: [
|
|
43
|
-
{
|
|
44
|
-
name: 'allowed-tool',
|
|
45
|
-
description: 'Allowed tool',
|
|
46
|
-
handler: () => {},
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
restrictTools: true,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const contextClient = client.contextualize(query);
|
|
53
|
-
|
|
54
|
-
// Should throw when calling a tool not in the allowed list
|
|
55
|
-
expect(async () => {
|
|
56
|
-
await contextClient.callTool('forbidden-tool', {});
|
|
57
|
-
}).toThrow('not allowed');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('callTool allows tools when restrictTools is false', async () => {
|
|
61
|
-
// Mock fetch to return success - Deno has native fetch!
|
|
62
|
-
globalThis.fetch = (async () => {
|
|
63
|
-
return new Response(
|
|
64
|
-
JSON.stringify({
|
|
65
|
-
jsonrpc: '2.0',
|
|
66
|
-
id: 1,
|
|
67
|
-
result: { data: 'success' },
|
|
68
|
-
}),
|
|
69
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
70
|
-
);
|
|
71
|
-
}) as unknown as typeof fetch;
|
|
72
|
-
|
|
73
|
-
const client = new MCPWebClient({
|
|
74
|
-
serverUrl: 'http://localhost:3002',
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const query = {
|
|
78
|
-
uuid: 'test-query',
|
|
79
|
-
prompt: 'test',
|
|
80
|
-
context: [],
|
|
81
|
-
tools: [
|
|
82
|
-
{
|
|
83
|
-
name: 'allowed-tool',
|
|
84
|
-
description: 'Allowed tool',
|
|
85
|
-
handler: () => {},
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
restrictTools: false, // Not restricting tools
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const contextClient = client.contextualize(query);
|
|
92
|
-
|
|
93
|
-
// Should succeed even with forbidden tool
|
|
94
|
-
const result = await contextClient.callTool('any-tool', {});
|
|
95
|
-
expect(result).toBeDefined();
|
|
96
|
-
|
|
97
|
-
globalThis.fetch = originalFetch;
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('callTool includes query context in request', async () => {
|
|
101
|
-
let capturedBody: unknown;
|
|
102
|
-
|
|
103
|
-
// Mock fetch and capture the request body
|
|
104
|
-
globalThis.fetch = (async (input: Request | URL | string, init?: RequestInit) => {
|
|
105
|
-
// Capture the body from init if it's a string
|
|
106
|
-
if (init && typeof init.body === 'string') {
|
|
107
|
-
capturedBody = JSON.parse(init.body);
|
|
108
|
-
} else if (input instanceof Request) {
|
|
109
|
-
// If input is a Request object, clone and read it
|
|
110
|
-
capturedBody = await input.clone().json();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return new Response(
|
|
114
|
-
JSON.stringify({
|
|
115
|
-
jsonrpc: '2.0',
|
|
116
|
-
id: 1,
|
|
117
|
-
result: { data: 'success' },
|
|
118
|
-
}),
|
|
119
|
-
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
120
|
-
);
|
|
121
|
-
}) as unknown as typeof fetch;
|
|
122
|
-
|
|
123
|
-
const client = new MCPWebClient({
|
|
124
|
-
serverUrl: 'http://localhost:3002',
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const query = {
|
|
128
|
-
uuid: 'test-query-123',
|
|
129
|
-
prompt: 'test',
|
|
130
|
-
context: [],
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const contextClient = client.contextualize(query);
|
|
134
|
-
await contextClient.callTool('test_tool', { arg: 'value' });
|
|
135
|
-
|
|
136
|
-
// Verify _meta was included in request body
|
|
137
|
-
expect(capturedBody).toBeDefined();
|
|
138
|
-
const requestBody = capturedBody as Record<string, unknown>;
|
|
139
|
-
expect(requestBody.params).toBeDefined();
|
|
140
|
-
const params = requestBody.params as Record<string, unknown>;
|
|
141
|
-
expect(params._meta).toBeDefined();
|
|
142
|
-
const metaParams = params._meta as Record<string, unknown>;
|
|
143
|
-
expect(metaParams.queryId).toBe('test-query-123');
|
|
144
|
-
|
|
145
|
-
globalThis.fetch = originalFetch;
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test('listTools returns query tools when available', async () => {
|
|
149
|
-
const client = new MCPWebClient({
|
|
150
|
-
serverUrl: 'http://localhost:3002',
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const query = {
|
|
154
|
-
uuid: 'test-query',
|
|
155
|
-
prompt: 'test',
|
|
156
|
-
context: [],
|
|
157
|
-
tools: [
|
|
158
|
-
{
|
|
159
|
-
name: 'tool1',
|
|
160
|
-
description: 'Tool 1',
|
|
161
|
-
handler: () => {},
|
|
162
|
-
inputSchema: { type: 'object' as const, properties: {} },
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
name: 'tool2',
|
|
166
|
-
description: 'Tool 2',
|
|
167
|
-
handler: () => {},
|
|
168
|
-
},
|
|
169
|
-
],
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const contextClient = client.contextualize(query);
|
|
173
|
-
const result = await contextClient.listTools();
|
|
174
|
-
|
|
175
|
-
expect(result.tools.length).toBe(2);
|
|
176
|
-
expect(result.tools[0].name).toBe('tool1');
|
|
177
|
-
expect(result.tools[1].name).toBe('tool2');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('complete marks query as completed and prevents further calls', async () => {
|
|
181
|
-
globalThis.fetch = (async () => {
|
|
182
|
-
return new Response(JSON.stringify({}), { status: 200 });
|
|
183
|
-
}) as unknown as typeof fetch;
|
|
184
|
-
|
|
185
|
-
const client = new MCPWebClient({
|
|
186
|
-
serverUrl: 'http://localhost:3002',
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const query = {
|
|
190
|
-
uuid: 'test-query',
|
|
191
|
-
prompt: 'test',
|
|
192
|
-
context: [],
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const contextClient = client.contextualize(query);
|
|
196
|
-
|
|
197
|
-
// Complete the query
|
|
198
|
-
await contextClient.complete('Done!');
|
|
199
|
-
|
|
200
|
-
// Should throw when trying to call tools after completion
|
|
201
|
-
expect(async () => {
|
|
202
|
-
await contextClient.callTool('test_tool', {});
|
|
203
|
-
}).toThrow(QueryDoneErrorCode);
|
|
204
|
-
|
|
205
|
-
// Should throw when trying to complete again
|
|
206
|
-
expect(async () => {
|
|
207
|
-
await contextClient.complete('Done again!');
|
|
208
|
-
}).toThrow(QueryDoneErrorCode);
|
|
209
|
-
|
|
210
|
-
globalThis.fetch = originalFetch;
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test('sendProgress throws on non-contextualized client', async () => {
|
|
214
|
-
const client = new MCPWebClient({
|
|
215
|
-
serverUrl: 'http://localhost:3002',
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
expect(async () => {
|
|
219
|
-
await client.sendProgress('test');
|
|
220
|
-
}).toThrow(ClientNotConextualizedErrorCode);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test('complete throws on non-contextualized client', async () => {
|
|
224
|
-
const client = new MCPWebClient({
|
|
225
|
-
serverUrl: 'http://localhost:3002',
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
expect(async () => {
|
|
229
|
-
await client.complete('test');
|
|
230
|
-
}).toThrow(ClientNotConextualizedErrorCode);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test('fail marks query as failed and prevents further operations', async () => {
|
|
234
|
-
globalThis.fetch = (async () => {
|
|
235
|
-
return new Response(JSON.stringify({}), { status: 200 });
|
|
236
|
-
}) as unknown as typeof fetch;
|
|
237
|
-
|
|
238
|
-
const client = new MCPWebClient({
|
|
239
|
-
serverUrl: 'http://localhost:3002',
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
const query = {
|
|
243
|
-
uuid: 'test-query',
|
|
244
|
-
prompt: 'test',
|
|
245
|
-
context: [],
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const contextClient = client.contextualize(query);
|
|
249
|
-
|
|
250
|
-
// Fail the query
|
|
251
|
-
await contextClient.fail('Something went wrong');
|
|
252
|
-
|
|
253
|
-
// Should throw when trying to call tools after failure
|
|
254
|
-
expect(async () => {
|
|
255
|
-
await contextClient.callTool('test_tool', {});
|
|
256
|
-
}).toThrow(QueryDoneErrorCode);
|
|
257
|
-
|
|
258
|
-
globalThis.fetch = originalFetch;
|
|
259
|
-
});
|
package/src/client.ts
DELETED
|
@@ -1,783 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ClientNotConextualizedErrorCode,
|
|
5
|
-
type ErroredListPromptsResult,
|
|
6
|
-
type ErroredListResourcesResult,
|
|
7
|
-
type ErroredListToolsResult,
|
|
8
|
-
type FatalError,
|
|
9
|
-
InvalidAuthenticationErrorCode,
|
|
10
|
-
type McpRequestMetaParams,
|
|
11
|
-
MissingAuthenticationErrorCode,
|
|
12
|
-
type Query,
|
|
13
|
-
QueryDoneErrorCode,
|
|
14
|
-
QueryNotActiveErrorCode,
|
|
15
|
-
QueryNotFoundErrorCode,
|
|
16
|
-
QuerySchema,
|
|
17
|
-
} from '@mcp-web/types';
|
|
18
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
-
import {
|
|
21
|
-
type CallToolRequest,
|
|
22
|
-
CallToolRequestSchema,
|
|
23
|
-
type CallToolResult,
|
|
24
|
-
ListPromptsRequestSchema,
|
|
25
|
-
type ListPromptsResult,
|
|
26
|
-
ListResourcesRequestSchema,
|
|
27
|
-
type ListResourcesResult,
|
|
28
|
-
ListToolsRequestSchema,
|
|
29
|
-
type ListToolsResult,
|
|
30
|
-
type ReadResourceRequest,
|
|
31
|
-
ReadResourceRequestSchema,
|
|
32
|
-
type ReadResourceResult,
|
|
33
|
-
type Tool,
|
|
34
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
35
|
-
import {
|
|
36
|
-
JsonRpcRequestSchema,
|
|
37
|
-
JsonRpcResponseSchema,
|
|
38
|
-
MCPWebClientConfigSchema,
|
|
39
|
-
} from './schemas.js';
|
|
40
|
-
import type {
|
|
41
|
-
Content,
|
|
42
|
-
MCPWebClientConfig,
|
|
43
|
-
MCPWebClientConfigOutput,
|
|
44
|
-
} from './types.js';
|
|
45
|
-
|
|
46
|
-
function isFatalError<T extends object>(result: T | FatalError): result is FatalError {
|
|
47
|
-
return 'errorIsFatal' in result && result.errorIsFatal === true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* MCP client that connects AI agents (like Claude Desktop) to the bridge server.
|
|
52
|
-
*
|
|
53
|
-
* MCPWebClient implements the MCP protocol and can run as a stdio server for
|
|
54
|
-
* AI host applications, or be used programmatically in agent server code.
|
|
55
|
-
*
|
|
56
|
-
* @example Running as MCP server for Claude Desktop
|
|
57
|
-
* ```typescript
|
|
58
|
-
* const client = new MCPWebClient({
|
|
59
|
-
* serverUrl: 'http://localhost:3001',
|
|
60
|
-
* authToken: 'your-auth-token',
|
|
61
|
-
* });
|
|
62
|
-
* await client.run(); // Starts stdio transport
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* @example Programmatic usage in agent code
|
|
66
|
-
* ```typescript
|
|
67
|
-
* const client = new MCPWebClient({
|
|
68
|
-
* serverUrl: 'http://localhost:3001',
|
|
69
|
-
* authToken: 'your-auth-token',
|
|
70
|
-
* });
|
|
71
|
-
*
|
|
72
|
-
* // List available tools
|
|
73
|
-
* const { tools } = await client.listTools();
|
|
74
|
-
*
|
|
75
|
-
* // Call a tool
|
|
76
|
-
* const result = await client.callTool('get_todos');
|
|
77
|
-
* ```
|
|
78
|
-
*
|
|
79
|
-
* @example With query context (for agent servers)
|
|
80
|
-
* ```typescript
|
|
81
|
-
* const contextualClient = client.contextualize(query);
|
|
82
|
-
* const result = await contextualClient.callTool('update_todo', { id: '1' });
|
|
83
|
-
* await contextualClient.complete('Todo updated successfully');
|
|
84
|
-
* ```
|
|
85
|
-
*/
|
|
86
|
-
export class MCPWebClient {
|
|
87
|
-
#config: MCPWebClientConfigOutput;
|
|
88
|
-
#server?: Server;
|
|
89
|
-
#query?: Query;
|
|
90
|
-
#isDone = false; // Track if query has been completed
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Creates a new MCPWebClient instance.
|
|
94
|
-
*
|
|
95
|
-
* @param config - Client configuration with server URL and auth token
|
|
96
|
-
* @param query - Optional query for contextualized instances (internal use)
|
|
97
|
-
*/
|
|
98
|
-
constructor(config: MCPWebClientConfig, query?: Query) {
|
|
99
|
-
this.#config = MCPWebClientConfigSchema.parse(config);
|
|
100
|
-
|
|
101
|
-
if (query) {
|
|
102
|
-
this.#query = QuerySchema.parse(query);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private getMetaParams(sessionId?: string): McpRequestMetaParams | undefined {
|
|
107
|
-
if (sessionId || this.#query?.uuid) {
|
|
108
|
-
const meta: McpRequestMetaParams = {};
|
|
109
|
-
if (sessionId) {
|
|
110
|
-
meta.sessionId = sessionId;
|
|
111
|
-
}
|
|
112
|
-
if (this.#query?.uuid) {
|
|
113
|
-
meta.queryId = this.#query.uuid;
|
|
114
|
-
}
|
|
115
|
-
return meta;
|
|
116
|
-
}
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private getParams(sessionId?: string): { _meta: McpRequestMetaParams } | undefined {
|
|
121
|
-
const meta = this.getMetaParams(sessionId);
|
|
122
|
-
if (meta) {
|
|
123
|
-
return { _meta: meta };
|
|
124
|
-
}
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private async makeToolCallRequest(request: CallToolRequest): Promise<CallToolResult> {
|
|
129
|
-
try {
|
|
130
|
-
const { name, arguments: args, _meta } = request.params as any;
|
|
131
|
-
|
|
132
|
-
const response = await this.makeRequest('tools/call', {
|
|
133
|
-
name,
|
|
134
|
-
arguments: args || {},
|
|
135
|
-
...(_meta && { _meta })
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Check if response is already in CallToolResult format (from bridge)
|
|
139
|
-
// This happens when the bridge wraps results for Remote MCP compatibility
|
|
140
|
-
if (
|
|
141
|
-
response &&
|
|
142
|
-
typeof response === 'object' &&
|
|
143
|
-
'content' in response &&
|
|
144
|
-
Array.isArray((response as { content: unknown }).content)
|
|
145
|
-
) {
|
|
146
|
-
return response as CallToolResult;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Handle different response formats (legacy/unwrapped responses)
|
|
150
|
-
// Check if this is an error response from bridge
|
|
151
|
-
if (response && typeof response === 'object' && 'error' in response) {
|
|
152
|
-
return {
|
|
153
|
-
content: [
|
|
154
|
-
{
|
|
155
|
-
type: 'text',
|
|
156
|
-
text: JSON.stringify(response, null, 2),
|
|
157
|
-
}
|
|
158
|
-
],
|
|
159
|
-
isError: true
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Handle successful responses
|
|
164
|
-
// The response could be:
|
|
165
|
-
// 1. A wrapped response: { data: <actual data> }
|
|
166
|
-
// 2. Direct tool result: any type (string, number, object, etc.)
|
|
167
|
-
let content: Content[];
|
|
168
|
-
let topLevelMeta: Record<string, unknown> | undefined;
|
|
169
|
-
let actualData = (response && typeof response === 'object' && 'data' in response)
|
|
170
|
-
? response.data
|
|
171
|
-
: response;
|
|
172
|
-
|
|
173
|
-
// Extract _meta from the data to place at the top level of CallToolResult.
|
|
174
|
-
// The MCP protocol expects _meta as a top-level field on the result object,
|
|
175
|
-
// not embedded inside the JSON text content.
|
|
176
|
-
if (actualData && typeof actualData === 'object' && '_meta' in actualData) {
|
|
177
|
-
const { _meta: extractedMeta, ...rest } = actualData as Record<string, unknown>;
|
|
178
|
-
if (extractedMeta && typeof extractedMeta === 'object') {
|
|
179
|
-
topLevelMeta = extractedMeta as Record<string, unknown>;
|
|
180
|
-
}
|
|
181
|
-
actualData = rest;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (typeof actualData === 'string') {
|
|
185
|
-
// Check if it's a data URL (image)
|
|
186
|
-
if (actualData.startsWith('data:image/')) {
|
|
187
|
-
content = [
|
|
188
|
-
{
|
|
189
|
-
type: 'image',
|
|
190
|
-
data: actualData.split(',')[1],
|
|
191
|
-
mimeType: actualData.split(';')[0].split(':')[1],
|
|
192
|
-
},
|
|
193
|
-
];
|
|
194
|
-
} else {
|
|
195
|
-
content = [
|
|
196
|
-
{
|
|
197
|
-
type: 'text',
|
|
198
|
-
text: actualData
|
|
199
|
-
}
|
|
200
|
-
];
|
|
201
|
-
}
|
|
202
|
-
} else if (actualData !== null && actualData !== undefined) {
|
|
203
|
-
content = [
|
|
204
|
-
{
|
|
205
|
-
type: 'text',
|
|
206
|
-
text: typeof actualData === 'object' ? JSON.stringify(actualData, null, 2) : String(actualData)
|
|
207
|
-
}
|
|
208
|
-
];
|
|
209
|
-
} else {
|
|
210
|
-
// null or undefined result
|
|
211
|
-
content = [
|
|
212
|
-
{
|
|
213
|
-
type: 'text',
|
|
214
|
-
text: ''
|
|
215
|
-
}
|
|
216
|
-
];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const callToolResult: CallToolResult = { content };
|
|
220
|
-
if (topLevelMeta) {
|
|
221
|
-
callToolResult._meta = topLevelMeta;
|
|
222
|
-
}
|
|
223
|
-
return callToolResult;
|
|
224
|
-
|
|
225
|
-
} catch (error) {
|
|
226
|
-
// Re-throw authentication and query errors
|
|
227
|
-
if (error instanceof Error) {
|
|
228
|
-
const errorMessage = error.message;
|
|
229
|
-
if (errorMessage === MissingAuthenticationErrorCode ||
|
|
230
|
-
errorMessage === InvalidAuthenticationErrorCode ||
|
|
231
|
-
errorMessage === QueryNotFoundErrorCode ||
|
|
232
|
-
errorMessage === QueryNotActiveErrorCode) {
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// All other errors get returned as CallToolResult with isError: true
|
|
238
|
-
return {
|
|
239
|
-
content: [
|
|
240
|
-
{
|
|
241
|
-
type: 'text',
|
|
242
|
-
text: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`
|
|
243
|
-
}
|
|
244
|
-
],
|
|
245
|
-
isError: true
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
private async makeListToolsRequest(sessionId?: string): Promise<ListToolsResult | ErroredListToolsResult> {
|
|
251
|
-
const response = await this.makeRequest<ListToolsResult | ErroredListToolsResult | FatalError>('tools/list', this.getParams(sessionId));
|
|
252
|
-
|
|
253
|
-
if (isFatalError(response)) {
|
|
254
|
-
throw new Error(response.error_message);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return response;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private async makeListResourcesRequest(sessionId?: string): Promise<ListResourcesResult | ErroredListResourcesResult> {
|
|
261
|
-
const response = await this.makeRequest<ListResourcesResult | ErroredListResourcesResult | FatalError>('resources/list', this.getParams(sessionId));
|
|
262
|
-
|
|
263
|
-
if (isFatalError(response)) {
|
|
264
|
-
throw new Error(response.error_message);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return response;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private async makeListPromptsRequest(sessionId?: string): Promise<ListPromptsResult | ErroredListPromptsResult> {
|
|
271
|
-
const response = await this.makeRequest<ListPromptsResult | ErroredListPromptsResult | FatalError>('prompts/list', this.getParams(sessionId));
|
|
272
|
-
|
|
273
|
-
if (isFatalError(response)) {
|
|
274
|
-
throw new Error(response.error_message);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return response;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private async makeReadResourceRequest(request: ReadResourceRequest): Promise<ReadResourceResult> {
|
|
281
|
-
const { uri, _meta } = request.params;
|
|
282
|
-
|
|
283
|
-
const response = await this.makeRequest<ReadResourceResult | FatalError>('resources/read', {
|
|
284
|
-
uri,
|
|
285
|
-
...(_meta && { _meta }),
|
|
286
|
-
...this.getParams(),
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
if (isFatalError(response)) {
|
|
290
|
-
throw new Error(response.error_message);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return response;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private setupHandlers() {
|
|
297
|
-
if (!this.#server) return;
|
|
298
|
-
|
|
299
|
-
// Handle tool listing
|
|
300
|
-
this.#server.setRequestHandler(ListToolsRequestSchema, () => this.makeListToolsRequest());
|
|
301
|
-
|
|
302
|
-
// Handle tool calls
|
|
303
|
-
this.#server.setRequestHandler(CallToolRequestSchema, this.makeToolCallRequest.bind(this));
|
|
304
|
-
|
|
305
|
-
// Handle resource listing
|
|
306
|
-
this.#server.setRequestHandler(ListResourcesRequestSchema, () => this.makeListResourcesRequest());
|
|
307
|
-
|
|
308
|
-
// Handle resource reading
|
|
309
|
-
this.#server.setRequestHandler(ReadResourceRequestSchema, (request: ReadResourceRequest) => this.makeReadResourceRequest(request));
|
|
310
|
-
|
|
311
|
-
// Handle prompt listing
|
|
312
|
-
this.#server.setRequestHandler(ListPromptsRequestSchema, () => this.makeListPromptsRequest());
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Creates a contextualized client for a specific query.
|
|
317
|
-
*
|
|
318
|
-
* All tool calls made through the returned client will be tagged with the
|
|
319
|
-
* query UUID, enabling the bridge to track tool calls for that query.
|
|
320
|
-
*
|
|
321
|
-
* @param query - The query object containing uuid and optional responseTool
|
|
322
|
-
* @returns A new MCPWebClient instance bound to the query context
|
|
323
|
-
*
|
|
324
|
-
* @example
|
|
325
|
-
* ```typescript
|
|
326
|
-
* const contextualClient = client.contextualize(query);
|
|
327
|
-
* await contextualClient.callTool('analyze_data');
|
|
328
|
-
* await contextualClient.complete('Analysis complete');
|
|
329
|
-
* ```
|
|
330
|
-
*/
|
|
331
|
-
contextualize(query: Query): MCPWebClient {
|
|
332
|
-
return new MCPWebClient(this.#config, query);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Calls a tool on the connected frontend.
|
|
337
|
-
*
|
|
338
|
-
* Automatically includes query context if this is a contextualized client.
|
|
339
|
-
* If the query has tool restrictions, only allowed tools can be called.
|
|
340
|
-
*
|
|
341
|
-
* @param name - Name of the tool to call
|
|
342
|
-
* @param args - Optional arguments to pass to the tool
|
|
343
|
-
* @param sessionId - Optional session ID for multi-session scenarios
|
|
344
|
-
* @returns Tool execution result
|
|
345
|
-
* @throws {Error} If query is already done or tool is not allowed
|
|
346
|
-
*
|
|
347
|
-
* @example
|
|
348
|
-
* ```typescript
|
|
349
|
-
* const result = await client.callTool('create_todo', {
|
|
350
|
-
* title: 'New task',
|
|
351
|
-
* priority: 'high',
|
|
352
|
-
* });
|
|
353
|
-
* ```
|
|
354
|
-
*/
|
|
355
|
-
async callTool(name: string, args?: Record<string, unknown>, sessionId?: string): Promise<CallToolResult> {
|
|
356
|
-
if (this.#query && this.#isDone) {
|
|
357
|
-
throw new Error(QueryDoneErrorCode);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Check tool restrictions if query has them
|
|
361
|
-
if (this.#query?.restrictTools && this.#query?.tools) {
|
|
362
|
-
const allowed = this.#query.tools.some(t => t.name === name);
|
|
363
|
-
if (!allowed) {
|
|
364
|
-
throw new Error(
|
|
365
|
-
`Tool '${name}' not allowed. Query restricted to: ${this.#query.tools.map(t => t.name).join(', ')}`
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const request: CallToolRequest = {
|
|
371
|
-
method: 'tools/call',
|
|
372
|
-
params: {
|
|
373
|
-
name,
|
|
374
|
-
arguments: args || {} as Record<string, unknown>,
|
|
375
|
-
// Augment with query context if this is a contextualized instance
|
|
376
|
-
...this.getParams(sessionId)
|
|
377
|
-
},
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const response = await this.makeToolCallRequest(request);
|
|
381
|
-
|
|
382
|
-
// Auto-complete if this was the responseTool and it succeeded
|
|
383
|
-
// Note: response.isError is true for errors, undefined for success
|
|
384
|
-
if (this.#query?.responseTool?.name === name && response.isError !== true) {
|
|
385
|
-
this.#isDone = true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return response;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Lists all available tools from the connected frontend.
|
|
393
|
-
*
|
|
394
|
-
* If this is a contextualized client with restricted tools, returns only
|
|
395
|
-
* those tools. Otherwise fetches all tools from the bridge.
|
|
396
|
-
*
|
|
397
|
-
* @param sessionId - Optional session ID for multi-session scenarios
|
|
398
|
-
* @returns List of available tools
|
|
399
|
-
* @throws {Error} If query is already done
|
|
400
|
-
*/
|
|
401
|
-
async listTools(sessionId?: string): Promise<ListToolsResult | ErroredListToolsResult> {
|
|
402
|
-
if (this.#isDone) {
|
|
403
|
-
throw new Error(QueryDoneErrorCode);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// If we have tools from the query, return those
|
|
407
|
-
if (this.#query?.tools) {
|
|
408
|
-
// Need to convert ToolDefinition to Tool format expected by MCP
|
|
409
|
-
const tools = this.#query.tools.map(t => ({
|
|
410
|
-
name: t.name,
|
|
411
|
-
description: t.description,
|
|
412
|
-
inputSchema: t.inputSchema || { type: 'object', properties: {}, required: [] }
|
|
413
|
-
}));
|
|
414
|
-
return { tools: tools as Tool[] };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Otherwise use the shared request handler
|
|
418
|
-
return this.makeListToolsRequest(sessionId);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Lists all available resources from the connected frontend.
|
|
423
|
-
*
|
|
424
|
-
* @param sessionId - Optional session ID for multi-session scenarios
|
|
425
|
-
* @returns List of available resources
|
|
426
|
-
* @throws {Error} If query is already done
|
|
427
|
-
*/
|
|
428
|
-
async listResources(sessionId?: string): Promise<ListResourcesResult | ErroredListResourcesResult> {
|
|
429
|
-
if (this.#isDone) {
|
|
430
|
-
throw new Error(QueryDoneErrorCode);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return this.makeListResourcesRequest(sessionId);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Lists all available prompts from the connected frontend.
|
|
438
|
-
*
|
|
439
|
-
* @param sessionId - Optional session ID for multi-session scenarios
|
|
440
|
-
* @returns List of available prompts
|
|
441
|
-
* @throws {Error} If query is already done
|
|
442
|
-
*/
|
|
443
|
-
async listPrompts(sessionId?: string): Promise<ListPromptsResult | ErroredListPromptsResult> {
|
|
444
|
-
if (this.#isDone) {
|
|
445
|
-
throw new Error(QueryDoneErrorCode);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return this.makeListPromptsRequest(sessionId);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Sends a progress update for the current query.
|
|
453
|
-
*
|
|
454
|
-
* Use this to provide intermediate updates during long-running operations.
|
|
455
|
-
* Can only be called on a contextualized client instance.
|
|
456
|
-
*
|
|
457
|
-
* @param message - Progress message to send to the frontend
|
|
458
|
-
* @throws {Error} If not a contextualized client or query is done
|
|
459
|
-
*
|
|
460
|
-
* @example
|
|
461
|
-
* ```typescript
|
|
462
|
-
* await contextualClient.sendProgress('Processing step 1 of 3...');
|
|
463
|
-
* // ... do work ...
|
|
464
|
-
* await contextualClient.sendProgress('Processing step 2 of 3...');
|
|
465
|
-
* ```
|
|
466
|
-
*/
|
|
467
|
-
async sendProgress(message: string): Promise<void> {
|
|
468
|
-
if (!this.#query) {
|
|
469
|
-
throw new Error(ClientNotConextualizedErrorCode);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (this.#isDone) {
|
|
473
|
-
throw new Error(QueryDoneErrorCode);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
477
|
-
const progressUrl = `${url}/query/${this.#query.uuid}/progress`;
|
|
478
|
-
const response = await fetch(progressUrl, {
|
|
479
|
-
method: 'POST',
|
|
480
|
-
headers: {
|
|
481
|
-
'Content-Type': 'application/json',
|
|
482
|
-
},
|
|
483
|
-
body: JSON.stringify({ message })
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
if (!response.ok) {
|
|
487
|
-
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
488
|
-
throw new Error(errorData.error || `Failed to send progress: HTTP ${response.status}`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* Marks the current query as complete with a message.
|
|
494
|
-
*
|
|
495
|
-
* Can only be called on a contextualized client instance.
|
|
496
|
-
* If the query specified a responseTool, call that tool instead - calling
|
|
497
|
-
* this method will result in an error.
|
|
498
|
-
*
|
|
499
|
-
* @param message - Completion message to send to the frontend
|
|
500
|
-
* @throws {Error} If not a contextualized client, query is done, or responseTool was specified
|
|
501
|
-
*
|
|
502
|
-
* @example
|
|
503
|
-
* ```typescript
|
|
504
|
-
* await contextualClient.complete('Analysis complete: found 5 issues');
|
|
505
|
-
* ```
|
|
506
|
-
*/
|
|
507
|
-
async complete(message: string): Promise<void> {
|
|
508
|
-
if (!this.#query) {
|
|
509
|
-
throw new Error(ClientNotConextualizedErrorCode);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (this.#isDone) {
|
|
513
|
-
throw new Error(QueryDoneErrorCode);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
517
|
-
const completeUrl = `${url}/query/${this.#query.uuid}/complete`;
|
|
518
|
-
|
|
519
|
-
try {
|
|
520
|
-
const response = await fetch(completeUrl, {
|
|
521
|
-
method: 'PUT',
|
|
522
|
-
headers: {
|
|
523
|
-
'Content-Type': 'application/json',
|
|
524
|
-
},
|
|
525
|
-
body: JSON.stringify({ message })
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
if (!response.ok) {
|
|
529
|
-
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
530
|
-
throw new Error(`Failed to complete query: ${errorData.error || response.statusText}`);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Only mark as completed after successful response
|
|
534
|
-
this.#isDone = true;
|
|
535
|
-
} catch (error) {
|
|
536
|
-
throw error;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Marks the current query as failed with an error message.
|
|
542
|
-
*
|
|
543
|
-
* Can only be called on a contextualized client instance.
|
|
544
|
-
* Use this when the query encounters an unrecoverable error.
|
|
545
|
-
*
|
|
546
|
-
* @param error - Error message or Error object describing the failure
|
|
547
|
-
* @throws {Error} If not a contextualized client or query is already done
|
|
548
|
-
*
|
|
549
|
-
* @example
|
|
550
|
-
* ```typescript
|
|
551
|
-
* try {
|
|
552
|
-
* await contextualClient.callTool('risky_operation');
|
|
553
|
-
* } catch (e) {
|
|
554
|
-
* await contextualClient.fail(e);
|
|
555
|
-
* }
|
|
556
|
-
* ```
|
|
557
|
-
*/
|
|
558
|
-
async fail(error: string | Error): Promise<void> {
|
|
559
|
-
if (!this.#query) {
|
|
560
|
-
throw new Error(ClientNotConextualizedErrorCode);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (this.#isDone) {
|
|
564
|
-
throw new Error(QueryDoneErrorCode);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const errorMessage = typeof error === 'string' ? error : error.message;
|
|
568
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
569
|
-
const failUrl = `${url}/query/${this.#query.uuid}/fail`;
|
|
570
|
-
|
|
571
|
-
try {
|
|
572
|
-
const response = await fetch(failUrl, {
|
|
573
|
-
method: 'PUT',
|
|
574
|
-
headers: {
|
|
575
|
-
'Content-Type': 'application/json',
|
|
576
|
-
},
|
|
577
|
-
body: JSON.stringify({ error: errorMessage })
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
if (!response.ok) {
|
|
581
|
-
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
582
|
-
throw new Error(`Failed to mark query as failed: ${errorData.error || response.statusText}`);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Mark as completed to prevent further operations
|
|
586
|
-
this.#isDone = true;
|
|
587
|
-
} catch (err) {
|
|
588
|
-
throw err;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Cancels the current query.
|
|
594
|
-
*
|
|
595
|
-
* Can only be called on a contextualized client instance.
|
|
596
|
-
* Use this when the user or system needs to abort query processing.
|
|
597
|
-
*
|
|
598
|
-
* @param reason - Optional reason for the cancellation
|
|
599
|
-
* @throws {Error} If not a contextualized client or query is already done
|
|
600
|
-
*
|
|
601
|
-
* @example
|
|
602
|
-
* ```typescript
|
|
603
|
-
* // User requested cancellation
|
|
604
|
-
* await contextualClient.cancel('User cancelled operation');
|
|
605
|
-
* ```
|
|
606
|
-
*/
|
|
607
|
-
async cancel(reason?: string): Promise<void> {
|
|
608
|
-
if (!this.#query) {
|
|
609
|
-
throw new Error(ClientNotConextualizedErrorCode);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (this.#isDone) {
|
|
613
|
-
throw new Error(QueryDoneErrorCode);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
617
|
-
const cancelUrl = `${url}/query/${this.#query.uuid}/cancel`;
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
const response = await fetch(cancelUrl, {
|
|
621
|
-
method: 'PUT',
|
|
622
|
-
headers: {
|
|
623
|
-
'Content-Type': 'application/json',
|
|
624
|
-
},
|
|
625
|
-
body: JSON.stringify(reason ? { reason } : {})
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
if (!response.ok) {
|
|
629
|
-
const errorData = await response.json().catch(() => ({ error: response.statusText })) as { error?: string };
|
|
630
|
-
throw new Error(`Failed to cancel query: ${errorData.error || response.statusText}`);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Mark as completed to prevent further operations
|
|
634
|
-
this.#isDone = true;
|
|
635
|
-
} catch (err) {
|
|
636
|
-
throw err;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
private async makeRequest<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T> {
|
|
641
|
-
const url = this.#config.serverUrl.replace('ws:', 'http:').replace('wss:', 'https:');
|
|
642
|
-
|
|
643
|
-
const requestBody = JsonRpcRequestSchema.parse({
|
|
644
|
-
jsonrpc: '2.0',
|
|
645
|
-
id: Date.now(),
|
|
646
|
-
method,
|
|
647
|
-
params
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
try {
|
|
651
|
-
const controller = new AbortController();
|
|
652
|
-
const timeoutId = setTimeout(() => controller.abort(), this.#config.timeout);
|
|
653
|
-
|
|
654
|
-
// Only include Authorization header if we have an authToken
|
|
655
|
-
const headers: Record<string, string> = {
|
|
656
|
-
'Content-Type': 'application/json',
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
if (this.#config.authToken) {
|
|
660
|
-
headers.Authorization = `Bearer ${this.#config.authToken}`;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const response = await fetch(url, {
|
|
664
|
-
method: 'POST',
|
|
665
|
-
headers,
|
|
666
|
-
body: JSON.stringify(requestBody),
|
|
667
|
-
signal: controller.signal
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
clearTimeout(timeoutId);
|
|
671
|
-
|
|
672
|
-
if (!response.ok) {
|
|
673
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const rawData = await response.json();
|
|
677
|
-
|
|
678
|
-
const data = JsonRpcResponseSchema.parse(rawData);
|
|
679
|
-
|
|
680
|
-
if (data.error) {
|
|
681
|
-
throw new Error(data.error.message);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
return data.result as T;
|
|
685
|
-
|
|
686
|
-
} catch (error: unknown) {
|
|
687
|
-
if (error instanceof Error) {
|
|
688
|
-
if (error.name === 'AbortError') {
|
|
689
|
-
throw new Error('Request timeout');
|
|
690
|
-
}
|
|
691
|
-
throw error;
|
|
692
|
-
}
|
|
693
|
-
throw new Error(`Unknown error: ${error}`);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Fetches server identity (name, version, icon) from the bridge.
|
|
699
|
-
* Falls back to defaults if the bridge is unreachable.
|
|
700
|
-
*/
|
|
701
|
-
private async fetchBridgeInfo(): Promise<{
|
|
702
|
-
name: string;
|
|
703
|
-
version: string;
|
|
704
|
-
icon?: string;
|
|
705
|
-
}> {
|
|
706
|
-
const defaults = { name: '@mcp-web/client', version: '1.0.0' };
|
|
707
|
-
try {
|
|
708
|
-
const url = this.#config.serverUrl
|
|
709
|
-
.replace('ws:', 'http:')
|
|
710
|
-
.replace('wss:', 'https:');
|
|
711
|
-
|
|
712
|
-
const controller = new AbortController();
|
|
713
|
-
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
714
|
-
|
|
715
|
-
const response = await fetch(url, {
|
|
716
|
-
method: 'GET',
|
|
717
|
-
signal: controller.signal,
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
clearTimeout(timeoutId);
|
|
721
|
-
|
|
722
|
-
if (!response.ok) return defaults;
|
|
723
|
-
|
|
724
|
-
const data = (await response.json()) as Record<string, unknown>;
|
|
725
|
-
return {
|
|
726
|
-
name: typeof data.name === 'string' ? data.name : defaults.name,
|
|
727
|
-
version:
|
|
728
|
-
typeof data.version === 'string'
|
|
729
|
-
? data.version
|
|
730
|
-
: defaults.version,
|
|
731
|
-
...(typeof data.icon === 'string' && { icon: data.icon }),
|
|
732
|
-
};
|
|
733
|
-
} catch {
|
|
734
|
-
return defaults;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* Starts the MCP server using stdio transport.
|
|
740
|
-
*
|
|
741
|
-
* This method is intended for running as a subprocess of an AI host like
|
|
742
|
-
* Claude Desktop. It connects to stdin/stdout for MCP communication.
|
|
743
|
-
*
|
|
744
|
-
* Cannot be called on contextualized client instances.
|
|
745
|
-
*
|
|
746
|
-
* @throws {Error} If called on a contextualized client or server not initialized
|
|
747
|
-
*
|
|
748
|
-
* @example
|
|
749
|
-
* ```typescript
|
|
750
|
-
* // In your entry point script
|
|
751
|
-
* const client = new MCPWebClient(config);
|
|
752
|
-
* await client.run();
|
|
753
|
-
* ```
|
|
754
|
-
*/
|
|
755
|
-
async run() {
|
|
756
|
-
if (this.#query) {
|
|
757
|
-
throw new Error('Cannot run a contextualized client instance. Only root clients can be run as MCP servers.');
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Fetch bridge identity before creating the MCP server
|
|
761
|
-
const bridgeInfo = await this.fetchBridgeInfo();
|
|
762
|
-
|
|
763
|
-
this.#server = new Server(
|
|
764
|
-
{
|
|
765
|
-
name: bridgeInfo.name,
|
|
766
|
-
version: bridgeInfo.version,
|
|
767
|
-
...(bridgeInfo.icon && { icon: bridgeInfo.icon }),
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
capabilities: {
|
|
771
|
-
tools: {},
|
|
772
|
-
resources: {},
|
|
773
|
-
prompts: {},
|
|
774
|
-
},
|
|
775
|
-
}
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
this.setupHandlers();
|
|
779
|
-
|
|
780
|
-
const transport = new StdioServerTransport();
|
|
781
|
-
await this.#server.connect(transport);
|
|
782
|
-
}
|
|
783
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { MCPWebClient } from './client.js';
|
|
4
|
-
import { MCPWebClientConfigSchema } from './schemas.js';
|
|
5
|
-
|
|
6
|
-
export type { Query } from '@mcp-web/types';
|
|
7
|
-
// Export for programmatic use
|
|
8
|
-
export { MCPWebClient } from './client.js';
|
|
9
|
-
export type * from './types.js';
|
|
10
|
-
|
|
11
|
-
// Only run as CLI if this is the main module in Node.js
|
|
12
|
-
// Guard against running in Deno or when bundled
|
|
13
|
-
// @ts-expect-error - Deno global exists in Deno runtime
|
|
14
|
-
const isDeno = typeof Deno !== 'undefined';
|
|
15
|
-
const isNodeCLI = !isDeno &&
|
|
16
|
-
typeof process !== 'undefined' &&
|
|
17
|
-
process.argv &&
|
|
18
|
-
import.meta.url === `file://${process.argv[1]}`;
|
|
19
|
-
|
|
20
|
-
if (isNodeCLI) {
|
|
21
|
-
// Handle graceful shutdown
|
|
22
|
-
process.on('SIGINT', () => {
|
|
23
|
-
console.error('Shutting down MCP Bridge Client...');
|
|
24
|
-
process.exit(0);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
process.on('SIGTERM', () => {
|
|
28
|
-
console.error('Shutting down MCP Bridge Client...');
|
|
29
|
-
process.exit(0);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const config = MCPWebClientConfigSchema.parse({
|
|
33
|
-
serverUrl: process.env.MCP_SERVER_URL,
|
|
34
|
-
authToken: process.env.AUTH_TOKEN,
|
|
35
|
-
...(process.env.TIMEOUT !== undefined ? { timeout: Number.parseInt(process.env.TIMEOUT, 10) } : {})
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Start the client
|
|
39
|
-
const client = new MCPWebClient(config);
|
|
40
|
-
client.run().catch((error) => {
|
|
41
|
-
console.error('Failed to start MCP Bridge Client:', error);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
});
|
|
44
|
-
}
|
package/src/schemas.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const JsonRpcResponseSchema = z.object({
|
|
4
|
-
jsonrpc: z.literal('2.0').default('2.0'),
|
|
5
|
-
id: z.union([z.string(), z.number()]),
|
|
6
|
-
result: z.any().optional(),
|
|
7
|
-
error: z.object({
|
|
8
|
-
code: z.number(),
|
|
9
|
-
message: z.string(),
|
|
10
|
-
data: z.any().optional()
|
|
11
|
-
}).optional()
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export const JsonRpcRequestSchema = z.object({
|
|
15
|
-
jsonrpc: z.literal('2.0').default('2.0'),
|
|
16
|
-
id: z.union([z.string(), z.number()]),
|
|
17
|
-
method: z.string(),
|
|
18
|
-
params: z.record(z.string(), z.unknown()).optional()
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
export const MCPWebClientConfigSchema = z.object({
|
|
22
|
-
serverUrl: z.url(),
|
|
23
|
-
authToken: z.string().min(1).optional(),
|
|
24
|
-
timeout: z.number().optional().default(30000)
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
export const TextContentSchema = z.object({
|
|
28
|
-
type: z.literal('text').default('text'),
|
|
29
|
-
text: z.string(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
export const ImageContentSchema = z.object({
|
|
33
|
-
type: z.literal('image').default('image'),
|
|
34
|
-
data: z.string(),
|
|
35
|
-
mimeType: z.string(),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
export const ContentSchema = z.union([TextContentSchema, ImageContentSchema]);
|
package/src/types.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { z } from "zod";
|
|
2
|
-
import type {
|
|
3
|
-
ContentSchema,
|
|
4
|
-
ImageContentSchema,
|
|
5
|
-
MCPWebClientConfigSchema,
|
|
6
|
-
TextContentSchema,
|
|
7
|
-
} from "./schemas.js";
|
|
8
|
-
|
|
9
|
-
export type MCPWebClientConfig = z.input<typeof MCPWebClientConfigSchema>;
|
|
10
|
-
export type MCPWebClientConfigOutput = z.infer<typeof MCPWebClientConfigSchema>;
|
|
11
|
-
|
|
12
|
-
export type TextContent = z.infer<typeof TextContentSchema>;
|
|
13
|
-
export type ImageContent = z.infer<typeof ImageContentSchema>;
|
|
14
|
-
export type Content = z.infer<typeof ContentSchema>;
|
package/src/utils.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export const camelToSnakeCase = (str: string) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
2
|
-
|
|
3
|
-
export const camelToSnakeCaseProps = (obj: Record<string, unknown>): Record<string, unknown> => {
|
|
4
|
-
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [camelToSnakeCase(key), value]))
|
|
5
|
-
};
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"module": "node16",
|
|
5
|
-
"moduleResolution": "node16",
|
|
6
|
-
"lib": [
|
|
7
|
-
"ES2022"
|
|
8
|
-
],
|
|
9
|
-
"outDir": "dist",
|
|
10
|
-
"rootDir": "src"
|
|
11
|
-
},
|
|
12
|
-
"include": [
|
|
13
|
-
"src/**/*"
|
|
14
|
-
],
|
|
15
|
-
"exclude": [
|
|
16
|
-
"node_modules",
|
|
17
|
-
"dist",
|
|
18
|
-
"**/*.test.ts"
|
|
19
|
-
]
|
|
20
|
-
}
|