mcp-gtw-provider 0.0.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paulo Coutinho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ <p align="center">
2
+ <img src="extras/images/logo-icon.png" width="150" alt="mcp-gtw-provider" />
3
+ </p>
4
+
5
+ <h1 align="center">mcp-gtw-provider</h1>
6
+
7
+ <p align="center">
8
+ The framework-agnostic JavaScript client for mcp-gateway โ€” a small, dependency-free ES module that
9
+ publishes and runs your MCP tools, resources, prompts and more from any web app over WebSocket.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://github.com/mcp-gtw/mcp-gtw-provider/actions/workflows/ci.yml"><img src="https://github.com/mcp-gtw/mcp-gtw-provider/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
14
+ <a href="https://www.npmjs.com/package/mcp-gtw-provider"><img src="https://img.shields.io/npm/v/mcp-gtw-provider.svg" alt="npm"></a>
15
+ <a href="LICENSE.md"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
16
+ </p>
17
+
18
+ ---
19
+
20
+ There is no dependency on React, Vue, or any DOM framework โ€” it uses only standard web platform APIs
21
+ (`WebSocket`, `AbortController`, timers). Import it into a plain HTML page, or wire it into React,
22
+ Vue, Svelte, or anything else.
23
+
24
+ ## ๐Ÿ”Œ How it fits
25
+
26
+ An MCP client talks to the gateway over Streamable HTTP. The gateway never knows the capabilities in
27
+ advance โ€” this provider connects from your app, registers them, and executes their handlers:
28
+
29
+ ```text
30
+ MCP client โ‡„ mcp-gateway (/mcp) โ‡„ gateway (/provider) โ‡„ McpGtwProvider (your app)
31
+ ```
32
+
33
+ ## ๐Ÿ“ฆ Install
34
+
35
+ ```bash
36
+ npm install mcp-gtw-provider
37
+ ```
38
+
39
+ No build step is required โ€” it ships as ES modules. You can also import the file directly in a page.
40
+
41
+ ## ๐Ÿš€ Quick start
42
+
43
+ ```javascript
44
+ import { McpGtwProvider } from "mcp-gtw-provider";
45
+
46
+ const provider = new McpGtwProvider({
47
+ url: "wss://your-gateway.example.com/provider?token=...",
48
+ onStatusChange: (status) => console.log("gateway:", status),
49
+ });
50
+
51
+ provider.registerTool(
52
+ {
53
+ name: "add",
54
+ description: "Add two numbers",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: { a: { type: "number" }, b: { type: "number" } },
58
+ required: ["a", "b"],
59
+ additionalProperties: false,
60
+ },
61
+ },
62
+ async ({ a, b }) => a + b,
63
+ );
64
+
65
+ await provider.connect();
66
+ ```
67
+
68
+ The handler's return value is wrapped into an MCP result automatically: a string becomes text, an
69
+ object becomes text plus `structuredContent`, and returning a full `{ content: [...] }` object gives
70
+ you exact control. Throwing turns into an error result.
71
+
72
+ ## ๐Ÿ“š Documentation
73
+
74
+ | Guide | What it covers |
75
+ | --- | --- |
76
+ | [Usage](docs/usage.md) | Every constructor option, tools, resources, prompts, completion, logging, progress, sampling, elicitation, lifecycle. |
77
+ | [Protocol](docs/protocol.md) | The private WebSocket frames this module speaks. |
78
+ | [Frameworks](docs/frameworks.md) | Using it from vanilla JS, React, and Vue. |
79
+
80
+ ## โœ… Requirements
81
+
82
+ - A browser, or any runtime with `WebSocket`, `AbortController`, and timers.
83
+ - Node 20+ only to develop this package โ€” tested on 20, 22 and 24 in CI.
84
+ - A running [`mcp-gateway`](https://github.com/mcp-gtw/mcp-gateway) to connect to.
85
+
86
+ ## ๐Ÿ’œ Support
87
+
88
+ If this project saved you time, consider supporting it:
89
+ [GitHub Sponsors](https://github.com/sponsors/paulocoutinhox) ยท [Ko-fi](https://ko-fi.com/paulocoutinho).
90
+
91
+ Made with care by [Paulo Coutinho](https://github.com/paulocoutinhox).
92
+
93
+ Licensed under [MIT](LICENSE.md).
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "mcp-gtw-provider",
3
+ "version": "0.0.1",
4
+ "description": "Framework-agnostic browser provider for the mcp-gateway: publish and run your MCP tools from any web app over WebSocket.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "gateway",
11
+ "websocket",
12
+ "browser",
13
+ "tools",
14
+ "provider"
15
+ ],
16
+ "homepage": "https://github.com/mcp-gtw/mcp-gtw-provider",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/mcp-gtw/mcp-gtw-provider.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/mcp-gtw/mcp-gtw-provider/issues"
23
+ },
24
+ "sideEffects": false,
25
+ "exports": {
26
+ ".": {
27
+ "types": "./types/index.d.ts",
28
+ "default": "./src/index.js"
29
+ }
30
+ },
31
+ "main": "./src/index.js",
32
+ "module": "./src/index.js",
33
+ "types": "./types/index.d.ts",
34
+ "files": [
35
+ "src",
36
+ "types",
37
+ "README.md",
38
+ "LICENSE.md"
39
+ ],
40
+ "scripts": {
41
+ "lint": "biome check .",
42
+ "format": "biome check --write .",
43
+ "test": "vitest run",
44
+ "coverage": "vitest run --coverage"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "devDependencies": {
50
+ "@biomejs/biome": "^2.3.0",
51
+ "@vitest/coverage-v8": "^3.2.0",
52
+ "vitest": "^3.2.0"
53
+ }
54
+ }
package/src/index.js ADDED
@@ -0,0 +1,507 @@
1
+ const EMPTY_INPUT_SCHEMA = { type: "object", properties: {}, additionalProperties: false };
2
+
3
+ export class McpGtwProvider {
4
+ constructor({
5
+ url,
6
+ reconnect = true,
7
+ reconnectMinDelayMs = 500,
8
+ reconnectMaxDelayMs = 10_000,
9
+ heartbeatIntervalMs = 20_000,
10
+ onStatusChange = null,
11
+ }) {
12
+ if (!url) {
13
+ throw new Error("A WebSocket URL is required");
14
+ }
15
+
16
+ this.url = url;
17
+ this.reconnect = reconnect;
18
+ this.reconnectMinDelayMs = reconnectMinDelayMs;
19
+ this.reconnectMaxDelayMs = reconnectMaxDelayMs;
20
+ this.heartbeatIntervalMs = heartbeatIntervalMs;
21
+ this.onStatusChange = onStatusChange;
22
+
23
+ this.socket = null;
24
+ this.tools = new Map();
25
+ this.resources = new Map();
26
+ this.resourceTemplates = new Map();
27
+ this.prompts = new Map();
28
+ this.onComplete = null;
29
+ this.onSubscribe = null;
30
+ this.onUnsubscribe = null;
31
+
32
+ this.runningCalls = new Map();
33
+ this.outgoingCalls = new Map();
34
+ this.outgoingId = 0;
35
+ this.reconnectAttempt = 0;
36
+ this.reconnectTimer = null;
37
+ this.heartbeatTimer = null;
38
+ this.connectPromise = null;
39
+ }
40
+
41
+ get connected() {
42
+ return this.socket?.readyState === WebSocket.OPEN;
43
+ }
44
+
45
+ registerTool(definition, handler) {
46
+ return this.#register(this.tools, "tools", definition, "name", handler, {
47
+ title: definition.title,
48
+ description: definition.description ?? "",
49
+ inputSchema: definition.inputSchema ?? EMPTY_INPUT_SCHEMA,
50
+ ...(definition.outputSchema ? { outputSchema: definition.outputSchema } : {}),
51
+ ...(definition.annotations ? { annotations: definition.annotations } : {}),
52
+ });
53
+ }
54
+
55
+ registerResource(definition, reader) {
56
+ return this.#register(this.resources, "resources", definition, "uri", reader, {
57
+ name: definition.name,
58
+ ...(definition.title ? { title: definition.title } : {}),
59
+ ...(definition.description ? { description: definition.description } : {}),
60
+ ...(definition.mimeType ? { mimeType: definition.mimeType } : {}),
61
+ });
62
+ }
63
+
64
+ registerResourceTemplate(definition) {
65
+ return this.#register(
66
+ this.resourceTemplates,
67
+ "resourceTemplates",
68
+ definition,
69
+ "uriTemplate",
70
+ null,
71
+ {
72
+ name: definition.name,
73
+ ...(definition.title ? { title: definition.title } : {}),
74
+ ...(definition.description ? { description: definition.description } : {}),
75
+ ...(definition.mimeType ? { mimeType: definition.mimeType } : {}),
76
+ },
77
+ );
78
+ }
79
+
80
+ registerPrompt(definition, handler) {
81
+ return this.#register(this.prompts, "prompts", definition, "name", handler, {
82
+ ...(definition.title ? { title: definition.title } : {}),
83
+ ...(definition.description ? { description: definition.description } : {}),
84
+ ...(definition.arguments ? { arguments: definition.arguments } : {}),
85
+ });
86
+ }
87
+
88
+ #register(registry, kind, definition, key, handler, extra) {
89
+ const identifier = definition[key];
90
+
91
+ if (!identifier || typeof identifier !== "string") {
92
+ throw new Error(`A ${kind} entry needs a string ${key}`);
93
+ }
94
+
95
+ registry.set(identifier, { definition: { [key]: identifier, ...extra }, handler });
96
+ this.#publish(kind);
97
+
98
+ return () => {
99
+ registry.delete(identifier);
100
+ this.#publish(kind);
101
+ };
102
+ }
103
+
104
+ async requestSampling(params) {
105
+ return this.#callClient("sampling/createMessage", params);
106
+ }
107
+
108
+ async requestElicit(message, requestedSchema) {
109
+ return this.#callClient("elicitation/create", { message, requestedSchema });
110
+ }
111
+
112
+ notifyResourceUpdated(uri) {
113
+ this.#notify("notifications/resources/updated", { uri });
114
+ }
115
+
116
+ log(level, data, logger) {
117
+ this.#notify("notifications/message", { level, data, logger });
118
+ }
119
+
120
+ async connect() {
121
+ if (this.connected) {
122
+ return;
123
+ }
124
+
125
+ if (this.connectPromise) {
126
+ return this.connectPromise;
127
+ }
128
+
129
+ this.connectPromise = this.#openSocket();
130
+
131
+ try {
132
+ await this.connectPromise;
133
+ } finally {
134
+ this.connectPromise = null;
135
+ }
136
+ }
137
+
138
+ disconnect() {
139
+ this.#clearReconnectTimer();
140
+ this.#stopHeartbeat();
141
+ this.#abortAllCalls("Provider disconnected");
142
+
143
+ if (this.socket) {
144
+ this.socket.close(1000, "Provider disconnected");
145
+ this.socket = null;
146
+ }
147
+ }
148
+
149
+ async #openSocket() {
150
+ const socket = new WebSocket(this.url);
151
+ this.socket = socket;
152
+
153
+ await new Promise((resolve, reject) => {
154
+ const handleOpen = () => {
155
+ cleanup();
156
+ resolve();
157
+ };
158
+
159
+ const handleError = () => {
160
+ cleanup();
161
+ reject(new Error("Could not connect to the MCP gateway"));
162
+ };
163
+
164
+ const handleClose = () => {
165
+ cleanup();
166
+ reject(new Error("The MCP gateway connection closed before it opened"));
167
+ };
168
+
169
+ const cleanup = () => {
170
+ socket.removeEventListener("open", handleOpen);
171
+ socket.removeEventListener("error", handleError);
172
+ socket.removeEventListener("close", handleClose);
173
+ };
174
+
175
+ socket.addEventListener("open", handleOpen, { once: true });
176
+ socket.addEventListener("error", handleError, { once: true });
177
+ socket.addEventListener("close", handleClose, { once: true });
178
+ });
179
+
180
+ if (socket !== this.socket) {
181
+ throw new Error("The MCP gateway connection was replaced before it opened");
182
+ }
183
+
184
+ this.reconnectAttempt = 0;
185
+ this.#installSocketListeners(socket);
186
+ this.#publishAll();
187
+ this.#startHeartbeat();
188
+ this.#emitStatus("connected");
189
+ }
190
+
191
+ #installSocketListeners(socket) {
192
+ socket.addEventListener("message", (event) => {
193
+ void this.#handleMessage(event).catch((error) => {
194
+ console.error("Failed to handle gateway message:", error);
195
+ });
196
+ });
197
+
198
+ socket.addEventListener("close", () => {
199
+ if (socket !== this.socket) {
200
+ return;
201
+ }
202
+
203
+ this.socket = null;
204
+ this.#stopHeartbeat();
205
+ this.#abortAllCalls("Gateway connection closed");
206
+ this.#emitStatus("disconnected");
207
+
208
+ if (this.reconnect) {
209
+ this.#scheduleReconnect();
210
+ }
211
+ });
212
+
213
+ socket.addEventListener("error", (error) => {
214
+ console.error("Gateway WebSocket error:", error);
215
+ });
216
+ }
217
+
218
+ async #handleMessage(event) {
219
+ const message = JSON.parse(event.data);
220
+
221
+ switch (message.type) {
222
+ case "hello.ack":
223
+ case "ack":
224
+ case "pong":
225
+ return;
226
+ case "request":
227
+ void this.#handleRequest(message);
228
+ return;
229
+ case "cancel": {
230
+ const controller = this.runningCalls.get(message.requestId);
231
+ controller?.abort(message.reason ?? "Cancelled by MCP gateway");
232
+ return;
233
+ }
234
+ case "response":
235
+ this.#resolveOutgoing(message);
236
+ return;
237
+ case "protocol.error":
238
+ console.error("MCP gateway protocol error:", message.message);
239
+ return;
240
+ default:
241
+ console.warn("Unknown MCP gateway message:", message);
242
+ }
243
+ }
244
+
245
+ async #handleRequest(message) {
246
+ const { requestId, method, params } = message;
247
+ const controller = new AbortController();
248
+ this.runningCalls.set(requestId, controller);
249
+
250
+ const context = {
251
+ signal: controller.signal,
252
+ requestId,
253
+ progress: (progress, total, msg) =>
254
+ this.#notify("notifications/progress", {
255
+ requestId,
256
+ progress,
257
+ total,
258
+ message: msg,
259
+ }),
260
+ requestSampling: (samplingParams) =>
261
+ this.#callClient("sampling/createMessage", samplingParams, requestId),
262
+ requestElicit: (message, requestedSchema) =>
263
+ this.#callClient("elicitation/create", { message, requestedSchema }, requestId),
264
+ };
265
+
266
+ try {
267
+ const result = await this.#dispatch(method, params, context);
268
+ this.#send({ type: "result", requestId, result });
269
+ } catch (error) {
270
+ const messageText = error instanceof Error ? error.message : String(error);
271
+
272
+ try {
273
+ this.#send({ type: "result", requestId, error: messageText });
274
+ } catch {
275
+ // The socket vanished mid-request; the result can no longer be delivered.
276
+ }
277
+ } finally {
278
+ this.runningCalls.delete(requestId);
279
+ }
280
+ }
281
+
282
+ async #dispatch(method, params, context) {
283
+ if (method === "tools/call") {
284
+ const entry = this.#require(this.tools, params.name, "tool");
285
+
286
+ const value = await entry.handler(params.arguments ?? {}, {
287
+ ...context,
288
+ toolName: params.name,
289
+ });
290
+
291
+ return normalizeToolResult(value);
292
+ }
293
+
294
+ if (method === "resources/read") {
295
+ const entry = this.#require(this.resources, params.uri, "resource");
296
+ const value = await entry.handler(params.uri, context);
297
+ return normalizeResourceResult(params.uri, value);
298
+ }
299
+
300
+ if (method === "prompts/get") {
301
+ const entry = this.#require(this.prompts, params.name, "prompt");
302
+ return entry.handler(params.arguments ?? {}, { ...context, promptName: params.name });
303
+ }
304
+
305
+ if (method === "completion/complete") {
306
+ if (!this.onComplete) {
307
+ return { values: [] };
308
+ }
309
+
310
+ return normalizeCompletion(
311
+ await this.onComplete(params.ref, params.argument, params.context),
312
+ );
313
+ }
314
+
315
+ if (method === "resources/subscribe") {
316
+ await this.onSubscribe?.(params.uri);
317
+ return {};
318
+ }
319
+
320
+ if (method === "resources/unsubscribe") {
321
+ await this.onUnsubscribe?.(params.uri);
322
+ return {};
323
+ }
324
+
325
+ throw new Error(`Unsupported method: ${method}`);
326
+ }
327
+
328
+ #require(registry, identifier, kind) {
329
+ const entry = registry.get(identifier);
330
+
331
+ if (!entry) {
332
+ throw new Error(`Unknown ${kind}: ${identifier}`);
333
+ }
334
+
335
+ return entry;
336
+ }
337
+
338
+ async #callClient(method, params, originatingRequestId = null) {
339
+ this.outgoingId += 1;
340
+ const requestId = `c${this.outgoingId}`;
341
+
342
+ const promise = new Promise((resolve, reject) => {
343
+ this.outgoingCalls.set(requestId, { resolve, reject });
344
+ });
345
+
346
+ const frame = { type: "call", requestId, method, params };
347
+
348
+ if (originatingRequestId !== null) {
349
+ frame.originatingRequestId = originatingRequestId;
350
+ }
351
+
352
+ try {
353
+ this.#send(frame);
354
+ } catch (error) {
355
+ this.outgoingCalls.delete(requestId);
356
+ throw error;
357
+ }
358
+
359
+ return promise;
360
+ }
361
+
362
+ #resolveOutgoing(message) {
363
+ const pending = this.outgoingCalls.get(message.requestId);
364
+
365
+ if (!pending) {
366
+ return;
367
+ }
368
+
369
+ this.outgoingCalls.delete(message.requestId);
370
+
371
+ if (message.error != null) {
372
+ pending.reject(new Error(message.error));
373
+ } else {
374
+ pending.resolve(message.result);
375
+ }
376
+ }
377
+
378
+ #publishAll() {
379
+ for (const kind of ["tools", "resources", "resourceTemplates", "prompts"]) {
380
+ if (this[kind].size > 0) {
381
+ this.#publish(kind);
382
+ }
383
+ }
384
+ }
385
+
386
+ #publish(kind) {
387
+ if (!this.connected) {
388
+ return;
389
+ }
390
+
391
+ const registry = this[kind];
392
+ const items = Array.from(registry.values(), (entry) => entry.definition);
393
+ this.#send({ type: "register", registry: kind, items });
394
+ }
395
+
396
+ #notify(method, params) {
397
+ if (this.connected) {
398
+ this.#send({ type: "notify", method, params });
399
+ }
400
+ }
401
+
402
+ #send(message) {
403
+ if (!this.connected) {
404
+ throw new Error("MCP gateway WebSocket is not connected");
405
+ }
406
+
407
+ this.socket.send(JSON.stringify(message));
408
+ }
409
+
410
+ #startHeartbeat() {
411
+ this.#stopHeartbeat();
412
+
413
+ this.heartbeatTimer = setInterval(() => {
414
+ if (this.connected) {
415
+ this.#send({ type: "ping" });
416
+ }
417
+ }, this.heartbeatIntervalMs);
418
+ }
419
+
420
+ #stopHeartbeat() {
421
+ if (this.heartbeatTimer !== null) {
422
+ clearInterval(this.heartbeatTimer);
423
+ this.heartbeatTimer = null;
424
+ }
425
+ }
426
+
427
+ #scheduleReconnect() {
428
+ this.#clearReconnectTimer();
429
+ const exponentialDelay = this.reconnectMinDelayMs * 2 ** this.reconnectAttempt;
430
+ const delay = Math.min(exponentialDelay, this.reconnectMaxDelayMs);
431
+ const jitteredDelay = Math.round(delay * (0.8 + Math.random() * 0.4));
432
+ this.reconnectAttempt += 1;
433
+
434
+ this.reconnectTimer = setTimeout(() => {
435
+ this.reconnectTimer = null;
436
+
437
+ void this.connect().catch((error) => {
438
+ console.error("MCP gateway reconnect failed:", error);
439
+ this.#scheduleReconnect();
440
+ });
441
+ }, jitteredDelay);
442
+ }
443
+
444
+ #clearReconnectTimer() {
445
+ if (this.reconnectTimer !== null) {
446
+ clearTimeout(this.reconnectTimer);
447
+ this.reconnectTimer = null;
448
+ }
449
+ }
450
+
451
+ #abortAllCalls(reason) {
452
+ for (const controller of this.runningCalls.values()) {
453
+ controller.abort(reason);
454
+ }
455
+
456
+ this.runningCalls.clear();
457
+
458
+ for (const pending of this.outgoingCalls.values()) {
459
+ pending.reject(new Error(reason));
460
+ }
461
+
462
+ this.outgoingCalls.clear();
463
+ }
464
+
465
+ #emitStatus(status) {
466
+ this.onStatusChange?.(status);
467
+ }
468
+ }
469
+
470
+ function normalizeToolResult(value) {
471
+ if (value && typeof value === "object" && Array.isArray(value.content)) {
472
+ return value;
473
+ }
474
+
475
+ const text = typeof value === "string" ? value : JSON.stringify(value ?? null);
476
+ const result = { content: [{ type: "text", text }], isError: false };
477
+
478
+ if (value && typeof value === "object" && !Array.isArray(value)) {
479
+ result.structuredContent = value;
480
+ }
481
+
482
+ return result;
483
+ }
484
+
485
+ function normalizeResourceResult(uri, value) {
486
+ if (value && typeof value === "object" && Array.isArray(value.contents)) {
487
+ return value;
488
+ }
489
+
490
+ if (Array.isArray(value)) {
491
+ return { contents: value };
492
+ }
493
+
494
+ if (typeof value === "string") {
495
+ return { contents: [{ uri, text: value }] };
496
+ }
497
+
498
+ return { contents: [{ uri, ...value }] };
499
+ }
500
+
501
+ function normalizeCompletion(value) {
502
+ if (Array.isArray(value)) {
503
+ return { values: value };
504
+ }
505
+
506
+ return value;
507
+ }
@@ -0,0 +1,264 @@
1
+ export type ProviderStatus = "connected" | "disconnected";
2
+
3
+ export interface McpGtwProviderOptions {
4
+ /** WebSocket URL of the gateway's private `/provider` endpoint, including the `token`. */
5
+ url: string;
6
+ /** Reconnect automatically after an unexpected close. Defaults to `true`. */
7
+ reconnect?: boolean;
8
+ /** Minimum backoff before the first reconnect attempt, in milliseconds. Defaults to `500`. */
9
+ reconnectMinDelayMs?: number;
10
+ /** Maximum backoff between reconnect attempts, in milliseconds. Defaults to `10000`. */
11
+ reconnectMaxDelayMs?: number;
12
+ /** Interval between `ping` heartbeats while connected, in milliseconds. Defaults to `20000`. */
13
+ heartbeatIntervalMs?: number;
14
+ /** Called on every connection state transition. */
15
+ onStatusChange?: ((status: ProviderStatus) => void) | null;
16
+ }
17
+
18
+ /** A JSON Schema object describing a tool's input or output. */
19
+ export type JsonSchema = Record<string, unknown>;
20
+
21
+ /** Removes a previously registered entry and republishes its list. */
22
+ export type Unregister = () => void;
23
+
24
+ /** Reports incremental progress for the running request. */
25
+ export type ProgressReporter = (progress: number, total?: number, message?: string) => void;
26
+
27
+ export interface RequestContext {
28
+ /** Aborts when the gateway cancels the request or the connection drops. */
29
+ signal: AbortSignal;
30
+ /** Correlation id of this request. */
31
+ requestId: string;
32
+ /** Emits a `notifications/progress` frame for this request. */
33
+ progress: ProgressReporter;
34
+ /** Asks the initiating client's LLM to sample a completion, routed back to that client. */
35
+ requestSampling(params: Record<string, unknown>): Promise<Record<string, unknown>>;
36
+ /** Asks the initiating client's user for structured input, routed back to that client. */
37
+ requestElicit(message: string, requestedSchema: JsonSchema): Promise<Record<string, unknown>>;
38
+ }
39
+
40
+ export interface ToolDefinition {
41
+ /** Unique tool name advertised to MCP clients. */
42
+ name: string;
43
+ /** Optional human-readable title. */
44
+ title?: string;
45
+ /** Human-readable description. Defaults to an empty string. */
46
+ description?: string;
47
+ /** JSON Schema for the arguments. Defaults to an empty object schema. */
48
+ inputSchema?: JsonSchema;
49
+ /** Optional JSON Schema for the structured result. */
50
+ outputSchema?: JsonSchema;
51
+ /** Optional MCP tool annotations. */
52
+ annotations?: Record<string, unknown>;
53
+ }
54
+
55
+ export interface ToolHandlerContext extends RequestContext {
56
+ /** Name of the tool being invoked. */
57
+ toolName: string;
58
+ }
59
+
60
+ /** A single MCP content block returned to the client. */
61
+ export interface ToolResultContent {
62
+ type: string;
63
+ [key: string]: unknown;
64
+ }
65
+
66
+ /** A fully formed MCP tool result. Return this to control the content blocks exactly. */
67
+ export interface ToolResult {
68
+ content: ToolResultContent[];
69
+ isError?: boolean;
70
+ structuredContent?: Record<string, unknown>;
71
+ }
72
+
73
+ /**
74
+ * A tool handler. Return a {@link ToolResult} to control the payload exactly, or return any
75
+ * JSON-serializable value and it is wrapped into a text (and structured, for objects) result.
76
+ */
77
+ export type ToolHandler = (
78
+ args: Record<string, unknown>,
79
+ context: ToolHandlerContext,
80
+ ) => ToolResult | unknown | Promise<ToolResult | unknown>;
81
+
82
+ export interface ResourceDefinition {
83
+ /** Concrete resource URI advertised to MCP clients. */
84
+ uri: string;
85
+ /** Human-readable name. */
86
+ name: string;
87
+ /** Optional human-readable title. */
88
+ title?: string;
89
+ /** Optional description. */
90
+ description?: string;
91
+ /** Optional MIME type of the resource contents. */
92
+ mimeType?: string;
93
+ }
94
+
95
+ /** A single MCP resource contents block. */
96
+ export interface ResourceContents {
97
+ uri?: string;
98
+ mimeType?: string;
99
+ text?: string;
100
+ blob?: string;
101
+ [key: string]: unknown;
102
+ }
103
+
104
+ /** A fully formed MCP resource read result. */
105
+ export interface ResourceReadResult {
106
+ contents: ResourceContents[];
107
+ }
108
+
109
+ /**
110
+ * A resource reader. Return a full {@link ResourceReadResult}, an array of contents, a string
111
+ * (wrapped as text), or a single contents object; all are normalized for the client.
112
+ */
113
+ export type ResourceReader = (
114
+ uri: string,
115
+ context: RequestContext,
116
+ ) =>
117
+ | ResourceReadResult
118
+ | ResourceContents[]
119
+ | ResourceContents
120
+ | string
121
+ | Promise<ResourceReadResult | ResourceContents[] | ResourceContents | string>;
122
+
123
+ export interface ResourceTemplateDefinition {
124
+ /** RFC 6570 URI template advertised to MCP clients. */
125
+ uriTemplate: string;
126
+ /** Human-readable name. */
127
+ name: string;
128
+ /** Optional human-readable title. */
129
+ title?: string;
130
+ /** Optional description. */
131
+ description?: string;
132
+ /** Optional MIME type of the resources the template produces. */
133
+ mimeType?: string;
134
+ }
135
+
136
+ export interface PromptArgument {
137
+ name: string;
138
+ description?: string;
139
+ required?: boolean;
140
+ [key: string]: unknown;
141
+ }
142
+
143
+ export interface PromptDefinition {
144
+ /** Unique prompt name advertised to MCP clients. */
145
+ name: string;
146
+ /** Optional human-readable title. */
147
+ title?: string;
148
+ /** Optional description. */
149
+ description?: string;
150
+ /** Optional declared arguments. */
151
+ arguments?: PromptArgument[];
152
+ }
153
+
154
+ export interface PromptHandlerContext extends RequestContext {
155
+ /** Name of the prompt being requested. */
156
+ promptName: string;
157
+ }
158
+
159
+ /** A fully formed MCP `prompts/get` result. */
160
+ export interface PromptResult {
161
+ description?: string;
162
+ messages: Array<Record<string, unknown>>;
163
+ }
164
+
165
+ export type PromptHandler = (
166
+ args: Record<string, unknown>,
167
+ context: PromptHandlerContext,
168
+ ) => PromptResult | Promise<PromptResult>;
169
+
170
+ /** A fully formed MCP completion result. */
171
+ export interface CompletionResult {
172
+ values: string[];
173
+ total?: number;
174
+ hasMore?: boolean;
175
+ }
176
+
177
+ /**
178
+ * Completion handler. Return a {@link CompletionResult} or a bare array of strings (wrapped as
179
+ * `{ values }`). Assign it to `onComplete`; when unset, completion returns no candidates.
180
+ */
181
+ export type CompletionHandler = (
182
+ ref: Record<string, unknown>,
183
+ argument: Record<string, unknown>,
184
+ context: Record<string, unknown> | undefined,
185
+ ) => CompletionResult | string[] | Promise<CompletionResult | string[]>;
186
+
187
+ /** Called when the client subscribes to (or unsubscribes from) a resource URI. */
188
+ export type SubscriptionHandler = (uri: string) => void | Promise<void>;
189
+
190
+ /** MCP logging levels, from least to most severe. */
191
+ export type LogLevel =
192
+ | "debug"
193
+ | "info"
194
+ | "notice"
195
+ | "warning"
196
+ | "error"
197
+ | "critical"
198
+ | "alert"
199
+ | "emergency";
200
+
201
+ /**
202
+ * Connects a web application to an MCP gateway over the private WebSocket, publishes its MCP
203
+ * capabilities, and runs their handlers. Framework agnostic: no React, Vue or DOM framework is
204
+ * required.
205
+ */
206
+ export class McpGtwProvider {
207
+ constructor(options: McpGtwProviderOptions);
208
+
209
+ readonly url: string;
210
+ reconnect: boolean;
211
+ reconnectMinDelayMs: number;
212
+ reconnectMaxDelayMs: number;
213
+ heartbeatIntervalMs: number;
214
+ onStatusChange: ((status: ProviderStatus) => void) | null;
215
+
216
+ /** Handles `completion/complete`. When `null`, completion returns no candidates. */
217
+ onComplete: CompletionHandler | null;
218
+ /** Called on `resources/subscribe`. Optional. */
219
+ onSubscribe: SubscriptionHandler | null;
220
+ /** Called on `resources/unsubscribe`. Optional. */
221
+ onUnsubscribe: SubscriptionHandler | null;
222
+
223
+ /** Whether the underlying socket is currently open. */
224
+ get connected(): boolean;
225
+
226
+ /** Registers (or replaces) a tool and republishes the list if connected. Returns an unregister fn. */
227
+ registerTool(definition: ToolDefinition, handler: ToolHandler): Unregister;
228
+
229
+ /** Registers (or replaces) a concrete resource and its reader. Returns an unregister fn. */
230
+ registerResource(definition: ResourceDefinition, reader: ResourceReader): Unregister;
231
+
232
+ /** Registers (or replaces) a resource template (listing only). Returns an unregister fn. */
233
+ registerResourceTemplate(definition: ResourceTemplateDefinition): Unregister;
234
+
235
+ /** Registers (or replaces) a prompt and its handler. Returns an unregister fn. */
236
+ registerPrompt(definition: PromptDefinition, handler: PromptHandler): Unregister;
237
+
238
+ /**
239
+ * Out-of-band sampling (`sampling/createMessage`), routed to the most recently active client.
240
+ * Inside a handler, prefer `context.requestSampling` so the call reaches the initiating client.
241
+ */
242
+ requestSampling(params: Record<string, unknown>): Promise<Record<string, unknown>>;
243
+
244
+ /**
245
+ * Out-of-band elicitation (`elicitation/create`), routed to the most recently active client.
246
+ * Inside a handler, prefer `context.requestElicit` so the call reaches the initiating client.
247
+ */
248
+ requestElicit(
249
+ message: string,
250
+ requestedSchema: JsonSchema,
251
+ ): Promise<Record<string, unknown>>;
252
+
253
+ /** Notifies subscribed clients that a resource changed (`notifications/resources/updated`). */
254
+ notifyResourceUpdated(uri: string): void;
255
+
256
+ /** Sends a log message to the client (`notifications/message`). */
257
+ log(level: LogLevel, data: unknown, logger?: string): void;
258
+
259
+ /** Opens the WebSocket and publishes the current registries. Idempotent while connected. */
260
+ connect(): Promise<void>;
261
+
262
+ /** Closes the socket, stops reconnecting, and aborts every in-flight call. */
263
+ disconnect(): void;
264
+ }