@mcp-ts/sdk 1.3.10 → 1.5.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/README.md +20 -27
- package/dist/adapters/agui-adapter.d.mts +16 -0
- package/dist/adapters/agui-adapter.d.ts +16 -0
- package/dist/adapters/agui-adapter.js +185 -0
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +185 -0
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +2 -0
- package/dist/adapters/agui-middleware.d.ts +2 -0
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +21 -0
- package/dist/adapters/ai-adapter.d.ts +21 -0
- package/dist/adapters/ai-adapter.js +175 -0
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +175 -0
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +16 -0
- package/dist/adapters/langchain-adapter.d.ts +16 -0
- package/dist/adapters/langchain-adapter.js +179 -0
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +179 -0
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/client/index.d.mts +4 -190
- package/dist/client/index.d.ts +4 -190
- 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 +31 -17
- package/dist/client/react.d.ts +31 -17
- package/dist/client/react.js +447 -103
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +443 -105
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +5 -4
- package/dist/client/vue.d.ts +5 -4
- 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-DcYfpY3H.d.mts +295 -0
- package/dist/index-GfC_eNEv.d.ts +295 -0
- package/dist/index.d.mts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1120 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1097 -60
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +18 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +18 -5
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +86 -4
- package/dist/shared/index.d.ts +86 -4
- package/dist/shared/index.js +874 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +865 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/tool-router-Bo8qZbsD.d.ts +325 -0
- package/dist/tool-router-XnWVxPzv.d.mts +325 -0
- package/dist/{types-CW6lghof.d.mts → types-CfCoIsWI.d.mts} +27 -1
- package/dist/{types-CW6lghof.d.ts → types-CfCoIsWI.d.ts} +27 -1
- package/package.json +15 -12
- package/src/adapters/agui-adapter.ts +79 -0
- package/src/adapters/ai-adapter.ts +75 -0
- package/src/adapters/langchain-adapter.ts +75 -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 +3 -0
- package/src/client/react/use-app-host.ts +8 -15
- package/src/client/react/use-mcp-apps.tsx +262 -49
- 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/index.ts +2 -0
- package/src/server/mcp/oauth-client.ts +34 -9
- package/src/shared/index.ts +36 -0
- package/src/shared/meta-tools.ts +387 -0
- package/src/shared/schema-compressor.ts +124 -0
- package/src/shared/tool-index.ts +499 -0
- package/src/shared/tool-router.ts +469 -0
- package/src/shared/types.ts +30 -0
|
@@ -2,6 +2,8 @@ import { MCPClient } from '../server/mcp/oauth-client';
|
|
|
2
2
|
import { MultiSessionClient } from '../server/mcp/multi-session-client';
|
|
3
3
|
import type { JSONSchema7 } from 'json-schema';
|
|
4
4
|
import type { ToolSet } from 'ai';
|
|
5
|
+
import { ToolRouter } from '../shared/tool-router.js';
|
|
6
|
+
import { executeMetaTool, isMetaTool } from '../shared/meta-tools.js';
|
|
5
7
|
|
|
6
8
|
export interface AIAdapterOptions {
|
|
7
9
|
/**
|
|
@@ -9,6 +11,17 @@ export interface AIAdapterOptions {
|
|
|
9
11
|
* Defaults to the client's serverId.
|
|
10
12
|
*/
|
|
11
13
|
prefix?: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Optional ToolRouter for intelligent tool selection.
|
|
17
|
+
*
|
|
18
|
+
* When provided with `strategy: 'search'`, the adapter exposes only
|
|
19
|
+
* meta-tools (search_tools, get_tool_schema) instead of all tool schemas,
|
|
20
|
+
* reducing context window usage by 80–95%.
|
|
21
|
+
*
|
|
22
|
+
* When not provided, all tools are returned as before (backward-compatible).
|
|
23
|
+
*/
|
|
24
|
+
toolRouter?: ToolRouter;
|
|
12
25
|
}
|
|
13
26
|
|
|
14
27
|
/**
|
|
@@ -80,6 +93,11 @@ export class AIAdapter {
|
|
|
80
93
|
async getTools(): Promise<ToolSet> {
|
|
81
94
|
await this.ensureJsonSchema();
|
|
82
95
|
|
|
96
|
+
// If a ToolRouter is provided, use its filtered output
|
|
97
|
+
if (this.options.toolRouter) {
|
|
98
|
+
return this.getToolsViaRouter(this.options.toolRouter);
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
// Use duck typing instead of instanceof to handle module bundling issues
|
|
84
102
|
// MultiSessionClient has getClients(), MCPClient does not
|
|
85
103
|
const isMultiSession = typeof (this.client as any).getClients === 'function';
|
|
@@ -106,6 +124,63 @@ export class AIAdapter {
|
|
|
106
124
|
return results.reduce((acc, tools) => ({ ...acc, ...tools }), {});
|
|
107
125
|
}
|
|
108
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Build a ToolSet from a ToolRouter's filtered output.
|
|
129
|
+
*
|
|
130
|
+
* In `search` strategy, only meta-tools are registered with the framework.
|
|
131
|
+
* Real tool execution is proxied through `mcp_execute_tool` which uses
|
|
132
|
+
* `router.callTool()` to route to the correct MCP client.
|
|
133
|
+
*/
|
|
134
|
+
private async getToolsViaRouter(router: ToolRouter): Promise<ToolSet> {
|
|
135
|
+
const filteredTools = await router.getFilteredTools();
|
|
136
|
+
|
|
137
|
+
// @ts-ignore: ToolSet type inference can be tricky with dynamic imports
|
|
138
|
+
return Object.fromEntries(
|
|
139
|
+
filteredTools.map((tool) => {
|
|
140
|
+
const routedTool = tool as typeof tool & { sessionId?: string; serverName?: string };
|
|
141
|
+
const namespace = routedTool.serverName ?? routedTool.sessionId;
|
|
142
|
+
const toolKey = isMetaTool(tool.name)
|
|
143
|
+
? tool.name
|
|
144
|
+
: this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverName);
|
|
145
|
+
|
|
146
|
+
return [
|
|
147
|
+
toolKey,
|
|
148
|
+
{
|
|
149
|
+
description: tool.description,
|
|
150
|
+
inputSchema: this.jsonSchema!(tool.inputSchema as JSONSchema7),
|
|
151
|
+
execute: async (args: any) => {
|
|
152
|
+
// Handle meta-tool calls via the router
|
|
153
|
+
if (isMetaTool(tool.name)) {
|
|
154
|
+
const result = await executeMetaTool(
|
|
155
|
+
tool.name,
|
|
156
|
+
args,
|
|
157
|
+
router,
|
|
158
|
+
(name, toolArgs, targetNamespace) => router.callTool(name, toolArgs, targetNamespace)
|
|
159
|
+
);
|
|
160
|
+
if (result) {
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// For non-meta tools in 'all' or 'groups' strategy,
|
|
166
|
+
// route directly to the correct MCP client
|
|
167
|
+
return await router.callTool(tool.name, args, namespace);
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private getRouterToolKey(toolName: string, sessionId?: string, serverName?: string): string {
|
|
176
|
+
const namespace = sessionId ?? serverName ?? 'mcp';
|
|
177
|
+
const normalized = namespace
|
|
178
|
+
.toLowerCase()
|
|
179
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
180
|
+
.replace(/^_+|_+$/g, '') || 'mcp';
|
|
181
|
+
return `tool_${normalized}_${toolName}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
109
184
|
/**
|
|
110
185
|
* Convenience static method to fetch tools in a single line.
|
|
111
186
|
*/
|
|
@@ -2,6 +2,8 @@ import { MCPClient } from '../server/mcp/oauth-client';
|
|
|
2
2
|
import { MultiSessionClient } from '../server/mcp/multi-session-client';
|
|
3
3
|
import type { DynamicStructuredTool, StructuredTool } from '@langchain/core/tools';
|
|
4
4
|
import type { z } from 'zod';
|
|
5
|
+
import { ToolRouter } from '../shared/tool-router.js';
|
|
6
|
+
import { executeMetaTool, isMetaTool } from '../shared/meta-tools.js';
|
|
5
7
|
|
|
6
8
|
export interface LangChainAdapterOptions {
|
|
7
9
|
/**
|
|
@@ -16,6 +18,12 @@ export interface LangChainAdapterOptions {
|
|
|
16
18
|
* @default false
|
|
17
19
|
*/
|
|
18
20
|
simplifyErrors?: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Optional ToolRouter for intelligent tool selection.
|
|
24
|
+
* See AIAdapterOptions.toolRouter for details.
|
|
25
|
+
*/
|
|
26
|
+
toolRouter?: ToolRouter;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
/**
|
|
@@ -37,7 +45,7 @@ export class LangChainAdapter {
|
|
|
37
45
|
if (!this.DynamicStructuredTool) {
|
|
38
46
|
try {
|
|
39
47
|
const langchain = await import('@langchain/core/tools');
|
|
40
|
-
this.DynamicStructuredTool = langchain.DynamicStructuredTool;
|
|
48
|
+
this.DynamicStructuredTool = langchain.DynamicStructuredTool as any;
|
|
41
49
|
|
|
42
50
|
const zod = await import('zod');
|
|
43
51
|
this.z = zod.z;
|
|
@@ -99,6 +107,11 @@ export class LangChainAdapter {
|
|
|
99
107
|
* Fetches tools from the MCP server and converts them to LangChain StructuredTools.
|
|
100
108
|
*/
|
|
101
109
|
async getTools(): Promise<StructuredTool[]> {
|
|
110
|
+
// If a ToolRouter is provided, use its filtered output
|
|
111
|
+
if (this.options.toolRouter) {
|
|
112
|
+
return this.getToolsViaRouter(this.options.toolRouter);
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
// Use duck typing instead of instanceof to handle module bundling issues
|
|
103
116
|
const isMultiSession = typeof (this.client as any).getClients === 'function';
|
|
104
117
|
const clients = isMultiSession
|
|
@@ -118,6 +131,67 @@ export class LangChainAdapter {
|
|
|
118
131
|
return results.flat();
|
|
119
132
|
}
|
|
120
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Build StructuredTools from a ToolRouter's filtered output.
|
|
136
|
+
*
|
|
137
|
+
* In `search` strategy, only meta-tools are registered with the framework.
|
|
138
|
+
* Real tool execution is proxied through `mcp_execute_tool` which uses
|
|
139
|
+
* `router.callTool()` to route to the correct MCP client.
|
|
140
|
+
*/
|
|
141
|
+
private async getToolsViaRouter(router: ToolRouter): Promise<StructuredTool[]> {
|
|
142
|
+
await this.ensureDependencies();
|
|
143
|
+
|
|
144
|
+
const filteredTools = await router.getFilteredTools();
|
|
145
|
+
|
|
146
|
+
return filteredTools.map((tool) => {
|
|
147
|
+
const routedTool = tool as typeof tool & { sessionId?: string; serverName?: string };
|
|
148
|
+
const namespace = routedTool.serverName ?? routedTool.sessionId;
|
|
149
|
+
const schema = this.jsonSchemaToZod(tool.inputSchema);
|
|
150
|
+
|
|
151
|
+
return new this.DynamicStructuredTool!({
|
|
152
|
+
name: isMetaTool(tool.name)
|
|
153
|
+
? tool.name
|
|
154
|
+
: this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverName),
|
|
155
|
+
description: tool.description || `Tool ${tool.name}`,
|
|
156
|
+
schema: schema,
|
|
157
|
+
func: async (args: any) => {
|
|
158
|
+
try {
|
|
159
|
+
// Handle meta-tool calls via the router
|
|
160
|
+
if (isMetaTool(tool.name)) {
|
|
161
|
+
const result = await executeMetaTool(
|
|
162
|
+
tool.name,
|
|
163
|
+
args,
|
|
164
|
+
router,
|
|
165
|
+
(name, toolArgs, namespace) => router.callTool(name, toolArgs, namespace)
|
|
166
|
+
);
|
|
167
|
+
if (result) {
|
|
168
|
+
return result.content.map((c: any) => c.text ?? '').join('\n');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// For non-meta tools in 'all' or 'groups' strategy,
|
|
173
|
+
// route directly to the correct MCP client
|
|
174
|
+
return await router.callTool(tool.name, args, namespace);
|
|
175
|
+
} catch (error: any) {
|
|
176
|
+
if (this.options.simplifyErrors) {
|
|
177
|
+
return `Error: ${error.message}`;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private getRouterToolKey(toolName: string, sessionId?: string, serverName?: string): string {
|
|
187
|
+
const namespace = sessionId ?? serverName ?? 'mcp';
|
|
188
|
+
const normalized = namespace
|
|
189
|
+
.toLowerCase()
|
|
190
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
191
|
+
.replace(/^_+|_+$/g, '') || 'mcp';
|
|
192
|
+
return `tool_${normalized}_${toolName}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
121
195
|
/**
|
|
122
196
|
* Convenience static method to fetch tools in a single line.
|
|
123
197
|
*/
|
|
@@ -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
|
}
|