@mcp-ts/sdk 1.0.0 → 1.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/README.md +25 -13
- package/dist/adapters/agui-adapter.d.mts +21 -44
- package/dist/adapters/agui-adapter.d.ts +21 -44
- package/dist/adapters/agui-adapter.js +93 -67
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +93 -68
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +32 -134
- package/dist/adapters/agui-middleware.d.ts +32 -134
- package/dist/adapters/agui-middleware.js +314 -350
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs +314 -351
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +2 -2
- package/dist/adapters/ai-adapter.d.ts +2 -2
- package/dist/adapters/langchain-adapter.d.mts +2 -2
- package/dist/adapters/langchain-adapter.d.ts +2 -2
- package/dist/adapters/mastra-adapter.d.mts +2 -2
- package/dist/adapters/mastra-adapter.d.ts +2 -2
- package/dist/client/index.d.mts +184 -57
- package/dist/client/index.d.ts +184 -57
- package/dist/client/index.js +535 -130
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +535 -131
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +40 -6
- package/dist/client/react.d.ts +40 -6
- package/dist/client/react.js +587 -142
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +586 -143
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +5 -5
- package/dist/client/vue.d.ts +5 -5
- package/dist/client/vue.js +545 -140
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +545 -141
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{events-BP6WyRNh.d.mts → events-BgeztGYZ.d.mts} +12 -1
- package/dist/{events-BP6WyRNh.d.ts → events-BgeztGYZ.d.ts} +12 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +779 -248
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +775 -245
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-DMF3ED2O.d.mts → multi-session-client-CxogNckF.d.mts} +1 -1
- package/dist/{multi-session-client-BOFgPypS.d.ts → multi-session-client-cox_WXUj.d.ts} +1 -1
- package/dist/server/index.d.mts +44 -40
- package/dist/server/index.d.ts +44 -40
- package/dist/server/index.js +242 -116
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +238 -112
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{types-SbDlA2VX.d.mts → types-CLccx9wW.d.mts} +1 -1
- package/dist/{types-SbDlA2VX.d.ts → types-CLccx9wW.d.ts} +1 -1
- package/package.json +8 -1
- package/src/adapters/agui-adapter.ts +121 -107
- package/src/adapters/agui-middleware.ts +474 -512
- package/src/client/core/app-host.ts +417 -0
- package/src/client/core/sse-client.ts +365 -212
- package/src/client/core/types.ts +31 -0
- package/src/client/index.ts +1 -0
- package/src/client/react/index.ts +1 -0
- package/src/client/react/use-mcp-app.ts +73 -0
- package/src/client/react/useMcp.ts +18 -0
- package/src/server/handlers/nextjs-handler.ts +8 -7
- package/src/server/handlers/sse-handler.ts +131 -164
- package/src/server/mcp/oauth-client.ts +32 -2
- package/src/server/storage/index.ts +17 -1
- package/src/server/storage/sqlite-backend.ts +185 -0
- package/src/server/storage/types.ts +1 -1
- package/src/shared/events.ts +12 -0
- package/src/shared/types.ts +4 -2
package/dist/client/react.js
CHANGED
|
@@ -2,24 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
var nanoid = require('nanoid');
|
|
5
|
+
var appBridge = require('@modelcontextprotocol/ext-apps/app-bridge');
|
|
5
6
|
|
|
6
7
|
var __defProp = Object.defineProperty;
|
|
7
8
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
9
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
10
|
+
var DEFAULT_REQUEST_TIMEOUT = 6e4;
|
|
11
|
+
var MAX_RECONNECT_ATTEMPTS = 5;
|
|
12
|
+
var BASE_RECONNECT_DELAY = 1e3;
|
|
9
13
|
var SSEClient = class {
|
|
10
14
|
constructor(options) {
|
|
11
15
|
this.options = options;
|
|
12
16
|
__publicField(this, "eventSource", null);
|
|
13
17
|
__publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
|
|
18
|
+
__publicField(this, "resourceCache", /* @__PURE__ */ new Map());
|
|
14
19
|
__publicField(this, "reconnectAttempts", 0);
|
|
15
|
-
__publicField(this, "maxReconnectAttempts", 5);
|
|
16
|
-
__publicField(this, "reconnectDelay", 1e3);
|
|
17
20
|
__publicField(this, "isManuallyDisconnected", false);
|
|
18
21
|
__publicField(this, "connectionPromise", null);
|
|
19
22
|
__publicField(this, "connectionResolver", null);
|
|
20
23
|
}
|
|
24
|
+
// ============================================
|
|
25
|
+
// Connection Management
|
|
26
|
+
// ============================================
|
|
21
27
|
/**
|
|
22
|
-
* Connect to SSE endpoint
|
|
28
|
+
* Connect to the SSE endpoint
|
|
23
29
|
*/
|
|
24
30
|
connect() {
|
|
25
31
|
if (this.eventSource) {
|
|
@@ -30,52 +36,12 @@ var SSEClient = class {
|
|
|
30
36
|
this.connectionPromise = new Promise((resolve) => {
|
|
31
37
|
this.connectionResolver = resolve;
|
|
32
38
|
});
|
|
33
|
-
const url =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
url.searchParams.set("token", this.options.authToken);
|
|
37
|
-
}
|
|
38
|
-
this.eventSource = new EventSource(url.toString());
|
|
39
|
-
this.eventSource.addEventListener("open", () => {
|
|
40
|
-
console.log("[SSEClient] Connected");
|
|
41
|
-
this.reconnectAttempts = 0;
|
|
42
|
-
this.options.onStatusChange?.("connected");
|
|
43
|
-
});
|
|
44
|
-
this.eventSource.addEventListener("connected", (e) => {
|
|
45
|
-
const data = JSON.parse(e.data);
|
|
46
|
-
console.log("[SSEClient] Server ready:", data);
|
|
47
|
-
if (this.connectionResolver) {
|
|
48
|
-
this.connectionResolver();
|
|
49
|
-
this.connectionResolver = null;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
this.eventSource.addEventListener("connection", (e) => {
|
|
53
|
-
const event = JSON.parse(e.data);
|
|
54
|
-
this.options.onConnectionEvent?.(event);
|
|
55
|
-
});
|
|
56
|
-
this.eventSource.addEventListener("observability", (e) => {
|
|
57
|
-
const event = JSON.parse(e.data);
|
|
58
|
-
this.options.onObservabilityEvent?.(event);
|
|
59
|
-
});
|
|
60
|
-
this.eventSource.addEventListener("rpc-response", (e) => {
|
|
61
|
-
const response = JSON.parse(e.data);
|
|
62
|
-
this.handleRpcResponse(response);
|
|
63
|
-
});
|
|
64
|
-
this.eventSource.addEventListener("error", () => {
|
|
65
|
-
console.error("[SSEClient] Connection error");
|
|
66
|
-
this.options.onStatusChange?.("error");
|
|
67
|
-
if (!this.isManuallyDisconnected && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
68
|
-
this.reconnectAttempts++;
|
|
69
|
-
console.log(`[SSEClient] Reconnecting (attempt ${this.reconnectAttempts})...`);
|
|
70
|
-
setTimeout(() => {
|
|
71
|
-
this.disconnect();
|
|
72
|
-
this.connect();
|
|
73
|
-
}, this.reconnectDelay * this.reconnectAttempts);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
39
|
+
const url = this.buildUrl();
|
|
40
|
+
this.eventSource = new EventSource(url);
|
|
41
|
+
this.setupEventListeners();
|
|
76
42
|
}
|
|
77
43
|
/**
|
|
78
|
-
* Disconnect from SSE endpoint
|
|
44
|
+
* Disconnect from the SSE endpoint
|
|
79
45
|
*/
|
|
80
46
|
disconnect() {
|
|
81
47
|
this.isManuallyDisconnected = true;
|
|
@@ -85,139 +51,281 @@ var SSEClient = class {
|
|
|
85
51
|
}
|
|
86
52
|
this.connectionPromise = null;
|
|
87
53
|
this.connectionResolver = null;
|
|
88
|
-
|
|
89
|
-
const error = new Error("Connection closed");
|
|
90
|
-
error.name = "ConnectionClosedError";
|
|
91
|
-
reject(error);
|
|
92
|
-
}
|
|
93
|
-
this.pendingRequests.clear();
|
|
54
|
+
this.rejectAllPendingRequests(new Error("Connection closed"));
|
|
94
55
|
this.options.onStatusChange?.("disconnected");
|
|
95
56
|
}
|
|
96
57
|
/**
|
|
97
|
-
*
|
|
98
|
-
* Note: SSE is unidirectional (server->client), so we need to send requests via POST
|
|
99
|
-
*/
|
|
100
|
-
async sendRequest(method, params) {
|
|
101
|
-
if (this.connectionPromise) {
|
|
102
|
-
await this.connectionPromise;
|
|
103
|
-
}
|
|
104
|
-
const id = `rpc_${nanoid.nanoid(10)}`;
|
|
105
|
-
const request = {
|
|
106
|
-
id,
|
|
107
|
-
method,
|
|
108
|
-
params
|
|
109
|
-
};
|
|
110
|
-
const promise = new Promise((resolve, reject) => {
|
|
111
|
-
this.pendingRequests.set(id, { resolve, reject });
|
|
112
|
-
setTimeout(() => {
|
|
113
|
-
if (this.pendingRequests.has(id)) {
|
|
114
|
-
this.pendingRequests.delete(id);
|
|
115
|
-
reject(new Error("Request timeout"));
|
|
116
|
-
}
|
|
117
|
-
}, 3e4);
|
|
118
|
-
});
|
|
119
|
-
try {
|
|
120
|
-
const url = new URL(this.options.url, typeof window !== "undefined" ? window.location.origin : void 0);
|
|
121
|
-
url.searchParams.set("identity", this.options.identity);
|
|
122
|
-
await fetch(url.toString(), {
|
|
123
|
-
method: "POST",
|
|
124
|
-
headers: {
|
|
125
|
-
"Content-Type": "application/json",
|
|
126
|
-
...this.options.authToken && { Authorization: `Bearer ${this.options.authToken}` }
|
|
127
|
-
},
|
|
128
|
-
body: JSON.stringify(request)
|
|
129
|
-
});
|
|
130
|
-
} catch (error) {
|
|
131
|
-
this.pendingRequests.delete(id);
|
|
132
|
-
throw error;
|
|
133
|
-
}
|
|
134
|
-
return promise;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Handle RPC response
|
|
58
|
+
* Check if connected to the SSE endpoint
|
|
138
59
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (pending) {
|
|
142
|
-
this.pendingRequests.delete(response.id);
|
|
143
|
-
if (response.error) {
|
|
144
|
-
pending.reject(new Error(response.error.message));
|
|
145
|
-
} else {
|
|
146
|
-
pending.resolve(response.result);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
60
|
+
isConnected() {
|
|
61
|
+
return this.eventSource?.readyState === EventSource.OPEN;
|
|
149
62
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
63
|
+
// ============================================
|
|
64
|
+
// RPC Methods
|
|
65
|
+
// ============================================
|
|
153
66
|
async getSessions() {
|
|
154
67
|
return this.sendRequest("getSessions");
|
|
155
68
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Connect to an MCP server
|
|
158
|
-
*/
|
|
159
69
|
async connectToServer(params) {
|
|
160
70
|
return this.sendRequest("connect", params);
|
|
161
71
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Disconnect from an MCP server
|
|
164
|
-
*/
|
|
165
72
|
async disconnectFromServer(sessionId) {
|
|
166
73
|
return this.sendRequest("disconnect", { sessionId });
|
|
167
74
|
}
|
|
168
|
-
/**
|
|
169
|
-
* List tools from a session
|
|
170
|
-
*/
|
|
171
75
|
async listTools(sessionId) {
|
|
172
76
|
return this.sendRequest("listTools", { sessionId });
|
|
173
77
|
}
|
|
78
|
+
async callTool(sessionId, toolName, toolArgs) {
|
|
79
|
+
const result = await this.sendRequest("callTool", { sessionId, toolName, toolArgs });
|
|
80
|
+
this.emitUiEventIfPresent(result, sessionId, toolName);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
async restoreSession(sessionId) {
|
|
84
|
+
return this.sendRequest("restoreSession", { sessionId });
|
|
85
|
+
}
|
|
86
|
+
async finishAuth(sessionId, code) {
|
|
87
|
+
return this.sendRequest("finishAuth", { sessionId, code });
|
|
88
|
+
}
|
|
89
|
+
async listPrompts(sessionId) {
|
|
90
|
+
return this.sendRequest("listPrompts", { sessionId });
|
|
91
|
+
}
|
|
92
|
+
async getPrompt(sessionId, name, args) {
|
|
93
|
+
return this.sendRequest("getPrompt", { sessionId, name, args });
|
|
94
|
+
}
|
|
95
|
+
async listResources(sessionId) {
|
|
96
|
+
return this.sendRequest("listResources", { sessionId });
|
|
97
|
+
}
|
|
98
|
+
async readResource(sessionId, uri) {
|
|
99
|
+
return this.sendRequest("readResource", { sessionId, uri });
|
|
100
|
+
}
|
|
101
|
+
// ============================================
|
|
102
|
+
// Resource Preloading (for instant UI loading)
|
|
103
|
+
// ============================================
|
|
174
104
|
/**
|
|
175
|
-
*
|
|
105
|
+
* Preload UI resources for tools that have UI metadata.
|
|
106
|
+
* Call this when tools are discovered to enable instant MCP App UI loading.
|
|
176
107
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
108
|
+
preloadToolUiResources(sessionId, tools) {
|
|
109
|
+
for (const tool of tools) {
|
|
110
|
+
const uri = this.extractUiResourceUri(tool);
|
|
111
|
+
if (!uri) continue;
|
|
112
|
+
if (this.resourceCache.has(uri)) {
|
|
113
|
+
this.log(`Resource already cached: ${uri}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
this.log(`Preloading UI resource for tool "${tool.name}": ${uri}`);
|
|
117
|
+
const promise = this.sendRequest("readResource", { sessionId, uri }).catch((err) => {
|
|
118
|
+
this.log(`Failed to preload resource ${uri}: ${err.message}`, "warn");
|
|
119
|
+
this.resourceCache.delete(uri);
|
|
120
|
+
return null;
|
|
121
|
+
});
|
|
122
|
+
this.resourceCache.set(uri, promise);
|
|
123
|
+
}
|
|
179
124
|
}
|
|
180
125
|
/**
|
|
181
|
-
*
|
|
126
|
+
* Get a preloaded resource from cache, or fetch if not cached.
|
|
182
127
|
*/
|
|
183
|
-
|
|
184
|
-
|
|
128
|
+
getOrFetchResource(sessionId, uri) {
|
|
129
|
+
const cached = this.resourceCache.get(uri);
|
|
130
|
+
if (cached) {
|
|
131
|
+
this.log(`Cache hit for resource: ${uri}`);
|
|
132
|
+
return cached;
|
|
133
|
+
}
|
|
134
|
+
this.log(`Cache miss, fetching resource: ${uri}`);
|
|
135
|
+
const promise = this.sendRequest("readResource", { sessionId, uri });
|
|
136
|
+
this.resourceCache.set(uri, promise);
|
|
137
|
+
return promise;
|
|
185
138
|
}
|
|
186
139
|
/**
|
|
187
|
-
*
|
|
140
|
+
* Check if a resource is already cached
|
|
188
141
|
*/
|
|
189
|
-
|
|
190
|
-
return this.
|
|
142
|
+
hasPreloadedResource(uri) {
|
|
143
|
+
return this.resourceCache.has(uri);
|
|
191
144
|
}
|
|
192
145
|
/**
|
|
193
|
-
*
|
|
146
|
+
* Clear the resource cache
|
|
194
147
|
*/
|
|
195
|
-
|
|
196
|
-
|
|
148
|
+
clearResourceCache() {
|
|
149
|
+
this.resourceCache.clear();
|
|
197
150
|
}
|
|
151
|
+
// ============================================
|
|
152
|
+
// Private: Request Handling
|
|
153
|
+
// ============================================
|
|
198
154
|
/**
|
|
199
|
-
*
|
|
155
|
+
* Send an RPC request and return the response directly from HTTP.
|
|
156
|
+
* This bypasses SSE latency by returning results in the HTTP response body.
|
|
200
157
|
*/
|
|
201
|
-
async
|
|
202
|
-
|
|
158
|
+
async sendRequest(method, params) {
|
|
159
|
+
if (this.connectionPromise) {
|
|
160
|
+
await this.connectionPromise;
|
|
161
|
+
}
|
|
162
|
+
const request = {
|
|
163
|
+
id: `rpc_${nanoid.nanoid(10)}`,
|
|
164
|
+
method,
|
|
165
|
+
params
|
|
166
|
+
};
|
|
167
|
+
const response = await fetch(this.buildUrl(), {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: this.buildHeaders(),
|
|
170
|
+
body: JSON.stringify(request)
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
174
|
+
}
|
|
175
|
+
const data = await response.json();
|
|
176
|
+
return this.parseRpcResponse(data, request.id);
|
|
203
177
|
}
|
|
204
178
|
/**
|
|
205
|
-
*
|
|
179
|
+
* Parse RPC response and handle different response formats
|
|
206
180
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
181
|
+
parseRpcResponse(data, requestId) {
|
|
182
|
+
if ("result" in data) {
|
|
183
|
+
return data.result;
|
|
184
|
+
}
|
|
185
|
+
if ("error" in data && data.error) {
|
|
186
|
+
throw new Error(data.error.message || "Unknown RPC error");
|
|
187
|
+
}
|
|
188
|
+
if ("acknowledged" in data) {
|
|
189
|
+
return this.waitForSseResponse(requestId);
|
|
190
|
+
}
|
|
191
|
+
throw new Error("Invalid RPC response format");
|
|
209
192
|
}
|
|
210
193
|
/**
|
|
211
|
-
*
|
|
194
|
+
* Wait for RPC response via SSE (legacy fallback)
|
|
212
195
|
*/
|
|
213
|
-
|
|
214
|
-
|
|
196
|
+
waitForSseResponse(requestId) {
|
|
197
|
+
const timeoutMs = this.options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
const timeoutId = setTimeout(() => {
|
|
200
|
+
this.pendingRequests.delete(requestId);
|
|
201
|
+
reject(new Error(`Request timeout after ${timeoutMs}ms`));
|
|
202
|
+
}, timeoutMs);
|
|
203
|
+
this.pendingRequests.set(requestId, {
|
|
204
|
+
resolve,
|
|
205
|
+
reject,
|
|
206
|
+
timeoutId
|
|
207
|
+
});
|
|
208
|
+
});
|
|
215
209
|
}
|
|
216
210
|
/**
|
|
217
|
-
*
|
|
211
|
+
* Handle RPC response received via SSE (legacy)
|
|
218
212
|
*/
|
|
219
|
-
|
|
220
|
-
|
|
213
|
+
handleRpcResponse(response) {
|
|
214
|
+
const pending = this.pendingRequests.get(response.id);
|
|
215
|
+
if (!pending) return;
|
|
216
|
+
clearTimeout(pending.timeoutId);
|
|
217
|
+
this.pendingRequests.delete(response.id);
|
|
218
|
+
if (response.error) {
|
|
219
|
+
pending.reject(new Error(response.error.message));
|
|
220
|
+
} else {
|
|
221
|
+
pending.resolve(response.result);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ============================================
|
|
225
|
+
// Private: Event Handling
|
|
226
|
+
// ============================================
|
|
227
|
+
setupEventListeners() {
|
|
228
|
+
if (!this.eventSource) return;
|
|
229
|
+
this.eventSource.addEventListener("open", () => {
|
|
230
|
+
this.log("Connected");
|
|
231
|
+
this.reconnectAttempts = 0;
|
|
232
|
+
this.options.onStatusChange?.("connected");
|
|
233
|
+
});
|
|
234
|
+
this.eventSource.addEventListener("connected", () => {
|
|
235
|
+
this.log("Server ready");
|
|
236
|
+
this.connectionResolver?.();
|
|
237
|
+
this.connectionResolver = null;
|
|
238
|
+
});
|
|
239
|
+
this.eventSource.addEventListener("connection", (e) => {
|
|
240
|
+
const event = JSON.parse(e.data);
|
|
241
|
+
this.options.onConnectionEvent?.(event);
|
|
242
|
+
});
|
|
243
|
+
this.eventSource.addEventListener("observability", (e) => {
|
|
244
|
+
const event = JSON.parse(e.data);
|
|
245
|
+
this.options.onObservabilityEvent?.(event);
|
|
246
|
+
});
|
|
247
|
+
this.eventSource.addEventListener("rpc-response", (e) => {
|
|
248
|
+
const response = JSON.parse(e.data);
|
|
249
|
+
this.handleRpcResponse(response);
|
|
250
|
+
});
|
|
251
|
+
this.eventSource.addEventListener("error", () => {
|
|
252
|
+
this.log("Connection error", "error");
|
|
253
|
+
this.options.onStatusChange?.("error");
|
|
254
|
+
this.attemptReconnect();
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
attemptReconnect() {
|
|
258
|
+
if (this.isManuallyDisconnected || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.reconnectAttempts++;
|
|
262
|
+
const delay = BASE_RECONNECT_DELAY * this.reconnectAttempts;
|
|
263
|
+
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
this.disconnect();
|
|
266
|
+
this.connect();
|
|
267
|
+
}, delay);
|
|
268
|
+
}
|
|
269
|
+
// ============================================
|
|
270
|
+
// Private: Utilities
|
|
271
|
+
// ============================================
|
|
272
|
+
buildUrl() {
|
|
273
|
+
const url = new URL(this.options.url, globalThis.location?.origin);
|
|
274
|
+
url.searchParams.set("identity", this.options.identity);
|
|
275
|
+
if (this.options.authToken) {
|
|
276
|
+
url.searchParams.set("token", this.options.authToken);
|
|
277
|
+
}
|
|
278
|
+
return url.toString();
|
|
279
|
+
}
|
|
280
|
+
buildHeaders() {
|
|
281
|
+
const headers = {
|
|
282
|
+
"Content-Type": "application/json"
|
|
283
|
+
};
|
|
284
|
+
if (this.options.authToken) {
|
|
285
|
+
headers["Authorization"] = `Bearer ${this.options.authToken}`;
|
|
286
|
+
}
|
|
287
|
+
return headers;
|
|
288
|
+
}
|
|
289
|
+
rejectAllPendingRequests(error) {
|
|
290
|
+
for (const [, pending] of this.pendingRequests) {
|
|
291
|
+
clearTimeout(pending.timeoutId);
|
|
292
|
+
pending.reject(error);
|
|
293
|
+
}
|
|
294
|
+
this.pendingRequests.clear();
|
|
295
|
+
}
|
|
296
|
+
extractUiResourceUri(tool) {
|
|
297
|
+
const meta = tool._meta?.ui;
|
|
298
|
+
if (!meta || typeof meta !== "object") return void 0;
|
|
299
|
+
if (meta.visibility && !meta.visibility.includes("app")) return void 0;
|
|
300
|
+
return meta.resourceUri ?? meta.uri;
|
|
301
|
+
}
|
|
302
|
+
emitUiEventIfPresent(result, sessionId, toolName) {
|
|
303
|
+
const meta = result?._meta;
|
|
304
|
+
const resourceUri = meta?.ui?.resourceUri ?? meta?.["ui/resourceUri"];
|
|
305
|
+
if (resourceUri) {
|
|
306
|
+
this.options.onEvent?.({
|
|
307
|
+
type: "mcp-apps-ui",
|
|
308
|
+
sessionId,
|
|
309
|
+
resourceUri,
|
|
310
|
+
toolName,
|
|
311
|
+
result,
|
|
312
|
+
timestamp: Date.now()
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
log(message, level = "info") {
|
|
317
|
+
if (!this.options.debug && level === "info") return;
|
|
318
|
+
const prefix = "[SSEClient]";
|
|
319
|
+
switch (level) {
|
|
320
|
+
case "warn":
|
|
321
|
+
console.warn(prefix, message);
|
|
322
|
+
break;
|
|
323
|
+
case "error":
|
|
324
|
+
console.error(prefix, message);
|
|
325
|
+
break;
|
|
326
|
+
default:
|
|
327
|
+
console.log(prefix, message);
|
|
328
|
+
}
|
|
221
329
|
}
|
|
222
330
|
};
|
|
223
331
|
|
|
@@ -257,7 +365,8 @@ function useMcp(options) {
|
|
|
257
365
|
if (isMountedRef.current) {
|
|
258
366
|
setStatus(newStatus);
|
|
259
367
|
}
|
|
260
|
-
}
|
|
368
|
+
},
|
|
369
|
+
requestTimeout: options.requestTimeout
|
|
261
370
|
};
|
|
262
371
|
const client = new SSEClient(clientOptions);
|
|
263
372
|
clientRef.current = client;
|
|
@@ -296,6 +405,9 @@ function useMcp(options) {
|
|
|
296
405
|
}
|
|
297
406
|
}
|
|
298
407
|
case "tools_discovered": {
|
|
408
|
+
if (clientRef.current && event.tools?.length) {
|
|
409
|
+
clientRef.current.preloadToolUiResources(event.sessionId, event.tools);
|
|
410
|
+
}
|
|
299
411
|
return prev.map(
|
|
300
412
|
(c) => c.sessionId === event.sessionId ? { ...c, tools: event.tools, state: "READY" } : c
|
|
301
413
|
);
|
|
@@ -482,11 +594,344 @@ function useMcp(options) {
|
|
|
482
594
|
listPrompts,
|
|
483
595
|
getPrompt,
|
|
484
596
|
listResources,
|
|
485
|
-
readResource
|
|
597
|
+
readResource,
|
|
598
|
+
client: clientRef.current
|
|
486
599
|
};
|
|
487
600
|
}
|
|
601
|
+
var HOST_INFO = { name: "mcp-ts-host", version: "1.0.0" };
|
|
602
|
+
var SANDBOX_PERMISSIONS = [
|
|
603
|
+
"allow-scripts",
|
|
604
|
+
// Required for app JavaScript execution
|
|
605
|
+
"allow-forms",
|
|
606
|
+
// Required for form submissions
|
|
607
|
+
"allow-same-origin",
|
|
608
|
+
// Required for Blob URL correctness
|
|
609
|
+
"allow-modals",
|
|
610
|
+
// Required for dialogs/alerts
|
|
611
|
+
"allow-popups",
|
|
612
|
+
// Required for opening links
|
|
613
|
+
"allow-downloads"
|
|
614
|
+
// Required for file downloads
|
|
615
|
+
].join(" ");
|
|
616
|
+
var MCP_URI_SCHEMES = ["ui://", "mcp-app://"];
|
|
617
|
+
var AppHost = class {
|
|
618
|
+
constructor(client, iframe, options) {
|
|
619
|
+
this.client = client;
|
|
620
|
+
this.iframe = iframe;
|
|
621
|
+
__publicField(this, "bridge");
|
|
622
|
+
__publicField(this, "sessionId");
|
|
623
|
+
__publicField(this, "resourceCache", /* @__PURE__ */ new Map());
|
|
624
|
+
__publicField(this, "debug");
|
|
625
|
+
/** Callback for app messages (e.g., chat messages from the app) */
|
|
626
|
+
__publicField(this, "onAppMessage");
|
|
627
|
+
this.debug = options?.debug ?? false;
|
|
628
|
+
this.configureSandbox();
|
|
629
|
+
this.bridge = this.initializeBridge();
|
|
630
|
+
}
|
|
631
|
+
// ============================================
|
|
632
|
+
// Public API
|
|
633
|
+
// ============================================
|
|
634
|
+
/**
|
|
635
|
+
* Start the host. This prepares the bridge handlers but doesn't connect yet.
|
|
636
|
+
* The actual connection happens in launch() after HTML is loaded.
|
|
637
|
+
* @returns Promise that resolves immediately (bridge connects during launch)
|
|
638
|
+
*/
|
|
639
|
+
async start() {
|
|
640
|
+
this.log("Host started, ready to launch");
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Preload UI resources to enable instant app loading.
|
|
644
|
+
* Call this when tools are discovered to cache their UI resources.
|
|
645
|
+
*/
|
|
646
|
+
preload(tools) {
|
|
647
|
+
for (const tool of tools) {
|
|
648
|
+
const uri = this.extractUiResourceUri(tool);
|
|
649
|
+
if (!uri || this.resourceCache.has(uri)) continue;
|
|
650
|
+
const promise = this.preloadResource(uri);
|
|
651
|
+
this.resourceCache.set(uri, promise);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Launch an MCP App from a URL or MCP resource URI.
|
|
656
|
+
* Loads the HTML first, then establishes bridge connection.
|
|
657
|
+
*/
|
|
658
|
+
async launch(url, sessionId) {
|
|
659
|
+
if (sessionId) this.sessionId = sessionId;
|
|
660
|
+
const initializedPromise = this.onAppReady();
|
|
661
|
+
if (this.isMcpUri(url)) {
|
|
662
|
+
await this.launchMcpApp(url);
|
|
663
|
+
} else {
|
|
664
|
+
this.iframe.src = url;
|
|
665
|
+
}
|
|
666
|
+
await this.onIframeReady();
|
|
667
|
+
await this.connectBridge();
|
|
668
|
+
this.log("Waiting for app initialization");
|
|
669
|
+
await Promise.race([
|
|
670
|
+
initializedPromise,
|
|
671
|
+
new Promise((resolve) => setTimeout(() => {
|
|
672
|
+
this.log("Initialization timeout - continuing anyway", "warn");
|
|
673
|
+
resolve();
|
|
674
|
+
}, 3e3))
|
|
675
|
+
]);
|
|
676
|
+
this.log("App launched and ready");
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Wait for app to signal initialization complete
|
|
680
|
+
*/
|
|
681
|
+
onAppReady() {
|
|
682
|
+
return new Promise((resolve) => {
|
|
683
|
+
const originalHandler = this.bridge.oninitialized;
|
|
684
|
+
this.bridge.oninitialized = (...args) => {
|
|
685
|
+
this.log("App initialized");
|
|
686
|
+
resolve();
|
|
687
|
+
this.bridge.oninitialized = originalHandler;
|
|
688
|
+
originalHandler?.(...args);
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Wait for iframe to finish loading
|
|
694
|
+
*/
|
|
695
|
+
onIframeReady() {
|
|
696
|
+
return new Promise((resolve) => {
|
|
697
|
+
if (this.iframe.contentDocument?.readyState === "complete") {
|
|
698
|
+
resolve();
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
this.iframe.addEventListener("load", () => resolve(), { once: true });
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Send tool input arguments to the MCP App.
|
|
706
|
+
* Call this after launch() when tool input is available.
|
|
707
|
+
*/
|
|
708
|
+
sendToolInput(args) {
|
|
709
|
+
this.log("Sending tool input to app");
|
|
710
|
+
this.bridge.sendToolInput({ arguments: args });
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Send tool result to the MCP App.
|
|
714
|
+
* Call this when the tool call completes.
|
|
715
|
+
*/
|
|
716
|
+
sendToolResult(result) {
|
|
717
|
+
this.log("Sending tool result to app");
|
|
718
|
+
this.bridge.sendToolResult(result);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Send tool cancellation to the MCP App.
|
|
722
|
+
* Call this when the tool call is cancelled or fails.
|
|
723
|
+
*/
|
|
724
|
+
sendToolCancelled(reason) {
|
|
725
|
+
this.log("Sending tool cancellation to app");
|
|
726
|
+
this.bridge.sendToolCancelled({ reason });
|
|
727
|
+
}
|
|
728
|
+
// ============================================
|
|
729
|
+
// Private: Initialization
|
|
730
|
+
// ============================================
|
|
731
|
+
configureSandbox() {
|
|
732
|
+
if (this.iframe.sandbox.value !== SANDBOX_PERMISSIONS) {
|
|
733
|
+
this.iframe.sandbox.value = SANDBOX_PERMISSIONS;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
initializeBridge() {
|
|
737
|
+
const bridge = new appBridge.AppBridge(
|
|
738
|
+
null,
|
|
739
|
+
HOST_INFO,
|
|
740
|
+
{
|
|
741
|
+
openLinks: {},
|
|
742
|
+
serverTools: {},
|
|
743
|
+
logging: {},
|
|
744
|
+
// Declare support for model context updates
|
|
745
|
+
updateModelContext: { text: {} }
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
// Initial host context
|
|
749
|
+
hostContext: {
|
|
750
|
+
theme: "dark",
|
|
751
|
+
platform: "web",
|
|
752
|
+
containerDimensions: { maxHeight: 6e3 },
|
|
753
|
+
displayMode: "inline",
|
|
754
|
+
availableDisplayModes: ["inline", "fullscreen"]
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
);
|
|
758
|
+
bridge.oncalltool = (params) => this.handleToolCall(params);
|
|
759
|
+
bridge.onopenlink = this.handleOpenLink.bind(this);
|
|
760
|
+
bridge.onmessage = this.handleMessage.bind(this);
|
|
761
|
+
bridge.onloggingmessage = (params) => this.log(`App log [${params.level}]: ${params.data}`);
|
|
762
|
+
bridge.onupdatemodelcontext = async () => ({});
|
|
763
|
+
bridge.onsizechange = async ({ width, height }) => {
|
|
764
|
+
if (height !== void 0) this.iframe.style.height = `${height}px`;
|
|
765
|
+
if (width !== void 0) this.iframe.style.minWidth = `min(${width}px, 100%)`;
|
|
766
|
+
return {};
|
|
767
|
+
};
|
|
768
|
+
bridge.onrequestdisplaymode = async (params) => ({
|
|
769
|
+
mode: params.mode === "fullscreen" ? "fullscreen" : "inline"
|
|
770
|
+
});
|
|
771
|
+
return bridge;
|
|
772
|
+
}
|
|
773
|
+
async connectBridge() {
|
|
774
|
+
this.log("Connecting bridge to iframe");
|
|
775
|
+
const transport = new appBridge.PostMessageTransport(
|
|
776
|
+
this.iframe.contentWindow,
|
|
777
|
+
this.iframe.contentWindow
|
|
778
|
+
);
|
|
779
|
+
try {
|
|
780
|
+
await this.bridge.connect(transport);
|
|
781
|
+
this.log("Bridge connected successfully");
|
|
782
|
+
} catch (error) {
|
|
783
|
+
this.log("Bridge connection failed", "error");
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// ============================================
|
|
788
|
+
// Private: Bridge Event Handlers
|
|
789
|
+
// ============================================
|
|
790
|
+
async handleToolCall(params) {
|
|
791
|
+
if (!this.client.isConnected()) {
|
|
792
|
+
throw new Error("Client disconnected");
|
|
793
|
+
}
|
|
794
|
+
const sessionId = await this.getSessionId();
|
|
795
|
+
if (!sessionId) {
|
|
796
|
+
throw new Error("No active session");
|
|
797
|
+
}
|
|
798
|
+
const result = await this.client.callTool(
|
|
799
|
+
sessionId,
|
|
800
|
+
params.name,
|
|
801
|
+
params.arguments ?? {}
|
|
802
|
+
);
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
805
|
+
async handleOpenLink(params) {
|
|
806
|
+
window.open(params.url, "_blank", "noopener,noreferrer");
|
|
807
|
+
return {};
|
|
808
|
+
}
|
|
809
|
+
async handleMessage(params) {
|
|
810
|
+
this.onAppMessage?.(params);
|
|
811
|
+
return {};
|
|
812
|
+
}
|
|
813
|
+
// ============================================
|
|
814
|
+
// Private: Resource Loading
|
|
815
|
+
// ============================================
|
|
816
|
+
async launchMcpApp(uri) {
|
|
817
|
+
if (!this.client.isConnected()) {
|
|
818
|
+
throw new Error("Client must be connected");
|
|
819
|
+
}
|
|
820
|
+
const sessionId = await this.getSessionId();
|
|
821
|
+
if (!sessionId) {
|
|
822
|
+
throw new Error("No active session");
|
|
823
|
+
}
|
|
824
|
+
const response = await this.fetchResourceWithCache(sessionId, uri);
|
|
825
|
+
if (!response?.contents?.length) {
|
|
826
|
+
throw new Error(`Empty resource: ${uri}`);
|
|
827
|
+
}
|
|
828
|
+
const content = response.contents[0];
|
|
829
|
+
const html = this.decodeContent(content);
|
|
830
|
+
if (!html) {
|
|
831
|
+
throw new Error(`Invalid content in resource: ${uri}`);
|
|
832
|
+
}
|
|
833
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
834
|
+
this.iframe.src = URL.createObjectURL(blob);
|
|
835
|
+
}
|
|
836
|
+
async fetchResourceWithCache(sessionId, uri) {
|
|
837
|
+
if (this.hasClientCache()) {
|
|
838
|
+
return this.client.getOrFetchResource(sessionId, uri);
|
|
839
|
+
}
|
|
840
|
+
const cached = this.resourceCache.get(uri);
|
|
841
|
+
if (cached) {
|
|
842
|
+
const result = await cached;
|
|
843
|
+
if (result) return result;
|
|
844
|
+
}
|
|
845
|
+
return this.client.readResource(sessionId, uri);
|
|
846
|
+
}
|
|
847
|
+
async preloadResource(uri) {
|
|
848
|
+
try {
|
|
849
|
+
const sessionId = await this.getSessionId();
|
|
850
|
+
if (!sessionId) return null;
|
|
851
|
+
return await this.client.readResource(sessionId, uri);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
this.log(`Preload failed for ${uri}`, "warn");
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// ============================================
|
|
858
|
+
// Private: Utilities
|
|
859
|
+
// ============================================
|
|
860
|
+
async getSessionId() {
|
|
861
|
+
if (this.sessionId) return this.sessionId;
|
|
862
|
+
const result = await this.client.getSessions();
|
|
863
|
+
return result.sessions?.[0]?.sessionId;
|
|
864
|
+
}
|
|
865
|
+
isMcpUri(url) {
|
|
866
|
+
return MCP_URI_SCHEMES.some((scheme) => url.startsWith(scheme));
|
|
867
|
+
}
|
|
868
|
+
hasClientCache() {
|
|
869
|
+
return "getOrFetchResource" in this.client && typeof this.client.getOrFetchResource === "function";
|
|
870
|
+
}
|
|
871
|
+
extractUiResourceUri(tool) {
|
|
872
|
+
const meta = tool._meta;
|
|
873
|
+
if (!meta?.ui) return void 0;
|
|
874
|
+
return meta.ui.resourceUri ?? meta.ui.uri;
|
|
875
|
+
}
|
|
876
|
+
decodeContent(content) {
|
|
877
|
+
if (content.blob) {
|
|
878
|
+
return atob(content.blob);
|
|
879
|
+
}
|
|
880
|
+
return content.text;
|
|
881
|
+
}
|
|
882
|
+
log(message, level = "info") {
|
|
883
|
+
if (!this.debug && level === "info") return;
|
|
884
|
+
const prefix = "[AppHost]";
|
|
885
|
+
switch (level) {
|
|
886
|
+
case "warn":
|
|
887
|
+
console.warn(prefix, message);
|
|
888
|
+
break;
|
|
889
|
+
case "error":
|
|
890
|
+
console.error(prefix, message);
|
|
891
|
+
break;
|
|
892
|
+
default:
|
|
893
|
+
console.log(prefix, message);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// src/client/react/use-mcp-app.ts
|
|
899
|
+
function useMcpApp(client, iframeRef, options) {
|
|
900
|
+
const [host, setHost] = react.useState(null);
|
|
901
|
+
const [error, setError] = react.useState(null);
|
|
902
|
+
const initializingRef = react.useRef(false);
|
|
903
|
+
const onMessageRef = react.useRef(options?.onMessage);
|
|
904
|
+
react.useEffect(() => {
|
|
905
|
+
onMessageRef.current = options?.onMessage;
|
|
906
|
+
}, [options?.onMessage]);
|
|
907
|
+
react.useEffect(() => {
|
|
908
|
+
if (!client || !iframeRef.current || initializingRef.current) return;
|
|
909
|
+
initializingRef.current = true;
|
|
910
|
+
const initHost = async () => {
|
|
911
|
+
try {
|
|
912
|
+
const appHost = new AppHost(client, iframeRef.current);
|
|
913
|
+
appHost.onAppMessage = (params) => {
|
|
914
|
+
onMessageRef.current?.(params);
|
|
915
|
+
};
|
|
916
|
+
setHost(appHost);
|
|
917
|
+
await appHost.start();
|
|
918
|
+
} catch (err) {
|
|
919
|
+
console.error("[useMcpApp] Failed to initialize AppHost:", err);
|
|
920
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
initHost();
|
|
924
|
+
return () => {
|
|
925
|
+
initializingRef.current = false;
|
|
926
|
+
setHost(null);
|
|
927
|
+
};
|
|
928
|
+
}, [client, iframeRef]);
|
|
929
|
+
return { host, error };
|
|
930
|
+
}
|
|
488
931
|
|
|
932
|
+
exports.AppHost = AppHost;
|
|
489
933
|
exports.SSEClient = SSEClient;
|
|
490
934
|
exports.useMcp = useMcp;
|
|
935
|
+
exports.useMcpApp = useMcpApp;
|
|
491
936
|
//# sourceMappingURL=react.js.map
|
|
492
937
|
//# sourceMappingURL=react.js.map
|