decorated-pi 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -59
- package/extensions/index.ts +130 -8
- package/extensions/io.ts +5 -3
- package/extensions/lsp/servers.ts +63 -3
- package/extensions/mcp/builtin.ts +260 -44
- package/extensions/mcp/client.ts +28 -19
- package/extensions/mcp/index.ts +336 -80
- package/extensions/model-integration.ts +6 -3
- package/extensions/rtk.ts +219 -0
- package/extensions/settings.ts +31 -1
- package/extensions/slash.ts +198 -66
- package/extensions/smart-at.ts +27 -1
- package/extensions/wakatime.ts +403 -0
- package/package.json +4 -5
- package/extensions/guidance.ts +0 -23
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
6
8
|
import { loadConfig } from "../settings.js";
|
|
9
|
+
import type { DependencyStatus } from "../rtk";
|
|
7
10
|
|
|
8
11
|
export interface McpServerConfig {
|
|
9
12
|
name: string;
|
|
10
|
-
url
|
|
13
|
+
url?: string;
|
|
14
|
+
command?: string;
|
|
15
|
+
args?: string[];
|
|
16
|
+
env?: Record<string, string>;
|
|
11
17
|
description?: string;
|
|
12
18
|
enabled: boolean;
|
|
13
19
|
source: "builtin" | "global" | "project";
|
|
@@ -18,70 +24,52 @@ export const BUILTIN_MCP_SERVERS: Omit<McpServerConfig, "source">[] = [
|
|
|
18
24
|
{
|
|
19
25
|
name: "context7",
|
|
20
26
|
url: "https://mcp.context7.com/mcp",
|
|
21
|
-
description: "Context7 documentation and code examples",
|
|
22
27
|
enabled: true,
|
|
23
28
|
},
|
|
24
29
|
{
|
|
25
30
|
name: "exa",
|
|
26
31
|
url: "https://mcp.exa.ai/mcp",
|
|
27
|
-
description: "Exa web search",
|
|
28
32
|
enabled: true,
|
|
29
33
|
},
|
|
30
34
|
];
|
|
31
35
|
|
|
32
36
|
// ── Project-level config discovery ─────────────────────────────────────────
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
".pi/mcp.json",
|
|
36
|
-
".pi/.mcp.json",
|
|
37
|
-
".agents/mcp.json",
|
|
38
|
-
".agents/.mcp.json",
|
|
39
|
-
".claude/mcp.json",
|
|
40
|
-
".claude/.mcp.json",
|
|
41
|
-
"mcp.json",
|
|
42
|
-
".mcp.json",
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
function readMcpJson(filePath: string): Record<string, { url: string; enabled?: boolean }> | null {
|
|
38
|
+
function readMcpJson(filePath: string): Record<string, { url?: string; command?: string; args?: string[]; env?: Record<string, string>; enabled?: boolean; description?: string }> | null {
|
|
46
39
|
try {
|
|
47
40
|
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
48
41
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
49
42
|
const servers = raw.mcpServers ?? raw["mcp-servers"];
|
|
50
43
|
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return null;
|
|
51
|
-
return servers as Record<string, { url
|
|
44
|
+
return servers as Record<string, { url?: string; command?: string; args?: string[]; env?: Record<string, string>; enabled?: boolean }>;
|
|
52
45
|
} catch {
|
|
53
46
|
return null;
|
|
54
47
|
}
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
/** Load project-level MCP configs from cwd
|
|
50
|
+
/** Load project-level MCP configs from cwd only. */
|
|
58
51
|
export function loadProjectMcpConfigs(cwd: string): McpServerConfig[] {
|
|
59
52
|
const configs: McpServerConfig[] = [];
|
|
60
53
|
const seen = new Set<string>();
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!fs.existsSync(filePath)) continue;
|
|
67
|
-
const servers = readMcpJson(filePath);
|
|
68
|
-
if (!servers) continue;
|
|
69
|
-
|
|
70
|
-
for (const [name, entry] of Object.entries(servers)) {
|
|
71
|
-
if (seen.has(name)) continue;
|
|
72
|
-
seen.add(name);
|
|
73
|
-
configs.push({
|
|
74
|
-
name,
|
|
75
|
-
url: entry.url,
|
|
76
|
-
enabled: entry.enabled !== false,
|
|
77
|
-
source: "project",
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
55
|
+
const filePath = path.join(cwd, ".pi/agent/mcp.json");
|
|
56
|
+
if (!fs.existsSync(filePath)) return [];
|
|
57
|
+
const servers = readMcpJson(filePath);
|
|
58
|
+
if (!servers) return [];
|
|
81
59
|
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
60
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
61
|
+
if (seen.has(name)) continue;
|
|
62
|
+
seen.add(name);
|
|
63
|
+
configs.push({
|
|
64
|
+
name,
|
|
65
|
+
url: entry.url,
|
|
66
|
+
command: entry.command,
|
|
67
|
+
args: entry.args,
|
|
68
|
+
env: entry.env,
|
|
69
|
+
enabled: entry.enabled !== false,
|
|
70
|
+
description: entry.description,
|
|
71
|
+
source: "project",
|
|
72
|
+
});
|
|
85
73
|
}
|
|
86
74
|
|
|
87
75
|
return configs;
|
|
@@ -95,11 +83,20 @@ export function loadGlobalMcpConfigs(): McpServerConfig[] {
|
|
|
95
83
|
return Object.entries(config.mcpServers).map(([name, entry]) => ({
|
|
96
84
|
name,
|
|
97
85
|
url: entry.url,
|
|
86
|
+
command: entry.command,
|
|
87
|
+
args: entry.args,
|
|
88
|
+
env: entry.env,
|
|
89
|
+
description: (entry as any).description as string | undefined,
|
|
98
90
|
enabled: entry.enabled !== false,
|
|
99
91
|
source: "global" as const,
|
|
100
92
|
}));
|
|
101
93
|
}
|
|
102
94
|
|
|
95
|
+
/** Returns true if the URL should use SSE transport (path ends with /sse). */
|
|
96
|
+
export function isSseUrl(url: string): boolean {
|
|
97
|
+
return url.endsWith("/sse") || url.endsWith("/sse/");
|
|
98
|
+
}
|
|
99
|
+
|
|
103
100
|
/**
|
|
104
101
|
* Merge all MCP configs: builtin → global → project.
|
|
105
102
|
* Later sources override earlier ones for the same server name.
|
|
@@ -112,15 +109,234 @@ export function resolveMcpConfigs(cwd: string): McpServerConfig[] {
|
|
|
112
109
|
byName.set(s.name, { ...s, source: "builtin" });
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
// Global
|
|
112
|
+
// Global — preserve url/command/description from builtin if not overridden
|
|
116
113
|
for (const s of loadGlobalMcpConfigs()) {
|
|
117
|
-
byName.
|
|
114
|
+
const existing = byName.get(s.name);
|
|
115
|
+
if (existing) {
|
|
116
|
+
byName.set(s.name, {
|
|
117
|
+
...existing,
|
|
118
|
+
...s,
|
|
119
|
+
url: s.url ?? existing.url,
|
|
120
|
+
command: s.command ?? existing.command,
|
|
121
|
+
args: s.args ?? existing.args,
|
|
122
|
+
env: s.env ?? existing.env,
|
|
123
|
+
description: s.description ?? existing.description,
|
|
124
|
+
source: "global",
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
byName.set(s.name, s);
|
|
128
|
+
}
|
|
118
129
|
}
|
|
119
130
|
|
|
120
|
-
// Project (highest priority)
|
|
131
|
+
// Project (highest priority) — same preservation logic
|
|
121
132
|
for (const s of loadProjectMcpConfigs(cwd)) {
|
|
122
|
-
byName.
|
|
133
|
+
const existing = byName.get(s.name);
|
|
134
|
+
if (existing) {
|
|
135
|
+
byName.set(s.name, {
|
|
136
|
+
...existing,
|
|
137
|
+
...s,
|
|
138
|
+
url: s.url ?? existing.url,
|
|
139
|
+
command: s.command ?? existing.command,
|
|
140
|
+
args: s.args ?? existing.args,
|
|
141
|
+
env: s.env ?? existing.env,
|
|
142
|
+
description: s.description ?? existing.description,
|
|
143
|
+
source: "project",
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
byName.set(s.name, s);
|
|
147
|
+
}
|
|
123
148
|
}
|
|
124
149
|
|
|
125
|
-
return [...byName.values()].filter((s) => s.
|
|
150
|
+
return [...byName.values()].filter((s) => s.url || s.command);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function collectMcpDependencyStatuses(cwd: string): DependencyStatus[] {
|
|
154
|
+
const seen = new Set<string>();
|
|
155
|
+
const statuses: DependencyStatus[] = [];
|
|
156
|
+
for (const cfg of resolveMcpConfigs(cwd)) {
|
|
157
|
+
if (!cfg.enabled || !cfg.command || seen.has(cfg.command)) continue;
|
|
158
|
+
seen.add(cfg.command);
|
|
159
|
+
statuses.push({
|
|
160
|
+
module: `mcp:${cfg.name}`,
|
|
161
|
+
label: cfg.command,
|
|
162
|
+
state: commandExists(cfg.command) ? "ok" : "missing",
|
|
163
|
+
detail: `Install the MCP server command for \"${cfg.name}\" or update its config.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return statuses;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function commandExists(command: string): boolean {
|
|
170
|
+
if (path.isAbsolute(command) || command.includes("/") || command.includes("\\")) {
|
|
171
|
+
return fs.existsSync(command);
|
|
172
|
+
}
|
|
173
|
+
const result = process.platform === "win32"
|
|
174
|
+
? spawnSync("where", [command], { encoding: "utf-8" })
|
|
175
|
+
: spawnSync(process.env.SHELL || "sh", ["-lc", `command -v '${command.replace(/'/g, `'"'"'`)}'`], { encoding: "utf-8" });
|
|
176
|
+
return result.status === 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Write auto-generated description back to project mcp.json. */
|
|
180
|
+
export function saveProjectMcpDescription(cwd: string, name: string, description: string): void {
|
|
181
|
+
const filePath = path.join(cwd, ".pi/agent/mcp.json");
|
|
182
|
+
const servers = readMcpJson(filePath);
|
|
183
|
+
if (!servers || !servers[name]) return;
|
|
184
|
+
servers[name] = { ...servers[name], description };
|
|
185
|
+
const dir = path.dirname(filePath);
|
|
186
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
187
|
+
fs.writeFileSync(filePath, JSON.stringify({ mcpServers: servers }, null, 2) + "\n", "utf-8");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Metadata cache (tool descriptions + schemas) ──────────────────────────
|
|
191
|
+
|
|
192
|
+
export interface McpToolCache {
|
|
193
|
+
name: string;
|
|
194
|
+
description?: string;
|
|
195
|
+
inputSchema: Record<string, unknown>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface McpServerCache {
|
|
199
|
+
description?: string;
|
|
200
|
+
tools: McpToolCache[];
|
|
201
|
+
cachedAt: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface McpCache {
|
|
205
|
+
servers: Record<string, McpServerCache>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function globalCachePath(): string {
|
|
209
|
+
return path.join(os.homedir(), ".pi/agent/mcp-cache.json");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function projectCachePath(cwd: string): string {
|
|
213
|
+
return path.join(cwd, ".pi/agent/mcp-cache.json");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readCacheFile(p: string): McpCache | null {
|
|
217
|
+
try {
|
|
218
|
+
if (!fs.existsSync(p)) return null;
|
|
219
|
+
return JSON.parse(fs.readFileSync(p, "utf-8")) as McpCache;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function writeCacheFile(p: string, cache: McpCache): void {
|
|
226
|
+
const dir = path.dirname(p);
|
|
227
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
228
|
+
const tmp = `${p}.tmp`;
|
|
229
|
+
fs.writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
|
|
230
|
+
fs.renameSync(tmp, p);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Load merged cache: global + project. */
|
|
234
|
+
export function loadMcpCache(cwd?: string): McpCache | null {
|
|
235
|
+
const merged: McpCache = { servers: {} };
|
|
236
|
+
|
|
237
|
+
const globalCache = readCacheFile(globalCachePath());
|
|
238
|
+
if (globalCache) {
|
|
239
|
+
merged.servers = { ...merged.servers, ...globalCache.servers };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (cwd) {
|
|
243
|
+
const projectCache = readCacheFile(projectCachePath(cwd));
|
|
244
|
+
if (projectCache) {
|
|
245
|
+
merged.servers = { ...merged.servers, ...projectCache.servers };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return merged;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Save cache to global or project scope. */
|
|
253
|
+
export function saveMcpCache(cache: McpCache, scope: "global" | "project", cwd?: string): void {
|
|
254
|
+
const p = scope === "project" && cwd ? projectCachePath(cwd) : globalCachePath();
|
|
255
|
+
writeCacheFile(p, cache);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Update a single server's entry in the appropriate cache. */
|
|
259
|
+
export function updateServerCache(
|
|
260
|
+
serverName: string,
|
|
261
|
+
entry: McpServerCache,
|
|
262
|
+
scope: "global" | "project",
|
|
263
|
+
cwd?: string,
|
|
264
|
+
): void {
|
|
265
|
+
const p = scope === "project" && cwd ? projectCachePath(cwd) : globalCachePath();
|
|
266
|
+
const existing = readCacheFile(p) || { servers: {} };
|
|
267
|
+
existing.servers[serverName] = entry;
|
|
268
|
+
writeCacheFile(p, existing);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Enable / Disable helpers ────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
function readMcpJsonSafe(filePath: string): Record<string, any> | null {
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Toggle a server's enabled state in the appropriate config file. */
|
|
282
|
+
export function toggleMcpServerEnabled(
|
|
283
|
+
serverName: string,
|
|
284
|
+
enabled: boolean,
|
|
285
|
+
scope: "global" | "project",
|
|
286
|
+
cwd?: string,
|
|
287
|
+
): boolean {
|
|
288
|
+
try {
|
|
289
|
+
if (scope === "project" && cwd) {
|
|
290
|
+
const filePath = path.join(cwd, ".pi/agent/mcp.json");
|
|
291
|
+
const raw = readMcpJsonSafe(filePath) || { mcpServers: {} };
|
|
292
|
+
const servers = raw.mcpServers ?? {};
|
|
293
|
+
servers[serverName] = { ...(servers[serverName] || {}), enabled };
|
|
294
|
+
raw.mcpServers = servers;
|
|
295
|
+
const dir = path.dirname(filePath);
|
|
296
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
297
|
+
fs.writeFileSync(filePath, JSON.stringify(raw, null, 2) + "\n", "utf-8");
|
|
298
|
+
} else {
|
|
299
|
+
const { loadConfig } = require("../settings.js");
|
|
300
|
+
const config = loadConfig();
|
|
301
|
+
config.mcpServers = config.mcpServers || {};
|
|
302
|
+
config.mcpServers[serverName] = { ...(config.mcpServers[serverName] || {}), enabled };
|
|
303
|
+
const configPath = path.join(os.homedir(), ".pi/agent/decorated-pi.json");
|
|
304
|
+
const dir = path.dirname(configPath);
|
|
305
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
306
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function cleanupOneCache(p: string, names: Set<string>): void {
|
|
315
|
+
const cache = readCacheFile(p);
|
|
316
|
+
if (!cache) return;
|
|
317
|
+
let changed = false;
|
|
318
|
+
for (const name of Object.keys(cache.servers)) {
|
|
319
|
+
if (!names.has(name)) {
|
|
320
|
+
delete cache.servers[name];
|
|
321
|
+
changed = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (changed) writeCacheFile(p, cache);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function cleanupStaleCache(configs: McpServerConfig[], cwd?: string): void {
|
|
328
|
+
const names = new Set(configs.map(c => c.name));
|
|
329
|
+
cleanupOneCache(globalCachePath(), names);
|
|
330
|
+
if (cwd) {
|
|
331
|
+
const projectCache = projectCachePath(cwd);
|
|
332
|
+
const projectMcpJson = path.join(cwd, ".pi/agent/mcp.json");
|
|
333
|
+
// If project mcp.json doesn't exist, remove project cache entirely
|
|
334
|
+
if (!fs.existsSync(projectMcpJson)) {
|
|
335
|
+
if (fs.existsSync(projectCache)) {
|
|
336
|
+
fs.unlinkSync(projectCache);
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
cleanupOneCache(projectCache, names);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
126
342
|
}
|
package/extensions/mcp/client.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
3
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
import type { McpServerConfig } from "./builtin.js";
|
|
6
|
+
import { isSseUrl } from "./builtin.js";
|
|
4
7
|
|
|
5
8
|
export interface McpToolSpec {
|
|
6
9
|
name: string;
|
|
@@ -8,10 +11,10 @@ export interface McpToolSpec {
|
|
|
8
11
|
inputSchema: Record<string, unknown>;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
/** Per-server MCP client wrapper
|
|
14
|
+
/** Per-server MCP client wrapper. Supports stdio, http, and sse transports. */
|
|
12
15
|
export class McpConnection {
|
|
13
16
|
client: Client;
|
|
14
|
-
transport: StreamableHTTPClientTransport | SSEClientTransport | undefined;
|
|
17
|
+
transport: StreamableHTTPClientTransport | SSEClientTransport | StdioClientTransport | undefined;
|
|
15
18
|
tools: McpToolSpec[] = [];
|
|
16
19
|
private connected = false;
|
|
17
20
|
|
|
@@ -19,7 +22,7 @@ export class McpConnection {
|
|
|
19
22
|
|
|
20
23
|
constructor(
|
|
21
24
|
public readonly serverName: string,
|
|
22
|
-
public readonly
|
|
25
|
+
public readonly config: McpServerConfig,
|
|
23
26
|
) {
|
|
24
27
|
this.client = new Client({
|
|
25
28
|
name: `decorated-pi-${serverName}`,
|
|
@@ -28,26 +31,32 @@ export class McpConnection {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
async connect(timeoutMs = 8000): Promise<void> {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const transport = new
|
|
34
|
+
const connectAndListTools = async (): Promise<void> => {
|
|
35
|
+
if (this.config.command) {
|
|
36
|
+
// Stdio transport — spawn a local process
|
|
37
|
+
const transport = new StdioClientTransport({
|
|
38
|
+
command: this.config.command,
|
|
39
|
+
args: this.config.args,
|
|
40
|
+
env: this.config.env,
|
|
41
|
+
stderr: "ignore",
|
|
42
|
+
});
|
|
35
43
|
await this.client.connect(transport);
|
|
36
44
|
this.transport = transport;
|
|
37
45
|
this.connected = true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const transport = new SSEClientTransport(new URL(this.url));
|
|
46
|
+
} else if (this.config.url) {
|
|
47
|
+
// HTTP or SSE transport — determined by URL path
|
|
48
|
+
if (isSseUrl(this.config.url)) {
|
|
49
|
+
const transport = new SSEClientTransport(new URL(this.config.url));
|
|
50
|
+
await this.client.connect(transport);
|
|
51
|
+
this.transport = transport;
|
|
52
|
+
} else {
|
|
53
|
+
const transport = new StreamableHTTPClientTransport(new URL(this.config.url));
|
|
42
54
|
await this.client.connect(transport);
|
|
43
55
|
this.transport = transport;
|
|
44
|
-
this.connected = true;
|
|
45
|
-
} catch (sseErr) {
|
|
46
|
-
const sseMessage = sseErr instanceof Error ? sseErr.message : String(sseErr);
|
|
47
|
-
throw new Error(
|
|
48
|
-
`MCP ${this.serverName}: StreamableHTTP failed (${lastErr.message}); SSE fallback also failed (${sseMessage})`,
|
|
49
|
-
);
|
|
50
56
|
}
|
|
57
|
+
this.connected = true;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error(`MCP ${this.serverName}: no url or command configured`);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
const result = (await this.client.listTools()) as unknown as {
|
|
@@ -69,7 +78,7 @@ export class McpConnection {
|
|
|
69
78
|
setTimeout(() => reject(new Error(`MCP ${this.serverName}: connection timed out after ${timeoutMs}ms`)), timeoutMs),
|
|
70
79
|
);
|
|
71
80
|
|
|
72
|
-
await Promise.race([
|
|
81
|
+
await Promise.race([connectAndListTools(), timeout]);
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
async callTool(name: string, args: Record<string, unknown>): Promise<string> {
|
|
@@ -103,4 +112,4 @@ export class McpConnection {
|
|
|
103
112
|
await this.client.close();
|
|
104
113
|
} catch {}
|
|
105
114
|
}
|
|
106
|
-
}
|
|
115
|
+
}
|