@evantahler/mcpx 0.15.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/.claude/settings.local.json +18 -0
- package/.claude/skills/mcpx.md +165 -0
- package/.claude/worktrees/elastic-jennings/.claude/settings.local.json +18 -0
- package/.claude/worktrees/elastic-jennings/.claude/skills/mcpcli.md +93 -0
- package/.claude/worktrees/elastic-jennings/.github/workflows/auto-release.yml +117 -0
- package/.claude/worktrees/elastic-jennings/.github/workflows/ci.yml +18 -0
- package/.claude/worktrees/elastic-jennings/.prettierignore +4 -0
- package/.claude/worktrees/elastic-jennings/.prettierrc +7 -0
- package/.claude/worktrees/elastic-jennings/CLAUDE.md +19 -0
- package/.claude/worktrees/elastic-jennings/LICENSE +21 -0
- package/.claude/worktrees/elastic-jennings/README.md +487 -0
- package/.claude/worktrees/elastic-jennings/bun.lock +381 -0
- package/.claude/worktrees/elastic-jennings/install.sh +55 -0
- package/.claude/worktrees/elastic-jennings/package.json +56 -0
- package/.claude/worktrees/elastic-jennings/src/cli.ts +39 -0
- package/.claude/worktrees/elastic-jennings/src/client/http.ts +100 -0
- package/.claude/worktrees/elastic-jennings/src/client/manager.ts +266 -0
- package/.claude/worktrees/elastic-jennings/src/client/oauth.ts +299 -0
- package/.claude/worktrees/elastic-jennings/src/client/stdio.ts +12 -0
- package/.claude/worktrees/elastic-jennings/src/commands/add.ts +155 -0
- package/.claude/worktrees/elastic-jennings/src/commands/auth.ts +114 -0
- package/.claude/worktrees/elastic-jennings/src/commands/exec.ts +91 -0
- package/.claude/worktrees/elastic-jennings/src/commands/index.ts +62 -0
- package/.claude/worktrees/elastic-jennings/src/commands/info.ts +38 -0
- package/.claude/worktrees/elastic-jennings/src/commands/list.ts +30 -0
- package/.claude/worktrees/elastic-jennings/src/commands/remove.ts +67 -0
- package/.claude/worktrees/elastic-jennings/src/commands/search.ts +45 -0
- package/.claude/worktrees/elastic-jennings/src/commands/skill.ts +70 -0
- package/.claude/worktrees/elastic-jennings/src/config/env.ts +41 -0
- package/.claude/worktrees/elastic-jennings/src/config/loader.ts +156 -0
- package/.claude/worktrees/elastic-jennings/src/config/schemas.ts +137 -0
- package/.claude/worktrees/elastic-jennings/src/context.ts +53 -0
- package/.claude/worktrees/elastic-jennings/src/output/formatter.ts +316 -0
- package/.claude/worktrees/elastic-jennings/src/output/logger.ts +114 -0
- package/.claude/worktrees/elastic-jennings/src/search/index.ts +69 -0
- package/.claude/worktrees/elastic-jennings/src/search/indexer.ts +92 -0
- package/.claude/worktrees/elastic-jennings/src/search/keyword.ts +86 -0
- package/.claude/worktrees/elastic-jennings/src/search/semantic.ts +75 -0
- package/.claude/worktrees/elastic-jennings/src/search/staleness.ts +8 -0
- package/.claude/worktrees/elastic-jennings/src/validation/schema.ts +77 -0
- package/.claude/worktrees/elastic-jennings/test/cli.test.ts +51 -0
- package/.claude/worktrees/elastic-jennings/test/client/manager.test.ts +249 -0
- package/.claude/worktrees/elastic-jennings/test/client/oauth.test.ts +328 -0
- package/.claude/worktrees/elastic-jennings/test/commands/add-remove.test.ts +253 -0
- package/.claude/worktrees/elastic-jennings/test/commands/exec.test.ts +105 -0
- package/.claude/worktrees/elastic-jennings/test/commands/info.test.ts +48 -0
- package/.claude/worktrees/elastic-jennings/test/commands/list.test.ts +39 -0
- package/.claude/worktrees/elastic-jennings/test/commands/skill.test.ts +98 -0
- package/.claude/worktrees/elastic-jennings/test/config/env.test.ts +61 -0
- package/.claude/worktrees/elastic-jennings/test/config/loader.test.ts +139 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/.keep +0 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/auth.json +10 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/mock-config/.keep +0 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/mock-config/servers.json +8 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/mock-server.ts +113 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/search.json +15 -0
- package/.claude/worktrees/elastic-jennings/test/fixtures/servers.json +18 -0
- package/.claude/worktrees/elastic-jennings/test/integration/stdio-server.test.ts +149 -0
- package/.claude/worktrees/elastic-jennings/test/output/formatter.test.ts +54 -0
- package/.claude/worktrees/elastic-jennings/test/output/logger.test.ts +89 -0
- package/.claude/worktrees/elastic-jennings/test/search/indexer.test.ts +32 -0
- package/.claude/worktrees/elastic-jennings/test/search/keyword.test.ts +80 -0
- package/.claude/worktrees/elastic-jennings/test/search/semantic.test.ts +32 -0
- package/.claude/worktrees/elastic-jennings/test/validation/schema.test.ts +113 -0
- package/.claude/worktrees/elastic-jennings/tsconfig.json +29 -0
- package/.cursor/rules/mcpx.mdc +165 -0
- package/LICENSE +21 -0
- package/README.md +627 -0
- package/package.json +58 -0
- package/src/cli.ts +72 -0
- package/src/client/browser.ts +24 -0
- package/src/client/debug-fetch.ts +81 -0
- package/src/client/elicitation.ts +368 -0
- package/src/client/http.ts +25 -0
- package/src/client/manager.ts +566 -0
- package/src/client/oauth.ts +314 -0
- package/src/client/sse.ts +17 -0
- package/src/client/stdio.ts +12 -0
- package/src/client/trace.ts +184 -0
- package/src/commands/add.ts +179 -0
- package/src/commands/auth.ts +114 -0
- package/src/commands/exec.ts +156 -0
- package/src/commands/index.ts +62 -0
- package/src/commands/info.ts +63 -0
- package/src/commands/list.ts +64 -0
- package/src/commands/ping.ts +69 -0
- package/src/commands/prompt.ts +60 -0
- package/src/commands/remove.ts +67 -0
- package/src/commands/resource.ts +46 -0
- package/src/commands/search.ts +49 -0
- package/src/commands/servers.ts +66 -0
- package/src/commands/skill.ts +112 -0
- package/src/commands/task.ts +82 -0
- package/src/config/env.ts +41 -0
- package/src/config/loader.ts +156 -0
- package/src/config/schemas.ts +152 -0
- package/src/context.ts +62 -0
- package/src/lib/input.ts +36 -0
- package/src/output/formatter.ts +884 -0
- package/src/output/logger.ts +173 -0
- package/src/search/index.ts +69 -0
- package/src/search/indexer.ts +92 -0
- package/src/search/keyword.ts +86 -0
- package/src/search/semantic.ts +75 -0
- package/src/search/staleness.ts +8 -0
- package/src/validation/schema.ts +103 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
|
+
import type { ResponseMessage } from "@modelcontextprotocol/sdk/shared/responseMessage.js";
|
|
4
|
+
import {
|
|
5
|
+
LoggingMessageNotificationSchema,
|
|
6
|
+
CallToolResultSchema,
|
|
7
|
+
ElicitRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import type {
|
|
10
|
+
LoggingLevel,
|
|
11
|
+
ServerCapabilities,
|
|
12
|
+
CallToolResult,
|
|
13
|
+
Task,
|
|
14
|
+
GetTaskResult,
|
|
15
|
+
ListTasksResult,
|
|
16
|
+
CancelTaskResult,
|
|
17
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
18
|
+
import { handleElicitation } from "./elicitation.ts";
|
|
19
|
+
import picomatch from "picomatch";
|
|
20
|
+
import pkg from "../../package.json";
|
|
21
|
+
import type {
|
|
22
|
+
Tool,
|
|
23
|
+
Resource,
|
|
24
|
+
Prompt,
|
|
25
|
+
ServerConfig,
|
|
26
|
+
ServersFile,
|
|
27
|
+
AuthFile,
|
|
28
|
+
} from "../config/schemas.ts";
|
|
29
|
+
import { isStdioServer, isHttpServer } from "../config/schemas.ts";
|
|
30
|
+
import { createStdioTransport } from "./stdio.ts";
|
|
31
|
+
import { createHttpTransport } from "./http.ts";
|
|
32
|
+
import { createSseTransport } from "./sse.ts";
|
|
33
|
+
import { McpOAuthProvider } from "./oauth.ts";
|
|
34
|
+
import { logger } from "../output/logger.ts";
|
|
35
|
+
import { wrapTransportWithTrace } from "./trace.ts";
|
|
36
|
+
|
|
37
|
+
export interface ToolWithServer {
|
|
38
|
+
server: string;
|
|
39
|
+
tool: Tool;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ResourceWithServer {
|
|
43
|
+
server: string;
|
|
44
|
+
resource: Resource;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PromptWithServer {
|
|
48
|
+
server: string;
|
|
49
|
+
prompt: Prompt;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ServerInfo {
|
|
53
|
+
version?: { name: string; version: string };
|
|
54
|
+
capabilities?: ServerCapabilities;
|
|
55
|
+
instructions?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ServerError {
|
|
59
|
+
server: string;
|
|
60
|
+
message: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ServerManagerOptions {
|
|
64
|
+
servers: ServersFile;
|
|
65
|
+
configDir: string;
|
|
66
|
+
auth: AuthFile;
|
|
67
|
+
concurrency?: number;
|
|
68
|
+
verbose?: boolean;
|
|
69
|
+
showSecrets?: boolean;
|
|
70
|
+
timeout?: number; // ms, default 1_800_000 (30 min)
|
|
71
|
+
maxRetries?: number; // default 3
|
|
72
|
+
logLevel?: string; // MCP log level, default "warning"
|
|
73
|
+
json?: boolean; // JSON output mode (for trace formatting)
|
|
74
|
+
noInteractive?: boolean; // decline elicitation requests
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class ServerManager {
|
|
78
|
+
private clients = new Map<string, Client>();
|
|
79
|
+
private connecting = new Map<string, Promise<Client>>();
|
|
80
|
+
private transports = new Map<string, Transport>();
|
|
81
|
+
private oauthProviders = new Map<string, McpOAuthProvider>();
|
|
82
|
+
private servers: ServersFile;
|
|
83
|
+
private configDir: string;
|
|
84
|
+
private auth: AuthFile;
|
|
85
|
+
private concurrency: number;
|
|
86
|
+
private verbose: boolean;
|
|
87
|
+
private showSecrets: boolean;
|
|
88
|
+
private timeout: number;
|
|
89
|
+
private maxRetries: number;
|
|
90
|
+
private logLevel: string;
|
|
91
|
+
private json: boolean;
|
|
92
|
+
private noInteractive: boolean;
|
|
93
|
+
|
|
94
|
+
constructor(opts: ServerManagerOptions) {
|
|
95
|
+
this.servers = opts.servers;
|
|
96
|
+
this.configDir = opts.configDir;
|
|
97
|
+
this.auth = opts.auth;
|
|
98
|
+
this.concurrency = opts.concurrency ?? 5;
|
|
99
|
+
this.verbose = opts.verbose ?? false;
|
|
100
|
+
this.showSecrets = opts.showSecrets ?? false;
|
|
101
|
+
this.timeout = opts.timeout ?? 1_800_000;
|
|
102
|
+
this.maxRetries = opts.maxRetries ?? 3;
|
|
103
|
+
this.logLevel = opts.logLevel ?? "warning";
|
|
104
|
+
this.json = opts.json ?? false;
|
|
105
|
+
this.noInteractive = opts.noInteractive ?? false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Get or create a connected client for a server */
|
|
109
|
+
async getClient(serverName: string): Promise<Client> {
|
|
110
|
+
const existing = this.clients.get(serverName);
|
|
111
|
+
if (existing) return existing;
|
|
112
|
+
|
|
113
|
+
// If a connection is already in flight, wait for it instead of opening a second one
|
|
114
|
+
const inflight = this.connecting.get(serverName);
|
|
115
|
+
if (inflight) return inflight;
|
|
116
|
+
|
|
117
|
+
const config = this.servers.mcpServers[serverName];
|
|
118
|
+
if (!config) {
|
|
119
|
+
throw new Error(`Unknown server: "${serverName}"`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const connectPromise = (async () => {
|
|
123
|
+
// Auto-refresh expired OAuth tokens before connecting to HTTP servers.
|
|
124
|
+
// Only enforce auth if the server has a partial/incomplete auth entry —
|
|
125
|
+
// servers that don't require OAuth won't have an auth entry at all.
|
|
126
|
+
if (isHttpServer(config)) {
|
|
127
|
+
const hasAuthEntry = !!this.auth[serverName];
|
|
128
|
+
if (hasAuthEntry) {
|
|
129
|
+
const provider = this.getOrCreateOAuthProvider(serverName);
|
|
130
|
+
if (!provider.isComplete()) {
|
|
131
|
+
throw new Error(`Not authenticated with "${serverName}". Run: mcpx auth ${serverName}`);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
await provider.refreshIfNeeded(config.url);
|
|
135
|
+
} catch {
|
|
136
|
+
// If refresh fails, continue — the transport will send the existing token
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const rawTransport = this.createTransport(serverName, config);
|
|
142
|
+
const transport = this.verbose
|
|
143
|
+
? wrapTransportWithTrace(rawTransport, { json: this.json, serverName })
|
|
144
|
+
: rawTransport;
|
|
145
|
+
this.transports.set(serverName, transport);
|
|
146
|
+
|
|
147
|
+
let client = this.createClient();
|
|
148
|
+
try {
|
|
149
|
+
await this.withTimeout(client.connect(transport), `connect(${serverName})`);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// Auto-fallback: if no explicit transport was set on an HTTP server,
|
|
152
|
+
// retry with the legacy SSE transport
|
|
153
|
+
if (isHttpServer(config) && !config.transport) {
|
|
154
|
+
if (this.verbose) {
|
|
155
|
+
logger.writeRaw(`Streamable HTTP failed for "${serverName}", trying SSE…\n`);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
await transport.close?.();
|
|
159
|
+
} catch {
|
|
160
|
+
// ignore close errors
|
|
161
|
+
}
|
|
162
|
+
const provider = this.getOrCreateOAuthProvider(serverName);
|
|
163
|
+
const rawSseTransport = createSseTransport(
|
|
164
|
+
config,
|
|
165
|
+
provider.isComplete() ? provider : undefined,
|
|
166
|
+
this.verbose,
|
|
167
|
+
this.showSecrets,
|
|
168
|
+
);
|
|
169
|
+
const sseTransport = this.verbose
|
|
170
|
+
? wrapTransportWithTrace(rawSseTransport, { json: this.json, serverName })
|
|
171
|
+
: rawSseTransport;
|
|
172
|
+
this.transports.set(serverName, sseTransport);
|
|
173
|
+
client = this.createClient();
|
|
174
|
+
await this.withTimeout(client.connect(sseTransport), `connect-sse(${serverName})`);
|
|
175
|
+
} else {
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
this.setupLogging(serverName, client);
|
|
180
|
+
this.clients.set(serverName, client);
|
|
181
|
+
this.connecting.delete(serverName);
|
|
182
|
+
|
|
183
|
+
return client;
|
|
184
|
+
})().catch((err) => {
|
|
185
|
+
this.connecting.delete(serverName);
|
|
186
|
+
throw err;
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.connecting.set(serverName, connectPromise);
|
|
190
|
+
return connectPromise;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private getOrCreateOAuthProvider(serverName: string): McpOAuthProvider {
|
|
194
|
+
let provider = this.oauthProviders.get(serverName);
|
|
195
|
+
if (!provider) {
|
|
196
|
+
provider = new McpOAuthProvider({
|
|
197
|
+
serverName,
|
|
198
|
+
configDir: this.configDir,
|
|
199
|
+
auth: this.auth,
|
|
200
|
+
});
|
|
201
|
+
this.oauthProviders.set(serverName, provider);
|
|
202
|
+
}
|
|
203
|
+
return provider;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Create a Client with elicitation capabilities and handler registered */
|
|
207
|
+
private createClient(): Client {
|
|
208
|
+
const client = new Client(
|
|
209
|
+
{ name: pkg.name, version: pkg.version },
|
|
210
|
+
{ capabilities: { elicitation: { form: {}, url: {} } } },
|
|
211
|
+
);
|
|
212
|
+
client.setRequestHandler(ElicitRequestSchema, (request) =>
|
|
213
|
+
handleElicitation(request, {
|
|
214
|
+
noInteractive: this.noInteractive,
|
|
215
|
+
json: this.json,
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
return client;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Subscribe to server log notifications and set the desired log level */
|
|
222
|
+
private setupLogging(serverName: string, client: Client): void {
|
|
223
|
+
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
|
|
224
|
+
logger.logServerMessage(serverName, notification.params);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const capabilities = client.getServerCapabilities();
|
|
228
|
+
if (capabilities?.logging) {
|
|
229
|
+
client.setLoggingLevel(this.logLevel as LoggingLevel).catch(() => {
|
|
230
|
+
// Server may not support setLevel despite declaring logging capability
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private createTransport(serverName: string, config: ServerConfig): Transport {
|
|
236
|
+
if (isStdioServer(config)) {
|
|
237
|
+
return createStdioTransport(config);
|
|
238
|
+
}
|
|
239
|
+
if (isHttpServer(config)) {
|
|
240
|
+
// Only pass the OAuth provider if the server already has tokens.
|
|
241
|
+
// Without tokens, passing the provider causes the SDK transport to
|
|
242
|
+
// auto-trigger the browser OAuth flow on 401, which fails because
|
|
243
|
+
// there's no callback server running. Users must run `mcpx auth <server>` first.
|
|
244
|
+
const provider = this.getOrCreateOAuthProvider(serverName);
|
|
245
|
+
const authProvider = provider.isComplete() ? provider : undefined;
|
|
246
|
+
|
|
247
|
+
if (config.transport === "sse") {
|
|
248
|
+
return createSseTransport(config, authProvider, this.verbose, this.showSecrets);
|
|
249
|
+
}
|
|
250
|
+
// Default (including explicit "streamable-http") uses Streamable HTTP.
|
|
251
|
+
// When no transport is set, getClient() will auto-fallback to SSE on failure.
|
|
252
|
+
return createHttpTransport(config, authProvider, this.verbose, this.showSecrets);
|
|
253
|
+
}
|
|
254
|
+
throw new Error("Invalid server config");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Process all servers in concurrent batches, collecting results and errors. */
|
|
258
|
+
private async gatherFromServers<T>(
|
|
259
|
+
fetchFn: (serverName: string) => Promise<T[]>,
|
|
260
|
+
): Promise<{ items: T[]; errors: ServerError[] }> {
|
|
261
|
+
const serverNames = Object.keys(this.servers.mcpServers);
|
|
262
|
+
const items: T[] = [];
|
|
263
|
+
const errors: ServerError[] = [];
|
|
264
|
+
|
|
265
|
+
for (let i = 0; i < serverNames.length; i += this.concurrency) {
|
|
266
|
+
const batch = serverNames.slice(i, i + this.concurrency);
|
|
267
|
+
const batchResults = await Promise.allSettled(batch.map((name) => fetchFn(name)));
|
|
268
|
+
|
|
269
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
270
|
+
const result = batchResults[j]!;
|
|
271
|
+
if (result.status === "fulfilled") {
|
|
272
|
+
items.push(...result.value);
|
|
273
|
+
} else {
|
|
274
|
+
const name = batch[j]!;
|
|
275
|
+
const message =
|
|
276
|
+
result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
277
|
+
errors.push({ server: name, message });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { items, errors };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Race a promise against a timeout */
|
|
286
|
+
private withTimeout<T>(promise: Promise<T>, label: string): Promise<T> {
|
|
287
|
+
if (this.timeout <= 0) return promise;
|
|
288
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
289
|
+
return Promise.race([
|
|
290
|
+
promise.finally(() => clearTimeout(timer)),
|
|
291
|
+
new Promise<never>((_, reject) => {
|
|
292
|
+
timer = setTimeout(
|
|
293
|
+
() => reject(new Error(`${label}: timed out after ${this.timeout / 1000}s`)),
|
|
294
|
+
this.timeout,
|
|
295
|
+
);
|
|
296
|
+
timer.unref();
|
|
297
|
+
}),
|
|
298
|
+
]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Retry a function up to maxRetries times, clearing cached client between attempts */
|
|
302
|
+
private async withRetry<T>(fn: () => Promise<T>, label: string, serverName?: string): Promise<T> {
|
|
303
|
+
let lastError: Error | undefined;
|
|
304
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
305
|
+
try {
|
|
306
|
+
return await fn();
|
|
307
|
+
} catch (err) {
|
|
308
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
309
|
+
if (attempt < this.maxRetries && serverName) {
|
|
310
|
+
// Clear cached client so next attempt reconnects fresh
|
|
311
|
+
try {
|
|
312
|
+
await this.clients.get(serverName)?.close();
|
|
313
|
+
} catch {
|
|
314
|
+
// ignore close errors
|
|
315
|
+
}
|
|
316
|
+
this.clients.delete(serverName);
|
|
317
|
+
this.connecting.delete(serverName);
|
|
318
|
+
this.transports.delete(serverName);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
throw lastError;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** List tools for a single server, applying allowedTools/disabledTools filters */
|
|
326
|
+
async listTools(serverName: string): Promise<Tool[]> {
|
|
327
|
+
return this.withRetry(
|
|
328
|
+
async () => {
|
|
329
|
+
const client = await this.getClient(serverName);
|
|
330
|
+
const result = await this.withTimeout(client.listTools(), `listTools(${serverName})`);
|
|
331
|
+
const config = this.servers.mcpServers[serverName]!;
|
|
332
|
+
return filterTools(result.tools, config.allowedTools, config.disabledTools);
|
|
333
|
+
},
|
|
334
|
+
`listTools(${serverName})`,
|
|
335
|
+
serverName,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** List tools across all configured servers */
|
|
340
|
+
async getAllTools(): Promise<{ tools: ToolWithServer[]; errors: ServerError[] }> {
|
|
341
|
+
const { items: tools, errors } = await this.gatherFromServers(async (name) => {
|
|
342
|
+
const serverTools = await this.listTools(name);
|
|
343
|
+
return serverTools.map((tool) => ({ server: name, tool }));
|
|
344
|
+
});
|
|
345
|
+
return { tools, errors };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Call a tool on a specific server */
|
|
349
|
+
async callTool(
|
|
350
|
+
serverName: string,
|
|
351
|
+
toolName: string,
|
|
352
|
+
args: Record<string, unknown> = {},
|
|
353
|
+
): Promise<unknown> {
|
|
354
|
+
return this.withRetry(
|
|
355
|
+
async () => {
|
|
356
|
+
const client = await this.getClient(serverName);
|
|
357
|
+
return this.withTimeout(
|
|
358
|
+
client.callTool({ name: toolName, arguments: args }),
|
|
359
|
+
`callTool(${serverName}/${toolName})`,
|
|
360
|
+
);
|
|
361
|
+
},
|
|
362
|
+
`callTool(${serverName}/${toolName})`,
|
|
363
|
+
serverName,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Get the schema for a specific tool */
|
|
368
|
+
async getToolSchema(serverName: string, toolName: string): Promise<Tool | undefined> {
|
|
369
|
+
const tools = await this.listTools(serverName);
|
|
370
|
+
return tools.find((t) => t.name === toolName);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Get server info (version, capabilities, instructions) */
|
|
374
|
+
async getServerInfo(serverName: string): Promise<ServerInfo> {
|
|
375
|
+
const client = await this.getClient(serverName);
|
|
376
|
+
return {
|
|
377
|
+
version: client.getServerVersion() as ServerInfo["version"],
|
|
378
|
+
capabilities: client.getServerCapabilities(),
|
|
379
|
+
instructions: client.getInstructions(),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** List resources for a single server */
|
|
384
|
+
async listResources(serverName: string): Promise<Resource[]> {
|
|
385
|
+
return this.withRetry(
|
|
386
|
+
async () => {
|
|
387
|
+
const client = await this.getClient(serverName);
|
|
388
|
+
const result = await this.withTimeout(
|
|
389
|
+
client.listResources(),
|
|
390
|
+
`listResources(${serverName})`,
|
|
391
|
+
);
|
|
392
|
+
return result.resources;
|
|
393
|
+
},
|
|
394
|
+
`listResources(${serverName})`,
|
|
395
|
+
serverName,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** List resources across all configured servers (skips servers without resources capability) */
|
|
400
|
+
async getAllResources(): Promise<{ resources: ResourceWithServer[]; errors: ServerError[] }> {
|
|
401
|
+
const { items: resources, errors } = await this.gatherFromServers(async (name) => {
|
|
402
|
+
const client = await this.getClient(name);
|
|
403
|
+
if (!client.getServerCapabilities()?.resources) return [];
|
|
404
|
+
const serverResources = await this.listResources(name);
|
|
405
|
+
return serverResources.map((resource) => ({ server: name, resource }));
|
|
406
|
+
});
|
|
407
|
+
return { resources, errors };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Read a specific resource by URI */
|
|
411
|
+
async readResource(serverName: string, uri: string): Promise<unknown> {
|
|
412
|
+
return this.withRetry(
|
|
413
|
+
async () => {
|
|
414
|
+
const client = await this.getClient(serverName);
|
|
415
|
+
return this.withTimeout(client.readResource({ uri }), `readResource(${serverName}/${uri})`);
|
|
416
|
+
},
|
|
417
|
+
`readResource(${serverName}/${uri})`,
|
|
418
|
+
serverName,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** List prompts for a single server */
|
|
423
|
+
async listPrompts(serverName: string): Promise<Prompt[]> {
|
|
424
|
+
return this.withRetry(
|
|
425
|
+
async () => {
|
|
426
|
+
const client = await this.getClient(serverName);
|
|
427
|
+
const result = await this.withTimeout(client.listPrompts(), `listPrompts(${serverName})`);
|
|
428
|
+
return result.prompts;
|
|
429
|
+
},
|
|
430
|
+
`listPrompts(${serverName})`,
|
|
431
|
+
serverName,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** List prompts across all configured servers (skips servers without prompts capability) */
|
|
436
|
+
async getAllPrompts(): Promise<{ prompts: PromptWithServer[]; errors: ServerError[] }> {
|
|
437
|
+
const { items: prompts, errors } = await this.gatherFromServers(async (name) => {
|
|
438
|
+
const client = await this.getClient(name);
|
|
439
|
+
if (!client.getServerCapabilities()?.prompts) return [];
|
|
440
|
+
const serverPrompts = await this.listPrompts(name);
|
|
441
|
+
return serverPrompts.map((prompt) => ({ server: name, prompt }));
|
|
442
|
+
});
|
|
443
|
+
return { prompts, errors };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Get a specific prompt by name, optionally with arguments */
|
|
447
|
+
async getPrompt(
|
|
448
|
+
serverName: string,
|
|
449
|
+
name: string,
|
|
450
|
+
args?: Record<string, string>,
|
|
451
|
+
): Promise<unknown> {
|
|
452
|
+
return this.withRetry(
|
|
453
|
+
async () => {
|
|
454
|
+
const client = await this.getClient(serverName);
|
|
455
|
+
return this.withTimeout(
|
|
456
|
+
client.getPrompt({ name, arguments: args }),
|
|
457
|
+
`getPrompt(${serverName}/${name})`,
|
|
458
|
+
);
|
|
459
|
+
},
|
|
460
|
+
`getPrompt(${serverName}/${name})`,
|
|
461
|
+
serverName,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Check if a server supports task-augmented tool calls */
|
|
466
|
+
async serverSupportsTask(serverName: string): Promise<boolean> {
|
|
467
|
+
const client = await this.getClient(serverName);
|
|
468
|
+
const caps = client.getServerCapabilities() as Record<string, unknown> | undefined;
|
|
469
|
+
const tasks = caps?.tasks as Record<string, unknown> | undefined;
|
|
470
|
+
const requests = tasks?.requests as Record<string, unknown> | undefined;
|
|
471
|
+
const tools = requests?.tools as Record<string, unknown> | undefined;
|
|
472
|
+
return !!tools?.call;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Call a tool with task-augmented streaming, yielding status updates */
|
|
476
|
+
async *callToolStream(
|
|
477
|
+
serverName: string,
|
|
478
|
+
toolName: string,
|
|
479
|
+
args: Record<string, unknown> = {},
|
|
480
|
+
taskOptions?: { ttl?: number; signal?: AbortSignal },
|
|
481
|
+
): AsyncGenerator<ResponseMessage<CallToolResult>> {
|
|
482
|
+
const client = await this.getClient(serverName);
|
|
483
|
+
const stream = client.experimental.tasks.callToolStream(
|
|
484
|
+
{ name: toolName, arguments: args },
|
|
485
|
+
CallToolResultSchema,
|
|
486
|
+
{
|
|
487
|
+
task: { ttl: taskOptions?.ttl },
|
|
488
|
+
signal: taskOptions?.signal,
|
|
489
|
+
},
|
|
490
|
+
);
|
|
491
|
+
yield* stream;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Get the status of a task */
|
|
495
|
+
async getTask(serverName: string, taskId: string): Promise<GetTaskResult> {
|
|
496
|
+
const client = await this.getClient(serverName);
|
|
497
|
+
return this.withTimeout(
|
|
498
|
+
client.experimental.tasks.getTask(taskId),
|
|
499
|
+
`getTask(${serverName}/${taskId})`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/** Retrieve the result of a completed task */
|
|
504
|
+
async getTaskResult(serverName: string, taskId: string): Promise<CallToolResult> {
|
|
505
|
+
const client = await this.getClient(serverName);
|
|
506
|
+
return this.withTimeout(
|
|
507
|
+
client.experimental.tasks.getTaskResult(taskId, CallToolResultSchema),
|
|
508
|
+
`getTaskResult(${serverName}/${taskId})`,
|
|
509
|
+
) as Promise<CallToolResult>;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** List tasks on a server */
|
|
513
|
+
async listTasks(serverName: string, cursor?: string): Promise<ListTasksResult> {
|
|
514
|
+
const client = await this.getClient(serverName);
|
|
515
|
+
return this.withTimeout(
|
|
516
|
+
client.experimental.tasks.listTasks(cursor),
|
|
517
|
+
`listTasks(${serverName})`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Cancel a running task */
|
|
522
|
+
async cancelTask(serverName: string, taskId: string): Promise<CancelTaskResult> {
|
|
523
|
+
const client = await this.getClient(serverName);
|
|
524
|
+
return this.withTimeout(
|
|
525
|
+
client.experimental.tasks.cancelTask(taskId),
|
|
526
|
+
`cancelTask(${serverName}/${taskId})`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** Get all server names */
|
|
531
|
+
getServerNames(): string[] {
|
|
532
|
+
return Object.keys(this.servers.mcpServers);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Disconnect all clients */
|
|
536
|
+
async close(): Promise<void> {
|
|
537
|
+
const closePromises = [...this.clients.entries()].map(async ([name, client]) => {
|
|
538
|
+
try {
|
|
539
|
+
await client.close();
|
|
540
|
+
} catch {
|
|
541
|
+
// Ignore close errors
|
|
542
|
+
}
|
|
543
|
+
this.clients.delete(name);
|
|
544
|
+
this.transports.delete(name);
|
|
545
|
+
});
|
|
546
|
+
await Promise.allSettled(closePromises);
|
|
547
|
+
this.connecting.clear();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Apply allowedTools/disabledTools glob filters to a tool list */
|
|
552
|
+
function filterTools(tools: Tool[], allowedTools?: string[], disabledTools?: string[]): Tool[] {
|
|
553
|
+
let filtered = tools;
|
|
554
|
+
|
|
555
|
+
if (allowedTools && allowedTools.length > 0) {
|
|
556
|
+
const isAllowed = picomatch(allowedTools);
|
|
557
|
+
filtered = filtered.filter((t) => isAllowed(t.name));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (disabledTools && disabledTools.length > 0) {
|
|
561
|
+
const isDisabled = picomatch(disabledTools);
|
|
562
|
+
filtered = filtered.filter((t) => !isDisabled(t.name));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return filtered;
|
|
566
|
+
}
|