@mcp-ts/sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -13
- package/dist/adapters/agui-adapter.d.mts +21 -44
- package/dist/adapters/agui-adapter.d.ts +21 -44
- package/dist/adapters/agui-adapter.js +93 -67
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +93 -68
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +32 -134
- package/dist/adapters/agui-middleware.d.ts +32 -134
- package/dist/adapters/agui-middleware.js +314 -350
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs +314 -351
- 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 +184 -57
- package/dist/client/index.d.ts +184 -57
- 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 +40 -6
- package/dist/client/react.d.ts +40 -6
- package/dist/client/react.js +587 -142
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +586 -143
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +5 -5
- package/dist/client/vue.d.ts +5 -5
- package/dist/client/vue.js +545 -140
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +545 -141
- 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 +779 -248
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +775 -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 +44 -40
- package/dist/server/index.d.ts +44 -40
- package/dist/server/index.js +242 -116
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +238 -112
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{types-SbDlA2VX.d.mts → types-CLccx9wW.d.mts} +1 -1
- package/dist/{types-SbDlA2VX.d.ts → types-CLccx9wW.d.ts} +1 -1
- package/package.json +8 -1
- package/src/adapters/agui-adapter.ts +121 -107
- package/src/adapters/agui-middleware.ts +474 -512
- 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/index.ts +1 -0
- package/src/client/react/use-mcp-app.ts +73 -0
- package/src/client/react/useMcp.ts +18 -0
- package/src/server/handlers/nextjs-handler.ts +8 -7
- package/src/server/handlers/sse-handler.ts +131 -164
- 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/server/storage/types.ts +1 -1
- package/src/shared/events.ts +12 -0
- package/src/shared/types.ts +4 -2
package/dist/index.mjs
CHANGED
|
@@ -4,8 +4,10 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
4
4
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
5
5
|
import { UnauthorizedError as UnauthorizedError$1, discoverOAuthProtectedResourceMetadata, discoverAuthorizationServerMetadata, refreshAuthorization } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
6
6
|
import { ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import * as fs2 from 'fs';
|
|
7
8
|
import { promises } from 'fs';
|
|
8
9
|
import * as path from 'path';
|
|
10
|
+
import { AppBridge, PostMessageTransport } from '@modelcontextprotocol/ext-apps/app-bridge';
|
|
9
11
|
|
|
10
12
|
var __defProp = Object.defineProperty;
|
|
11
13
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -507,6 +509,148 @@ var FileStorageBackend = class {
|
|
|
507
509
|
async disconnect() {
|
|
508
510
|
}
|
|
509
511
|
};
|
|
512
|
+
var SqliteStorage = class {
|
|
513
|
+
constructor(options = {}) {
|
|
514
|
+
__publicField(this, "db", null);
|
|
515
|
+
__publicField(this, "table");
|
|
516
|
+
__publicField(this, "initialized", false);
|
|
517
|
+
__publicField(this, "dbPath");
|
|
518
|
+
this.dbPath = options.path || "./sessions.db";
|
|
519
|
+
this.table = options.table || "mcp_sessions";
|
|
520
|
+
}
|
|
521
|
+
async init() {
|
|
522
|
+
if (this.initialized) return;
|
|
523
|
+
try {
|
|
524
|
+
const DatabaseConstructor = (await import('better-sqlite3')).default;
|
|
525
|
+
const dir = path.dirname(this.dbPath);
|
|
526
|
+
if (!fs2.existsSync(dir)) {
|
|
527
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
528
|
+
}
|
|
529
|
+
this.db = new DatabaseConstructor(this.dbPath);
|
|
530
|
+
this.db.exec(`
|
|
531
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
532
|
+
sessionId TEXT PRIMARY KEY,
|
|
533
|
+
identity TEXT NOT NULL,
|
|
534
|
+
data TEXT NOT NULL,
|
|
535
|
+
expiresAt INTEGER
|
|
536
|
+
);
|
|
537
|
+
CREATE INDEX IF NOT EXISTS idx_${this.table}_identity ON ${this.table}(identity);
|
|
538
|
+
`);
|
|
539
|
+
this.initialized = true;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (error.code === "MODULE_NOT_FOUND" || error.message?.includes("better-sqlite3")) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
"better-sqlite3 is not installed. Please install it with: npm install better-sqlite3"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
ensureInitialized() {
|
|
550
|
+
if (!this.initialized) {
|
|
551
|
+
throw new Error("SqliteStorage not initialized. Call init() first.");
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
generateSessionId() {
|
|
555
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
556
|
+
let result = "";
|
|
557
|
+
for (let i = 0; i < 32; i++) {
|
|
558
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
559
|
+
}
|
|
560
|
+
return result;
|
|
561
|
+
}
|
|
562
|
+
async createSession(session, ttl) {
|
|
563
|
+
this.ensureInitialized();
|
|
564
|
+
const { sessionId, identity } = session;
|
|
565
|
+
if (!sessionId || !identity) {
|
|
566
|
+
throw new Error("identity and sessionId required");
|
|
567
|
+
}
|
|
568
|
+
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
569
|
+
try {
|
|
570
|
+
const stmt = this.db.prepare(
|
|
571
|
+
`INSERT INTO ${this.table} (sessionId, identity, data, expiresAt) VALUES (?, ?, ?, ?)`
|
|
572
|
+
);
|
|
573
|
+
stmt.run(sessionId, identity, JSON.stringify(session), expiresAt);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
576
|
+
throw new Error(`Session ${sessionId} already exists`);
|
|
577
|
+
}
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async updateSession(identity, sessionId, data, ttl) {
|
|
582
|
+
this.ensureInitialized();
|
|
583
|
+
if (!sessionId || !identity) {
|
|
584
|
+
throw new Error("identity and sessionId required");
|
|
585
|
+
}
|
|
586
|
+
const currentSession = await this.getSession(identity, sessionId);
|
|
587
|
+
if (!currentSession) {
|
|
588
|
+
throw new Error(`Session ${sessionId} not found for identity ${identity}`);
|
|
589
|
+
}
|
|
590
|
+
const updatedSession = { ...currentSession, ...data };
|
|
591
|
+
const expiresAt = ttl ? Date.now() + ttl * 1e3 : null;
|
|
592
|
+
const stmt = this.db.prepare(
|
|
593
|
+
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND identity = ?`
|
|
594
|
+
);
|
|
595
|
+
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, identity);
|
|
596
|
+
}
|
|
597
|
+
async getSession(identity, sessionId) {
|
|
598
|
+
this.ensureInitialized();
|
|
599
|
+
const stmt = this.db.prepare(
|
|
600
|
+
`SELECT data FROM ${this.table} WHERE sessionId = ? AND identity = ?`
|
|
601
|
+
);
|
|
602
|
+
const row = stmt.get(sessionId, identity);
|
|
603
|
+
if (!row) return null;
|
|
604
|
+
return JSON.parse(row.data);
|
|
605
|
+
}
|
|
606
|
+
async getIdentitySessionsData(identity) {
|
|
607
|
+
this.ensureInitialized();
|
|
608
|
+
const stmt = this.db.prepare(
|
|
609
|
+
`SELECT data FROM ${this.table} WHERE identity = ?`
|
|
610
|
+
);
|
|
611
|
+
const rows = stmt.all(identity);
|
|
612
|
+
return rows.map((row) => JSON.parse(row.data));
|
|
613
|
+
}
|
|
614
|
+
async getIdentityMcpSessions(identity) {
|
|
615
|
+
this.ensureInitialized();
|
|
616
|
+
const stmt = this.db.prepare(
|
|
617
|
+
`SELECT sessionId FROM ${this.table} WHERE identity = ?`
|
|
618
|
+
);
|
|
619
|
+
const rows = stmt.all(identity);
|
|
620
|
+
return rows.map((row) => row.sessionId);
|
|
621
|
+
}
|
|
622
|
+
async removeSession(identity, sessionId) {
|
|
623
|
+
this.ensureInitialized();
|
|
624
|
+
const stmt = this.db.prepare(
|
|
625
|
+
`DELETE FROM ${this.table} WHERE sessionId = ? AND identity = ?`
|
|
626
|
+
);
|
|
627
|
+
stmt.run(sessionId, identity);
|
|
628
|
+
}
|
|
629
|
+
async getAllSessionIds() {
|
|
630
|
+
this.ensureInitialized();
|
|
631
|
+
const stmt = this.db.prepare(`SELECT sessionId FROM ${this.table}`);
|
|
632
|
+
const rows = stmt.all();
|
|
633
|
+
return rows.map((row) => row.sessionId);
|
|
634
|
+
}
|
|
635
|
+
async clearAll() {
|
|
636
|
+
this.ensureInitialized();
|
|
637
|
+
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
638
|
+
stmt.run();
|
|
639
|
+
}
|
|
640
|
+
async cleanupExpiredSessions() {
|
|
641
|
+
this.ensureInitialized();
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
const stmt = this.db.prepare(
|
|
644
|
+
`DELETE FROM ${this.table} WHERE expiresAt IS NOT NULL AND expiresAt < ?`
|
|
645
|
+
);
|
|
646
|
+
stmt.run(now);
|
|
647
|
+
}
|
|
648
|
+
async disconnect() {
|
|
649
|
+
if (this.db) {
|
|
650
|
+
this.db.close();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
};
|
|
510
654
|
|
|
511
655
|
// src/server/storage/index.ts
|
|
512
656
|
var storageInstance = null;
|
|
@@ -538,6 +682,13 @@ async function createStorage() {
|
|
|
538
682
|
store.init().catch((err) => console.error("[Storage] Failed to initialize file storage:", err));
|
|
539
683
|
return store;
|
|
540
684
|
}
|
|
685
|
+
if (type === "sqlite") {
|
|
686
|
+
const dbPath = process.env.MCP_TS_STORAGE_SQLITE_PATH;
|
|
687
|
+
console.log(`[Storage] Using SQLite storage (${dbPath || "default"}) (Explicit)`);
|
|
688
|
+
const store = new SqliteStorage({ path: dbPath });
|
|
689
|
+
store.init().catch((err) => console.error("[Storage] Failed to initialize SQLite storage:", err));
|
|
690
|
+
return store;
|
|
691
|
+
}
|
|
541
692
|
if (type === "memory") {
|
|
542
693
|
console.log("[Storage] Using In-Memory storage (Explicit)");
|
|
543
694
|
return new MemoryStorageBackend();
|
|
@@ -560,6 +711,12 @@ async function createStorage() {
|
|
|
560
711
|
store.init().catch((err) => console.error("[Storage] Failed to initialize file storage:", err));
|
|
561
712
|
return store;
|
|
562
713
|
}
|
|
714
|
+
if (process.env.MCP_TS_STORAGE_SQLITE_PATH) {
|
|
715
|
+
console.log(`[Storage] Auto-detected MCP_TS_STORAGE_SQLITE_PATH. Using SQLite storage (${process.env.MCP_TS_STORAGE_SQLITE_PATH}).`);
|
|
716
|
+
const store = new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH });
|
|
717
|
+
store.init().catch((err) => console.error("[Storage] Failed to initialize SQLite storage:", err));
|
|
718
|
+
return store;
|
|
719
|
+
}
|
|
563
720
|
console.log("[Storage] No storage configured. Using In-Memory storage (Default).");
|
|
564
721
|
return new MemoryStorageBackend();
|
|
565
722
|
}
|
|
@@ -1135,7 +1292,15 @@ var MCPClient = class _MCPClient {
|
|
|
1135
1292
|
name: "mcp-ts-oauth-client",
|
|
1136
1293
|
version: "2.0"
|
|
1137
1294
|
},
|
|
1138
|
-
{
|
|
1295
|
+
{
|
|
1296
|
+
capabilities: {
|
|
1297
|
+
extensions: {
|
|
1298
|
+
"io.modelcontextprotocol/ui": {
|
|
1299
|
+
mimeTypes: ["text/html+mcp"]
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1139
1304
|
);
|
|
1140
1305
|
}
|
|
1141
1306
|
const existingSession = await storage.getSession(this.identity, this.sessionId);
|
|
@@ -1317,7 +1482,15 @@ var MCPClient = class _MCPClient {
|
|
|
1317
1482
|
name: "mcp-ts-oauth-client",
|
|
1318
1483
|
version: "2.0"
|
|
1319
1484
|
},
|
|
1320
|
-
{
|
|
1485
|
+
{
|
|
1486
|
+
capabilities: {
|
|
1487
|
+
extensions: {
|
|
1488
|
+
"io.modelcontextprotocol/ui": {
|
|
1489
|
+
mimeTypes: ["text/html+mcp"]
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1321
1494
|
);
|
|
1322
1495
|
this.emitStateChange("CONNECTING");
|
|
1323
1496
|
await this.client.connect(this.transport);
|
|
@@ -1873,6 +2046,7 @@ var MultiSessionClient = class {
|
|
|
1873
2046
|
};
|
|
1874
2047
|
|
|
1875
2048
|
// src/server/handlers/sse-handler.ts
|
|
2049
|
+
var DEFAULT_HEARTBEAT_INTERVAL = 3e4;
|
|
1876
2050
|
var SSEConnectionManager = class {
|
|
1877
2051
|
constructor(options, sendEvent) {
|
|
1878
2052
|
this.options = options;
|
|
@@ -1902,7 +2076,7 @@ var SSEConnectionManager = class {
|
|
|
1902
2076
|
* Start heartbeat to keep connection alive
|
|
1903
2077
|
*/
|
|
1904
2078
|
startHeartbeat() {
|
|
1905
|
-
const interval = this.options.heartbeatInterval
|
|
2079
|
+
const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
|
|
1906
2080
|
this.heartbeatTimer = setInterval(() => {
|
|
1907
2081
|
if (this.isActive) {
|
|
1908
2082
|
this.sendEvent({
|
|
@@ -1915,6 +2089,7 @@ var SSEConnectionManager = class {
|
|
|
1915
2089
|
}
|
|
1916
2090
|
/**
|
|
1917
2091
|
* Handle incoming RPC requests
|
|
2092
|
+
* Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
|
|
1918
2093
|
*/
|
|
1919
2094
|
async handleRequest(request) {
|
|
1920
2095
|
try {
|
|
@@ -1956,39 +2131,29 @@ var SSEConnectionManager = class {
|
|
|
1956
2131
|
default:
|
|
1957
2132
|
throw new Error(`Unknown method: ${request.method}`);
|
|
1958
2133
|
}
|
|
1959
|
-
|
|
2134
|
+
const response = {
|
|
1960
2135
|
id: request.id,
|
|
1961
2136
|
result
|
|
1962
|
-
}
|
|
2137
|
+
};
|
|
2138
|
+
this.sendEvent(response);
|
|
2139
|
+
return response;
|
|
1963
2140
|
} catch (error) {
|
|
1964
|
-
|
|
2141
|
+
const errorResponse = {
|
|
1965
2142
|
id: request.id,
|
|
1966
2143
|
error: {
|
|
1967
2144
|
code: RpcErrorCodes.EXECUTION_ERROR,
|
|
1968
2145
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
1969
2146
|
}
|
|
1970
|
-
}
|
|
2147
|
+
};
|
|
2148
|
+
this.sendEvent(errorResponse);
|
|
2149
|
+
return errorResponse;
|
|
1971
2150
|
}
|
|
1972
2151
|
}
|
|
1973
2152
|
/**
|
|
1974
|
-
* Get all
|
|
2153
|
+
* Get all sessions for the current identity
|
|
1975
2154
|
*/
|
|
1976
2155
|
async getSessions() {
|
|
1977
2156
|
const sessions = await storage.getIdentitySessionsData(this.identity);
|
|
1978
|
-
this.sendEvent({
|
|
1979
|
-
level: "debug",
|
|
1980
|
-
message: `Retrieved ${sessions.length} sessions for identity ${this.identity}`,
|
|
1981
|
-
timestamp: Date.now(),
|
|
1982
|
-
metadata: {
|
|
1983
|
-
identity: this.identity,
|
|
1984
|
-
sessionCount: sessions.length,
|
|
1985
|
-
sessions: sessions.map((s) => ({
|
|
1986
|
-
sessionId: s.sessionId,
|
|
1987
|
-
serverId: s.serverId,
|
|
1988
|
-
serverName: s.serverName
|
|
1989
|
-
}))
|
|
1990
|
-
}
|
|
1991
|
-
});
|
|
1992
2157
|
return {
|
|
1993
2158
|
sessions: sessions.map((s) => ({
|
|
1994
2159
|
sessionId: s.sessionId,
|
|
@@ -2003,7 +2168,8 @@ var SSEConnectionManager = class {
|
|
|
2003
2168
|
* Connect to an MCP server
|
|
2004
2169
|
*/
|
|
2005
2170
|
async connect(params) {
|
|
2006
|
-
const {
|
|
2171
|
+
const { serverName, serverUrl, callbackUrl, transportType } = params;
|
|
2172
|
+
const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : await storage.generateSessionId();
|
|
2007
2173
|
const existingSessions = await storage.getIdentitySessionsData(this.identity);
|
|
2008
2174
|
const duplicate = existingSessions.find(
|
|
2009
2175
|
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
@@ -2052,10 +2218,6 @@ var SSEConnectionManager = class {
|
|
|
2052
2218
|
});
|
|
2053
2219
|
await client.connect();
|
|
2054
2220
|
const tools = await client.listTools();
|
|
2055
|
-
const sessionAfterConnect = await storage.getSession(this.identity, sessionId);
|
|
2056
|
-
console.log(`[SSE Handler] After connect() - Session ${sessionId}:`, {
|
|
2057
|
-
serverId: sessionAfterConnect?.serverId
|
|
2058
|
-
});
|
|
2059
2221
|
this.emitConnectionEvent({
|
|
2060
2222
|
type: "tools_discovered",
|
|
2061
2223
|
sessionId,
|
|
@@ -2097,24 +2259,21 @@ var SSEConnectionManager = class {
|
|
|
2097
2259
|
return { success: true };
|
|
2098
2260
|
}
|
|
2099
2261
|
/**
|
|
2100
|
-
*
|
|
2262
|
+
* Get an existing client or create and connect a new one for the session.
|
|
2101
2263
|
*/
|
|
2102
2264
|
async getOrCreateClient(sessionId) {
|
|
2103
|
-
|
|
2104
|
-
if (
|
|
2105
|
-
|
|
2106
|
-
identity: this.identity,
|
|
2107
|
-
sessionId
|
|
2108
|
-
});
|
|
2109
|
-
client.onConnectionEvent((event) => {
|
|
2110
|
-
this.emitConnectionEvent(event);
|
|
2111
|
-
});
|
|
2112
|
-
client.onObservabilityEvent((event) => {
|
|
2113
|
-
this.sendEvent(event);
|
|
2114
|
-
});
|
|
2115
|
-
await client.connect();
|
|
2116
|
-
this.clients.set(sessionId, client);
|
|
2265
|
+
const existing = this.clients.get(sessionId);
|
|
2266
|
+
if (existing) {
|
|
2267
|
+
return existing;
|
|
2117
2268
|
}
|
|
2269
|
+
const client = new MCPClient({
|
|
2270
|
+
identity: this.identity,
|
|
2271
|
+
sessionId
|
|
2272
|
+
});
|
|
2273
|
+
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
2274
|
+
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
2275
|
+
await client.connect();
|
|
2276
|
+
this.clients.set(sessionId, client);
|
|
2118
2277
|
return client;
|
|
2119
2278
|
}
|
|
2120
2279
|
/**
|
|
@@ -2127,51 +2286,35 @@ var SSEConnectionManager = class {
|
|
|
2127
2286
|
return { tools: result.tools };
|
|
2128
2287
|
}
|
|
2129
2288
|
/**
|
|
2130
|
-
* Call a tool
|
|
2289
|
+
* Call a tool on the MCP server
|
|
2131
2290
|
*/
|
|
2132
2291
|
async callTool(params) {
|
|
2133
2292
|
const { sessionId, toolName, toolArgs } = params;
|
|
2134
2293
|
const client = await this.getOrCreateClient(sessionId);
|
|
2135
|
-
|
|
2294
|
+
const result = await client.callTool(toolName, toolArgs);
|
|
2295
|
+
const meta = result._meta || {};
|
|
2296
|
+
return {
|
|
2297
|
+
...result,
|
|
2298
|
+
_meta: {
|
|
2299
|
+
...meta,
|
|
2300
|
+
sessionId
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2136
2303
|
}
|
|
2137
2304
|
/**
|
|
2138
|
-
*
|
|
2305
|
+
* Restore and validate an existing session
|
|
2139
2306
|
*/
|
|
2140
2307
|
async restoreSession(params) {
|
|
2141
2308
|
const { sessionId } = params;
|
|
2142
|
-
this.sendEvent({
|
|
2143
|
-
level: "debug",
|
|
2144
|
-
message: `Starting session refresh for ${sessionId}`,
|
|
2145
|
-
timestamp: Date.now(),
|
|
2146
|
-
metadata: { sessionId, identity: this.identity }
|
|
2147
|
-
});
|
|
2148
2309
|
const session = await storage.getSession(this.identity, sessionId);
|
|
2149
2310
|
if (!session) {
|
|
2150
|
-
this.sendEvent({
|
|
2151
|
-
level: "error",
|
|
2152
|
-
message: `Session not found: ${sessionId}`,
|
|
2153
|
-
timestamp: Date.now(),
|
|
2154
|
-
metadata: { sessionId, identity: this.identity }
|
|
2155
|
-
});
|
|
2156
2311
|
throw new Error("Session not found");
|
|
2157
2312
|
}
|
|
2158
|
-
this.sendEvent({
|
|
2159
|
-
level: "debug",
|
|
2160
|
-
message: `Session found in Redis`,
|
|
2161
|
-
timestamp: Date.now(),
|
|
2162
|
-
metadata: {
|
|
2163
|
-
sessionId,
|
|
2164
|
-
serverId: session.serverId,
|
|
2165
|
-
serverName: session.serverName,
|
|
2166
|
-
serverUrl: session.serverUrl,
|
|
2167
|
-
transportType: session.transportType
|
|
2168
|
-
}
|
|
2169
|
-
});
|
|
2170
2313
|
this.emitConnectionEvent({
|
|
2171
2314
|
type: "state_changed",
|
|
2172
2315
|
sessionId,
|
|
2173
|
-
serverId: session.serverId
|
|
2174
|
-
serverName: session.serverName
|
|
2316
|
+
serverId: session.serverId ?? "unknown",
|
|
2317
|
+
serverName: session.serverName ?? "Unknown",
|
|
2175
2318
|
state: "VALIDATING",
|
|
2176
2319
|
previousState: "DISCONNECTED",
|
|
2177
2320
|
timestamp: Date.now()
|
|
@@ -2182,21 +2325,16 @@ var SSEConnectionManager = class {
|
|
|
2182
2325
|
identity: this.identity,
|
|
2183
2326
|
sessionId,
|
|
2184
2327
|
...clientMetadata
|
|
2185
|
-
// Include metadata for consistency
|
|
2186
|
-
});
|
|
2187
|
-
client.onConnectionEvent((event) => {
|
|
2188
|
-
this.emitConnectionEvent(event);
|
|
2189
|
-
});
|
|
2190
|
-
client.onObservabilityEvent((event) => {
|
|
2191
|
-
this.sendEvent(event);
|
|
2192
2328
|
});
|
|
2329
|
+
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
2330
|
+
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
2193
2331
|
await client.connect();
|
|
2194
2332
|
this.clients.set(sessionId, client);
|
|
2195
2333
|
const tools = await client.listTools();
|
|
2196
2334
|
this.emitConnectionEvent({
|
|
2197
2335
|
type: "tools_discovered",
|
|
2198
2336
|
sessionId,
|
|
2199
|
-
serverId: session.serverId
|
|
2337
|
+
serverId: session.serverId ?? "unknown",
|
|
2200
2338
|
toolCount: tools.tools.length,
|
|
2201
2339
|
tools: tools.tools,
|
|
2202
2340
|
timestamp: Date.now()
|
|
@@ -2206,7 +2344,7 @@ var SSEConnectionManager = class {
|
|
|
2206
2344
|
this.emitConnectionEvent({
|
|
2207
2345
|
type: "error",
|
|
2208
2346
|
sessionId,
|
|
2209
|
-
serverId: session.serverId
|
|
2347
|
+
serverId: session.serverId ?? "unknown",
|
|
2210
2348
|
error: error instanceof Error ? error.message : "Validation failed",
|
|
2211
2349
|
errorType: "validation",
|
|
2212
2350
|
timestamp: Date.now()
|
|
@@ -2215,16 +2353,10 @@ var SSEConnectionManager = class {
|
|
|
2215
2353
|
}
|
|
2216
2354
|
}
|
|
2217
2355
|
/**
|
|
2218
|
-
* Complete OAuth authorization
|
|
2356
|
+
* Complete OAuth authorization flow
|
|
2219
2357
|
*/
|
|
2220
2358
|
async finishAuth(params) {
|
|
2221
2359
|
const { sessionId, code } = params;
|
|
2222
|
-
this.sendEvent({
|
|
2223
|
-
level: "debug",
|
|
2224
|
-
message: `Completing OAuth for session ${sessionId}`,
|
|
2225
|
-
timestamp: Date.now(),
|
|
2226
|
-
metadata: { sessionId, identity: this.identity }
|
|
2227
|
-
});
|
|
2228
2360
|
const session = await storage.getSession(this.identity, sessionId);
|
|
2229
2361
|
if (!session) {
|
|
2230
2362
|
throw new Error("Session not found");
|
|
@@ -2232,8 +2364,8 @@ var SSEConnectionManager = class {
|
|
|
2232
2364
|
this.emitConnectionEvent({
|
|
2233
2365
|
type: "state_changed",
|
|
2234
2366
|
sessionId,
|
|
2235
|
-
serverId: session.serverId
|
|
2236
|
-
serverName: session.serverName
|
|
2367
|
+
serverId: session.serverId ?? "unknown",
|
|
2368
|
+
serverName: session.serverName ?? "Unknown",
|
|
2237
2369
|
state: "AUTHENTICATING",
|
|
2238
2370
|
previousState: "DISCONNECTED",
|
|
2239
2371
|
timestamp: Date.now()
|
|
@@ -2243,16 +2375,14 @@ var SSEConnectionManager = class {
|
|
|
2243
2375
|
identity: this.identity,
|
|
2244
2376
|
sessionId
|
|
2245
2377
|
});
|
|
2246
|
-
client.onConnectionEvent((event) =>
|
|
2247
|
-
this.emitConnectionEvent(event);
|
|
2248
|
-
});
|
|
2378
|
+
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
2249
2379
|
await client.finishAuth(code);
|
|
2250
2380
|
this.clients.set(sessionId, client);
|
|
2251
2381
|
const tools = await client.listTools();
|
|
2252
2382
|
this.emitConnectionEvent({
|
|
2253
2383
|
type: "tools_discovered",
|
|
2254
2384
|
sessionId,
|
|
2255
|
-
serverId: session.serverId
|
|
2385
|
+
serverId: session.serverId ?? "unknown",
|
|
2256
2386
|
toolCount: tools.tools.length,
|
|
2257
2387
|
tools: tools.tools,
|
|
2258
2388
|
timestamp: Date.now()
|
|
@@ -2262,7 +2392,7 @@ var SSEConnectionManager = class {
|
|
|
2262
2392
|
this.emitConnectionEvent({
|
|
2263
2393
|
type: "error",
|
|
2264
2394
|
sessionId,
|
|
2265
|
-
serverId: session.serverId
|
|
2395
|
+
serverId: session.serverId ?? "unknown",
|
|
2266
2396
|
error: error instanceof Error ? error.message : "OAuth completion failed",
|
|
2267
2397
|
errorType: "auth",
|
|
2268
2398
|
timestamp: Date.now()
|
|
@@ -2302,7 +2432,7 @@ var SSEConnectionManager = class {
|
|
|
2302
2432
|
async readResource(params) {
|
|
2303
2433
|
const { sessionId, uri } = params;
|
|
2304
2434
|
const client = await this.getOrCreateClient(sessionId);
|
|
2305
|
-
return
|
|
2435
|
+
return client.readResource(uri);
|
|
2306
2436
|
}
|
|
2307
2437
|
/**
|
|
2308
2438
|
* Emit connection event
|
|
@@ -2332,19 +2462,17 @@ function createSSEHandler(options) {
|
|
|
2332
2462
|
"Connection": "keep-alive",
|
|
2333
2463
|
"Access-Control-Allow-Origin": "*"
|
|
2334
2464
|
});
|
|
2335
|
-
|
|
2465
|
+
writeSSEEvent(res, "connected", { timestamp: Date.now() });
|
|
2336
2466
|
const manager = new SSEConnectionManager(options, (event) => {
|
|
2337
2467
|
if ("id" in event) {
|
|
2338
|
-
|
|
2468
|
+
writeSSEEvent(res, "rpc-response", event);
|
|
2339
2469
|
} else if ("type" in event && "sessionId" in event) {
|
|
2340
|
-
|
|
2470
|
+
writeSSEEvent(res, "connection", event);
|
|
2341
2471
|
} else {
|
|
2342
|
-
|
|
2472
|
+
writeSSEEvent(res, "observability", event);
|
|
2343
2473
|
}
|
|
2344
2474
|
});
|
|
2345
|
-
req.on("close", () =>
|
|
2346
|
-
manager.dispose();
|
|
2347
|
-
});
|
|
2475
|
+
req.on("close", () => manager.dispose());
|
|
2348
2476
|
if (req.method === "POST") {
|
|
2349
2477
|
let body = "";
|
|
2350
2478
|
req.on("data", (chunk) => {
|
|
@@ -2354,14 +2482,13 @@ function createSSEHandler(options) {
|
|
|
2354
2482
|
try {
|
|
2355
2483
|
const request = JSON.parse(body);
|
|
2356
2484
|
await manager.handleRequest(request);
|
|
2357
|
-
} catch
|
|
2358
|
-
console.error("[SSE] Error handling request:", error);
|
|
2485
|
+
} catch {
|
|
2359
2486
|
}
|
|
2360
2487
|
});
|
|
2361
2488
|
}
|
|
2362
2489
|
};
|
|
2363
2490
|
}
|
|
2364
|
-
function
|
|
2491
|
+
function writeSSEEvent(res, event, data) {
|
|
2365
2492
|
res.write(`event: ${event}
|
|
2366
2493
|
`);
|
|
2367
2494
|
res.write(`data: ${JSON.stringify(data)}
|
|
@@ -2396,7 +2523,7 @@ function createNextMcpHandler(options = {}) {
|
|
|
2396
2523
|
const stream = new TransformStream();
|
|
2397
2524
|
const writer = stream.writable.getWriter();
|
|
2398
2525
|
const encoder = new TextEncoder();
|
|
2399
|
-
const
|
|
2526
|
+
const sendSSE = (event, data) => {
|
|
2400
2527
|
const message = `event: ${event}
|
|
2401
2528
|
data: ${JSON.stringify(data)}
|
|
2402
2529
|
|
|
@@ -2404,7 +2531,6 @@ data: ${JSON.stringify(data)}
|
|
|
2404
2531
|
writer.write(encoder.encode(message)).catch(() => {
|
|
2405
2532
|
});
|
|
2406
2533
|
};
|
|
2407
|
-
sendSSE2("connected", { timestamp: Date.now() });
|
|
2408
2534
|
const previousManager = managers.get(identity);
|
|
2409
2535
|
if (previousManager) {
|
|
2410
2536
|
previousManager.dispose();
|
|
@@ -2419,15 +2545,16 @@ data: ${JSON.stringify(data)}
|
|
|
2419
2545
|
},
|
|
2420
2546
|
(event) => {
|
|
2421
2547
|
if ("id" in event) {
|
|
2422
|
-
|
|
2548
|
+
sendSSE("rpc-response", event);
|
|
2423
2549
|
} else if ("type" in event && "sessionId" in event) {
|
|
2424
|
-
|
|
2550
|
+
sendSSE("connection", event);
|
|
2425
2551
|
} else {
|
|
2426
|
-
|
|
2552
|
+
sendSSE("observability", event);
|
|
2427
2553
|
}
|
|
2428
2554
|
}
|
|
2429
2555
|
);
|
|
2430
2556
|
managers.set(identity, manager);
|
|
2557
|
+
sendSSE("connected", { timestamp: Date.now() });
|
|
2431
2558
|
const abortController = new AbortController();
|
|
2432
2559
|
request.signal?.addEventListener("abort", () => {
|
|
2433
2560
|
manager.dispose();
|
|
@@ -2470,8 +2597,8 @@ data: ${JSON.stringify(data)}
|
|
|
2470
2597
|
{ status: 400 }
|
|
2471
2598
|
);
|
|
2472
2599
|
}
|
|
2473
|
-
await manager.handleRequest(body);
|
|
2474
|
-
return Response.json(
|
|
2600
|
+
const response = await manager.handleRequest(body);
|
|
2601
|
+
return Response.json(response);
|
|
2475
2602
|
} catch (error) {
|
|
2476
2603
|
return Response.json(
|
|
2477
2604
|
{
|
|
@@ -2486,20 +2613,25 @@ data: ${JSON.stringify(data)}
|
|
|
2486
2613
|
}
|
|
2487
2614
|
return { GET, POST };
|
|
2488
2615
|
}
|
|
2616
|
+
var DEFAULT_REQUEST_TIMEOUT = 6e4;
|
|
2617
|
+
var MAX_RECONNECT_ATTEMPTS = 5;
|
|
2618
|
+
var BASE_RECONNECT_DELAY = 1e3;
|
|
2489
2619
|
var SSEClient = class {
|
|
2490
2620
|
constructor(options) {
|
|
2491
2621
|
this.options = options;
|
|
2492
2622
|
__publicField(this, "eventSource", null);
|
|
2493
2623
|
__publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
|
|
2624
|
+
__publicField(this, "resourceCache", /* @__PURE__ */ new Map());
|
|
2494
2625
|
__publicField(this, "reconnectAttempts", 0);
|
|
2495
|
-
__publicField(this, "maxReconnectAttempts", 5);
|
|
2496
|
-
__publicField(this, "reconnectDelay", 1e3);
|
|
2497
2626
|
__publicField(this, "isManuallyDisconnected", false);
|
|
2498
2627
|
__publicField(this, "connectionPromise", null);
|
|
2499
2628
|
__publicField(this, "connectionResolver", null);
|
|
2500
2629
|
}
|
|
2630
|
+
// ============================================
|
|
2631
|
+
// Connection Management
|
|
2632
|
+
// ============================================
|
|
2501
2633
|
/**
|
|
2502
|
-
* Connect to SSE endpoint
|
|
2634
|
+
* Connect to the SSE endpoint
|
|
2503
2635
|
*/
|
|
2504
2636
|
connect() {
|
|
2505
2637
|
if (this.eventSource) {
|
|
@@ -2510,52 +2642,12 @@ var SSEClient = class {
|
|
|
2510
2642
|
this.connectionPromise = new Promise((resolve) => {
|
|
2511
2643
|
this.connectionResolver = resolve;
|
|
2512
2644
|
});
|
|
2513
|
-
const url =
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
url.searchParams.set("token", this.options.authToken);
|
|
2517
|
-
}
|
|
2518
|
-
this.eventSource = new EventSource(url.toString());
|
|
2519
|
-
this.eventSource.addEventListener("open", () => {
|
|
2520
|
-
console.log("[SSEClient] Connected");
|
|
2521
|
-
this.reconnectAttempts = 0;
|
|
2522
|
-
this.options.onStatusChange?.("connected");
|
|
2523
|
-
});
|
|
2524
|
-
this.eventSource.addEventListener("connected", (e) => {
|
|
2525
|
-
const data = JSON.parse(e.data);
|
|
2526
|
-
console.log("[SSEClient] Server ready:", data);
|
|
2527
|
-
if (this.connectionResolver) {
|
|
2528
|
-
this.connectionResolver();
|
|
2529
|
-
this.connectionResolver = null;
|
|
2530
|
-
}
|
|
2531
|
-
});
|
|
2532
|
-
this.eventSource.addEventListener("connection", (e) => {
|
|
2533
|
-
const event = JSON.parse(e.data);
|
|
2534
|
-
this.options.onConnectionEvent?.(event);
|
|
2535
|
-
});
|
|
2536
|
-
this.eventSource.addEventListener("observability", (e) => {
|
|
2537
|
-
const event = JSON.parse(e.data);
|
|
2538
|
-
this.options.onObservabilityEvent?.(event);
|
|
2539
|
-
});
|
|
2540
|
-
this.eventSource.addEventListener("rpc-response", (e) => {
|
|
2541
|
-
const response = JSON.parse(e.data);
|
|
2542
|
-
this.handleRpcResponse(response);
|
|
2543
|
-
});
|
|
2544
|
-
this.eventSource.addEventListener("error", () => {
|
|
2545
|
-
console.error("[SSEClient] Connection error");
|
|
2546
|
-
this.options.onStatusChange?.("error");
|
|
2547
|
-
if (!this.isManuallyDisconnected && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
2548
|
-
this.reconnectAttempts++;
|
|
2549
|
-
console.log(`[SSEClient] Reconnecting (attempt ${this.reconnectAttempts})...`);
|
|
2550
|
-
setTimeout(() => {
|
|
2551
|
-
this.disconnect();
|
|
2552
|
-
this.connect();
|
|
2553
|
-
}, this.reconnectDelay * this.reconnectAttempts);
|
|
2554
|
-
}
|
|
2555
|
-
});
|
|
2645
|
+
const url = this.buildUrl();
|
|
2646
|
+
this.eventSource = new EventSource(url);
|
|
2647
|
+
this.setupEventListeners();
|
|
2556
2648
|
}
|
|
2557
2649
|
/**
|
|
2558
|
-
* Disconnect from SSE endpoint
|
|
2650
|
+
* Disconnect from the SSE endpoint
|
|
2559
2651
|
*/
|
|
2560
2652
|
disconnect() {
|
|
2561
2653
|
this.isManuallyDisconnected = true;
|
|
@@ -2565,139 +2657,577 @@ var SSEClient = class {
|
|
|
2565
2657
|
}
|
|
2566
2658
|
this.connectionPromise = null;
|
|
2567
2659
|
this.connectionResolver = null;
|
|
2568
|
-
|
|
2569
|
-
const error = new Error("Connection closed");
|
|
2570
|
-
error.name = "ConnectionClosedError";
|
|
2571
|
-
reject(error);
|
|
2572
|
-
}
|
|
2573
|
-
this.pendingRequests.clear();
|
|
2660
|
+
this.rejectAllPendingRequests(new Error("Connection closed"));
|
|
2574
2661
|
this.options.onStatusChange?.("disconnected");
|
|
2575
2662
|
}
|
|
2576
2663
|
/**
|
|
2577
|
-
*
|
|
2578
|
-
|
|
2664
|
+
* Check if connected to the SSE endpoint
|
|
2665
|
+
*/
|
|
2666
|
+
isConnected() {
|
|
2667
|
+
return this.eventSource?.readyState === EventSource.OPEN;
|
|
2668
|
+
}
|
|
2669
|
+
// ============================================
|
|
2670
|
+
// RPC Methods
|
|
2671
|
+
// ============================================
|
|
2672
|
+
async getSessions() {
|
|
2673
|
+
return this.sendRequest("getSessions");
|
|
2674
|
+
}
|
|
2675
|
+
async connectToServer(params) {
|
|
2676
|
+
return this.sendRequest("connect", params);
|
|
2677
|
+
}
|
|
2678
|
+
async disconnectFromServer(sessionId) {
|
|
2679
|
+
return this.sendRequest("disconnect", { sessionId });
|
|
2680
|
+
}
|
|
2681
|
+
async listTools(sessionId) {
|
|
2682
|
+
return this.sendRequest("listTools", { sessionId });
|
|
2683
|
+
}
|
|
2684
|
+
async callTool(sessionId, toolName, toolArgs) {
|
|
2685
|
+
const result = await this.sendRequest("callTool", { sessionId, toolName, toolArgs });
|
|
2686
|
+
this.emitUiEventIfPresent(result, sessionId, toolName);
|
|
2687
|
+
return result;
|
|
2688
|
+
}
|
|
2689
|
+
async restoreSession(sessionId) {
|
|
2690
|
+
return this.sendRequest("restoreSession", { sessionId });
|
|
2691
|
+
}
|
|
2692
|
+
async finishAuth(sessionId, code) {
|
|
2693
|
+
return this.sendRequest("finishAuth", { sessionId, code });
|
|
2694
|
+
}
|
|
2695
|
+
async listPrompts(sessionId) {
|
|
2696
|
+
return this.sendRequest("listPrompts", { sessionId });
|
|
2697
|
+
}
|
|
2698
|
+
async getPrompt(sessionId, name, args) {
|
|
2699
|
+
return this.sendRequest("getPrompt", { sessionId, name, args });
|
|
2700
|
+
}
|
|
2701
|
+
async listResources(sessionId) {
|
|
2702
|
+
return this.sendRequest("listResources", { sessionId });
|
|
2703
|
+
}
|
|
2704
|
+
async readResource(sessionId, uri) {
|
|
2705
|
+
return this.sendRequest("readResource", { sessionId, uri });
|
|
2706
|
+
}
|
|
2707
|
+
// ============================================
|
|
2708
|
+
// Resource Preloading (for instant UI loading)
|
|
2709
|
+
// ============================================
|
|
2710
|
+
/**
|
|
2711
|
+
* Preload UI resources for tools that have UI metadata.
|
|
2712
|
+
* Call this when tools are discovered to enable instant MCP App UI loading.
|
|
2713
|
+
*/
|
|
2714
|
+
preloadToolUiResources(sessionId, tools) {
|
|
2715
|
+
for (const tool of tools) {
|
|
2716
|
+
const uri = this.extractUiResourceUri(tool);
|
|
2717
|
+
if (!uri) continue;
|
|
2718
|
+
if (this.resourceCache.has(uri)) {
|
|
2719
|
+
this.log(`Resource already cached: ${uri}`);
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
this.log(`Preloading UI resource for tool "${tool.name}": ${uri}`);
|
|
2723
|
+
const promise = this.sendRequest("readResource", { sessionId, uri }).catch((err) => {
|
|
2724
|
+
this.log(`Failed to preload resource ${uri}: ${err.message}`, "warn");
|
|
2725
|
+
this.resourceCache.delete(uri);
|
|
2726
|
+
return null;
|
|
2727
|
+
});
|
|
2728
|
+
this.resourceCache.set(uri, promise);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* Get a preloaded resource from cache, or fetch if not cached.
|
|
2733
|
+
*/
|
|
2734
|
+
getOrFetchResource(sessionId, uri) {
|
|
2735
|
+
const cached = this.resourceCache.get(uri);
|
|
2736
|
+
if (cached) {
|
|
2737
|
+
this.log(`Cache hit for resource: ${uri}`);
|
|
2738
|
+
return cached;
|
|
2739
|
+
}
|
|
2740
|
+
this.log(`Cache miss, fetching resource: ${uri}`);
|
|
2741
|
+
const promise = this.sendRequest("readResource", { sessionId, uri });
|
|
2742
|
+
this.resourceCache.set(uri, promise);
|
|
2743
|
+
return promise;
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Check if a resource is already cached
|
|
2747
|
+
*/
|
|
2748
|
+
hasPreloadedResource(uri) {
|
|
2749
|
+
return this.resourceCache.has(uri);
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Clear the resource cache
|
|
2753
|
+
*/
|
|
2754
|
+
clearResourceCache() {
|
|
2755
|
+
this.resourceCache.clear();
|
|
2756
|
+
}
|
|
2757
|
+
// ============================================
|
|
2758
|
+
// Private: Request Handling
|
|
2759
|
+
// ============================================
|
|
2760
|
+
/**
|
|
2761
|
+
* Send an RPC request and return the response directly from HTTP.
|
|
2762
|
+
* This bypasses SSE latency by returning results in the HTTP response body.
|
|
2579
2763
|
*/
|
|
2580
2764
|
async sendRequest(method, params) {
|
|
2581
2765
|
if (this.connectionPromise) {
|
|
2582
2766
|
await this.connectionPromise;
|
|
2583
2767
|
}
|
|
2584
|
-
const id = `rpc_${nanoid(10)}`;
|
|
2585
2768
|
const request = {
|
|
2586
|
-
id
|
|
2769
|
+
id: `rpc_${nanoid(10)}`,
|
|
2587
2770
|
method,
|
|
2588
2771
|
params
|
|
2589
2772
|
};
|
|
2590
|
-
const
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
this.pendingRequests.delete(id);
|
|
2595
|
-
reject(new Error("Request timeout"));
|
|
2596
|
-
}
|
|
2597
|
-
}, 3e4);
|
|
2773
|
+
const response = await fetch(this.buildUrl(), {
|
|
2774
|
+
method: "POST",
|
|
2775
|
+
headers: this.buildHeaders(),
|
|
2776
|
+
body: JSON.stringify(request)
|
|
2598
2777
|
});
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
url.searchParams.set("identity", this.options.identity);
|
|
2602
|
-
await fetch(url.toString(), {
|
|
2603
|
-
method: "POST",
|
|
2604
|
-
headers: {
|
|
2605
|
-
"Content-Type": "application/json",
|
|
2606
|
-
...this.options.authToken && { Authorization: `Bearer ${this.options.authToken}` }
|
|
2607
|
-
},
|
|
2608
|
-
body: JSON.stringify(request)
|
|
2609
|
-
});
|
|
2610
|
-
} catch (error) {
|
|
2611
|
-
this.pendingRequests.delete(id);
|
|
2612
|
-
throw error;
|
|
2778
|
+
if (!response.ok) {
|
|
2779
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2613
2780
|
}
|
|
2614
|
-
|
|
2781
|
+
const data = await response.json();
|
|
2782
|
+
return this.parseRpcResponse(data, request.id);
|
|
2615
2783
|
}
|
|
2616
2784
|
/**
|
|
2617
|
-
*
|
|
2785
|
+
* Parse RPC response and handle different response formats
|
|
2618
2786
|
*/
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
this.pendingRequests.delete(response.id);
|
|
2623
|
-
if (response.error) {
|
|
2624
|
-
pending.reject(new Error(response.error.message));
|
|
2625
|
-
} else {
|
|
2626
|
-
pending.resolve(response.result);
|
|
2627
|
-
}
|
|
2787
|
+
parseRpcResponse(data, requestId) {
|
|
2788
|
+
if ("result" in data) {
|
|
2789
|
+
return data.result;
|
|
2628
2790
|
}
|
|
2791
|
+
if ("error" in data && data.error) {
|
|
2792
|
+
throw new Error(data.error.message || "Unknown RPC error");
|
|
2793
|
+
}
|
|
2794
|
+
if ("acknowledged" in data) {
|
|
2795
|
+
return this.waitForSseResponse(requestId);
|
|
2796
|
+
}
|
|
2797
|
+
throw new Error("Invalid RPC response format");
|
|
2629
2798
|
}
|
|
2630
2799
|
/**
|
|
2631
|
-
*
|
|
2800
|
+
* Wait for RPC response via SSE (legacy fallback)
|
|
2632
2801
|
*/
|
|
2633
|
-
|
|
2634
|
-
|
|
2802
|
+
waitForSseResponse(requestId) {
|
|
2803
|
+
const timeoutMs = this.options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
|
|
2804
|
+
return new Promise((resolve, reject) => {
|
|
2805
|
+
const timeoutId = setTimeout(() => {
|
|
2806
|
+
this.pendingRequests.delete(requestId);
|
|
2807
|
+
reject(new Error(`Request timeout after ${timeoutMs}ms`));
|
|
2808
|
+
}, timeoutMs);
|
|
2809
|
+
this.pendingRequests.set(requestId, {
|
|
2810
|
+
resolve,
|
|
2811
|
+
reject,
|
|
2812
|
+
timeoutId
|
|
2813
|
+
});
|
|
2814
|
+
});
|
|
2635
2815
|
}
|
|
2636
2816
|
/**
|
|
2637
|
-
*
|
|
2817
|
+
* Handle RPC response received via SSE (legacy)
|
|
2638
2818
|
*/
|
|
2639
|
-
|
|
2640
|
-
|
|
2819
|
+
handleRpcResponse(response) {
|
|
2820
|
+
const pending = this.pendingRequests.get(response.id);
|
|
2821
|
+
if (!pending) return;
|
|
2822
|
+
clearTimeout(pending.timeoutId);
|
|
2823
|
+
this.pendingRequests.delete(response.id);
|
|
2824
|
+
if (response.error) {
|
|
2825
|
+
pending.reject(new Error(response.error.message));
|
|
2826
|
+
} else {
|
|
2827
|
+
pending.resolve(response.result);
|
|
2828
|
+
}
|
|
2641
2829
|
}
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2830
|
+
// ============================================
|
|
2831
|
+
// Private: Event Handling
|
|
2832
|
+
// ============================================
|
|
2833
|
+
setupEventListeners() {
|
|
2834
|
+
if (!this.eventSource) return;
|
|
2835
|
+
this.eventSource.addEventListener("open", () => {
|
|
2836
|
+
this.log("Connected");
|
|
2837
|
+
this.reconnectAttempts = 0;
|
|
2838
|
+
this.options.onStatusChange?.("connected");
|
|
2839
|
+
});
|
|
2840
|
+
this.eventSource.addEventListener("connected", () => {
|
|
2841
|
+
this.log("Server ready");
|
|
2842
|
+
this.connectionResolver?.();
|
|
2843
|
+
this.connectionResolver = null;
|
|
2844
|
+
});
|
|
2845
|
+
this.eventSource.addEventListener("connection", (e) => {
|
|
2846
|
+
const event = JSON.parse(e.data);
|
|
2847
|
+
this.options.onConnectionEvent?.(event);
|
|
2848
|
+
});
|
|
2849
|
+
this.eventSource.addEventListener("observability", (e) => {
|
|
2850
|
+
const event = JSON.parse(e.data);
|
|
2851
|
+
this.options.onObservabilityEvent?.(event);
|
|
2852
|
+
});
|
|
2853
|
+
this.eventSource.addEventListener("rpc-response", (e) => {
|
|
2854
|
+
const response = JSON.parse(e.data);
|
|
2855
|
+
this.handleRpcResponse(response);
|
|
2856
|
+
});
|
|
2857
|
+
this.eventSource.addEventListener("error", () => {
|
|
2858
|
+
this.log("Connection error", "error");
|
|
2859
|
+
this.options.onStatusChange?.("error");
|
|
2860
|
+
this.attemptReconnect();
|
|
2861
|
+
});
|
|
2647
2862
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2863
|
+
attemptReconnect() {
|
|
2864
|
+
if (this.isManuallyDisconnected || this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
this.reconnectAttempts++;
|
|
2868
|
+
const delay = BASE_RECONNECT_DELAY * this.reconnectAttempts;
|
|
2869
|
+
this.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
2870
|
+
setTimeout(() => {
|
|
2871
|
+
this.disconnect();
|
|
2872
|
+
this.connect();
|
|
2873
|
+
}, delay);
|
|
2874
|
+
}
|
|
2875
|
+
// ============================================
|
|
2876
|
+
// Private: Utilities
|
|
2877
|
+
// ============================================
|
|
2878
|
+
buildUrl() {
|
|
2879
|
+
const url = new URL(this.options.url, globalThis.location?.origin);
|
|
2880
|
+
url.searchParams.set("identity", this.options.identity);
|
|
2881
|
+
if (this.options.authToken) {
|
|
2882
|
+
url.searchParams.set("token", this.options.authToken);
|
|
2883
|
+
}
|
|
2884
|
+
return url.toString();
|
|
2653
2885
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2886
|
+
buildHeaders() {
|
|
2887
|
+
const headers = {
|
|
2888
|
+
"Content-Type": "application/json"
|
|
2889
|
+
};
|
|
2890
|
+
if (this.options.authToken) {
|
|
2891
|
+
headers["Authorization"] = `Bearer ${this.options.authToken}`;
|
|
2892
|
+
}
|
|
2893
|
+
return headers;
|
|
2894
|
+
}
|
|
2895
|
+
rejectAllPendingRequests(error) {
|
|
2896
|
+
for (const [, pending] of this.pendingRequests) {
|
|
2897
|
+
clearTimeout(pending.timeoutId);
|
|
2898
|
+
pending.reject(error);
|
|
2899
|
+
}
|
|
2900
|
+
this.pendingRequests.clear();
|
|
2901
|
+
}
|
|
2902
|
+
extractUiResourceUri(tool) {
|
|
2903
|
+
const meta = tool._meta?.ui;
|
|
2904
|
+
if (!meta || typeof meta !== "object") return void 0;
|
|
2905
|
+
if (meta.visibility && !meta.visibility.includes("app")) return void 0;
|
|
2906
|
+
return meta.resourceUri ?? meta.uri;
|
|
2907
|
+
}
|
|
2908
|
+
emitUiEventIfPresent(result, sessionId, toolName) {
|
|
2909
|
+
const meta = result?._meta;
|
|
2910
|
+
const resourceUri = meta?.ui?.resourceUri ?? meta?.["ui/resourceUri"];
|
|
2911
|
+
if (resourceUri) {
|
|
2912
|
+
this.options.onEvent?.({
|
|
2913
|
+
type: "mcp-apps-ui",
|
|
2914
|
+
sessionId,
|
|
2915
|
+
resourceUri,
|
|
2916
|
+
toolName,
|
|
2917
|
+
result,
|
|
2918
|
+
timestamp: Date.now()
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
log(message, level = "info") {
|
|
2923
|
+
if (!this.options.debug && level === "info") return;
|
|
2924
|
+
const prefix = "[SSEClient]";
|
|
2925
|
+
switch (level) {
|
|
2926
|
+
case "warn":
|
|
2927
|
+
console.warn(prefix, message);
|
|
2928
|
+
break;
|
|
2929
|
+
case "error":
|
|
2930
|
+
console.error(prefix, message);
|
|
2931
|
+
break;
|
|
2932
|
+
default:
|
|
2933
|
+
console.log(prefix, message);
|
|
2934
|
+
}
|
|
2659
2935
|
}
|
|
2936
|
+
};
|
|
2937
|
+
var HOST_INFO = { name: "mcp-ts-host", version: "1.0.0" };
|
|
2938
|
+
var SANDBOX_PERMISSIONS = [
|
|
2939
|
+
"allow-scripts",
|
|
2940
|
+
// Required for app JavaScript execution
|
|
2941
|
+
"allow-forms",
|
|
2942
|
+
// Required for form submissions
|
|
2943
|
+
"allow-same-origin",
|
|
2944
|
+
// Required for Blob URL correctness
|
|
2945
|
+
"allow-modals",
|
|
2946
|
+
// Required for dialogs/alerts
|
|
2947
|
+
"allow-popups",
|
|
2948
|
+
// Required for opening links
|
|
2949
|
+
"allow-downloads"
|
|
2950
|
+
// Required for file downloads
|
|
2951
|
+
].join(" ");
|
|
2952
|
+
var MCP_URI_SCHEMES = ["ui://", "mcp-app://"];
|
|
2953
|
+
var AppHost = class {
|
|
2954
|
+
constructor(client, iframe, options) {
|
|
2955
|
+
this.client = client;
|
|
2956
|
+
this.iframe = iframe;
|
|
2957
|
+
__publicField(this, "bridge");
|
|
2958
|
+
__publicField(this, "sessionId");
|
|
2959
|
+
__publicField(this, "resourceCache", /* @__PURE__ */ new Map());
|
|
2960
|
+
__publicField(this, "debug");
|
|
2961
|
+
/** Callback for app messages (e.g., chat messages from the app) */
|
|
2962
|
+
__publicField(this, "onAppMessage");
|
|
2963
|
+
this.debug = options?.debug ?? false;
|
|
2964
|
+
this.configureSandbox();
|
|
2965
|
+
this.bridge = this.initializeBridge();
|
|
2966
|
+
}
|
|
2967
|
+
// ============================================
|
|
2968
|
+
// Public API
|
|
2969
|
+
// ============================================
|
|
2660
2970
|
/**
|
|
2661
|
-
*
|
|
2971
|
+
* Start the host. This prepares the bridge handlers but doesn't connect yet.
|
|
2972
|
+
* The actual connection happens in launch() after HTML is loaded.
|
|
2973
|
+
* @returns Promise that resolves immediately (bridge connects during launch)
|
|
2662
2974
|
*/
|
|
2663
|
-
async
|
|
2664
|
-
|
|
2975
|
+
async start() {
|
|
2976
|
+
this.log("Host started, ready to launch");
|
|
2665
2977
|
}
|
|
2666
2978
|
/**
|
|
2667
|
-
*
|
|
2979
|
+
* Preload UI resources to enable instant app loading.
|
|
2980
|
+
* Call this when tools are discovered to cache their UI resources.
|
|
2668
2981
|
*/
|
|
2669
|
-
|
|
2670
|
-
|
|
2982
|
+
preload(tools) {
|
|
2983
|
+
for (const tool of tools) {
|
|
2984
|
+
const uri = this.extractUiResourceUri(tool);
|
|
2985
|
+
if (!uri || this.resourceCache.has(uri)) continue;
|
|
2986
|
+
const promise = this.preloadResource(uri);
|
|
2987
|
+
this.resourceCache.set(uri, promise);
|
|
2988
|
+
}
|
|
2671
2989
|
}
|
|
2672
2990
|
/**
|
|
2673
|
-
*
|
|
2991
|
+
* Launch an MCP App from a URL or MCP resource URI.
|
|
2992
|
+
* Loads the HTML first, then establishes bridge connection.
|
|
2674
2993
|
*/
|
|
2675
|
-
async
|
|
2676
|
-
|
|
2994
|
+
async launch(url, sessionId) {
|
|
2995
|
+
if (sessionId) this.sessionId = sessionId;
|
|
2996
|
+
const initializedPromise = this.onAppReady();
|
|
2997
|
+
if (this.isMcpUri(url)) {
|
|
2998
|
+
await this.launchMcpApp(url);
|
|
2999
|
+
} else {
|
|
3000
|
+
this.iframe.src = url;
|
|
3001
|
+
}
|
|
3002
|
+
await this.onIframeReady();
|
|
3003
|
+
await this.connectBridge();
|
|
3004
|
+
this.log("Waiting for app initialization");
|
|
3005
|
+
await Promise.race([
|
|
3006
|
+
initializedPromise,
|
|
3007
|
+
new Promise((resolve) => setTimeout(() => {
|
|
3008
|
+
this.log("Initialization timeout - continuing anyway", "warn");
|
|
3009
|
+
resolve();
|
|
3010
|
+
}, 3e3))
|
|
3011
|
+
]);
|
|
3012
|
+
this.log("App launched and ready");
|
|
3013
|
+
}
|
|
3014
|
+
/**
|
|
3015
|
+
* Wait for app to signal initialization complete
|
|
3016
|
+
*/
|
|
3017
|
+
onAppReady() {
|
|
3018
|
+
return new Promise((resolve) => {
|
|
3019
|
+
const originalHandler = this.bridge.oninitialized;
|
|
3020
|
+
this.bridge.oninitialized = (...args) => {
|
|
3021
|
+
this.log("App initialized");
|
|
3022
|
+
resolve();
|
|
3023
|
+
this.bridge.oninitialized = originalHandler;
|
|
3024
|
+
originalHandler?.(...args);
|
|
3025
|
+
};
|
|
3026
|
+
});
|
|
2677
3027
|
}
|
|
2678
3028
|
/**
|
|
2679
|
-
*
|
|
3029
|
+
* Wait for iframe to finish loading
|
|
2680
3030
|
*/
|
|
2681
|
-
|
|
2682
|
-
return
|
|
3031
|
+
onIframeReady() {
|
|
3032
|
+
return new Promise((resolve) => {
|
|
3033
|
+
if (this.iframe.contentDocument?.readyState === "complete") {
|
|
3034
|
+
resolve();
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
this.iframe.addEventListener("load", () => resolve(), { once: true });
|
|
3038
|
+
});
|
|
2683
3039
|
}
|
|
2684
3040
|
/**
|
|
2685
|
-
*
|
|
3041
|
+
* Send tool input arguments to the MCP App.
|
|
3042
|
+
* Call this after launch() when tool input is available.
|
|
2686
3043
|
*/
|
|
2687
|
-
|
|
2688
|
-
|
|
3044
|
+
sendToolInput(args) {
|
|
3045
|
+
this.log("Sending tool input to app");
|
|
3046
|
+
this.bridge.sendToolInput({ arguments: args });
|
|
2689
3047
|
}
|
|
2690
3048
|
/**
|
|
2691
|
-
*
|
|
3049
|
+
* Send tool result to the MCP App.
|
|
3050
|
+
* Call this when the tool call completes.
|
|
2692
3051
|
*/
|
|
2693
|
-
|
|
2694
|
-
|
|
3052
|
+
sendToolResult(result) {
|
|
3053
|
+
this.log("Sending tool result to app");
|
|
3054
|
+
this.bridge.sendToolResult(result);
|
|
2695
3055
|
}
|
|
2696
3056
|
/**
|
|
2697
|
-
*
|
|
3057
|
+
* Send tool cancellation to the MCP App.
|
|
3058
|
+
* Call this when the tool call is cancelled or fails.
|
|
2698
3059
|
*/
|
|
2699
|
-
|
|
2700
|
-
|
|
3060
|
+
sendToolCancelled(reason) {
|
|
3061
|
+
this.log("Sending tool cancellation to app");
|
|
3062
|
+
this.bridge.sendToolCancelled({ reason });
|
|
3063
|
+
}
|
|
3064
|
+
// ============================================
|
|
3065
|
+
// Private: Initialization
|
|
3066
|
+
// ============================================
|
|
3067
|
+
configureSandbox() {
|
|
3068
|
+
if (this.iframe.sandbox.value !== SANDBOX_PERMISSIONS) {
|
|
3069
|
+
this.iframe.sandbox.value = SANDBOX_PERMISSIONS;
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
initializeBridge() {
|
|
3073
|
+
const bridge = new AppBridge(
|
|
3074
|
+
null,
|
|
3075
|
+
HOST_INFO,
|
|
3076
|
+
{
|
|
3077
|
+
openLinks: {},
|
|
3078
|
+
serverTools: {},
|
|
3079
|
+
logging: {},
|
|
3080
|
+
// Declare support for model context updates
|
|
3081
|
+
updateModelContext: { text: {} }
|
|
3082
|
+
},
|
|
3083
|
+
{
|
|
3084
|
+
// Initial host context
|
|
3085
|
+
hostContext: {
|
|
3086
|
+
theme: "dark",
|
|
3087
|
+
platform: "web",
|
|
3088
|
+
containerDimensions: { maxHeight: 6e3 },
|
|
3089
|
+
displayMode: "inline",
|
|
3090
|
+
availableDisplayModes: ["inline", "fullscreen"]
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
);
|
|
3094
|
+
bridge.oncalltool = (params) => this.handleToolCall(params);
|
|
3095
|
+
bridge.onopenlink = this.handleOpenLink.bind(this);
|
|
3096
|
+
bridge.onmessage = this.handleMessage.bind(this);
|
|
3097
|
+
bridge.onloggingmessage = (params) => this.log(`App log [${params.level}]: ${params.data}`);
|
|
3098
|
+
bridge.onupdatemodelcontext = async () => ({});
|
|
3099
|
+
bridge.onsizechange = async ({ width, height }) => {
|
|
3100
|
+
if (height !== void 0) this.iframe.style.height = `${height}px`;
|
|
3101
|
+
if (width !== void 0) this.iframe.style.minWidth = `min(${width}px, 100%)`;
|
|
3102
|
+
return {};
|
|
3103
|
+
};
|
|
3104
|
+
bridge.onrequestdisplaymode = async (params) => ({
|
|
3105
|
+
mode: params.mode === "fullscreen" ? "fullscreen" : "inline"
|
|
3106
|
+
});
|
|
3107
|
+
return bridge;
|
|
3108
|
+
}
|
|
3109
|
+
async connectBridge() {
|
|
3110
|
+
this.log("Connecting bridge to iframe");
|
|
3111
|
+
const transport = new PostMessageTransport(
|
|
3112
|
+
this.iframe.contentWindow,
|
|
3113
|
+
this.iframe.contentWindow
|
|
3114
|
+
);
|
|
3115
|
+
try {
|
|
3116
|
+
await this.bridge.connect(transport);
|
|
3117
|
+
this.log("Bridge connected successfully");
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
this.log("Bridge connection failed", "error");
|
|
3120
|
+
throw error;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
// ============================================
|
|
3124
|
+
// Private: Bridge Event Handlers
|
|
3125
|
+
// ============================================
|
|
3126
|
+
async handleToolCall(params) {
|
|
3127
|
+
if (!this.client.isConnected()) {
|
|
3128
|
+
throw new Error("Client disconnected");
|
|
3129
|
+
}
|
|
3130
|
+
const sessionId = await this.getSessionId();
|
|
3131
|
+
if (!sessionId) {
|
|
3132
|
+
throw new Error("No active session");
|
|
3133
|
+
}
|
|
3134
|
+
const result = await this.client.callTool(
|
|
3135
|
+
sessionId,
|
|
3136
|
+
params.name,
|
|
3137
|
+
params.arguments ?? {}
|
|
3138
|
+
);
|
|
3139
|
+
return result;
|
|
3140
|
+
}
|
|
3141
|
+
async handleOpenLink(params) {
|
|
3142
|
+
window.open(params.url, "_blank", "noopener,noreferrer");
|
|
3143
|
+
return {};
|
|
3144
|
+
}
|
|
3145
|
+
async handleMessage(params) {
|
|
3146
|
+
this.onAppMessage?.(params);
|
|
3147
|
+
return {};
|
|
3148
|
+
}
|
|
3149
|
+
// ============================================
|
|
3150
|
+
// Private: Resource Loading
|
|
3151
|
+
// ============================================
|
|
3152
|
+
async launchMcpApp(uri) {
|
|
3153
|
+
if (!this.client.isConnected()) {
|
|
3154
|
+
throw new Error("Client must be connected");
|
|
3155
|
+
}
|
|
3156
|
+
const sessionId = await this.getSessionId();
|
|
3157
|
+
if (!sessionId) {
|
|
3158
|
+
throw new Error("No active session");
|
|
3159
|
+
}
|
|
3160
|
+
const response = await this.fetchResourceWithCache(sessionId, uri);
|
|
3161
|
+
if (!response?.contents?.length) {
|
|
3162
|
+
throw new Error(`Empty resource: ${uri}`);
|
|
3163
|
+
}
|
|
3164
|
+
const content = response.contents[0];
|
|
3165
|
+
const html = this.decodeContent(content);
|
|
3166
|
+
if (!html) {
|
|
3167
|
+
throw new Error(`Invalid content in resource: ${uri}`);
|
|
3168
|
+
}
|
|
3169
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
3170
|
+
this.iframe.src = URL.createObjectURL(blob);
|
|
3171
|
+
}
|
|
3172
|
+
async fetchResourceWithCache(sessionId, uri) {
|
|
3173
|
+
if (this.hasClientCache()) {
|
|
3174
|
+
return this.client.getOrFetchResource(sessionId, uri);
|
|
3175
|
+
}
|
|
3176
|
+
const cached = this.resourceCache.get(uri);
|
|
3177
|
+
if (cached) {
|
|
3178
|
+
const result = await cached;
|
|
3179
|
+
if (result) return result;
|
|
3180
|
+
}
|
|
3181
|
+
return this.client.readResource(sessionId, uri);
|
|
3182
|
+
}
|
|
3183
|
+
async preloadResource(uri) {
|
|
3184
|
+
try {
|
|
3185
|
+
const sessionId = await this.getSessionId();
|
|
3186
|
+
if (!sessionId) return null;
|
|
3187
|
+
return await this.client.readResource(sessionId, uri);
|
|
3188
|
+
} catch (error) {
|
|
3189
|
+
this.log(`Preload failed for ${uri}`, "warn");
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
// ============================================
|
|
3194
|
+
// Private: Utilities
|
|
3195
|
+
// ============================================
|
|
3196
|
+
async getSessionId() {
|
|
3197
|
+
if (this.sessionId) return this.sessionId;
|
|
3198
|
+
const result = await this.client.getSessions();
|
|
3199
|
+
return result.sessions?.[0]?.sessionId;
|
|
3200
|
+
}
|
|
3201
|
+
isMcpUri(url) {
|
|
3202
|
+
return MCP_URI_SCHEMES.some((scheme) => url.startsWith(scheme));
|
|
3203
|
+
}
|
|
3204
|
+
hasClientCache() {
|
|
3205
|
+
return "getOrFetchResource" in this.client && typeof this.client.getOrFetchResource === "function";
|
|
3206
|
+
}
|
|
3207
|
+
extractUiResourceUri(tool) {
|
|
3208
|
+
const meta = tool._meta;
|
|
3209
|
+
if (!meta?.ui) return void 0;
|
|
3210
|
+
return meta.ui.resourceUri ?? meta.ui.uri;
|
|
3211
|
+
}
|
|
3212
|
+
decodeContent(content) {
|
|
3213
|
+
if (content.blob) {
|
|
3214
|
+
return atob(content.blob);
|
|
3215
|
+
}
|
|
3216
|
+
return content.text;
|
|
3217
|
+
}
|
|
3218
|
+
log(message, level = "info") {
|
|
3219
|
+
if (!this.debug && level === "info") return;
|
|
3220
|
+
const prefix = "[AppHost]";
|
|
3221
|
+
switch (level) {
|
|
3222
|
+
case "warn":
|
|
3223
|
+
console.warn(prefix, message);
|
|
3224
|
+
break;
|
|
3225
|
+
case "error":
|
|
3226
|
+
console.error(prefix, message);
|
|
3227
|
+
break;
|
|
3228
|
+
default:
|
|
3229
|
+
console.log(prefix, message);
|
|
3230
|
+
}
|
|
2701
3231
|
}
|
|
2702
3232
|
};
|
|
2703
3233
|
|
|
@@ -2718,6 +3248,6 @@ function isCallToolSuccess(response) {
|
|
|
2718
3248
|
return "content" in response;
|
|
2719
3249
|
}
|
|
2720
3250
|
|
|
2721
|
-
export { AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_POLICY_URI, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, REDIS_KEY_PREFIX, RpcErrorCodes, SESSION_TTL_SECONDS, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, TOKEN_EXPIRY_BUFFER_MS, ToolExecutionError, UnauthorizedError, createNextMcpHandler, createSSEHandler, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, sanitizeServerLabel, storage };
|
|
3251
|
+
export { AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_POLICY_URI, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, REDIS_KEY_PREFIX, RpcErrorCodes, SESSION_TTL_SECONDS, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, TOKEN_EXPIRY_BUFFER_MS, ToolExecutionError, UnauthorizedError, createNextMcpHandler, createSSEHandler, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, sanitizeServerLabel, storage };
|
|
2722
3252
|
//# sourceMappingURL=index.mjs.map
|
|
2723
3253
|
//# sourceMappingURL=index.mjs.map
|