@mcp-ts/sdk 1.0.1 → 1.2.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 (83) hide show
  1. package/README.md +42 -19
  2. package/dist/adapters/agui-adapter.d.mts +6 -6
  3. package/dist/adapters/agui-adapter.d.ts +6 -6
  4. package/dist/adapters/agui-adapter.js +50 -25
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +50 -25
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +23 -8
  9. package/dist/adapters/agui-middleware.d.ts +23 -8
  10. package/dist/adapters/agui-middleware.js +71 -43
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs +71 -44
  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 +182 -55
  21. package/dist/client/index.d.ts +182 -55
  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 +386 -4
  27. package/dist/client/react.d.ts +386 -4
  28. package/dist/client/react.js +890 -143
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +883 -145
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +3 -3
  33. package/dist/client/vue.d.ts +3 -3
  34. package/dist/client/vue.js +546 -141
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +546 -142
  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 +797 -248
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +791 -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 +41 -37
  49. package/dist/server/index.d.ts +41 -37
  50. package/dist/server/index.js +241 -116
  51. package/dist/server/index.js.map +1 -1
  52. package/dist/server/index.mjs +237 -112
  53. package/dist/server/index.mjs.map +1 -1
  54. package/dist/shared/index.d.mts +39 -3
  55. package/dist/shared/index.d.ts +39 -3
  56. package/dist/shared/index.js +19 -0
  57. package/dist/shared/index.js.map +1 -1
  58. package/dist/shared/index.mjs +18 -1
  59. package/dist/shared/index.mjs.map +1 -1
  60. package/package.json +9 -2
  61. package/src/adapters/agui-adapter.ts +58 -35
  62. package/src/adapters/agui-middleware.ts +83 -45
  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/agui-subscriber.ts +275 -0
  68. package/src/client/react/index.ts +23 -3
  69. package/src/client/react/use-agui-subscriber.ts +270 -0
  70. package/src/client/react/use-app-host.ts +73 -0
  71. package/src/client/react/use-mcp-app-iframe.ts +164 -0
  72. package/src/client/react/{useMcp.ts → use-mcp.ts} +18 -0
  73. package/src/client/vue/index.ts +1 -1
  74. package/src/server/handlers/nextjs-handler.ts +8 -7
  75. package/src/server/handlers/sse-handler.ts +129 -165
  76. package/src/server/mcp/oauth-client.ts +32 -2
  77. package/src/server/storage/index.ts +17 -1
  78. package/src/server/storage/sqlite-backend.ts +185 -0
  79. package/src/shared/events.ts +12 -0
  80. package/src/shared/index.ts +6 -1
  81. package/src/shared/tool-utils.ts +61 -0
  82. package/src/shared/types.ts +3 -1
  83. /package/src/client/vue/{useMcp.ts → use-mcp.ts} +0 -0
@@ -1,24 +1,30 @@
1
1
  'use strict';
2
2
 
3
3
  var nanoid = require('nanoid');
4
+ var appBridge = require('@modelcontextprotocol/ext-apps/app-bridge');
4
5
 
5
6
  var __defProp = Object.defineProperty;
