@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.
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/client/index.d.mts +3 -189
- package/dist/client/index.d.ts +3 -189
- package/dist/client/index.js +218 -54
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +215 -55
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +21 -14
- package/dist/client/react.d.ts +21 -14
- package/dist/client/react.js +402 -83
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +400 -85
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +3 -2
- package/dist/client/vue.d.ts +3 -2
- package/dist/client/vue.js +239 -63
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +236 -64
- package/dist/client/vue.mjs.map +1 -1
- package/dist/index-CQr9q0bF.d.mts +295 -0
- package/dist/index-nE_7Io0I.d.ts +295 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +237 -58
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -59
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.js +15 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +15 -4
- package/dist/server/index.mjs.map +1 -1
- package/package.json +13 -11
- package/src/adapters/langchain-adapter.ts +1 -1
- package/src/client/core/app-host.ts +252 -65
- package/src/client/core/constants.ts +30 -0
- package/src/client/index.ts +6 -1
- package/src/client/react/index.ts +1 -0
- package/src/client/react/use-app-host.ts +8 -15
- package/src/client/react/use-mcp-apps.tsx +221 -26
- package/src/client/react/use-mcp.ts +23 -12
- package/src/client/utils/app-host-utils.ts +62 -0
- package/src/client/vue/use-mcp.ts +23 -12
- 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
|
+
"version": "1.4.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"description": "A
|
|
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": "^
|
|
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
|
|
156
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
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": "^
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
88
|
-
this.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
223
|
+
if (!htmlToRender && source.uri) {
|
|
224
|
+
if (this.isMcpUri(source.uri)) {
|
|
225
|
+
htmlToRender = await this.readMcpAppHtml(source.uri);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
140
228
|
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
+
;(bridge as any).fallbackRequestHandler = this.options.onFallbackRequest;
|
|
366
|
+
|
|
245
367
|
bridge.oncalltool = (params) => this.handleToolCall(params);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 (
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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 (
|
|
285
|
-
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
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;
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
-
import type {
|
|
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:
|
|
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)
|