@mcp-ts/sdk 1.3.10 → 1.4.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 (44) hide show
  1. package/dist/adapters/langchain-adapter.js.map +1 -1
  2. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  3. package/dist/client/index.d.mts +3 -189
  4. package/dist/client/index.d.ts +3 -189
  5. package/dist/client/index.js +218 -54
  6. package/dist/client/index.js.map +1 -1
  7. package/dist/client/index.mjs +215 -55
  8. package/dist/client/index.mjs.map +1 -1
  9. package/dist/client/react.d.mts +21 -14
  10. package/dist/client/react.d.ts +21 -14
  11. package/dist/client/react.js +402 -83
  12. package/dist/client/react.js.map +1 -1
  13. package/dist/client/react.mjs +400 -85
  14. package/dist/client/react.mjs.map +1 -1
  15. package/dist/client/vue.d.mts +3 -2
  16. package/dist/client/vue.d.ts +3 -2
  17. package/dist/client/vue.js +239 -63
  18. package/dist/client/vue.js.map +1 -1
  19. package/dist/client/vue.mjs +236 -64
  20. package/dist/client/vue.mjs.map +1 -1
  21. package/dist/index-CQr9q0bF.d.mts +295 -0
  22. package/dist/index-nE_7Io0I.d.ts +295 -0
  23. package/dist/index.d.mts +2 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +237 -58
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +230 -59
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/server/index.js +15 -4
  30. package/dist/server/index.js.map +1 -1
  31. package/dist/server/index.mjs +15 -4
  32. package/dist/server/index.mjs.map +1 -1
  33. package/package.json +13 -11
  34. package/src/adapters/langchain-adapter.ts +1 -1
  35. package/src/client/core/app-host.ts +252 -65
  36. package/src/client/core/constants.ts +30 -0
  37. package/src/client/index.ts +6 -1
  38. package/src/client/react/index.ts +1 -0
  39. package/src/client/react/use-app-host.ts +8 -15
  40. package/src/client/react/use-mcp-apps.tsx +221 -26
  41. package/src/client/react/use-mcp.ts +23 -12
  42. package/src/client/utils/app-host-utils.ts +62 -0
  43. package/src/client/vue/use-mcp.ts +23 -12
  44. package/src/server/mcp/oauth-client.ts +31 -8
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@mcp-ts/sdk",
3
- "version": "1.3.10",
3
+ "version": "1.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "A Lightweight MCP (Model Context Protocol) client library for JavaScript applications, supporting multiple storage backends (Memory, File, Redis, Supabase) and designed for both server and client environments.",
7
+ "description": "A lightweight MCP (Model Context Protocol) client library for JavaScript and cross-runtime environments, supporting MCP Apps in host applications and multiple storage backends (Memory, File, Redis, Supabase).",
8
8
  "main": "./dist/index.js",
9
9
  "module": "./dist/index.mjs",
10
10
  "types": "./dist/index.d.ts",
@@ -113,14 +113,14 @@
113
113
  "homepage": "https://github.com/zonlabs/mcp-ts#readme",
114
114
  "peerDependencies": {
115
115
  "@ag-ui/client": ">=0.0.40",
116
- "@langchain/core": "^0.3.0",
116
+ "@langchain/core": "^1.1.39",
117
+ "@supabase/supabase-js": "^2.0.0",
117
118
  "ai": "^6.0.0",
118
119
  "better-sqlite3": "^12.0.0",
119
120
  "ioredis": "^5.0.0",
120
121
  "react": ">=18.0.0",
121
122
  "rxjs": ">=7.0.0",
122
- "zod": "^3.23.0",
123
- "@supabase/supabase-js": "^2.0.0"
123
+ "zod": "^3.23.0"
124
124
  },
125
125
  "peerDependenciesMeta": {
126
126
  "react": {
@@ -152,34 +152,36 @@
152
152
  }
153
153
  },
