@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
@@ -0,0 +1,417 @@
1
+ /**
2
+ * MCP App Host
3
+ *
4
+ * Bridges the gap between an iframe (MCP App) and the SSEClient (MCP Server).
5
+ * Handles secure iframe sandboxing, resource loading, and bi-directional
6
+ * communication via the AppBridge protocol.
7
+ *
8
+ * Key features:
9
+ * - Secure iframe sandboxing with minimal permissions
10
+ * - Resource preloading for instant MCP App UI loading
11
+ * - Cache-aware resource fetching (SSEClient cache → local cache → direct fetch)
12
+ * - Support for ui:// and mcp-app:// resource URIs
13
+ */
14
+
15
+ import { AppBridge, PostMessageTransport } from '@modelcontextprotocol/ext-apps/app-bridge';
16
+ import type { AppHostClient } from './types';
17
+
18
+ // ============================================
19
+ // Types & Interfaces
20
+ // ============================================
21
+
22
+ export interface AppHostOptions {
23
+ /** Enable debug logging @default false */
24
+ debug?: boolean;
25
+ }
26
+
27
+ export interface AppMessageParams {
28
+ role: string;
29
+ content: unknown;
30
+ }
31
+
32
+ interface ToolCallParams {
33
+ name: string;
34
+ arguments?: Record<string, unknown>;
35
+ }
36
+
37
+ interface ResourceContent {
38
+ blob?: string;
39
+ text?: string;
40
+ }
41
+
42
+ interface ResourceResponse {
43
+ contents: ResourceContent[];
44
+ }
45
+
46
+ // ============================================
47
+ // Constants
48
+ // ============================================
49
+
50
+ const HOST_INFO = { name: 'mcp-ts-host', version: '1.0.0' };
51
+
52
+ /** Sandbox permissions - minimal set required for MCP Apps to function */
53
+ const SANDBOX_PERMISSIONS = [
54
+ 'allow-scripts', // Required for app JavaScript execution
55
+ 'allow-forms', // Required for form submissions
56
+ 'allow-same-origin', // Required for Blob URL correctness
57
+ 'allow-modals', // Required for dialogs/alerts
58
+ 'allow-popups', // Required for opening links
59
+ 'allow-downloads' // Required for file downloads
60
+ ].join(' ');
61
+
62
+ /** Supported MCP App URI schemes */
63
+ const MCP_URI_SCHEMES = ['ui://', 'mcp-app://'] as const;
64
+
65
+ // ============================================
66
+ // AppHost Class
67
+ // ============================================
68
+
69
+ /**
70
+ * Host for MCP Apps embedded in iframes.
71
+ * Manages secure communication between the app and the MCP server.
72
+ */
73
+ export class AppHost {
74
+ private bridge: AppBridge;
75
+ private sessionId?: string;
76
+ private resourceCache = new Map<string, Promise<ResourceResponse | null>>();
77
+ private debug: boolean;
78
+
79
+ /** Callback for app messages (e.g., chat messages from the app) */
80
+ public onAppMessage?: (params: AppMessageParams) => void;
81
+
82
+ constructor(
83
+ private readonly client: AppHostClient,
84
+ private readonly iframe: HTMLIFrameElement,
85
+ options?: AppHostOptions
86
+ ) {
87
+ this.debug = options?.debug ?? false;
88
+ this.configureSandbox();
89
+ this.bridge = this.initializeBridge();
90
+ }
91
+
92
+ // ============================================
93
+ // Public API
94
+ // ============================================
95
+
96
+ /**
97
+ * Start the host. This prepares the bridge handlers but doesn't connect yet.
98
+ * The actual connection happens in launch() after HTML is loaded.
99
+ * @returns Promise that resolves immediately (bridge connects during launch)
100
+ */
101
+ async start(): Promise<void> {
102
+ // Bridge handlers are already registered in constructor.
103
+ // Connection happens in launch() after HTML is loaded.
104
+ this.log('Host started, ready to launch');
105
+ }
106
+
107
+ /**
108
+ * Preload UI resources to enable instant app loading.
109
+ * Call this when tools are discovered to cache their UI resources.
110
+ */
111
+ preload(tools: Array<{ _meta?: unknown }>): void {
112
+ for (const tool of tools) {
113
+ const uri = this.extractUiResourceUri(tool);
114
+ if (!uri || this.resourceCache.has(uri)) continue;
115
+
116
+ const promise = this.preloadResource(uri);
117
+ this.resourceCache.set(uri, promise);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Launch an MCP App from a URL or MCP resource URI.
123
+ * Loads the HTML first, then establishes bridge connection.
124
+ */
125
+ async launch(url: string, sessionId?: string): Promise<void> {
126
+ if (sessionId) this.sessionId = sessionId;
127
+
128
+ // Set up initialization promise BEFORE connecting
129
+ const initializedPromise = this.onAppReady();
130
+
131
+ // Load HTML into iframe first
132
+ if (this.isMcpUri(url)) {
133
+ await this.launchMcpApp(url);
134
+ } else {
135
+ this.iframe.src = url;
136
+ }
137
+
138
+ // Wait for iframe to load before connecting bridge
139
+ await this.onIframeReady();
140
+
141
+ // Connect the bridge (HTML is loaded, contentWindow is ready)
142
+ await this.connectBridge();
143
+
144
+ // Wait for app to signal it's initialized (with timeout)
145
+ this.log('Waiting for app initialization');
146
+ await Promise.race([
147
+ initializedPromise,
148
+ new Promise<void>((resolve) => setTimeout(() => {
149
+ this.log('Initialization timeout - continuing anyway', 'warn');
150
+ resolve();
151
+ }, 3000))
152
+ ]);
153
+ this.log('App launched and ready');
154
+ }
155
+
156
+ /**
157
+ * Wait for app to signal initialization complete
158
+ */
159
+ private onAppReady(): Promise<void> {
160
+ return new Promise<void>((resolve) => {
161
+ const originalHandler = this.bridge.oninitialized;
162
+ this.bridge.oninitialized = (...args) => {
163
+ this.log('App initialized');
164
+ resolve();
165
+ this.bridge.oninitialized = originalHandler;
166
+ originalHandler?.(...args);
167
+ };
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Wait for iframe to finish loading
173
+ */
174
+ private onIframeReady(): Promise<void> {
175
+ return new Promise((resolve) => {
176
+ if (this.iframe.contentDocument?.readyState === 'complete') {
177
+ resolve();
178
+ return;
179
+ }
180
+ this.iframe.addEventListener('load', () => resolve(), { once: true });
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Send tool input arguments to the MCP App.
186
+ * Call this after launch() when tool input is available.
187
+ */
188
+ sendToolInput(args: Record<string, unknown>): void {
189
+ this.log('Sending tool input to app');
190
+ this.bridge.sendToolInput({ arguments: args });
191
+ }
192
+
193
+ /**
194
+ * Send tool result to the MCP App.
195
+ * Call this when the tool call completes.
196
+ */
197
+ sendToolResult(result: unknown): void {
198
+ this.log('Sending tool result to app');
199
+ this.bridge.sendToolResult(result as any);
200
+ }
201
+
202
+ /**
203
+ * Send tool cancellation to the MCP App.
204
+ * Call this when the tool call is cancelled or fails.
205
+ */
206
+ sendToolCancelled(reason: string): void {
207
+ this.log('Sending tool cancellation to app');
208
+ this.bridge.sendToolCancelled({ reason });
209
+ }
210
+
211
+ // ============================================
212
+ // Private: Initialization
213
+ // ============================================
214
+
215
+ private configureSandbox(): void {
216
+ if (this.iframe.sandbox.value !== SANDBOX_PERMISSIONS) {
217
+ this.iframe.sandbox.value = SANDBOX_PERMISSIONS;
218
+ }
219
+ }
220
+
221
+ private initializeBridge(): AppBridge {
222
+ const bridge = new AppBridge(
223
+ null,
224
+ HOST_INFO,
225
+ {
226
+ openLinks: {},
227
+ serverTools: {},
228
+ logging: {},
229
+ // Declare support for model context updates
230
+ updateModelContext: { text: {} },
231
+ },
232
+ {
233
+ // Initial host context
234
+ hostContext: {
235
+ theme: 'dark',
236
+ platform: 'web',
237
+ containerDimensions: { maxHeight: 6000 },
238
+ displayMode: 'inline',
239
+ availableDisplayModes: ['inline', 'fullscreen'],
240
+ },
241
+ }
242
+ );
243
+
244
+ // Register handlers - must be done BEFORE connect()
245
+ bridge.oncalltool = (params) => this.handleToolCall(params);
246
+ bridge.onopenlink = this.handleOpenLink.bind(this);
247
+ bridge.onmessage = this.handleMessage.bind(this);
248
+ bridge.onloggingmessage = (params) => this.log(`App log [${params.level}]: ${params.data}`);
249
+ bridge.onupdatemodelcontext = async () => ({});
250
+ bridge.onsizechange = async ({ width, height }) => {
251
+ if (height !== undefined) this.iframe.style.height = `${height}px`;
252
+ if (width !== undefined) this.iframe.style.minWidth = `min(${width}px, 100%)`;
253
+ return {};
254
+ };
255
+ bridge.onrequestdisplaymode = async (params) => ({
256
+ mode: params.mode === 'fullscreen' ? 'fullscreen' : 'inline'
257
+ });
258
+
259
+ return bridge;
260
+ }
261
+
262
+ private async connectBridge(): Promise<void> {
263
+ this.log('Connecting bridge to iframe');
264
+
265
+ const transport = new PostMessageTransport(
266
+ this.iframe.contentWindow!,
267
+ this.iframe.contentWindow!
268
+ );
269
+
270
+ try {
271
+ await this.bridge.connect(transport);
272
+ this.log('Bridge connected successfully');
273
+ } catch (error) {
274
+ this.log('Bridge connection failed', 'error');
275
+ throw error;
276
+ }
277
+ }
278
+
279
+ // ============================================
280
+ // Private: Bridge Event Handlers
281
+ // ============================================
282
+
283
+ private async handleToolCall(params: ToolCallParams) {
284
+ if (!this.client.isConnected()) {
285
+ throw new Error('Client disconnected');
286
+ }
287
+
288
+ const sessionId = await this.getSessionId();
289
+ if (!sessionId) {
290
+ throw new Error('No active session');
291
+ }
292
+
293
+ const result = await this.client.callTool(
294
+ sessionId,
295
+ params.name,
296
+ params.arguments ?? {}
297
+ );
298
+ return result as any;
299
+ }
300
+
301
+ private async handleOpenLink(params: { url: string }): Promise<Record<string, never>> {
302
+ window.open(params.url, '_blank', 'noopener,noreferrer');
303
+ return {};
304
+ }
305
+
306
+ private async handleMessage(params: AppMessageParams): Promise<Record<string, never>> {
307
+ this.onAppMessage?.(params);
308
+ return {};
309
+ }
310
+
311
+ // ============================================
312
+ // Private: Resource Loading
313
+ // ============================================
314
+
315
+ private async launchMcpApp(uri: string): Promise<void> {
316
+ if (!this.client.isConnected()) {
317
+ throw new Error('Client must be connected');
318
+ }
319
+
320
+ const sessionId = await this.getSessionId();
321
+ if (!sessionId) {
322
+ throw new Error('No active session');
323
+ }
324
+
325
+ // Fetch resource using cache hierarchy: SSEClient cache → local cache → direct fetch
326
+ const response = await this.fetchResourceWithCache(sessionId, uri);
327
+ if (!response?.contents?.length) {
328
+ throw new Error(`Empty resource: ${uri}`);
329
+ }
330
+
331
+ const content = response.contents[0];
332
+ const html = this.decodeContent(content);
333
+ if (!html) {
334
+ throw new Error(`Invalid content in resource: ${uri}`);
335
+ }
336
+
337
+ // Render via Blob URL for clean isolation
338
+ const blob = new Blob([html], { type: 'text/html' });
339
+ this.iframe.src = URL.createObjectURL(blob);
340
+ }
341
+
342
+ private async fetchResourceWithCache(sessionId: string, uri: string): Promise<ResourceResponse> {
343
+ // Priority 1: SSEClient's built-in cache (best performance)
344
+ if (this.hasClientCache()) {
345
+ return (this.client as any).getOrFetchResource(sessionId, uri);
346
+ }
347
+
348
+ // Priority 2: Local preload cache
349
+ const cached = this.resourceCache.get(uri);
350
+ if (cached) {
351
+ const result = await cached;
352
+ if (result) return result;
353
+ }
354
+
355
+ // Priority 3: Direct fetch
356
+ return this.client.readResource(sessionId, uri) as Promise<ResourceResponse>;
357
+ }
358
+
359
+ private async preloadResource(uri: string): Promise<ResourceResponse | null> {
360
+ try {
361
+ const sessionId = await this.getSessionId();
362
+ if (!sessionId) return null;
363
+ return await this.client.readResource(sessionId, uri) as ResourceResponse;
364
+ } catch (error) {
365
+ this.log(`Preload failed for ${uri}`, 'warn');
366
+ return null;
367
+ }
368
+ }
369
+
370
+ // ============================================
371
+ // Private: Utilities
372
+ // ============================================
373
+
374
+ private async getSessionId(): Promise<string | undefined> {
375
+ if (this.sessionId) return this.sessionId;
376
+ const result = await this.client.getSessions();
377
+ return result.sessions?.[0]?.sessionId;
378
+ }
379
+
380
+ private isMcpUri(url: string): boolean {
381
+ return MCP_URI_SCHEMES.some(scheme => url.startsWith(scheme));
382
+ }
383
+
384
+ private hasClientCache(): boolean {
385
+ return 'getOrFetchResource' in this.client &&
386
+ typeof (this.client as any).getOrFetchResource === 'function';
387
+ }
388
+
389
+ private extractUiResourceUri(tool: { _meta?: unknown }): string | undefined {
390
+ const meta = tool._meta as { ui?: { resourceUri?: string; uri?: string } } | undefined;
391
+ if (!meta?.ui) return undefined;
392
+ return meta.ui.resourceUri ?? meta.ui.uri;
393
+ }
394
+
395
+ private decodeContent(content: ResourceContent): string | undefined {
396
+ if (content.blob) {
397
+ return atob(content.blob);
398
+ }
399
+ return content.text;
400
+ }
401
+
402
+ private log(message: string, level: 'info' | 'warn' | 'error' = 'info'): void {
403
+ if (!this.debug && level === 'info') return;
404
+
405
+ const prefix = '[AppHost]';
406
+ switch (level) {
407
+ case 'warn':
408
+ console.warn(prefix, message);
409
+ break;
410
+ case 'error':
411
+ console.error(prefix, message);
412
+ break;
413
+ default:
414
+ console.log(prefix, message);
415
+ }
416
+ }
417
+ }