pi-mcp-adapter 5.3.1 → 5.3.2
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 +19 -0
- package/commands.ts +5 -3
- package/config.ts +107 -9
- package/dist/config.js +94 -10
- package/dist/config.js.map +1 -1
- package/dist/metadata-cache.d.ts +5 -0
- package/dist/metadata-cache.js +19 -0
- package/dist/metadata-cache.js.map +1 -1
- package/dist/project-server-trust.d.ts +8 -0
- package/dist/project-server-trust.js +45 -12
- package/dist/project-server-trust.js.map +1 -1
- package/dist/sampling-handler.d.ts +3 -2
- package/dist/sampling-handler.js +12 -3
- package/dist/sampling-handler.js.map +1 -1
- package/docs/configuration.md +3 -1
- package/index.ts +2 -17
- package/init.ts +1 -0
- package/metadata-cache.ts +21 -0
- package/package.json +1 -1
- package/project-server-trust.ts +49 -12
- package/sampling-handler.ts +21 -15
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [5.3.2] - 2026-10-17
|
|
11
|
+
|
|
12
|
+
### Highlights
|
|
13
|
+
|
|
14
|
+
- The GitHub preset in `/mcp-adapter setup` now signs in with your GitHub CLI login, so connecting to GitHub works.
|
|
15
|
+
- Saving direct tools in the MCP panel no longer brings back the "Allow project MCP server?" prompt for a server you already allowed.
|
|
16
|
+
- The MCP panel shows a connected server's tools and your saved direct tools, even for servers like GitHub that don't let their tool list be cached.
|
|
17
|
+
- MCP sampling uses your Pi thinking level, so servers that sample through a reasoning-only model stop failing.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- The GitHub preset in `/mcp-adapter setup` now signs in with your GitHub CLI login (`gh auth token`). Before, it used MCP OAuth, which GitHub doesn't offer to apps it hasn't registered, so `/mcp-auth github` always failed with "does not support dynamic client registration". Install `gh` and run `gh auth login` first. If you added the old preset, remove its `github` entry and add GitHub again from `/mcp-adapter setup`. Thanks to [@Milor123](https://github.com/Milor123) for [#841](https://github.com/nicobailon/pi-mcp-adapter/issues/841).
|
|
22
|
+
- When one config file gives a server a different `url` than another file that set its headers or token, the adapter still leaves those credentials out so they aren't sent to the new address, but it now tells you at startup which server, which settings and which two files are involved. Before, the credentials disappeared silently and the server failed with sign-in errors. Thanks to [@Milor123](https://github.com/Milor123) for [#841](https://github.com/nicobailon/pi-mcp-adapter/issues/841).
|
|
23
|
+
- The MCP panel now lists the tools of a server you already connected to in this session, with your saved direct tools checked, even when the server doesn't let its tool list be cached, as with GitHub. Before, such a server showed "(not cached)" with no tools every time you opened the panel, so direct tools you had saved looked lost. Thanks to [@Milor123](https://github.com/Milor123) for [#841](https://github.com/nicobailon/pi-mcp-adapter/issues/841).
|
|
24
|
+
- Saving direct tools in the MCP panel for a project server you already allowed no longer makes Pi ask "Allow project MCP server?" again on the next start or `/reload`. Saving used to change the server's config in a way that cancelled your approval. A server you didn't allow still asks, and so does any other change to the server's config. Thanks to [@Milor123](https://github.com/Milor123) for [#841](https://github.com/nicobailon/pi-mcp-adapter/issues/841).
|
|
25
|
+
- Approving a project MCP server now sticks when Pi is started from the same folder spelled with different capitalization, such as `C:\Users\Me` and `c:\users\me` on Windows. Before, each spelling counted as a separate project and asked "Allow project MCP server?" again. An approval saved under a differently capitalized spelling asks once more after this update. Thanks to [@Milor123](https://github.com/Milor123) for [#841](https://github.com/nicobailon/pi-mcp-adapter/issues/841).
|
|
26
|
+
- MCP sampling requests now use your current Pi thinking level, so a server that samples through a model that requires reasoning no longer fails with "Reasoning is mandatory for this endpoint and cannot be disabled". Before, sampling always ran with thinking off. On Pi 0.84 and 0.85 sampling still runs with thinking off. Thanks to [@bardic](https://github.com/bardic) for [#846](https://github.com/nicobailon/pi-mcp-adapter/issues/846).
|
|
27
|
+
|
|
10
28
|
## [5.3.1] - 2026-10-15
|
|
11
29
|
|
|
12
30
|
### Highlights
|
|
@@ -136,6 +154,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
136
154
|
- `/mcp-adapter setup` adds Figma (desktop) to the global config instead of writing `.mcp.json` into whatever project you opened setup from.
|
|
137
155
|
- `/mcp-adapter setup` warns when a server you add won't be used, because another config file already defines or disables it, or the current config mode doesn't read the file it was written to.
|
|
138
156
|
- A bearer token or header value that isn't a valid HTTP header value, such as one containing a newline, no longer appears in connection errors. This covers `bearerToken` (including `!command` and `${VAR}` forms), `bearerTokenEnv`, and configured `headers`; the error now names the server and header instead.
|
|
157
|
+
|
|
139
158
|
## [4.0.0] - 2026-09-30
|
|
140
159
|
|
|
141
160
|
### Highlights
|
package/commands.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
} from "./config.ts";
|
|
27
27
|
import { markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateMetadataCache, updateStatusBar, getFailureAgeSeconds, getFailureMessage, clearFailure, recordFailure } from "./init.ts";
|
|
28
28
|
import { isServerInActiveFailureBackoff } from "./failure-backoff.ts";
|
|
29
|
-
import { loadMetadataCache, reconstructPromptMetadata } from "./metadata-cache.ts";
|
|
29
|
+
import { loadMetadataCache, reconstructPromptMetadata, withSessionMetadata } from "./metadata-cache.ts";
|
|
30
30
|
import { buildToolMetadata } from "./tool-metadata.ts";
|
|
31
31
|
import { supportsOAuth, authenticate, removeAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
32
32
|
import { getAuthStorageOptions, inspectAuthForUrl } from "./mcp-auth.ts";
|
|
@@ -36,7 +36,7 @@ import { findPiSignInImports, importPiSignIn } from "./pi-signin-import.ts";
|
|
|
36
36
|
import { formatTerminalError, openPath, providerSignInMessage, resolveServerUrl, sanitizeTerminalText } from "./utils.ts";
|
|
37
37
|
import { isAbortError } from "./runtime-owner.ts";
|
|
38
38
|
import { resolveJevCredential } from "./jev-key-store.ts";
|
|
39
|
-
import { describeProjectServerBlock } from "./project-server-trust.ts";
|
|
39
|
+
import { describeProjectServerBlock, keepProjectServerApprovals } from "./project-server-trust.ts";
|
|
40
40
|
|
|
41
41
|
function terminalHyperlink(label: string, url: string): string {
|
|
42
42
|
return `\u001B]8;;${sanitizeTerminalText(url)}\u001B\\${sanitizeTerminalText(label)}\u001B]8;;\u001B\\`;
|
|
@@ -792,7 +792,7 @@ export async function openMcpPanel(
|
|
|
792
792
|
}
|
|
793
793
|
|
|
794
794
|
const config = state.config;
|
|
795
|
-
const cache = loadMetadataCache();
|
|
795
|
+
const cache = withSessionMetadata(loadMetadataCache(), config, state.sessionMetadata);
|
|
796
796
|
const configPath = pi.getFlag("mcp-config") as string | undefined ?? configOverridePath;
|
|
797
797
|
const provenanceMap = getServerProvenance(configPath, ctx.cwd);
|
|
798
798
|
const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);
|
|
@@ -838,9 +838,11 @@ export async function openMcpPanel(
|
|
|
838
838
|
}
|
|
839
839
|
}
|
|
840
840
|
if (!result.cancelled && result.changes.size > 0) {
|
|
841
|
+
const before = loadMcpConfig(configPath, ctx.cwd);
|
|
841
842
|
writeDirectToolsConfig(result.changes, provenanceMap, config, () => {
|
|
842
843
|
directToolsFileWritten = true;
|
|
843
844
|
});
|
|
845
|
+
keepProjectServerApprovals(ctx, before, loadMcpConfig(configPath, ctx.cwd), result.changes.keys());
|
|
844
846
|
directToolsSaved = true;
|
|
845
847
|
await onDirectToolsConfigChanged?.(result.changes);
|
|
846
848
|
ctx.ui.notify("Direct tools updated for this session.", "info");
|
package/config.ts
CHANGED
|
@@ -79,8 +79,10 @@ export const KNOWN_SERVER_PRESETS: readonly KnownServerPreset[] = [
|
|
|
79
79
|
{
|
|
80
80
|
id: "github",
|
|
81
81
|
name: "GitHub",
|
|
82
|
-
summary: "Work with GitHub
|
|
83
|
-
|
|
82
|
+
summary: "Work with GitHub using your GitHub CLI sign-in. Needs `gh` installed and `gh auth login` done.",
|
|
83
|
+
// GitHub's OAuth server has no dynamic client registration, so MCP OAuth can't sign in;
|
|
84
|
+
// the GitHub CLI token works and is read at connect time, never written to config.
|
|
85
|
+
entry: { url: "https://api.githubcopilot.com/mcp/", auth: "bearer", bearerToken: "!gh auth token", protocolVersion: "auto" },
|
|
84
86
|
},
|
|
85
87
|
{
|
|
86
88
|
id: "chrome-devtools",
|
|
@@ -269,7 +271,88 @@ function legacyMcpConfigHasContent(filePath: string): boolean {
|
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
export function getLegacyMcpMigrationNotices(cwd = process.cwd(), overridePath?: string): string[] {
|
|
272
|
-
return [
|
|
274
|
+
return [
|
|
275
|
+
...getPiMcpConfigNotices(cwd, overridePath),
|
|
276
|
+
...getOpenCodeImportNotices(cwd, overridePath),
|
|
277
|
+
...getDroppedCredentialNotices(cwd, overridePath),
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** server name -> credential field -> the file that supplied it. */
|
|
282
|
+
type CredentialFiles = Map<string, Map<string, string>>;
|
|
283
|
+
|
|
284
|
+
function credentialFiles(servers: Record<string, ServerEntry>, fileOf: (name: string, field: string) => string): CredentialFiles {
|
|
285
|
+
return new Map(Object.entries(servers).map(([name, definition]) => [
|
|
286
|
+
name,
|
|
287
|
+
new Map(CREDENTIAL_FIELDS.filter((field) => definition[field] !== undefined).map((field) => [field, fileOf(name, field)])),
|
|
288
|
+
]));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Replays the server merges of loadMcpConfigWithSources with the file behind each credential field,
|
|
293
|
+
* and reports the fields mergeServerMaps drops because a later file points the server at another url.
|
|
294
|
+
*/
|
|
295
|
+
function getDroppedCredentialNotices(cwd: string, overridePath?: string): string[] {
|
|
296
|
+
const notices: string[] = [];
|
|
297
|
+
const merge = (
|
|
298
|
+
base: Record<string, ServerEntry>,
|
|
299
|
+
baseFiles: CredentialFiles,
|
|
300
|
+
next: Record<string, ServerEntry>,
|
|
301
|
+
nextFiles: CredentialFiles,
|
|
302
|
+
urlFile: (name: string) => string,
|
|
303
|
+
): [Record<string, ServerEntry>, CredentialFiles] => {
|
|
304
|
+
const files: CredentialFiles = new Map(baseFiles);
|
|
305
|
+
for (const [name, definition] of Object.entries(next)) {
|
|
306
|
+
const existing = base[name];
|
|
307
|
+
const fieldFiles = new Map(files.get(name));
|
|
308
|
+
const byFile = new Map<string, string[]>();
|
|
309
|
+
for (const field of existing ? droppedUrlBoundAuth(existing, definition) : []) {
|
|
310
|
+
const from = fieldFiles.get(field);
|
|
311
|
+
if (from !== undefined) byFile.set(from, [...byFile.get(from) ?? [], field]);
|
|
312
|
+
fieldFiles.delete(field);
|
|
313
|
+
}
|
|
314
|
+
for (const [from, fields] of byFile) {
|
|
315
|
+
notices.push(`"${name}": ignoring ${fields.join(", ")} from ${from}, because ${urlFile(name)} sets a different url for this server. Use one url, or set ${fields.length === 1 ? "it" : "them"} next to that url.`);
|
|
316
|
+
}
|
|
317
|
+
for (const [field, path] of nextFiles.get(name) ?? []) fieldFiles.set(field, path);
|
|
318
|
+
files.set(name, fieldFiles);
|
|
319
|
+
}
|
|
320
|
+
return [mergeServerMaps(base, next), files];
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const host = !isExclusiveConfigMode() && getConfiguredHostConfigDiscovery(overridePath, cwd) === "on"
|
|
324
|
+
? loadDiscoveredHostConfigs(cwd)
|
|
325
|
+
: { config: { mcpServers: {} }, serverSources: new Map<string, ImportedServerSource>() };
|
|
326
|
+
const hostFile = (name: string) => host.serverSources.get(name)?.path ?? "a host config";
|
|
327
|
+
let servers = host.config.mcpServers;
|
|
328
|
+
let files = credentialFiles(servers, hostFile);
|
|
329
|
+
const sourceSpecs = getConfigSources(overridePath, cwd);
|
|
330
|
+
const piProjectSource = sourceSpecs.find((source) => source.id === "pi-mcp-project");
|
|
331
|
+
const piProjectNames = Object.keys((piProjectSource && readPiMcpConfig(piProjectSource.readPath))?.mcpServers ?? {});
|
|
332
|
+
for (const source of sourceSpecs) {
|
|
333
|
+
const loaded = readSourceConfig(source.id, source.readPath);
|
|
334
|
+
if (!loaded) continue;
|
|
335
|
+
if (source.id === "pi-mcp-global") {
|
|
336
|
+
for (const name of piProjectNames) delete loaded.mcpServers[name];
|
|
337
|
+
}
|
|
338
|
+
// The same two merges as expandImports and mergeConfigs: the file's own entries over its imports,
|
|
339
|
+
// then the file over the files before it.
|
|
340
|
+
const imported = expandImports({ ...loaded, mcpServers: {} }, cwd);
|
|
341
|
+
// An OpenCode project entry can inherit a credential from the global file, so name the file per key.
|
|
342
|
+
const importFile = (name: string, field: string) =>
|
|
343
|
+
imported.keySources?.get(name)?.get(field) ?? imported.serverSources.get(name)?.path ?? source.readPath;
|
|
344
|
+
const ownFile = () => source.readPath;
|
|
345
|
+
const [fileServers, fileFiles] = merge(
|
|
346
|
+
imported.config.mcpServers,
|
|
347
|
+
credentialFiles(imported.config.mcpServers, importFile),
|
|
348
|
+
loaded.mcpServers,
|
|
349
|
+
credentialFiles(loaded.mcpServers, ownFile),
|
|
350
|
+
ownFile,
|
|
351
|
+
);
|
|
352
|
+
const urlFile = (name: string) => typeof loaded.mcpServers[name]?.url === "string" ? source.readPath : importFile(name, "url");
|
|
353
|
+
[servers, files] = merge(servers, files, fileServers, fileFiles, urlFile);
|
|
354
|
+
}
|
|
355
|
+
return notices;
|
|
273
356
|
}
|
|
274
357
|
|
|
275
358
|
function getPiMcpConfigNotices(cwd: string, overridePath?: string): string[] {
|
|
@@ -476,6 +559,8 @@ interface ImportedServerSource {
|
|
|
476
559
|
interface LoadedHostConfig {
|
|
477
560
|
config: McpConfig;
|
|
478
561
|
serverSources: Map<string, ImportedServerSource>;
|
|
562
|
+
/** OpenCode imports only: the file that last set each key of each server. */
|
|
563
|
+
keySources?: Map<string, Map<string, string>>;
|
|
479
564
|
}
|
|
480
565
|
|
|
481
566
|
export const MCP_CONFIG_SOURCE_METADATA = Symbol.for("pi-mcp-adapter/config-source-metadata");
|
|
@@ -939,6 +1024,18 @@ function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
|
|
|
939
1024
|
// otherwise the original endpoint's credentials would be shipped to the new
|
|
940
1025
|
// url. See the SECURITY note in mergeServerMaps.
|
|
941
1026
|
const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv", "bearerTokenStore", "requestHeadersCommand", "caFile"] as const;
|
|
1027
|
+
const CREDENTIAL_FIELDS = [...URL_BOUND_AUTH_FIELDS, "auth", "oauth"] as const;
|
|
1028
|
+
|
|
1029
|
+
/** The credential fields of `existing` that mergeServerMaps drops for `definition`'s different url. */
|
|
1030
|
+
function droppedUrlBoundAuth(existing: ServerEntry, definition: ServerEntry): (typeof CREDENTIAL_FIELDS)[number][] {
|
|
1031
|
+
if (typeof definition.url !== "string" || definition.url === existing.url) return [];
|
|
1032
|
+
return CREDENTIAL_FIELDS.filter((field) => {
|
|
1033
|
+
if (Object.hasOwn(definition, field)) return false;
|
|
1034
|
+
if (field === "auth") return typeof existing.auth === "object";
|
|
1035
|
+
if (field === "oauth") return existing.oauth !== undefined && existing.oauth !== false;
|
|
1036
|
+
return existing[field] !== undefined;
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
942
1039
|
|
|
943
1040
|
function mergeServerMaps(
|
|
944
1041
|
base: Record<string, ServerEntry>,
|
|
@@ -985,14 +1082,11 @@ function mergeServerMaps(
|
|
|
985
1082
|
}
|
|
986
1083
|
if (existing && typeof definition.url === "string" && definition.url !== existing.url) {
|
|
987
1084
|
if (baseEntry === existing) baseEntry = { ...existing };
|
|
988
|
-
|
|
1085
|
+
// Provider tokens (object `auth`) and OAuth config are bound to the url too; the startup
|
|
1086
|
+
// notice reports exactly these fields, so both read the one rule.
|
|
1087
|
+
for (const field of droppedUrlBoundAuth(existing, definition)) {
|
|
989
1088
|
delete baseEntry[field];
|
|
990
1089
|
}
|
|
991
|
-
// A provider token is bound to the url that asked for it, like the fields above.
|
|
992
|
-
if (typeof baseEntry.auth === "object") delete baseEntry.auth;
|
|
993
|
-
if (baseEntry.oauth !== false) {
|
|
994
|
-
delete baseEntry.oauth;
|
|
995
|
-
}
|
|
996
1090
|
}
|
|
997
1091
|
if (existing && Object.hasOwn(definition, "env") && isBuiltInAgentPlugin(existing, "env") && !Object.hasOwn(definition, "literalEnv")) {
|
|
998
1092
|
if (baseEntry === existing) baseEntry = { ...existing };
|
|
@@ -1014,6 +1108,7 @@ function expandImports(config: McpConfig, cwd = process.cwd()): LoadedHostConfig
|
|
|
1014
1108
|
|
|
1015
1109
|
const importedServers: Record<string, ServerEntry> = {};
|
|
1016
1110
|
const serverSources = new Map<string, ImportedServerSource>();
|
|
1111
|
+
const keySources = new Map<string, Map<string, string>>();
|
|
1017
1112
|
for (const importKind of config.imports) {
|
|
1018
1113
|
const imported = loadImportedConfig(importKind, cwd, `Failed to import MCP config from ${importKind}:`);
|
|
1019
1114
|
if (!imported) continue;
|
|
@@ -1023,6 +1118,8 @@ function expandImports(config: McpConfig, cwd = process.cwd()): LoadedHostConfig
|
|
|
1023
1118
|
if (!importedServers[name]) {
|
|
1024
1119
|
importedServers[name] = definition;
|
|
1025
1120
|
serverSources.set(name, imported.serverSources.get(name) ?? imported.source);
|
|
1121
|
+
const keys = imported.keySources?.get(name);
|
|
1122
|
+
if (keys) keySources.set(name, keys);
|
|
1026
1123
|
}
|
|
1027
1124
|
}
|
|
1028
1125
|
}
|
|
@@ -1035,6 +1132,7 @@ function expandImports(config: McpConfig, cwd = process.cwd()): LoadedHostConfig
|
|
|
1035
1132
|
mcpServers: mergeServerMaps(importedServers, config.mcpServers),
|
|
1036
1133
|
},
|
|
1037
1134
|
serverSources,
|
|
1135
|
+
keySources,
|
|
1038
1136
|
};
|
|
1039
1137
|
}
|
|
1040
1138
|
|
package/dist/config.js
CHANGED
|
@@ -68,8 +68,10 @@ export const KNOWN_SERVER_PRESETS = [
|
|
|
68
68
|
{
|
|
69
69
|
id: "github",
|
|
70
70
|
name: "GitHub",
|
|
71
|
-
summary: "Work with GitHub
|
|
72
|
-
|
|
71
|
+
summary: "Work with GitHub using your GitHub CLI sign-in. Needs `gh` installed and `gh auth login` done.",
|
|
72
|
+
// GitHub's OAuth server has no dynamic client registration, so MCP OAuth can't sign in;
|
|
73
|
+
// the GitHub CLI token works and is read at connect time, never written to config.
|
|
74
|
+
entry: { url: "https://api.githubcopilot.com/mcp/", auth: "bearer", bearerToken: "!gh auth token", protocolVersion: "auto" },
|
|
73
75
|
},
|
|
74
76
|
{
|
|
75
77
|
id: "chrome-devtools",
|
|
@@ -162,7 +164,73 @@ function legacyMcpConfigHasContent(filePath) {
|
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
export function getLegacyMcpMigrationNotices(cwd = process.cwd(), overridePath) {
|
|
165
|
-
return [
|
|
167
|
+
return [
|
|
168
|
+
...getPiMcpConfigNotices(cwd, overridePath),
|
|
169
|
+
...getOpenCodeImportNotices(cwd, overridePath),
|
|
170
|
+
...getDroppedCredentialNotices(cwd, overridePath),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
function credentialFiles(servers, fileOf) {
|
|
174
|
+
return new Map(Object.entries(servers).map(([name, definition]) => [
|
|
175
|
+
name,
|
|
176
|
+
new Map(CREDENTIAL_FIELDS.filter((field) => definition[field] !== undefined).map((field) => [field, fileOf(name, field)])),
|
|
177
|
+
]));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Replays the server merges of loadMcpConfigWithSources with the file behind each credential field,
|
|
181
|
+
* and reports the fields mergeServerMaps drops because a later file points the server at another url.
|
|
182
|
+
*/
|
|
183
|
+
function getDroppedCredentialNotices(cwd, overridePath) {
|
|
184
|
+
const notices = [];
|
|
185
|
+
const merge = (base, baseFiles, next, nextFiles, urlFile) => {
|
|
186
|
+
const files = new Map(baseFiles);
|
|
187
|
+
for (const [name, definition] of Object.entries(next)) {
|
|
188
|
+
const existing = base[name];
|
|
189
|
+
const fieldFiles = new Map(files.get(name));
|
|
190
|
+
const byFile = new Map();
|
|
191
|
+
for (const field of existing ? droppedUrlBoundAuth(existing, definition) : []) {
|
|
192
|
+
const from = fieldFiles.get(field);
|
|
193
|
+
if (from !== undefined)
|
|
194
|
+
byFile.set(from, [...byFile.get(from) ?? [], field]);
|
|
195
|
+
fieldFiles.delete(field);
|
|
196
|
+
}
|
|
197
|
+
for (const [from, fields] of byFile) {
|
|
198
|
+
notices.push(`"${name}": ignoring ${fields.join(", ")} from ${from}, because ${urlFile(name)} sets a different url for this server. Use one url, or set ${fields.length === 1 ? "it" : "them"} next to that url.`);
|
|
199
|
+
}
|
|
200
|
+
for (const [field, path] of nextFiles.get(name) ?? [])
|
|
201
|
+
fieldFiles.set(field, path);
|
|
202
|
+
files.set(name, fieldFiles);
|
|
203
|
+
}
|
|
204
|
+
return [mergeServerMaps(base, next), files];
|
|
205
|
+
};
|
|
206
|
+
const host = !isExclusiveConfigMode() && getConfiguredHostConfigDiscovery(overridePath, cwd) === "on"
|
|
207
|
+
? loadDiscoveredHostConfigs(cwd)
|
|
208
|
+
: { config: { mcpServers: {} }, serverSources: new Map() };
|
|
209
|
+
const hostFile = (name) => host.serverSources.get(name)?.path ?? "a host config";
|
|
210
|
+
let servers = host.config.mcpServers;
|
|
211
|
+
let files = credentialFiles(servers, hostFile);
|
|
212
|
+
const sourceSpecs = getConfigSources(overridePath, cwd);
|
|
213
|
+
const piProjectSource = sourceSpecs.find((source) => source.id === "pi-mcp-project");
|
|
214
|
+
const piProjectNames = Object.keys((piProjectSource && readPiMcpConfig(piProjectSource.readPath))?.mcpServers ?? {});
|
|
215
|
+
for (const source of sourceSpecs) {
|
|
216
|
+
const loaded = readSourceConfig(source.id, source.readPath);
|
|
217
|
+
if (!loaded)
|
|
218
|
+
continue;
|
|
219
|
+
if (source.id === "pi-mcp-global") {
|
|
220
|
+
for (const name of piProjectNames)
|
|
221
|
+
delete loaded.mcpServers[name];
|
|
222
|
+
}
|
|
223
|
+
// The same two merges as expandImports and mergeConfigs: the file's own entries over its imports,
|
|
224
|
+
// then the file over the files before it.
|
|
225
|
+
const imported = expandImports({ ...loaded, mcpServers: {} }, cwd);
|
|
226
|
+
// An OpenCode project entry can inherit a credential from the global file, so name the file per key.
|
|
227
|
+
const importFile = (name, field) => imported.keySources?.get(name)?.get(field) ?? imported.serverSources.get(name)?.path ?? source.readPath;
|
|
228
|
+
const ownFile = () => source.readPath;
|
|
229
|
+
const [fileServers, fileFiles] = merge(imported.config.mcpServers, credentialFiles(imported.config.mcpServers, importFile), loaded.mcpServers, credentialFiles(loaded.mcpServers, ownFile), ownFile);
|
|
230
|
+
const urlFile = (name) => typeof loaded.mcpServers[name]?.url === "string" ? source.readPath : importFile(name, "url");
|
|
231
|
+
[servers, files] = merge(servers, files, fileServers, fileFiles, urlFile);
|
|
232
|
+
}
|
|
233
|
+
return notices;
|
|
166
234
|
}
|
|
167
235
|
function getPiMcpConfigNotices(cwd, overridePath) {
|
|
168
236
|
if (piMcpConfigEnabled) {
|
|
@@ -779,6 +847,21 @@ function mergeConfigs(base, next) {
|
|
|
779
847
|
// otherwise the original endpoint's credentials would be shipped to the new
|
|
780
848
|
// url. See the SECURITY note in mergeServerMaps.
|
|
781
849
|
const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv", "bearerTokenStore", "requestHeadersCommand", "caFile"];
|
|
850
|
+
const CREDENTIAL_FIELDS = [...URL_BOUND_AUTH_FIELDS, "auth", "oauth"];
|
|
851
|
+
/** The credential fields of `existing` that mergeServerMaps drops for `definition`'s different url. */
|
|
852
|
+
function droppedUrlBoundAuth(existing, definition) {
|
|
853
|
+
if (typeof definition.url !== "string" || definition.url === existing.url)
|
|
854
|
+
return [];
|
|
855
|
+
return CREDENTIAL_FIELDS.filter((field) => {
|
|
856
|
+
if (Object.hasOwn(definition, field))
|
|
857
|
+
return false;
|
|
858
|
+
if (field === "auth")
|
|
859
|
+
return typeof existing.auth === "object";
|
|
860
|
+
if (field === "oauth")
|
|
861
|
+
return existing.oauth !== undefined && existing.oauth !== false;
|
|
862
|
+
return existing[field] !== undefined;
|
|
863
|
+
});
|
|
864
|
+
}
|
|
782
865
|
function mergeServerMaps(base, next) {
|
|
783
866
|
const merged = { ...base };
|
|
784
867
|
for (const [name, definition] of Object.entries(next)) {
|
|
@@ -824,15 +907,11 @@ function mergeServerMaps(base, next) {
|
|
|
824
907
|
if (existing && typeof definition.url === "string" && definition.url !== existing.url) {
|
|
825
908
|
if (baseEntry === existing)
|
|
826
909
|
baseEntry = { ...existing };
|
|
827
|
-
|
|
910
|
+
// Provider tokens (object `auth`) and OAuth config are bound to the url too; the startup
|
|
911
|
+
// notice reports exactly these fields, so both read the one rule.
|
|
912
|
+
for (const field of droppedUrlBoundAuth(existing, definition)) {
|
|
828
913
|
delete baseEntry[field];
|
|
829
914
|
}
|
|
830
|
-
// A provider token is bound to the url that asked for it, like the fields above.
|
|
831
|
-
if (typeof baseEntry.auth === "object")
|
|
832
|
-
delete baseEntry.auth;
|
|
833
|
-
if (baseEntry.oauth !== false) {
|
|
834
|
-
delete baseEntry.oauth;
|
|
835
|
-
}
|
|
836
915
|
}
|
|
837
916
|
if (existing && Object.hasOwn(definition, "env") && isBuiltInAgentPlugin(existing, "env") && !Object.hasOwn(definition, "literalEnv")) {
|
|
838
917
|
if (baseEntry === existing)
|
|
@@ -854,6 +933,7 @@ function expandImports(config, cwd = process.cwd()) {
|
|
|
854
933
|
return { config, serverSources: new Map() };
|
|
855
934
|
const importedServers = {};
|
|
856
935
|
const serverSources = new Map();
|
|
936
|
+
const keySources = new Map();
|
|
857
937
|
for (const importKind of config.imports) {
|
|
858
938
|
const imported = loadImportedConfig(importKind, cwd, `Failed to import MCP config from ${importKind}:`);
|
|
859
939
|
if (!imported)
|
|
@@ -863,6 +943,9 @@ function expandImports(config, cwd = process.cwd()) {
|
|
|
863
943
|
if (!importedServers[name]) {
|
|
864
944
|
importedServers[name] = definition;
|
|
865
945
|
serverSources.set(name, imported.serverSources.get(name) ?? imported.source);
|
|
946
|
+
const keys = imported.keySources?.get(name);
|
|
947
|
+
if (keys)
|
|
948
|
+
keySources.set(name, keys);
|
|
866
949
|
}
|
|
867
950
|
}
|
|
868
951
|
}
|
|
@@ -874,6 +957,7 @@ function expandImports(config, cwd = process.cwd()) {
|
|
|
874
957
|
mcpServers: mergeServerMaps(importedServers, config.mcpServers),
|
|
875
958
|
},
|
|
876
959
|
serverSources,
|
|
960
|
+
keySources,
|
|
877
961
|
};
|
|
878
962
|
}
|
|
879
963
|
function resolveImportCandidates(importKind, cwd) {
|