min-agent 0.3.0 → 0.4.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/README.md +111 -28
- package/dist/agent.js +1119 -256
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/exec.js +32 -0
- package/dist/cli/commands/history.js +58 -0
- package/dist/cli/commands/index.js +224 -0
- package/dist/cli/commands/init.js +18 -0
- package/dist/cli/commands/mcp.js +173 -0
- package/dist/cli/commands/memory.js +69 -0
- package/dist/cli/commands/models.js +21 -0
- package/dist/cli/commands/permission.js +12 -0
- package/dist/cli/commands/rules.js +33 -0
- package/dist/cli/commands/sandbox.js +13 -0
- package/dist/cli/commands/serve.js +9 -0
- package/dist/cli/commands/setup.js +4 -0
- package/dist/cli/commands/shared.js +16 -0
- package/dist/cli/commands/skills.js +119 -0
- package/dist/cli/commands/update.js +7 -0
- package/dist/cli/commands/write-config.js +30 -0
- package/dist/cli/errors.js +36 -0
- package/dist/cli/exec-prompt.js +26 -0
- package/dist/cli/option-helpers.js +53 -0
- package/dist/cli/program.js +180 -0
- package/dist/cli.js +5 -888
- package/dist/code-mode.js +32 -14
- package/dist/compaction.js +347 -160
- package/dist/config.js +119 -10
- package/dist/confirm.js +56 -9
- package/dist/context-window.js +107 -39
- package/dist/doom-loop.js +264 -29
- package/dist/fetch-timeout.js +152 -0
- package/dist/http-approvals.js +60 -0
- package/dist/instructions.js +21 -0
- package/dist/logger.js +33 -4
- package/dist/markdown.js +37 -11
- package/dist/mcp.js +328 -30
- package/dist/memory.js +97 -56
- package/dist/output.js +7 -5
- package/dist/permission-cli.js +43 -0
- package/dist/plugins.js +46 -8
- package/dist/pricing.js +4 -4
- package/dist/provider.js +23 -6
- package/dist/question-format.js +60 -0
- package/dist/sandbox-cli.js +82 -0
- package/dist/sandbox.js +403 -0
- package/dist/save-throttle.js +45 -0
- package/dist/serve/common.js +404 -0
- package/dist/serve/routes-chat.js +347 -0
- package/dist/serve/routes-mcp.js +212 -0
- package/dist/serve/routes-memory.js +66 -0
- package/dist/serve/routes-meta.js +205 -0
- package/dist/serve/routes-sessions.js +61 -0
- package/dist/serve/routes-skills.js +70 -0
- package/dist/serve.js +33 -883
- package/dist/sessions.js +53 -9
- package/dist/skills.js +82 -18
- package/dist/title-gen.js +8 -2
- package/dist/token-display.js +36 -0
- package/dist/tool-display.js +5 -0
- package/dist/tool-output.js +1 -3
- package/dist/tools/apply_patch.js +85 -11
- package/dist/tools/atomic-file.js +35 -0
- package/dist/tools/backend.js +2 -2
- package/dist/tools/bash.js +57 -19
- package/dist/tools/code_search.js +7 -1
- package/dist/tools/edit.js +11 -10
- package/dist/tools/explore.js +74 -14
- package/dist/tools/glob.js +4 -0
- package/dist/tools/grep.js +17 -10
- package/dist/tools/index.js +6 -21
- package/dist/tools/question.js +28 -9
- package/dist/tools/read.js +6 -4
- package/dist/tools/search-searxng.js +223 -0
- package/dist/tools/search-serper.js +189 -0
- package/dist/tools/task.js +84 -30
- package/dist/tools/todo.js +120 -19
- package/dist/tools/web_fetch.js +11 -3
- package/dist/tools/web_search.js +66 -556
- package/dist/tools/write.js +23 -6
- package/dist/tui/App.js +63 -14
- package/dist/tui/ConfirmBar.js +45 -13
- package/dist/tui/InputBar.js +150 -35
- package/dist/tui/MessageList.js +266 -125
- package/dist/tui/ModelPicker.js +8 -3
- package/dist/tui/QuestionBar.js +51 -19
- package/dist/tui/SessionPicker.js +79 -0
- package/dist/tui/StatusBar.js +8 -14
- package/dist/tui/agent-runner.js +142 -22
- package/dist/tui/caret-pos.js +48 -5
- package/dist/tui/caret.js +1 -1
- package/dist/tui/click-count.js +13 -0
- package/dist/tui/drag-state.js +8 -3
- package/dist/tui/hydrate.js +129 -0
- package/dist/tui/index.js +42 -13
- package/dist/tui/input-history.js +92 -11
- package/dist/tui/layout.js +75 -4
- package/dist/tui/prompt-queue.js +24 -0
- package/dist/tui/selection.js +113 -21
- package/dist/tui/session-switch.js +28 -0
- package/dist/tui/slash-commands.js +22 -6
- package/dist/tui/slash-handler.js +233 -58
- package/dist/tui/text-width.js +38 -16
- package/dist/tui/token-info.js +7 -0
- package/dist/tui/tool-children.js +19 -0
- package/dist/tui/undo-stack.js +1 -1
- package/dist/tui/use-sgr-mouse.js +3 -1
- package/dist/tui-chat.js +276 -40
- package/dist/updater.js +88 -29
- package/dist/xml-search.js +194 -0
- package/docs/API.md +257 -25
- package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
- package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
- package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
- package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
- package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
- package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
- package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
- package/package.json +6 -1
- package/skills/self-config/SKILL.md +90 -0
- package/skills/self-config/reference.md +149 -0
package/dist/markdown.js
CHANGED
|
@@ -52,7 +52,15 @@ export class MarkdownRenderer {
|
|
|
52
52
|
break;
|
|
53
53
|
const line = this.buffer.slice(0, nlIdx);
|
|
54
54
|
this.buffer = this.buffer.slice(nlIdx + 1);
|
|
55
|
-
|
|
55
|
+
if (this.inCodeBlock || line.startsWith("```")) {
|
|
56
|
+
if (this.inTable) {
|
|
57
|
+
output += this.renderTable(c);
|
|
58
|
+
this.tableRows = [];
|
|
59
|
+
this.inTable = false;
|
|
60
|
+
}
|
|
61
|
+
output += this.formatLine(line, c) + "\n";
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
56
64
|
if (this.isTableRow(line)) {
|
|
57
65
|
if (this.isTableSeparator(line)) {
|
|
58
66
|
this.inTable = true;
|
|
@@ -127,29 +135,47 @@ export class MarkdownRenderer {
|
|
|
127
135
|
return /^\s*\|[\s:]*-+[\s:|-]*\|\s*$/.test(line);
|
|
128
136
|
}
|
|
129
137
|
parseTableRow(line) {
|
|
130
|
-
return line
|
|
138
|
+
return line
|
|
139
|
+
.trim()
|
|
140
|
+
.slice(1, -1)
|
|
141
|
+
.split("|")
|
|
142
|
+
.map((cell) => cell.trim());
|
|
131
143
|
}
|
|
132
144
|
/** Pad string to target display width */
|
|
145
|
+
clipToWidth(str, targetWidth) {
|
|
146
|
+
let width = 0;
|
|
147
|
+
let out = "";
|
|
148
|
+
for (const ch of str) {
|
|
149
|
+
const w = displayWidth(ch);
|
|
150
|
+
if (width + w > targetWidth)
|
|
151
|
+
break;
|
|
152
|
+
out += ch;
|
|
153
|
+
width += w;
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
133
157
|
padToWidth(str, targetWidth) {
|
|
134
|
-
const
|
|
135
|
-
const padding = targetWidth -
|
|
136
|
-
return padding > 0 ?
|
|
158
|
+
const clipped = this.clipToWidth(str, targetWidth);
|
|
159
|
+
const padding = targetWidth - displayWidth(clipped);
|
|
160
|
+
return padding > 0 ? clipped + " ".repeat(padding) : clipped;
|
|
137
161
|
}
|
|
138
162
|
renderTable(c) {
|
|
139
163
|
if (this.tableRows.length === 0)
|
|
140
164
|
return "";
|
|
141
|
-
|
|
142
|
-
|
|
165
|
+
const colCount = Math.max(0, ...this.tableRows.map((r) => r.length));
|
|
166
|
+
if (colCount === 0)
|
|
167
|
+
return "";
|
|
143
168
|
const widths = Array(colCount).fill(0);
|
|
169
|
+
const maxCol = 64;
|
|
144
170
|
for (const row of this.tableRows) {
|
|
145
171
|
for (let i = 0; i < row.length; i++) {
|
|
146
|
-
widths[i] = Math.max(widths[i], displayWidth(row[i] ?? ""));
|
|
172
|
+
widths[i] = Math.min(maxCol, Math.max(widths[i], displayWidth(row[i] ?? "")));
|
|
147
173
|
}
|
|
148
174
|
}
|
|
149
175
|
const lines = [];
|
|
150
|
-
const top = `${c.dim}┌${widths.map((w) => "─".repeat(w + 2)).join("┬")}┐${c.reset}`;
|
|
151
|
-
const mid = `${c.dim}├${widths.map((w) => "─".repeat(w + 2)).join("┼")}┤${c.reset}`;
|
|
152
|
-
const bot = `${c.dim}└${widths.map((w) => "─".repeat(w + 2)).join("┴")}┘${c.reset}`;
|
|
176
|
+
const top = `${c.dim}┌${widths.map((w) => "─".repeat(Math.max(0, w + 2))).join("┬")}┐${c.reset}`;
|
|
177
|
+
const mid = `${c.dim}├${widths.map((w) => "─".repeat(Math.max(0, w + 2))).join("┼")}┤${c.reset}`;
|
|
178
|
+
const bot = `${c.dim}└${widths.map((w) => "─".repeat(Math.max(0, w + 2))).join("┴")}┘${c.reset}`;
|
|
153
179
|
lines.push(top);
|
|
154
180
|
for (let r = 0; r < this.tableRows.length; r++) {
|
|
155
181
|
const row = this.tableRows[r];
|
package/dist/mcp.js
CHANGED
|
@@ -32,6 +32,10 @@ function truncateStructured(value) {
|
|
|
32
32
|
}
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
+
const MAX_LISTED_RESOURCES = 200;
|
|
36
|
+
const MAX_LISTED_TEMPLATES = 100;
|
|
37
|
+
const MAX_LISTED_PROMPTS = 100;
|
|
38
|
+
const MAX_RESOURCE_BLOB_CHARS = 2048;
|
|
35
39
|
const DEFAULT_TIMEOUT = 30000;
|
|
36
40
|
const STDERD_KEEP_LINES = 20;
|
|
37
41
|
const OAUTH_BROWSER_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -48,6 +52,7 @@ let connectedServers = {};
|
|
|
48
52
|
const mcpServerErrors = {};
|
|
49
53
|
/** Cache of built tool wrappers; invalidated when MCP servers (re)connect. */
|
|
50
54
|
let mcpToolsCache = null;
|
|
55
|
+
let mcpReadOnlyToolIds = new Set();
|
|
51
56
|
const configCaches = new Map();
|
|
52
57
|
function loadJsonConfig(filePath) {
|
|
53
58
|
const cache = configCaches.get(filePath);
|
|
@@ -151,13 +156,100 @@ async function listAllTools(client) {
|
|
|
151
156
|
} while (cursor);
|
|
152
157
|
return all;
|
|
153
158
|
}
|
|
154
|
-
async function
|
|
159
|
+
async function pageAll(fetchPage, cap) {
|
|
160
|
+
const all = [];
|
|
161
|
+
let cursor;
|
|
162
|
+
do {
|
|
163
|
+
const page = await fetchPage(cursor);
|
|
164
|
+
all.push(...page.items);
|
|
165
|
+
cursor = page.nextCursor;
|
|
166
|
+
if (all.length >= cap)
|
|
167
|
+
return all.slice(0, cap);
|
|
168
|
+
} while (cursor);
|
|
169
|
+
return all;
|
|
170
|
+
}
|
|
171
|
+
export async function discoverMcpExtras(client) {
|
|
172
|
+
const caps = client.getServerCapabilities();
|
|
173
|
+
let resources = [];
|
|
174
|
+
let resourceTemplates = [];
|
|
175
|
+
let prompts = [];
|
|
176
|
+
if (caps?.resources) {
|
|
177
|
+
try {
|
|
178
|
+
resources = await pageAll(async (cursor) => {
|
|
179
|
+
const result = await client.listResources(cursor ? { cursor } : undefined);
|
|
180
|
+
return {
|
|
181
|
+
items: result.resources.map((r) => ({
|
|
182
|
+
uri: r.uri,
|
|
183
|
+
name: r.name,
|
|
184
|
+
...(r.description ? { description: r.description } : {}),
|
|
185
|
+
...(r.mimeType ? { mimeType: r.mimeType } : {}),
|
|
186
|
+
})),
|
|
187
|
+
nextCursor: result.nextCursor,
|
|
188
|
+
};
|
|
189
|
+
}, MAX_LISTED_RESOURCES);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
resources = [];
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
resourceTemplates = await pageAll(async (cursor) => {
|
|
196
|
+
const result = await client.listResourceTemplates(cursor ? { cursor } : undefined);
|
|
197
|
+
return {
|
|
198
|
+
items: result.resourceTemplates.map((t) => ({
|
|
199
|
+
uriTemplate: t.uriTemplate,
|
|
200
|
+
name: t.name,
|
|
201
|
+
...(t.description ? { description: t.description } : {}),
|
|
202
|
+
...(t.mimeType ? { mimeType: t.mimeType } : {}),
|
|
203
|
+
})),
|
|
204
|
+
nextCursor: result.nextCursor,
|
|
205
|
+
};
|
|
206
|
+
}, MAX_LISTED_TEMPLATES);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
resourceTemplates = [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (caps?.prompts) {
|
|
213
|
+
try {
|
|
214
|
+
prompts = await pageAll(async (cursor) => {
|
|
215
|
+
const result = await client.listPrompts(cursor ? { cursor } : undefined);
|
|
216
|
+
return {
|
|
217
|
+
items: result.prompts.map((p) => ({
|
|
218
|
+
name: p.name,
|
|
219
|
+
...(p.description ? { description: p.description } : {}),
|
|
220
|
+
...(p.arguments && p.arguments.length > 0 ? { arguments: p.arguments } : {}),
|
|
221
|
+
})),
|
|
222
|
+
nextCursor: result.nextCursor,
|
|
223
|
+
};
|
|
224
|
+
}, MAX_LISTED_PROMPTS);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
prompts = [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { resources, resourceTemplates, prompts };
|
|
231
|
+
}
|
|
232
|
+
async function connectedFromClient(client, transport, config, extra = {}) {
|
|
233
|
+
const tools = await listAllTools(client);
|
|
234
|
+
const extras = await discoverMcpExtras(client);
|
|
235
|
+
return {
|
|
236
|
+
client,
|
|
237
|
+
transport,
|
|
238
|
+
tools,
|
|
239
|
+
...extras,
|
|
240
|
+
callTimeout: resolveMcpTimeouts(config).callTimeout,
|
|
241
|
+
signature: configSignature(config),
|
|
242
|
+
...extra,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function openStdioMcpServer(name, config, hooks) {
|
|
155
246
|
// Support both formats:
|
|
156
247
|
// { command: ["uvx", "mcp-server-time"] } — min-agent native
|
|
157
248
|
// { command: "uvx", args: ["mcp-server-time"] } — opencode/claude style
|
|
158
249
|
let cmd;
|
|
159
250
|
let args;
|
|
160
251
|
if (Array.isArray(config.command)) {
|
|
252
|
+
;
|
|
161
253
|
[cmd, ...args] = config.command;
|
|
162
254
|
}
|
|
163
255
|
else if (typeof config.command === "string") {
|
|
@@ -179,16 +271,17 @@ async function openStdioMcpServer(name, config, onTransport) {
|
|
|
179
271
|
});
|
|
180
272
|
// Drain the child's stderr so a chatty server can never block on a full pipe buffer.
|
|
181
273
|
transport.stderr?.pipe(stderrSink.stream);
|
|
182
|
-
onTransport?.(transport);
|
|
274
|
+
hooks?.onTransport?.(transport);
|
|
183
275
|
const client = new Client({ name: "min-agent", version: "0.1.0" });
|
|
184
276
|
try {
|
|
185
277
|
await client.connect(transport);
|
|
186
|
-
|
|
187
|
-
return { client, transport, tools, callTimeout: resolveMcpTimeouts(config).callTimeout, signature: configSignature(config), stderrSink };
|
|
278
|
+
return await connectedFromClient(client, transport, config, { stderrSink });
|
|
188
279
|
}
|
|
189
280
|
catch (err) {
|
|
190
281
|
await transport.close().catch(() => { });
|
|
191
|
-
const tail = stderrSink.lines.length
|
|
282
|
+
const tail = stderrSink.lines.length
|
|
283
|
+
? `\nServer stderr (last ${stderrSink.lines.length} lines):\n${stderrSink.lines.map((l) => ` ${l}`).join("\n")}`
|
|
284
|
+
: "";
|
|
192
285
|
throw new Error(`${err?.message ?? String(err)}${tail}`);
|
|
193
286
|
}
|
|
194
287
|
}
|
|
@@ -407,7 +500,7 @@ async function runOAuthFlow(name, config, baseUrl) {
|
|
|
407
500
|
throw new Error(`MCP "${name}" OAuth failed: ${err?.message ?? String(err)}`);
|
|
408
501
|
}
|
|
409
502
|
}
|
|
410
|
-
async function openRemoteMcpServer(name, config,
|
|
503
|
+
async function openRemoteMcpServer(name, config, hooks) {
|
|
411
504
|
const rawUrl = config.url?.trim();
|
|
412
505
|
if (!rawUrl) {
|
|
413
506
|
throw new Error(`MCP "${name}" has empty url`);
|
|
@@ -425,15 +518,17 @@ async function openRemoteMcpServer(name, config, onTransport) {
|
|
|
425
518
|
const requestInit = buildRemoteRequestInit(config);
|
|
426
519
|
const mode = config.remoteTransport ?? "auto";
|
|
427
520
|
const oauthProvider = await runOAuthFlow(name, config, baseUrl);
|
|
428
|
-
|
|
521
|
+
// Expose the provider so an outer timeout can dispose it; on success the
|
|
522
|
+
// ConnectedServer takes ownership (closeConnectedServer closes it).
|
|
523
|
+
if (oauthProvider)
|
|
524
|
+
hooks?.onOAuthProvider?.(oauthProvider);
|
|
429
525
|
const connectStreamable = async () => {
|
|
430
526
|
const client = new Client({ name: "min-agent", version: "0.1.0" });
|
|
431
527
|
const transport = new StreamableHTTPClientTransport(baseUrl, requestInit || oauthProvider ? { requestInit, authProvider: oauthProvider } : undefined);
|
|
432
|
-
onTransport?.(transport);
|
|
528
|
+
hooks?.onTransport?.(transport);
|
|
433
529
|
try {
|
|
434
530
|
await client.connect(transport);
|
|
435
|
-
|
|
436
|
-
return { client, transport, tools, callTimeout, signature: configSignature(config), oauthProvider };
|
|
531
|
+
return await connectedFromClient(client, transport, config, { oauthProvider });
|
|
437
532
|
}
|
|
438
533
|
catch (err) {
|
|
439
534
|
await transport.close().catch(() => { });
|
|
@@ -443,11 +538,10 @@ async function openRemoteMcpServer(name, config, onTransport) {
|
|
|
443
538
|
const connectSse = async () => {
|
|
444
539
|
const client = new Client({ name: "min-agent", version: "0.1.0" });
|
|
445
540
|
const transport = new SSEClientTransport(baseUrl, requestInit || oauthProvider ? { requestInit, authProvider: oauthProvider } : undefined);
|
|
446
|
-
onTransport?.(transport);
|
|
541
|
+
hooks?.onTransport?.(transport);
|
|
447
542
|
try {
|
|
448
543
|
await client.connect(transport);
|
|
449
|
-
|
|
450
|
-
return { client, transport, tools, callTimeout, signature: configSignature(config), oauthProvider };
|
|
544
|
+
return await connectedFromClient(client, transport, config, { oauthProvider });
|
|
451
545
|
}
|
|
452
546
|
catch (err) {
|
|
453
547
|
await transport.close().catch(() => { });
|
|
@@ -473,6 +567,9 @@ async function openRemoteMcpServer(name, config, onTransport) {
|
|
|
473
567
|
}
|
|
474
568
|
}
|
|
475
569
|
catch (err) {
|
|
570
|
+
// The connection failed, so nobody else owns the provider — close its
|
|
571
|
+
// callback server instead of leaking the port.
|
|
572
|
+
oauthProvider?.close();
|
|
476
573
|
const message = err instanceof Error ? err.message : String(err);
|
|
477
574
|
// Handle OAuth/Unauthorized errors
|
|
478
575
|
if (err instanceof UnauthorizedError || message.includes("Unauthorized") || message.includes("401")) {
|
|
@@ -485,11 +582,11 @@ async function openRemoteMcpServer(name, config, onTransport) {
|
|
|
485
582
|
throw new Error(`MCP "${name}" remote connection failed: ${message}`);
|
|
486
583
|
}
|
|
487
584
|
}
|
|
488
|
-
async function openMcpServer(name, config,
|
|
585
|
+
async function openMcpServer(name, config, hooks) {
|
|
489
586
|
if (isRemoteMcpConfig(config)) {
|
|
490
|
-
return await openRemoteMcpServer(name, config,
|
|
587
|
+
return await openRemoteMcpServer(name, config, hooks);
|
|
491
588
|
}
|
|
492
|
-
return await openStdioMcpServer(name, config,
|
|
589
|
+
return await openStdioMcpServer(name, config, hooks);
|
|
493
590
|
}
|
|
494
591
|
/** One-line summary for CLI / logs (no secrets). */
|
|
495
592
|
export function formatMcpServerBinding(config) {
|
|
@@ -497,18 +594,26 @@ export function formatMcpServerBinding(config) {
|
|
|
497
594
|
const mode = config.remoteTransport ?? "auto";
|
|
498
595
|
return `${config.url} [remote:${mode}]`;
|
|
499
596
|
}
|
|
500
|
-
const cmd = Array.isArray(config.command)
|
|
597
|
+
const cmd = Array.isArray(config.command)
|
|
598
|
+
? config.command.join(" ")
|
|
599
|
+
: `${config.command ?? ""} ${(config.args ?? []).join(" ")}`.trim();
|
|
501
600
|
return cmd || "(no command)";
|
|
502
601
|
}
|
|
503
602
|
async function openMcpServerWithTimeout(name, config) {
|
|
504
603
|
const timeout = resolveMcpTimeouts(config).connectTimeout;
|
|
505
604
|
let timer;
|
|
506
605
|
const transportHolder = { transport: null };
|
|
606
|
+
const oauthHolder = { provider: null };
|
|
507
607
|
const timeoutPromise = new Promise((_, reject) => {
|
|
508
608
|
timer = setTimeout(() => reject(new Error(`connection timed out after ${timeout}ms`)), timeout);
|
|
509
609
|
});
|
|
510
|
-
const connectPromise = openMcpServer(name, config,
|
|
511
|
-
|
|
610
|
+
const connectPromise = openMcpServer(name, config, {
|
|
611
|
+
onTransport: (t) => {
|
|
612
|
+
transportHolder.transport = t;
|
|
613
|
+
},
|
|
614
|
+
onOAuthProvider: (p) => {
|
|
615
|
+
oauthHolder.provider = p;
|
|
616
|
+
},
|
|
512
617
|
});
|
|
513
618
|
connectPromise.catch(() => { });
|
|
514
619
|
try {
|
|
@@ -517,6 +622,12 @@ async function openMcpServerWithTimeout(name, config) {
|
|
|
517
622
|
}
|
|
518
623
|
catch (error) {
|
|
519
624
|
await transportHolder.transport?.close().catch(() => { });
|
|
625
|
+
oauthHolder.provider?.close();
|
|
626
|
+
// The abandoned connect may still resolve later (e.g. a slow OAuth login);
|
|
627
|
+
// dispose the result immediately so neither the client nor the OAuth
|
|
628
|
+
// callback server leaks. The registry is untouched: this server was never
|
|
629
|
+
// registered, and a newer connection may own the name by now.
|
|
630
|
+
void connectPromise.then((server) => disposeServerResources(server)).catch(() => { });
|
|
520
631
|
return { error };
|
|
521
632
|
}
|
|
522
633
|
finally {
|
|
@@ -530,8 +641,8 @@ export async function connectMcpServer(name, config) {
|
|
|
530
641
|
const { server, error } = await openMcpServerWithTimeout(name, config);
|
|
531
642
|
if (server) {
|
|
532
643
|
delete mcpServerErrors[name];
|
|
533
|
-
console.log(`\x1b[90m MCP "${name}" connected (${server.tools.length} tools)\x1b[0m`);
|
|
534
|
-
log("info", `mcp "${name}" connected (${server.tools.length} tools)`);
|
|
644
|
+
console.log(`\x1b[90m MCP "${name}" connected (${server.tools.length} tools, ${server.resources.length} resources, ${server.prompts.length} prompts)\x1b[0m`);
|
|
645
|
+
log("info", `mcp "${name}" connected (${server.tools.length} tools, ${server.resources.length} resources, ${server.prompts.length} prompts)`);
|
|
535
646
|
return server;
|
|
536
647
|
}
|
|
537
648
|
const message = error?.message ?? String(error);
|
|
@@ -559,19 +670,26 @@ export async function checkMcpServer(name, config) {
|
|
|
559
670
|
await server.client.close();
|
|
560
671
|
}
|
|
561
672
|
catch { }
|
|
673
|
+
server.oauthProvider?.close();
|
|
562
674
|
return { name, enabled: true, ok: true, toolCount };
|
|
563
675
|
}
|
|
564
676
|
export async function checkAllMcpServers() {
|
|
565
677
|
const entries = loadMcpConfigEntries();
|
|
566
678
|
const settled = await Promise.allSettled(entries.map(({ name, config }) => checkMcpServer(name, config)));
|
|
567
|
-
return settled.map((r, i) => r.status === "fulfilled"
|
|
679
|
+
return settled.map((r, i) => r.status === "fulfilled"
|
|
680
|
+
? r.value
|
|
681
|
+
: { name: entries[i]?.name ?? "unknown", enabled: true, ok: false, toolCount: 0, error: String(r.reason) });
|
|
568
682
|
}
|
|
569
|
-
|
|
683
|
+
/** Close client + OAuth provider without touching the connection registry. */
|
|
684
|
+
async function disposeServerResources(server) {
|
|
570
685
|
try {
|
|
571
686
|
await server.client.close();
|
|
572
687
|
}
|
|
573
688
|
catch { }
|
|
574
689
|
server.oauthProvider?.close();
|
|
690
|
+
}
|
|
691
|
+
async function closeConnectedServer(name, server) {
|
|
692
|
+
await disposeServerResources(server);
|
|
575
693
|
delete connectedServers[name];
|
|
576
694
|
delete mcpServerErrors[name];
|
|
577
695
|
}
|
|
@@ -648,15 +766,20 @@ export async function initMcp() {
|
|
|
648
766
|
export function getMcpTools() {
|
|
649
767
|
if (mcpToolsCache)
|
|
650
768
|
return mcpToolsCache;
|
|
651
|
-
|
|
769
|
+
mcpReadOnlyToolIds = new Set();
|
|
770
|
+
mcpToolsCache = buildMcpToolsMap(connectedServers, mcpReadOnlyToolIds);
|
|
652
771
|
return mcpToolsCache;
|
|
653
772
|
}
|
|
773
|
+
export function getMcpReadOnlyToolIds() {
|
|
774
|
+
getMcpTools();
|
|
775
|
+
return mcpReadOnlyToolIds;
|
|
776
|
+
}
|
|
654
777
|
/**
|
|
655
778
|
* Build AI SDK tool wrappers for a set of connected MCP servers.
|
|
656
779
|
* Tool IDs are collision-safe (suffixed when sanitized names clash) and
|
|
657
780
|
* descriptions are prefixed with the owning server name.
|
|
658
781
|
*/
|
|
659
|
-
export function buildMcpToolsMap(servers) {
|
|
782
|
+
export function buildMcpToolsMap(servers, readOnlyIds) {
|
|
660
783
|
const tools = {};
|
|
661
784
|
const used = new Set();
|
|
662
785
|
for (const [serverName, server] of Object.entries(servers)) {
|
|
@@ -673,15 +796,14 @@ export function buildMcpToolsMap(servers) {
|
|
|
673
796
|
...(baseSchema.properties ? {} : { properties: {} }),
|
|
674
797
|
};
|
|
675
798
|
const readOnly = mcpTool.annotations?.readOnlyHint === true;
|
|
676
|
-
|
|
677
|
-
(
|
|
799
|
+
if (readOnly)
|
|
800
|
+
readOnlyIds?.add(toolId);
|
|
801
|
+
const description = `[${serverName}] ${mcpTool.description ?? `MCP tool: ${mcpTool.name}`}` + (readOnly ? " (read-only)" : "");
|
|
678
802
|
const outputSchema = mcpTool.outputSchema;
|
|
679
803
|
tools[toolId] = tool({
|
|
680
804
|
description,
|
|
681
805
|
inputSchema: jsonSchema(schema),
|
|
682
|
-
...(outputSchema && outputSchema.type === "object"
|
|
683
|
-
? { outputSchema: jsonSchema(outputSchema) }
|
|
684
|
-
: {}),
|
|
806
|
+
...(outputSchema && outputSchema.type === "object" ? { outputSchema: jsonSchema(outputSchema) } : {}),
|
|
685
807
|
execute: async (args, options) => {
|
|
686
808
|
try {
|
|
687
809
|
const signals = [AbortSignal.timeout(server.callTimeout)];
|
|
@@ -722,11 +844,187 @@ export function getMcpStatus() {
|
|
|
722
844
|
connected: !!server,
|
|
723
845
|
enabled: config.enabled !== false,
|
|
724
846
|
tools: server?.tools.map((t) => t.name) ?? [],
|
|
847
|
+
resource_count: server?.resources.length ?? 0,
|
|
848
|
+
prompt_count: server?.prompts.length ?? 0,
|
|
725
849
|
...(mcpServerErrors[name] ? { error: mcpServerErrors[name] } : {}),
|
|
726
850
|
};
|
|
727
851
|
}
|
|
728
852
|
return status;
|
|
729
853
|
}
|
|
854
|
+
export function listMcpCatalog(serverName) {
|
|
855
|
+
const names = serverName ? [serverName] : Object.keys(connectedServers);
|
|
856
|
+
return {
|
|
857
|
+
servers: names.flatMap((name) => {
|
|
858
|
+
const server = connectedServers[name];
|
|
859
|
+
if (!server)
|
|
860
|
+
return [];
|
|
861
|
+
return [{ name, resources: server.resources, templates: server.resourceTemplates, prompts: server.prompts }];
|
|
862
|
+
}),
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function connectedNames() {
|
|
866
|
+
return Object.keys(connectedServers);
|
|
867
|
+
}
|
|
868
|
+
function resolveCatalogServer(serverName, kind) {
|
|
869
|
+
if (serverName) {
|
|
870
|
+
const server = connectedServers[serverName];
|
|
871
|
+
if (!server)
|
|
872
|
+
return `MCP server "${serverName}" is not connected. Connected: ${connectedNames().join(", ") || "none"}`;
|
|
873
|
+
return server;
|
|
874
|
+
}
|
|
875
|
+
const names = connectedNames();
|
|
876
|
+
if (names.length === 0)
|
|
877
|
+
return "No MCP servers connected.";
|
|
878
|
+
if (names.length === 1)
|
|
879
|
+
return connectedServers[names[0]];
|
|
880
|
+
return `Multiple MCP servers are connected (${names.join(", ")}). Pass server to choose which ${kind} to use.`;
|
|
881
|
+
}
|
|
882
|
+
export async function readMcpResource(uri, serverName) {
|
|
883
|
+
const resolved = resolveCatalogServer(serverName, "resource");
|
|
884
|
+
if (typeof resolved === "string")
|
|
885
|
+
return resolved;
|
|
886
|
+
try {
|
|
887
|
+
const result = await resolved.client.readResource({ uri }, { timeout: resolved.callTimeout });
|
|
888
|
+
return formatResourceContents(result.contents);
|
|
889
|
+
}
|
|
890
|
+
catch (err) {
|
|
891
|
+
return `Failed to read resource: ${err instanceof Error ? err.message : String(err)}`;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
export async function getMcpPrompt(name, args, serverName) {
|
|
895
|
+
const resolved = resolveCatalogServer(serverName, "prompt");
|
|
896
|
+
if (typeof resolved === "string")
|
|
897
|
+
return resolved;
|
|
898
|
+
try {
|
|
899
|
+
const result = await resolved.client.getPrompt({ name, ...(args && Object.keys(args).length > 0 ? { arguments: args } : {}) }, { timeout: resolved.callTimeout });
|
|
900
|
+
return formatPromptResult(result);
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
return `Failed to get prompt: ${err instanceof Error ? err.message : String(err)}`;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
export function formatResourceContents(contents) {
|
|
907
|
+
if (contents.length === 0)
|
|
908
|
+
return "(empty resource)";
|
|
909
|
+
return contents
|
|
910
|
+
.map((part) => {
|
|
911
|
+
if (typeof part.text === "string")
|
|
912
|
+
return part.text;
|
|
913
|
+
if (typeof part.blob === "string") {
|
|
914
|
+
if (part.blob.length <= MAX_RESOURCE_BLOB_CHARS)
|
|
915
|
+
return part.blob;
|
|
916
|
+
return `[binary ${part.mimeType ?? "application/octet-stream"} ${part.blob.length} chars, omitted]`;
|
|
917
|
+
}
|
|
918
|
+
return JSON.stringify(part);
|
|
919
|
+
})
|
|
920
|
+
.join("\n\n");
|
|
921
|
+
}
|
|
922
|
+
export function formatPromptResult(result) {
|
|
923
|
+
const header = result.description ? `${result.description}\n\n` : "";
|
|
924
|
+
const body = result.messages
|
|
925
|
+
.map((m) => {
|
|
926
|
+
const content = m.content;
|
|
927
|
+
if (content &&
|
|
928
|
+
typeof content === "object" &&
|
|
929
|
+
"type" in content &&
|
|
930
|
+
content.type === "text" &&
|
|
931
|
+
"text" in content) {
|
|
932
|
+
return `${m.role}: ${String(content.text)}`;
|
|
933
|
+
}
|
|
934
|
+
return `${m.role}: ${JSON.stringify(content)}`;
|
|
935
|
+
})
|
|
936
|
+
.join("\n");
|
|
937
|
+
return header + body;
|
|
938
|
+
}
|
|
939
|
+
export function getMcpCatalogTools() {
|
|
940
|
+
if (Object.keys(connectedServers).length === 0)
|
|
941
|
+
return {};
|
|
942
|
+
const listResources = tool({
|
|
943
|
+
description: "List MCP resources and resource URI templates from connected servers. Use this before mcp_read_resource. Optional server name limits the listing to one server.",
|
|
944
|
+
inputSchema: jsonSchema({
|
|
945
|
+
type: "object",
|
|
946
|
+
properties: {
|
|
947
|
+
server: { type: "string", description: "MCP server name. Omit to list every connected server." },
|
|
948
|
+
},
|
|
949
|
+
}),
|
|
950
|
+
execute: async ({ server }) => {
|
|
951
|
+
const catalog = listMcpCatalog(server);
|
|
952
|
+
if (catalog.servers.length === 0) {
|
|
953
|
+
return server ? `MCP server "${server}" is not connected.` : "No MCP servers connected.";
|
|
954
|
+
}
|
|
955
|
+
const lines = catalog.servers.flatMap((s) => {
|
|
956
|
+
const res = s.resources.map((r) => ` resource ${r.uri} ${r.name}${r.description ? ` — ${r.description}` : ""}`);
|
|
957
|
+
const templates = s.templates.map((t) => ` template ${t.uriTemplate} ${t.name}${t.description ? ` — ${t.description}` : ""}`);
|
|
958
|
+
if (res.length === 0 && templates.length === 0)
|
|
959
|
+
return [`[${s.name}] (no resources)`];
|
|
960
|
+
return [`[${s.name}]`, ...res, ...templates];
|
|
961
|
+
});
|
|
962
|
+
return truncateToolOutput(lines.join("\n"), { direction: "head" }).content;
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
const readResource = tool({
|
|
966
|
+
description: "Read an MCP resource by URI. Use mcp_list_resources first. Pass server when more than one MCP server is connected.",
|
|
967
|
+
inputSchema: jsonSchema({
|
|
968
|
+
type: "object",
|
|
969
|
+
properties: {
|
|
970
|
+
uri: { type: "string", description: "Resource URI from mcp_list_resources" },
|
|
971
|
+
server: { type: "string", description: "MCP server name when multiple servers are connected" },
|
|
972
|
+
},
|
|
973
|
+
required: ["uri"],
|
|
974
|
+
}),
|
|
975
|
+
execute: async ({ uri, server }) => truncateToolOutput(await readMcpResource(uri, server), { direction: "head" }).content,
|
|
976
|
+
});
|
|
977
|
+
const listPrompts = tool({
|
|
978
|
+
description: "List MCP prompt templates from connected servers. Use this before mcp_get_prompt. Optional server name limits the listing to one server.",
|
|
979
|
+
inputSchema: jsonSchema({
|
|
980
|
+
type: "object",
|
|
981
|
+
properties: {
|
|
982
|
+
server: { type: "string", description: "MCP server name. Omit to list every connected server." },
|
|
983
|
+
},
|
|
984
|
+
}),
|
|
985
|
+
execute: async ({ server }) => {
|
|
986
|
+
const catalog = listMcpCatalog(server);
|
|
987
|
+
if (catalog.servers.length === 0) {
|
|
988
|
+
return server ? `MCP server "${server}" is not connected.` : "No MCP servers connected.";
|
|
989
|
+
}
|
|
990
|
+
const lines = catalog.servers.flatMap((s) => {
|
|
991
|
+
if (s.prompts.length === 0)
|
|
992
|
+
return [`[${s.name}] (no prompts)`];
|
|
993
|
+
return [
|
|
994
|
+
`[${s.name}]`,
|
|
995
|
+
...s.prompts.map((p) => {
|
|
996
|
+
const args = p.arguments?.map((a) => `${a.name}${a.required ? "*" : ""}`).join(", ");
|
|
997
|
+
return ` ${p.name}${args ? `(${args})` : ""}${p.description ? ` — ${p.description}` : ""}`;
|
|
998
|
+
}),
|
|
999
|
+
];
|
|
1000
|
+
});
|
|
1001
|
+
return truncateToolOutput(lines.join("\n"), { direction: "head" }).content;
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
const getPrompt = tool({
|
|
1005
|
+
description: "Fill an MCP prompt template. Use mcp_list_prompts first. Pass server when more than one MCP server is connected.",
|
|
1006
|
+
inputSchema: jsonSchema({
|
|
1007
|
+
type: "object",
|
|
1008
|
+
properties: {
|
|
1009
|
+
name: { type: "string", description: "Prompt name from mcp_list_prompts" },
|
|
1010
|
+
server: { type: "string", description: "MCP server name when multiple servers are connected" },
|
|
1011
|
+
arguments: {
|
|
1012
|
+
type: "object",
|
|
1013
|
+
additionalProperties: { type: "string" },
|
|
1014
|
+
description: "Prompt arguments as string values",
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
required: ["name"],
|
|
1018
|
+
}),
|
|
1019
|
+
execute: async ({ name, server, arguments: promptArgs }) => truncateToolOutput(await getMcpPrompt(name, promptArgs, server), { direction: "head" }).content,
|
|
1020
|
+
});
|
|
1021
|
+
return {
|
|
1022
|
+
mcp_list_resources: listResources,
|
|
1023
|
+
mcp_read_resource: readResource,
|
|
1024
|
+
mcp_list_prompts: listPrompts,
|
|
1025
|
+
mcp_get_prompt: getPrompt,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
730
1028
|
function sanitize(s) {
|
|
731
1029
|
return s.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
732
1030
|
}
|