pi-mcp-adapter 2.26.1 → 2.28.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.
Files changed (67) hide show
  1. package/CHANGELOG.md +68 -7
  2. package/README.md +73 -4
  3. package/cli.js +163 -1
  4. package/commands.ts +106 -24
  5. package/config.ts +60 -12
  6. package/direct-tools.ts +107 -95
  7. package/dist/agent-dir.d.ts +10 -0
  8. package/dist/agent-dir.js +63 -0
  9. package/dist/agent-dir.js.map +1 -0
  10. package/dist/agent-plugin-loader.d.ts +8 -0
  11. package/dist/agent-plugin-loader.js +356 -0
  12. package/dist/agent-plugin-loader.js.map +1 -0
  13. package/dist/config.d.ts +123 -0
  14. package/dist/config.js +1077 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/metadata-cache.d.ts +19 -0
  17. package/dist/metadata-cache.js +319 -0
  18. package/dist/metadata-cache.js.map +1 -0
  19. package/dist/package-mcp-loader.d.ts +2 -0
  20. package/dist/package-mcp-loader.js +156 -0
  21. package/dist/package-mcp-loader.js.map +1 -0
  22. package/dist/resource-tools.d.ts +1 -0
  23. package/dist/resource-tools.js +15 -0
  24. package/dist/resource-tools.js.map +1 -0
  25. package/dist/types.d.ts +564 -0
  26. package/dist/types.js +287 -0
  27. package/dist/types.js.map +1 -0
  28. package/dist/ui-app-bridge-helpers.d.ts +6 -0
  29. package/dist/ui-app-bridge-helpers.js +37 -0
  30. package/dist/ui-app-bridge-helpers.js.map +1 -0
  31. package/dist/ui-stream-types.d.ts +104 -0
  32. package/dist/ui-stream-types.js +59 -0
  33. package/dist/ui-stream-types.js.map +1 -0
  34. package/dist/ui-tool-visibility.d.ts +4 -0
  35. package/dist/ui-tool-visibility.js +27 -0
  36. package/dist/ui-tool-visibility.js.map +1 -0
  37. package/dist/utils.d.ts +32 -0
  38. package/dist/utils.js +338 -0
  39. package/dist/utils.js.map +1 -0
  40. package/failure-backoff.ts +23 -0
  41. package/index.ts +325 -60
  42. package/init.ts +46 -31
  43. package/lifecycle.ts +89 -23
  44. package/mcp-auth-flow.ts +39 -6
  45. package/mcp-bearer-store.ts +387 -0
  46. package/mcp-callback-server.ts +27 -3
  47. package/mcp-code.ts +6 -0
  48. package/mcp-oauth-provider.ts +66 -16
  49. package/mcp-output-guard.ts +95 -7
  50. package/mcp-panel.ts +42 -13
  51. package/mcp-probe.ts +18 -4
  52. package/mcp-references.ts +368 -0
  53. package/mcp-status.ts +8 -19
  54. package/metadata-cache.ts +52 -33
  55. package/namespace-tools.ts +276 -0
  56. package/package-mcp-loader.ts +168 -0
  57. package/package.json +26 -6
  58. package/proxy-modes.ts +71 -14
  59. package/request-headers-command.ts +31 -37
  60. package/search-ranking.ts +94 -22
  61. package/server-manager.ts +86 -17
  62. package/state.ts +1 -3
  63. package/tool-result-renderer.ts +16 -2
  64. package/types.ts +50 -1
  65. package/ui-server.ts +21 -35
  66. package/ui-session.ts +2 -1
  67. package/utils.ts +91 -19
package/config.ts CHANGED
@@ -6,6 +6,7 @@ import { parse as parseToml } from "smol-toml";
6
6
  import stripJsonComments from "strip-json-comments";
7
7
  import { getAgentPath, getConfigDirName } from "./agent-dir.ts";
8
8
  import { getAgentPluginSummaries, loadAgentPluginConfigs, type AgentPluginSummary } from "./agent-plugin-loader.ts";