6
7
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
8
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
9
+ var DEFAULT_REQUEST_TIMEOUT = 6e4;
10
+ var MAX_RECONNECT_ATTEMPTS = 5;
11
+ var BASE_RECONNECT_DELAY = 1e3;
8
12
  var SSEClient = class {
9
13
  constructor(options) {
10
14
  this.options = options;
11
15
  __publicField(this, "eventSource", null);
12
16
  __publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
17
+ __publicField(this, "resourceCache", /* @__PURE__ */ new Map());
13
18
  __publicField(this, "reconnectAttempts", 0);
14
- __publicField(this, "maxReconnectAttempts", 5);
15
- __publicField(this, "reconnectDelay", 1e3);
16
19
  __publicField(this, "isManuallyDisconnected", false);
17
20
  __publicField(this, "connectionPromise", null);
18
21
  __publicField(this, "connectionResolver", null);
19
22
  }
23
+ // ============================================
24
+ // Connection Management
25
+ // ============================================
20
26
  /**
21
- * Connect to SSE endpoint
27
+ * Connect to the SSE endpoint
22
28
  */
23
29
  connect() {
24
30
  if (this.eventSource) {
@@ -29,52 +35,12 @@ var SSEClient = class {
29
35
  this.connectionPromise = new Promise((resolve) => {
30
36
  this.connectionResolver = resolve;
31
37
  });
32
- const url = new URL(this.options.url, typeof window !== "undefined" ? window.location.origin : void 0);
33
- url.searchParams.set("identity", this.options.identity);
34
- if (this.options.authToken) {
35
- url.searchParams.set("token", this.options.authToken);
36
- }
37
- this.eventSource = new EventSource(url.toString());
38
- this.eventSource.addEventListener("open", () => {
39
- console.log("[SSEClient] Connected");
40
- this.reconnectAttempts = 0;
41
- this.options.onStatusChange?.("connected");
42
- });
43
- this.eventSource.addEventListener("connected", (e) => {
44
- const data = JSON.parse(e.data);
45
- console.log("[SSEClient] Server ready:", data);
46
- if (this.connectionResolver) {
47
- this.connectionResolver();
48
- this.connectionResolver = null;
49
- }
50
- });
51
- this.eventSource.addEventListener("connection", (e) => {
52
- const event = JSON.parse(e.data);
53
- this.options.onConnectionEvent?.(event);
54
- });
55
- this.eventSource.addEventListener("observability", (e) => {
56
- const event = JSON.parse(e.data);
57
- this.options.onObservabilityEvent?.(event);
58
- });
59
- this.eventSource.addEventListener("rpc-response", (e) => {
60
- const response = JSON.parse(e.data);
61
- this.handleRpcResponse(response);
62
- });
63
- this.eventSource.addEventListener("error", () => {
64
- console.error("[SSEClient] Connection error");
65
- this.options.onStatusChange?.("error");
66
- if (!this.isManuallyDisconnected && this.reconnectAttempts < this.maxReconnectAttempts) {
67
- this.reconnectAttempts++;
68
- console.log(`[SSEClient] Reconnecting (attempt ${this.reconnectAttempts})...`);
69
- setTimeout(() => {
70
- this.disconnect();
71
- this.connect();
72
- }, this.reconnectDelay * this.reconnectAttempts);
73
- }
74
- });
38
+ const url = this.buildUrl();
39
+ this.eventSource = new EventSource(url);
40
+ this.setupEventListeners();
75
41
  }
76
42
  /**
77
- * Disconnect from SSE endpoint
43
+ * Disconnect from the SSE endpoint
78
44
  */
79
45
  disconnect() {
80
46
  this.isManuallyDisconnected = true;
@@ -84,142 +50,581 @@ var SSEClient = class {
84
50
  }
85
51
  this.connectionPromise = null;
86
52
  this.connectionResolver = null;
87
- for (const [id, { reject }] of this.pendingRequests.entries()) {
88
- const error = new Error("Connection closed");
89
- error.name = "ConnectionClosedError";
90
- reject(error);
91
- }
92
- this.pendingRequests.clear();
53
+ this.rejectAllPendingRequests(new Error("Connection closed"));
93
54
  this.options.onStatusChange?.("disconnected");
94
55
  }
95
56
  /**
96
- * Send RPC request via SSE
97
- * Note: SSE is unidirectional (server->client), so we need to send requests via POST
57
+ * Check if connected to the SSE endpoint
58
+ */
59
+ isConnected() {
60
+ return this.eventSource?.readyState === EventSource.OPEN;
61
+ }
62
+ // ============================================
63
+ // RPC Methods
64
+ // ============================================
65
+ async getSessions() {
66
+ return this.sendRequest("getSessions");
67
+ }
68
+ async connectToServer(params) {
69
+ return this.sendRequest("connect", params);
70
+ }
71
+ async disconnectFromServer(sessionId) {
72
+ return this.sendRequest("disconnect", { sessionId });
73
+ }
74
+ async listTools(sessionId) {
75
+ return this.sendRequest("listTools", { sessionId });
76
+ }
77
+ async callTool(sessionId, toolName, toolArgs) {
78
+ const result = await this.sendRequest("callTool", { sessionId, toolName, toolArgs });
79
+ this.emitUiEventIfPresent(result, sessionId, toolName);
80
+ return result;
81
+ }
82
+ async restoreSession(sessionId) {
83
+ return this.sendRequest("restoreSession", { sessionId });
84
+ }
85
+ async finishAuth(sessionId, code) {
86
+ return this.sendRequest("finishAuth", { sessionId, code });
87
+ }
88
+ async listPrompts(sessionId) {
89
+ return this.sendRequest("listPrompts", { sessionId });
90
+ }
91
+ async getPrompt(sessionId, name, args) {
92
+ return this.sendRequest("getPrompt", { sessionId, name, args });
93
+ }
94
+ async listResources(sessionId) {
95
+ return this.sendRequest("listResources", { sessionId });
96
+ }
97
+ async readResource(sessionId, uri) {
98
+ return this.sendRequest("readResource", { sessionId, uri });
99
+ }
100
+ // ============================================
101
+ // Resource Preloading (for instant UI loading)
102
+ // ============================================
103
+ /**
104
+ * Preload UI resources for tools that have UI metadata.
105
+ * Call this when tools are discovered to enable instant MCP App UI loading.
106
+ */
107
+ preloadToolUiResources(sessionId, tools) {
108
+ for (const tool of tools) {
109
+ const uri = this.extractUiResourceUri(tool);
110
+ if (!uri) continue;
111
+ if (this.resourceCache.has(uri)) {
112
+ this.log(`Resource already cached: ${uri}`);
113
+ continue;
114
+ }
115
+ this.log(`Preloading UI resource for tool "${tool.name}": ${uri}`);
116
+ const promise = this.sendRequest("readResource", { sessionId, uri }).catch((err) => {
117
+ this.log(`Failed to preload resource ${uri}: ${err.message}`, "warn");
118
+ this.resourceCache.delete(uri);
119
+ return null;
120
+ });
121
+ this.resourceCache.set(uri, promise);
122
+ }
123
+ }
124
+ /**
125
+ * Get a preloaded resource from cache, or fetch if not cached.
126
+ */
127
+ getOrFetchResource(sessionId, uri) {
128
+ const cached = this.resourceCache.get(uri);
129
+ if (cached) {
130
+ this.log(`Cache hit for resource: ${uri}`);
131
+ return cached;
132
+ }
133
+ this.log(`Cache miss, fetching resource: ${uri}`);
134
+ const promise = this.sendRequest("readResource", { sessionId, uri });
135
+ this.resourceCache.set(uri, promise);
136
+ return promise;
137
+ }
138
+ /**
139
+ * Check if a resource is already cached
140
+ */
141
+ hasPreloadedResource(uri) {
142
+ return this.resourceCache.has(uri);
143
+ }
144
+ /**
145
+ * Clear the resource cache
146
+ */
147
+ clearResourceCache() {
148
+ this.resourceCache.clear();
149
+ }
150
+ // ============================================
151
+ // Private: Request Handling
152
+ // ============================================
153
+ /**
154
+ * Send an RPC request and return the response directly from HTTP.
155
+ * This bypasses SSE latency by returning results in the HTTP response body.
98
156
  */
99
157
  async sendRequest(method, params) {
100
158
  if (this.connectionPromise) {
101
159
  await this.connectionPromise;
102
160
  }
103
- const id = `rpc_${nanoid.nanoid(10)}`;
104
161
  const request = {
105
- id,
162
+ id: `rpc_${nanoid.nanoid(10)}`,
106
163
  method,
107
164
  params
108
165
  };
109
- const promise = new Promise((resolve, reject) => {
110
- this.pendingRequests.set(id, { resolve, reject });
111
- setTimeout(() => {
112
- if (this.pendingRequests.has(id)) {
113
- this.pendingRequests.delete(id);
114
- reject(new Error("Request timeout"));
115
- }
116
- }, 3e4);
166
+ const response = await fetch(this.buildUrl(), {
167
+ method: "POST",
168
+ headers: this.buildHeaders(),
169
+ body: JSON.stringify(request)
117
170
  });
118
- try {
119
- const url = new URL(this.options.url, typeof window !== "undefined" ? window.location.origin : void 0);
120
- url.searchParams.set("identity", this.options.identity);
121
- await fetch(url.toString(), {
122
- method: "POST",
123
- headers: {
124
- "Content-Type": "application/json",
125
- ...this.options.authToken && { Authorization: `Bearer ${this.options.authToken}` }
126
- },
127
- body: JSON.stringify(request)
128
- });
129
- } catch (error) {
130
- this.pendingRequests.delete(id);
131
- throw error;
171
+ if (!response.ok) {
172
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
132
173
  }
133
- return promise;
174
+ const data = await response.json();
175
+ return this.parseRpcResponse(data, request.id);
134
176
  }
135
177
  /**
136
- * Handle RPC response
178
+ * Parse RPC response and handle different response formats
137
179
  */
138
- handleRpcResponse(response) {
139
- const pending = this.pendingRequests.get(response.id);
140
- if (pending) {
141
- this.pendingRequests.delete(response.id);
142
- if (response.error) {
143
- pending.reject(new Error(response.error.message));
144
- } else {
145
- pending.resolve(response.result);
146
- }
180
+ parseRpcResponse(data, requestId) {
181
+ if ("result" in data) {
182
+ return data.result;
183
+ }
184
+ if ("error" in data && data.error) {
185
+ throw new Error(data.error.message || "Unknown RPC error");
147
186
  }
187
+ if ("acknowledged" in data) {
188
+ return this.waitForSseResponse(requestId);
189
+ }
190
+ throw new Error("Invalid RPC response format");
148
191
  }
149
192
  /**
150
- * Get all user sessions
193
+ * Wait for RPC response via SSE (legacy fallback)
151
194
  */
152
- async getSessions() {
153
- return this.sendRequest("getSessions");
195
+ waitForSseResponse(requestId) {
196
+ const timeoutMs = this.options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
197
+ return new Promise((resolve, reject) => {
198
+ const timeoutId = setTimeout(() => {
199
+ this.pendingRequests.delete(requestId);
200
+ reject(new Error(`Request timeout after ${timeoutMs}ms`));
201
+ }, timeoutMs);
202
+ this.pendingRequests.set(requestId, {
203
+ resolve,
204
+ reject,
205
+ timeoutId
206
+ });
207
+ });
154
208
  }
155
209
  /**
156
- * Connect to an MCP server
210
+ * Handle RPC response received via SSE (legacy)
157
211
  */
158
- async connectToServer(params) {
159
- return this.sendRequest("connect", params);
212
+ handleRpcResponse(response) {
213
+ const pending = this.pendingRequests.get(response.id);
214
+ if (!pending) return;
215
+ clearTimeout(pending.timeoutId);
216
+ this.pendingRequests.delete(response.id);
217
+ if (response.error) {
218
+ pending.reject(new Error(response.error.message));
219
+ } else {
220
+ pending.resolve(response.result);
221
+ }
160
222
  }
161
- /**
162
- * Disconnect from an MCP server
163
- */
164
- async disconnectFromServer(sessionId) {
165
- return this.sendRequest("disconnect", { sessionId });
223
+ // ============================================
224
+ // Private: Event Handling
225
+ // ============================================
226
+ setupEventListeners() {
227
+ if (!this.eventSource) return;
228
+ this.eventSource.addEventListener("open", () => {
229
+ this.log("Connected");
230
+ this.reconnectAttempts = 0;
231
+ this.options.onStatusChange?.("connected");
232
+ });
233
+ this.eventSource.addEventListener("connected", () => {
234
+ this.log("Server ready");
235
+ this.connectionResolver?.();
236
+ this.connectionResolver = null;
237
+ });
238
+ this.eventSource.addEventListener("connection", (e) => {
239
+ const event = JSON.parse(e.data);
240
+ this.options.onConnectionEvent?.(event);
241
+ });
242
+ this.eventSource.addEventListener("observability", (e) => {
243
+ const event = JSON.parse(e.data);
244
+ this.options.onObservabilityEvent?.(event);
245
+ });
246
+ this.eventSource.addEventListener("rpc-response", (e) => {
247
+ const response = JSON.parse(e.data);
248
+ this.handleRpcResponse(response);
249
+ });
250
+ this.eventSource.addEventListener("error", () => {
251
+ this.log("Connection error", "error");
252
+ this.options.onStatusChange?.("error");
253
+ this.attemptReconnect();
254
+ });
166
255
  }
167
- /**
168
- * List tools from a session
169
- */
170
- async listTools(sessionId) {
171
- return this.sendRequest("listTools", { sessionId });
256
+ attemptReconnect() {
257
+ if (this.isManuallyDisconnected || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
258
+ return;
259
+ }
260
+ this.reconnectAttempts++;
261
+ const delay = BASE_RECONNECT_DELAY * this.reconnectAttempts;
262
+ this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
263
+ setTimeout(() => {
264
+ this.disconnect();
265
+ this.connect();
266
+ }, delay);
267
+ }
268
+ // ============================================
269
+ // Private: Utilities
270
+ // ============================================
271
+ buildUrl() {
272
+ const url = new URL(this.options.url, globalThis.location?.origin);
273
+ url.searchParams.set("identity", this.options.identity);
274
+ if (this.options.authToken) {
275
+ url.searchParams.set("token", this.options.authToken);
276
+ }
277
+ return url.toString();
278
+ }
279
+ buildHeaders() {
280
+ const headers = {
281
+ "Content-Type": "application/json"
282
+ };
283
+ if (this.options.authToken) {
284
+ headers["Authorization"] = `Bearer ${this.options.authToken}`;
285
+ }
286
+ return headers;
287
+ }
288
+ rejectAllPendingRequests(error) {
289
+ for (const [, pending] of this.pendingRequests) {
290
+ clearTimeout(pending.timeoutId);
291
+ pending.reject(error);
292
+ }
293
+ this.pendingRequests.clear();
172
294
  }
295
+ extractUiResourceUri(tool) {
296
+ const meta = tool._meta?.ui;
297
+ if (!meta || typeof meta !== "object") return void 0;
298
+ if (meta.visibility && !meta.visibility.includes("app")) return void 0;
299
+ return meta.resourceUri ?? meta.uri;
300
+ }
301
+ emitUiEventIfPresent(result, sessionId, toolName) {
302
+ const meta = result?._meta;
303
+ const resourceUri = meta?.ui?.resourceUri ?? meta?.["ui/resourceUri"];
304
+ if (resourceUri) {
305
+ this.options.onEvent?.({
306
+ type: "mcp-apps-ui",
307
+ sessionId,
308
+ resourceUri,
309
+ toolName,
310
+ result,
311
+ timestamp: Date.now()
312
+ });
313
+ }
314
+ }
315
+ log(message, level = "info") {
316
+ if (!this.options.debug && level === "info") return;
317
+ const prefix = "[SSEClient]";
318
+ switch (level) {
319
+ case "warn":
320
+ console.warn(prefix, message);
321
+ break;
322
+ case "error":
323
+ console.error(prefix, message);
324
+ break;
325
+ default:
326
+ console.log(prefix, message);
327
+ }
328
+ }
329
+ };
330
+ var HOST_INFO = { name: "mcp-ts-host", version: "1.0.0" };
331
+ var SANDBOX_PERMISSIONS = [
332
+ "allow-scripts",
333
+ // Required for app JavaScript execution
334
+ "allow-forms",
335
+ // Required for form submissions
336
+ "allow-same-origin",
337
+ // Required for Blob URL correctness
338
+ "allow-modals",
339
+ // Required for dialogs/alerts
340
+ "allow-popups",
341
+ // Required for opening links
342
+ "allow-downloads"
343
+ // Required for file downloads
344
+ ].join(" ");
345
+ var MCP_URI_SCHEMES = ["ui://", "mcp-app://"];
346
+ var AppHost = class {
347
+ constructor(client, iframe, options) {
348
+ this.client = client;
349
+ this.iframe = iframe;
350
+ __publicField(this, "bridge");
351
+ __publicField(this, "sessionId");
352
+ __publicField(this, "resourceCache", /* @__PURE__ */ new Map());
353
+ __publicField(this, "debug");
354
+ /** Callback for app messages (e.g., chat messages from the app) */
355
+ __publicField(this, "onAppMessage");
356
+ this.debug = options?.debug ?? false;
357
+ this.configureSandbox();
358
+ this.bridge = this.initializeBridge();
359
+ }
360
+ // ============================================
361
+ // Public API
362
+ // ============================================
173
363
  /**
174
- * Call a tool
364
+ * Start the host. This prepares the bridge handlers but doesn't connect yet.
365
+ * The actual connection happens in launch() after HTML is loaded.
366
+ * @returns Promise that resolves immediately (bridge connects during launch)
175
367
  */
176
- async callTool(sessionId, toolName, toolArgs) {
177
- return this.sendRequest("callTool", { sessionId, toolName, toolArgs });
368
+ async start() {
369
+ this.log("Host started, ready to launch");
178
370
  }
179
371
  /**
180
- * Refresh/validate a session
372
+ * Preload UI resources to enable instant app loading.
373
+ * Call this when tools are discovered to cache their UI resources.
181
374
  */
182
- async restoreSession(sessionId) {
183
- return this.sendRequest("restoreSession", { sessionId });
375
+ preload(tools) {
376
+ for (const tool of tools) {
377
+ const uri = this.extractUiResourceUri(tool);
378
+ if (!uri || this.resourceCache.has(uri)) continue;
379
+ const promise = this.preloadResource(uri);
380
+ this.resourceCache.set(uri, promise);
381
+ }
184
382
  }
185
383
  /**
186
- * Complete OAuth authorization
384
+ * Launch an MCP App from a URL or MCP resource URI.
385
+ * Loads the HTML first, then establishes bridge connection.
187
386
  */
188
- async finishAuth(sessionId, code) {
189
- return this.sendRequest("finishAuth", { sessionId, code });
387
+ async launch(url, sessionId) {
388
+ if (sessionId) this.sessionId = sessionId;
389
+ const initializedPromise = this.onAppReady();
390
+ if (this.isMcpUri(url)) {
391
+ await this.launchMcpApp(url);
392
+ } else {
393
+ this.iframe.src = url;
394
+ }
395
+ await this.onIframeReady();
396
+ await this.connectBridge();
397
+ this.log("Waiting for app initialization");
398
+ await Promise.race([
399
+ initializedPromise,
400
+ new Promise((resolve) => setTimeout(() => {
401
+ this.log("Initialization timeout - continuing anyway", "warn");
402
+ resolve();
403
+ }, 3e3))
404
+ ]);
405
+ this.log("App launched and ready");
190
406
  }
191
407
  /**
192
- * List available prompts
408
+ * Wait for app to signal initialization complete
193
409
  */
194
- async listPrompts(sessionId) {
195
- return this.sendRequest("listPrompts", { sessionId });
410
+ onAppReady() {
411
+ return new Promise((resolve) => {
412
+ const originalHandler = this.bridge.oninitialized;
413
+ this.bridge.oninitialized = (...args) => {
414
+ this.log("App initialized");
415
+ resolve();
416
+ this.bridge.oninitialized = originalHandler;
417
+ originalHandler?.(...args);
418
+ };
419
+ });
196
420
  }
197
421
  /**
198
- * Get a specific prompt with arguments
422
+ * Wait for iframe to finish loading
199
423
  */
200
- async getPrompt(sessionId, name, args) {
201
- return this.sendRequest("getPrompt", { sessionId, name, args });
424
+ onIframeReady() {
425
+ return new Promise((resolve) => {
426
+ if (this.iframe.contentDocument?.readyState === "complete") {
427
+ resolve();
428
+ return;
429
+ }
430
+ this.iframe.addEventListener("load", () => resolve(), { once: true });
431
+ });
202
432
  }
203
433
  /**
204
- * List available resources
434
+ * Send tool input arguments to the MCP App.
435
+ * Call this after launch() when tool input is available.
205
436
  */
206
- async listResources(sessionId) {
207
- return this.sendRequest("listResources", { sessionId });
437
+ sendToolInput(args) {
438
+ this.log("Sending tool input to app");
439
+ this.bridge.sendToolInput({ arguments: args });
208
440
  }
209
441
  /**
210
- * Read a specific resource
442
+ * Send tool result to the MCP App.
443
+ * Call this when the tool call completes.
211
444
  */
212
- async readResource(sessionId, uri) {
213
- return this.sendRequest("readResource", { sessionId, uri });
445
+ sendToolResult(result) {
446
+ this.log("Sending tool result to app");
447
+ this.bridge.sendToolResult(result);
214
448
  }
215
449
  /**
216
- * Check if connected
450
+ * Send tool cancellation to the MCP App.
451
+ * Call this when the tool call is cancelled or fails.
217
452
  */
218
- isConnected() {
219
- return this.eventSource !== null && this.eventSource.readyState === EventSource.OPEN;
453
+ sendToolCancelled(reason) {
454
+ this.log("Sending tool cancellation to app");
455
+ this.bridge.sendToolCancelled({ reason });
456
+ }
457
+ // ============================================
458
+ // Private: Initialization
459
+ // ============================================
460
+ configureSandbox() {
461
+ if (this.iframe.sandbox.value !== SANDBOX_PERMISSIONS) {
462
+ this.iframe.sandbox.value = SANDBOX_PERMISSIONS;
463
+ }
464
+ }
465
+ initializeBridge() {
466
+ const bridge = new appBridge.AppBridge(
467
+ null,
468
+ HOST_INFO,
469
+ {
470
+ openLinks: {},
471
+ serverTools: {},
472
+ logging: {},
473
+ // Declare support for model context updates
474
+ updateModelContext: { text: {} }
475
+ },
476
+ {
477
+ // Initial host context
478
+ hostContext: {
479
+ theme: "dark",
480
+ platform: "web",
481
+ containerDimensions: { maxHeight: 6e3 },
482
+ displayMode: "inline",
483
+ availableDisplayModes: ["inline", "fullscreen"]
484
+ }
485
+ }
486
+ );
487
+ bridge.oncalltool = (params) => this.handleToolCall(params);
488
+ bridge.onopenlink = this.handleOpenLink.bind(this);
489
+ bridge.onmessage = this.handleMessage.bind(this);
490
+ bridge.onloggingmessage = (params) => this.log(`App log [${params.level}]: ${params.data}`);
491
+ bridge.onupdatemodelcontext = async () => ({});
492
+ bridge.onsizechange = async ({ width, height }) => {
493
+ if (height !== void 0) this.iframe.style.height = `${height}px`;
494
+ if (width !== void 0) this.iframe.style.minWidth = `min(${width}px, 100%)`;
495
+ return {};
496
+ };
497
+ bridge.onrequestdisplaymode = async (params) => ({
498
+ mode: params.mode === "fullscreen" ? "fullscreen" : "inline"
499
+ });
500
+ return bridge;
501
+ }
502
+ async connectBridge() {
503
+ this.log("Connecting bridge to iframe");
504
+ const transport = new appBridge.PostMessageTransport(
505
+ this.iframe.contentWindow,
506
+ this.iframe.contentWindow
507
+ );
508
+ try {
509
+ await this.bridge.connect(transport);
510
+ this.log("Bridge connected successfully");
511
+ } catch (error) {
512
+ this.log("Bridge connection failed", "error");
513
+ throw error;
514
+ }
515
+ }
516
+ // ============================================
517
+ // Private: Bridge Event Handlers
518
+ // ============================================
519
+ async handleToolCall(params) {
520
+ if (!this.client.isConnected()) {
521
+ throw new Error("Client disconnected");
522
+ }
523
+ const sessionId = await this.getSessionId();
524
+ if (!sessionId) {
525
+ throw new Error("No active session");
526
+ }
527
+ const result = await this.client.callTool(
528
+ sessionId,
529
+ params.name,
530
+ params.arguments ?? {}
531
+ );
532
+ return result;
533
+ }
534
+ async handleOpenLink(params) {
535
+ window.open(params.url, "_blank", "noopener,noreferrer");
536
+ return {};
537
+ }
538
+ async handleMessage(params) {
539
+ this.onAppMessage?.(params);
540
+ return {};
541
+ }
542
+ // ============================================
543
+ // Private: Resource Loading
544
+ // ============================================
545
+ async launchMcpApp(uri) {
546
+ if (!this.client.isConnected()) {
547
+ throw new Error("Client must be connected");
548
+ }
549
+ const sessionId = await this.getSessionId();
550
+ if (!sessionId) {
551
+ throw new Error("No active session");
552
+ }
553
+ const response = await this.fetchResourceWithCache(sessionId, uri);
554
+ if (!response?.contents?.length) {
555
+ throw new Error(`Empty resource: ${uri}`);
556
+ }
557
+ const content = response.contents[0];
558
+ const html = this.decodeContent(content);
559
+ if (!html) {
560
+ throw new Error(`Invalid content in resource: ${uri}`);
561
+ }
562
+ const blob = new Blob([html], { type: "text/html" });
563
+ this.iframe.src = URL.createObjectURL(blob);
564
+ }
565
+ async fetchResourceWithCache(sessionId, uri) {
566
+ if (this.hasClientCache()) {
567
+ return this.client.getOrFetchResource(sessionId, uri);
568
+ }
569
+ const cached = this.resourceCache.get(uri);
570
+ if (cached) {
571
+ const result = await cached;
572
+ if (result) return result;
573
+ }
574
+ return this.client.readResource(sessionId, uri);
575
+ }
576
+ async preloadResource(uri) {
577
+ try {
578
+ const sessionId = await this.getSessionId();
579
+ if (!sessionId) return null;
580
+ return await this.client.readResource(sessionId, uri);
581
+ } catch (error) {
582
+ this.log(`Preload failed for ${uri}`, "warn");
583
+ return null;
584
+ }
585
+ }
586
+ // ============================================
587
+ // Private: Utilities
588
+ // ============================================
589
+ async getSessionId() {
590
+ if (this.sessionId) return this.sessionId;
591
+ const result = await this.client.getSessions();
592
+ return result.sessions?.[0]?.sessionId;
593
+ }
594
+ isMcpUri(url) {
595
+ return MCP_URI_SCHEMES.some((scheme) => url.startsWith(scheme));
596
+ }
597
+ hasClientCache() {
598
+ return "getOrFetchResource" in this.client && typeof this.client.getOrFetchResource === "function";
599
+ }
600
+ extractUiResourceUri(tool) {
601
+ const meta = tool._meta;
602
+ if (!meta?.ui) return void 0;
603
+ return meta.ui.resourceUri ?? meta.ui.uri;
604
+ }
605
+ decodeContent(content) {
606
+ if (content.blob) {
607
+ return atob(content.blob);
608
+ }
609
+ return content.text;
610
+ }
611
+ log(message, level = "info") {
612
+ if (!this.debug && level === "info") return;
613
+ const prefix = "[AppHost]";
614
+ switch (level) {
615
+ case "warn":
616
+ console.warn(prefix, message);
617
+ break;
618
+ case "error":
619
+ console.error(prefix, message);
620
+ break;
621
+ default:
622
+ console.log(prefix, message);
623
+ }
220
624
  }
221
625
  };
222
626
 
627
+ exports.AppHost = AppHost;
223
628
  exports.SSEClient = SSEClient;
224
629
  //# sourceMappingURL=index.js.map
225
630
  //# sourceMappingURL=index.js.map