154
154
  "dependencies": {
155
- "@modelcontextprotocol/ext-apps": "^1.0.1",
156
- "@modelcontextprotocol/sdk": "^1.25.3",
155
+ "@modelcontextprotocol/ext-apps": "^1.5.0",
156
+ "@modelcontextprotocol/sdk": "^1.29.0",
157
157
  "json-schema": "^0.4.0",
158
158
  "json-schema-to-zod": "^2.7.0",
159
159
  "nanoid": "^5.1.6"
160
160
  },
161
161
  "devDependencies": {
162
162
  "@ag-ui/client": "^0.0.42",
163
- "@langchain/core": "^0.3.33",
163
+ "@langchain/core": "^1.1.39",
164
164
  "@playwright/test": "^1.58.0",
165
+ "@supabase/supabase-js": "^2.48.0",
165
166
  "@types/better-sqlite3": "^7.6.13",
166
167
  "@types/json-schema": "^7.0.15",
167
168
  "@types/node": "^25.0.10",
168
169
  "@types/react": "^18.3.18",
170
+ "@types/react-dom": "^18.3.7",
169
171
  "ai": "^6.0.49",
170
172
  "better-sqlite3": "^12.6.2",
171
173
  "ioredis": "^5.9.2",
172
174
  "ioredis-mock": "^8.13.1",
173
175
  "playwright": "^1.58.0",
174
176
  "react": "^18.3.1",
177
+ "react-dom": "^18.3.1",
175
178
  "rimraf": "^6.1.2",
176
179
  "tsup": "^8.5.1",
177
180
  "typescript": "^5.9.3",
178
181
  "vue": "^3.5.27",
179
- "zod": "^3.24.1",
180
- "@supabase/supabase-js": "^2.48.0"
182
+ "zod": "^3.24.1"
181
183
  },
182
184
  "engines": {
183
185
  "node": ">=18.0.0"
184
186
  }
