mono-pilot 0.2.9 → 0.2.10
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 +10 -5
- package/dist/src/extensions/session-hints.js +61 -35
- package/dist/src/extensions/user-message.js +24 -49
- package/dist/src/mcp/config.js +112 -0
- package/dist/src/mcp/protocol.js +164 -0
- package/dist/src/mcp/servers.js +90 -0
- package/dist/src/rules/discovery.js +41 -0
- package/dist/src/utils/mcp-client.js +32 -13
- package/dist/tools/README.md +1 -1
- package/dist/tools/call-mcp-tool.js +24 -104
- package/dist/tools/fetch-mcp-resource.js +28 -100
- package/dist/tools/list-mcp-resources.js +18 -58
- package/dist/tools/list-mcp-tools.js +18 -64
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, join, resolve } from "node:path";
|
|
5
|
+
export const RULES_RELATIVE_DIR = join(".pi", "rules");
|
|
6
|
+
/** List *.rule.txt full paths from a directory, sorted. Returns empty array if directory is missing. */
|
|
7
|
+
export async function listRuleFiles(dirPath) {
|
|
8
|
+
if (!existsSync(dirPath))
|
|
9
|
+
return [];
|
|
10
|
+
try {
|
|
11
|
+
const entries = await readdir(dirPath, { withFileTypes: true, encoding: "utf8" });
|
|
12
|
+
return entries
|
|
13
|
+
.filter((e) => e.isFile() && e.name.endsWith(".rule.txt"))
|
|
14
|
+
.map((e) => resolve(dirPath, e.name))
|
|
15
|
+
.sort((a, b) => a.localeCompare(b));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Discover rule files from project (.pi/rules/) and user (~/.pi/rules/) directories.
|
|
23
|
+
* Project rules take priority: a user rule with the same basename is excluded.
|
|
24
|
+
*/
|
|
25
|
+
export async function discoverRules(cwd) {
|
|
26
|
+
const projectDir = resolve(cwd, RULES_RELATIVE_DIR);
|
|
27
|
+
const userDir = resolve(homedir(), RULES_RELATIVE_DIR);
|
|
28
|
+
const [projectFiles, userFiles] = await Promise.all([listRuleFiles(projectDir), listRuleFiles(userDir)]);
|
|
29
|
+
// Project rules are processed first so they claim seenNames → user duplicates are dropped
|
|
30
|
+
const seenNames = new Set();
|
|
31
|
+
const dedupeByName = (files) => files.filter((filePath) => {
|
|
32
|
+
const name = basename(filePath, ".rule.txt");
|
|
33
|
+
if (seenNames.has(name))
|
|
34
|
+
return false;
|
|
35
|
+
seenNames.add(name);
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
const projectRules = dedupeByName(projectFiles);
|
|
39
|
+
const userRules = dedupeByName(userFiles);
|
|
40
|
+
return { userRules, projectRules };
|
|
41
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import process from "node:process";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
6
5
|
export const MCP_CONFIG_RELATIVE_PATH = join(".pi", "mcp.json");
|
|
7
6
|
export const MCP_PROTOCOL_VERSION = "2025-03-26";
|
|
8
7
|
export const MCP_CLIENT_NAME = "mono-pilot";
|
|
@@ -30,18 +29,38 @@ export function formatErrorMessage(error) {
|
|
|
30
29
|
return String(error);
|
|
31
30
|
}
|
|
32
31
|
export function resolveMcpConfigPath(workspaceCwd) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
const sources = resolveMcpConfigSources(workspaceCwd);
|
|
33
|
+
return sources[0]?.path;
|
|
34
|
+
}
|
|
35
|
+
export function getMcpConfigCandidates(workspaceCwd) {
|
|
36
|
+
return [resolve(workspaceCwd, MCP_CONFIG_RELATIVE_PATH), resolve(homedir(), MCP_CONFIG_RELATIVE_PATH)];
|
|
37
|
+
}
|
|
38
|
+
export function resolveMcpConfigSources(workspaceCwd) {
|
|
39
|
+
const projectPath = resolve(workspaceCwd, MCP_CONFIG_RELATIVE_PATH);
|
|
40
|
+
const userPath = resolve(homedir(), MCP_CONFIG_RELATIVE_PATH);
|
|
41
|
+
const sources = [];
|
|
42
|
+
if (existsSync(projectPath))
|
|
43
|
+
sources.push({ scope: "project", path: projectPath });
|
|
44
|
+
if (existsSync(userPath))
|
|
45
|
+
sources.push({ scope: "user", path: userPath });
|
|
46
|
+
return sources;
|
|
47
|
+
}
|
|
48
|
+
export async function loadMcpConfig(workspaceCwd) {
|
|
49
|
+
const sources = resolveMcpConfigSources(workspaceCwd);
|
|
50
|
+
if (sources.length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const servers = {};
|
|
53
|
+
const sourceByServer = {};
|
|
54
|
+
for (const source of sources) {
|
|
55
|
+
const parsed = await parseMcpConfig(source.path);
|
|
56
|
+
for (const [serverName, serverConfig] of Object.entries(parsed)) {
|
|
57
|
+
if (sourceByServer[serverName])
|
|
58
|
+
continue;
|
|
59
|
+
servers[serverName] = serverConfig;
|
|
60
|
+
sourceByServer[serverName] = source;
|
|
61
|
+
}
|
|
43
62
|
}
|
|
44
|
-
return
|
|
63
|
+
return { servers, sources, sourceByServer };
|
|
45
64
|
}
|
|
46
65
|
export async function parseMcpConfig(configPath) {
|
|
47
66
|
const rawText = await readFile(configPath, "utf-8");
|
package/dist/tools/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Tool descriptions are now loaded by the tool implementation and exposed via the
|
|
|
36
36
|
- `subagent.ts` / `subagent-description.md` (`Subagent`)
|
|
37
37
|
- Launch delegated subagent subprocesses with foreground/background and parallel orchestration.
|
|
38
38
|
- `list-mcp-resources.ts` (`ListMcpResources`)
|
|
39
|
-
- Read `.pi/mcp.json`
|
|
39
|
+
- Read `.pi/mcp.json` (project) and `~/.pi/mcp.json` (user) configs to list MCP resources with server metadata.
|
|
40
40
|
- `fetch-mcp-resource.ts` (`FetchMcpResource`)
|
|
41
41
|
- Fetch a specific MCP resource by server + URI, optionally writing to workspace.
|
|
42
42
|
- `list-mcp-tools.ts` (`ListMcpTools`)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
1
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, keyHint, truncateTail, } from "@mariozechner/pi-coding-agent";
|
|
4
2
|
import { Text } from "@mariozechner/pi-tui";
|
|
5
3
|
import { Type } from "@sinclair/typebox";
|
|
6
|
-
import {
|
|
4
|
+
import { McpServerError, resolveTargetServer } from "../src/mcp/servers.js";
|
|
5
|
+
import { createRpcRequestId, formatJsonRpcError, initializeMcpSession, postJsonRpcRequest } from "../src/mcp/protocol.js";
|
|
6
|
+
import { formatErrorMessage, getHeaderKeys, isRecord, toBoolean, toNonEmptyString } from "../src/mcp/config.js";
|
|
7
7
|
const DESCRIPTION = `Call an MCP tool by server identifier and tool name with arbitrary JSON arguments. IMPORTANT: Always read the tool's schema/descriptor BEFORE calling to ensure correct parameters.
|
|
8
8
|
|
|
9
9
|
Example:
|
|
@@ -45,10 +45,7 @@ async function callRemoteMcpTool(options) {
|
|
|
45
45
|
if (!callBody || callBody.result === undefined) {
|
|
46
46
|
throw new Error("MCP tools/call returned no result.");
|
|
47
47
|
}
|
|
48
|
-
return {
|
|
49
|
-
result: callBody.result,
|
|
50
|
-
sessionId,
|
|
51
|
-
};
|
|
48
|
+
return { result: callBody.result, sessionId };
|
|
52
49
|
}
|
|
53
50
|
function normalizeServerName(value) {
|
|
54
51
|
const normalized = value.trim();
|
|
@@ -142,19 +139,12 @@ function formatToolCallResultOutput(server, toolName, result) {
|
|
|
142
139
|
lines.push(JSON.stringify(result, null, 2));
|
|
143
140
|
lines.push("```");
|
|
144
141
|
}
|
|
145
|
-
return {
|
|
146
|
-
text: lines.join("\n"),
|
|
147
|
-
contentItems,
|
|
148
|
-
isError,
|
|
149
|
-
hasStructuredContent,
|
|
150
|
-
};
|
|
142
|
+
return { text: lines.join("\n"), contentItems, isError, hasStructuredContent };
|
|
151
143
|
}
|
|
152
144
|
function getCollapsedResultText(text, expanded) {
|
|
153
|
-
if (text.length === 0)
|
|
145
|
+
if (text.length === 0)
|
|
154
146
|
return { output: text, remaining: 0 };
|
|
155
|
-
}
|
|
156
147
|
const lines = text.split("\n");
|
|
157
|
-
// Use 20 lines as the standard collapse threshold
|
|
158
148
|
const MAX_COLLAPSED_RESULT_LINES = 20;
|
|
159
149
|
if (expanded || lines.length <= MAX_COLLAPSED_RESULT_LINES) {
|
|
160
150
|
return { output: text, remaining: 0 };
|
|
@@ -217,90 +207,21 @@ export default function callMcpToolExtension(pi) {
|
|
|
217
207
|
isError: true,
|
|
218
208
|
};
|
|
219
209
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const workspaceCandidate = resolve(ctx.cwd, MCP_CONFIG_RELATIVE_PATH);
|
|
223
|
-
const homeCandidate = resolve(homedir(), MCP_CONFIG_RELATIVE_PATH);
|
|
224
|
-
const message = `MCP config not found. Checked:\n- ${workspaceCandidate}\n- ${homeCandidate}`;
|
|
225
|
-
return {
|
|
226
|
-
content: [{ type: "text", text: message }],
|
|
227
|
-
details: {
|
|
228
|
-
server: serverName,
|
|
229
|
-
tool_name: toolName,
|
|
230
|
-
error: message,
|
|
231
|
-
},
|
|
232
|
-
isError: true,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
let servers;
|
|
210
|
+
let server;
|
|
211
|
+
let configPaths;
|
|
236
212
|
try {
|
|
237
|
-
|
|
213
|
+
const result = await resolveTargetServer(ctx.cwd, serverName);
|
|
214
|
+
server = result.server;
|
|
215
|
+
configPaths = result.configPaths;
|
|
238
216
|
}
|
|
239
217
|
catch (error) {
|
|
240
218
|
const message = formatErrorMessage(error);
|
|
219
|
+
const transport = error instanceof McpServerError ? error.transport : undefined;
|
|
220
|
+
const paths = error instanceof McpServerError ? error.configPaths : undefined;
|
|
241
221
|
return {
|
|
242
222
|
content: [{ type: "text", text: message }],
|
|
243
223
|
details: {
|
|
244
|
-
|
|
245
|
-
server: serverName,
|
|
246
|
-
tool_name: toolName,
|
|
247
|
-
error: message,
|
|
248
|
-
},
|
|
249
|
-
isError: true,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
const serverConfig = servers[serverName];
|
|
253
|
-
if (!serverConfig) {
|
|
254
|
-
const message = `MCP server '${serverName}' not found in ${configPath}.`;
|
|
255
|
-
return {
|
|
256
|
-
content: [{ type: "text", text: message }],
|
|
257
|
-
details: {
|
|
258
|
-
config_path: configPath,
|
|
259
|
-
server: serverName,
|
|
260
|
-
tool_name: toolName,
|
|
261
|
-
error: message,
|
|
262
|
-
},
|
|
263
|
-
isError: true,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
if (!isServerEnabled(serverConfig)) {
|
|
267
|
-
const message = `MCP server '${serverName}' is disabled in config.`;
|
|
268
|
-
return {
|
|
269
|
-
content: [{ type: "text", text: message }],
|
|
270
|
-
details: {
|
|
271
|
-
config_path: configPath,
|
|
272
|
-
server: serverName,
|
|
273
|
-
tool_name: toolName,
|
|
274
|
-
transport: inferTransport(serverConfig),
|
|
275
|
-
error: message,
|
|
276
|
-
},
|
|
277
|
-
isError: true,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
const transport = inferTransport(serverConfig);
|
|
281
|
-
if (transport === "stdio") {
|
|
282
|
-
const command = toNonEmptyString(serverConfig.command);
|
|
283
|
-
const message = `MCP stdio transport is not supported yet in CallMcpTool.` +
|
|
284
|
-
(command ? ` Configured command: ${command}` : "");
|
|
285
|
-
return {
|
|
286
|
-
content: [{ type: "text", text: message }],
|
|
287
|
-
details: {
|
|
288
|
-
config_path: configPath,
|
|
289
|
-
server: serverName,
|
|
290
|
-
tool_name: toolName,
|
|
291
|
-
transport,
|
|
292
|
-
error: message,
|
|
293
|
-
},
|
|
294
|
-
isError: true,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
const serverUrl = toNonEmptyString(serverConfig.url);
|
|
298
|
-
if (!serverUrl) {
|
|
299
|
-
const message = `MCP server '${serverName}' is missing a remote URL.`;
|
|
300
|
-
return {
|
|
301
|
-
content: [{ type: "text", text: message }],
|
|
302
|
-
details: {
|
|
303
|
-
config_path: configPath,
|
|
224
|
+
config_paths: paths,
|
|
304
225
|
server: serverName,
|
|
305
226
|
tool_name: toolName,
|
|
306
227
|
transport,
|
|
@@ -309,11 +230,10 @@ export default function callMcpToolExtension(pi) {
|
|
|
309
230
|
isError: true,
|
|
310
231
|
};
|
|
311
232
|
}
|
|
312
|
-
const headers = extractStringHeaders(serverConfig.headers);
|
|
313
233
|
try {
|
|
314
234
|
const remoteResult = await callRemoteMcpTool({
|
|
315
|
-
serverUrl,
|
|
316
|
-
serverHeaders: headers,
|
|
235
|
+
serverUrl: server.url,
|
|
236
|
+
serverHeaders: server.headers,
|
|
317
237
|
toolName,
|
|
318
238
|
argumentsValue,
|
|
319
239
|
signal,
|
|
@@ -331,12 +251,12 @@ export default function callMcpToolExtension(pi) {
|
|
|
331
251
|
return {
|
|
332
252
|
content: [{ type: "text", text: output }],
|
|
333
253
|
details: {
|
|
334
|
-
|
|
254
|
+
config_paths: configPaths,
|
|
335
255
|
server: serverName,
|
|
336
256
|
tool_name: toolName,
|
|
337
|
-
transport,
|
|
338
|
-
server_url:
|
|
339
|
-
header_keys: getHeaderKeys(headers),
|
|
257
|
+
transport: "remote",
|
|
258
|
+
server_url: server.url,
|
|
259
|
+
header_keys: getHeaderKeys(server.headers),
|
|
340
260
|
session_id: remoteResult.sessionId,
|
|
341
261
|
content_items: formatted.contentItems,
|
|
342
262
|
is_error: formatted.isError,
|
|
@@ -351,12 +271,12 @@ export default function callMcpToolExtension(pi) {
|
|
|
351
271
|
return {
|
|
352
272
|
content: [{ type: "text", text: `CallMcpTool failed: ${message}` }],
|
|
353
273
|
details: {
|
|
354
|
-
|
|
274
|
+
config_paths: configPaths,
|
|
355
275
|
server: serverName,
|
|
356
276
|
tool_name: toolName,
|
|
357
|
-
transport,
|
|
358
|
-
server_url:
|
|
359
|
-
header_keys: getHeaderKeys(headers),
|
|
277
|
+
transport: "remote",
|
|
278
|
+
server_url: server.url,
|
|
279
|
+
header_keys: getHeaderKeys(server.headers),
|
|
360
280
|
error: message,
|
|
361
281
|
},
|
|
362
282
|
isError: true,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
|
-
import {
|
|
4
|
+
import { McpServerError, resolveTargetServer } from "../src/mcp/servers.js";
|
|
5
|
+
import { createRpcRequestId, formatJsonRpcError, initializeMcpSession, postJsonRpcRequest } from "../src/mcp/protocol.js";
|
|
6
|
+
import { formatErrorMessage, isRecord, toNonEmptyString } from "../src/mcp/config.js";
|
|
5
7
|
const DESCRIPTION = `Reads a specific resource from an MCP server, identified by server name and resource URI. Optionally, set downloadPath (relative to the workspace) to save the resource to disk; when set, the resource will be downloaded and not returned to the model.`;
|
|
6
8
|
const fetchMcpResourceSchema = Type.Object({
|
|
7
9
|
server: Type.String({ description: "The MCP server identifier" }),
|
|
@@ -19,9 +21,7 @@ async function fetchRemoteMcpResource(options) {
|
|
|
19
21
|
jsonrpc: "2.0",
|
|
20
22
|
id: createRpcRequestId(`${options.toolCallId}:resources.read`),
|
|
21
23
|
method: "resources/read",
|
|
22
|
-
params: {
|
|
23
|
-
uri: options.uri,
|
|
24
|
-
},
|
|
24
|
+
params: { uri: options.uri },
|
|
25
25
|
},
|
|
26
26
|
parentSignal: options.signal,
|
|
27
27
|
sessionId,
|
|
@@ -104,75 +104,21 @@ export default function fetchMcpResourceExtension(pi) {
|
|
|
104
104
|
isError: true,
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const workspaceCandidate = resolve(ctx.cwd, MCP_CONFIG_RELATIVE_PATH);
|
|
110
|
-
const homeCandidate = resolve(homedir(), MCP_CONFIG_RELATIVE_PATH);
|
|
111
|
-
const message = `MCP config not found. Checked:\n- ${workspaceCandidate}\n- ${homeCandidate}`;
|
|
112
|
-
return {
|
|
113
|
-
content: [{ type: "text", text: message }],
|
|
114
|
-
details: {
|
|
115
|
-
server: serverName,
|
|
116
|
-
uri,
|
|
117
|
-
error: message,
|
|
118
|
-
},
|
|
119
|
-
isError: true,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
let servers;
|
|
107
|
+
let server;
|
|
108
|
+
let configPaths;
|
|
123
109
|
try {
|
|
124
|
-
|
|
110
|
+
const result = await resolveTargetServer(ctx.cwd, serverName);
|
|
111
|
+
server = result.server;
|
|
112
|
+
configPaths = result.configPaths;
|
|
125
113
|
}
|
|
126
114
|
catch (error) {
|
|
127
115
|
const message = formatErrorMessage(error);
|
|
116
|
+
const transport = error instanceof McpServerError ? error.transport : undefined;
|
|
117
|
+
const paths = error instanceof McpServerError ? error.configPaths : undefined;
|
|
128
118
|
return {
|
|
129
119
|
content: [{ type: "text", text: message }],
|
|
130
120
|
details: {
|
|
131
|
-
|
|
132
|
-
server: serverName,
|
|
133
|
-
uri,
|
|
134
|
-
error: message,
|
|
135
|
-
},
|
|
136
|
-
isError: true,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
const serverConfig = servers[serverName];
|
|
140
|
-
if (!serverConfig) {
|
|
141
|
-
const message = `MCP server '${serverName}' not found in ${configPath}.`;
|
|
142
|
-
return {
|
|
143
|
-
content: [{ type: "text", text: message }],
|
|
144
|
-
details: {
|
|
145
|
-
config_path: configPath,
|
|
146
|
-
server: serverName,
|
|
147
|
-
uri,
|
|
148
|
-
error: message,
|
|
149
|
-
},
|
|
150
|
-
isError: true,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
if (!isServerEnabled(serverConfig)) {
|
|
154
|
-
const message = `MCP server '${serverName}' is disabled in config.`;
|
|
155
|
-
return {
|
|
156
|
-
content: [{ type: "text", text: message }],
|
|
157
|
-
details: {
|
|
158
|
-
config_path: configPath,
|
|
159
|
-
server: serverName,
|
|
160
|
-
uri,
|
|
161
|
-
transport: inferTransport(serverConfig),
|
|
162
|
-
error: message,
|
|
163
|
-
},
|
|
164
|
-
isError: true,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
const transport = inferTransport(serverConfig);
|
|
168
|
-
if (transport === "stdio") {
|
|
169
|
-
const command = toNonEmptyString(serverConfig.command);
|
|
170
|
-
const message = `MCP stdio transport is not supported yet in FetchMcpResource.` +
|
|
171
|
-
(command ? ` Configured command: ${command}` : "");
|
|
172
|
-
return {
|
|
173
|
-
content: [{ type: "text", text: message }],
|
|
174
|
-
details: {
|
|
175
|
-
config_path: configPath,
|
|
121
|
+
config_paths: paths,
|
|
176
122
|
server: serverName,
|
|
177
123
|
uri,
|
|
178
124
|
transport,
|
|
@@ -181,26 +127,10 @@ export default function fetchMcpResourceExtension(pi) {
|
|
|
181
127
|
isError: true,
|
|
182
128
|
};
|
|
183
129
|
}
|
|
184
|
-
const serverUrl = toNonEmptyString(serverConfig.url);
|
|
185
|
-
if (!serverUrl) {
|
|
186
|
-
const message = `MCP server '${serverName}' is missing a remote URL.`;
|
|
187
|
-
return {
|
|
188
|
-
content: [{ type: "text", text: message }],
|
|
189
|
-
details: {
|
|
190
|
-
config_path: configPath,
|
|
191
|
-
server: serverName,
|
|
192
|
-
uri,
|
|
193
|
-
transport,
|
|
194
|
-
error: message,
|
|
195
|
-
},
|
|
196
|
-
isError: true,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
const headers = extractStringHeaders(serverConfig.headers);
|
|
200
130
|
try {
|
|
201
131
|
const contents = await fetchRemoteMcpResource({
|
|
202
|
-
serverUrl,
|
|
203
|
-
serverHeaders: headers,
|
|
132
|
+
serverUrl: server.url,
|
|
133
|
+
serverHeaders: server.headers,
|
|
204
134
|
uri,
|
|
205
135
|
signal,
|
|
206
136
|
toolCallId,
|
|
@@ -209,20 +139,19 @@ export default function fetchMcpResourceExtension(pi) {
|
|
|
209
139
|
return {
|
|
210
140
|
content: [{ type: "text", text: `Resource '${uri}' returned no content.` }],
|
|
211
141
|
details: {
|
|
212
|
-
|
|
142
|
+
config_paths: configPaths,
|
|
213
143
|
server: serverName,
|
|
214
144
|
uri,
|
|
215
|
-
transport,
|
|
216
|
-
server_url:
|
|
145
|
+
transport: "remote",
|
|
146
|
+
server_url: server.url,
|
|
217
147
|
contents_count: 0,
|
|
218
148
|
},
|
|
219
149
|
};
|
|
220
150
|
}
|
|
221
151
|
if (downloadPath) {
|
|
222
|
-
//
|
|
152
|
+
// Only the first content item is downloaded
|
|
223
153
|
const contentToDownload = contents[0];
|
|
224
154
|
const absoluteTarget = resolve(ctx.cwd, downloadPath);
|
|
225
|
-
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
226
155
|
await mkdir(dirname(absoluteTarget), { recursive: true });
|
|
227
156
|
let bytesWritten = 0;
|
|
228
157
|
if (contentToDownload.blob !== undefined) {
|
|
@@ -245,17 +174,16 @@ export default function fetchMcpResourceExtension(pi) {
|
|
|
245
174
|
},
|
|
246
175
|
],
|
|
247
176
|
details: {
|
|
248
|
-
|
|
177
|
+
config_paths: configPaths,
|
|
249
178
|
server: serverName,
|
|
250
179
|
uri,
|
|
251
|
-
transport,
|
|
252
|
-
server_url:
|
|
180
|
+
transport: "remote",
|
|
181
|
+
server_url: server.url,
|
|
253
182
|
contents_count: contents.length,
|
|
254
183
|
downloaded_to: downloadPath,
|
|
255
184
|
},
|
|
256
185
|
};
|
|
257
186
|
}
|
|
258
|
-
// Return contents directly
|
|
259
187
|
const lines = [];
|
|
260
188
|
for (let i = 0; i < contents.length; i++) {
|
|
261
189
|
const item = contents[i];
|
|
@@ -279,11 +207,11 @@ export default function fetchMcpResourceExtension(pi) {
|
|
|
279
207
|
return {
|
|
280
208
|
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
281
209
|
details: {
|
|
282
|
-
|
|
210
|
+
config_paths: configPaths,
|
|
283
211
|
server: serverName,
|
|
284
212
|
uri,
|
|
285
|
-
transport,
|
|
286
|
-
server_url:
|
|
213
|
+
transport: "remote",
|
|
214
|
+
server_url: server.url,
|
|
287
215
|
contents_count: contents.length,
|
|
288
216
|
},
|
|
289
217
|
};
|
|
@@ -293,11 +221,11 @@ export default function fetchMcpResourceExtension(pi) {
|
|
|
293
221
|
return {
|
|
294
222
|
content: [{ type: "text", text: `FetchMcpResource failed: ${message}` }],
|
|
295
223
|
details: {
|
|
296
|
-
|
|
224
|
+
config_paths: configPaths,
|
|
297
225
|
server: serverName,
|
|
298
226
|
uri,
|
|
299
|
-
transport,
|
|
300
|
-
server_url:
|
|
227
|
+
transport: "remote",
|
|
228
|
+
server_url: server.url,
|
|
301
229
|
error: message,
|
|
302
230
|
},
|
|
303
231
|
isError: true,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
1
|
import { Type } from "@sinclair/typebox";
|
|
4
|
-
import {
|
|
2
|
+
import { resolveTargetServers } from "../src/mcp/servers.js";
|
|
3
|
+
import { createRpcRequestId, formatJsonRpcError, initializeMcpSession, postJsonRpcRequest } from "../src/mcp/protocol.js";
|
|
4
|
+
import { formatErrorMessage, isRecord, toNonEmptyString } from "../src/mcp/config.js";
|
|
5
5
|
const DESCRIPTION = `List available resources from configured MCP servers. Each returned resource will include all standard MCP resource fields plus a 'server' field indicating which server the resource belongs to. MCP resources are _not_ the same as tools, so don't call this function to discover MCP tools.`;
|
|
6
6
|
const listMcpResourcesSchema = Type.Object({
|
|
7
7
|
server: Type.Optional(Type.String({
|
|
@@ -49,12 +49,9 @@ async function listRemoteMcpResources(options) {
|
|
|
49
49
|
mimeType: toNonEmptyString(raw.mimeType),
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
-
return {
|
|
53
|
-
resources,
|
|
54
|
-
nextCursor: toNonEmptyString(resourcesBody.result.nextCursor),
|
|
55
|
-
};
|
|
52
|
+
return { resources, nextCursor: toNonEmptyString(resourcesBody.result.nextCursor) };
|
|
56
53
|
}
|
|
57
|
-
function
|
|
54
|
+
function normalizeOptionalString(value) {
|
|
58
55
|
if (typeof value !== "string")
|
|
59
56
|
return undefined;
|
|
60
57
|
const normalized = value.trim();
|
|
@@ -67,9 +64,13 @@ export default function listMcpResourcesExtension(pi) {
|
|
|
67
64
|
description: DESCRIPTION,
|
|
68
65
|
parameters: listMcpResourcesSchema,
|
|
69
66
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
70
|
-
|
|
67
|
+
const serverFilter = normalizeOptionalString(params.server);
|
|
68
|
+
let targetServers;
|
|
69
|
+
let sources;
|
|
71
70
|
try {
|
|
72
|
-
|
|
71
|
+
const result = await resolveTargetServers(ctx.cwd, serverFilter);
|
|
72
|
+
targetServers = result.servers;
|
|
73
|
+
sources = result.sources;
|
|
73
74
|
}
|
|
74
75
|
catch (error) {
|
|
75
76
|
const message = formatErrorMessage(error);
|
|
@@ -79,49 +80,6 @@ export default function listMcpResourcesExtension(pi) {
|
|
|
79
80
|
isError: true,
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
|
-
const configPath = resolveMcpConfigPath(ctx.cwd);
|
|
83
|
-
if (!configPath) {
|
|
84
|
-
const workspaceCandidate = resolve(ctx.cwd, MCP_CONFIG_RELATIVE_PATH);
|
|
85
|
-
const homeCandidate = resolve(homedir(), MCP_CONFIG_RELATIVE_PATH);
|
|
86
|
-
const message = `MCP config not found. Checked:\n- ${workspaceCandidate}\n- ${homeCandidate}`;
|
|
87
|
-
return {
|
|
88
|
-
content: [{ type: "text", text: message }],
|
|
89
|
-
details: { error: message },
|
|
90
|
-
isError: true,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
let servers;
|
|
94
|
-
try {
|
|
95
|
-
servers = await parseMcpConfig(configPath);
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
const message = formatErrorMessage(error);
|
|
99
|
-
return {
|
|
100
|
-
content: [{ type: "text", text: message }],
|
|
101
|
-
details: {
|
|
102
|
-
config_path: configPath,
|
|
103
|
-
error: message,
|
|
104
|
-
},
|
|
105
|
-
isError: true,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
const targetServers = [];
|
|
109
|
-
for (const [serverName, serverConfig] of Object.entries(servers)) {
|
|
110
|
-
if (serverFilter && serverName !== serverFilter)
|
|
111
|
-
continue;
|
|
112
|
-
if (!isServerEnabled(serverConfig))
|
|
113
|
-
continue;
|
|
114
|
-
if (inferTransport(serverConfig) !== "remote")
|
|
115
|
-
continue;
|
|
116
|
-
const serverUrl = toNonEmptyString(serverConfig.url);
|
|
117
|
-
if (!serverUrl)
|
|
118
|
-
continue;
|
|
119
|
-
targetServers.push({
|
|
120
|
-
name: serverName,
|
|
121
|
-
url: serverUrl,
|
|
122
|
-
headers: extractStringHeaders(serverConfig.headers),
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
83
|
if (targetServers.length === 0) {
|
|
126
84
|
const message = serverFilter
|
|
127
85
|
? `No active remote MCP server found matching '${serverFilter}'.`
|
|
@@ -129,13 +87,16 @@ export default function listMcpResourcesExtension(pi) {
|
|
|
129
87
|
return {
|
|
130
88
|
content: [{ type: "text", text: message }],
|
|
131
89
|
details: {
|
|
132
|
-
|
|
90
|
+
config_paths: sources.map((s) => s.path),
|
|
133
91
|
servers_matched: 0,
|
|
134
92
|
},
|
|
135
93
|
};
|
|
136
94
|
}
|
|
137
95
|
const lines = [];
|
|
138
|
-
lines.push(
|
|
96
|
+
lines.push("MCP config:");
|
|
97
|
+
for (const source of sources) {
|
|
98
|
+
lines.push(`- ${source.scope}: ${source.path}`);
|
|
99
|
+
}
|
|
139
100
|
lines.push(`Servers matched: ${targetServers.length}`);
|
|
140
101
|
if (serverFilter)
|
|
141
102
|
lines.push(`Server filter: ${serverFilter}`);
|
|
@@ -162,8 +123,7 @@ export default function listMcpResourcesExtension(pi) {
|
|
|
162
123
|
if (resource.mimeType)
|
|
163
124
|
lines.push(` mimeType: ${resource.mimeType}`);
|
|
164
125
|
if (resource.description) {
|
|
165
|
-
const
|
|
166
|
-
for (const descLine of descLines) {
|
|
126
|
+
for (const descLine of resource.description.split("\n")) {
|
|
167
127
|
lines.push(` ${descLine}`);
|
|
168
128
|
}
|
|
169
129
|
}
|
|
@@ -180,7 +140,7 @@ export default function listMcpResourcesExtension(pi) {
|
|
|
180
140
|
return {
|
|
181
141
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
182
142
|
details: {
|
|
183
|
-
|
|
143
|
+
config_paths: sources.map((s) => s.path),
|
|
184
144
|
servers_matched: targetServers.length,
|
|
185
145
|
servers_queried: targetServers.length,
|
|
186
146
|
servers_failed: serversFailed,
|