mini-coder 0.5.11 → 0.5.13
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/BENCHMARK.md +107 -0
- package/PROGRESS.md +4 -0
- package/README.md +66 -21
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-baseline.sh +15 -0
- package/benchmark-loop.sh +19 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +20 -0
- package/src/cli.ts +2 -1
- package/src/headless.ts +152 -38
- package/src/index.ts +107 -143
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +10 -12
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +218 -22
- package/src/shared.ts +39 -0
- package/src/skills.ts +12 -3
- package/src/submit.ts +20 -25
- package/src/text.ts +71 -0
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +93 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +1001 -0
- package/src/tools.ts +190 -995
- package/src/ui/agent.ts +206 -110
- package/src/ui/commands.test.ts +489 -17
- package/src/ui/commands.ts +231 -55
- package/src/ui/conversation.test.ts +585 -0
- package/src/ui/conversation.ts +878 -455
- package/src/ui/help.ts +27 -11
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +422 -185
- package/src/plugins.ts +0 -183
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Context Protocol client discovery and tool integration.
|
|
3
|
+
*
|
|
4
|
+
* mini-coder tracks configured Streamable HTTP MCP servers, connects enabled
|
|
5
|
+
* ones when needed, imports their tools, and exposes those tools through the
|
|
6
|
+
* normal pi-ai tool interface.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Static, Tool, ToolCall, TSchema } from "@mariozechner/pi-ai";
|
|
12
|
+
import { Type, validateToolArguments } from "@mariozechner/pi-ai";
|
|
13
|
+
import { Client } from "@modelcontextprotocol/sdk/client";
|
|
14
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
15
|
+
import type { ToolHandler, ToolUpdateCallback } from "./agent.ts";
|
|
16
|
+
import { getErrorMessage } from "./errors.ts";
|
|
17
|
+
import type { McpSettings } from "./settings.ts";
|
|
18
|
+
import { type ToolExecResult, textResult } from "./tool-common.ts";
|
|
19
|
+
|
|
20
|
+
const MCP_DISCOVERY_TIMEOUT_MS = 3_000;
|
|
21
|
+
|
|
22
|
+
interface McpListedTool {
|
|
23
|
+
/** JSON Schema describing the tool input object. */
|
|
24
|
+
inputSchema: {
|
|
25
|
+
properties?: Record<string, object>;
|
|
26
|
+
required?: string[];
|
|
27
|
+
type: "object";
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
/** Human-readable tool description when provided by the server. */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Server-defined MCP tool name. */
|
|
33
|
+
name: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type McpCallToolResult =
|
|
37
|
+
| {
|
|
38
|
+
/** Tool content blocks returned by the MCP server. */
|
|
39
|
+
content: McpToolResultContent[];
|
|
40
|
+
/** Optional structured data returned alongside content. */
|
|
41
|
+
structuredContent?: unknown;
|
|
42
|
+
/** Whether the server marked this result as an error. */
|
|
43
|
+
isError?: boolean;
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
/** Compatibility payload from older MCP tool result formats. */
|
|
47
|
+
toolResult: unknown;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type McpToolResultContent =
|
|
51
|
+
| {
|
|
52
|
+
type: "text";
|
|
53
|
+
text: string;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
type: "image";
|
|
57
|
+
data: string;
|
|
58
|
+
mimeType: string;
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
type: "audio";
|
|
62
|
+
data: string;
|
|
63
|
+
mimeType: string;
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
type: "resource";
|
|
67
|
+
resource:
|
|
68
|
+
| {
|
|
69
|
+
uri: string;
|
|
70
|
+
text: string;
|
|
71
|
+
mimeType?: string;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
uri: string;
|
|
75
|
+
blob: string;
|
|
76
|
+
mimeType?: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
type: "resource_link";
|
|
81
|
+
uri: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description?: string;
|
|
84
|
+
mimeType?: string;
|
|
85
|
+
size?: number;
|
|
86
|
+
title?: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type McpProgress = {
|
|
90
|
+
/** Current completed work units reported by the server. */
|
|
91
|
+
progress: number;
|
|
92
|
+
/** Total work units when the server reports them. */
|
|
93
|
+
total?: number;
|
|
94
|
+
/** Optional human-readable progress message. */
|
|
95
|
+
message?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
interface McpTransportLike {
|
|
99
|
+
close(): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface McpClientLike {
|
|
103
|
+
connect(
|
|
104
|
+
transport: McpTransportLike,
|
|
105
|
+
options?: { timeout?: number },
|
|
106
|
+
): Promise<void>;
|
|
107
|
+
listTools(
|
|
108
|
+
params?: { cursor?: string },
|
|
109
|
+
options?: { timeout?: number },
|
|
110
|
+
): Promise<{
|
|
111
|
+
tools: McpListedTool[];
|
|
112
|
+
nextCursor?: string;
|
|
113
|
+
}>;
|
|
114
|
+
callTool(
|
|
115
|
+
params: {
|
|
116
|
+
name: string;
|
|
117
|
+
arguments?: Record<string, unknown>;
|
|
118
|
+
},
|
|
119
|
+
resultSchema?: undefined,
|
|
120
|
+
options?: {
|
|
121
|
+
signal?: AbortSignal;
|
|
122
|
+
onprogress?: (progress: McpProgress) => void;
|
|
123
|
+
},
|
|
124
|
+
): Promise<McpCallToolResult>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface McpRuntime {
|
|
128
|
+
createClient(): McpClientLike;
|
|
129
|
+
createTransport(url: URL): McpTransportLike;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const defaultMcpRuntime: McpRuntime = {
|
|
133
|
+
createClient() {
|
|
134
|
+
return new Client(
|
|
135
|
+
{
|
|
136
|
+
name: "mini-coder",
|
|
137
|
+
version: "0.0.0",
|
|
138
|
+
},
|
|
139
|
+
{ capabilities: {} },
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
createTransport(url) {
|
|
143
|
+
return new StreamableHTTPClientTransport(url);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
async function noopClose(): Promise<void> {}
|
|
148
|
+
|
|
149
|
+
/** A configured MCP server and its current runtime connection state. */
|
|
150
|
+
export interface McpServerState {
|
|
151
|
+
/** User-configured server identifier from `settings.json`. */
|
|
152
|
+
name: string;
|
|
153
|
+
/** Absolute Streamable HTTP endpoint URL. */
|
|
154
|
+
url: string;
|
|
155
|
+
/** Whether this server should be available to future turns. */
|
|
156
|
+
enabled: boolean;
|
|
157
|
+
/** Whether mini-coder currently has an active connection to this server. */
|
|
158
|
+
connected: boolean;
|
|
159
|
+
/** pi-ai tool definitions derived from the server's tool list. */
|
|
160
|
+
tools: Tool[];
|
|
161
|
+
/** Tool name → handler map for calling the remote MCP tools. */
|
|
162
|
+
toolHandlers: Map<string, ToolHandler>;
|
|
163
|
+
/** Close the underlying MCP transport when connected. */
|
|
164
|
+
close(): Promise<void>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface McpConnectionState {
|
|
168
|
+
/** Non-fatal warnings encountered while importing tools. */
|
|
169
|
+
warnings: string[];
|
|
170
|
+
/** pi-ai tool definitions derived from the server's tool list. */
|
|
171
|
+
tools: Tool[];
|
|
172
|
+
/** Tool name → handler map for calling the remote MCP tools. */
|
|
173
|
+
toolHandlers: Map<string, ToolHandler>;
|
|
174
|
+
/** Close the underlying MCP transport. */
|
|
175
|
+
close(): Promise<void>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface McpDiscoveryResult {
|
|
179
|
+
/** MCP servers kept in runtime state after startup discovery. */
|
|
180
|
+
servers: McpServerState[];
|
|
181
|
+
/** Non-fatal startup warnings for skipped or unreachable servers. */
|
|
182
|
+
warnings: string[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createDisconnectedMcpServer(
|
|
186
|
+
entry: NonNullable<McpSettings["servers"]>[number],
|
|
187
|
+
): McpServerState {
|
|
188
|
+
return {
|
|
189
|
+
name: entry.name,
|
|
190
|
+
url: entry.url,
|
|
191
|
+
enabled: entry.enabled,
|
|
192
|
+
connected: false,
|
|
193
|
+
tools: [],
|
|
194
|
+
toolHandlers: new Map(),
|
|
195
|
+
close: noopClose,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resetMcpServerConnection(server: McpServerState): void {
|
|
200
|
+
server.connected = false;
|
|
201
|
+
server.tools = [];
|
|
202
|
+
server.toolHandlers = new Map();
|
|
203
|
+
server.close = noopClose;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseMcpServerUrl(rawUrl: string): URL | null {
|
|
207
|
+
try {
|
|
208
|
+
return new URL(rawUrl);
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Connect a configured MCP server and populate its imported tools.
|
|
216
|
+
*
|
|
217
|
+
* On failure the server is left disconnected and the returned warnings describe
|
|
218
|
+
* why the connection was skipped.
|
|
219
|
+
*
|
|
220
|
+
* @param server - Runtime MCP server state to connect.
|
|
221
|
+
* @param runtime - Internal runtime injection for tests.
|
|
222
|
+
* @returns Non-fatal warnings produced while connecting or importing tools.
|
|
223
|
+
*/
|
|
224
|
+
export async function connectMcpServer(
|
|
225
|
+
server: McpServerState,
|
|
226
|
+
runtime: McpRuntime = defaultMcpRuntime,
|
|
227
|
+
): Promise<string[]> {
|
|
228
|
+
if (server.connected) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const connection = await createMcpServerConnection(
|
|
233
|
+
server.name,
|
|
234
|
+
server.url,
|
|
235
|
+
runtime,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (!("tools" in connection)) {
|
|
239
|
+
resetMcpServerConnection(server);
|
|
240
|
+
return [connection.warning];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
server.connected = true;
|
|
244
|
+
server.tools = connection.tools;
|
|
245
|
+
server.toolHandlers = connection.toolHandlers;
|
|
246
|
+
server.close = connection.close;
|
|
247
|
+
return connection.warnings;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Disconnect an MCP server and clear its imported tool state.
|
|
252
|
+
*
|
|
253
|
+
* @param server - Runtime MCP server state to disconnect.
|
|
254
|
+
*/
|
|
255
|
+
export async function disconnectMcpServer(
|
|
256
|
+
server: McpServerState,
|
|
257
|
+
): Promise<void> {
|
|
258
|
+
if (!server.connected) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const close = server.close;
|
|
263
|
+
resetMcpServerConnection(server);
|
|
264
|
+
await close();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Connect enabled MCP servers at startup while keeping disabled ones in state.
|
|
269
|
+
*
|
|
270
|
+
* Invalid or unreachable servers do not fail startup. Enabled servers that
|
|
271
|
+
* cannot connect are skipped with warnings. Disabled servers stay disconnected
|
|
272
|
+
* until the user turns them back on.
|
|
273
|
+
*
|
|
274
|
+
* @param settings - Optional MCP settings from `settings.json`.
|
|
275
|
+
* @param runtime - Internal runtime injection for tests.
|
|
276
|
+
* @returns MCP server state plus any non-fatal startup warnings.
|
|
277
|
+
*/
|
|
278
|
+
export async function discoverMcpServers(
|
|
279
|
+
settings: McpSettings | undefined,
|
|
280
|
+
runtime: McpRuntime = defaultMcpRuntime,
|
|
281
|
+
): Promise<McpDiscoveryResult> {
|
|
282
|
+
const servers: McpServerState[] = [];
|
|
283
|
+
const warnings: string[] = [];
|
|
284
|
+
|
|
285
|
+
for (const entry of settings?.servers ?? []) {
|
|
286
|
+
if (!parseMcpServerUrl(entry.url)) {
|
|
287
|
+
warnings.push(`MCP server "${entry.name}": invalid URL (${entry.url})`);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const server = createDisconnectedMcpServer(entry);
|
|
292
|
+
if (!entry.enabled) {
|
|
293
|
+
servers.push(server);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const connectWarnings = await connectMcpServer(server, runtime);
|
|
298
|
+
warnings.push(...connectWarnings);
|
|
299
|
+
if (!server.connected) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
servers.push(server);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return { servers, warnings };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function createMcpServerConnection(
|
|
310
|
+
serverName: string,
|
|
311
|
+
serverUrl: string,
|
|
312
|
+
runtime: McpRuntime,
|
|
313
|
+
): Promise<McpConnectionState | { warning: string }> {
|
|
314
|
+
const url = parseMcpServerUrl(serverUrl);
|
|
315
|
+
if (!url) {
|
|
316
|
+
return {
|
|
317
|
+
warning: `MCP server "${serverName}": invalid URL (${serverUrl})`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const transport = runtime.createTransport(url);
|
|
322
|
+
const client = runtime.createClient();
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
await client.connect(transport, { timeout: MCP_DISCOVERY_TIMEOUT_MS });
|
|
326
|
+
const listedTools = await listAllTools(client);
|
|
327
|
+
|
|
328
|
+
if (listedTools.length === 0) {
|
|
329
|
+
await closeQuietly(transport);
|
|
330
|
+
return {
|
|
331
|
+
warning: `MCP server "${serverName}" exposes no tools and was skipped.`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const warnings: string[] = [];
|
|
336
|
+
const tools: Tool[] = [];
|
|
337
|
+
const toolHandlers = new Map<string, ToolHandler>();
|
|
338
|
+
|
|
339
|
+
for (const listedTool of listedTools) {
|
|
340
|
+
const { tool, handler } = createMcpTool(serverName, listedTool, client);
|
|
341
|
+
if (toolHandlers.has(tool.name)) {
|
|
342
|
+
warnings.push(
|
|
343
|
+
`MCP server "${serverName}": duplicate tool name "${listedTool.name}" was skipped.`,
|
|
344
|
+
);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
tools.push(tool);
|
|
348
|
+
toolHandlers.set(tool.name, handler);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (tools.length === 0) {
|
|
352
|
+
await closeQuietly(transport);
|
|
353
|
+
return {
|
|
354
|
+
warning: `MCP server "${serverName}" exposes no usable tools and was skipped.`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
warnings,
|
|
360
|
+
tools,
|
|
361
|
+
toolHandlers,
|
|
362
|
+
close: () => transport.close(),
|
|
363
|
+
};
|
|
364
|
+
} catch (error) {
|
|
365
|
+
await closeQuietly(transport);
|
|
366
|
+
return {
|
|
367
|
+
warning: `MCP server "${serverName}": ${getErrorMessage(error)} (${serverUrl})`,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function listAllTools(client: McpClientLike): Promise<McpListedTool[]> {
|
|
373
|
+
const tools: McpListedTool[] = [];
|
|
374
|
+
let cursor: string | undefined;
|
|
375
|
+
|
|
376
|
+
do {
|
|
377
|
+
const page = await client.listTools(cursor ? { cursor } : undefined, {
|
|
378
|
+
timeout: MCP_DISCOVERY_TIMEOUT_MS,
|
|
379
|
+
});
|
|
380
|
+
tools.push(...page.tools);
|
|
381
|
+
cursor = page.nextCursor;
|
|
382
|
+
} while (cursor);
|
|
383
|
+
|
|
384
|
+
return tools;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function createMcpTool(
|
|
388
|
+
serverName: string,
|
|
389
|
+
listedTool: McpListedTool,
|
|
390
|
+
client: McpClientLike,
|
|
391
|
+
): {
|
|
392
|
+
tool: Tool;
|
|
393
|
+
handler: ToolHandler;
|
|
394
|
+
} {
|
|
395
|
+
const toolName = `${serverName}__${listedTool.name}`;
|
|
396
|
+
const tool: Tool = {
|
|
397
|
+
name: toolName,
|
|
398
|
+
description: listedTool.description
|
|
399
|
+
? `[MCP ${serverName}] ${listedTool.description}`
|
|
400
|
+
: `[MCP ${serverName}] ${listedTool.name}`,
|
|
401
|
+
parameters: Type.Unsafe<Record<string, unknown>>(listedTool.inputSchema),
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const handler: ToolHandler = async (args, _cwd, signal, onUpdate) => {
|
|
405
|
+
const validatedArgs = validateMcpToolArgs(tool, args) as Record<
|
|
406
|
+
string,
|
|
407
|
+
unknown
|
|
408
|
+
>;
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const result = await client.callTool(
|
|
412
|
+
{
|
|
413
|
+
name: listedTool.name,
|
|
414
|
+
arguments: validatedArgs,
|
|
415
|
+
},
|
|
416
|
+
undefined,
|
|
417
|
+
{
|
|
418
|
+
...(signal ? { signal } : {}),
|
|
419
|
+
...(onUpdate
|
|
420
|
+
? {
|
|
421
|
+
onprogress: (progress: McpProgress) => {
|
|
422
|
+
emitMcpProgressUpdate(
|
|
423
|
+
serverName,
|
|
424
|
+
listedTool.name,
|
|
425
|
+
progress,
|
|
426
|
+
onUpdate,
|
|
427
|
+
);
|
|
428
|
+
},
|
|
429
|
+
}
|
|
430
|
+
: {}),
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
return convertMcpToolResult(result);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
return textResult(
|
|
437
|
+
`MCP server "${serverName}" tool "${listedTool.name}" failed: ${getErrorMessage(error)}`,
|
|
438
|
+
true,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
return { tool, handler };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function emitMcpProgressUpdate(
|
|
447
|
+
serverName: string,
|
|
448
|
+
toolName: string,
|
|
449
|
+
progress: McpProgress,
|
|
450
|
+
onUpdate: ToolUpdateCallback,
|
|
451
|
+
): void {
|
|
452
|
+
const status =
|
|
453
|
+
progress.total != null
|
|
454
|
+
? `${progress.progress}/${progress.total}`
|
|
455
|
+
: `${progress.progress}`;
|
|
456
|
+
const suffix = progress.message ? ` — ${progress.message}` : "";
|
|
457
|
+
onUpdate(
|
|
458
|
+
textResult(
|
|
459
|
+
`MCP ${serverName}/${toolName} progress: ${status}${suffix}`,
|
|
460
|
+
false,
|
|
461
|
+
),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function convertMcpToolResult(result: McpCallToolResult): ToolExecResult {
|
|
466
|
+
if ("toolResult" in result) {
|
|
467
|
+
return textResult(formatUnknownValue(result.toolResult), false);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const content: ToolExecResult["content"] = [];
|
|
471
|
+
|
|
472
|
+
for (const item of result.content) {
|
|
473
|
+
switch (item.type) {
|
|
474
|
+
case "text":
|
|
475
|
+
content.push({ type: "text", text: item.text });
|
|
476
|
+
break;
|
|
477
|
+
case "image":
|
|
478
|
+
content.push({
|
|
479
|
+
type: "image",
|
|
480
|
+
data: item.data,
|
|
481
|
+
mimeType: item.mimeType,
|
|
482
|
+
});
|
|
483
|
+
break;
|
|
484
|
+
case "audio":
|
|
485
|
+
content.push({
|
|
486
|
+
type: "text",
|
|
487
|
+
text: `Audio output (${item.mimeType}) is not renderable in mini-coder tool results.`,
|
|
488
|
+
});
|
|
489
|
+
break;
|
|
490
|
+
case "resource":
|
|
491
|
+
content.push({
|
|
492
|
+
type: "text",
|
|
493
|
+
text: formatResourceContent(item.resource),
|
|
494
|
+
});
|
|
495
|
+
break;
|
|
496
|
+
case "resource_link":
|
|
497
|
+
content.push({
|
|
498
|
+
type: "text",
|
|
499
|
+
text: formatResourceLinkContent(item),
|
|
500
|
+
});
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (content.length === 0) {
|
|
506
|
+
if (result.structuredContent !== undefined) {
|
|
507
|
+
content.push({
|
|
508
|
+
type: "text",
|
|
509
|
+
text: formatUnknownValue(result.structuredContent),
|
|
510
|
+
});
|
|
511
|
+
} else {
|
|
512
|
+
content.push({
|
|
513
|
+
type: "text",
|
|
514
|
+
text: "MCP tool returned no content.",
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
content,
|
|
521
|
+
details:
|
|
522
|
+
result.structuredContent === undefined
|
|
523
|
+
? undefined
|
|
524
|
+
: { structuredContent: result.structuredContent },
|
|
525
|
+
isError: result.isError ?? false,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function formatResourceContent(
|
|
530
|
+
resource:
|
|
531
|
+
| {
|
|
532
|
+
uri: string;
|
|
533
|
+
text: string;
|
|
534
|
+
mimeType?: string;
|
|
535
|
+
}
|
|
536
|
+
| {
|
|
537
|
+
uri: string;
|
|
538
|
+
blob: string;
|
|
539
|
+
mimeType?: string;
|
|
540
|
+
},
|
|
541
|
+
): string {
|
|
542
|
+
if ("text" in resource) {
|
|
543
|
+
return resource.mimeType
|
|
544
|
+
? `Resource ${resource.uri} (${resource.mimeType})\n\n${resource.text}`
|
|
545
|
+
: `Resource ${resource.uri}\n\n${resource.text}`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return resource.mimeType
|
|
549
|
+
? `Resource blob ${resource.uri} (${resource.mimeType})`
|
|
550
|
+
: `Resource blob ${resource.uri}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function formatResourceLinkContent(item: {
|
|
554
|
+
uri: string;
|
|
555
|
+
name: string;
|
|
556
|
+
description?: string;
|
|
557
|
+
mimeType?: string;
|
|
558
|
+
size?: number;
|
|
559
|
+
title?: string;
|
|
560
|
+
}): string {
|
|
561
|
+
const lines = [
|
|
562
|
+
item.title ? `Resource link: ${item.title}` : `Resource link: ${item.name}`,
|
|
563
|
+
`URI: ${item.uri}`,
|
|
564
|
+
];
|
|
565
|
+
|
|
566
|
+
if (item.description) {
|
|
567
|
+
lines.push(`Description: ${item.description}`);
|
|
568
|
+
}
|
|
569
|
+
if (item.mimeType) {
|
|
570
|
+
lines.push(`MIME type: ${item.mimeType}`);
|
|
571
|
+
}
|
|
572
|
+
if (item.size != null) {
|
|
573
|
+
lines.push(`Size: ${item.size}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return lines.join("\n");
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function formatUnknownValue(value: unknown): string {
|
|
580
|
+
if (typeof value === "string") {
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
return JSON.stringify(value, null, 2);
|
|
586
|
+
} catch {
|
|
587
|
+
return String(value);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function validateMcpToolArgs<TParameters extends TSchema>(
|
|
592
|
+
tool: Tool<TParameters>,
|
|
593
|
+
args: Record<string, unknown>,
|
|
594
|
+
): Static<TParameters> {
|
|
595
|
+
return validateToolArguments(tool, {
|
|
596
|
+
type: "toolCall",
|
|
597
|
+
id: tool.name,
|
|
598
|
+
name: tool.name,
|
|
599
|
+
arguments: args,
|
|
600
|
+
} satisfies ToolCall) as Static<TParameters>;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function closeQuietly(transport: McpTransportLike): Promise<void> {
|
|
604
|
+
try {
|
|
605
|
+
await transport.close();
|
|
606
|
+
} catch {
|
|
607
|
+
// Best effort only.
|
|
608
|
+
}
|
|
609
|
+
}
|
package/src/prompt.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* System prompt construction.
|
|
3
3
|
*
|
|
4
4
|
* Assembles the full system prompt from the core prompt template plus
|
|
5
|
-
* dynamic context: AGENTS.md files, skill catalog,
|
|
6
|
-
*
|
|
5
|
+
* dynamic context: AGENTS.md files, the skill catalog, and the current
|
|
6
|
+
* environment block.
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
@@ -44,8 +44,6 @@ interface BuildSystemPromptOpts {
|
|
|
44
44
|
agentsMd?: AgentsMdFile[];
|
|
45
45
|
/** Discovered agent skills. */
|
|
46
46
|
skills?: Skill[];
|
|
47
|
-
/** Plugin system prompt suffixes. */
|
|
48
|
-
pluginSuffixes?: string[];
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
// ---------------------------------------------------------------------------
|
|
@@ -210,6 +208,8 @@ function buildCorePrompt(opts: BuildSystemPromptOpts): string {
|
|
|
210
208
|
|
|
211
209
|
lines.push(
|
|
212
210
|
`- Shell: ${opts.shell}. Use \`command -v <name>\` to check what is available to you; do not assume environment support.`,
|
|
211
|
+
"- Read: Read a text file from disk with offset/limit support.",
|
|
212
|
+
"- Grep: Search file contents with ripgrep-style options and structured results.",
|
|
213
213
|
"- Edit: Safe exact-text replacement in a single file.",
|
|
214
214
|
);
|
|
215
215
|
|
|
@@ -238,6 +238,12 @@ function buildCorePrompt(opts: BuildSystemPromptOpts): string {
|
|
|
238
238
|
"- Don't assume the environment supports all commands; check before using them.",
|
|
239
239
|
"- Avoid destructive commands that can discard changes or override edits.",
|
|
240
240
|
"",
|
|
241
|
+
"### Choosing tools:",
|
|
242
|
+
"",
|
|
243
|
+
"- Prefer `read` for reading file contents instead of `cat`, `sed`, `head`, or `tail`.",
|
|
244
|
+
"- Prefer `grep` for content search instead of raw `grep` / `rg`.",
|
|
245
|
+
"- Use shell `ls` and `fd` for lightweight exploration when you just need to inspect directories or discover candidate files.",
|
|
246
|
+
"",
|
|
241
247
|
"### Working with code:",
|
|
242
248
|
"",
|
|
243
249
|
"- Describe changes before implementing them",
|
|
@@ -279,7 +285,6 @@ function buildCorePrompt(opts: BuildSystemPromptOpts): string {
|
|
|
279
285
|
* 1. Core prompt template (including the current environment block)
|
|
280
286
|
* 2. AGENTS.md content (project-specific)
|
|
281
287
|
* 3. Skills catalog (XML)
|
|
282
|
-
* 4. Plugin suffixes
|
|
283
288
|
*
|
|
284
289
|
* @param opts - Prompt construction options.
|
|
285
290
|
* @returns The assembled system prompt string.
|
|
@@ -305,12 +310,5 @@ export function buildSystemPrompt(opts: BuildSystemPromptOpts): string {
|
|
|
305
310
|
if (catalog) sections.push(catalog);
|
|
306
311
|
}
|
|
307
312
|
|
|
308
|
-
// 4. Plugin suffixes
|
|
309
|
-
if (opts.pluginSuffixes) {
|
|
310
|
-
for (const suffix of opts.pluginSuffixes) {
|
|
311
|
-
sections.push(suffix);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
313
|
return sections.join("\n\n");
|
|
316
314
|
}
|