@mcp-web/client 0.1.0
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/LICENSE +201 -0
- package/README.md +146 -0
- package/dist/client.d.ts +216 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +663 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas.d.ts +40 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +33 -0
- package/dist/schemas.js.map +1 -0
- package/dist/standalone.js +27226 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +5 -0
- package/dist/utils.js.map +1 -0
- package/esbuild.standalone.mjs +17 -0
- package/package.json +29 -0
- package/src/client.test.ts +259 -0
- package/src/client.ts +783 -0
- package/src/index.ts +44 -0
- package/src/schemas.ts +38 -0
- package/src/types.ts +14 -0
- package/src/utils.ts +5 -0
- package/tsconfig.json +20 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { ContentSchema, ImageContentSchema, MCPWebClientConfigSchema, TextContentSchema } from "./schemas.js";
|
|
3
|
+
export type MCPWebClientConfig = z.input<typeof MCPWebClientConfigSchema>;
|
|
4
|
+
export type MCPWebClientConfigOutput = z.infer<typeof MCPWebClientConfigSchema>;
|
|
5
|
+
export type TextContent = z.infer<typeof TextContentSchema>;
|
|
6
|
+
export type ImageContent = z.infer<typeof ImageContentSchema>;
|
|
7
|
+
export type Content = z.infer<typeof ContentSchema>;
|
|
8
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EACV,aAAa,EACb,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,KAAK,MAAM,WAAgE,CAAC;AAE7G,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAE1F,CAAC"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const camelToSnakeCase = (str) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
2
|
+
export const camelToSnakeCaseProps = (obj) => {
|
|
3
|
+
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [camelToSnakeCase(key), value]));
|
|
4
|
+
};
|
|
5
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;AAE7G,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAA4B,EAA2B,EAAE;IAC7F,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;AACtG,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
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/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-web/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"@mcp-web/client": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
14
|
+
"zod": "~4.1.12",
|
|
15
|
+
"@mcp-web/types": "0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^25.0.9",
|
|
19
|
+
"esbuild": "^0.24.2",
|
|
20
|
+
"typescript": "~5.9.3"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsc --watch",
|
|
24
|
+
"build": "tsc && node esbuild.standalone.mjs",
|
|
25
|
+
"build:standalone": "node esbuild.standalone.mjs",
|
|
26
|
+
"clean": "rm -rf dist",
|
|
27
|
+
"start": "node dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
});
|