pi-mcp-adapter 3.3.0 → 5.0.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 +95 -1
- package/README.md +78 -974
- package/cli.js +210 -0
- package/commands.ts +55 -4
- package/config.ts +308 -33
- package/direct-tool-surface.ts +9 -7
- package/direct-tools.ts +43 -29
- package/dist/config.d.ts +15 -4
- package/dist/config.js +317 -30
- package/dist/config.js.map +1 -1
- package/dist/elicitation-handler.d.ts +4 -0
- package/dist/elicitation-handler.js +14 -1
- package/dist/elicitation-handler.js.map +1 -1
- package/dist/mcp-auth-flow.d.ts +2 -0
- package/dist/mcp-auth-flow.js +12 -3
- package/dist/mcp-auth-flow.js.map +1 -1
- package/dist/mcp-auth.js +3 -1
- package/dist/mcp-auth.js.map +1 -1
- package/dist/metadata-cache.d.ts +15 -1
- package/dist/metadata-cache.js +76 -8
- package/dist/metadata-cache.js.map +1 -1
- package/dist/output-shape.d.ts +18 -5
- package/dist/output-shape.js +162 -18
- package/dist/output-shape.js.map +1 -1
- package/dist/package-mcp-loader.js +9 -5
- package/dist/package-mcp-loader.js.map +1 -1
- package/dist/project-server-trust.d.ts +17 -0
- package/dist/project-server-trust.js +209 -0
- package/dist/project-server-trust.js.map +1 -0
- package/dist/server-manager.d.ts +5 -0
- package/dist/server-manager.js +98 -26
- package/dist/server-manager.js.map +1 -1
- package/dist/session-recovery.js +4 -3
- package/dist/session-recovery.js.map +1 -1
- package/dist/state.d.ts +2 -0
- package/dist/types.d.ts +29 -2
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +5 -1
- package/dist/utils.js +16 -0
- package/dist/utils.js.map +1 -1
- package/{OAUTH.md → docs/auth.md} +66 -11
- package/docs/configuration.md +221 -0
- package/docs/extension-api.md +246 -0
- package/docs/pi-builtin-comparison.md +113 -0
- package/docs/prompts-and-ui.md +84 -0
- package/docs/scripting.md +131 -0
- package/docs/servers.md +142 -0
- package/docs/tools.md +259 -0
- package/elicitation-handler.ts +13 -1
- package/index.ts +268 -37
- package/init.ts +97 -46
- package/mcp-auth-flow.ts +13 -2
- package/mcp-auth.ts +3 -1
- package/mcp-code.ts +125 -77
- package/mcp-output-guard.ts +24 -5
- package/mcp-panel.ts +52 -4
- package/mcp-script-worker.mjs +6 -2
- package/mcp-setup-panel.ts +587 -303
- package/metadata-cache.ts +79 -8
- package/onboarding-state.ts +20 -0
- package/output-shape.ts +161 -22
- package/package-mcp-loader.ts +8 -5
- package/package.json +8 -6
- package/pi-builtin-mcp.ts +32 -0
- package/pi-signin-import.ts +147 -0
- package/project-server-trust.ts +8 -4
- package/prompts.ts +11 -3
- package/proxy-modes.ts +163 -138
- package/search-ranking.ts +12 -3
- package/server-manager.ts +101 -24
- package/session-recovery.ts +4 -3
- package/skills/mcp-scripting/SKILL.md +24 -30
- package/skills/mcp-scripting/references/jev.md +3 -0
- package/state.ts +2 -0
- package/types.ts +18 -2
- package/ui-server.ts +34 -32
- package/utils.ts +16 -1
package/config.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { cloneBuiltInAgentPluginEntry, isBuiltInAgentPlugin, mergeBuiltInAgentPl
|
|
|
10
10
|
import { loadClaudePluginBundles } from "./claude-plugin-loader.ts";
|
|
11
11
|
import { loadPackageMcpConfigs } from "./package-mcp-loader.ts";
|
|
12
12
|
import { validateJevSettings } from "./jev-client.ts";
|
|
13
|
-
import { formatServerNamespace, isServerDisabled, type ClaudePluginConfig, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
|
|
14
|
-
import { parseJsonWithComments, stripUtf8Bom, toStringRecord } from "./utils.ts";
|
|
13
|
+
import { formatServerNamespace, isServerDisabled, type ClaudePluginConfig, type HostConfigDiscovery, type McpConfig, type OAuthConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
|
|
14
|
+
import { getMissingEnvVars, parseJsonWithComments, providerAuthUrlError, stripUtf8Bom, toStringRecord } from "./utils.ts";
|
|
15
15
|
|
|
16
16
|
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
17
17
|
const AGENTS_GLOBAL_CONFIG_PATHS = [
|
|
@@ -31,6 +31,8 @@ export interface KnownServerPreset {
|
|
|
31
31
|
name: string;
|
|
32
32
|
summary: string;
|
|
33
33
|
entry: ServerEntry;
|
|
34
|
+
/** Offered only when one of these app paths exists; the local server is probed after adding. */
|
|
35
|
+
desktopApp?: { paths: readonly string[]; enableSteps: string };
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export const KNOWN_SERVER_PRESETS: readonly KnownServerPreset[] = [
|
|
@@ -74,6 +76,20 @@ export const KNOWN_SERVER_PRESETS: readonly KnownServerPreset[] = [
|
|
|
74
76
|
summary: "Inspect and automate a local Chrome browser.",
|
|
75
77
|
entry: { command: "npx", args: ["-y", "chrome-devtools-mcp@1.6.0"] },
|
|
76
78
|
},
|
|
79
|
+
{
|
|
80
|
+
id: "figma",
|
|
81
|
+
name: "Figma (desktop)",
|
|
82
|
+
summary: "Read designs through the Figma desktop app. Needs a Dev or Full seat on a paid Figma plan.",
|
|
83
|
+
entry: { url: "http://127.0.0.1:3845/mcp", protocolVersion: "auto" },
|
|
84
|
+
desktopApp: {
|
|
85
|
+
paths: [
|
|
86
|
+
"/Applications/Figma.app",
|
|
87
|
+
join(homedir(), "Applications", "Figma.app"),
|
|
88
|
+
join(homedir(), "AppData", "Local", "Figma", "Figma.exe"),
|
|
89
|
+
],
|
|
90
|
+
enableSteps: "To enable it, open a Design file in Figma, switch to Dev Mode (Shift+D), and click 'Enable desktop MCP server' in the inspect panel.",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
77
93
|
];
|
|
78
94
|
|
|
79
95
|
const IMPORT_PATHS: Record<ImportKind, string[]> = {
|
|
@@ -97,7 +113,7 @@ const IMPORT_PATHS: Record<ImportKind, string[]> = {
|
|
|
97
113
|
};
|
|
98
114
|
|
|
99
115
|
interface ConfigSourceSpec {
|
|
100
|
-
id: "shared-global" | "agents-global" | "agents-nested-global" | "pi-global" | "shared-project-ancestor" | "pi-project-ancestor" | "shared-project" | "pi-project";
|
|
116
|
+
id: "shared-global" | "agents-global" | "agents-nested-global" | "pi-mcp-global" | "pi-global" | "shared-project-ancestor" | "pi-project-ancestor" | "shared-project" | "pi-mcp-project" | "pi-project";
|
|
101
117
|
label: string;
|
|
102
118
|
readPath: string;
|
|
103
119
|
writePath: string;
|
|
@@ -162,6 +178,7 @@ export interface McpDiscoverySummary {
|
|
|
162
178
|
totalServerCount: number;
|
|
163
179
|
fingerprint: string;
|
|
164
180
|
repoPrompt: RepoPromptDiscovery;
|
|
181
|
+
knownServerPresets: readonly KnownServerPreset[];
|
|
165
182
|
}
|
|
166
183
|
|
|
167
184
|
export interface McpStandardConfigSummary {
|
|
@@ -185,10 +202,15 @@ export function getPiGlobalConfigPath(overridePath?: string): string {
|
|
|
185
202
|
return overridePath ? resolve(overridePath) : getAgentPath(ADAPTER_CONFIG_NAME);
|
|
186
203
|
}
|
|
187
204
|
|
|
188
|
-
export function
|
|
205
|
+
export function getPiMcpGlobalConfigPath(): string {
|
|
189
206
|
return getAgentPath(PI_MCP_CONFIG_NAME);
|
|
190
207
|
}
|
|
191
208
|
|
|
209
|
+
/** @internal Pi's built-in MCP stores OAuth sign-ins here, keyed by `String(new URL(serverUrl))`. */
|
|
210
|
+
export function getPiMcpAuthPath(): string {
|
|
211
|
+
return getAgentPath("mcp-auth.json");
|
|
212
|
+
}
|
|
213
|
+
|
|
192
214
|
export function getGenericGlobalConfigPath(): string {
|
|
193
215
|
return GENERIC_GLOBAL_CONFIG_PATH;
|
|
194
216
|
}
|
|
@@ -201,22 +223,30 @@ export function getProjectPiConfigPath(cwd = process.cwd()): string {
|
|
|
201
223
|
return resolve(cwd, getConfigDirName(), ADAPTER_CONFIG_NAME);
|
|
202
224
|
}
|
|
203
225
|
|
|
204
|
-
export function
|
|
226
|
+
export function getProjectPiMcpConfigPath(cwd = process.cwd()): string {
|
|
205
227
|
return resolve(cwd, getConfigDirName(), PI_MCP_CONFIG_NAME);
|
|
206
228
|
}
|
|
207
229
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
let piMcpConfigEnabled = false;
|
|
231
|
+
|
|
232
|
+
/** @internal Set once by the extension on Pi 0.99+, so every loader in the process agrees. */
|
|
233
|
+
export function setPiMcpConfigEnabled(enabled: boolean): void {
|
|
234
|
+
piMcpConfigEnabled = enabled;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** @internal */
|
|
238
|
+
export function isPiMcpConfigEnabled(): boolean {
|
|
239
|
+
return piMcpConfigEnabled;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Adapter content in an old adapter `mcp.json` on Pi versions without their own MCP config. */
|
|
243
|
+
function legacyMcpConfigHasContent(filePath: string): boolean {
|
|
214
244
|
if (!existsSync(filePath)) return false;
|
|
215
245
|
try {
|
|
216
246
|
const raw = parseJsonWithComments(readFileSync(filePath, "utf-8"));
|
|
217
247
|
if (!isRecord(raw)) return false;
|
|
218
248
|
const hasServers = (value: unknown) => isRecord(value) && Object.keys(value).length > 0;
|
|
219
|
-
return
|
|
249
|
+
return hasServers(raw.mcpServers)
|
|
220
250
|
|| hasServers(raw["mcp-servers"])
|
|
221
251
|
|| raw.settings !== undefined
|
|
222
252
|
|| raw.imports !== undefined
|
|
@@ -226,18 +256,32 @@ function legacyMcpConfigHasContent(filePath: string, piOwnsServers: boolean): bo
|
|
|
226
256
|
}
|
|
227
257
|
}
|
|
228
258
|
|
|
229
|
-
export function getLegacyMcpMigrationNotices(cwd = process.cwd(), overridePath?: string
|
|
259
|
+
export function getLegacyMcpMigrationNotices(cwd = process.cwd(), overridePath?: string): string[] {
|
|
260
|
+
if (piMcpConfigEnabled) {
|
|
261
|
+
return getConfigSources(overridePath, cwd).flatMap((source) => {
|
|
262
|
+
const file = isPiMcpSource(source.id) ? readPiMcpConfig(source.readPath) : null;
|
|
263
|
+
if (!file) return [];
|
|
264
|
+
const parts: string[] = [];
|
|
265
|
+
if (file.adapterKeys.length > 0) {
|
|
266
|
+
const serversHint = file.adapterKeys.includes("mcp-servers") ? `, with "mcp-servers" entries under its "mcpServers" key` : "";
|
|
267
|
+
parts.push(`pi-mcp-adapter does not read ${file.adapterKeys.join(", ")} in this file; move them into ${source.writePath}${serversHint}.`);
|
|
268
|
+
}
|
|
269
|
+
if (file.skipped.length > 0) parts.push(`Skipped ${file.skipped.join("; ")}.`);
|
|
270
|
+
if (file.ignoredSettings.size > 0) {
|
|
271
|
+
const ignored = [...file.ignoredSettings].map(([name, settings]) => `"${name}": ${settings.join(", ")}`);
|
|
272
|
+
parts.push(`Ignored settings (details in /mcp-adapter): ${ignored.join("; ")}.`);
|
|
273
|
+
}
|
|
274
|
+
return parts.length > 0 ? [`${source.readPath}: ${parts.join(" ")}`] : [];
|
|
275
|
+
});
|
|
276
|
+
}
|
|
230
277
|
const explicitPath = overridePath ? resolve(overridePath) : undefined;
|
|
231
278
|
const candidates = [
|
|
232
|
-
[
|
|
233
|
-
[
|
|
279
|
+
[getPiMcpGlobalConfigPath(), getPiGlobalConfigPath()],
|
|
280
|
+
[getProjectPiMcpConfigPath(cwd), getProjectPiConfigPath(cwd)],
|
|
234
281
|
] as const;
|
|
235
282
|
return candidates.flatMap(([source, target]) => {
|
|
236
283
|
// An explicitly selected config is loaded verbatim, whatever its name.
|
|
237
|
-
if (resolve(source) === explicitPath || !legacyMcpConfigHasContent(source
|
|
238
|
-
if (piOwnsServers) {
|
|
239
|
-
return [`${source} contains pi-mcp-adapter settings that neither Pi nor the adapter reads. Move settings, imports, and claudePlugins into ${target}, and put any "mcp-servers" entries under its "mcpServers" key.`];
|
|
240
|
-
}
|
|
284
|
+
if (resolve(source) === explicitPath || !legacyMcpConfigHasContent(source)) return [];
|
|
241
285
|
const fix = existsSync(target)
|
|
242
286
|
? `Merge ${source} into ${target}, then remove ${source}.`
|
|
243
287
|
: `Move it with: mv ${JSON.stringify(source)} ${JSON.stringify(target)}`;
|
|
@@ -273,7 +317,7 @@ export function findAvailableImportConfigs(cwd = process.cwd()): DiscoveredImpor
|
|
|
273
317
|
|
|
274
318
|
function getConfigSourceSummaries(sourceSpecs: ConfigSourceSpec[]): ConfigDiscoverySource[] {
|
|
275
319
|
return sourceSpecs.map((source) => {
|
|
276
|
-
const loaded =
|
|
320
|
+
const loaded = readSourceConfig(source.id, source.readPath);
|
|
277
321
|
return {
|
|
278
322
|
id: source.id,
|
|
279
323
|
label: source.label,
|
|
@@ -360,6 +404,7 @@ export function getMcpDiscoverySummary(
|
|
|
360
404
|
...summaryWithoutRepoPrompt,
|
|
361
405
|
fingerprint,
|
|
362
406
|
repoPrompt: detectRepoPrompt(summaryWithoutRepoPrompt, cwd),
|
|
407
|
+
knownServerPresets: KNOWN_SERVER_PRESETS.filter(({ desktopApp }) => !desktopApp || desktopApp.paths.some((path) => existsSync(path))),
|
|
363
408
|
};
|
|
364
409
|
}
|
|
365
410
|
|
|
@@ -422,9 +467,15 @@ export function loadMcpConfigWithSources(overridePath?: string, cwd = process.cw
|
|
|
422
467
|
if (source.scope === "project") projectServers.set(name, { path: source.path });
|
|
423
468
|
}
|
|
424
469
|
|
|
470
|
+
const piProjectSource = sourceSpecs.find((source) => source.id === "pi-mcp-project");
|
|
471
|
+
const piProjectNames = Object.keys((piProjectSource && readPiMcpConfig(piProjectSource.readPath))?.mcpServers ?? {});
|
|
425
472
|
for (const source of sourceSpecs) {
|
|
426
|
-
const loaded =
|
|
473
|
+
const loaded = readSourceConfig(source.id, source.readPath);
|
|
427
474
|
if (!loaded) continue;
|
|
475
|
+
// Pi replaces a global entry that the project file redefines; mergeServerMaps would merge fields.
|
|
476
|
+
if (source.id === "pi-mcp-global") {
|
|
477
|
+
for (const name of piProjectNames) delete loaded.mcpServers[name];
|
|
478
|
+
}
|
|
428
479
|
const expandedImport = expandImports(loaded, cwd);
|
|
429
480
|
const expanded = expandedImport.config;
|
|
430
481
|
const sourceRef = { path: source.readPath };
|
|
@@ -452,7 +503,7 @@ export function loadMcpConfigWithSources(overridePath?: string, cwd = process.cw
|
|
|
452
503
|
}
|
|
453
504
|
|
|
454
505
|
if (isExclusiveConfigMode()) {
|
|
455
|
-
return { config: resolveConfiguredClaudePluginMcp(config, cwd), projectServers, projectServerPolicy };
|
|
506
|
+
return { config: stripProjectProviderAuth(resolveConfiguredClaudePluginMcp(config, cwd), projectServers), projectServers, projectServerPolicy };
|
|
456
507
|
}
|
|
457
508
|
|
|
458
509
|
const packageConfig = loadPackageMcpConfigs(cwd);
|
|
@@ -477,12 +528,25 @@ export function loadMcpConfigWithSources(overridePath?: string, cwd = process.cw
|
|
|
477
528
|
for (const name of claudePluginServers) projectServers.set(name, projectClaudePluginSource);
|
|
478
529
|
}
|
|
479
530
|
return {
|
|
480
|
-
config: mergedConfig,
|
|
531
|
+
config: stripProjectProviderAuth(mergedConfig, projectServers),
|
|
481
532
|
projectServers,
|
|
482
533
|
projectServerPolicy,
|
|
483
534
|
};
|
|
484
535
|
}
|
|
485
536
|
|
|
537
|
+
/** Pi provider tokens go only to servers from user-global config; Pi forbids `auth` in project files too. */
|
|
538
|
+
function stripProjectProviderAuth(config: McpConfig, projectServers: Map<string, ProjectServerSource>): McpConfig {
|
|
539
|
+
const stripped = [...projectServers.keys()].filter((name) => typeof config.mcpServers[name]?.auth === "object");
|
|
540
|
+
if (stripped.length === 0) return config;
|
|
541
|
+
const mcpServers = { ...config.mcpServers };
|
|
542
|
+
for (const name of stripped) {
|
|
543
|
+
delete mcpServers[name];
|
|
544
|
+
projectServers.delete(name);
|
|
545
|
+
}
|
|
546
|
+
console.warn(`Ignoring MCP servers ${stripped.map((name) => `"${name}"`).join(", ")}: auth.provider is only allowed in user-global config, and project config defines or overrides them`);
|
|
547
|
+
return { ...config, mcpServers };
|
|
548
|
+
}
|
|
549
|
+
|
|
486
550
|
export function resolveConfiguredClaudePluginMcp(config: McpConfig, cwd = process.cwd()): McpConfig {
|
|
487
551
|
return mergeClaudePluginMcpDefaults(config.claudePlugins, config, cwd);
|
|
488
552
|
}
|
|
@@ -522,7 +586,7 @@ function applySettingDefaults(config: McpConfig): McpConfig {
|
|
|
522
586
|
function getMergedSettings(overridePath?: string, cwd = process.cwd()): McpSettings | undefined {
|
|
523
587
|
let settings: McpSettings | undefined;
|
|
524
588
|
for (const source of getConfigSources(overridePath, cwd)) {
|
|
525
|
-
const loaded =
|
|
589
|
+
const loaded = readSourceConfig(source.id, source.readPath);
|
|
526
590
|
if (loaded?.settings) settings = { ...settings, ...loaded.settings };
|
|
527
591
|
}
|
|
528
592
|
return settings;
|
|
@@ -576,7 +640,7 @@ function getConfigConflicts(
|
|
|
576
640
|
}
|
|
577
641
|
}
|
|
578
642
|
for (const source of sourceSpecs) {
|
|
579
|
-
const loaded =
|
|
643
|
+
const loaded = readSourceConfig(source.id, source.readPath);
|
|
580
644
|
if (!loaded) continue;
|
|
581
645
|
if (loaded.imports?.length) {
|
|
582
646
|
for (const importKind of loaded.imports) {
|
|
@@ -646,6 +710,20 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
646
710
|
});
|
|
647
711
|
}
|
|
648
712
|
|
|
713
|
+
const piMcpGlobalPath = getPiMcpGlobalConfigPath();
|
|
714
|
+
if (piMcpConfigEnabled && piMcpGlobalPath !== userPath && !sources.some((source) => source.readPath === piMcpGlobalPath)) {
|
|
715
|
+
sources.push({
|
|
716
|
+
id: "pi-mcp-global",
|
|
717
|
+
label: "user-global Pi MCP",
|
|
718
|
+
readPath: piMcpGlobalPath,
|
|
719
|
+
writePath: userPath,
|
|
720
|
+
kind: "import",
|
|
721
|
+
importKind: "Pi mcp.json",
|
|
722
|
+
shared: true,
|
|
723
|
+
scope: "global",
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
649
727
|
sources.push({
|
|
650
728
|
id: "pi-global",
|
|
651
729
|
label: "MCP adapter global override",
|
|
@@ -658,10 +736,12 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
658
736
|
|
|
659
737
|
// Compare file identities so symlink aliases cannot reload a global source
|
|
660
738
|
// at ancestor precedence. Keep original paths for display and writes.
|
|
739
|
+
const projectPiMcpPath = getProjectPiMcpConfigPath(cwd);
|
|
661
740
|
const reservedPaths = new Set([
|
|
662
741
|
...sources.map((source) => getConfigPathIdentity(source.readPath)),
|
|
663
742
|
getConfigPathIdentity(projectPath),
|
|
664
743
|
getConfigPathIdentity(projectPiPath),
|
|
744
|
+
...(piMcpConfigEnabled ? [getConfigPathIdentity(projectPiMcpPath)] : []),
|
|
665
745
|
]);
|
|
666
746
|
// Only user-global files (including an explicit override) may opt in to
|
|
667
747
|
// ancestor discovery. Project files cannot extend this trust boundary.
|
|
@@ -705,6 +785,20 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
705
785
|
});
|
|
706
786
|
}
|
|
707
787
|
|
|
788
|
+
// Pi reads `.pi/mcp.json` only in the cwd, so it is not part of ancestor discovery.
|
|
789
|
+
if (piMcpConfigEnabled && projectPiMcpPath !== userPath && projectPiMcpPath !== piMcpGlobalPath) {
|
|
790
|
+
sources.push({
|
|
791
|
+
id: "pi-mcp-project",
|
|
792
|
+
label: "project Pi MCP",
|
|
793
|
+
readPath: projectPiMcpPath,
|
|
794
|
+
writePath: projectPiPath,
|
|
795
|
+
kind: "import",
|
|
796
|
+
importKind: "Pi project mcp.json",
|
|
797
|
+
shared: true,
|
|
798
|
+
scope: "project",
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
708
802
|
if (projectPiPath !== userPath && projectPiPath !== projectPath) {
|
|
709
803
|
sources.push({
|
|
710
804
|
id: "pi-project",
|
|
@@ -737,7 +831,7 @@ function isWithin(base: string, target: string): boolean {
|
|
|
737
831
|
function getConfiguredAncestorRoot(globalSources: ConfigSourceSpec[], cwd: string): string | undefined {
|
|
738
832
|
let configured: unknown;
|
|
739
833
|
for (const source of globalSources) {
|
|
740
|
-
const roots =
|
|
834
|
+
const roots = readSourceConfig(source.id, source.readPath)?.settings?.ancestorConfigRoots;
|
|
741
835
|
if (roots !== undefined) configured = roots;
|
|
742
836
|
}
|
|
743
837
|
if (configured === undefined || (Array.isArray(configured) && configured.length === 0)) return undefined;
|
|
@@ -853,6 +947,8 @@ function mergeServerMaps(
|
|
|
853
947
|
for (const field of URL_BOUND_AUTH_FIELDS) {
|
|
854
948
|
delete baseEntry[field];
|
|
855
949
|
}
|
|
950
|
+
// A provider token is bound to the url that asked for it, like the fields above.
|
|
951
|
+
if (typeof baseEntry.auth === "object") delete baseEntry.auth;
|
|
856
952
|
if (baseEntry.oauth !== false) {
|
|
857
953
|
delete baseEntry.oauth;
|
|
858
954
|
}
|
|
@@ -1031,6 +1127,159 @@ function readValidatedConfig(path: string, label: string): McpConfig | null {
|
|
|
1031
1127
|
}
|
|
1032
1128
|
}
|
|
1033
1129
|
|
|
1130
|
+
function isPiMcpSource(id: ConfigSourceSpec["id"]): boolean {
|
|
1131
|
+
return id === "pi-mcp-global" || id === "pi-mcp-project";
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function readSourceConfig(id: ConfigSourceSpec["id"], path: string): (McpConfig & { ignoredSettings?: Map<string, string[]> }) | null {
|
|
1135
|
+
return isPiMcpSource(id) ? readPiMcpConfig(path) : readValidatedConfig(path, `MCP config from ${path}`);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
interface PiMcpConfigFile {
|
|
1139
|
+
mcpServers: Record<string, ServerEntry>;
|
|
1140
|
+
/** Top-level keys of old adapter configs, which only mcp-adapter.json reads. */
|
|
1141
|
+
adapterKeys: string[];
|
|
1142
|
+
skipped: string[];
|
|
1143
|
+
ignoredSettings: Map<string, string[]>;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const PI_MCP_EXPOSURES = ["codemode", "codemode-deferred", "deferred", "direct", "hidden"];
|
|
1147
|
+
|
|
1148
|
+
/** Pi's top-level `autoEnableCodemode` has no adapter equivalent and is ignored. */
|
|
1149
|
+
function readPiMcpConfig(path: string): PiMcpConfigFile | null {
|
|
1150
|
+
if (!existsSync(path)) return null;
|
|
1151
|
+
let raw: Record<string, unknown>;
|
|
1152
|
+
try {
|
|
1153
|
+
const text = readFileSync(path, "utf-8");
|
|
1154
|
+
if (stripJsonComments(text, { trailingCommas: true }).trim() === "") return null;
|
|
1155
|
+
const parsed = parseJsonWithComments(text);
|
|
1156
|
+
if (!isRecord(parsed) || (parsed.mcpServers !== undefined && !isRecord(parsed.mcpServers))) {
|
|
1157
|
+
throw new Error('expected an object with an "mcpServers" object');
|
|
1158
|
+
}
|
|
1159
|
+
raw = parsed;
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
console.warn(`Failed to load Pi MCP config from ${path}:`, error);
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
const file: PiMcpConfigFile = {
|
|
1165
|
+
mcpServers: {},
|
|
1166
|
+
adapterKeys: ["settings", "imports", "claudePlugins", "mcp-servers"].filter((key) => raw[key] !== undefined),
|
|
1167
|
+
skipped: [],
|
|
1168
|
+
ignoredSettings: new Map(),
|
|
1169
|
+
};
|
|
1170
|
+
for (const [name, value] of Object.entries((raw.mcpServers ?? {}) as Record<string, unknown>)) {
|
|
1171
|
+
const translated = translatePiMcpServer(name, value);
|
|
1172
|
+
if (typeof translated === "string") {
|
|
1173
|
+
file.skipped.push(`"${name}" (${translated})`);
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
file.mcpServers[name] = translated.entry;
|
|
1177
|
+
if (translated.ignored.length > 0) file.ignoredSettings.set(name, translated.ignored);
|
|
1178
|
+
}
|
|
1179
|
+
return file;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** Returns why the entry is skipped when Pi would reject it or the adapter can't run it. */
|
|
1183
|
+
export function translatePiMcpServer(name: string, value: unknown): { entry: ServerEntry; ignored: string[] } | string {
|
|
1184
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) return 'invalid server name (use letters, digits, "_" and "-")';
|
|
1185
|
+
if (!isRecord(value)) return "must be an object";
|
|
1186
|
+
const { type, command, args, env, cwd, url, headers, oauth, exposure, toolExposure, enabled, timeout, auth, description, ...unknown } = value;
|
|
1187
|
+
const isExposure = (candidate: unknown) => typeof candidate === "string" && PI_MCP_EXPOSURES.includes(candidate);
|
|
1188
|
+
if (exposure !== undefined && !isExposure(exposure)) return `exposure must be one of ${PI_MCP_EXPOSURES.join(", ")}`;
|
|
1189
|
+
if (toolExposure !== undefined && (!isRecord(toolExposure) || !Object.values(toolExposure).every(isExposure))) {
|
|
1190
|
+
return `toolExposure must map tool names to one of ${PI_MCP_EXPOSURES.join(", ")}`;
|
|
1191
|
+
}
|
|
1192
|
+
if (enabled !== undefined && typeof enabled !== "boolean") return "enabled must be a boolean";
|
|
1193
|
+
if (timeout !== undefined && (typeof timeout !== "number" || !(timeout > 0))) return "timeout must be a positive number of seconds";
|
|
1194
|
+
if (description !== undefined && typeof description !== "string") return "description must be a string";
|
|
1195
|
+
if (type === "sse") return "legacy SSE transport is not supported; use the streamable HTTP URL";
|
|
1196
|
+
const ignored = Object.keys(unknown);
|
|
1197
|
+
|
|
1198
|
+
let entry: ServerEntry;
|
|
1199
|
+
let otherTransportKeys: string[];
|
|
1200
|
+
if (typeof url === "string" && (type === undefined || type === "http" || type === "streamable-http")) {
|
|
1201
|
+
if (!URL.canParse(url) || !/^https?:$/.test(new URL(url).protocol)) return "url must be an http or https URL";
|
|
1202
|
+
if (headers !== undefined && !isStringRecord(headers)) return "headers must map names to strings";
|
|
1203
|
+
const translatedOAuth = oauth === undefined ? undefined : translatePiOAuth(oauth);
|
|
1204
|
+
if (typeof translatedOAuth === "string") return translatedOAuth;
|
|
1205
|
+
entry = { url, ...(headers !== undefined ? { headers } : {}), ...(translatedOAuth ? { oauth: translatedOAuth.oauth } : {}) };
|
|
1206
|
+
if (auth !== undefined) {
|
|
1207
|
+
if (!isRecord(auth) || typeof auth.provider !== "string" || !auth.provider) return "auth.provider must be a provider name";
|
|
1208
|
+
const urlError = providerAuthUrlError(url);
|
|
1209
|
+
if (urlError) return urlError;
|
|
1210
|
+
entry.auth = { provider: auth.provider };
|
|
1211
|
+
}
|
|
1212
|
+
ignored.push(...translatedOAuth?.ignored ?? []);
|
|
1213
|
+
otherTransportKeys = ["command", "args", "env", "cwd"];
|
|
1214
|
+
} else if (typeof command === "string" && (type === undefined || type === "stdio")) {
|
|
1215
|
+
if (args !== undefined && !(Array.isArray(args) && args.every((arg) => typeof arg === "string"))) return "args must be an array of strings";
|
|
1216
|
+
if (env !== undefined && !isStringRecord(env)) return "env must map names to strings";
|
|
1217
|
+
if (cwd !== undefined && typeof cwd !== "string") return "cwd must be a string";
|
|
1218
|
+
entry = {
|
|
1219
|
+
command,
|
|
1220
|
+
...(args !== undefined ? { args: args as string[] } : {}),
|
|
1221
|
+
...(env !== undefined ? { env } : {}),
|
|
1222
|
+
...(cwd !== undefined ? { cwd } : {}),
|
|
1223
|
+
};
|
|
1224
|
+
otherTransportKeys = ["url", "headers", "oauth", "auth"];
|
|
1225
|
+
} else {
|
|
1226
|
+
return 'needs either "command" (stdio) or "url" (streamable HTTP)';
|
|
1227
|
+
}
|
|
1228
|
+
ignored.push(...otherTransportKeys.filter((key) => value[key] !== undefined));
|
|
1229
|
+
if (description !== undefined) entry.description = description;
|
|
1230
|
+
|
|
1231
|
+
// `codemode-deferred` is an alias of `codemode` since Pi 0.99.2; both are proxy-only here.
|
|
1232
|
+
const serverExposure = exposure === undefined || exposure === "codemode-deferred" ? "codemode" : exposure;
|
|
1233
|
+
if (enabled === false || serverExposure === "hidden") entry.disabled = true;
|
|
1234
|
+
if (serverExposure === "direct") entry.directTools = true;
|
|
1235
|
+
if (serverExposure === "deferred") entry.directTools = "search";
|
|
1236
|
+
if (timeout !== undefined) entry.requestTimeoutMs = Math.round(timeout * 1000);
|
|
1237
|
+
|
|
1238
|
+
// excludeTools matches a superset of Pi's hidden entries (prefixed names, aliases, `?` as a
|
|
1239
|
+
// wildcard), so it never exposes a tool Pi hides.
|
|
1240
|
+
const directTools: string[] = [];
|
|
1241
|
+
const excludeTools: string[] = [];
|
|
1242
|
+
for (const [tool, rawValue] of Object.entries(toolExposure ?? {})) {
|
|
1243
|
+
const toolValue = rawValue === "codemode-deferred" ? "codemode" : rawValue;
|
|
1244
|
+
if (toolValue === serverExposure) continue;
|
|
1245
|
+
if (toolValue === "hidden") excludeTools.push(tool);
|
|
1246
|
+
else if (toolValue === "direct" && !tool.includes("*") && serverExposure === "codemode") directTools.push(tool);
|
|
1247
|
+
else ignored.push(`toolExposure ${JSON.stringify(tool)}: ${toolValue}`);
|
|
1248
|
+
}
|
|
1249
|
+
if (directTools.length > 0) entry.directTools = directTools;
|
|
1250
|
+
if (excludeTools.length > 0) entry.excludeTools = excludeTools;
|
|
1251
|
+
return { entry, ignored };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function translatePiOAuth(oauth: unknown): { oauth: OAuthConfig; ignored: string[] } | string {
|
|
1255
|
+
if (!isRecord(oauth)) return "oauth must be an object";
|
|
1256
|
+
const { clientId, clientSecret, scope, clientName, callbackPort, callbackUrl, ...unknown } = oauth;
|
|
1257
|
+
const copied = { clientId, clientSecret, scope, clientName };
|
|
1258
|
+
for (const [key, field] of Object.entries(copied)) {
|
|
1259
|
+
if (field !== undefined && (typeof field !== "string" || (key === "clientName" && !field.trim()))) {
|
|
1260
|
+
return `oauth.${key} must be a ${key === "clientName" ? "non-empty " : ""}string`;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (callbackPort !== undefined && (typeof callbackPort !== "number" || !Number.isInteger(callbackPort) || callbackPort < 1 || callbackPort > 65535)) {
|
|
1264
|
+
return "oauth.callbackPort must be a port number";
|
|
1265
|
+
}
|
|
1266
|
+
const result = Object.fromEntries(Object.entries(copied).filter(([, field]) => field !== undefined)) as OAuthConfig;
|
|
1267
|
+
if (callbackUrl !== undefined) {
|
|
1268
|
+
const parsed = typeof callbackUrl === "string" && URL.canParse(callbackUrl) ? new URL(callbackUrl) : undefined;
|
|
1269
|
+
if (!parsed || parsed.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname) || parsed.search !== "" || parsed.hash !== "") {
|
|
1270
|
+
return "oauth.callbackUrl must be an http URI on localhost, 127.0.0.1, or [::1] without query or fragment";
|
|
1271
|
+
}
|
|
1272
|
+
if (parsed.port && callbackPort !== undefined && Number(parsed.port) !== callbackPort) {
|
|
1273
|
+
return "oauth.callbackUrl and oauth.callbackPort name different ports";
|
|
1274
|
+
}
|
|
1275
|
+
// Pi sends a URI with a port as written; without one it adds callbackPort or a free port.
|
|
1276
|
+
result.redirectUri = parsed.port ? callbackUrl as string : `http://${parsed.hostname}:${callbackPort ?? "{port}"}${parsed.pathname}`;
|
|
1277
|
+
} else if (callbackPort !== undefined) {
|
|
1278
|
+
result.redirectUri = `http://127.0.0.1:${callbackPort}/callback`;
|
|
1279
|
+
}
|
|
1280
|
+
return { oauth: result, ignored: Object.keys(unknown).map((key) => `oauth.${key}`) };
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1034
1283
|
function validateConfig(raw: unknown): McpConfig {
|
|
1035
1284
|
if (!isRecord(raw)) {
|
|
1036
1285
|
return { mcpServers: {} };
|
|
@@ -1087,7 +1336,27 @@ function toServerEntries(servers: unknown): Record<string, ServerEntry> {
|
|
|
1087
1336
|
if (!isRecord(servers)) return {};
|
|
1088
1337
|
const entries: Record<string, ServerEntry> = {};
|
|
1089
1338
|
for (const [name, entry] of Object.entries(servers)) {
|
|
1090
|
-
if (isServerEntry(entry))
|
|
1339
|
+
if (!isServerEntry(entry)) continue;
|
|
1340
|
+
const auth: unknown = entry.auth;
|
|
1341
|
+
if (typeof auth === "object") {
|
|
1342
|
+
const provider = isRecord(auth) ? auth.provider : undefined;
|
|
1343
|
+
const error = typeof provider !== "string" || !provider ? "auth.provider must be a provider name"
|
|
1344
|
+
: typeof entry.url !== "string" ? "auth.provider requires a url"
|
|
1345
|
+
// A URL with env references is checked once resolved, when it connects.
|
|
1346
|
+
: getMissingEnvVars(entry.url, {}).length > 0 ? undefined
|
|
1347
|
+
: providerAuthUrlError(entry.url);
|
|
1348
|
+
if (error) {
|
|
1349
|
+
console.warn(`Ignoring MCP server "${name}": ${error}`);
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (entry.description !== undefined && typeof entry.description !== "string") {
|
|
1354
|
+
console.warn(`Ignoring invalid description for MCP server "${name}": expected a string`);
|
|
1355
|
+
const { description: _description, ...rest } = entry;
|
|
1356
|
+
entries[name] = rest;
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
entries[name] = entry;
|
|
1091
1360
|
}
|
|
1092
1361
|
return entries;
|
|
1093
1362
|
}
|
|
@@ -1100,6 +1369,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
1100
1369
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1101
1370
|
}
|
|
1102
1371
|
|
|
1372
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
1373
|
+
return isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1103
1376
|
function mergeOpenCodeConfigs(base: Record<string, unknown>, next: Record<string, unknown>): Record<string, unknown> {
|
|
1104
1377
|
const baseMcp = base.mcp;
|
|
1105
1378
|
const nextMcp = next.mcp;
|
|
@@ -1268,7 +1541,7 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
|
|
|
1268
1541
|
mappedServers[name] = mapped as ServerEntry;
|
|
1269
1542
|
}
|
|
1270
1543
|
|
|
1271
|
-
return mappedServers;
|
|
1544
|
+
return toServerEntries(mappedServers);
|
|
1272
1545
|
}
|
|
1273
1546
|
|
|
1274
1547
|
function serializeRawConfig(raw: Record<string, unknown>): string {
|
|
@@ -1468,7 +1741,7 @@ export function writeProjectServerDisabledOverride(
|
|
|
1468
1741
|
let lowerConfig: McpConfig = { mcpServers: {} };
|
|
1469
1742
|
for (const source of getConfigSources(overridePath, cwd)) {
|
|
1470
1743
|
if (source.readPath === filePath) continue;
|
|
1471
|
-
const loaded =
|
|
1744
|
+
const loaded = readSourceConfig(source.id, source.readPath);
|
|
1472
1745
|
if (loaded) lowerConfig = mergeConfigs(lowerConfig, expandImports(loaded, cwd).config);
|
|
1473
1746
|
}
|
|
1474
1747
|
if (raw.imports !== undefined) {
|
|
@@ -1531,10 +1804,10 @@ function buildRepoPromptEntry(executablePath: string): ServerEntry {
|
|
|
1531
1804
|
};
|
|
1532
1805
|
}
|
|
1533
1806
|
|
|
1534
|
-
function detectRepoPrompt(summary: Omit<McpDiscoverySummary, "fingerprint" | "repoPrompt">, cwd = process.cwd()): RepoPromptDiscovery {
|
|
1807
|
+
function detectRepoPrompt(summary: Omit<McpDiscoverySummary, "fingerprint" | "repoPrompt" | "knownServerPresets">, cwd = process.cwd()): RepoPromptDiscovery {
|
|
1535
1808
|
for (const source of summary.sources) {
|
|
1536
1809
|
if (source.kind !== "shared" || source.serverCount === 0) continue;
|
|
1537
|
-
const config =
|
|
1810
|
+
const config = readSourceConfig(source.id, source.path);
|
|
1538
1811
|
if (!config) continue;
|
|
1539
1812
|
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
1540
1813
|
if (isRepoPromptServer(name, entry)) {
|
|
@@ -1654,9 +1927,8 @@ export function getServerProvenance(overridePath?: string, cwd = process.cwd()):
|
|
|
1654
1927
|
}
|
|
1655
1928
|
|
|
1656
1929
|
for (const source of getConfigSources(overridePath, cwd)) {
|
|
1657
|
-
const loaded =
|
|
1930
|
+
const loaded = readSourceConfig(source.id, source.readPath);
|
|
1658
1931
|
if (!loaded) continue;
|
|
1659
|
-
|
|
1660
1932
|
if (loaded.imports?.length) {
|
|
1661
1933
|
for (const importKind of loaded.imports) {
|
|
1662
1934
|
const imported = loadImportedConfig(importKind, cwd, `Failed to inspect imported MCP config from ${importKind}:`);
|
|
@@ -1672,10 +1944,13 @@ export function getServerProvenance(overridePath?: string, cwd = process.cwd()):
|
|
|
1672
1944
|
}
|
|
1673
1945
|
|
|
1674
1946
|
for (const name of Object.keys(loaded.mcpServers)) {
|
|
1947
|
+
// A Pi entry brings its own list; other sources keep the one from the Pi entry below them.
|
|
1948
|
+
const ignoredSettings = loaded.ignoredSettings ? loaded.ignoredSettings.get(name) : provenance.get(name)?.ignoredSettings;
|
|
1675
1949
|
provenance.set(name, {
|
|
1676
1950
|
path: source.writePath,
|
|
1677
1951
|
kind: source.kind,
|
|
1678
1952
|
...(source.importKind !== undefined ? { importKind: source.importKind } : {}),
|
|
1953
|
+
...(ignoredSettings !== undefined ? { ignoredSettings } : {}),
|
|
1679
1954
|
});
|
|
1680
1955
|
}
|
|
1681
1956
|
}
|
package/direct-tool-surface.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Check, Errors } from "typebox/value";
|
|
2
|
-
import type { DirectToolSpec, McpConfig, ToolPrefix } from "./types.ts";
|
|
2
|
+
import type { DirectToolSpec, McpConfig, ToolPrefix, ToolSelectorCandidateIndex } from "./types.ts";
|
|
3
3
|
import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix, resolveUniqueNameOwnership } from "./types.ts";
|
|
4
4
|
import type { MetadataCache } from "./metadata-cache.ts";
|
|
5
5
|
import { isServerCacheValid, parseDirectToolSelectors } from "./metadata-cache.ts";
|
|
@@ -14,7 +14,7 @@ export function getLargeDirectToolsAdvisory(config: McpConfig, specs: readonly D
|
|
|
14
14
|
if (config.settings?.warnOnLargeDirectTools === false) return undefined;
|
|
15
15
|
const eagerCount = specs.filter((spec) => !spec.lazy).length;
|
|
16
16
|
if (eagerCount < DIRECT_TOOLS_ADVISORY_THRESHOLD) return undefined;
|
|
17
|
-
return `MCP: ${eagerCount} direct tools resolved. Each direct tool adds prompt context;
|
|
17
|
+
return `MCP: ${eagerCount} direct tools resolved. Each direct tool adds prompt context; the direct tools guide in docs/tools.md 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.`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -82,6 +82,7 @@ export function resolveDirectTools(
|
|
|
82
82
|
|
|
83
83
|
const envSelection = envOverride ? parseDirectToolSelectors(envOverride) : null;
|
|
84
84
|
const globalDirect = config.settings?.directTools;
|
|
85
|
+
let selectorCandidateIndex: ToolSelectorCandidateIndex | undefined;
|
|
85
86
|
|
|
86
87
|
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
87
88
|
if (isServerDisabled(definition)) continue;
|
|
@@ -115,7 +116,7 @@ export function resolveDirectTools(
|
|
|
115
116
|
const hasToolFilters =
|
|
116
117
|
(Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
|
|
117
118
|
(Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
|
|
118
|
-
|
|
119
|
+
if (hasToolFilters && !selectorCandidateIndex) {
|
|
119
120
|
const candidates = new Set<string>();
|
|
120
121
|
for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
|
|
121
122
|
const otherCache = cache.servers[otherServerName];
|
|
@@ -132,8 +133,8 @@ export function resolveDirectTools(
|
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
|
-
|
|
136
|
-
}
|
|
136
|
+
selectorCandidateIndex = createToolSelectorCandidateIndex(candidates);
|
|
137
|
+
}
|
|
137
138
|
|
|
138
139
|
for (const tool of serverCache.tools ?? []) {
|
|
139
140
|
if (!isUiToolVisibleToModel(tool.uiVisibility)) continue;
|
|
@@ -199,8 +200,9 @@ export function resolveDirectTools(
|
|
|
199
200
|
* Live counts/status belong to `mcp({ })`, full instructions to
|
|
200
201
|
* `mcp({ instructions })`.
|
|
201
202
|
*/
|
|
202
|
-
export function buildProxyDescription(config: McpConfig): string {
|
|
203
|
-
|
|
203
|
+
export function buildProxyDescription(config: McpConfig, scriptTool = false): string {
|
|
204
|
+
const scriptHint = scriptTool ? " Use mcpScript for several MCP calls with logic between them." : "";
|
|
205
|
+
let desc = `MCP gateway — URL installation, server status, tool search/describe, auth, and single MCP tool calls. When a user supplies an MCP endpoint URL, install it with the install action.${scriptHint} Non-MCP Pi tools should be called directly, not through mcp.\n`;
|
|
204
206
|
|
|
205
207
|
const serverNames = Object.keys(config.mcpServers)
|
|
206
208
|
.filter((serverName) => !isServerDisabled(config.mcpServers[serverName]));
|