@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.
Files changed (77) hide show
  1. package/README.md +25 -13
  2. package/dist/adapters/agui-adapter.d.mts +21 -44
  3. package/dist/adapters/agui-adapter.d.ts +21 -44
  4. package/dist/adapters/agui-adapter.js +93 -67
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +93 -68
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +32 -134
  9. package/dist/adapters/agui-middleware.d.ts +32 -134
  10. package/dist/adapters/agui-middleware.js +314 -350
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs +314 -351
  13. package/dist/adapters/agui-middleware.mjs.map +1 -1
  14. package/dist/adapters/ai-adapter.d.mts +2 -2
  15. package/dist/adapters/ai-adapter.d.ts +2 -2
  16. package/dist/adapters/langchain-adapter.d.mts +2 -2
  17. package/dist/adapters/langchain-adapter.d.ts +2 -2
  18. package/dist/adapters/mastra-adapter.d.mts +2 -2
  19. package/dist/adapters/mastra-adapter.d.ts +2 -2
  20. package/dist/client/index.d.mts +184 -57
  21. package/dist/client/index.d.ts +184 -57
  22. package/dist/client/index.js +535 -130
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/index.mjs +535 -131
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/client/react.d.mts +40 -6
  27. package/dist/client/react.d.ts +40 -6
  28. package/dist/client/react.js +587 -142
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +586 -143
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +5 -5
  33. package/dist/client/vue.d.ts +5 -5
  34. package/dist/client/vue.js +545 -140
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +545 -141
  37. package/dist/client/vue.mjs.map +1 -1
  38. package/dist/{events-BP6WyRNh.d.mts → events-BgeztGYZ.d.mts} +12 -1
  39. package/dist/{events-BP6WyRNh.d.ts → events-BgeztGYZ.d.ts} +12 -1
  40. package/dist/index.d.mts +4 -4
  41. package/dist/index.d.ts +4 -4
  42. package/dist/index.js +779 -248
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +775 -245
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/{multi-session-client-DMF3ED2O.d.mts → multi-session-client-CxogNckF.d.mts} +1 -1
  47. package/dist/{multi-session-client-BOFgPypS.d.ts → multi-session-client-cox_WXUj.d.ts} +1 -1
  48. package/dist/server/index.d.mts +44 -40
  49. package/dist/server/index.d.ts +44 -40
  50. package/dist/server/index.js +242 -116
  51. package/dist/server/index.js.map +1 -1
  52. package/dist/server/index.mjs +238 -112
  53. package/dist/server/index.mjs.map +1 -1
  54. package/dist/shared/index.d.mts +2 -2
  55. package/dist/shared/index.d.ts +2 -2
  56. package/dist/shared/index.js.map +1 -1
  57. package/dist/shared/index.mjs.map +1 -1
  58. package/dist/{types-SbDlA2VX.d.mts → types-CLccx9wW.d.mts} +1 -1
  59. package/dist/{types-SbDlA2VX.d.ts → types-CLccx9wW.d.ts} +1 -1
  60. package/package.json +8 -1
  61. package/src/adapters/agui-adapter.ts +121 -107
  62. package/src/adapters/agui-middleware.ts +474 -512
  63. package/src/client/core/app-host.ts +417 -0
  64. package/src/client/core/sse-client.ts +365 -212
  65. package/src/client/core/types.ts +31 -0
  66. package/src/client/index.ts +1 -0
  67. package/src/client/react/index.ts +1 -0
  68. package/src/client/react/use-mcp-app.ts +73 -0
  69. package/src/client/react/useMcp.ts +18 -0
  70. package/src/server/handlers/nextjs-handler.ts +8 -7
  71. package/src/server/handlers/sse-handler.ts +131 -164
  72. package/src/server/mcp/oauth-client.ts +32 -2
  73. package/src/server/storage/index.ts +17 -1
  74. package/src/server/storage/sqlite-backend.ts +185 -0
  75. package/src/server/storage/types.ts +1 -1
  76. package/src/shared/events.ts +12 -0
  77. package/src/shared/types.ts +4 -2
@@ -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 = new URL(this.options.url, typeof window !== "undefined" ? window.location.origin : void 0);
34
- url.searchParams.set("identity", this.options.identity);
35
- if (this.options.authToken) {
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
- for (const [id, { reject }] of this.pendingRequests.entries()) {
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
- * Send RPC request via SSE
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
- handleRpcResponse(response) {
140
- const pending = this.pendingRequests.get(response.id);
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
- * Get all user sessions
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
- * Call a tool
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
- async callTool(sessionId, toolName, toolArgs) {
178
- return this.sendRequest("callTool", { sessionId, toolName, toolArgs });
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
- * Refresh/validate a session
126
+ * Get a preloaded resource from cache, or fetch if not cached.
182
127
  */
183
- async restoreSession(sessionId) {
184
- return this.sendRequest("restoreSession", { sessionId });
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
- * Complete OAuth authorization
140
+ * Check if a resource is already cached
188
141
  */
189
- async finishAuth(sessionId, code) {
190
- return this.sendRequest("finishAuth", { sessionId, code });
142
+ hasPreloadedResource(uri) {
143
+ return this.resourceCache.has(uri);
191
144
  }
192
145
  /**
193
- * List available prompts
146
+ * Clear the resource cache
194
147
  */
195
- async listPrompts(sessionId) {
196
- return this.sendRequest("listPrompts", { sessionId });
148
+ clearResourceCache() {
149
+ this.resourceCache.clear();
197
150
  }
151
+ // ============================================
152
+ // Private: Request Handling
153
+ // ============================================
198
154
  /**
199
- * Get a specific prompt with arguments
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 getPrompt(sessionId, name, args) {
202
- return this.sendRequest("getPrompt", { sessionId, name, args });
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
- * List available resources
179
+ * Parse RPC response and handle different response formats
206
180
  */
207
- async listResources(sessionId) {
208
- return this.sendRequest("listResources", { sessionId });
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
- * Read a specific resource
194
+ * Wait for RPC response via SSE (legacy fallback)
212
195
  */
213
- async readResource(sessionId, uri) {
214
- return this.sendRequest("readResource", { sessionId, uri });
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
- * Check if connected
211
+ * Handle RPC response received via SSE (legacy)
218
212
  */
219
- isConnected() {
220
- return this.eventSource !== null && this.eventSource.readyState === EventSource.OPEN;
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