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.
@@ -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 { isAbsolute, join, resolve } from "node:path";
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 envOverride = toNonEmptyString(process.env.MONOPILOT_MCP_CONFIG);
34
- const candidates = [];
35
- if (envOverride) {
36
- candidates.push(isAbsolute(envOverride) ? resolve(envOverride) : resolve(workspaceCwd, envOverride));
37
- }
38
- candidates.push(resolve(workspaceCwd, MCP_CONFIG_RELATIVE_PATH));
39
- candidates.push(resolve(homedir(), MCP_CONFIG_RELATIVE_PATH));
40
- for (const candidate of candidates) {
41
- if (existsSync(candidate))
42
- return candidate;
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 undefined;
63
+ return { servers, sources, sourceByServer };
45
64
  }
46
65
  export async function parseMcpConfig(configPath) {
47
66
  const rawText = await readFile(configPath, "utf-8");
@@ -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` config and list MCP resources with server metadata.
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 { createRpcRequestId, extractStringHeaders, formatErrorMessage, formatJsonRpcError, getHeaderKeys, inferTransport, isRecord, isServerEnabled, MCP_CONFIG_RELATIVE_PATH, parseMcpConfig, postJsonRpcRequest, resolveMcpConfigPath, toBoolean, toNonEmptyString, initializeMcpSession, } from "../src/utils/mcp-client.js";
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
- const configPath = resolveMcpConfigPath(ctx.cwd);
221
- if (!configPath) {
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
- servers = await parseMcpConfig(configPath);
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
- config_path: configPath,
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
- config_path: configPath,
254
+ config_paths: configPaths,
335
255
  server: serverName,
336
256
  tool_name: toolName,
337
- transport,
338
- server_url: serverUrl,
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
- config_path: configPath,
274
+ config_paths: configPaths,
355
275
  server: serverName,
356
276
  tool_name: toolName,
357
- transport,
358
- server_url: serverUrl,
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 { homedir } from "node:os";
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 { createRpcRequestId, extractStringHeaders, formatErrorMessage, formatJsonRpcError, inferTransport, isRecord, isServerEnabled, MCP_CONFIG_RELATIVE_PATH, parseMcpConfig, postJsonRpcRequest, resolveMcpConfigPath, toNonEmptyString, initializeMcpSession, } from "../src/utils/mcp-client.js";
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
- const configPath = resolveMcpConfigPath(ctx.cwd);
108
- if (!configPath) {
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
- servers = await parseMcpConfig(configPath);
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
- config_path: configPath,
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
- config_path: configPath,
142
+ config_paths: configPaths,
213
143
  server: serverName,
214
144
  uri,
215
- transport,
216
- server_url: serverUrl,
145
+ transport: "remote",
146
+ server_url: server.url,
217
147
  contents_count: 0,
218
148
  },
219
149
  };
220
150
  }
221
151
  if (downloadPath) {
222
- // We only support downloading the first content item currently
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
- config_path: configPath,
177
+ config_paths: configPaths,
249
178
  server: serverName,
250
179
  uri,
251
- transport,
252
- server_url: serverUrl,
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
- config_path: configPath,
210
+ config_paths: configPaths,
283
211
  server: serverName,
284
212
  uri,
285
- transport,
286
- server_url: serverUrl,
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
- config_path: configPath,
224
+ config_paths: configPaths,
297
225
  server: serverName,
298
226
  uri,
299
- transport,
300
- server_url: serverUrl,
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 { createRpcRequestId, extractStringHeaders, formatErrorMessage, formatJsonRpcError, inferTransport, isRecord, isServerEnabled, MCP_CONFIG_RELATIVE_PATH, parseMcpConfig, postJsonRpcRequest, resolveMcpConfigPath, toNonEmptyString, initializeMcpSession, } from "../src/utils/mcp-client.js";
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 normalizeServerFilter(value) {
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
- let serverFilter;
67
+ const serverFilter = normalizeOptionalString(params.server);
68
+ let targetServers;
69
+ let sources;
71
70
  try {
72
- serverFilter = normalizeServerFilter(params.server);
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
- config_path: configPath,
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(`MCP config: ${configPath}`);
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 descLines = resource.description.split("\n");
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
- config_path: configPath,
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,