9
+ import { loadPackageMcpConfigs } from "./package-mcp-loader.ts";
9
10
  import { isServerDisabled, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
10
11
  import { toStringRecord } from "./utils.ts";
11
12
 
@@ -189,6 +190,7 @@ export function getConfigDiscoveryPaths(overridePath?: string, cwd = process.cwd
189
190
  }
190
191
 
191
192
  export function findAvailableImportConfigs(cwd = process.cwd()): DiscoveredImportConfig[] {
193
+ if (isExclusiveConfigMode()) return [];
192
194
  const discovered: DiscoveredImportConfig[] = [];
193
195
 
194
196
  for (const importKind of Object.keys(IMPORT_PATHS) as ImportKind[]) {
@@ -234,8 +236,11 @@ export function getMcpDiscoverySummary(
234
236
  const sources = getConfigSourceSummaries(sourceSpecs);
235
237
  const includeHostConfigs = options.includeHostConfigs !== false;
236
238
 
239
+ const importKinds = isExclusiveConfigMode()
240
+ ? (readValidatedConfig(getEffectivePiGlobalConfigPath(overridePath), "MCP exclusive config")?.imports ?? [])
241
+ : (Object.keys(IMPORT_PATHS) as ImportKind[]);
237
242
  const imports = includeHostConfigs
238
- ? (Object.keys(IMPORT_PATHS) as ImportKind[])
243
+ ? importKinds
239
244
  .map((kind) => {
240
245
  const imported = loadImportedConfig(kind, cwd, `Failed to inspect imported MCP config from ${kind}:`);
241
246
  if (!imported) return null;
@@ -247,10 +252,14 @@ export function getMcpDiscoverySummary(
247
252
  })
248
253
  .filter((value): value is ImportConfigSummary => value !== null)
249
254
  : [];
250
- const hostConfigDiscovery = getConfiguredHostConfigDiscovery(overridePath, cwd);
255
+ const hostConfigDiscovery = isExclusiveConfigMode()
256
+ ? "off"
257
+ : getConfiguredHostConfigDiscovery(overridePath, cwd);
251
258
  const hostConfigs = imports.map((entry) => ({ ...entry, active: hostConfigDiscovery === "on" }));
252
259
  const settings = getMergedSettings(overridePath, cwd);
253
- const agentPlugins = getAgentPluginSummaries(settings?.agentPluginPaths, cwd);
260
+ const agentPlugins = isExclusiveConfigMode()
261
+ ? []
262
+ : getAgentPluginSummaries(settings?.agentPluginPaths, cwd);
254
263
  const totalServerCount = sources.reduce((sum, source) => sum + source.serverCount, 0) + agentPlugins.reduce((sum, plugin) => sum + plugin.serverCount, 0);
255
264
  const hasSharedServers = sources.some((source) => source.kind === "shared" && source.serverCount > 0) || agentPlugins.some(plugin => plugin.serverCount > 0);
256
265
  const hasPiOwnedServers = sources.some((source) => source.kind === "pi" && source.serverCount > 0);
@@ -296,7 +305,7 @@ export function loadMcpConfig(overridePath?: string, cwd = process.cwd()): McpCo
296
305
  // Host files are a lower-precedence fallback. This ordering means an opt-in
297
306
  // discovery cannot override a shared or Pi-owned definition, and all normal
298
307
  // URL-bound credential stripping remains in mergeServerMaps.
299
- let config: McpConfig = hostConfigDiscovery === "on"
308
+ let config: McpConfig = !isExclusiveConfigMode() && hostConfigDiscovery === "on"
300
309
  ? loadDiscoveredHostConfigs(cwd)
301
310
  : { mcpServers: {} };
302
311
 
@@ -306,8 +315,14 @@ export function loadMcpConfig(overridePath?: string, cwd = process.cwd()): McpCo
306
315
  config = mergeConfigs(config, expandImports(loaded, cwd));
307
316
  }
308
317
 
318
+ if (isExclusiveConfigMode()) return config;
319
+
320
+ const packageConfig = loadPackageMcpConfigs(cwd);
309
321
  const pluginConfig = loadAgentPluginConfigs(config.settings?.agentPluginPaths, cwd);
310
- return mergeConfigs(pluginConfig, config);
322
+ const packageServers = Object.fromEntries(
323
+ Object.entries(packageConfig.mcpServers).filter(([name]) => !Object.hasOwn(pluginConfig.mcpServers, name)),
324
+ );
325
+ return mergeConfigs({ mcpServers: packageServers }, mergeConfigs(pluginConfig, config));
311
326
  }
312
327
 
313
328
  function getMergedSettings(overridePath?: string, cwd = process.cwd()): McpSettings | undefined {
@@ -387,11 +402,23 @@ function getConfigConflicts(
387
402
  }
388
403
 
389
404
  function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSourceSpec[] {
390
- const userPath = getPiGlobalConfigPath(overridePath);
405
+ const userPath = getEffectivePiGlobalConfigPath(overridePath);
391
406
  const projectPath = getProjectConfigPath(cwd);
392
407
  const projectPiPath = getProjectPiConfigPath(cwd);
393
408
  const sources: ConfigSourceSpec[] = [];
394
409
 
410
+ if (isExclusiveConfigMode()) {
411
+ return [{
412
+ id: "pi-global",
413
+ label: "Pi exclusive config",
414
+ readPath: userPath,
415
+ writePath: userPath,
416
+ kind: "user",
417
+ shared: false,
418
+ scope: "global",
419
+ }];
420
+ }
421
+
395
422
  if (GENERIC_GLOBAL_CONFIG_PATH !== userPath) {
396
423
  sources.push({
397
424
  id: "shared-global",
@@ -456,6 +483,14 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
456
483
  return sources;
457
484
  }
458
485
 
486
+ function isExclusiveConfigMode(): boolean {
487
+ return process.env.PI_MCP_CONFIG_MODE?.trim().toLowerCase() === "exclusive";
488
+ }
489
+
490
+ function getEffectivePiGlobalConfigPath(overridePath?: string): string {
491
+ return getPiGlobalConfigPath(isExclusiveConfigMode() ? undefined : overridePath);
492
+ }
493
+
459
494
  function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
460
495
  const imports = mergeImports(base.imports, next.imports);
461
496
  const settings = next.settings ? { ...base.settings, ...next.settings } : base.settings;
@@ -471,7 +506,7 @@ function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
471
506
  // different url, these MUST NOT be inherited from the lower-precedence entry —
472
507
  // otherwise the original endpoint's credentials would be shipped to the new
473
508
  // url. See the SECURITY note in mergeServerMaps.
474
- const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv", "requestHeadersCommand"] as const;
509
+ const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv", "bearerTokenStore", "requestHeadersCommand"] as const;
475
510
 
476
511
  function mergeServerMaps(
477
512
  base: Record<string, ServerEntry>,
@@ -491,17 +526,30 @@ function mergeServerMaps(
491
526
  // applies (it is spread last). Behaviour is unchanged when the url is
492
527
  // identical or the override omits `url` (partial overrides still inherit).
493
528
  let baseEntry: ServerEntry = existing ?? {};
494
- if (existing && typeof definition.socket === "string") {
529
+ if (existing && typeof definition.command === "string") {
530
+ baseEntry = { ...existing };
531
+ for (const field of [
532
+ "url", "headers", "requestHeadersCommand", "auth", "bearerToken",
533
+ "bearerTokenEnv", "oauth", "httpTransport", "socket",
534
+ ] as const) {
535
+ delete baseEntry[field];
536
+ }
537
+ } else if (existing && typeof definition.url === "string") {
495
538
  baseEntry = { ...existing };
496
539
  for (const field of [
497
- "command", "args", "env", "cwd", "url", "headers", "auth",
498
- "bearerToken", "bearerTokenEnv", "oauth",
540
+ "command", "args", "env", "cwd", "pluginDataDir", "literalEnv", "socket",
499
541
  ] as const) {
500
542
  delete baseEntry[field];
501
543
  }
502
- } else if (existing?.socket && (typeof definition.command === "string" || typeof definition.url === "string")) {
544
+ } else if (existing && typeof definition.socket === "string") {
503
545
  baseEntry = { ...existing };
504
- delete baseEntry.socket;
546
+ for (const field of [
547
+ "command", "args", "env", "cwd", "pluginDataDir", "literalEnv", "url",
548
+ "headers", "requestHeadersCommand", "auth", "bearerToken", "bearerTokenEnv",
549
+ "oauth", "httpTransport",
550
+ ] as const) {
551
+ delete baseEntry[field];
552
+ }
505
553
  }
506
554
  if (existing && typeof definition.url === "string" && definition.url !== existing.url) {
507
555
  if (baseEntry === existing) baseEntry = { ...existing };
package/direct-tools.ts CHANGED
@@ -15,16 +15,16 @@ import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates
15
15
  import { isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
16
16
  import { resourceNameToToolName } from "./resource-tools.ts";
17
17
  import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
18
- import { formatAuthRequiredMessage, resolveServerUrl, truncateAtWord } from "./utils.ts";
18
+ import { formatAuthRequiredMessage, normalizeToolArguments, resolveServerUrl } from "./utils.ts";
19
19
  import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.ts";
20
20
  import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
21
21
  import { ensureToolCallApproved } from "./tool-approval.ts";
22
+ import { Check, Errors } from "typebox/value";
22
23
 
23
24
  type ClientCallToolResult = Awaited<ReturnType<Client["callTool"]>>;
24
25
  type ClientReadResourceResult = Awaited<ReturnType<Client["readResource"]>>;
25
26
 
26
27
  const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
27
- const INSTRUCTIONS_SNIPPET_LENGTH = 150;
28
28
  export const DIRECT_TOOLS_ADVISORY_THRESHOLD = 75;
29
29
 
30
30
  type DirectAutoAuthResult =
@@ -32,6 +32,58 @@ type DirectAutoAuthResult =
32
32
  | { status: "success" }
33
33
  | { status: "failed"; message: string };
34
34
 
35
+ /**
36
+ * Recover one model-emitted JSON layer for schema-declared object and array
37
+ * properties, then validate the complete input against the same schema.
38
+ */
39
+ export function prepareDirectToolArguments(inputSchema: unknown, args: unknown): unknown {
40
+ if (!inputSchema || typeof inputSchema !== "object" || Array.isArray(inputSchema)) return args;
41
+ const schema = inputSchema as Record<string, unknown>;
42
+ if (schema.type !== "object") return args;
43
+ const input = args && typeof args === "object" && !Array.isArray(args)
44
+ ? args as Record<string, unknown>
45
+ : null;
46
+ const properties = schema.properties;
47
+ let prepared: Record<string, unknown> | undefined;
48
+
49
+ if (input && properties && typeof properties === "object" && !Array.isArray(properties)) {
50
+ for (const [name, propertySchema] of Object.entries(properties)) {
51
+ if (!Object.hasOwn(input, name) || typeof input[name] !== "string"
52
+ || !propertySchema || typeof propertySchema !== "object" || Array.isArray(propertySchema)) continue;
53
+ const expectedType = (propertySchema as Record<string, unknown>).type;
54
+ if (expectedType !== "object" && expectedType !== "array") continue;
55
+ try {
56
+ const parsed: unknown = JSON.parse(input[name] as string);
57
+ const matches = expectedType === "array"
58
+ ? Array.isArray(parsed)
59
+ : parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
60
+ if (matches) {
61
+ prepared ??= { ...input };
62
+ prepared[name] = parsed;
63
+ }
64
+ } catch {
65
+ // Validation below reports malformed or shape-incompatible values.
66
+ }
67
+ }
68
+ }
69
+
70
+ const candidate = prepared ?? args;
71
+ if (!Check(inputSchema as never, candidate)) {
72
+ const errors = Errors(inputSchema as never, candidate);
73
+ const issues = errors.slice(0, 8).map((error) => ({
74
+ instancePath: error.instancePath || "/",
75
+ keyword: error.keyword,
76
+ message: error.message,
77
+ }));
78
+ throw new TypeError(`MCP direct tool arguments do not match the advertised input schema: ${JSON.stringify({
79
+ issues,
80
+ total: errors.length,
81
+ truncated: errors.length > issues.length,
82
+ })}`);
83
+ }
84
+ return candidate;
85
+ }
86
+
35
87
  function getDirectAuthRequiredMessage(
36
88
  state: McpExtensionState,
37
89
  serverName: string,
@@ -117,6 +169,8 @@ export function resolveDirectTools(
117
169
  cache: MetadataCache | null,
118
170
  prefix: ToolPrefix,
119
171
  envOverride?: string[],
172
+ unavailableServers: ReadonlySet<string> = new Set(),
173
+ reservedNames?: Set<string>,
120
174
  ): DirectToolSpec[] {
121
175
  const specs: DirectToolSpec[] = [];
122
176
  if (!cache) return specs;
@@ -224,82 +278,33 @@ export function resolveDirectTools(
224
278
  }
225
279
  }
226
280
 
227
- if (config.settings?.warnOnLargeDirectTools !== false && specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
228
- console.warn(`MCP: ${specs.length} direct tools resolved. Each direct tool adds prompt context; README guidance recommends targeted sets of 5-20 tools and using the proxy or an explicit string[] when 75+ direct tools would be registered.`);
281
+ for (const spec of specs) reservedNames?.add(spec.prefixedName);
282
+
283
+ const emittedSpecs = unavailableServers.size === 0
284
+ ? specs
285
+ : specs.filter((spec) => !unavailableServers.has(spec.serverName));
286
+
287
+ if (config.settings?.warnOnLargeDirectTools !== false && emittedSpecs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
288
+ console.warn(`MCP: ${emittedSpecs.length} direct tools resolved. Each direct tool adds prompt context; README guidance recommends targeted sets of 5-20 tools and using the proxy or an explicit string[] when 75+ direct tools would be registered. Set settings.warnOnLargeDirectTools to false to hide this advisory.`);
229
289
  }
230
290
 
231
- return specs;
291
+ return emittedSpecs;
232
292
  }
233
293
 
234
- export function buildProxyDescription(
235
- config: McpConfig,
236
- cache: MetadataCache | null,
237
- directSpecs: DirectToolSpec[],
238
- ): string {
239
- const prefix = config.settings?.toolPrefix ?? "server";
294
+ /**
295
+ * Pure function of config: the description must stay byte-stable across
296
+ * runtime metadata changes (tool counts, instructions, connection state) so
297
+ * re-registering the proxy tool never rewrites the cached prompt prefix.
298
+ * Live counts/status belong to `mcp({ })`, full instructions to
299
+ * `mcp({ instructions })`.
300
+ */
301
+ export function buildProxyDescription(config: McpConfig): string {
240
302
  let desc = `MCP gateway — server status, tool search/describe, auth, and single MCP tool calls. When one request needs several MCP calls with logic between them, use mcpScript. Non-MCP Pi tools should be called directly, not through mcp.\n`;
241
303
 
242
- const directByServer = new Map<string, number>();
243
- for (const spec of directSpecs) {
244
- directByServer.set(spec.serverName, (directByServer.get(spec.serverName) ?? 0) + 1);
245
- }
246
- if (directByServer.size > 0) {
247
- const parts = [...directByServer.entries()].map(
248
- ([server, count]) => `${server} (${count})`,
249
- );
250
- desc += `\nDirect tools available (call as normal tools): ${parts.join(", ")}\n`;
251
- }
252
-
253
- const serverSummaries: string[] = [];
254
- for (const serverName of Object.keys(config.mcpServers)) {
255
- const definition = config.mcpServers[serverName];
256
- if (!definition || isServerDisabled(definition)) continue;
257
- const cachedEntry = cache?.servers?.[serverName];
258
- const entry = cachedEntry && isServerCacheValid(cachedEntry, definition) ? cachedEntry : undefined;
259
- const effectivePrefix = resolveToolPrefix(definition, prefix);
260
- const hasToolFilters =
261
- (Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
262
- (Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
263
- const selectorCandidateIndex = hasToolFilters && cache ? (() => {
264
- const candidates = new Set<string>();
265
- for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
266
- const otherEntry = cache.servers[otherServerName];
267
- if (!otherEntry || !isServerCacheValid(otherEntry, otherDefinition) || isServerDisabled(otherDefinition)) continue;
268
- const otherPrefix = resolveToolPrefix(otherDefinition, prefix);
269
- for (const otherTool of otherEntry.tools ?? []) {
270
- if (!isUiToolVisibleToModel(otherTool.uiVisibility)) continue;
271
- for (const candidate of getToolNameCandidates(otherTool.name, otherServerName, otherPrefix, false)) candidates.add(candidate);
272
- }
273
- if (otherDefinition.exposeResources !== false) {
274
- for (const resource of otherEntry.resources ?? []) {
275
- const baseName = `read_${resourceNameToToolName(resource.name)}`;
276
- for (const candidate of getToolNameCandidates(baseName, otherServerName, otherPrefix, false)) candidates.add(candidate);
277
- }
278
- }
279
- }
280
- return createToolSelectorCandidateIndex(candidates);
281
- })() : undefined;
282
- const toolCount = (entry?.tools ?? []).filter(
283
- (tool) => isUiToolVisibleToModel(tool.uiVisibility)
284
- && isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex),
285
- ).length;
286
- const resourceCount = definition?.exposeResources !== false
287
- ? (entry?.resources ?? []).filter((resource) => {
288
- const baseName = `read_${resourceNameToToolName(resource.name)}`;
289
- return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex);
290
- }).length
291
- : 0;
292
- const totalItems = toolCount + resourceCount;
293
- if (totalItems === 0) continue;
294
- const directCount = directByServer.get(serverName) ?? 0;
295
- const proxyCount = totalItems - directCount;
296
- if (proxyCount > 0) {
297
- serverSummaries.push(`${serverName} (${proxyCount} tools)`);
298
- }
299
- }
300
-
301
- if (serverSummaries.length > 0) {
302
- desc += `\nServers: ${serverSummaries.join(", ")}\n`;
304
+ const serverNames = Object.keys(config.mcpServers)
305
+ .filter((serverName) => !isServerDisabled(config.mcpServers[serverName]));
306
+ if (serverNames.length > 0) {
307
+ desc += `\nServers: ${serverNames.join(", ")}\n`;
303
308
  }
304
309
 
305
310
  const disabledServers = Object.entries(config.mcpServers)
@@ -309,22 +314,8 @@ export function buildProxyDescription(
309
314
  desc += `\nDisabled servers (enable with /mcp enable <server> and /reload): ${disabledServers.join(", ")}\n`;
310
315
  }
311
316
 
312
- const instructionSummaries: string[] = [];
313
- for (const serverName of Object.keys(config.mcpServers)) {
314
- if (isServerDisabled(config.mcpServers[serverName])) continue;
315
- const definition = config.mcpServers[serverName];
316
- const entry = definition && cache?.servers?.[serverName];
317
- const instructions = entry && definition && isServerCacheValid(entry, definition) ? entry.instructions : undefined;
318
- if (!instructions) continue;
319
- const snippet = truncateAtWord(instructions.replace(/\s+/g, " ").trim(), INSTRUCTIONS_SNIPPET_LENGTH);
320
- instructionSummaries.push(` ${serverName}: ${snippet}`);
321
- }
322
- if (instructionSummaries.length > 0) {
323
- desc += `\nServer instructions (truncated - full text via mcp({ instructions: "name" })):\n${instructionSummaries.join("\n")}\n`;
324
- }
325
-
326
317
  desc += `\nUsage:\n`;
327
- desc += ` mcp({ }) → Show server status\n`;
318
+ desc += ` mcp({ }) → Show server status and tool counts\n`;
328
319
  desc += ` mcp({ server: "name" }) → List tools from server\n`;
329
320
  desc += ` mcp({ search: "query" }) → Search MCP tools by name/description\n`;
330
321
  desc += ` mcp({ describe: "tool_name" }) → Show tool details and parameters\n`;
@@ -429,6 +420,7 @@ export function createDirectToolExecutor(
429
420
  };
430
421
  }
431
422
 
423
+ const normalizedParams = spec.resourceUri ? params : normalizeToolArguments(params);
432
424
  const approval = await ensureToolCallApproved(state, spec.serverName, {
433
425
  name: spec.prefixedName,
434
426
  originalName: spec.originalName,
@@ -437,7 +429,7 @@ export function createDirectToolExecutor(
437
429
  ...(spec.resourceUri !== undefined ? { resourceUri: spec.resourceUri } : {}),
438
430
  ...(spec.uiResourceUri !== undefined ? { uiResourceUri: spec.uiResourceUri } : {}),
439
431
  ...(spec.uiStreamMode !== undefined ? { uiStreamMode: spec.uiStreamMode } : {}),
440
- }, params, ownedSignal, spec.resourceUri ? "resource" : "direct");
432
+ }, normalizedParams, ownedSignal, spec.resourceUri ? "resource" : "direct");
441
433
  if (approval.ok === false) {
442
434
  const denied = approval.reason === "denied";
443
435
  const message = denied
@@ -497,7 +489,10 @@ export function createDirectToolExecutor(
497
489
  (conn) => conn.client.readResource({ uri: spec.resourceUri! }, requestOptions),
498
490
  );
499
491
  const content = transformMcpResourceContents(result.contents ?? [], state.owner?.signal);
500
- const guarded = await guardMcpOutput(content.length > 0 ? content : [{ type: "text" as const, text: "(empty resource)" }], outputGuardOptions);
492
+ const guarded = await guardMcpOutput(content.length > 0 ? content : [{ type: "text" as const, text: "(empty resource)" }], {
493
+ ...outputGuardOptions,
494
+ ...(state.config.settings?.directToolResultDetails === "bounded" ? { rawMcpResult: result } : {}),
495
+ });
501
496
  return {
502
497
  content: guarded.content,
503
498
  details: { server: spec.serverName, resourceUri: spec.resourceUri, ...guardedMcpDetails(guarded) },
@@ -509,7 +504,7 @@ export function createDirectToolExecutor(
509
504
  ? await maybeStartUiSession(state, {
510
505
  serverName: spec.serverName,
511
506
  toolName: spec.originalName,
512
- toolArgs: params ?? {},
507
+ toolArgs: normalizedParams,
513
508
  uiResourceUri: spec.uiResourceUri!,
514
509
  ...(spec.uiStreamMode !== undefined ? { streamMode: spec.uiStreamMode } : {}),
515
510
  ...(signal ? { signal } : {}),
@@ -527,7 +522,7 @@ export function createDirectToolExecutor(
527
522
  spec.serverName,
528
523
  (conn) => abortable(conn.client.callTool({
529
524
  name: spec.originalName,
530
- arguments: params ?? {},
525
+ arguments: normalizedParams,
531
526
  _meta: uiSession?.requestMeta,
532
527
  }, requestOptions), ownedSignal),
533
528
  );
@@ -538,7 +533,13 @@ export function createDirectToolExecutor(
538
533
  const content = transformMcpContent(mcpContent, state.owner?.signal);
539
534
  const outputContent = content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }];
540
535
  const schemaText = spec.inputSchema ? `\n\nExpected parameters:\n${formatSchema(spec.inputSchema)}` : "";
541
- const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions, prefix: "Error: ", suffix: schemaText, emptyTextFallback: "Tool execution failed" });
536
+ const guarded = await guardMcpOutput(outputContent, {
537
+ ...outputGuardOptions,
538
+ prefix: "Error: ",
539
+ suffix: schemaText,
540
+ emptyTextFallback: "Tool execution failed",
541
+ ...(state.config.settings?.directToolResultDetails === "bounded" ? { rawMcpResult: result } : {}),
542
+ });
542
543
  return {
543
544
  content: guarded.content,
544
545
  details: { error: "tool_error", server: spec.serverName, ...guardedMcpDetails(guarded) },
@@ -549,7 +550,11 @@ export function createDirectToolExecutor(
549
550
  const outputContent = content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }];
550
551
  if (hasUi) {
551
552
  const uiSummary = summarizeUiSessionResult(uiSession);
552
- const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions, suffix: `\n\n${uiSummary.message}` });
553
+ const guarded = await guardMcpOutput(outputContent, {
554
+ ...outputGuardOptions,
555
+ suffix: `\n\n${uiSummary.message}`,
556
+ ...(state.config.settings?.directToolResultDetails === "bounded" ? { rawMcpResult: result } : {}),
557
+ });
553
558
  return {
554
559
  content: guarded.content,
555
560
  details: {
@@ -563,7 +568,10 @@ export function createDirectToolExecutor(
563
568
  };
564
569
  }
565
570
 
566
- const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions });
571
+ const guarded = await guardMcpOutput(outputContent, {
572
+ ...outputGuardOptions,
573
+ ...(state.config.settings?.directToolResultDetails === "bounded" ? { rawMcpResult: result } : {}),
574
+ });
567
575
  return {
568
576
  content: guarded.content,
569
577
  details: { server: spec.serverName, tool: spec.originalName, ...guardedMcpDetails(guarded) },
@@ -589,12 +597,16 @@ export function createDirectToolExecutor(
589
597
  };
590
598
  }
591
599
  const message = error instanceof Error ? error.message : String(error);
600
+ const aborted = isAbortError(error, ownedSignal);
601
+ if (!aborted) {
602
+ await state.manager.close(spec.serverName).catch(() => {});
603
+ }
592
604
  uiSession?.sendToolCancelled(message);
593
605
  const schemaText = spec.inputSchema ? `\n\nExpected parameters:\n${formatSchema(spec.inputSchema)}` : "";
594
606
  const guarded = await guardMcpOutput([{ type: "text" as const, text: message }], { ...outputGuardOptions, prefix: "Failed to call tool: ", suffix: schemaText });
595
607
  return {
596
608
  content: guarded.content,
597
- details: { error: isAbortError(error, ownedSignal) ? "aborted" : "call_failed", server: spec.serverName, ...guardedMcpDetails(guarded) },
609
+ details: { error: aborted ? "aborted" : "call_failed", server: spec.serverName, ...guardedMcpDetails(guarded) },
598
610
  };
599
611
  } finally {
600
612
  if (uiSession?.reused) {
@@ -0,0 +1,10 @@
1
+ export declare function getConfigDirName(): string;
2
+ export declare function getAgentDir(): string;
3
+ export declare function getAgentPath(...segments: string[]): string;
4
+ export declare function getAppName(): string;
5
+ /**
6
+ * Home page the host declares for itself, via `piConfig.clientUri` in the same
7
+ * manifest that carries `piConfig.name`. Only the distribution knows its own
8
+ * URL, so this is the one place it can come from without guessing.
9
+ */
10
+ export declare function getAppClientUri(): string | undefined;
@@ -0,0 +1,63 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ export function getConfigDirName() {
5
+ const configDir = readPiConfig()?.configDir;
6
+ return typeof configDir === "string" && configDir.trim() ? configDir.trim() : ".pi";
7
+ }
8
+ export function getAgentDir() {
9
+ const piConfig = readPiConfig();
10
+ const name = piConfig?.name;
11
+ const appName = typeof name === "string" && name.trim() ? name.trim() : "pi";
12
+ const configured = process.env[`${appName.toUpperCase()}_CODING_AGENT_DIR`]?.trim();
13
+ if (!configured) {
14
+ return join(homedir(), getConfigDirName(), "agent");
15
+ }
16
+ if (configured === "~") {
17
+ return homedir();
18
+ }
19
+ if (configured.startsWith("~/")) {
20
+ return resolve(homedir(), configured.slice(2));
21
+ }
22
+ return resolve(configured);
23
+ }
24
+ export function getAgentPath(...segments) {
25
+ return join(getAgentDir(), ...segments);
26
+ }
27
+ /**
28
+ * What the host calls itself.
29
+ *
30
+ * pi supports rebranding through `piConfig.name` in the package.json that its
31
+ * `getPackageDir()` resolves, and distributions built on pi (arc, tau, …) point
32
+ * `PI_PACKAGE_DIR` at their own manifest. Read that manifest directly rather
33
+ * than importing pi: this package deliberately depends on pi-ai and pi-tui
34
+ * only, and `getAgentDir()` above reads its env var the same self-contained way.
35
+ *
36
+ * Falls back to "pi", which is what pi's own APP_NAME resolves to.
37
+ */
38
+ function readPiConfig() {
39
+ const dir = process.env.PI_PACKAGE_DIR?.trim();
40
+ if (!dir)
41
+ return undefined;
42
+ try {
43
+ const manifest = JSON.parse(readFileSync(join(resolve(dir), "package.json"), "utf8"));
44
+ return manifest.piConfig;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ export function getAppName() {
51
+ const name = readPiConfig()?.name;
52
+ return typeof name === "string" && name.trim() ? name.trim() : "pi";
53
+ }
54
+ /**
55
+ * Home page the host declares for itself, via `piConfig.clientUri` in the same
56
+ * manifest that carries `piConfig.name`. Only the distribution knows its own
57
+ * URL, so this is the one place it can come from without guessing.
58
+ */
59
+ export function getAppClientUri() {
60
+ const uri = readPiConfig()?.clientUri;
61
+ return typeof uri === "string" && uri.trim() ? uri.trim() : undefined;
62
+ }
63
+ //# sourceMappingURL=agent-dir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-dir.js","sourceRoot":"","sources":["../agent-dir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,UAAU,gBAAgB;IAC9B,MAAM,SAAS,GAAG,YAAY,EAAE,EAAE,SAAS,CAAC;IAC5C,OAAO,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AACtF,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,CAAC;IAC5B,MAAM,OAAO,GAAG,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7E,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,WAAW,EAAE,mBAAmB,CAAC,EAAE,IAAI,EAAE,CAAC;IACpF,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,gBAAgB,EAAE,EAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;QACvB,OAAO,OAAO,EAAE,CAAC;IACnB,CAAC;IACD,IAAI,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAChC,OAAO,OAAO,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,OAAO,CAAC,UAAU,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAG,QAAkB;IAChD,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,GAAG,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,YAAY;IACnB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,CAAA;IAC9C,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAA;IAC1B,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAEnF,CAAA;QACD,OAAO,QAAQ,CAAC,QAAQ,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,YAAY,EAAE,EAAE,IAAI,CAAA;IACjC,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACrE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,GAAG,GAAG,YAAY,EAAE,EAAE,SAAS,CAAA;IACrC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACvE,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { McpConfig } from "./types.ts";
2
+ export interface AgentPluginSummary {
3
+ path: string;
4
+ name?: string;
5
+ serverCount: number;
6
+ }
7
+ export declare function loadAgentPluginConfigs(paths: unknown, cwd?: string): McpConfig;
8
+ export declare function getAgentPluginSummaries(paths: unknown, cwd?: string): AgentPluginSummary[];