@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.
Files changed (77) hide show
  1. package/README.md +25 -13
  2. package/dist/adapters/agui-adapter.d.mts +21 -44
  3. package/dist/adapters/agui-adapter.d.ts +21 -44
  4. package/dist/adapters/agui-adapter.js +93 -67
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +93 -68
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +32 -134
  9. package/dist/adapters/agui-middleware.d.ts +32 -134
  10. package/dist/adapters/agui-middleware.js +314 -350
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs +314 -351
  13. package/dist/adapters/agui-middleware.mjs.map +1 -1
  14. package/dist/adapters/ai-adapter.d.mts +2 -2
  15. package/dist/adapters/ai-adapter.d.ts +2 -2
  16. package/dist/adapters/langchain-adapter.d.mts +2 -2
  17. package/dist/adapters/langchain-adapter.d.ts +2 -2
  18. package/dist/adapters/mastra-adapter.d.mts +2 -2
  19. package/dist/adapters/mastra-adapter.d.ts +2 -2
  20. package/dist/client/index.d.mts +184 -57
  21. package/dist/client/index.d.ts +184 -57
  22. package/dist/client/index.js +535 -130
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/index.mjs +535 -131
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/client/react.d.mts +40 -6
  27. package/dist/client/react.d.ts +40 -6
  28. package/dist/client/react.js +587 -142
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +586 -143
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +5 -5
  33. package/dist/client/vue.d.ts +5 -5
  34. package/dist/client/vue.js +545 -140
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +545 -141
  37. package/dist/client/vue.mjs.map +1 -1
  38. package/dist/{events-BP6WyRNh.d.mts → events-BgeztGYZ.d.mts} +12 -1
  39. package/dist/{events-BP6WyRNh.d.ts → events-BgeztGYZ.d.ts} +12 -1
  40. package/dist/index.d.mts +4 -4
  41. package/dist/index.d.ts +4 -4
  42. package/dist/index.js +779 -248
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +775 -245
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/{multi-session-client-DMF3ED2O.d.mts → multi-session-client-CxogNckF.d.mts} +1 -1
  47. package/dist/{multi-session-client-BOFgPypS.d.ts → multi-session-client-cox_WXUj.d.ts} +1 -1
  48. package/dist/server/index.d.mts +44 -40
  49. package/dist/server/index.d.ts +44 -40
  50. package/dist/server/index.js +242 -116
  51. package/dist/server/index.js.map +1 -1
  52. package/dist/server/index.mjs +238 -112
  53. package/dist/server/index.mjs.map +1 -1
  54. package/dist/shared/index.d.mts +2 -2
  55. package/dist/shared/index.d.ts +2 -2
  56. package/dist/shared/index.js.map +1 -1
  57. package/dist/shared/index.mjs.map +1 -1
  58. package/dist/{types-SbDlA2VX.d.mts → types-CLccx9wW.d.mts} +1 -1
  59. package/dist/{types-SbDlA2VX.d.ts → types-CLccx9wW.d.ts} +1 -1
  60. package/package.json +8 -1
  61. package/src/adapters/agui-adapter.ts +121 -107
  62. package/src/adapters/agui-middleware.ts +474 -512
  63. package/src/client/core/app-host.ts +417 -0
  64. package/src/client/core/sse-client.ts +365 -212
  65. package/src/client/core/types.ts +31 -0
  66. package/src/client/index.ts +1 -0
  67. package/src/client/react/index.ts +1 -0
  68. package/src/client/react/use-mcp-app.ts +73 -0
  69. package/src/client/react/useMcp.ts +18 -0
  70. package/src/server/handlers/nextjs-handler.ts +8 -7
  71. package/src/server/handlers/sse-handler.ts +131 -164
  72. package/src/server/mcp/oauth-client.ts +32 -2
  73. package/src/server/storage/index.ts +17 -1
  74. package/src/server/storage/sqlite-backend.ts +185 -0
  75. package/src/server/storage/types.ts +1 -1
  76. package/src/shared/events.ts +12 -0
  77. 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
