@mcp-ts/sdk 1.0.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/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/adapters/agui-adapter.d.mts +119 -0
- package/dist/adapters/agui-adapter.d.ts +119 -0
- package/dist/adapters/agui-adapter.js +109 -0
- package/dist/adapters/agui-adapter.js.map +1 -0
- package/dist/adapters/agui-adapter.mjs +107 -0
- package/dist/adapters/agui-adapter.mjs.map +1 -0
- package/dist/adapters/agui-middleware.d.mts +171 -0
- package/dist/adapters/agui-middleware.d.ts +171 -0
- package/dist/adapters/agui-middleware.js +429 -0
- package/dist/adapters/agui-middleware.js.map +1 -0
- package/dist/adapters/agui-middleware.mjs +417 -0
- package/dist/adapters/agui-middleware.mjs.map +1 -0
- package/dist/adapters/ai-adapter.d.mts +38 -0
- package/dist/adapters/ai-adapter.d.ts +38 -0
- package/dist/adapters/ai-adapter.js +82 -0
- package/dist/adapters/ai-adapter.js.map +1 -0
- package/dist/adapters/ai-adapter.mjs +80 -0
- package/dist/adapters/ai-adapter.mjs.map +1 -0
- package/dist/adapters/langchain-adapter.d.mts +46 -0
- package/dist/adapters/langchain-adapter.d.ts +46 -0
- package/dist/adapters/langchain-adapter.js +102 -0
- package/dist/adapters/langchain-adapter.js.map +1 -0
- package/dist/adapters/langchain-adapter.mjs +100 -0
- package/dist/adapters/langchain-adapter.mjs.map +1 -0
- package/dist/adapters/mastra-adapter.d.mts +49 -0
- package/dist/adapters/mastra-adapter.d.ts +49 -0
- package/dist/adapters/mastra-adapter.js +95 -0
- package/dist/adapters/mastra-adapter.js.map +1 -0
- package/dist/adapters/mastra-adapter.mjs +93 -0
- package/dist/adapters/mastra-adapter.mjs.map +1 -0
- package/dist/client/index.d.mts +119 -0
- package/dist/client/index.d.ts +119 -0
- package/dist/client/index.js +225 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +223 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/client/react.d.mts +151 -0
- package/dist/client/react.d.ts +151 -0
- package/dist/client/react.js +492 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/react.mjs +489 -0
- package/dist/client/react.mjs.map +1 -0
- package/dist/client/vue.d.mts +157 -0
- package/dist/client/vue.d.ts +157 -0
- package/dist/client/vue.js +474 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client/vue.mjs +471 -0
- package/dist/client/vue.mjs.map +1 -0
- package/dist/events-BP6WyRNh.d.mts +110 -0
- package/dist/events-BP6WyRNh.d.ts +110 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2784 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2723 -0
- package/dist/index.mjs.map +1 -0
- package/dist/multi-session-client-BOFgPypS.d.ts +389 -0
- package/dist/multi-session-client-DMF3ED2O.d.mts +389 -0
- package/dist/server/index.d.mts +269 -0
- package/dist/server/index.d.ts +269 -0
- package/dist/server/index.js +2444 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +2414 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/shared/index.d.mts +24 -0
- package/dist/shared/index.d.ts +24 -0
- package/dist/shared/index.js +223 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/index.mjs +190 -0
- package/dist/shared/index.mjs.map +1 -0
- package/dist/types-SbDlA2VX.d.mts +153 -0
- package/dist/types-SbDlA2VX.d.ts +153 -0
- package/dist/utils-0qmYrqoa.d.mts +92 -0
- package/dist/utils-0qmYrqoa.d.ts +92 -0
- package/package.json +165 -0
- package/src/adapters/agui-adapter.ts +210 -0
- package/src/adapters/agui-middleware.ts +512 -0
- package/src/adapters/ai-adapter.ts +115 -0
- package/src/adapters/langchain-adapter.ts +127 -0
- package/src/adapters/mastra-adapter.ts +126 -0
- package/src/client/core/sse-client.ts +340 -0
- package/src/client/index.ts +26 -0
- package/src/client/react/index.ts +10 -0
- package/src/client/react/useMcp.ts +558 -0
- package/src/client/vue/index.ts +10 -0
- package/src/client/vue/useMcp.ts +542 -0
- package/src/index.ts +11 -0
- package/src/server/handlers/nextjs-handler.ts +216 -0
- package/src/server/handlers/sse-handler.ts +699 -0
- package/src/server/index.ts +57 -0
- package/src/server/mcp/multi-session-client.ts +132 -0
- package/src/server/mcp/oauth-client.ts +1168 -0
- package/src/server/mcp/storage-oauth-provider.ts +239 -0
- package/src/server/storage/file-backend.ts +169 -0
- package/src/server/storage/index.ts +115 -0
- package/src/server/storage/memory-backend.ts +132 -0
- package/src/server/storage/redis-backend.ts +210 -0
- package/src/server/storage/redis.ts +160 -0
- package/src/server/storage/types.ts +109 -0
- package/src/shared/constants.ts +29 -0
- package/src/shared/errors.ts +133 -0
- package/src/shared/events.ts +166 -0
- package/src/shared/index.ts +70 -0
- package/src/shared/types.ts +274 -0
- package/src/shared/utils.ts +16 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2723 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { customAlphabet, nanoid } from 'nanoid';
|
|
3
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
5
|
+
import { UnauthorizedError as UnauthorizedError$1, discoverOAuthProtectedResourceMetadata, discoverAuthorizationServerMetadata, refreshAuthorization } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
6
|
+
import { ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { promises } from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
13
|
+
var __esm = (fn, res) => function __init() {
|
|
14
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
|
+
};
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
|
|
22
|
+
// src/server/storage/redis.ts
|
|
23
|
+
var redis_exports = {};
|
|
24
|
+
__export(redis_exports, {
|
|
25
|
+
closeRedis: () => closeRedis,
|
|
26
|
+
getRedis: () => getRedis,
|
|
27
|
+
initRedis: () => initRedis,
|
|
28
|
+
redis: () => redis,
|
|
29
|
+
setRedisInstance: () => setRedisInstance
|
|
30
|
+
});
|
|
31
|
+
async function initRedis(config) {
|
|
32
|
+
if (redisInstance) {
|
|
33
|
+
return redisInstance;
|
|
34
|
+
}
|
|
35
|
+
const url = config.url ?? process.env.REDIS_URL;
|
|
36
|
+
if (!url) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
"Redis URL is required. Set REDIS_URL environment variable or pass url in config."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
let Redis;
|
|
42
|
+
if (config.RedisConstructor) {
|
|
43
|
+
Redis = config.RedisConstructor;
|
|
44
|
+
} else {
|
|
45
|
+
try {
|
|
46
|
+
const ioredis = await import('ioredis');
|
|
47
|
+
Redis = ioredis.Redis;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"ioredis is not installed. Install it with:\n npm install ioredis\n\nOr use a different storage backend:\n MCP_TS_STORAGE_TYPE=memory (for development)\n MCP_TS_STORAGE_TYPE=file (for local persistence)"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
redisInstance = new Redis(url, {
|
|
55
|
+
lazyConnect: config.lazyConnect ?? true,
|
|
56
|
+
maxRetriesPerRequest: config.maxRetriesPerRequest ?? 1
|
|
57
|
+
});
|
|
58
|
+
if (config.verbose !== false) {
|
|
59
|
+
redisInstance.on("ready", () => {
|
|
60
|
+
console.log("\u2705 Redis connected");
|
|
61
|
+
});
|
|
62
|
+
redisInstance.on("error", (err) => {
|
|
63
|
+
console.error("\u274C Redis error:", err.message);
|
|
64
|
+
});
|
|
65
|
+
redisInstance.on("reconnecting", () => {
|
|
66
|
+
console.log("\u{1F504} Redis reconnecting...");
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
global.__redis = redisInstance;
|
|
70
|
+
global.__redisConfig = config;
|
|
71
|
+
return redisInstance;
|
|
72
|
+
}
|
|
73
|
+
async function getRedis() {
|
|
74
|
+
if (redisInstance) {
|
|
75
|
+
return redisInstance;
|
|
76
|
+
}
|
|
77
|
+
if (global.__redis) {
|
|
78
|
+
redisInstance = global.__redis;
|
|
79
|
+
return redisInstance;
|
|
80
|
+
}
|
|
81
|
+
return await initRedis({});
|
|
82
|
+
}
|
|
83
|
+
function setRedisInstance(instance) {
|
|
84
|
+
redisInstance = instance;
|
|
85
|
+
global.__redis = instance;
|
|
86
|
+
}
|
|
87
|
+
async function closeRedis() {
|
|
88
|
+
if (redisInstance) {
|
|
89
|
+
await redisInstance.quit();
|
|
90
|
+
redisInstance = null;
|
|
91
|
+
global.__redis = void 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
var redisInstance, redis;
|
|
95
|
+
var init_redis = __esm({
|
|
96
|
+
"src/server/storage/redis.ts"() {
|
|
97
|
+
redisInstance = null;
|
|
98
|
+
redis = new Proxy({}, {
|
|
99
|
+
get(_target, prop) {
|
|
100
|
+
return async (...args) => {
|
|
101
|
+
const instance = await getRedis();
|
|
102
|
+
const value = instance[prop];
|
|
103
|
+
if (typeof value === "function") {
|
|
104
|
+
return value.apply(instance, args);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// src/shared/constants.ts
|
|
114
|
+
var SESSION_TTL_SECONDS = 43200;
|
|
115
|
+
var STATE_EXPIRATION_MS = 10 * 60 * 1e3;
|
|
116
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
117
|
+
var REDIS_KEY_PREFIX = "mcp:session:";
|
|
118
|
+
var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
119
|
+
var DEFAULT_CLIENT_NAME = "MCP Assistant";
|
|
120
|
+
var DEFAULT_CLIENT_URI = "https://mcp-assistant.in";
|
|
121
|
+
var DEFAULT_LOGO_URI = "https://mcp-assistant.in/logo.png";
|
|
122
|
+
var DEFAULT_POLICY_URI = "https://mcp-assistant.in/privacy";
|
|
123
|
+
var SOFTWARE_ID = "@mcp-ts";
|
|
124
|
+
var SOFTWARE_VERSION = "1.0.0-beta.5";
|
|
125
|
+
var MCP_CLIENT_NAME = "mcp-ts-oauth-client";
|
|
126
|
+
var MCP_CLIENT_VERSION = "2.0";
|
|
127
|
+
|
|
128
|
+
// src/server/storage/redis-backend.ts
|
|
129
|
+
var firstChar = customAlphabet(
|
|
130
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
131
|
+
1
|
|
132
|
+
);
|
|
133
|
+
var rest = customAlphabet(
|
|
134
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
|
135
|
+
11
|
|
136
|
+
);
|
|
137
|
+
var RedisStorageBackend = class {
|
|
138
|
+
constructor(redis2) {
|
|
139
|
+
this.redis = redis2;
|
|
140
|
+
__publicField(this, "DEFAULT_TTL", SESSION_TTL_SECONDS);
|
|
141
|
+
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Generates Redis key for a specific session
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
getSessionKey(identity, sessionId) {
|
|
148
|
+
return `${this.KEY_PREFIX}${identity}:${sessionId}`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Generates Redis key for tracking all sessions for an identity
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
getIdentityKey(identity) {
|
|
155
|
+
return `mcp:identity:${identity}:sessions`;
|
|
156
|
+
}
|
|
157
|
+
generateSessionId() {
|
|
158
|
+
return firstChar() + rest();
|
|
159
|
+
}
|
|
160
|
+
async createSession(session, ttl) {
|
|
161
|
+
const { sessionId, identity } = session;
|
|
162
|
+
if (!sessionId || !identity) throw new Error("identity and sessionId required");
|
|
163
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
164
|
+
const identityKey = this.getIdentityKey(identity);
|
|
165
|
+
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
166
|
+
const result = await this.redis.set(
|
|
167
|
+
sessionKey,
|
|
168
|
+
JSON.stringify(session),
|
|
169
|
+
"EX",
|
|
170
|
+
effectiveTtl,
|
|
171
|
+
"NX"
|
|
172
|
+
);
|
|
173
|
+
if (result !== "OK") {
|
|
174
|
+
throw new Error(`Session ${sessionId} already exists`);
|
|
175
|
+
}
|
|
176
|
+
await this.redis.sadd(identityKey, sessionId);
|
|
177
|
+
}
|
|
178
|
+
async updateSession(identity, sessionId, data, ttl) {
|
|
179
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
180
|
+
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
181
|
+
const script = `
|
|
182
|
+
local currentStr = redis.call("GET", KEYS[1])
|
|
183
|
+
if not currentStr then
|
|
184
|
+
return 0
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
local current = cjson.decode(currentStr)
|
|
188
|
+
local updates = cjson.decode(ARGV[1])
|
|
189
|
+
|
|
190
|
+
for k,v in pairs(updates) do
|
|
191
|
+
current[k] = v
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
redis.call("SET", KEYS[1], cjson.encode(current), "EX", ARGV[2])
|
|
195
|
+
return 1
|
|
196
|
+
`;
|
|
197
|
+
const result = await this.redis.eval(
|
|
198
|
+
script,
|
|
199
|
+
1,
|
|
200
|
+
sessionKey,
|
|
201
|
+
JSON.stringify(data),
|
|
202
|
+
effectiveTtl
|
|
203
|
+
);
|
|
204
|
+
if (result === 0) {
|
|
205
|
+
throw new Error(`Session ${sessionId} not found for identity ${identity}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async getSession(identity, sessionId) {
|
|
209
|
+
try {
|
|
210
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
211
|
+
const sessionDataStr = await this.redis.get(sessionKey);
|
|
212
|
+
if (!sessionDataStr) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const sessionData = JSON.parse(sessionDataStr);
|
|
216
|
+
return sessionData;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error("[RedisStorage] Failed to get session:", error);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async getIdentityMcpSessions(identity) {
|
|
223
|
+
const identityKey = this.getIdentityKey(identity);
|
|
224
|
+
try {
|
|
225
|
+
return await this.redis.smembers(identityKey);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error(`[RedisStorage] Failed to get sessions for ${identity}:`, error);
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async getIdentitySessionsData(identity) {
|
|
232
|
+
try {
|
|
233
|
+
const sessionIds = await this.redis.smembers(this.getIdentityKey(identity));
|
|
234
|
+
if (sessionIds.length === 0) return [];
|
|
235
|
+
const results = await Promise.all(
|
|
236
|
+
sessionIds.map(async (sessionId) => {
|
|
237
|
+
const data = await this.redis.get(this.getSessionKey(identity, sessionId));
|
|
238
|
+
return data ? JSON.parse(data) : null;
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
return results.filter((session) => session !== null);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(`[RedisStorage] Failed to get session data for ${identity}:`, error);
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async removeSession(identity, sessionId) {
|
|
248
|
+
try {
|
|
249
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
250
|
+
const identityKey = this.getIdentityKey(identity);
|
|
251
|
+
await this.redis.srem(identityKey, sessionId);
|
|
252
|
+
await this.redis.del(sessionKey);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error("[RedisStorage] Failed to remove session:", error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async getAllSessionIds() {
|
|
258
|
+
try {
|
|
259
|
+
const pattern = `${this.KEY_PREFIX}*`;
|
|
260
|
+
const keys = await this.redis.keys(pattern);
|
|
261
|
+
return keys.map((key) => key.replace(this.KEY_PREFIX, ""));
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error("[RedisStorage] Failed to get all sessions:", error);
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async clearAll() {
|
|
268
|
+
try {
|
|
269
|
+
const pattern = `${this.KEY_PREFIX}*`;
|
|
270
|
+
const keys = await this.redis.keys(pattern);
|
|
271
|
+
if (keys.length > 0) {
|
|
272
|
+
await this.redis.del(...keys);
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error("[RedisStorage] Failed to clear sessions:", error);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async cleanupExpiredSessions() {
|
|
279
|
+
try {
|
|
280
|
+
const pattern = `${this.KEY_PREFIX}*`;
|
|
281
|
+
const keys = await this.redis.keys(pattern);
|
|
282
|
+
for (const key of keys) {
|
|
283
|
+
const ttl = await this.redis.ttl(key);
|
|
284
|
+
if (ttl <= 0) {
|
|
285
|
+
await this.redis.del(key);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error("[RedisStorage] Failed to cleanup expired sessions:", error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async disconnect() {
|
|
293
|
+
try {
|
|
294
|
+
await this.redis.quit();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
console.error("[RedisStorage] Failed to disconnect:", error);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
var firstChar2 = customAlphabet(
|
|
301
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
302
|
+
1
|
|
303
|
+
);
|
|
304
|
+
var rest2 = customAlphabet(
|
|
305
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
|
306
|
+
11
|
|
307
|
+
);
|
|
308
|
+
var MemoryStorageBackend = class {
|
|
309
|
+
constructor() {
|
|
310
|
+
// Map<identity:sessionId, SessionData>
|
|
311
|
+
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
312
|
+
// Map<identity, Set<sessionId>>
|
|
313
|
+
__publicField(this, "identitySessions", /* @__PURE__ */ new Map());
|
|
314
|
+
}
|
|
315
|
+
getSessionKey(identity, sessionId) {
|
|
316
|
+
return `${identity}:${sessionId}`;
|
|
317
|
+
}
|
|
318
|
+
generateSessionId() {
|
|
319
|
+
return firstChar2() + rest2();
|
|
320
|
+
}
|
|
321
|
+
async createSession(session, ttl) {
|
|
322
|
+
const { sessionId, identity } = session;
|
|
323
|
+
if (!sessionId || !identity) throw new Error("identity and sessionId required");
|
|
324
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
325
|
+
if (this.sessions.has(sessionKey)) {
|
|
326
|
+
throw new Error(`Session ${sessionId} already exists`);
|
|
327
|
+
}
|
|
328
|
+
this.sessions.set(sessionKey, session);
|
|
329
|
+
if (!this.identitySessions.has(identity)) {
|
|
330
|
+
this.identitySessions.set(identity, /* @__PURE__ */ new Set());
|
|
331
|
+
}
|
|
332
|
+
this.identitySessions.get(identity).add(sessionId);
|
|
333
|
+
}
|
|
334
|
+
async updateSession(identity, sessionId, data, ttl) {
|
|
335
|
+
if (!identity || !sessionId) throw new Error("identity and sessionId required");
|
|
336
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
337
|
+
const current = this.sessions.get(sessionKey);
|
|
338
|
+
if (!current) {
|
|
339
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
340
|
+
}
|
|
341
|
+
const updated = {
|
|
342
|
+
...current,
|
|
343
|
+
...data
|
|
344
|
+
};
|
|
345
|
+
this.sessions.set(sessionKey, updated);
|
|
346
|
+
}
|
|
347
|
+
async getSession(identity, sessionId) {
|
|
348
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
349
|
+
return this.sessions.get(sessionKey) || null;
|
|
350
|
+
}
|
|
351
|
+
async getIdentityMcpSessions(identity) {
|
|
352
|
+
const set = this.identitySessions.get(identity);
|
|
353
|
+
return set ? Array.from(set) : [];
|
|
354
|
+
}
|
|
355
|
+
async getIdentitySessionsData(identity) {
|
|
356
|
+
const set = this.identitySessions.get(identity);
|
|
357
|
+
if (!set) return [];
|
|
358
|
+
const results = [];
|
|
359
|
+
for (const sessionId of set) {
|
|
360
|
+
const session = this.sessions.get(this.getSessionKey(identity, sessionId));
|
|
361
|
+
if (session) {
|
|
362
|
+
results.push(session);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return results;
|
|
366
|
+
}
|
|
367
|
+
async removeSession(identity, sessionId) {
|
|
368
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
369
|
+
this.sessions.delete(sessionKey);
|
|
370
|
+
const set = this.identitySessions.get(identity);
|
|
371
|
+
if (set) {
|
|
372
|
+
set.delete(sessionId);
|
|
373
|
+
if (set.size === 0) {
|
|
374
|
+
this.identitySessions.delete(identity);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async getAllSessionIds() {
|
|
379
|
+
return Array.from(this.sessions.values()).map((s) => s.sessionId);
|
|
380
|
+
}
|
|
381
|
+
async clearAll() {
|
|
382
|
+
this.sessions.clear();
|
|
383
|
+
this.identitySessions.clear();
|
|
384
|
+
}
|
|
385
|
+
async cleanupExpiredSessions() {
|
|
386
|
+
}
|
|
387
|
+
async disconnect() {
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
var firstChar3 = customAlphabet(
|
|
391
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
392
|
+
1
|
|
393
|
+
);
|
|
394
|
+
var rest3 = customAlphabet(
|
|
395
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
|
396
|
+
11
|
|
397
|
+
);
|
|
398
|
+
var FileStorageBackend = class {
|
|
399
|
+
/**
|
|
400
|
+
* @param options.path Path to the JSON file storage (default: ./sessions.json)
|
|
401
|
+
*/
|
|
402
|
+
constructor(options = {}) {
|
|
403
|
+
__publicField(this, "filePath");
|
|
404
|
+
__publicField(this, "memoryCache", null);
|
|
405
|
+
__publicField(this, "initialized", false);
|
|
406
|
+
this.filePath = options.path || "./sessions.json";
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Initialize storage: ensure file exists and load into memory cache
|
|
410
|
+
*/
|
|
411
|
+
async init() {
|
|
412
|
+
if (this.initialized) return;
|
|
413
|
+
try {
|
|
414
|
+
const dir = path.dirname(this.filePath);
|
|
415
|
+
await promises.mkdir(dir, { recursive: true });
|
|
416
|
+
const data = await promises.readFile(this.filePath, "utf-8");
|
|
417
|
+
const json = JSON.parse(data);
|
|
418
|
+
this.memoryCache = /* @__PURE__ */ new Map();
|
|
419
|
+
if (Array.isArray(json)) {
|
|
420
|
+
json.forEach((s) => {
|
|
421
|
+
this.memoryCache.set(this.getSessionKey(s.identity || "unknown", s.sessionId), s);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
} catch (error) {
|
|
425
|
+
if (error.code === "ENOENT") {
|
|
426
|
+
this.memoryCache = /* @__PURE__ */ new Map();
|
|
427
|
+
await this.flush();
|
|
428
|
+
} else {
|
|
429
|
+
console.error("[FileStorage] Failed to load sessions:", error);
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
this.initialized = true;
|
|
434
|
+
}
|
|
435
|
+
async ensureInitialized() {
|
|
436
|
+
if (!this.initialized) await this.init();
|
|
437
|
+
}
|
|
438
|
+
async flush() {
|
|
439
|
+
if (!this.memoryCache) return;
|
|
440
|
+
const sessions = Array.from(this.memoryCache.values());
|
|
441
|
+
await promises.writeFile(this.filePath, JSON.stringify(sessions, null, 2), "utf-8");
|
|
442
|
+
}
|
|
443
|
+
getSessionKey(identity, sessionId) {
|
|
444
|
+
return `${identity}:${sessionId}`;
|
|
445
|
+
}
|
|
446
|
+
generateSessionId() {
|
|
447
|
+
return firstChar3() + rest3();
|
|
448
|
+
}
|
|
449
|
+
async createSession(session, ttl) {
|
|
450
|
+
await this.ensureInitialized();
|
|
451
|
+
const { sessionId, identity } = session;
|
|
452
|
+
if (!sessionId || !identity) throw new Error("identity and sessionId required");
|
|
453
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
454
|
+
if (this.memoryCache.has(sessionKey)) {
|
|
455
|
+
throw new Error(`Session ${sessionId} already exists`);
|
|
456
|
+
}
|
|
457
|
+
this.memoryCache.set(sessionKey, session);
|
|
458
|
+
await this.flush();
|
|
459
|
+
}
|
|
460
|
+
async updateSession(identity, sessionId, data, ttl) {
|
|
461
|
+
await this.ensureInitialized();
|
|
462
|
+
if (!identity || !sessionId) throw new Error("identity and sessionId required");
|
|
463
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
464
|
+
const current = this.memoryCache.get(sessionKey);
|
|
465
|
+
if (!current) {
|
|
466
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
467
|
+
}
|
|
468
|
+
const updated = {
|
|
469
|
+
...current,
|
|
470
|
+
...data
|
|
471
|
+
};
|
|
472
|
+
this.memoryCache.set(sessionKey, updated);
|
|
473
|
+
await this.flush();
|
|
474
|
+
}
|
|
475
|
+
async getSession(identity, sessionId) {
|
|
476
|
+
await this.ensureInitialized();
|
|
477
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
478
|
+
return this.memoryCache.get(sessionKey) || null;
|
|
479
|
+
}
|
|
480
|
+
async getIdentitySessionsData(identity) {
|
|
481
|
+
await this.ensureInitialized();
|
|
482
|
+
return Array.from(this.memoryCache.values()).filter((s) => s.identity === identity);
|
|
483
|
+
}
|
|
484
|
+
async getIdentityMcpSessions(identity) {
|
|
485
|
+
await this.ensureInitialized();
|
|
486
|
+
return Array.from(this.memoryCache.values()).filter((s) => s.identity === identity).map((s) => s.sessionId);
|
|
487
|
+
}
|
|
488
|
+
async removeSession(identity, sessionId) {
|
|
489
|
+
await this.ensureInitialized();
|
|
490
|
+
const sessionKey = this.getSessionKey(identity, sessionId);
|
|
491
|
+
if (this.memoryCache.delete(sessionKey)) {
|
|
492
|
+
await this.flush();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async getAllSessionIds() {
|
|
496
|
+
await this.ensureInitialized();
|
|
497
|
+
return Array.from(this.memoryCache.values()).map((s) => s.sessionId);
|
|
498
|
+
}
|
|
499
|
+
async clearAll() {
|
|
500
|
+
await this.ensureInitialized();
|
|
501
|
+
this.memoryCache.clear();
|
|
502
|
+
await this.flush();
|
|
503
|
+
}
|
|
504
|
+
async cleanupExpiredSessions() {
|
|
505
|
+
await this.ensureInitialized();
|
|
506
|
+
}
|
|
507
|
+
async disconnect() {
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// src/server/storage/index.ts
|
|
512
|
+
var storageInstance = null;
|
|
513
|
+
var storagePromise = null;
|
|
514
|
+
async function createStorage() {
|
|
515
|
+
const type = process.env.MCP_TS_STORAGE_TYPE?.toLowerCase();
|
|
516
|
+
if (type === "redis") {
|
|
517
|
+
if (!process.env.REDIS_URL) {
|
|
518
|
+
console.warn('[Storage] MCP_TS_STORAGE_TYPE is "redis" but REDIS_URL is missing');
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const { getRedis: getRedis2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
|
|
522
|
+
const redis2 = await getRedis2();
|
|
523
|
+
console.log("[Storage] Using Redis storage (Explicit)");
|
|
524
|
+
return new RedisStorageBackend(redis2);
|
|
525
|
+
} catch (error) {
|
|
526
|
+
console.error("[Storage] Failed to initialize Redis:", error.message);
|
|
527
|
+
console.log("[Storage] Falling back to In-Memory storage");
|
|
528
|
+
return new MemoryStorageBackend();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (type === "file") {
|
|
532
|
+
const filePath = process.env.MCP_TS_STORAGE_FILE;
|
|
533
|
+
if (!filePath) {
|
|
534
|
+
console.warn('[Storage] MCP_TS_STORAGE_TYPE is "file" but MCP_TS_STORAGE_FILE is missing');
|
|
535
|
+
}
|
|
536
|
+
console.log(`[Storage] Using File storage (${filePath}) (Explicit)`);
|
|
537
|
+
const store = new FileStorageBackend({ path: filePath });
|
|
538
|
+
store.init().catch((err) => console.error("[Storage] Failed to initialize file storage:", err));
|
|
539
|
+
return store;
|
|
540
|
+
}
|
|
541
|
+
if (type === "memory") {
|
|
542
|
+
console.log("[Storage] Using In-Memory storage (Explicit)");
|
|
543
|
+
return new MemoryStorageBackend();
|
|
544
|
+
}
|
|
545
|
+
if (process.env.REDIS_URL) {
|
|
546
|
+
try {
|
|
547
|
+
const { getRedis: getRedis2 } = await Promise.resolve().then(() => (init_redis(), redis_exports));
|
|
548
|
+
const redis2 = await getRedis2();
|
|
549
|
+
console.log("[Storage] Auto-detected REDIS_URL. Using Redis storage.");
|
|
550
|
+
return new RedisStorageBackend(redis2);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
console.error("[Storage] Redis auto-detection failed:", error.message);
|
|
553
|
+
console.log("[Storage] Falling back to In-Memory storage");
|
|
554
|
+
return new MemoryStorageBackend();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (process.env.MCP_TS_STORAGE_FILE) {
|
|
558
|
+
console.log(`[Storage] Auto-detected MCP_TS_STORAGE_FILE. Using File storage (${process.env.MCP_TS_STORAGE_FILE}).`);
|
|
559
|
+
const store = new FileStorageBackend({ path: process.env.MCP_TS_STORAGE_FILE });
|
|
560
|
+
store.init().catch((err) => console.error("[Storage] Failed to initialize file storage:", err));
|
|
561
|
+
return store;
|
|
562
|
+
}
|
|
563
|
+
console.log("[Storage] No storage configured. Using In-Memory storage (Default).");
|
|
564
|
+
return new MemoryStorageBackend();
|
|
565
|
+
}
|
|
566
|
+
async function getStorage() {
|
|
567
|
+
if (storageInstance) {
|
|
568
|
+
return storageInstance;
|
|
569
|
+
}
|
|
570
|
+
if (!storagePromise) {
|
|
571
|
+
storagePromise = createStorage();
|
|
572
|
+
}
|
|
573
|
+
storageInstance = await storagePromise;
|
|
574
|
+
return storageInstance;
|
|
575
|
+
}
|
|
576
|
+
var storage = new Proxy({}, {
|
|
577
|
+
get(_target, prop) {
|
|
578
|
+
return async (...args) => {
|
|
579
|
+
const instance = await getStorage();
|
|
580
|
+
const value = instance[prop];
|
|
581
|
+
if (typeof value === "function") {
|
|
582
|
+
return value.apply(instance, args);
|
|
583
|
+
}
|
|
584
|
+
return value;
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// src/server/mcp/storage-oauth-provider.ts
|
|
590
|
+
var StorageOAuthClientProvider = class {
|
|
591
|
+
/**
|
|
592
|
+
* Creates a new Storage-backed OAuth provider
|
|
593
|
+
* @param identity - User/Client identifier
|
|
594
|
+
* @param serverId - Server identifier (for tracking which server this OAuth session belongs to)
|
|
595
|
+
* @param sessionId - Session identifier (used as OAuth state)
|
|
596
|
+
* @param clientName - OAuth client name
|
|
597
|
+
* @param baseRedirectUrl - OAuth callback URL
|
|
598
|
+
* @param onRedirect - Optional callback when redirect to authorization is needed
|
|
599
|
+
*/
|
|
600
|
+
constructor(identity, serverId, sessionId, clientName, baseRedirectUrl, onRedirect) {
|
|
601
|
+
this.identity = identity;
|
|
602
|
+
this.serverId = serverId;
|
|
603
|
+
this.sessionId = sessionId;
|
|
604
|
+
this.clientName = clientName;
|
|
605
|
+
this.baseRedirectUrl = baseRedirectUrl;
|
|
606
|
+
__publicField(this, "_authUrl");
|
|
607
|
+
__publicField(this, "_clientId");
|
|
608
|
+
__publicField(this, "onRedirectCallback");
|
|
609
|
+
__publicField(this, "tokenExpiresAt");
|
|
610
|
+
this.onRedirectCallback = onRedirect;
|
|
611
|
+
}
|
|
612
|
+
get clientMetadata() {
|
|
613
|
+
return {
|
|
614
|
+
client_name: this.clientName,
|
|
615
|
+
client_uri: this.clientUri,
|
|
616
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
617
|
+
redirect_uris: [this.redirectUrl],
|
|
618
|
+
response_types: ["code"],
|
|
619
|
+
token_endpoint_auth_method: "none",
|
|
620
|
+
...this._clientId ? { client_id: this._clientId } : {}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
get clientUri() {
|
|
624
|
+
return new URL(this.redirectUrl).origin;
|
|
625
|
+
}
|
|
626
|
+
get redirectUrl() {
|
|
627
|
+
return this.baseRedirectUrl;
|
|
628
|
+
}
|
|
629
|
+
get clientId() {
|
|
630
|
+
return this._clientId;
|
|
631
|
+
}
|
|
632
|
+
set clientId(clientId_) {
|
|
633
|
+
this._clientId = clientId_;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Loads OAuth data from storage session
|
|
637
|
+
* @private
|
|
638
|
+
*/
|
|
639
|
+
async getSessionData() {
|
|
640
|
+
const data = await storage.getSession(this.identity, this.sessionId);
|
|
641
|
+
if (!data) {
|
|
642
|
+
return {};
|
|
643
|
+
}
|
|
644
|
+
return data;
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Saves OAuth data to storage
|
|
648
|
+
* @param data - Partial OAuth data to save
|
|
649
|
+
* @private
|
|
650
|
+
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
651
|
+
*/
|
|
652
|
+
async saveSessionData(data) {
|
|
653
|
+
await storage.updateSession(this.identity, this.sessionId, data);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Retrieves stored OAuth client information
|
|
657
|
+
*/
|
|
658
|
+
async clientInformation() {
|
|
659
|
+
const data = await this.getSessionData();
|
|
660
|
+
if (data.clientId && !this._clientId) {
|
|
661
|
+
this._clientId = data.clientId;
|
|
662
|
+
}
|
|
663
|
+
return data.clientInformation;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Stores OAuth client information
|
|
667
|
+
*/
|
|
668
|
+
async saveClientInformation(clientInformation) {
|
|
669
|
+
await this.saveSessionData({
|
|
670
|
+
clientInformation,
|
|
671
|
+
clientId: clientInformation.client_id
|
|
672
|
+
});
|
|
673
|
+
this.clientId = clientInformation.client_id;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Stores OAuth tokens
|
|
677
|
+
*/
|
|
678
|
+
async saveTokens(tokens) {
|
|
679
|
+
const data = { tokens };
|
|
680
|
+
if (tokens.expires_in) {
|
|
681
|
+
this.tokenExpiresAt = Date.now() + tokens.expires_in * 1e3 - TOKEN_EXPIRY_BUFFER_MS;
|
|
682
|
+
}
|
|
683
|
+
await this.saveSessionData(data);
|
|
684
|
+
}
|
|
685
|
+
get authUrl() {
|
|
686
|
+
return this._authUrl;
|
|
687
|
+
}
|
|
688
|
+
async state() {
|
|
689
|
+
return this.sessionId;
|
|
690
|
+
}
|
|
691
|
+
async checkState(state) {
|
|
692
|
+
const data = await storage.getSession(this.identity, this.sessionId);
|
|
693
|
+
if (!data) {
|
|
694
|
+
return { valid: false, error: "Session not found" };
|
|
695
|
+
}
|
|
696
|
+
return { valid: true, serverId: this.serverId };
|
|
697
|
+
}
|
|
698
|
+
async consumeState(state) {
|
|
699
|
+
}
|
|
700
|
+
async redirectToAuthorization(authUrl) {
|
|
701
|
+
this._authUrl = authUrl.toString();
|
|
702
|
+
if (this.onRedirectCallback) {
|
|
703
|
+
this.onRedirectCallback(authUrl.toString());
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async invalidateCredentials(scope) {
|
|
707
|
+
if (scope === "all") {
|
|
708
|
+
await storage.removeSession(this.identity, this.sessionId);
|
|
709
|
+
} else {
|
|
710
|
+
await this.getSessionData();
|
|
711
|
+
const updates = {};
|
|
712
|
+
if (scope === "client") {
|
|
713
|
+
updates.clientInformation = void 0;
|
|
714
|
+
updates.clientId = void 0;
|
|
715
|
+
} else if (scope === "tokens") {
|
|
716
|
+
updates.tokens = void 0;
|
|
717
|
+
} else if (scope === "verifier") {
|
|
718
|
+
updates.codeVerifier = void 0;
|
|
719
|
+
}
|
|
720
|
+
await this.saveSessionData(updates);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async saveCodeVerifier(verifier) {
|
|
724
|
+
await this.saveSessionData({ codeVerifier: verifier });
|
|
725
|
+
}
|
|
726
|
+
async codeVerifier() {
|
|
727
|
+
const data = await this.getSessionData();
|
|
728
|
+
if (data.clientId && !this._clientId) {
|
|
729
|
+
this._clientId = data.clientId;
|
|
730
|
+
}
|
|
731
|
+
if (!data.codeVerifier) {
|
|
732
|
+
throw new Error("No code verifier found");
|
|
733
|
+
}
|
|
734
|
+
return data.codeVerifier;
|
|
735
|
+
}
|
|
736
|
+
async deleteCodeVerifier() {
|
|
737
|
+
await this.saveSessionData({ codeVerifier: void 0 });
|
|
738
|
+
}
|
|
739
|
+
async tokens() {
|
|
740
|
+
const data = await this.getSessionData();
|
|
741
|
+
if (data.clientId && !this._clientId) {
|
|
742
|
+
this._clientId = data.clientId;
|
|
743
|
+
}
|
|
744
|
+
return data.tokens;
|
|
745
|
+
}
|
|
746
|
+
isTokenExpired() {
|
|
747
|
+
if (!this.tokenExpiresAt) {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
return Date.now() >= this.tokenExpiresAt;
|
|
751
|
+
}
|
|
752
|
+
setTokenExpiresAt(expiresAt) {
|
|
753
|
+
this.tokenExpiresAt = expiresAt;
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// src/shared/utils.ts
|
|
758
|
+
function sanitizeServerLabel(name) {
|
|
759
|
+
let sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "_").replace(/_{2,}/g, "_").toLowerCase();
|
|
760
|
+
if (!/^[a-zA-Z]/.test(sanitized)) {
|
|
761
|
+
sanitized = "s_" + sanitized;
|
|
762
|
+
}
|
|
763
|
+
return sanitized;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/shared/events.ts
|
|
767
|
+
var Emitter = class {
|
|
768
|
+
constructor() {
|
|
769
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Set());
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Subscribe to events
|
|
773
|
+
* @param listener - Callback function to handle events
|
|
774
|
+
* @returns Disposable to unsubscribe
|
|
775
|
+
*/
|
|
776
|
+
get event() {
|
|
777
|
+
return (listener) => {
|
|
778
|
+
this.listeners.add(listener);
|
|
779
|
+
return {
|
|
780
|
+
dispose: () => {
|
|
781
|
+
this.listeners.delete(listener);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Fire an event to all listeners
|
|
788
|
+
* @param event - Event data to emit
|
|
789
|
+
*/
|
|
790
|
+
fire(event) {
|
|
791
|
+
for (const listener of this.listeners) {
|
|
792
|
+
try {
|
|
793
|
+
listener(event);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
console.error("[Emitter] Error in event listener:", error);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Clear all listeners
|
|
801
|
+
*/
|
|
802
|
+
dispose() {
|
|
803
|
+
this.listeners.clear();
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Get number of active listeners
|
|
807
|
+
*/
|
|
808
|
+
get listenerCount() {
|
|
809
|
+
return this.listeners.size;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
var DisposableStore = class {
|
|
813
|
+
constructor() {
|
|
814
|
+
__publicField(this, "disposables", /* @__PURE__ */ new Set());
|
|
815
|
+
}
|
|
816
|
+
add(disposable) {
|
|
817
|
+
this.disposables.add(disposable);
|
|
818
|
+
}
|
|
819
|
+
dispose() {
|
|
820
|
+
for (const disposable of this.disposables) {
|
|
821
|
+
disposable.dispose();
|
|
822
|
+
}
|
|
823
|
+
this.disposables.clear();
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// src/shared/errors.ts
|
|
828
|
+
var McpError = class extends Error {
|
|
829
|
+
constructor(code, message, cause) {
|
|
830
|
+
super(message);
|
|
831
|
+
this.code = code;
|
|
832
|
+
this.cause = cause;
|
|
833
|
+
this.name = "McpError";
|
|
834
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
835
|
+
}
|
|
836
|
+
toJSON() {
|
|
837
|
+
return {
|
|
838
|
+
name: this.name,
|
|
839
|
+
code: this.code,
|
|
840
|
+
message: this.message,
|
|
841
|
+
...this.cause ? { cause: this.cause.message } : {}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
var UnauthorizedError = class extends McpError {
|
|
846
|
+
constructor(message = "OAuth authorization required", cause) {
|
|
847
|
+
super("UNAUTHORIZED", message, cause);
|
|
848
|
+
this.name = "UnauthorizedError";
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
var ConnectionError = class extends McpError {
|
|
852
|
+
constructor(message, cause) {
|
|
853
|
+
super("CONNECTION_ERROR", message, cause);
|
|
854
|
+
this.name = "ConnectionError";
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
var SessionNotFoundError = class extends McpError {
|
|
858
|
+
constructor(sessionId, cause) {
|
|
859
|
+
super("SESSION_NOT_FOUND", `Session not found: ${sessionId}`, cause);
|
|
860
|
+
this.name = "SessionNotFoundError";
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
var SessionValidationError = class extends McpError {
|
|
864
|
+
constructor(message, cause) {
|
|
865
|
+
super("SESSION_VALIDATION_ERROR", message, cause);
|
|
866
|
+
this.name = "SessionValidationError";
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
var AuthenticationError = class extends McpError {
|
|
870
|
+
constructor(message, cause) {
|
|
871
|
+
super("AUTH_ERROR", message, cause);
|
|
872
|
+
this.name = "AuthenticationError";
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
var InvalidStateError = class extends McpError {
|
|
876
|
+
constructor(message = "Invalid OAuth state", cause) {
|
|
877
|
+
super("INVALID_STATE", message, cause);
|
|
878
|
+
this.name = "InvalidStateError";
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
var NotConnectedError = class extends McpError {
|
|
882
|
+
constructor(message = "Not connected to server", cause) {
|
|
883
|
+
super("NOT_CONNECTED", message, cause);
|
|
884
|
+
this.name = "NotConnectedError";
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
var ConfigurationError = class extends McpError {
|
|
888
|
+
constructor(message, cause) {
|
|
889
|
+
super("CONFIGURATION_ERROR", message, cause);
|
|
890
|
+
this.name = "ConfigurationError";
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
var ToolExecutionError = class extends McpError {
|
|
894
|
+
constructor(toolName, message, cause) {
|
|
895
|
+
super("TOOL_EXECUTION_ERROR", `Tool '${toolName}' failed: ${message}`, cause);
|
|
896
|
+
this.name = "ToolExecutionError";
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var RpcErrorCodes = {
|
|
900
|
+
EXECUTION_ERROR: "EXECUTION_ERROR",
|
|
901
|
+
MISSING_IDENTITY: "MISSING_IDENTITY",
|
|
902
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
903
|
+
NO_CONNECTION: "NO_CONNECTION",
|
|
904
|
+
UNKNOWN_METHOD: "UNKNOWN_METHOD",
|
|
905
|
+
INVALID_PARAMS: "INVALID_PARAMS"
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
// src/server/mcp/oauth-client.ts
|
|
909
|
+
var MCPClient = class _MCPClient {
|
|
910
|
+
/**
|
|
911
|
+
* Creates a new MCP client instance
|
|
912
|
+
* Can be initialized with minimal options (identity + sessionId) for session restoration
|
|
913
|
+
* @param options - Client configuration options
|
|
914
|
+
*/
|
|
915
|
+
constructor(options) {
|
|
916
|
+
__publicField(this, "client", null);
|
|
917
|
+
__publicField(this, "oauthProvider", null);
|
|
918
|
+
__publicField(this, "transport", null);
|
|
919
|
+
__publicField(this, "identity");
|
|
920
|
+
__publicField(this, "serverId");
|
|
921
|
+
__publicField(this, "sessionId");
|
|
922
|
+
__publicField(this, "serverName");
|
|
923
|
+
__publicField(this, "transportType");
|
|
924
|
+
__publicField(this, "serverUrl");
|
|
925
|
+
__publicField(this, "callbackUrl");
|
|
926
|
+
__publicField(this, "onRedirect");
|
|
927
|
+
__publicField(this, "tokens");
|
|
928
|
+
__publicField(this, "tokenExpiresAt");
|
|
929
|
+
__publicField(this, "clientInformation");
|
|
930
|
+
__publicField(this, "clientId");
|
|
931
|
+
__publicField(this, "clientSecret");
|
|
932
|
+
__publicField(this, "onSaveTokens");
|
|
933
|
+
__publicField(this, "headers");
|
|
934
|
+
/** OAuth Client Metadata */
|
|
935
|
+
__publicField(this, "clientName");
|
|
936
|
+
__publicField(this, "clientUri");
|
|
937
|
+
__publicField(this, "logoUri");
|
|
938
|
+
__publicField(this, "policyUri");
|
|
939
|
+
/** Event emitters for connection lifecycle */
|
|
940
|
+
__publicField(this, "_onConnectionEvent", new Emitter());
|
|
941
|
+
__publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
|
|
942
|
+
__publicField(this, "_onObservabilityEvent", new Emitter());
|
|
943
|
+
__publicField(this, "onObservabilityEvent", this._onObservabilityEvent.event);
|
|
944
|
+
__publicField(this, "currentState", "DISCONNECTED");
|
|
945
|
+
this.serverUrl = options.serverUrl;
|
|
946
|
+
this.serverName = options.serverName;
|
|
947
|
+
this.callbackUrl = options.callbackUrl;
|
|
948
|
+
this.onRedirect = options.onRedirect;
|
|
949
|
+
this.identity = options.identity;
|
|
950
|
+
this.serverId = options.serverId;
|
|
951
|
+
this.sessionId = options.sessionId;
|
|
952
|
+
this.transportType = options.transportType;
|
|
953
|
+
this.tokens = options.tokens;
|
|
954
|
+
this.tokenExpiresAt = options.tokenExpiresAt;
|
|
955
|
+
this.clientInformation = options.clientInformation;
|
|
956
|
+
this.clientId = options.clientId;
|
|
957
|
+
this.clientSecret = options.clientSecret;
|
|
958
|
+
this.onSaveTokens = options.onSaveTokens;
|
|
959
|
+
this.headers = options.headers;
|
|
960
|
+
this.clientName = options.clientName;
|
|
961
|
+
this.clientUri = options.clientUri;
|
|
962
|
+
this.logoUri = options.logoUri;
|
|
963
|
+
this.policyUri = options.policyUri;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Emit a connection state change event
|
|
967
|
+
* @private
|
|
968
|
+
*/
|
|
969
|
+
emitStateChange(newState) {
|
|
970
|
+
const previousState = this.currentState;
|
|
971
|
+
this.currentState = newState;
|
|
972
|
+
if (!this.serverId) return;
|
|
973
|
+
this._onConnectionEvent.fire({
|
|
974
|
+
type: "state_changed",
|
|
975
|
+
sessionId: this.sessionId,
|
|
976
|
+
serverId: this.serverId,
|
|
977
|
+
serverName: this.serverName || this.serverId,
|
|
978
|
+
state: newState,
|
|
979
|
+
previousState,
|
|
980
|
+
timestamp: Date.now()
|
|
981
|
+
});
|
|
982
|
+
this._onObservabilityEvent.fire({
|
|
983
|
+
type: "mcp:client:state_change",
|
|
984
|
+
level: "info",
|
|
985
|
+
message: `Connection state: ${previousState} \u2192 ${newState}`,
|
|
986
|
+
displayMessage: `State changed to ${newState}`,
|
|
987
|
+
sessionId: this.sessionId,
|
|
988
|
+
serverId: this.serverId,
|
|
989
|
+
payload: { previousState, newState },
|
|
990
|
+
timestamp: Date.now(),
|
|
991
|
+
id: nanoid()
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Emit an error event
|
|
996
|
+
* @private
|
|
997
|
+
*/
|
|
998
|
+
emitError(error, errorType = "unknown") {
|
|
999
|
+
if (!this.serverId) return;
|
|
1000
|
+
this._onConnectionEvent.fire({
|
|
1001
|
+
type: "error",
|
|
1002
|
+
sessionId: this.sessionId,
|
|
1003
|
+
serverId: this.serverId,
|
|
1004
|
+
error,
|
|
1005
|
+
errorType,
|
|
1006
|
+
timestamp: Date.now()
|
|
1007
|
+
});
|
|
1008
|
+
this._onObservabilityEvent.fire({
|
|
1009
|
+
type: "mcp:client:error",
|
|
1010
|
+
level: "error",
|
|
1011
|
+
message: error,
|
|
1012
|
+
displayMessage: error,
|
|
1013
|
+
sessionId: this.sessionId,
|
|
1014
|
+
serverId: this.serverId,
|
|
1015
|
+
payload: { errorType, error },
|
|
1016
|
+
timestamp: Date.now(),
|
|
1017
|
+
id: nanoid()
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Emit a progress event
|
|
1022
|
+
* @private
|
|
1023
|
+
*/
|
|
1024
|
+
emitProgress(message) {
|
|
1025
|
+
if (!this.serverId) return;
|
|
1026
|
+
this._onConnectionEvent.fire({
|
|
1027
|
+
type: "progress",
|
|
1028
|
+
sessionId: this.sessionId,
|
|
1029
|
+
serverId: this.serverId,
|
|
1030
|
+
message,
|
|
1031
|
+
timestamp: Date.now()
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Get current connection state
|
|
1036
|
+
*/
|
|
1037
|
+
getConnectionState() {
|
|
1038
|
+
return this.currentState;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Helper to create a transport instance
|
|
1042
|
+
* @param type - The transport type to create
|
|
1043
|
+
* @returns Configured transport instance
|
|
1044
|
+
* @private
|
|
1045
|
+
*/
|
|
1046
|
+
getTransport(type) {
|
|
1047
|
+
if (!this.serverUrl) {
|
|
1048
|
+
throw new Error("Server URL is required to create transport");
|
|
1049
|
+
}
|
|
1050
|
+
const baseUrl = new URL(this.serverUrl);
|
|
1051
|
+
const transportOptions = {
|
|
1052
|
+
authProvider: this.oauthProvider,
|
|
1053
|
+
...this.headers && { headers: this.headers },
|
|
1054
|
+
/**
|
|
1055
|
+
* Custom fetch implementation to handle connection timeouts.
|
|
1056
|
+
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
1057
|
+
* This wrapper enforces a timeout and properly uses AbortController to unblock the request.
|
|
1058
|
+
*/
|
|
1059
|
+
fetch: (url, init) => {
|
|
1060
|
+
const timeout = 3e4;
|
|
1061
|
+
const controller = new AbortController();
|
|
1062
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1063
|
+
const signal = init?.signal ? (
|
|
1064
|
+
// @ts-ignore: AbortSignal.any is available in Node 20+
|
|
1065
|
+
AbortSignal.any ? AbortSignal.any([init.signal, controller.signal]) : controller.signal
|
|
1066
|
+
) : controller.signal;
|
|
1067
|
+
return fetch(url, { ...init, signal }).finally(() => clearTimeout(timeoutId));
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
if (type === "sse") {
|
|
1071
|
+
return new SSEClientTransport(baseUrl, transportOptions);
|
|
1072
|
+
} else {
|
|
1073
|
+
return new StreamableHTTPClientTransport(baseUrl, transportOptions);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Initializes client components (client, transport, OAuth provider)
|
|
1078
|
+
* Loads missing configuration from Redis session store if needed
|
|
1079
|
+
* This method is idempotent and safe to call multiple times
|
|
1080
|
+
* @private
|
|
1081
|
+
*/
|
|
1082
|
+
async initialize() {
|
|
1083
|
+
if (this.client && this.oauthProvider) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
this.emitStateChange("INITIALIZING");
|
|
1087
|
+
this.emitProgress("Loading session configuration...");
|
|
1088
|
+
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1089
|
+
const sessionData = await storage.getSession(this.identity, this.sessionId);
|
|
1090
|
+
if (!sessionData) {
|
|
1091
|
+
throw new Error(`Session not found: ${this.sessionId}`);
|
|
1092
|
+
}
|
|
1093
|
+
this.serverUrl = this.serverUrl || sessionData.serverUrl;
|
|
1094
|
+
this.callbackUrl = this.callbackUrl || sessionData.callbackUrl;
|
|
1095
|
+
this.serverName = this.serverName || sessionData.serverName;
|
|
1096
|
+
this.serverId = this.serverId || sessionData.serverId || "unknown";
|
|
1097
|
+
this.headers = this.headers || sessionData.headers;
|
|
1098
|
+
}
|
|
1099
|
+
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
1100
|
+
throw new Error("Missing required connection metadata");
|
|
1101
|
+
}
|
|
1102
|
+
const clientMetadata = {
|
|
1103
|
+
client_name: this.clientName || "MCP Assistant",
|
|
1104
|
+
redirect_uris: [this.callbackUrl],
|
|
1105
|
+
token_endpoint_auth_method: this.clientSecret ? "client_secret_basic" : "none",
|
|
1106
|
+
client_uri: this.clientUri || "https://mcp-assistant.in",
|
|
1107
|
+
logo_uri: this.logoUri || "https://mcp-assistant.in/logo.png",
|
|
1108
|
+
policy_uri: this.policyUri || "https://mcp-assistant.in/privacy",
|
|
1109
|
+
...this.clientId ? { client_id: this.clientId } : {},
|
|
1110
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
1111
|
+
};
|
|
1112
|
+
if (!this.oauthProvider) {
|
|
1113
|
+
if (!this.serverId) {
|
|
1114
|
+
throw new Error("serverId required for OAuth provider initialization");
|
|
1115
|
+
}
|
|
1116
|
+
this.oauthProvider = new StorageOAuthClientProvider(
|
|
1117
|
+
this.identity,
|
|
1118
|
+
this.serverId,
|
|
1119
|
+
this.sessionId,
|
|
1120
|
+
clientMetadata.client_name ?? "MCP Assistant",
|
|
1121
|
+
this.callbackUrl,
|
|
1122
|
+
(redirectUrl) => {
|
|
1123
|
+
if (this.onRedirect) {
|
|
1124
|
+
this.onRedirect(redirectUrl);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
);
|
|
1128
|
+
if (this.clientId && this.oauthProvider) {
|
|
1129
|
+
this.oauthProvider.clientId = this.clientId;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (!this.client) {
|
|
1133
|
+
this.client = new Client(
|
|
1134
|
+
{
|
|
1135
|
+
name: "mcp-ts-oauth-client",
|
|
1136
|
+
version: "2.0"
|
|
1137
|
+
},
|
|
1138
|
+
{ capabilities: {} }
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
const existingSession = await storage.getSession(this.identity, this.sessionId);
|
|
1142
|
+
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
1143
|
+
console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
|
|
1144
|
+
await storage.createSession({
|
|
1145
|
+
sessionId: this.sessionId,
|
|
1146
|
+
identity: this.identity,
|
|
1147
|
+
serverId: this.serverId,
|
|
1148
|
+
serverName: this.serverName,
|
|
1149
|
+
serverUrl: this.serverUrl,
|
|
1150
|
+
callbackUrl: this.callbackUrl,
|
|
1151
|
+
transportType: this.transportType || "streamable_http",
|
|
1152
|
+
createdAt: Date.now()
|
|
1153
|
+
}, Math.floor(STATE_EXPIRATION_MS / 1e3));
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Saves current session state to storage
|
|
1158
|
+
* Creates new session if it doesn't exist, updates if it does
|
|
1159
|
+
* @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
|
|
1160
|
+
* @private
|
|
1161
|
+
*/
|
|
1162
|
+
async saveSession(ttl = SESSION_TTL_SECONDS) {
|
|
1163
|
+
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const sessionData = {
|
|
1167
|
+
sessionId: this.sessionId,
|
|
1168
|
+
identity: this.identity,
|
|
1169
|
+
serverId: this.serverId,
|
|
1170
|
+
serverName: this.serverName,
|
|
1171
|
+
serverUrl: this.serverUrl,
|
|
1172
|
+
callbackUrl: this.callbackUrl,
|
|
1173
|
+
transportType: this.transportType || "streamable_http",
|
|
1174
|
+
createdAt: Date.now()
|
|
1175
|
+
};
|
|
1176
|
+
const existingSession = await storage.getSession(this.identity, this.sessionId);
|
|
1177
|
+
if (existingSession) {
|
|
1178
|
+
await storage.updateSession(this.identity, this.sessionId, sessionData, ttl);
|
|
1179
|
+
} else {
|
|
1180
|
+
await storage.createSession(sessionData, ttl);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Try to connect using available transports
|
|
1185
|
+
* @returns The corrected transport type object if successful
|
|
1186
|
+
* @private
|
|
1187
|
+
*/
|
|
1188
|
+
async tryConnect() {
|
|
1189
|
+
const transportsToTry = this.transportType ? [this.transportType] : ["streamable_http", "sse"];
|
|
1190
|
+
let lastError;
|
|
1191
|
+
for (const currentType of transportsToTry) {
|
|
1192
|
+
const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
|
|
1193
|
+
try {
|
|
1194
|
+
const transport = this.getTransport(currentType);
|
|
1195
|
+
this.transport = transport;
|
|
1196
|
+
await this.client.connect(transport);
|
|
1197
|
+
return { transportType: currentType };
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
lastError = error;
|
|
1200
|
+
const isAuthError = error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized");
|
|
1201
|
+
if (isAuthError) {
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
if (isLastAttempt) {
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
1207
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1208
|
+
this.emitProgress(`Connection attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
1209
|
+
this._onObservabilityEvent.fire({
|
|
1210
|
+
level: "warn",
|
|
1211
|
+
message: `Transport ${currentType} failed, falling back`,
|
|
1212
|
+
sessionId: this.sessionId,
|
|
1213
|
+
serverId: this.serverId,
|
|
1214
|
+
metadata: {
|
|
1215
|
+
failedTransport: currentType,
|
|
1216
|
+
error: errorMessage
|
|
1217
|
+
},
|
|
1218
|
+
timestamp: Date.now()
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
throw lastError || new Error("No transports available");
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Connects to the MCP server
|
|
1226
|
+
* Automatically validates and refreshes OAuth tokens if needed
|
|
1227
|
+
* Saves session to Redis on first successful connection
|
|
1228
|
+
* @throws {UnauthorizedError} When OAuth authorization is required
|
|
1229
|
+
* @throws {Error} When connection fails for other reasons
|
|
1230
|
+
*/
|
|
1231
|
+
async connect() {
|
|
1232
|
+
await this.initialize();
|
|
1233
|
+
if (!this.client || !this.oauthProvider) {
|
|
1234
|
+
const error = "Client or OAuth provider not initialized";
|
|
1235
|
+
this.emitError(error, "connection");
|
|
1236
|
+
this.emitStateChange("FAILED");
|
|
1237
|
+
throw new Error(error);
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
this.emitProgress("Validating OAuth tokens...");
|
|
1241
|
+
await this.getValidTokens();
|
|
1242
|
+
this.emitStateChange("CONNECTING");
|
|
1243
|
+
const { transportType } = await this.tryConnect();
|
|
1244
|
+
this.transportType = transportType;
|
|
1245
|
+
this.emitStateChange("CONNECTED");
|
|
1246
|
+
this.emitProgress("Connected successfully");
|
|
1247
|
+
const existingSession = await storage.getSession(this.identity, this.sessionId);
|
|
1248
|
+
if (!existingSession || existingSession.transportType !== this.transportType) {
|
|
1249
|
+
console.log(`[MCPClient] Saving session ${this.sessionId} (new or transport changed)`);
|
|
1250
|
+
await this.saveSession(SESSION_TTL_SECONDS);
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
if (error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized")) {
|
|
1254
|
+
this.emitStateChange("AUTHENTICATING");
|
|
1255
|
+
console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
|
|
1256
|
+
await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1e3));
|
|
1257
|
+
let authUrl = "";
|
|
1258
|
+
if (this.oauthProvider) {
|
|
1259
|
+
authUrl = this.oauthProvider.authUrl || "";
|
|
1260
|
+
}
|
|
1261
|
+
if (this.serverId) {
|
|
1262
|
+
this._onConnectionEvent.fire({
|
|
1263
|
+
type: "auth_required",
|
|
1264
|
+
sessionId: this.sessionId,
|
|
1265
|
+
serverId: this.serverId,
|
|
1266
|
+
authUrl,
|
|
1267
|
+
timestamp: Date.now()
|
|
1268
|
+
});
|
|
1269
|
+
if (authUrl && this.onRedirect) {
|
|
1270
|
+
this.onRedirect(authUrl);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
throw new UnauthorizedError("OAuth authorization required");
|
|
1274
|
+
}
|
|
1275
|
+
const errorMessage = error instanceof Error ? error.message : "Connection failed";
|
|
1276
|
+
this.emitError(errorMessage, "connection");
|
|
1277
|
+
this.emitStateChange("FAILED");
|
|
1278
|
+
throw error;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Completes OAuth authorization flow by exchanging authorization code for tokens
|
|
1283
|
+
* Creates new authenticated client and transport, then establishes connection
|
|
1284
|
+
* Saves active session to Redis after successful authentication
|
|
1285
|
+
* @param authCode - Authorization code received from OAuth callback
|
|
1286
|
+
*/
|
|
1287
|
+
// TODO: needs to be optimized
|
|
1288
|
+
async finishAuth(authCode) {
|
|
1289
|
+
this.emitStateChange("AUTHENTICATING");
|
|
1290
|
+
this.emitProgress("Exchanging authorization code for tokens...");
|
|
1291
|
+
await this.initialize();
|
|
1292
|
+
if (!this.oauthProvider) {
|
|
1293
|
+
const error = "OAuth provider not initialized";
|
|
1294
|
+
this.emitError(error, "auth");
|
|
1295
|
+
this.emitStateChange("FAILED");
|
|
1296
|
+
throw new Error(error);
|
|
1297
|
+
}
|
|
1298
|
+
const transportsToTry = this.transportType ? [this.transportType] : ["streamable_http", "sse"];
|
|
1299
|
+
let lastError;
|
|
1300
|
+
let tokensExchanged = false;
|
|
1301
|
+
for (const currentType of transportsToTry) {
|
|
1302
|
+
const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
|
|
1303
|
+
try {
|
|
1304
|
+
const transport = this.getTransport(currentType);
|
|
1305
|
+
this.transport = transport;
|
|
1306
|
+
if (!tokensExchanged) {
|
|
1307
|
+
await transport.finishAuth(authCode);
|
|
1308
|
+
tokensExchanged = true;
|
|
1309
|
+
} else {
|
|
1310
|
+
this.emitProgress(`Tokens already exchanged, skipping auth step for ${currentType}...`);
|
|
1311
|
+
}
|
|
1312
|
+
this.transportType = currentType;
|
|
1313
|
+
this.emitStateChange("AUTHENTICATED");
|
|
1314
|
+
this.emitProgress("Creating authenticated client...");
|
|
1315
|
+
this.client = new Client(
|
|
1316
|
+
{
|
|
1317
|
+
name: "mcp-ts-oauth-client",
|
|
1318
|
+
version: "2.0"
|
|
1319
|
+
},
|
|
1320
|
+
{ capabilities: {} }
|
|
1321
|
+
);
|
|
1322
|
+
this.emitStateChange("CONNECTING");
|
|
1323
|
+
await this.client.connect(this.transport);
|
|
1324
|
+
this.emitStateChange("CONNECTED");
|
|
1325
|
+
console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
|
|
1326
|
+
await this.saveSession(SESSION_TTL_SECONDS);
|
|
1327
|
+
return;
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
lastError = error;
|
|
1330
|
+
const isAuthError = error instanceof UnauthorizedError$1 || error instanceof Error && error.message.toLowerCase().includes("unauthorized");
|
|
1331
|
+
if (isAuthError) {
|
|
1332
|
+
throw error;
|
|
1333
|
+
}
|
|
1334
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1335
|
+
if (!tokensExchanged && errorMessage.toLowerCase().includes("invalid authorization code")) {
|
|
1336
|
+
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
1337
|
+
this.emitError(msg, "auth");
|
|
1338
|
+
this.emitStateChange("FAILED");
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
if (isLastAttempt) {
|
|
1342
|
+
const msg = error instanceof Error ? error.message : "Authentication failed";
|
|
1343
|
+
this.emitError(msg, "auth");
|
|
1344
|
+
this.emitStateChange("FAILED");
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
this.emitProgress(`Auth attempt with ${currentType} failed: ${errorMessage}. Retrying...`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (lastError) {
|
|
1351
|
+
const errorMessage = lastError instanceof Error ? lastError.message : "Authentication failed";
|
|
1352
|
+
this.emitError(errorMessage, "auth");
|
|
1353
|
+
this.emitStateChange("FAILED");
|
|
1354
|
+
throw lastError;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Lists all available tools from the connected MCP server
|
|
1359
|
+
* @returns List of tools with their schemas and descriptions
|
|
1360
|
+
* @throws {Error} When client is not connected
|
|
1361
|
+
*/
|
|
1362
|
+
async listTools() {
|
|
1363
|
+
if (!this.client) {
|
|
1364
|
+
throw new Error("Not connected to server");
|
|
1365
|
+
}
|
|
1366
|
+
this.emitStateChange("DISCOVERING");
|
|
1367
|
+
try {
|
|
1368
|
+
const request = {
|
|
1369
|
+
method: "tools/list",
|
|
1370
|
+
params: {}
|
|
1371
|
+
};
|
|
1372
|
+
const result = await this.client.request(request, ListToolsResultSchema);
|
|
1373
|
+
if (this.serverId) {
|
|
1374
|
+
this._onConnectionEvent.fire({
|
|
1375
|
+
type: "tools_discovered",
|
|
1376
|
+
sessionId: this.sessionId,
|
|
1377
|
+
serverId: this.serverId,
|
|
1378
|
+
toolCount: result.tools.length,
|
|
1379
|
+
tools: result.tools,
|
|
1380
|
+
timestamp: Date.now()
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
this.emitStateChange("READY");
|
|
1384
|
+
this.emitProgress(`Discovered ${result.tools.length} tools`);
|
|
1385
|
+
return result;
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to list tools";
|
|
1388
|
+
this.emitError(errorMessage, "validation");
|
|
1389
|
+
this.emitStateChange("FAILED");
|
|
1390
|
+
throw error;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Executes a tool on the connected MCP server
|
|
1395
|
+
* @param toolName - Name of the tool to execute
|
|
1396
|
+
* @param toolArgs - Arguments to pass to the tool
|
|
1397
|
+
* @returns Tool execution result
|
|
1398
|
+
* @throws {Error} When client is not connected
|
|
1399
|
+
*/
|
|
1400
|
+
async callTool(toolName, toolArgs) {
|
|
1401
|
+
if (!this.client) {
|
|
1402
|
+
throw new Error("Not connected to server");
|
|
1403
|
+
}
|
|
1404
|
+
const request = {
|
|
1405
|
+
method: "tools/call",
|
|
1406
|
+
params: {
|
|
1407
|
+
name: toolName,
|
|
1408
|
+
arguments: toolArgs
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
try {
|
|
1412
|
+
const result = await this.client.request(request, CallToolResultSchema);
|
|
1413
|
+
this._onObservabilityEvent.fire({
|
|
1414
|
+
type: "mcp:client:tool_call",
|
|
1415
|
+
level: "info",
|
|
1416
|
+
message: `Tool ${toolName} called successfully`,
|
|
1417
|
+
displayMessage: `Called tool ${toolName}`,
|
|
1418
|
+
sessionId: this.sessionId,
|
|
1419
|
+
serverId: this.serverId,
|
|
1420
|
+
payload: {
|
|
1421
|
+
toolName,
|
|
1422
|
+
args: toolArgs
|
|
1423
|
+
},
|
|
1424
|
+
timestamp: Date.now(),
|
|
1425
|
+
id: nanoid()
|
|
1426
|
+
});
|
|
1427
|
+
return result;
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
const errorMessage = error instanceof Error ? error.message : `Failed to call tool ${toolName}`;
|
|
1430
|
+
this._onObservabilityEvent.fire({
|
|
1431
|
+
type: "mcp:client:error",
|
|
1432
|
+
level: "error",
|
|
1433
|
+
message: errorMessage,
|
|
1434
|
+
displayMessage: `Failed to call tool ${toolName}`,
|
|
1435
|
+
sessionId: this.sessionId,
|
|
1436
|
+
serverId: this.serverId,
|
|
1437
|
+
payload: {
|
|
1438
|
+
errorType: "tool_execution",
|
|
1439
|
+
error: errorMessage,
|
|
1440
|
+
toolName,
|
|
1441
|
+
args: toolArgs
|
|
1442
|
+
},
|
|
1443
|
+
timestamp: Date.now(),
|
|
1444
|
+
id: nanoid()
|
|
1445
|
+
});
|
|
1446
|
+
throw error;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Lists all available prompts from the connected MCP server
|
|
1451
|
+
* @returns List of available prompts
|
|
1452
|
+
* @throws {Error} When client is not connected
|
|
1453
|
+
*/
|
|
1454
|
+
async listPrompts() {
|
|
1455
|
+
if (!this.client) {
|
|
1456
|
+
throw new Error("Not connected to server");
|
|
1457
|
+
}
|
|
1458
|
+
this.emitStateChange("DISCOVERING");
|
|
1459
|
+
try {
|
|
1460
|
+
const request = {
|
|
1461
|
+
method: "prompts/list",
|
|
1462
|
+
params: {}
|
|
1463
|
+
};
|
|
1464
|
+
const result = await this.client.request(request, ListPromptsResultSchema);
|
|
1465
|
+
this.emitStateChange("READY");
|
|
1466
|
+
this.emitProgress(`Discovered ${result.prompts.length} prompts`);
|
|
1467
|
+
return result;
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to list prompts";
|
|
1470
|
+
this.emitError(errorMessage, "validation");
|
|
1471
|
+
this.emitStateChange("FAILED");
|
|
1472
|
+
throw error;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Gets a specific prompt with arguments
|
|
1477
|
+
* @param name - Name of the prompt
|
|
1478
|
+
* @param args - Arguments for the prompt
|
|
1479
|
+
* @returns Prompt content
|
|
1480
|
+
* @throws {Error} When client is not connected
|
|
1481
|
+
*/
|
|
1482
|
+
async getPrompt(name, args) {
|
|
1483
|
+
if (!this.client) {
|
|
1484
|
+
throw new Error("Not connected to server");
|
|
1485
|
+
}
|
|
1486
|
+
const request = {
|
|
1487
|
+
method: "prompts/get",
|
|
1488
|
+
params: {
|
|
1489
|
+
name,
|
|
1490
|
+
arguments: args
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
return await this.client.request(request, GetPromptResultSchema);
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Lists all available resources from the connected MCP server
|
|
1497
|
+
* @returns List of available resources
|
|
1498
|
+
* @throws {Error} When client is not connected
|
|
1499
|
+
*/
|
|
1500
|
+
async listResources() {
|
|
1501
|
+
if (!this.client) {
|
|
1502
|
+
throw new Error("Not connected to server");
|
|
1503
|
+
}
|
|
1504
|
+
this.emitStateChange("DISCOVERING");
|
|
1505
|
+
try {
|
|
1506
|
+
const request = {
|
|
1507
|
+
method: "resources/list",
|
|
1508
|
+
params: {}
|
|
1509
|
+
};
|
|
1510
|
+
const result = await this.client.request(request, ListResourcesResultSchema);
|
|
1511
|
+
this.emitStateChange("READY");
|
|
1512
|
+
this.emitProgress(`Discovered ${result.resources.length} resources`);
|
|
1513
|
+
return result;
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to list resources";
|
|
1516
|
+
this.emitError(errorMessage, "validation");
|
|
1517
|
+
this.emitStateChange("FAILED");
|
|
1518
|
+
throw error;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Reads a specific resource
|
|
1523
|
+
* @param uri - URI of the resource to read
|
|
1524
|
+
* @returns Resource content
|
|
1525
|
+
* @throws {Error} When client is not connected
|
|
1526
|
+
*/
|
|
1527
|
+
async readResource(uri) {
|
|
1528
|
+
if (!this.client) {
|
|
1529
|
+
throw new Error("Not connected to server");
|
|
1530
|
+
}
|
|
1531
|
+
const request = {
|
|
1532
|
+
method: "resources/read",
|
|
1533
|
+
params: {
|
|
1534
|
+
uri
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
return await this.client.request(request, ReadResourceResultSchema);
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Refreshes the OAuth access token using the refresh token
|
|
1541
|
+
* Discovers OAuth metadata from server and exchanges refresh token for new access token
|
|
1542
|
+
* @returns True if refresh was successful, false otherwise
|
|
1543
|
+
*/
|
|
1544
|
+
async refreshToken() {
|
|
1545
|
+
await this.initialize();
|
|
1546
|
+
if (!this.oauthProvider) {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
const tokens = await this.oauthProvider.tokens();
|
|
1550
|
+
if (!tokens || !tokens.refresh_token) {
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
const clientInformation = await this.oauthProvider.clientInformation();
|
|
1554
|
+
if (!clientInformation) {
|
|
1555
|
+
return false;
|
|
1556
|
+
}
|
|
1557
|
+
try {
|
|
1558
|
+
const resourceMetadata = await discoverOAuthProtectedResourceMetadata(this.serverUrl);
|
|
1559
|
+
const authServerUrl = resourceMetadata?.authorization_servers?.[0] || this.serverUrl;
|
|
1560
|
+
const authMetadata = await discoverAuthorizationServerMetadata(authServerUrl);
|
|
1561
|
+
const newTokens = await refreshAuthorization(authServerUrl, {
|
|
1562
|
+
metadata: authMetadata,
|
|
1563
|
+
clientInformation,
|
|
1564
|
+
refreshToken: tokens.refresh_token
|
|
1565
|
+
});
|
|
1566
|
+
await this.oauthProvider.saveTokens(newTokens);
|
|
1567
|
+
return true;
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
console.error("[OAuth] Token refresh failed:", error);
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Ensures OAuth tokens are valid, refreshing them if expired
|
|
1575
|
+
* Called automatically by connect() - rarely needs to be called manually
|
|
1576
|
+
* @returns True if valid tokens are available, false otherwise
|
|
1577
|
+
*/
|
|
1578
|
+
async getValidTokens() {
|
|
1579
|
+
await this.initialize();
|
|
1580
|
+
if (!this.oauthProvider) {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
const tokens = await this.oauthProvider.tokens();
|
|
1584
|
+
if (!tokens) {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
if (this.oauthProvider.isTokenExpired()) {
|
|
1588
|
+
return await this.refreshToken();
|
|
1589
|
+
}
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Reconnects to MCP server using existing OAuth provider from Redis
|
|
1594
|
+
* Used for session restoration in serverless environments
|
|
1595
|
+
* Creates new client and transport without re-initializing OAuth provider
|
|
1596
|
+
* @throws {Error} When OAuth provider is not initialized
|
|
1597
|
+
*/
|
|
1598
|
+
async reconnect() {
|
|
1599
|
+
await this.initialize();
|
|
1600
|
+
if (!this.oauthProvider) {
|
|
1601
|
+
throw new Error("OAuth provider not initialized");
|
|
1602
|
+
}
|
|
1603
|
+
this.client = new Client(
|
|
1604
|
+
{
|
|
1605
|
+
name: "mcp-ts-oauth-client",
|
|
1606
|
+
version: "2.0"
|
|
1607
|
+
},
|
|
1608
|
+
{ capabilities: {} }
|
|
1609
|
+
);
|
|
1610
|
+
const tt = this.transportType || "streamable_http";
|
|
1611
|
+
this.transport = this.getTransport(tt);
|
|
1612
|
+
await this.client.connect(this.transport);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Completely removes the session from Redis including all OAuth data
|
|
1616
|
+
* Invalidates credentials and disconnects the client
|
|
1617
|
+
*/
|
|
1618
|
+
async clearSession() {
|
|
1619
|
+
try {
|
|
1620
|
+
await this.initialize();
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
console.warn("[MCPClient] Initialization failed during clearSession:", error);
|
|
1623
|
+
}
|
|
1624
|
+
if (this.oauthProvider) {
|
|
1625
|
+
await this.oauthProvider.invalidateCredentials("all");
|
|
1626
|
+
}
|
|
1627
|
+
await storage.removeSession(this.identity, this.sessionId);
|
|
1628
|
+
this.disconnect();
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Checks if the client is currently connected to an MCP server
|
|
1632
|
+
* @returns True if connected, false otherwise
|
|
1633
|
+
*/
|
|
1634
|
+
isConnected() {
|
|
1635
|
+
return this.client !== null;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Disconnects from the MCP server and cleans up resources
|
|
1639
|
+
* Does not remove session from Redis - use clearSession() for that
|
|
1640
|
+
*/
|
|
1641
|
+
disconnect(reason) {
|
|
1642
|
+
if (this.client) {
|
|
1643
|
+
this.client.close();
|
|
1644
|
+
}
|
|
1645
|
+
this.client = null;
|
|
1646
|
+
this.oauthProvider = null;
|
|
1647
|
+
this.transport = null;
|
|
1648
|
+
if (this.serverId) {
|
|
1649
|
+
this._onConnectionEvent.fire({
|
|
1650
|
+
type: "disconnected",
|
|
1651
|
+
sessionId: this.sessionId,
|
|
1652
|
+
serverId: this.serverId,
|
|
1653
|
+
reason,
|
|
1654
|
+
timestamp: Date.now()
|
|
1655
|
+
});
|
|
1656
|
+
this._onObservabilityEvent.fire({
|
|
1657
|
+
type: "mcp:client:disconnect",
|
|
1658
|
+
level: "info",
|
|
1659
|
+
message: `Disconnected from ${this.serverId}`,
|
|
1660
|
+
sessionId: this.sessionId,
|
|
1661
|
+
serverId: this.serverId,
|
|
1662
|
+
payload: {
|
|
1663
|
+
reason: reason || "unknown"
|
|
1664
|
+
},
|
|
1665
|
+
timestamp: Date.now(),
|
|
1666
|
+
id: nanoid()
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
this.emitStateChange("DISCONNECTED");
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Dispose of all event emitters
|
|
1673
|
+
* Call this when the client is no longer needed
|
|
1674
|
+
*/
|
|
1675
|
+
dispose() {
|
|
1676
|
+
this._onConnectionEvent.dispose();
|
|
1677
|
+
this._onObservabilityEvent.dispose();
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Gets the server URL
|
|
1681
|
+
* @returns Server URL or empty string if not set
|
|
1682
|
+
*/
|
|
1683
|
+
getServerUrl() {
|
|
1684
|
+
return this.serverUrl || "";
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* Gets the OAuth callback URL
|
|
1688
|
+
* @returns Callback URL or empty string if not set
|
|
1689
|
+
*/
|
|
1690
|
+
getCallbackUrl() {
|
|
1691
|
+
return this.callbackUrl || "";
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Gets the transport type being used
|
|
1695
|
+
* @returns Transport type (defaults to 'streamable_http')
|
|
1696
|
+
*/
|
|
1697
|
+
getTransportType() {
|
|
1698
|
+
return this.transportType || "streamable_http";
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Gets the human-readable server name
|
|
1702
|
+
* @returns Server name or undefined
|
|
1703
|
+
*/
|
|
1704
|
+
getServerName() {
|
|
1705
|
+
return this.serverName;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Gets the server ID
|
|
1709
|
+
* @returns Server ID or undefined
|
|
1710
|
+
*/
|
|
1711
|
+
getServerId() {
|
|
1712
|
+
return this.serverId;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Gets the session ID
|
|
1716
|
+
* @returns Session ID
|
|
1717
|
+
*/
|
|
1718
|
+
getSessionId() {
|
|
1719
|
+
return this.sessionId;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Gets MCP server configuration for all active user sessions
|
|
1723
|
+
* Loads sessions from Redis, validates OAuth tokens, refreshes if expired
|
|
1724
|
+
* Returns ready-to-use configuration with valid auth headers
|
|
1725
|
+
* @param identity - User ID to fetch sessions for
|
|
1726
|
+
* @returns Object keyed by sanitized server labels containing transport, url, headers, etc.
|
|
1727
|
+
* @static
|
|
1728
|
+
*/
|
|
1729
|
+
static async getMcpServerConfig(identity) {
|
|
1730
|
+
const mcpConfig = {};
|
|
1731
|
+
const sessions = await storage.getIdentitySessionsData(identity);
|
|
1732
|
+
await Promise.all(
|
|
1733
|
+
sessions.map(async (sessionData) => {
|
|
1734
|
+
const { sessionId } = sessionData;
|
|
1735
|
+
try {
|
|
1736
|
+
if (!sessionData.serverId || !sessionData.transportType || !sessionData.serverUrl || !sessionData.callbackUrl) {
|
|
1737
|
+
await storage.removeSession(identity, sessionId);
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
let headers;
|
|
1741
|
+
try {
|
|
1742
|
+
const client = new _MCPClient({
|
|
1743
|
+
identity,
|
|
1744
|
+
sessionId,
|
|
1745
|
+
serverId: sessionData.serverId,
|
|
1746
|
+
serverUrl: sessionData.serverUrl,
|
|
1747
|
+
callbackUrl: sessionData.callbackUrl,
|
|
1748
|
+
serverName: sessionData.serverName,
|
|
1749
|
+
transportType: sessionData.transportType,
|
|
1750
|
+
headers: sessionData.headers
|
|
1751
|
+
});
|
|
1752
|
+
await client.initialize();
|
|
1753
|
+
const hasValidTokens = await client.getValidTokens();
|
|
1754
|
+
if (hasValidTokens && client.oauthProvider) {
|
|
1755
|
+
const tokens = await client.oauthProvider.tokens();
|
|
1756
|
+
if (tokens?.access_token) {
|
|
1757
|
+
headers = { Authorization: `Bearer ${tokens.access_token}` };
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
console.warn(`[MCP] Failed to get OAuth tokens for ${sessionId}:`, error);
|
|
1762
|
+
}
|
|
1763
|
+
const label = sanitizeServerLabel(
|
|
1764
|
+
sessionData.serverName || sessionData.serverId || "server"
|
|
1765
|
+
);
|
|
1766
|
+
mcpConfig[label] = {
|
|
1767
|
+
transport: sessionData.transportType,
|
|
1768
|
+
url: sessionData.serverUrl,
|
|
1769
|
+
...sessionData.serverName && {
|
|
1770
|
+
serverName: sessionData.serverName,
|
|
1771
|
+
serverLabel: label
|
|
1772
|
+
},
|
|
1773
|
+
...headers && { headers }
|
|
1774
|
+
};
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
await storage.removeSession(identity, sessionId);
|
|
1777
|
+
console.warn(`[MCP] Failed to process session ${sessionId}:`, error);
|
|
1778
|
+
}
|
|
1779
|
+
})
|
|
1780
|
+
);
|
|
1781
|
+
return mcpConfig;
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
// src/server/mcp/multi-session-client.ts
|
|
1786
|
+
var MultiSessionClient = class {
|
|
1787
|
+
constructor(identity, options = {}) {
|
|
1788
|
+
__publicField(this, "clients", []);
|
|
1789
|
+
__publicField(this, "identity");
|
|
1790
|
+
__publicField(this, "options");
|
|
1791
|
+
this.identity = identity;
|
|
1792
|
+
this.options = {
|
|
1793
|
+
timeout: 15e3,
|
|
1794
|
+
maxRetries: 2,
|
|
1795
|
+
retryDelay: 1e3,
|
|
1796
|
+
...options
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
async getActiveSessions() {
|
|
1800
|
+
const sessions = await storage.getIdentitySessionsData(this.identity);
|
|
1801
|
+
console.log(
|
|
1802
|
+
`[MultiSessionClient] All sessions for ${this.identity}:`,
|
|
1803
|
+
sessions.map((s) => ({ sessionId: s.sessionId, serverId: s.serverId }))
|
|
1804
|
+
);
|
|
1805
|
+
const valid = sessions.filter((s) => s.serverId && s.serverUrl && s.callbackUrl);
|
|
1806
|
+
console.log(`[MultiSessionClient] Filtered valid sessions:`, valid.length);
|
|
1807
|
+
return valid;
|
|
1808
|
+
}
|
|
1809
|
+
async connectInBatches(sessions) {
|
|
1810
|
+
const BATCH_SIZE = 5;
|
|
1811
|
+
for (let i = 0; i < sessions.length; i += BATCH_SIZE) {
|
|
1812
|
+
const batch = sessions.slice(i, i + BATCH_SIZE);
|
|
1813
|
+
await Promise.all(batch.map((session) => this.connectSession(session)));
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
async connectSession(session) {
|
|
1817
|
+
const existingClient = this.clients.find((c) => c.getSessionId() === session.sessionId);
|
|
1818
|
+
if (existingClient?.isConnected()) {
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const maxRetries = this.options.maxRetries ?? 2;
|
|
1822
|
+
const retryDelay = this.options.retryDelay ?? 1e3;
|
|
1823
|
+
let lastError;
|
|
1824
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1825
|
+
try {
|
|
1826
|
+
const client = await this.createAndConnectClient(session);
|
|
1827
|
+
this.clients.push(client);
|
|
1828
|
+
return;
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
lastError = error;
|
|
1831
|
+
if (attempt < maxRetries) {
|
|
1832
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
|
|
1837
|
+
}
|
|
1838
|
+
async createAndConnectClient(session) {
|
|
1839
|
+
const client = new MCPClient({
|
|
1840
|
+
identity: this.identity,
|
|
1841
|
+
sessionId: session.sessionId,
|
|
1842
|
+
serverId: session.serverId,
|
|
1843
|
+
serverUrl: session.serverUrl,
|
|
1844
|
+
callbackUrl: session.callbackUrl,
|
|
1845
|
+
serverName: session.serverName,
|
|
1846
|
+
transportType: session.transportType,
|
|
1847
|
+
headers: session.headers
|
|
1848
|
+
});
|
|
1849
|
+
const timeoutMs = this.options.timeout ?? 15e3;
|
|
1850
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1851
|
+
setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
1852
|
+
});
|
|
1853
|
+
await Promise.race([client.connect(), timeoutPromise]);
|
|
1854
|
+
return client;
|
|
1855
|
+
}
|
|
1856
|
+
async connect() {
|
|
1857
|
+
const sessions = await this.getActiveSessions();
|
|
1858
|
+
await this.connectInBatches(sessions);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Returns the array of currently connected clients.
|
|
1862
|
+
*/
|
|
1863
|
+
getClients() {
|
|
1864
|
+
return this.clients;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Disconnects all clients.
|
|
1868
|
+
*/
|
|
1869
|
+
disconnect() {
|
|
1870
|
+
this.clients.forEach((client) => client.disconnect());
|
|
1871
|
+
this.clients = [];
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
|
|
1875
|
+
// src/server/handlers/sse-handler.ts
|
|
1876
|
+
var SSEConnectionManager = class {
|
|
1877
|
+
constructor(options, sendEvent) {
|
|
1878
|
+
this.options = options;
|
|
1879
|
+
this.sendEvent = sendEvent;
|
|
1880
|
+
__publicField(this, "identity");
|
|
1881
|
+
__publicField(this, "clients", /* @__PURE__ */ new Map());
|
|
1882
|
+
__publicField(this, "heartbeatTimer");
|
|
1883
|
+
__publicField(this, "isActive", true);
|
|
1884
|
+
this.identity = options.identity;
|
|
1885
|
+
this.startHeartbeat();
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Get resolved client metadata (dynamic > static > defaults)
|
|
1889
|
+
*/
|
|
1890
|
+
async getResolvedClientMetadata(request) {
|
|
1891
|
+
let metadata = {};
|
|
1892
|
+
if (this.options.clientDefaults) {
|
|
1893
|
+
metadata = { ...this.options.clientDefaults };
|
|
1894
|
+
}
|
|
1895
|
+
if (this.options.getClientMetadata) {
|
|
1896
|
+
const dynamicMetadata = await this.options.getClientMetadata(request);
|
|
1897
|
+
metadata = { ...metadata, ...dynamicMetadata };
|
|
1898
|
+
}
|
|
1899
|
+
return metadata;
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Start heartbeat to keep connection alive
|
|
1903
|
+
*/
|
|
1904
|
+
startHeartbeat() {
|
|
1905
|
+
const interval = this.options.heartbeatInterval || 3e4;
|
|
1906
|
+
this.heartbeatTimer = setInterval(() => {
|
|
1907
|
+
if (this.isActive) {
|
|
1908
|
+
this.sendEvent({
|
|
1909
|
+
level: "debug",
|
|
1910
|
+
message: "heartbeat",
|
|
1911
|
+
timestamp: Date.now()
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
}, interval);
|
|
1915
|
+
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Handle incoming RPC requests
|
|
1918
|
+
*/
|
|
1919
|
+
async handleRequest(request) {
|
|
1920
|
+
try {
|
|
1921
|
+
let result;
|
|
1922
|
+
switch (request.method) {
|
|
1923
|
+
case "getSessions":
|
|
1924
|
+
result = await this.getSessions();
|
|
1925
|
+
break;
|
|
1926
|
+
case "connect":
|
|
1927
|
+
result = await this.connect(request.params);
|
|
1928
|
+
break;
|
|
1929
|
+
case "disconnect":
|
|
1930
|
+
result = await this.disconnect(request.params);
|
|
1931
|
+
break;
|
|
1932
|
+
case "listTools":
|
|
1933
|
+
result = await this.listTools(request.params);
|
|
1934
|
+
break;
|
|
1935
|
+
case "callTool":
|
|
1936
|
+
result = await this.callTool(request.params);
|
|
1937
|
+
break;
|
|
1938
|
+
case "restoreSession":
|
|
1939
|
+
result = await this.restoreSession(request.params);
|
|
1940
|
+
break;
|
|
1941
|
+
case "finishAuth":
|
|
1942
|
+
result = await this.finishAuth(request.params);
|
|
1943
|
+
break;
|
|
1944
|
+
case "listPrompts":
|
|
1945
|
+
result = await this.listPrompts(request.params);
|
|
1946
|
+
break;
|
|
1947
|
+
case "getPrompt":
|
|
1948
|
+
result = await this.getPrompt(request.params);
|
|
1949
|
+
break;
|
|
1950
|
+
case "listResources":
|
|
1951
|
+
result = await this.listResources(request.params);
|
|
1952
|
+
break;
|
|
1953
|
+
case "readResource":
|
|
1954
|
+
result = await this.readResource(request.params);
|
|
1955
|
+
break;
|
|
1956
|
+
default:
|
|
1957
|
+
throw new Error(`Unknown method: ${request.method}`);
|
|
1958
|
+
}
|
|
1959
|
+
this.sendEvent({
|
|
1960
|
+
id: request.id,
|
|
1961
|
+
result
|
|
1962
|
+
});
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
this.sendEvent({
|
|
1965
|
+
id: request.id,
|
|
1966
|
+
error: {
|
|
1967
|
+
code: RpcErrorCodes.EXECUTION_ERROR,
|
|
1968
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Get all user sessions
|
|
1975
|
+
*/
|
|
1976
|
+
async getSessions() {
|
|
1977
|
+
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
|
+
return {
|
|
1993
|
+
sessions: sessions.map((s) => ({
|
|
1994
|
+
sessionId: s.sessionId,
|
|
1995
|
+
serverId: s.serverId,
|
|
1996
|
+
serverName: s.serverName,
|
|
1997
|
+
serverUrl: s.serverUrl,
|
|
1998
|
+
transport: s.transportType
|
|
1999
|
+
}))
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Connect to an MCP server
|
|
2004
|
+
*/
|
|
2005
|
+
async connect(params) {
|
|
2006
|
+
const { serverId, serverName, serverUrl, callbackUrl, transportType } = params;
|
|
2007
|
+
const existingSessions = await storage.getIdentitySessionsData(this.identity);
|
|
2008
|
+
const duplicate = existingSessions.find(
|
|
2009
|
+
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
2010
|
+
);
|
|
2011
|
+
if (duplicate) {
|
|
2012
|
+
throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
|
|
2013
|
+
}
|
|
2014
|
+
const sessionId = await storage.generateSessionId();
|
|
2015
|
+
this.emitConnectionEvent({
|
|
2016
|
+
type: "state_changed",
|
|
2017
|
+
sessionId,
|
|
2018
|
+
serverId,
|
|
2019
|
+
serverName,
|
|
2020
|
+
state: "CONNECTING",
|
|
2021
|
+
previousState: "DISCONNECTED",
|
|
2022
|
+
timestamp: Date.now()
|
|
2023
|
+
});
|
|
2024
|
+
try {
|
|
2025
|
+
const clientMetadata = await this.getResolvedClientMetadata();
|
|
2026
|
+
const client = new MCPClient({
|
|
2027
|
+
identity: this.identity,
|
|
2028
|
+
sessionId,
|
|
2029
|
+
serverId,
|
|
2030
|
+
serverName,
|
|
2031
|
+
serverUrl,
|
|
2032
|
+
callbackUrl,
|
|
2033
|
+
transportType,
|
|
2034
|
+
...clientMetadata,
|
|
2035
|
+
// Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
2036
|
+
onRedirect: (authUrl) => {
|
|
2037
|
+
this.emitConnectionEvent({
|
|
2038
|
+
type: "auth_required",
|
|
2039
|
+
sessionId,
|
|
2040
|
+
serverId,
|
|
2041
|
+
authUrl,
|
|
2042
|
+
timestamp: Date.now()
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
this.clients.set(sessionId, client);
|
|
2047
|
+
client.onConnectionEvent((event) => {
|
|
2048
|
+
this.emitConnectionEvent(event);
|
|
2049
|
+
});
|
|
2050
|
+
client.onObservabilityEvent((event) => {
|
|
2051
|
+
this.sendEvent(event);
|
|
2052
|
+
});
|
|
2053
|
+
await client.connect();
|
|
2054
|
+
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
|
+
this.emitConnectionEvent({
|
|
2060
|
+
type: "tools_discovered",
|
|
2061
|
+
sessionId,
|
|
2062
|
+
serverId,
|
|
2063
|
+
toolCount: tools.tools.length,
|
|
2064
|
+
tools: tools.tools,
|
|
2065
|
+
timestamp: Date.now()
|
|
2066
|
+
});
|
|
2067
|
+
return {
|
|
2068
|
+
sessionId,
|
|
2069
|
+
success: true
|
|
2070
|
+
};
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
this.emitConnectionEvent({
|
|
2073
|
+
type: "error",
|
|
2074
|
+
sessionId,
|
|
2075
|
+
serverId,
|
|
2076
|
+
error: error instanceof Error ? error.message : "Connection failed",
|
|
2077
|
+
errorType: "connection",
|
|
2078
|
+
timestamp: Date.now()
|
|
2079
|
+
});
|
|
2080
|
+
this.clients.delete(sessionId);
|
|
2081
|
+
throw error;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Disconnect from an MCP server
|
|
2086
|
+
*/
|
|
2087
|
+
async disconnect(params) {
|
|
2088
|
+
const { sessionId } = params;
|
|
2089
|
+
const client = this.clients.get(sessionId);
|
|
2090
|
+
if (client) {
|
|
2091
|
+
await client.clearSession();
|
|
2092
|
+
client.disconnect();
|
|
2093
|
+
this.clients.delete(sessionId);
|
|
2094
|
+
} else {
|
|
2095
|
+
await storage.removeSession(this.identity, sessionId);
|
|
2096
|
+
}
|
|
2097
|
+
return { success: true };
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Helper to get or restore a client
|
|
2101
|
+
*/
|
|
2102
|
+
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);
|
|
2117
|
+
}
|
|
2118
|
+
return client;
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* List tools from a session
|
|
2122
|
+
*/
|
|
2123
|
+
async listTools(params) {
|
|
2124
|
+
const { sessionId } = params;
|
|
2125
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
2126
|
+
const result = await client.listTools();
|
|
2127
|
+
return { tools: result.tools };
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Call a tool
|
|
2131
|
+
*/
|
|
2132
|
+
async callTool(params) {
|
|
2133
|
+
const { sessionId, toolName, toolArgs } = params;
|
|
2134
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
2135
|
+
return await client.callTool(toolName, toolArgs);
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Refresh/validate a session
|
|
2139
|
+
*/
|
|
2140
|
+
async restoreSession(params) {
|
|
2141
|
+
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
|
+
const session = await storage.getSession(this.identity, sessionId);
|
|
2149
|
+
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
|
+
throw new Error("Session not found");
|
|
2157
|
+
}
|
|
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
|
+
this.emitConnectionEvent({
|
|
2171
|
+
type: "state_changed",
|
|
2172
|
+
sessionId,
|
|
2173
|
+
serverId: session.serverId || "unknown",
|
|
2174
|
+
serverName: session.serverName || "Unknown",
|
|
2175
|
+
state: "VALIDATING",
|
|
2176
|
+
previousState: "DISCONNECTED",
|
|
2177
|
+
timestamp: Date.now()
|
|
2178
|
+
});
|
|
2179
|
+
try {
|
|
2180
|
+
const clientMetadata = await this.getResolvedClientMetadata();
|
|
2181
|
+
const client = new MCPClient({
|
|
2182
|
+
identity: this.identity,
|
|
2183
|
+
sessionId,
|
|
2184
|
+
...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
|
+
});
|
|
2193
|
+
await client.connect();
|
|
2194
|
+
this.clients.set(sessionId, client);
|
|
2195
|
+
const tools = await client.listTools();
|
|
2196
|
+
this.emitConnectionEvent({
|
|
2197
|
+
type: "tools_discovered",
|
|
2198
|
+
sessionId,
|
|
2199
|
+
serverId: session.serverId || "unknown",
|
|
2200
|
+
toolCount: tools.tools.length,
|
|
2201
|
+
tools: tools.tools,
|
|
2202
|
+
timestamp: Date.now()
|
|
2203
|
+
});
|
|
2204
|
+
return { success: true, toolCount: tools.tools.length };
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
this.emitConnectionEvent({
|
|
2207
|
+
type: "error",
|
|
2208
|
+
sessionId,
|
|
2209
|
+
serverId: session.serverId || "unknown",
|
|
2210
|
+
error: error instanceof Error ? error.message : "Validation failed",
|
|
2211
|
+
errorType: "validation",
|
|
2212
|
+
timestamp: Date.now()
|
|
2213
|
+
});
|
|
2214
|
+
throw error;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Complete OAuth authorization
|
|
2219
|
+
*/
|
|
2220
|
+
async finishAuth(params) {
|
|
2221
|
+
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
|
+
const session = await storage.getSession(this.identity, sessionId);
|
|
2229
|
+
if (!session) {
|
|
2230
|
+
throw new Error("Session not found");
|
|
2231
|
+
}
|
|
2232
|
+
this.emitConnectionEvent({
|
|
2233
|
+
type: "state_changed",
|
|
2234
|
+
sessionId,
|
|
2235
|
+
serverId: session.serverId || "unknown",
|
|
2236
|
+
serverName: session.serverName || "Unknown",
|
|
2237
|
+
state: "AUTHENTICATING",
|
|
2238
|
+
previousState: "DISCONNECTED",
|
|
2239
|
+
timestamp: Date.now()
|
|
2240
|
+
});
|
|
2241
|
+
try {
|
|
2242
|
+
const client = new MCPClient({
|
|
2243
|
+
identity: this.identity,
|
|
2244
|
+
sessionId
|
|
2245
|
+
});
|
|
2246
|
+
client.onConnectionEvent((event) => {
|
|
2247
|
+
this.emitConnectionEvent(event);
|
|
2248
|
+
});
|
|
2249
|
+
await client.finishAuth(code);
|
|
2250
|
+
this.clients.set(sessionId, client);
|
|
2251
|
+
const tools = await client.listTools();
|
|
2252
|
+
this.emitConnectionEvent({
|
|
2253
|
+
type: "tools_discovered",
|
|
2254
|
+
sessionId,
|
|
2255
|
+
serverId: session.serverId || "unknown",
|
|
2256
|
+
toolCount: tools.tools.length,
|
|
2257
|
+
tools: tools.tools,
|
|
2258
|
+
timestamp: Date.now()
|
|
2259
|
+
});
|
|
2260
|
+
return { success: true, toolCount: tools.tools.length };
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
this.emitConnectionEvent({
|
|
2263
|
+
type: "error",
|
|
2264
|
+
sessionId,
|
|
2265
|
+
serverId: session.serverId || "unknown",
|
|
2266
|
+
error: error instanceof Error ? error.message : "OAuth completion failed",
|
|
2267
|
+
errorType: "auth",
|
|
2268
|
+
timestamp: Date.now()
|
|
2269
|
+
});
|
|
2270
|
+
throw error;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* List prompts from a session
|
|
2275
|
+
*/
|
|
2276
|
+
async listPrompts(params) {
|
|
2277
|
+
const { sessionId } = params;
|
|
2278
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
2279
|
+
const result = await client.listPrompts();
|
|
2280
|
+
return { prompts: result.prompts };
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Get a specific prompt
|
|
2284
|
+
*/
|
|
2285
|
+
async getPrompt(params) {
|
|
2286
|
+
const { sessionId, name, args } = params;
|
|
2287
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
2288
|
+
return await client.getPrompt(name, args);
|
|
2289
|
+
}
|
|
2290
|
+
/**
|
|
2291
|
+
* List resources from a session
|
|
2292
|
+
*/
|
|
2293
|
+
async listResources(params) {
|
|
2294
|
+
const { sessionId } = params;
|
|
2295
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
2296
|
+
const result = await client.listResources();
|
|
2297
|
+
return { resources: result.resources };
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Read a specific resource
|
|
2301
|
+
*/
|
|
2302
|
+
async readResource(params) {
|
|
2303
|
+
const { sessionId, uri } = params;
|
|
2304
|
+
const client = await this.getOrCreateClient(sessionId);
|
|
2305
|
+
return await client.readResource(uri);
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Emit connection event
|
|
2309
|
+
*/
|
|
2310
|
+
emitConnectionEvent(event) {
|
|
2311
|
+
this.sendEvent(event);
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Cleanup and close all connections
|
|
2315
|
+
*/
|
|
2316
|
+
dispose() {
|
|
2317
|
+
this.isActive = false;
|
|
2318
|
+
if (this.heartbeatTimer) {
|
|
2319
|
+
clearInterval(this.heartbeatTimer);
|
|
2320
|
+
}
|
|
2321
|
+
for (const client of this.clients.values()) {
|
|
2322
|
+
client.disconnect();
|
|
2323
|
+
}
|
|
2324
|
+
this.clients.clear();
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
function createSSEHandler(options) {
|
|
2328
|
+
return async (req, res) => {
|
|
2329
|
+
res.writeHead(200, {
|
|
2330
|
+
"Content-Type": "text/event-stream",
|
|
2331
|
+
"Cache-Control": "no-cache",
|
|
2332
|
+
"Connection": "keep-alive",
|
|
2333
|
+
"Access-Control-Allow-Origin": "*"
|
|
2334
|
+
});
|
|
2335
|
+
sendSSE(res, "connected", { timestamp: Date.now() });
|
|
2336
|
+
const manager = new SSEConnectionManager(options, (event) => {
|
|
2337
|
+
if ("id" in event) {
|
|
2338
|
+
sendSSE(res, "rpc-response", event);
|
|
2339
|
+
} else if ("type" in event && "sessionId" in event) {
|
|
2340
|
+
sendSSE(res, "connection", event);
|
|
2341
|
+
} else {
|
|
2342
|
+
sendSSE(res, "observability", event);
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
2345
|
+
req.on("close", () => {
|
|
2346
|
+
manager.dispose();
|
|
2347
|
+
});
|
|
2348
|
+
if (req.method === "POST") {
|
|
2349
|
+
let body = "";
|
|
2350
|
+
req.on("data", (chunk) => {
|
|
2351
|
+
body += chunk.toString();
|
|
2352
|
+
});
|
|
2353
|
+
req.on("end", async () => {
|
|
2354
|
+
try {
|
|
2355
|
+
const request = JSON.parse(body);
|
|
2356
|
+
await manager.handleRequest(request);
|
|
2357
|
+
} catch (error) {
|
|
2358
|
+
console.error("[SSE] Error handling request:", error);
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
function sendSSE(res, event, data) {
|
|
2365
|
+
res.write(`event: ${event}
|
|
2366
|
+
`);
|
|
2367
|
+
res.write(`data: ${JSON.stringify(data)}
|
|
2368
|
+
|
|
2369
|
+
`);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// src/server/handlers/nextjs-handler.ts
|
|
2373
|
+
var managers = /* @__PURE__ */ new Map();
|
|
2374
|
+
function createNextMcpHandler(options = {}) {
|
|
2375
|
+
const {
|
|
2376
|
+
getIdentity = (request) => new URL(request.url).searchParams.get("identity"),
|
|
2377
|
+
getAuthToken = (request) => {
|
|
2378
|
+
const url = new URL(request.url);
|
|
2379
|
+
return url.searchParams.get("token") || request.headers.get("authorization");
|
|
2380
|
+
},
|
|
2381
|
+
authenticate = () => true,
|
|
2382
|
+
heartbeatInterval = 3e4,
|
|
2383
|
+
clientDefaults,
|
|
2384
|
+
getClientMetadata
|
|
2385
|
+
} = options;
|
|
2386
|
+
async function GET(request) {
|
|
2387
|
+
const identity = getIdentity(request);
|
|
2388
|
+
const authToken = getAuthToken(request);
|
|
2389
|
+
if (!identity) {
|
|
2390
|
+
return new Response("Missing identity", { status: 400 });
|
|
2391
|
+
}
|
|
2392
|
+
const isAuthorized = await authenticate(identity, authToken);
|
|
2393
|
+
if (!isAuthorized) {
|
|
2394
|
+
return new Response("Unauthorized", { status: 401 });
|
|
2395
|
+
}
|
|
2396
|
+
const stream = new TransformStream();
|
|
2397
|
+
const writer = stream.writable.getWriter();
|
|
2398
|
+
const encoder = new TextEncoder();
|
|
2399
|
+
const sendSSE2 = (event, data) => {
|
|
2400
|
+
const message = `event: ${event}
|
|
2401
|
+
data: ${JSON.stringify(data)}
|
|
2402
|
+
|
|
2403
|
+
`;
|
|
2404
|
+
writer.write(encoder.encode(message)).catch(() => {
|
|
2405
|
+
});
|
|
2406
|
+
};
|
|
2407
|
+
sendSSE2("connected", { timestamp: Date.now() });
|
|
2408
|
+
const previousManager = managers.get(identity);
|
|
2409
|
+
if (previousManager) {
|
|
2410
|
+
previousManager.dispose();
|
|
2411
|
+
}
|
|
2412
|
+
const resolvedClientMetadata = getClientMetadata ? await getClientMetadata(request) : clientDefaults;
|
|
2413
|
+
const manager = new SSEConnectionManager(
|
|
2414
|
+
{
|
|
2415
|
+
identity,
|
|
2416
|
+
heartbeatInterval,
|
|
2417
|
+
clientDefaults: resolvedClientMetadata
|
|
2418
|
+
// Pass resolved metadata
|
|
2419
|
+
},
|
|
2420
|
+
(event) => {
|
|
2421
|
+
if ("id" in event) {
|
|
2422
|
+
sendSSE2("rpc-response", event);
|
|
2423
|
+
} else if ("type" in event && "sessionId" in event) {
|
|
2424
|
+
sendSSE2("connection", event);
|
|
2425
|
+
} else {
|
|
2426
|
+
sendSSE2("observability", event);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
);
|
|
2430
|
+
managers.set(identity, manager);
|
|
2431
|
+
const abortController = new AbortController();
|
|
2432
|
+
request.signal?.addEventListener("abort", () => {
|
|
2433
|
+
manager.dispose();
|
|
2434
|
+
managers.delete(identity);
|
|
2435
|
+
writer.close().catch(() => {
|
|
2436
|
+
});
|
|
2437
|
+
abortController.abort();
|
|
2438
|
+
});
|
|
2439
|
+
return new Response(stream.readable, {
|
|
2440
|
+
status: 200,
|
|
2441
|
+
headers: {
|
|
2442
|
+
"Content-Type": "text/event-stream",
|
|
2443
|
+
"Cache-Control": "no-cache, no-transform",
|
|
2444
|
+
"Connection": "keep-alive",
|
|
2445
|
+
"X-Accel-Buffering": "no"
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
async function POST(request) {
|
|
2450
|
+
const identity = getIdentity(request);
|
|
2451
|
+
const authToken = getAuthToken(request);
|
|
2452
|
+
if (!identity) {
|
|
2453
|
+
return Response.json({ error: { code: "MISSING_IDENTITY", message: "Missing identity" } }, { status: 400 });
|
|
2454
|
+
}
|
|
2455
|
+
const isAuthorized = await authenticate(identity, authToken);
|
|
2456
|
+
if (!isAuthorized) {
|
|
2457
|
+
return Response.json({ error: { code: "UNAUTHORIZED", message: "Unauthorized" } }, { status: 401 });
|
|
2458
|
+
}
|
|
2459
|
+
try {
|
|
2460
|
+
const body = await request.json();
|
|
2461
|
+
const manager = managers.get(identity);
|
|
2462
|
+
if (!manager) {
|
|
2463
|
+
return Response.json(
|
|
2464
|
+
{
|
|
2465
|
+
error: {
|
|
2466
|
+
code: "NO_CONNECTION",
|
|
2467
|
+
message: "No SSE connection found. Please establish SSE connection first."
|
|
2468
|
+
}
|
|
2469
|
+
},
|
|
2470
|
+
{ status: 400 }
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
await manager.handleRequest(body);
|
|
2474
|
+
return Response.json({ acknowledged: true });
|
|
2475
|
+
} catch (error) {
|
|
2476
|
+
return Response.json(
|
|
2477
|
+
{
|
|
2478
|
+
error: {
|
|
2479
|
+
code: "EXECUTION_ERROR",
|
|
2480
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
2481
|
+
}
|
|
2482
|
+
},
|
|
2483
|
+
{ status: 500 }
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return { GET, POST };
|
|
2488
|
+
}
|
|
2489
|
+
var SSEClient = class {
|
|
2490
|
+
constructor(options) {
|
|
2491
|
+
this.options = options;
|
|
2492
|
+
__publicField(this, "eventSource", null);
|
|
2493
|
+
__publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
|
|
2494
|
+
__publicField(this, "reconnectAttempts", 0);
|
|
2495
|
+
__publicField(this, "maxReconnectAttempts", 5);
|
|
2496
|
+
__publicField(this, "reconnectDelay", 1e3);
|
|
2497
|
+
__publicField(this, "isManuallyDisconnected", false);
|
|
2498
|
+
__publicField(this, "connectionPromise", null);
|
|
2499
|
+
__publicField(this, "connectionResolver", null);
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Connect to SSE endpoint
|
|
2503
|
+
*/
|
|
2504
|
+
connect() {
|
|
2505
|
+
if (this.eventSource) {
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
this.isManuallyDisconnected = false;
|
|
2509
|
+
this.options.onStatusChange?.("connecting");
|
|
2510
|
+
this.connectionPromise = new Promise((resolve) => {
|
|
2511
|
+
this.connectionResolver = resolve;
|
|
2512
|
+
});
|
|
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
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
/**
|
|
2558
|
+
* Disconnect from SSE endpoint
|
|
2559
|
+
*/
|
|
2560
|
+
disconnect() {
|
|
2561
|
+
this.isManuallyDisconnected = true;
|
|
2562
|
+
if (this.eventSource) {
|
|
2563
|
+
this.eventSource.close();
|
|
2564
|
+
this.eventSource = null;
|
|
2565
|
+
}
|
|
2566
|
+
this.connectionPromise = null;
|
|
2567
|
+
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();
|
|
2574
|
+
this.options.onStatusChange?.("disconnected");
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Send RPC request via SSE
|
|
2578
|
+
* Note: SSE is unidirectional (server->client), so we need to send requests via POST
|
|
2579
|
+
*/
|
|
2580
|
+
async sendRequest(method, params) {
|
|
2581
|
+
if (this.connectionPromise) {
|
|
2582
|
+
await this.connectionPromise;
|
|
2583
|
+
}
|
|
2584
|
+
const id = `rpc_${nanoid(10)}`;
|
|
2585
|
+
const request = {
|
|
2586
|
+
id,
|
|
2587
|
+
method,
|
|
2588
|
+
params
|
|
2589
|
+
};
|
|
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);
|
|
2598
|
+
});
|
|
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;
|
|
2613
|
+
}
|
|
2614
|
+
return promise;
|
|
2615
|
+
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Handle RPC response
|
|
2618
|
+
*/
|
|
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
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
/**
|
|
2631
|
+
* Get all user sessions
|
|
2632
|
+
*/
|
|
2633
|
+
async getSessions() {
|
|
2634
|
+
return this.sendRequest("getSessions");
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* Connect to an MCP server
|
|
2638
|
+
*/
|
|
2639
|
+
async connectToServer(params) {
|
|
2640
|
+
return this.sendRequest("connect", params);
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Disconnect from an MCP server
|
|
2644
|
+
*/
|
|
2645
|
+
async disconnectFromServer(sessionId) {
|
|
2646
|
+
return this.sendRequest("disconnect", { sessionId });
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* List tools from a session
|
|
2650
|
+
*/
|
|
2651
|
+
async listTools(sessionId) {
|
|
2652
|
+
return this.sendRequest("listTools", { sessionId });
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Call a tool
|
|
2656
|
+
*/
|
|
2657
|
+
async callTool(sessionId, toolName, toolArgs) {
|
|
2658
|
+
return this.sendRequest("callTool", { sessionId, toolName, toolArgs });
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Refresh/validate a session
|
|
2662
|
+
*/
|
|
2663
|
+
async restoreSession(sessionId) {
|
|
2664
|
+
return this.sendRequest("restoreSession", { sessionId });
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Complete OAuth authorization
|
|
2668
|
+
*/
|
|
2669
|
+
async finishAuth(sessionId, code) {
|
|
2670
|
+
return this.sendRequest("finishAuth", { sessionId, code });
|
|
2671
|
+
}
|
|
2672
|
+
/**
|
|
2673
|
+
* List available prompts
|
|
2674
|
+
*/
|
|
2675
|
+
async listPrompts(sessionId) {
|
|
2676
|
+
return this.sendRequest("listPrompts", { sessionId });
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Get a specific prompt with arguments
|
|
2680
|
+
*/
|
|
2681
|
+
async getPrompt(sessionId, name, args) {
|
|
2682
|
+
return this.sendRequest("getPrompt", { sessionId, name, args });
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* List available resources
|
|
2686
|
+
*/
|
|
2687
|
+
async listResources(sessionId) {
|
|
2688
|
+
return this.sendRequest("listResources", { sessionId });
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Read a specific resource
|
|
2692
|
+
*/
|
|
2693
|
+
async readResource(sessionId, uri) {
|
|
2694
|
+
return this.sendRequest("readResource", { sessionId, uri });
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* Check if connected
|
|
2698
|
+
*/
|
|
2699
|
+
isConnected() {
|
|
2700
|
+
return this.eventSource !== null && this.eventSource.readyState === EventSource.OPEN;
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
|
|
2704
|
+
// src/shared/types.ts
|
|
2705
|
+
function isConnectSuccess(response) {
|
|
2706
|
+
return "success" in response && response.success === true;
|
|
2707
|
+
}
|
|
2708
|
+
function isConnectAuthRequired(response) {
|
|
2709
|
+
return "requiresAuth" in response && response.requiresAuth === true;
|
|
2710
|
+
}
|
|
2711
|
+
function isConnectError(response) {
|
|
2712
|
+
return "error" in response;
|
|
2713
|
+
}
|
|
2714
|
+
function isListToolsSuccess(response) {
|
|
2715
|
+
return "tools" in response;
|
|
2716
|
+
}
|
|
2717
|
+
function isCallToolSuccess(response) {
|
|
2718
|
+
return "content" in response;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
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 };
|
|
2722
|
+
//# sourceMappingURL=index.mjs.map
|
|
2723
|
+
//# sourceMappingURL=index.mjs.map
|