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.
- package/CHANGELOG.md +68 -7
- package/README.md +73 -4
- package/cli.js +163 -1
- package/commands.ts +106 -24
- package/config.ts +60 -12
- package/direct-tools.ts +107 -95
- package/dist/agent-dir.d.ts +10 -0
- package/dist/agent-dir.js +63 -0
- package/dist/agent-dir.js.map +1 -0
- package/dist/agent-plugin-loader.d.ts +8 -0
- package/dist/agent-plugin-loader.js +356 -0
- package/dist/agent-plugin-loader.js.map +1 -0
- package/dist/config.d.ts +123 -0
- package/dist/config.js +1077 -0
- package/dist/config.js.map +1 -0
- package/dist/metadata-cache.d.ts +19 -0
- package/dist/metadata-cache.js +319 -0
- package/dist/metadata-cache.js.map +1 -0
- package/dist/package-mcp-loader.d.ts +2 -0
- package/dist/package-mcp-loader.js +156 -0
- package/dist/package-mcp-loader.js.map +1 -0
- package/dist/resource-tools.d.ts +1 -0
- package/dist/resource-tools.js +15 -0
- package/dist/resource-tools.js.map +1 -0
- package/dist/types.d.ts +564 -0
- package/dist/types.js +287 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-app-bridge-helpers.d.ts +6 -0
- package/dist/ui-app-bridge-helpers.js +37 -0
- package/dist/ui-app-bridge-helpers.js.map +1 -0
- package/dist/ui-stream-types.d.ts +104 -0
- package/dist/ui-stream-types.js +59 -0
- package/dist/ui-stream-types.js.map +1 -0
- package/dist/ui-tool-visibility.d.ts +4 -0
- package/dist/ui-tool-visibility.js +27 -0
- package/dist/ui-tool-visibility.js.map +1 -0
- package/dist/utils.d.ts +32 -0
- package/dist/utils.js +338 -0
- package/dist/utils.js.map +1 -0
- package/failure-backoff.ts +23 -0
- package/index.ts +325 -60
- package/init.ts +46 -31
- package/lifecycle.ts +89 -23
- package/mcp-auth-flow.ts +39 -6
- package/mcp-bearer-store.ts +387 -0
- package/mcp-callback-server.ts +27 -3
- package/mcp-code.ts +6 -0
- package/mcp-oauth-provider.ts +66 -16
- package/mcp-output-guard.ts +95 -7
- package/mcp-panel.ts +42 -13
- package/mcp-probe.ts +18 -4
- package/mcp-references.ts +368 -0
- package/mcp-status.ts +8 -19
- package/metadata-cache.ts +52 -33
- package/namespace-tools.ts +276 -0
- package/package-mcp-loader.ts +168 -0
- package/package.json +26 -6
- package/proxy-modes.ts +71 -14
- package/request-headers-command.ts +31 -37
- package/search-ranking.ts +94 -22
- package/server-manager.ts +86 -17
- package/state.ts +1 -3
- package/tool-result-renderer.ts +16 -2
- package/types.ts +50 -1
- package/ui-server.ts +21 -35
- package/ui-session.ts +2 -1
- 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
|
-
?
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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", "
|
|
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
|
|
544
|
+
} else if (existing && typeof definition.socket === "string") {
|
|
503
545
|
baseEntry = { ...existing };
|
|
504
|
-
|
|
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,
|
|
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
|
-
|
|
228
|
-
|
|
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
|
|
291
|
+
return emittedSpecs;
|
|
232
292
|
}
|
|
233
293
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
)
|
|
239
|
-
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
},
|
|
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)" }],
|
|
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:
|
|
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:
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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:
|
|
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[];
|