@mcp-ts/sdk 1.3.6 → 1.3.7
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/dist/adapters/agui-adapter.d.mts +1 -1
- package/dist/adapters/agui-adapter.d.ts +1 -1
- package/dist/adapters/agui-adapter.js +2 -2
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +2 -2
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +1 -1
- package/dist/adapters/agui-middleware.d.ts +1 -1
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +1 -1
- package/dist/adapters/ai-adapter.d.ts +1 -1
- package/dist/adapters/ai-adapter.js +1 -1
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +1 -1
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +1 -1
- package/dist/adapters/langchain-adapter.d.ts +1 -1
- package/dist/adapters/langchain-adapter.js +1 -1
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +1 -1
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.js +1 -1
- package/dist/adapters/mastra-adapter.js.map +1 -1
- package/dist/adapters/mastra-adapter.mjs +1 -1
- package/dist/adapters/mastra-adapter.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +134 -71
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +134 -71
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-BYLarghq.d.ts → multi-session-client-CHE8QpVE.d.ts} +75 -5
- package/dist/{multi-session-client-CzhMkE0k.d.mts → multi-session-client-CQsRbxYI.d.mts} +75 -5
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +134 -71
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +134 -71
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.js +10 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +10 -2
- package/dist/shared/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/adapters/agui-adapter.ts +222 -222
- package/src/adapters/ai-adapter.ts +115 -115
- package/src/adapters/langchain-adapter.ts +127 -127
- package/src/adapters/mastra-adapter.ts +126 -126
- package/src/server/mcp/multi-session-client.ts +135 -39
- package/src/server/storage/file-backend.ts +3 -16
- package/src/server/storage/index.ts +1 -0
- package/src/server/storage/memory-backend.ts +3 -16
- package/src/server/storage/redis-backend.ts +3 -16
- package/src/server/storage/sqlite-backend.ts +2 -6
- package/src/server/storage/supabase-backend.ts +2 -1
- package/src/shared/utils.ts +22 -0
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
import { MCPClient } from '../server/mcp/oauth-client';
|
|
2
|
-
import { MultiSessionClient } from '../server/mcp/multi-session-client';
|
|
3
|
-
import type { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
export interface MastraAdapterOptions {
|
|
6
|
-
/**
|
|
7
|
-
* Prefix for tool names to avoid collision with other tools.
|
|
8
|
-
* Defaults to the client's serverId.
|
|
9
|
-
*/
|
|
10
|
-
prefix?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Interface definition for a Mastra tool since we might not have the SDK installed.
|
|
15
|
-
* Based on Mastra documentation.
|
|
16
|
-
*/
|
|
17
|
-
export interface MastraTool {
|
|
18
|
-
id: string;
|
|
19
|
-
description: string;
|
|
20
|
-
inputSchema: z.ZodType<any>;
|
|
21
|
-
execute: (args: any) => Promise<any>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Adapter to use MCP tools within Mastra agents.
|
|
26
|
-
*/
|
|
27
|
-
export class MastraAdapter {
|
|
28
|
-
private z: typeof z | undefined;
|
|
29
|
-
|
|
30
|
-
constructor(
|
|
31
|
-
private client: MCPClient | MultiSessionClient,
|
|
32
|
-
private options: MastraAdapterOptions = {}
|
|
33
|
-
) { }
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Lazy-loads Zod dependency
|
|
37
|
-
*/
|
|
38
|
-
private async ensureZod() {
|
|
39
|
-
if (!this.z) {
|
|
40
|
-
try {
|
|
41
|
-
const zod = await import('zod');
|
|
42
|
-
this.z = zod.z;
|
|
43
|
-
} catch (error) {
|
|
44
|
-
throw new Error(
|
|
45
|
-
'zod is not installed. Install with:\n' +
|
|
46
|
-
' npm install zod'
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
private async transformTools(client: MCPClient): Promise<Record<string, MastraTool>> {
|
|
55
|
-
if (!client.isConnected()) {
|
|
56
|
-
return {};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
await this.ensureZod();
|
|
60
|
-
|
|
61
|
-
const result = await client.listTools();
|
|
62
|
-
const prefix = this.options.prefix ?? client.getServerId() ?? 'mcp';
|
|
63
|
-
const tools: Record<string, MastraTool> = {};
|
|
64
|
-
|
|
65
|
-
for (const tool of result.tools) {
|
|
66
|
-
const toolName = `${prefix}_${tool.name}`;
|
|
67
|
-
|
|
68
|
-
// In a real implementation, you would use a library like 'json-schema-to-zod'
|
|
69
|
-
const schema = this.jsonSchemaToZod(tool.inputSchema);
|
|
70
|
-
|
|
71
|
-
tools[toolName] = {
|
|
72
|
-
id: toolName,
|
|
73
|
-
description: tool.description || `Tool ${tool.name}`,
|
|
74
|
-
inputSchema: schema,
|
|
75
|
-
execute: async (args: any) => {
|
|
76
|
-
return await client.callTool(tool.name, args);
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return tools;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private jsonSchemaToZod(schema: any): z.ZodType<any> {
|
|
85
|
-
try {
|
|
86
|
-
const { parseSchema } = require('json-schema-to-zod');
|
|
87
|
-
const zodSchemaString = parseSchema(schema);
|
|
88
|
-
// eslint-disable-next-line
|
|
89
|
-
return new Function('z', 'return ' + zodSchemaString)(this.z);
|
|
90
|
-
} catch (error) {
|
|
91
|
-
// Fallback: Accept any object if conversion fails
|
|
92
|
-
console.warn('[MastraAdapter] Failed to convert JSON Schema to Zod, using fallback:', error);
|
|
93
|
-
return this.z!.record(this.z!.any()).optional().describe("Dynamic Input");
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Fetches tools from the MCP server and converts them to Mastra tools.
|
|
99
|
-
*/
|
|
100
|
-
async getTools(): Promise<Record<string, MastraTool>> {
|
|
101
|
-
// Use duck typing instead of instanceof to handle module bundling issues
|
|
102
|
-
const isMultiSession = typeof (this.client as any).getClients === 'function';
|
|
103
|
-
const clients = isMultiSession
|
|
104
|
-
? (this.client as MultiSessionClient).getClients()
|
|
105
|
-
: [this.client as MCPClient];
|
|
106
|
-
|
|
107
|
-
const results = await Promise.all(
|
|
108
|
-
clients.map(async (client) => {
|
|
109
|
-
try {
|
|
110
|
-
return await this.transformTools(client);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.error(`[MastraAdapter] Failed to fetch tools from ${client.getServerId()}:`, error);
|
|
113
|
-
return {};
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
);
|
|
117
|
-
return results.reduce((acc, tools) => ({ ...acc, ...tools }), {});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Convenience static method to fetch tools in a single line.
|
|
122
|
-
*/
|
|
123
|
-
static async getTools(client: MCPClient | MultiSessionClient, options: MastraAdapterOptions = {}): Promise<Record<string, MastraTool>> {
|
|
124
|
-
return new MastraAdapter(client, options).getTools();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
1
|
+
import { MCPClient } from '../server/mcp/oauth-client';
|
|
2
|
+
import { MultiSessionClient } from '../server/mcp/multi-session-client';
|
|
3
|
+
import type { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
export interface MastraAdapterOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Prefix for tool names to avoid collision with other tools.
|
|
8
|
+
* Defaults to the client's serverId.
|
|
9
|
+
*/
|
|
10
|
+
prefix?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Interface definition for a Mastra tool since we might not have the SDK installed.
|
|
15
|
+
* Based on Mastra documentation.
|
|
16
|
+
*/
|
|
17
|
+
export interface MastraTool {
|
|
18
|
+
id: string;
|
|
19
|
+
description: string;
|
|
20
|
+
inputSchema: z.ZodType<any>;
|
|
21
|
+
execute: (args: any) => Promise<any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adapter to use MCP tools within Mastra agents.
|
|
26
|
+
*/
|
|
27
|
+
export class MastraAdapter {
|
|
28
|
+
private z: typeof z | undefined;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private client: MCPClient | MultiSessionClient,
|
|
32
|
+
private options: MastraAdapterOptions = {}
|
|
33
|
+
) { }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lazy-loads Zod dependency
|
|
37
|
+
*/
|
|
38
|
+
private async ensureZod() {
|
|
39
|
+
if (!this.z) {
|
|
40
|
+
try {
|
|
41
|
+
const zod = await import('zod');
|
|
42
|
+
this.z = zod.z;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'zod is not installed. Install with:\n' +
|
|
46
|
+
' npm install zod'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
private async transformTools(client: MCPClient): Promise<Record<string, MastraTool>> {
|
|
55
|
+
if (!client.isConnected()) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await this.ensureZod();
|
|
60
|
+
|
|
61
|
+
const result = await client.listTools();
|
|
62
|
+
const prefix = this.options.prefix ?? client.getServerId()?.replace(/-/g, '').substring(0, 8) ?? 'mcp';
|
|
63
|
+
const tools: Record<string, MastraTool> = {};
|
|
64
|
+
|
|
65
|
+
for (const tool of result.tools) {
|
|
66
|
+
const toolName = `${prefix}_${tool.name}`;
|
|
67
|
+
|
|
68
|
+
// In a real implementation, you would use a library like 'json-schema-to-zod'
|
|
69
|
+
const schema = this.jsonSchemaToZod(tool.inputSchema);
|
|
70
|
+
|
|
71
|
+
tools[toolName] = {
|
|
72
|
+
id: toolName,
|
|
73
|
+
description: tool.description || `Tool ${tool.name}`,
|
|
74
|
+
inputSchema: schema,
|
|
75
|
+
execute: async (args: any) => {
|
|
76
|
+
return await client.callTool(tool.name, args);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return tools;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private jsonSchemaToZod(schema: any): z.ZodType<any> {
|
|
85
|
+
try {
|
|
86
|
+
const { parseSchema } = require('json-schema-to-zod');
|
|
87
|
+
const zodSchemaString = parseSchema(schema);
|
|
88
|
+
// eslint-disable-next-line
|
|
89
|
+
return new Function('z', 'return ' + zodSchemaString)(this.z);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Fallback: Accept any object if conversion fails
|
|
92
|
+
console.warn('[MastraAdapter] Failed to convert JSON Schema to Zod, using fallback:', error);
|
|
93
|
+
return this.z!.record(this.z!.any()).optional().describe("Dynamic Input");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fetches tools from the MCP server and converts them to Mastra tools.
|
|
99
|
+
*/
|
|
100
|
+
async getTools(): Promise<Record<string, MastraTool>> {
|
|
101
|
+
// Use duck typing instead of instanceof to handle module bundling issues
|
|
102
|
+
const isMultiSession = typeof (this.client as any).getClients === 'function';
|
|
103
|
+
const clients = isMultiSession
|
|
104
|
+
? (this.client as MultiSessionClient).getClients()
|
|
105
|
+
: [this.client as MCPClient];
|
|
106
|
+
|
|
107
|
+
const results = await Promise.all(
|
|
108
|
+
clients.map(async (client) => {
|
|
109
|
+
try {
|
|
110
|
+
return await this.transformTools(client);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`[MastraAdapter] Failed to fetch tools from ${client.getServerId()}:`, error);
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
return results.reduce((acc, tools) => ({ ...acc, ...tools }), {});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Convenience static method to fetch tools in a single line.
|
|
122
|
+
*/
|
|
123
|
+
static async getTools(client: MCPClient | MultiSessionClient, options: MastraAdapterOptions = {}): Promise<Record<string, MastraTool>> {
|
|
124
|
+
return new MastraAdapter(client, options).getTools();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
import { MCPClient } from './oauth-client.js';
|
|
4
4
|
import { storage, type SessionData } from '../storage/index.js';
|
|
5
5
|
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
7
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
8
|
+
const DEFAULT_RETRY_DELAY_MS = 1000;
|
|
9
|
+
const CONNECTION_BATCH_SIZE = 5;
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* Manages multiple MCP connections for a single user identity.
|
|
8
13
|
* Allows aggregating tools from all connected servers.
|
|
@@ -26,57 +31,155 @@ export interface MultiSessionOptions {
|
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
|
-
* Manages multiple MCP connections for a single user identity.
|
|
30
|
-
*
|
|
34
|
+
* Manages multiple MCP client connections for a single user identity.
|
|
35
|
+
*
|
|
36
|
+
* On a traditional long-running server, you can cache this instance per user
|
|
37
|
+
* so the connections stay alive between requests. On serverless, a new instance
|
|
38
|
+
* will be created per invocation, but the underlying session data is always
|
|
39
|
+
* read from the storage backend so nothing is lost between calls.
|
|
31
40
|
*/
|
|
32
41
|
export class MultiSessionClient {
|
|
33
42
|
private clients: MCPClient[] = [];
|
|
34
43
|
private identity: string;
|
|
35
44
|
private options: MultiSessionOptions;
|
|
36
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new MultiSessionClient for the given user identity.
|
|
48
|
+
*
|
|
49
|
+
* @param identity - A unique string identifying the user (e.g. user ID or email).
|
|
50
|
+
* @param options - Optional tuning for connection timeout, retry count, and retry delay.
|
|
51
|
+
* Falls back to sensible defaults if not provided.
|
|
52
|
+
*/
|
|
37
53
|
constructor(identity: string, options: MultiSessionOptions = {}) {
|
|
38
54
|
this.identity = identity;
|
|
39
55
|
this.options = {
|
|
40
|
-
timeout:
|
|
41
|
-
maxRetries:
|
|
42
|
-
retryDelay:
|
|
56
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
57
|
+
maxRetries: DEFAULT_MAX_RETRIES,
|
|
58
|
+
retryDelay: DEFAULT_RETRY_DELAY_MS,
|
|
43
59
|
...options
|
|
44
60
|
};
|
|
45
61
|
}
|
|
46
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Fetches all sessions for this identity from storage and returns only the
|
|
65
|
+
* ones that are ready to connect.
|
|
66
|
+
*
|
|
67
|
+
* A session is considered connectable when:
|
|
68
|
+
* - It has a `serverId`, `serverUrl`, and `callbackUrl` (i.e. it was fully initialized)
|
|
69
|
+
* - Its `active` flag is not explicitly `false` — sessions with `active: false` are
|
|
70
|
+
* either mid-OAuth flow, auth-pending, or previously failed. We skip those here
|
|
71
|
+
* and let the OAuth flow complete separately before we try to reconnect them.
|
|
72
|
+
*
|
|
73
|
+
* Note: Sessions where `active` is `undefined` (legacy records) are included
|
|
74
|
+
* for backwards compatibility.
|
|
75
|
+
*/
|
|
47
76
|
private async getActiveSessions(): Promise<SessionData[]> {
|
|
48
77
|
const sessions = await storage.getIdentitySessionsData(this.identity);
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
const valid = sessions.filter(s =>
|
|
79
|
+
s.serverId &&
|
|
80
|
+
s.serverUrl &&
|
|
81
|
+
s.callbackUrl &&
|
|
82
|
+
s.active !== false // exclude OAuth-pending / failed sessions
|
|
51
83
|
);
|
|
52
|
-
const valid = sessions.filter(s => s.serverId && s.serverUrl && s.callbackUrl);
|
|
53
|
-
console.log(`[MultiSessionClient] Filtered valid sessions:`, valid.length);
|
|
54
84
|
return valid;
|
|
55
85
|
}
|
|
56
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Connects to a list of sessions in controlled batches of `CONNECTION_BATCH_SIZE`.
|
|
89
|
+
*
|
|
90
|
+
* Batching prevents overwhelming the event loop or external servers when a user
|
|
91
|
+
* has many active MCP sessions (e.g. 20+ servers). Within each batch, sessions
|
|
92
|
+
* are connected concurrently using `Promise.all` for speed.
|
|
93
|
+
*/
|
|
57
94
|
private async connectInBatches(sessions: SessionData[]): Promise<void> {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const batch = sessions.slice(i, i + BATCH_SIZE);
|
|
95
|
+
for (let i = 0; i < sessions.length; i += CONNECTION_BATCH_SIZE) {
|
|
96
|
+
const batch = sessions.slice(i, i + CONNECTION_BATCH_SIZE);
|
|
61
97
|
await Promise.all(batch.map(session => this.connectSession(session)));
|
|
62
98
|
}
|
|
63
99
|
}
|
|
64
100
|
|
|
101
|
+
private connectionPromises = new Map<string, Promise<void>>();
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Connects a single session, with built-in deduplication to prevent race conditions.
|
|
105
|
+
*
|
|
106
|
+
* - If a client for this session already exists and is connected, returns immediately.
|
|
107
|
+
* - If a connection attempt for this session is already in-flight (e.g. from a
|
|
108
|
+
* concurrent call), it joins the existing promise instead of starting a new one.
|
|
109
|
+
* This is the key concurrency lock — the `connectionPromises` map acts as a
|
|
110
|
+
* per-session mutex so we never spin up two physical connections for the same session.
|
|
111
|
+
* - On completion (success or failure), the promise is cleaned up from the map.
|
|
112
|
+
*/
|
|
65
113
|
private async connectSession(session: SessionData): Promise<void> {
|
|
66
114
|
const existingClient = this.clients.find(c => c.getSessionId() === session.sessionId);
|
|
67
115
|
if (existingClient?.isConnected()) {
|
|
68
116
|
return;
|
|
69
117
|
}
|
|
70
118
|
|
|
71
|
-
|
|
72
|
-
|
|
119
|
+
// Avoid concurrent connection attempts for the same session
|
|
120
|
+
if (this.connectionPromises.has(session.sessionId)) {
|
|
121
|
+
return this.connectionPromises.get(session.sessionId)!;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const connectPromise = this.establishConnectionWithRetries(session);
|
|
125
|
+
|
|
126
|
+
this.connectionPromises.set(session.sessionId, connectPromise);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await connectPromise;
|
|
130
|
+
} finally {
|
|
131
|
+
this.connectionPromises.delete(session.sessionId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The core connection loop for a single session.
|
|
137
|
+
*
|
|
138
|
+
* Attempts to establish a physical MCP connection, retrying up to `maxRetries` times
|
|
139
|
+
* if the connection fails. Each attempt:
|
|
140
|
+
* 1. Creates a fresh `MCPClient` instance from the session data.
|
|
141
|
+
* 2. Races the connect call against a timeout promise — if the server doesn't respond
|
|
142
|
+
* within `timeoutMs`, the attempt is aborted and counted as a failure.
|
|
143
|
+
* 3. On success, replaces any stale client entry for this session in the `clients` array.
|
|
144
|
+
* 4. On failure, waits `retryDelay` ms before the next attempt.
|
|
145
|
+
*
|
|
146
|
+
* If all attempts are exhausted, logs an error and returns silently (does not throw),
|
|
147
|
+
* so a single bad server doesn't block the rest of the batch from connecting.
|
|
148
|
+
*/
|
|
149
|
+
private async establishConnectionWithRetries(session: SessionData): Promise<void> {
|
|
150
|
+
const maxRetries = this.options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
151
|
+
const retryDelay = this.options.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
|
|
73
152
|
let lastError: unknown;
|
|
74
153
|
|
|
75
154
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
76
155
|
try {
|
|
77
|
-
const client =
|
|
156
|
+
const client = new MCPClient({
|
|
157
|
+
identity: this.identity,
|
|
158
|
+
sessionId: session.sessionId,
|
|
159
|
+
serverId: session.serverId,
|
|
160
|
+
serverUrl: session.serverUrl,
|
|
161
|
+
callbackUrl: session.callbackUrl,
|
|
162
|
+
serverName: session.serverName,
|
|
163
|
+
transportType: session.transportType,
|
|
164
|
+
headers: session.headers,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const timeoutMs = this.options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
168
|
+
let timeoutTimer: ReturnType<typeof setTimeout>;
|
|
169
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
170
|
+
timeoutTimer = setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await Promise.race([client.connect(), timeoutPromise]);
|
|
175
|
+
} finally {
|
|
176
|
+
clearTimeout(timeoutTimer!);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Always replace the disconnected client entry
|
|
180
|
+
this.clients = this.clients.filter(c => c.getSessionId() !== session.sessionId);
|
|
78
181
|
this.clients.push(client);
|
|
79
|
-
return;
|
|
182
|
+
return; // successfully connected
|
|
80
183
|
} catch (error) {
|
|
81
184
|
lastError = error;
|
|
82
185
|
if (attempt < maxRetries) {
|
|
@@ -88,41 +191,34 @@ export class MultiSessionClient {
|
|
|
88
191
|
console.error(`[MultiSessionClient] Failed to connect to session ${session.sessionId} after ${maxRetries + 1} attempts:`, lastError);
|
|
89
192
|
}
|
|
90
193
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
transportType: session.transportType,
|
|
100
|
-
headers: session.headers,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
const timeoutMs = this.options.timeout ?? 15000;
|
|
104
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
105
|
-
setTimeout(() => reject(new Error(`Connection timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
await Promise.race([client.connect(), timeoutPromise]);
|
|
109
|
-
return client;
|
|
110
|
-
}
|
|
111
|
-
|
|
194
|
+
/**
|
|
195
|
+
* The main entry point. Fetches all active sessions for this identity from
|
|
196
|
+
* storage and establishes connections to all of them in batches.
|
|
197
|
+
*
|
|
198
|
+
* Call this once after creating the client. On traditional servers, you can
|
|
199
|
+
* cache the `MultiSessionClient` instance after calling `connect()` to avoid
|
|
200
|
+
* re-fetching and re-connecting on every request.
|
|
201
|
+
*/
|
|
112
202
|
async connect(): Promise<void> {
|
|
113
203
|
const sessions = await this.getActiveSessions();
|
|
114
204
|
await this.connectInBatches(sessions);
|
|
115
205
|
}
|
|
116
206
|
|
|
117
207
|
/**
|
|
118
|
-
* Returns
|
|
208
|
+
* Returns all currently connected `MCPClient` instances.
|
|
209
|
+
*
|
|
210
|
+
* Use this to enumerate available tools across all connected servers,
|
|
211
|
+
* or to route a tool call to the right client by `serverId`.
|
|
119
212
|
*/
|
|
120
213
|
getClients(): MCPClient[] {
|
|
121
214
|
return this.clients;
|
|
122
215
|
}
|
|
123
216
|
|
|
124
217
|
/**
|
|
125
|
-
*
|
|
218
|
+
* Gracefully disconnects all active MCP clients and clears the internal client list.
|
|
219
|
+
*
|
|
220
|
+
* Call this during server shutdown or when a user logs out to free up
|
|
221
|
+
* underlying transport resources (SSE streams, HTTP connections, etc.).
|
|
126
222
|
*/
|
|
127
223
|
disconnect(): void {
|
|
128
224
|
this.clients.forEach((client) => client.disconnect());
|
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
|
|
2
1
|
import { promises as fs } from 'fs';
|
|
3
2
|
import * as path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
// first char: letters only (required by OpenAI)
|
|
8
|
-
const firstChar = customAlphabet(
|
|
9
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
10
|
-
1
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
// remaining chars: alphanumeric
|
|
14
|
-
const rest = customAlphabet(
|
|
15
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
|
16
|
-
11
|
|
17
|
-
);
|
|
3
|
+
import { StorageBackend, SessionData, SetClientOptions } from './types.js';
|
|
4
|
+
import { generateSessionId } from '../../shared/utils.js';
|
|
18
5
|
|
|
19
6
|
/**
|
|
20
7
|
* File system implementation of StorageBackend
|
|
@@ -83,7 +70,7 @@ export class FileStorageBackend implements StorageBackend {
|
|
|
83
70
|
}
|
|
84
71
|
|
|
85
72
|
generateSessionId(): string {
|
|
86
|
-
return
|
|
73
|
+
return generateSessionId();
|
|
87
74
|
}
|
|
88
75
|
|
|
89
76
|
async createSession(session: SessionData, ttl?: number): Promise<void> {
|
|
@@ -8,6 +8,7 @@ import type { StorageBackend } from './types.js';
|
|
|
8
8
|
|
|
9
9
|
// Re-export types
|
|
10
10
|
export * from './types.js';
|
|
11
|
+
export { generateSessionId } from '../../shared/utils.js';
|
|
11
12
|
export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend, SqliteStorage, SupabaseStorageBackend };
|
|
12
13
|
|
|
13
14
|
export function createSupabaseStorageBackend(client: any): SupabaseStorageBackend {
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import { StorageBackend, SessionData, SetClientOptions } from './types';
|
|
4
|
-
|
|
5
|
-
// first char: letters only (required by OpenAI)
|
|
6
|
-
const firstChar = customAlphabet(
|
|
7
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
8
|
-
1
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
// remaining chars: alphanumeric
|
|
12
|
-
const rest = customAlphabet(
|
|
13
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
|
14
|
-
11
|
|
15
|
-
);
|
|
1
|
+
import { StorageBackend, SessionData, SetClientOptions } from './types.js';
|
|
2
|
+
import { generateSessionId } from '../../shared/utils.js';
|
|
16
3
|
|
|
17
4
|
/**
|
|
18
5
|
* In-memory implementation of StorageBackend
|
|
@@ -36,7 +23,7 @@ export class MemoryStorageBackend implements StorageBackend {
|
|
|
36
23
|
}
|
|
37
24
|
|
|
38
25
|
generateSessionId(): string {
|
|
39
|
-
return
|
|
26
|
+
return generateSessionId();
|
|
40
27
|
}
|
|
41
28
|
|
|
42
29
|
async createSession(session: SessionData, ttl?: number): Promise<void> {
|
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
|
|
2
1
|
import type { Redis } from 'ioredis';
|
|
3
|
-
import {
|
|
4
|
-
import { StorageBackend, SessionData } from './types';
|
|
2
|
+
import { StorageBackend, SessionData } from './types.js';
|
|
5
3
|
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
6
|
-
|
|
7
|
-
/** first char: letters only (required by OpenAI) */
|
|
8
|
-
const firstChar = customAlphabet(
|
|
9
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
10
|
-
1
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
/** remaining chars: alphanumeric */
|
|
14
|
-
const rest = customAlphabet(
|
|
15
|
-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
|
16
|
-
11
|
|
17
|
-
);
|
|
4
|
+
import { generateSessionId } from '../../shared/utils.js';
|
|
18
5
|
|
|
19
6
|
/**
|
|
20
7
|
* Redis implementation of StorageBackend
|
|
@@ -88,7 +75,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
88
75
|
}
|
|
89
76
|
|
|
90
77
|
generateSessionId(): string {
|
|
91
|
-
return
|
|
78
|
+
return generateSessionId();
|
|
92
79
|
}
|
|
93
80
|
|
|
94
81
|
async createSession(session: SessionData, ttl?: number): Promise<void> {
|
|
@@ -2,6 +2,7 @@ import type { Database } from 'better-sqlite3';
|
|
|
2
2
|
import { StorageBackend, SessionData } from './types.js'; // Ensure .js extension
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
+
import { generateSessionId } from '../../shared/utils.js';
|
|
5
6
|
|
|
6
7
|
export interface SqliteStorageOptions {
|
|
7
8
|
path?: string;
|
|
@@ -62,12 +63,7 @@ export class SqliteStorage implements StorageBackend {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
generateSessionId(): string {
|
|
65
|
-
|
|
66
|
-
let result = '';
|
|
67
|
-
for (let i = 0; i < 32; i++) {
|
|
68
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
69
|
-
}
|
|
70
|
-
return result;
|
|
66
|
+
return generateSessionId();
|
|
71
67
|
}
|
|
72
68
|
|
|
73
69
|
async createSession(session: SessionData, ttl?: number): Promise<void> {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import { StorageBackend, SessionData } from './types.js';
|
|
3
3
|
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
4
|
+
import { generateSessionId } from '../../shared/utils.js';
|
|
4
5
|
|
|
5
6
|
export class SupabaseStorageBackend implements StorageBackend {
|
|
6
7
|
private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
|
|
@@ -29,7 +30,7 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
generateSessionId(): string {
|
|
32
|
-
return
|
|
33
|
+
return generateSessionId();
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
private mapRowToSessionData(row: any): SessionData {
|
package/src/shared/utils.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import { customAlphabet } from 'nanoid';
|
|
2
|
+
|
|
3
|
+
/** first char: letters only (required by OpenAI) */
|
|
4
|
+
const firstChar = customAlphabet(
|
|
5
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
6
|
+
1
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
/** remaining chars: alphanumeric */
|
|
10
|
+
const rest = customAlphabet(
|
|
11
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
|
12
|
+
11
|
|
13
|
+
);
|
|
14
|
+
|
|
1
15
|
/**
|
|
2
16
|
* Sanitize server name to create a valid server label
|
|
3
17
|
* Must start with a letter and contain only letters, digits, '-' and '_'
|
|
@@ -14,3 +28,11 @@ export function sanitizeServerLabel(name: string): string {
|
|
|
14
28
|
|
|
15
29
|
return sanitized;
|
|
16
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generates a standard 12-character session ID compliant with external tool restrictions.
|
|
34
|
+
* First character is always a letter.
|
|
35
|
+
*/
|
|
36
|
+
export function generateSessionId(): string {
|
|
37
|
+
return firstChar() + rest();
|
|
38
|
+
}
|