185
- }
187
+ }
@@ -37,7 +37,7 @@ export class LangChainAdapter {
37
37
  if (!this.DynamicStructuredTool) {
38
38
  try {
39
39
  const langchain = await import('@langchain/core/tools');
40
- this.DynamicStructuredTool = langchain.DynamicStructuredTool;
40
+ this.DynamicStructuredTool = langchain.DynamicStructuredTool as any;
41
41
 
42
42
  const zod = await import('zod');
43
43
  this.z = zod.z;
@@ -6,22 +6,119 @@
6
6
  * communication via the AppBridge protocol.
7
7
  *
8
8
  * Key features:
9
- * - Secure iframe sandboxing with minimal permissions
9
+ * - Secure iframe sandboxing with minimal permissions (proxy-based)
10
10
  * - Resource preloading for instant MCP App UI loading
11
11
  * - Cache-aware resource fetching (SSEClient cache → local cache → direct fetch)
12
12
  * - Support for ui:// and mcp-app:// resource URIs
13
13
  */
14
14
 
15
- import { AppBridge, PostMessageTransport } from '@modelcontextprotocol/ext-apps/app-bridge';
15
+ import {
16
+ AppBridge,
17
+ PostMessageTransport
18
+ } from '@modelcontextprotocol/ext-apps/app-bridge';
19
+ import type { LoggingMessageNotification } from '@modelcontextprotocol/sdk/types.js';
16
20
  import type { AppHostClient } from './types';
21
+ import { setupSandboxProxyIframe } from '../utils/app-host-utils.js';
22
+ import { APP_HOST_DEFAULTS } from './constants.js';
23
+
24
+ export type McpUiResourceCsp = Record<string, string>;
25
+ export type McpUiHostContext = Record<string, unknown>;
26
+
27
+ // Define types dynamically from AppBridge properties instead of direct imports
28
+ // which seem to fail in this tsconfig environment
29
+ type OnMessageHandler = NonNullable<AppBridge['onmessage']>;
30
+ export type McpUiMessageParams = Parameters<OnMessageHandler>[0];
31
+ export type RequestHandlerExtra = Parameters<OnMessageHandler>[1];
32
+ export type McpUiMessageResult = ReturnType<OnMessageHandler> extends Promise<infer R> ? R : never;
33
+
34
+ type OnOpenLinkHandler = NonNullable<AppBridge['onopenlink']>;
35
+ export type McpUiOpenLinkParams = Parameters<OnOpenLinkHandler>[0];
36
+ export type McpUiOpenLinkResult = ReturnType<OnOpenLinkHandler> extends Promise<infer R> ? R : never;
37
+
38
+ type OnSizeChangeHandler = NonNullable<AppBridge['onsizechange']>;
39
+ export type McpUiSizeChangedParams = Parameters<OnSizeChangeHandler>[0];
40
+
41
+ type OnRequestDisplayModeHandler = NonNullable<AppBridge['onrequestdisplaymode']>;
42
+ export type McpUiRequestDisplayModeParams = Parameters<OnRequestDisplayModeHandler>[0];
43
+ export type McpUiRequestDisplayModeResult = ReturnType<OnRequestDisplayModeHandler> extends Promise<infer R> ? R : never;
44
+
17
45
 
18
46
  // ============================================
19
47
  // Types & Interfaces
20
48
  // ============================================
21
49
 
50
+ export interface SandboxConfig {
51
+ url: URL | string;
52
+ permissions?: string;
53
+ csp?: McpUiResourceCsp;
54
+ }
55
+
56
+ /**
57
+ * Default Content-Security-Policy for MCP App iframes.
58
+ *
59
+ * Allows inline scripts/styles (required by most MCP App frameworks),
60
+ * outbound network connections, and common asset sources, while blocking
61
+ * nested frames and plugin objects.
62
+ *
63
+ * Pass this (or a spread of it) as `sandbox.csp` to enforce it:
64
+ * @example
65
+ * sandbox={{ url: '/sandbox.html', csp: DEFAULT_MCP_APP_CSP }}
66
+ * // or to extend:
67
+ * sandbox={{ url: '/sandbox.html', csp: { ...DEFAULT_MCP_APP_CSP, 'connect-src': "'self' https://api.example.com" } }}
68
+ */
69
+ export const DEFAULT_MCP_APP_CSP: McpUiResourceCsp = {
70
+ 'default-src': "'self'",
71
+ 'script-src': "'self' 'unsafe-inline' 'unsafe-eval' https: blob:",
72
+ 'style-src': "'self' 'unsafe-inline' https:",
73
+ 'connect-src': "'self' https: wss:",
74
+ 'img-src': "'self' data: https: blob:",
75
+ 'font-src': "'self' data: https:",
76
+ 'media-src': "'self' https: blob:",
77
+ 'frame-src': "'none'",
78
+ 'object-src': "'none'",
79
+ 'base-uri': "'self'",
80
+ };
81
+
22
82
  export interface AppHostOptions {
23
83
  /** Enable debug logging @default false */
24
84
  debug?: boolean;
85
+ /** Sandbox proxy configuration */
86
+ sandbox?: SandboxConfig;
87
+ /** Host context for theming, viewport, locale */
88
+ hostContext?: McpUiHostContext;
89
+ /** Custom handler for call tool requests, overriding automatic client forwarding */
90
+ onCallTool?: (params: ToolCallParams) => Promise<unknown>;
91
+ /** Custom handler for resources/read */
92
+ onReadResource?: (uri: string) => Promise<ResourceResponse>;
93
+ /** Custom handler for fallback JSON-RPC requests */
94
+ onFallbackRequest?: (request: any) => Promise<any>;
95
+
96
+ /** Handler for open-link requests from the guest UI */
97
+ onOpenLink?: (
98
+ params: McpUiOpenLinkParams,
99
+ extra: RequestHandlerExtra,
100
+ ) => Promise<McpUiOpenLinkResult>;
101
+
102
+ /** Handler for message requests from the guest UI */
103
+ onMessage?: (
104
+ params: McpUiMessageParams,
105
+ extra: RequestHandlerExtra,
106
+ ) => Promise<McpUiMessageResult>;
107
+
108
+ /** Handler for logging messages from the guest UI */
109
+ onLoggingMessage?: (params: LoggingMessageNotification['params']) => void;
110
+
111
+ /** Handler for size change notifications from the guest UI */
112
+ onSizeChanged?: (params: McpUiSizeChangedParams) => void;
113
+
114
+ /** Callback invoked when an error occurs during setup or message handling */
115
+ onError?: (error: Error) => void;
116
+
117
+ /** Handler for display mode change requests from the guest UI */
118
+ onRequestDisplayMode?: (
119
+ params: McpUiRequestDisplayModeParams,
120
+ extra: RequestHandlerExtra,
121
+ ) => Promise<McpUiRequestDisplayModeResult>;
25
122
  }
26
123
 
27
124
  export interface AppMessageParams {
@@ -47,20 +144,11 @@ interface ResourceResponse {
47
144
  // Constants
48
145
  // ============================================
49
146
 
50
- const HOST_INFO = { name: 'mcp-ts-host', version: '1.0.0' };
147
+ const HOST_INFO = APP_HOST_DEFAULTS.HOST_INFO;
51
148
 
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
149
 
62
150
  /** Supported MCP App URI schemes */
63
- const MCP_URI_SCHEMES = ['ui://', 'mcp-app://'] as const;
151
+ const MCP_URI_SCHEMES = APP_HOST_DEFAULTS.URI_SCHEMES;
64
152
 
65
153
  // ============================================
66
154
  // AppHost Class
@@ -76,16 +164,19 @@ export class AppHost {
76
164
  private resourceCache = new Map<string, Promise<ResourceResponse | null>>();
77
165
  private debug: boolean;
78
166
 
79
- /** Callback for app messages (e.g., chat messages from the app) */
167
+ private sandboxConfig?: SandboxConfig;
168
+ private options: AppHostOptions;
80
169
  public onAppMessage?: (params: AppMessageParams) => void;
81
170
 
82
171
  constructor(
83
- private readonly client: AppHostClient,
172
+ private readonly client: AppHostClient | null,
84
173
  private readonly iframe: HTMLIFrameElement,
85
174
  options?: AppHostOptions
86
175
  ) {
87
- this.debug = options?.debug ?? false;
88
- this.configureSandbox();
176
+ this.options = options || {};
177
+ this.debug = this.options.debug ?? false;
178
+ this.sandboxConfig = this.options.sandbox;
179
+
89
180
  this.bridge = this.initializeBridge();
90
181
  }
91
182
 
@@ -119,29 +210,42 @@ export class AppHost {
119
210
  }
120
211
 
121
212
  /**
122
- * Launch an MCP App from a URL or MCP resource URI.
213
+ * Launch an MCP App from a URL, MCP resource URI, or RAW HTML.
123
214
  * Loads the HTML first, then establishes bridge connection.
124
215
  */
125
- async launch(url: string, sessionId?: string): Promise<void> {
216
+ async launch(source: { uri?: string; html?: string }, sessionId?: string): Promise<void> {
126
217
  if (sessionId) this.sessionId = sessionId;
127
218
 
128
- // Set up initialization promise BEFORE connecting
129
219
  const initializedPromise = this.onAppReady();
130
220
 
131
- // Load HTML into iframe first
132
- if (this.isMcpUri(url)) {
133
- await this.launchMcpApp(url);
134
- } else {
135
- this.iframe.src = url;
136
- }
221
+ let htmlToRender = source.html;
137
222
 
138
- // Wait for iframe to load before connecting bridge
139
- await this.onIframeReady();
223
+ if (!htmlToRender && source.uri) {
224
+ if (this.isMcpUri(source.uri)) {
225
+ htmlToRender = await this.readMcpAppHtml(source.uri);
226
+ }
227
+ }
140
228
 
141
- // Connect the bridge (HTML is loaded, contentWindow is ready)
142
- await this.connectBridge();
229
+ if (!htmlToRender && source.uri && !this.isMcpUri(source.uri)) {
230
+ // Fallback for regular urls without proxy
231
+ this.iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads');
232
+ this.iframe.src = source.uri;
233
+ await this.onIframeReady();
234
+ await this.connectBridge();
235
+ } else if (htmlToRender) {
236
+ if (!this.sandboxConfig) {
237
+ throw new Error("Sandbox configuration requires a proxy URL to render HTML safely.");
238
+ }
239
+ await this.launchSandboxedHtml(htmlToRender, this.sandboxConfig);
240
+ await this.connectBridge();
241
+
242
+ this.log('Sending HTML resource to sandbox proxy (MCP Apps notification)');
243
+ await this.bridge.sendSandboxResourceReady({
244
+ html: htmlToRender,
245
+ csp: this.sandboxConfig.csp,
246
+ });
247
+ }
143
248
 
144
- // Wait for app to signal it's initialized (with timeout)
145
249
  this.log('Waiting for app initialization');
146
250
  await Promise.race([
147
251
  initializedPromise,
@@ -153,6 +257,21 @@ export class AppHost {
153
257
  this.log('App launched and ready');
154
258
  }
155
259
 
260
+ // Set host context manually
261
+ setHostContext(context: McpUiHostContext): void {
262
+ this.options.hostContext = context;
263
+ if (this.bridge) {
264
+ this.bridge.setHostContext(context);
265
+ }
266
+ }
267
+
268
+ // Send streaming inputs manually
269
+ sendToolInputPartial(params: any): void {
270
+ if (this.bridge) {
271
+ (this.bridge as any).sendToolInputPartial(params);
272
+ }
273
+ }
274
+
156
275
  /**
157
276
  * Wait for app to signal initialization complete
158
277
  */
@@ -208,15 +327,19 @@ export class AppHost {
208
327
  this.bridge.sendToolCancelled({ reason });
209
328
  }
210
329
 
330
+ /**
331
+ * Tell the guest UI the resource is being torn down (unload / cleanup).
332
+ * Forwards to {@link AppBridge.teardownResource} on `@modelcontextprotocol/ext-apps/app-bridge`.
333
+ */
334
+ teardownResource(params: Record<string, unknown> = {}): void {
335
+ this.log('Sending resource teardown to app');
336
+ this.bridge.teardownResource(params as never);
337
+ }
338
+
211
339
  // ============================================
212
340
  // Private: Initialization
213
341
  // ============================================
214
342
 
215
- private configureSandbox(): void {
216
- if (this.iframe.sandbox.value !== SANDBOX_PERMISSIONS) {
217
- this.iframe.sandbox.value = SANDBOX_PERMISSIONS;
218
- }
219
- }
220
343
 
221
344
  private initializeBridge(): AppBridge {
222
345
  const bridge = new AppBridge(
@@ -226,12 +349,10 @@ export class AppHost {
226
349
  openLinks: {},
227
350
  serverTools: {},
228
351
  logging: {},
229
- // Declare support for model context updates
230
352
  updateModelContext: { text: {} },
231
353
  },
232
354
  {
233
- // Initial host context
234
- hostContext: {
355
+ hostContext: this.options.hostContext || {
235
356
  theme: 'dark',
236
357
  platform: 'web',
237
358
  containerDimensions: { maxHeight: 6000 },
@@ -241,20 +362,59 @@ export class AppHost {
241
362
  }
242
363
  );
243
364
 
244
- // Register handlers - must be done BEFORE connect()
365
+ ;(bridge as any).fallbackRequestHandler = this.options.onFallbackRequest;
366
+
245
367
  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}`);
368
+ if (this.options.onReadResource) {
369
+ bridge.onreadresource = async (params) => {
370
+ const resp = await this.options.onReadResource!(params.uri);
371
+ return {
372
+ contents: resp.contents.map(c => ({
373
+ uri: params.uri,
374
+ text: c.text as string,
375
+ blob: c.blob as string,
376
+ }))
377
+ };
378
+ };
379
+ }
380
+
381
+ bridge.onopenlink = async (params, extra) => {
382
+ if (this.options.onOpenLink) {
383
+ return await this.options.onOpenLink(params, extra as any);
384
+ }
385
+ return this.handleOpenLink(params);
386
+ };
387
+ bridge.onmessage = async (params, extra) => {
388
+ if (this.options.onMessage) {
389
+ return await this.options.onMessage(params, extra as any);
390
+ }
391
+ return this.handleMessage(params as any);
392
+ };
393
+ bridge.onloggingmessage = (params) => {
394
+ this.log(`App log [${params.level}]: ${params.data}`);
395
+ if (this.options.onLoggingMessage) {
396
+ this.options.onLoggingMessage(params);
397
+ }
398
+ };
249
399
  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%)`;
400
+ bridge.onsizechange = async (params) => {
401
+ const { width, height } = params;
402
+ // Guard: ignore transient 0px resize events (e.g. fired by guest during viewport transitions)
403
+ if (height !== undefined && height > 0) {
404
+ this.iframe.style.height = `${height}px`;
405
+ }
406
+ if (width !== undefined && width > 0) this.iframe.style.minWidth = `min(${width}px, 100%)`;
407
+ if (this.options.onSizeChanged) {
408
+ this.options.onSizeChanged(params);
409
+ }
253
410
  return {};
254
411
  };
255
- bridge.onrequestdisplaymode = async (params) => ({
256
- mode: params.mode === 'fullscreen' ? 'fullscreen' : 'inline'
257
- });
412
+ bridge.onrequestdisplaymode = async (params, extra) => {
413
+ if (this.options.onRequestDisplayMode) {
414
+ return await this.options.onRequestDisplayMode(params, extra as any);
415
+ }
416
+ return { mode: params.mode === 'fullscreen' ? 'fullscreen' : 'inline' };
417
+ };
258
418
 
259
419
  return bridge;
260
420
  }
@@ -272,6 +432,9 @@ export class AppHost {
272
432
  this.log('Bridge connected successfully');
273
433
  } catch (error) {
274
434
  this.log('Bridge connection failed', 'error');
435
+ if (this.options.onError) {
436
+ this.options.onError(error instanceof Error ? error : new Error(String(error)));
437
+ }
275
438
  throw error;
276
439
  }
277
440
  }
@@ -281,8 +444,12 @@ export class AppHost {
281
444
  // ============================================
282
445
 
283
446
  private async handleToolCall(params: ToolCallParams) {
284
- if (!this.client.isConnected()) {
285
- throw new Error('Client disconnected');
447
+ if (this.options.onCallTool) {
448
+ return await this.options.onCallTool(params);
449
+ }
450
+
451
+ if (!this.client || !this.client.isConnected()) {
452
+ throw new Error('Client disconnected or not provided');
286
453
  }
287
454
 
288
455
  const sessionId = await this.getSessionId();
@@ -312,34 +479,49 @@ export class AppHost {
312
479
  // Private: Resource Loading
313
480
  // ============================================
314
481
 
315
- private async launchMcpApp(uri: string): Promise<void> {
316
- if (!this.client.isConnected()) {
317
- throw new Error('Client must be connected');
482
+ private async launchSandboxedHtml(html: string, config: SandboxConfig): Promise<void> {
483
+ const sandboxUrlString = config.url instanceof URL ? config.url.href : config.url;
484
+ const url = new URL(sandboxUrlString, globalThis.location?.href);
485
+ if (config.csp && Object.keys(config.csp).length > 0) {
486
+ url.searchParams.set('csp', JSON.stringify(config.csp));
318
487
  }
319
488
 
489
+ const { onReady } = await setupSandboxProxyIframe(this.iframe, url);
490
+ await onReady;
491
+ }
492
+
493
+
494
+ private async readMcpAppHtml(uri: string): Promise<string> {
320
495
  const sessionId = await this.getSessionId();
321
- if (!sessionId) {
322
- throw new Error('No active session');
496
+ if (!sessionId && !this.options.onReadResource) {
497
+ throw new Error('No active session.');
323
498
  }
324
-
325
- // Fetch resource using cache hierarchy: SSEClient cache → local cache → direct fetch
326
499
  const response = await this.fetchResourceWithCache(sessionId, uri);
327
500
  if (!response?.contents?.length) {
328
501
  throw new Error(`Empty resource: ${uri}`);
329
502
  }
330
-
503
+
331
504
  const content = response.contents[0];
332
505
  const html = this.decodeContent(content);
333
506
  if (!html) {
334
507
  throw new Error(`Invalid content in resource: ${uri}`);
335
508
  }
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);
509
+ return html;
340
510
  }
341
511
 
342
- private async fetchResourceWithCache(sessionId: string, uri: string): Promise<ResourceResponse> {
512
+ private async fetchResourceWithCache(sessionId: string | undefined, uri: string): Promise<ResourceResponse> {
513
+ if (this.options.onReadResource) {
514
+ return await this.options.onReadResource(uri);
515
+ }
516
+
517
+ if (!sessionId) {
518
+ throw new Error('No active session');
519
+ }
520
+
521
+ if (!this.client) {
522
+ throw new Error('No client to read resource from');
523
+ }
524
+
343
525
  // Priority 1: SSEClient's built-in cache (best performance)
344
526
  if (this.hasClientCache()) {
345
527
  return (this.client as any).getOrFetchResource(sessionId, uri);
@@ -358,8 +540,11 @@ export class AppHost {
358
540
 
359
541
  private async preloadResource(uri: string): Promise<ResourceResponse | null> {
360
542
  try {
543
+ if (this.options.onReadResource) {
544
+ return await this.options.onReadResource(uri);
545
+ }
361
546
  const sessionId = await this.getSessionId();
362
- if (!sessionId) return null;
547
+ if (!sessionId || !this.client) return null;
363
548
  return await this.client.readResource(sessionId, uri) as ResourceResponse;
364
549
  } catch (error) {
365
550
  this.log(`Preload failed for ${uri}`, 'warn');
@@ -373,6 +558,7 @@ export class AppHost {
373
558
 
374
559
  private async getSessionId(): Promise<string | undefined> {
375
560
  if (this.sessionId) return this.sessionId;
561
+ if (!this.client) return undefined;
376
562
  const result = await this.client.getSessions();
377
563
  return result.sessions?.[0]?.sessionId;
378
564
  }
@@ -382,6 +568,7 @@ export class AppHost {
382
568
  }
383
569
 
384
570
  private hasClientCache(): boolean {
571
+ if (!this.client) return false;
385
572
  return 'getOrFetchResource' in this.client &&
386
573
  typeof (this.client as any).getOrFetchResource === 'function';
387
574
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Default configuration values for the App Host.
3
+ *
4
+ * `SANDBOX_*_READY_METHOD` match `@modelcontextprotocol/ext-apps` (see
5
+ * https://github.com/modelcontextprotocol/ext-apps/blob/main/src/types.ts ).
6
+ * Duplicated here because the package root `app.d.ts` often omits these value exports under
7
+ * `moduleResolution: "NodeNext"`.
8
+ */
9
+ export const SANDBOX_PROXY_READY_METHOD = 'ui/notifications/sandbox-proxy-ready' as const;
10
+ export const SANDBOX_RESOURCE_READY_METHOD = 'ui/notifications/sandbox-resource-ready' as const;
11
+
12
+ export const APP_HOST_DEFAULTS = {
13
+ /** Default timeout for waiting for the sandbox proxy to be ready (ms). */
14
+ SANDBOX_TIMEOUT_MS: 10000,
15
+
16
+ /** Default host info reported to guest apps. */
17
+ HOST_INFO: { name: 'mcp-ts-host', version: '1.0.0' },
18
+
19
+ /** Supported MCP App URI schemes. */
20
+ URI_SCHEMES: ['ui://', 'mcp-app://'] as const,
21
+
22
+ /** Default theme for the host context. */
23
+ THEME: 'dark',
24
+
25
+ /** Default platform for the host context. */
26
+ PLATFORM: 'web',
27
+
28
+ /** Default max height for the iframe container (px). */
29
+ MAX_HEIGHT: 6000,
30
+ } as const;
@@ -5,7 +5,12 @@
5
5
 
6
6
  /** SSE client for real-time connections */
7
7
  export { SSEClient, type SSEClientOptions } from './core/sse-client';
8
- export { AppHost } from './core/app-host';
8
+ export { AppHost, DEFAULT_MCP_APP_CSP } from './core/app-host';
9
+ export {
10
+ APP_HOST_DEFAULTS,
11
+ SANDBOX_PROXY_READY_METHOD,
12
+ SANDBOX_RESOURCE_READY_METHOD,
13
+ } from './core/constants.js';
9
14
 
10
15
 
11
16
 
@@ -13,6 +13,7 @@ export { useAppHost } from './use-app-host.js';
13
13
  export {
14
14
  useMcpApps,
15
15
  type McpAppRendererProps,
16
+ type McpAppRendererHandle,
16
17
  type McpAppMetadata,
17
18
  } from './use-mcp-apps.js';
18
19
 
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef, useState, useCallback } from 'react';
2
- import type { SSEClient } from '../core/sse-client';
3
- import { AppHost } from '../core/app-host';
2
+ import type { AppHostClient } from '../core/types';
3
+ import { AppHost, type AppHostOptions } from '../core/app-host';
4
4
 
5
5
  /**
6
6
  * Hook to host an MCP App in a React component
@@ -16,19 +16,17 @@ import { AppHost } from '../core/app-host';
16
16
  * @param options - Optional configuration
17
17
  * @returns Object containing the AppHost instance (or null) and error state
18
18
  */
19
+ export type UseAppHostOptions = AppHostOptions;
20
+
19
21
  export function useAppHost(
20
- client: SSEClient,
22
+ client: AppHostClient | null,
21
23
  iframeRef: React.RefObject<HTMLIFrameElement>,
22
- options?: {
23
- /** Callback when the App sends a message (e.g. to chat) */
24
- onMessage?: (params: { role: string; content: unknown }) => void;
25
- }
24
+ options?: UseAppHostOptions
26
25
  ) {
27
26
  const [host, setHost] = useState<AppHost | null>(null);
28
27
  const [error, setError] = useState<Error | null>(null);
29
28
  const initializingRef = useRef(false);
30
29
 
31
- // Store latest callback in ref to avoid re-initializing AppHost on callback change
32
30
  const onMessageRef = useRef(options?.onMessage);
33
31
  useEffect(() => {
34
32
  onMessageRef.current = options?.onMessage;
@@ -42,13 +40,8 @@ export function useAppHost(
42
40
 
43
41
  const initHost = async () => {
44
42
  try {
45
- // Initialize AppHost with security enforcement
46
- const appHost = new AppHost(client, iframeRef.current!);
47
-
48
- // Register message handler
49
- appHost.onAppMessage = (params) => {
50
- onMessageRef.current?.(params);
51
- };
43
+ // Initialize AppHost with security enforcement and options
44
+ const appHost = new AppHost(client, iframeRef.current!, options);
52
45
 
53
46
  // Set host immediately so launch can be called
54
47
  // (launch will wait for bridge if needed)