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