- { capabilities: {} }
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
- { capabilities: {} }
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 || 3e4;
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
- this.sendEvent({
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
- this.sendEvent({
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 user sessions
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 { serverId, serverName, serverUrl, callbackUrl, transportType } = params;
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
- * Helper to get or restore a client
2262
+ * Get an existing client or create and connect a new one for the session.
2101
2263
  */
2102
2264
  async getOrCreateClient(sessionId) {
2103
- let client = this.clients.get(sessionId);
2104
- if (!client) {
2105
- client = new MCPClient({
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
- return await client.callTool(toolName, toolArgs);
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
- * Refresh/validate a session
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 || "unknown",
2174
- serverName: session.serverName || "Unknown",
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 || "unknown",
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 || "unknown",
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 || "unknown",
2236
- serverName: session.serverName || "Unknown",
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 || "unknown",
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 || "unknown",
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 await client.readResource(uri);
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
- sendSSE(res, "connected", { timestamp: Date.now() });
2465
+ writeSSEEvent(res, "connected", { timestamp: Date.now() });
2336
2466
  const manager = new SSEConnectionManager(options, (event) => {
2337
2467
  if ("id" in event) {
2338
- sendSSE(res, "rpc-response", event);
2468
+ writeSSEEvent(res, "rpc-response", event);
2339
2469
  } else if ("type" in event && "sessionId" in event) {
2340
- sendSSE(res, "connection", event);
2470
+ writeSSEEvent(res, "connection", event);
2341
2471
  } else {
2342
- sendSSE(res, "observability", event);
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 (error) {
2358
- console.error("[SSE] Error handling request:", error);
2485
+ } catch {
2359
2486
  }
2360
2487
  });
2361
2488
  }
2362
2489
  };
2363
2490
  }
2364
- function sendSSE(res, event, data) {
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 sendSSE2 = (event, data) => {
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
- sendSSE2("rpc-response", event);
2548
+ sendSSE("rpc-response", event);
2423
2549
  } else if ("type" in event && "sessionId" in event) {
2424
- sendSSE2("connection", event);
2550
+ sendSSE("connection", event);
2425
2551
  } else {
2426
- sendSSE2("observability", event);
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({ acknowledged: true });
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 = new URL(this.options.url, typeof window !== "undefined" ? window.location.origin : void 0);
2514
- url.searchParams.set("identity", this.options.identity);
2515
- if (this.options.authToken) {
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
- for (const [id, { reject }] of this.pendingRequests.entries()) {
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
- * Send RPC request via SSE
2578
- * Note: SSE is unidirectional (server->client), so we need to send requests via POST
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 promise = new Promise((resolve, reject) => {
2591
- this.pendingRequests.set(id, { resolve, reject });
2592
- setTimeout(() => {
2593
- if (this.pendingRequests.has(id)) {
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
- try {
2600
- const url = new URL(this.options.url, typeof window !== "undefined" ? window.location.origin : void 0);
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
- return promise;
2781
+ const data = await response.json();
2782
+ return this.parseRpcResponse(data, request.id);
2615
2783
  }
2616
2784
  /**
2617
- * Handle RPC response
2785
+ * Parse RPC response and handle different response formats
2618
2786
  */
2619
- handleRpcResponse(response) {
2620
- const pending = this.pendingRequests.get(response.id);
2621
- if (pending) {
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
- * Get all user sessions
2800
+ * Wait for RPC response via SSE (legacy fallback)
2632
2801
  */
2633
- async getSessions() {
2634
- return this.sendRequest("getSessions");
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
- * Connect to an MCP server
2817
+ * Handle RPC response received via SSE (legacy)
2638
2818
  */
2639
- async connectToServer(params) {
2640
- return this.sendRequest("connect", params);
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
- * Disconnect from an MCP server
2644
- */
2645
- async disconnectFromServer(sessionId) {
2646
- return this.sendRequest("disconnect", { sessionId });
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
- * List tools from a session
2650
- */
2651
- async listTools(sessionId) {
2652
- return this.sendRequest("listTools", { sessionId });
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
- * Call a tool
2656
- */
2657
- async callTool(sessionId, toolName, toolArgs) {
2658
- return this.sendRequest("callTool", { sessionId, toolName, toolArgs });
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
- * Refresh/validate a session
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 restoreSession(sessionId) {
2664
- return this.sendRequest("restoreSession", { sessionId });
2975
+ async start() {
2976
+ this.log("Host started, ready to launch");
2665
2977
  }
2666
2978
  /**
2667
- * Complete OAuth authorization
2979
+ * Preload UI resources to enable instant app loading.
2980
+ * Call this when tools are discovered to cache their UI resources.
2668
2981
  */
2669
- async finishAuth(sessionId, code) {
2670
- return this.sendRequest("finishAuth", { sessionId, code });
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
- * List available prompts
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 listPrompts(sessionId) {
2676
- return this.sendRequest("listPrompts", { sessionId });
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
- * Get a specific prompt with arguments
3029
+ * Wait for iframe to finish loading
2680
3030
  */
2681
- async getPrompt(sessionId, name, args) {
2682
- return this.sendRequest("getPrompt", { sessionId, name, args });
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
- * List available resources
3041
+ * Send tool input arguments to the MCP App.
3042
+ * Call this after launch() when tool input is available.
2686
3043
  */
2687
- async listResources(sessionId) {
2688
- return this.sendRequest("listResources", { sessionId });
3044
+ sendToolInput(args) {
3045
+ this.log("Sending tool input to app");
3046
+ this.bridge.sendToolInput({ arguments: args });
2689
3047
  }
2690
3048
  /**
2691
- * Read a specific resource
3049
+ * Send tool result to the MCP App.
3050
+ * Call this when the tool call completes.
2692
3051
  */
2693
- async readResource(sessionId, uri) {
2694
- return this.sendRequest("readResource", { sessionId, uri });
3052
+ sendToolResult(result) {
3053
+ this.log("Sending tool result to app");
3054
+ this.bridge.sendToolResult(result);
2695
3055
  }
2696
3056
  /**
2697
- * Check if connected
3057
+ * Send tool cancellation to the MCP App.
3058
+ * Call this when the tool call is cancelled or fails.
2698
3059
  */
2699
- isConnected() {
2700
- return this.eventSource !== null && this.eventSource.readyState === EventSource.OPEN;
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