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