pi-mcp-client 0.4.0 → 0.5.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/README.md +32 -713
- package/dist/index.js +113 -61
- package/docs/authentication.md +175 -0
- package/docs/behavior.md +114 -0
- package/docs/commands.md +171 -0
- package/docs/configuration.md +150 -0
- package/docs/tool-reference.md +147 -0
- package/docs/troubleshooting.md +67 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1879,7 +1879,7 @@ function diagnostic(code, context) {
|
|
|
1879
1879
|
const target = server ?? "<server>";
|
|
1880
1880
|
const hints = {
|
|
1881
1881
|
configuration_invalid: "Check mcpServers in mcp.json / .mcp.json, including inline server options and environment variables. Move options from any top-level pi section into mcpServers.<server>. Reload Pi after editing.",
|
|
1882
|
-
authentication_required: context.oauth ? `Run /mcp login ${target}.` :
|
|
1882
|
+
authentication_required: context.oauth ? `Run /mcp login ${target}.` : `For OAuth, run /mcp login ${target}. If you use an Authorization header, check its credentials instead.`,
|
|
1883
1883
|
permission_denied: "Check the account's permissions, OAuth scopes, and service access policy.",
|
|
1884
1884
|
credential_store_unavailable: "Unlock or enable the OS keyring. Linux requires a Secret Service session; there is no plaintext fallback.",
|
|
1885
1885
|
secret_lookup_failed: "Check the secret helper's availability, login, exit status, and nonempty stdout. The limit is 64 KiB and at most 10 seconds.",
|
|
@@ -2121,7 +2121,6 @@ function object(value) {
|
|
|
2121
2121
|
}
|
|
2122
2122
|
var OPTION_FIELDS = [
|
|
2123
2123
|
"description",
|
|
2124
|
-
"oauth",
|
|
2125
2124
|
"oauthClientId",
|
|
2126
2125
|
"oauthScopes",
|
|
2127
2126
|
"oauthCallbackPort",
|
|
@@ -2138,15 +2137,13 @@ function validateOptions(entry, fail) {
|
|
|
2138
2137
|
if (entry[key] !== void 0 && (!Array.isArray(entry[key]) || !entry[key].every((x) => typeof x === "string")))
|
|
2139
2138
|
fail(key);
|
|
2140
2139
|
}
|
|
2141
|
-
if (entry.oauthClientId !== void 0 && (
|
|
2142
|
-
fail("oauthClientId (requires
|
|
2143
|
-
if (entry.oauthScopes !== void 0 && (
|
|
2144
|
-
fail("oauthScopes (requires
|
|
2145
|
-
if (entry.oauthCallbackPort !== void 0 && (
|
|
2146
|
-
fail("oauthCallbackPort (requires
|
|
2147
|
-
|
|
2148
|
-
if (entry[key] !== void 0 && typeof entry[key] !== "boolean") fail(key);
|
|
2149
|
-
}
|
|
2140
|
+
if (entry.oauthClientId !== void 0 && (typeof entry.oauthClientId !== "string" || !entry.oauthClientId.trim() || entry.oauthClientId.length > 4096 || /[\u0000-\u001f\u007f]/u.test(entry.oauthClientId)))
|
|
2141
|
+
fail("oauthClientId (requires OAuth and a nonempty client ID)");
|
|
2142
|
+
if (entry.oauthScopes !== void 0 && (!Array.isArray(entry.oauthScopes) || entry.oauthScopes.length === 0 || entry.oauthScopes.length > 100 || !entry.oauthScopes.every((scope) => typeof scope === "string" && scope.length <= 256 && /^[\x21\x23-\x5b\x5d-\x7e]+$/u.test(scope)) || new Set(entry.oauthScopes).size !== entry.oauthScopes.length))
|
|
2143
|
+
fail("oauthScopes (requires OAuth and 1\u2013100 unique OAuth scope tokens)");
|
|
2144
|
+
if (entry.oauthCallbackPort !== void 0 && (!Number.isInteger(entry.oauthCallbackPort) || Number(entry.oauthCallbackPort) < 1 || Number(entry.oauthCallbackPort) > 65535))
|
|
2145
|
+
fail("oauthCallbackPort (requires OAuth and a port from 1\u201365535)");
|
|
2146
|
+
if (entry.disabled !== void 0 && typeof entry.disabled !== "boolean") fail("disabled");
|
|
2150
2147
|
if (entry.timeoutMs !== void 0 && (!Number.isInteger(entry.timeoutMs) || Number(entry.timeoutMs) < 100 || Number(entry.timeoutMs) > 6e5))
|
|
2151
2148
|
fail("timeoutMs (100\u2013600000)");
|
|
2152
2149
|
if (entry.protocol !== void 0 && entry.protocol !== "legacy" && entry.protocol !== "auto")
|
|
@@ -2176,6 +2173,8 @@ function parseConnections(value, source) {
|
|
|
2176
2173
|
const fail = (field) => {
|
|
2177
2174
|
throw new Error(`${source}: invalid ${field} for server ${name}.`);
|
|
2178
2175
|
};
|
|
2176
|
+
if (Object.hasOwn(entry, "oauth"))
|
|
2177
|
+
throw new Error(`${source}: remove oauth from server ${name}; HTTP authentication is automatic.`);
|
|
2179
2178
|
for (const key of Object.keys(entry)) if (!fields.has(key)) fail(key);
|
|
2180
2179
|
validateOptions(entry, fail);
|
|
2181
2180
|
if (entry.type !== void 0 && entry.type !== "stdio" && entry.type !== "http")
|
|
@@ -2196,7 +2195,7 @@ function parseConnections(value, source) {
|
|
|
2196
2195
|
fail("transport (provide exactly one of command or url)");
|
|
2197
2196
|
if (entry.type === "stdio" && !entry.command || entry.type === "http" && !entry.url)
|
|
2198
2197
|
fail("type (must match command or url)");
|
|
2199
|
-
if (entry.command && (entry.
|
|
2198
|
+
if (entry.command && (entry.headers || entry.oauthClientId !== void 0 || entry.oauthScopes !== void 0 || entry.oauthCallbackPort !== void 0))
|
|
2200
2199
|
fail("HTTP options on stdio transport");
|
|
2201
2200
|
if (entry.url && (entry.args || entry.cwd || entry.env))
|
|
2202
2201
|
fail("stdio options on HTTP transport");
|
|
@@ -2245,11 +2244,12 @@ async function configTarget(path) {
|
|
|
2245
2244
|
}
|
|
2246
2245
|
}
|
|
2247
2246
|
async function updateServerConfig(agentDir, cwd, trusted, mutation, validate) {
|
|
2248
|
-
|
|
2247
|
+
const scoped = mutation.action === "add" || mutation.action === "remove";
|
|
2248
|
+
if (scoped && mutation.scope === "project" && !trusted)
|
|
2249
2249
|
throw new ConfigMutationError("Project configuration requires a trusted project. Use global scope or trust the project first.");
|
|
2250
2250
|
const paths = [join(agentDir, "mcp.json"), ...trusted ? [join(cwd, ".mcp.json")] : []];
|
|
2251
2251
|
const targets = await Promise.all(paths.map(configTarget));
|
|
2252
|
-
if (
|
|
2252
|
+
if (scoped && targets.length === 2 && targets[0] === targets[1])
|
|
2253
2253
|
throw new ConfigMutationError("Global and project configuration share a file; separate them before scoped edits.");
|
|
2254
2254
|
const locks = [...new Set(targets)].sort();
|
|
2255
2255
|
const locked = async (index) => {
|
|
@@ -2258,8 +2258,8 @@ async function updateServerConfig(agentDir, cwd, trusted, mutation, validate) {
|
|
|
2258
2258
|
const documents = await Promise.all(targets.map(readJson));
|
|
2259
2259
|
const parsed = documents.map((document2, index2) => document2 === void 0 ? /* @__PURE__ */ Object.create(null) : parseConfig(document2, paths[index2]));
|
|
2260
2260
|
const { server } = mutation;
|
|
2261
|
-
let source =
|
|
2262
|
-
if (
|
|
2261
|
+
let source = scoped ? mutation.scope === "global" ? 0 : 1 : -1;
|
|
2262
|
+
if (!scoped) {
|
|
2263
2263
|
for (const [index2, config2] of parsed.entries()) if (Object.hasOwn(config2, server)) source = index2;
|
|
2264
2264
|
if (source < 0) throw new ConfigMutationError("Server is no longer configured. Run /mcp reload.");
|
|
2265
2265
|
}
|
|
@@ -2339,12 +2339,15 @@ function resolveServer(config, cwd) {
|
|
|
2339
2339
|
throw new Error("MCP URLs must use HTTP(S), without credentials or fragments.");
|
|
2340
2340
|
result.url = url.href;
|
|
2341
2341
|
}
|
|
2342
|
-
if (result.
|
|
2342
|
+
if ((result.oauthClientId !== void 0 || result.oauthScopes !== void 0 || result.oauthCallbackPort !== void 0) && Object.keys(result.headers ?? {}).some(
|
|
2343
2343
|
(key) => key.toLowerCase() === "authorization"
|
|
2344
2344
|
))
|
|
2345
2345
|
throw new Error("Use OAuth or an Authorization header, not both.");
|
|
2346
2346
|
return result;
|
|
2347
2347
|
}
|
|
2348
|
+
function usesOAuth(config) {
|
|
2349
|
+
return Boolean(config?.url) && !Object.keys(config?.headers ?? {}).some((name) => name.toLowerCase() === "authorization");
|
|
2350
|
+
}
|
|
2348
2351
|
function fingerprint(value) {
|
|
2349
2352
|
const canonical = (item) => Array.isArray(item) ? item.map(canonical) : object(item) ? Object.fromEntries(
|
|
2350
2353
|
Object.keys(item).sort().map((key) => [key, canonical(item[key])])
|
|
@@ -2566,7 +2569,7 @@ function searchCapabilities(tools, resources, query, server, limit = DEFAULT_SEA
|
|
|
2566
2569
|
}
|
|
2567
2570
|
|
|
2568
2571
|
// src/config-commands.ts
|
|
2569
|
-
var ADD_USAGE = "Usage: /mcp add --scope global|project [--replace] [--transport http|stdio] [--header 'Name: value'] [--env KEY=value] [--oauth
|
|
2572
|
+
var ADD_USAGE = "Usage: /mcp add --scope global|project [--replace] [--transport http|stdio] [--header 'Name: value'] [--env KEY=value] [--oauth-client-id ID] [--oauth-scope SCOPE] [--oauth-callback-port PORT] <server> <url> OR <server> -- <command> [args...]. Put options before the server name.";
|
|
2570
2573
|
var REMOVE_USAGE = "Usage: /mcp remove --scope global|project <server>. Credentials are retained; log out first if you want to remove them.";
|
|
2571
2574
|
function commandWords(input) {
|
|
2572
2575
|
const words = [];
|
|
@@ -2627,10 +2630,8 @@ function parseConfigCommand(input) {
|
|
|
2627
2630
|
replace = true;
|
|
2628
2631
|
continue;
|
|
2629
2632
|
}
|
|
2630
|
-
if (flag === "--oauth"
|
|
2631
|
-
|
|
2632
|
-
continue;
|
|
2633
|
-
}
|
|
2633
|
+
if (flag === "--oauth")
|
|
2634
|
+
throw new ConfigMutationError("Remove --oauth; HTTP authentication is automatic.");
|
|
2634
2635
|
const value = words[index++];
|
|
2635
2636
|
if (value === void 0 || value.startsWith("--")) fail();
|
|
2636
2637
|
if (flag === "--scope") {
|
|
@@ -2667,7 +2668,7 @@ function parseConfigCommand(input) {
|
|
|
2667
2668
|
if (transport === "http") fail();
|
|
2668
2669
|
definition.command = words[++index];
|
|
2669
2670
|
definition.args = words.slice(index + 1);
|
|
2670
|
-
if (!definition.command || definition.
|
|
2671
|
+
if (!definition.command || definition.oauthClientId || definition.oauthScopes || definition.oauthCallbackPort || definition.headers) fail();
|
|
2671
2672
|
} else {
|
|
2672
2673
|
if (transport === "stdio" || index + 1 !== words.length || definition.env) fail();
|
|
2673
2674
|
definition.url = words[index];
|
|
@@ -2689,7 +2690,7 @@ function configCommandCompletions(input, servers) {
|
|
|
2689
2690
|
const args = /^--scope\s+(global|project)\s+([^\s]*)$/u.exec(rest);
|
|
2690
2691
|
if (!args) return [];
|
|
2691
2692
|
const [, scope, partial] = args;
|
|
2692
|
-
const values = action === "remove" ? servers.sort() : ["--replace", "--transport", "--header", "--env", "--oauth
|
|
2693
|
+
const values = action === "remove" ? servers.sort() : ["--replace", "--transport", "--header", "--env", "--oauth-client-id", "--oauth-scope", "--oauth-callback-port"];
|
|
2693
2694
|
return values.filter((value) => value.startsWith(partial)).map((value) => ({
|
|
2694
2695
|
value: `${action} --scope ${scope} ${value}`,
|
|
2695
2696
|
label: value
|
|
@@ -2777,7 +2778,6 @@ function oauthCallbackHtml(status) {
|
|
|
2777
2778
|
}
|
|
2778
2779
|
|
|
2779
2780
|
// src/auth.ts
|
|
2780
|
-
var REDIRECT = "http://127.0.0.1:19847/callback";
|
|
2781
2781
|
function callbackUrl(options = {}) {
|
|
2782
2782
|
return `http://127.0.0.1:${options.oauthCallbackPort ?? 19847}/callback`;
|
|
2783
2783
|
}
|
|
@@ -2796,7 +2796,7 @@ function parseCallback(value, redirect, state) {
|
|
|
2796
2796
|
return params;
|
|
2797
2797
|
}
|
|
2798
2798
|
function credentialKey(url, clientId) {
|
|
2799
|
-
return fingerprint({ url,
|
|
2799
|
+
return fingerprint({ url, clientId });
|
|
2800
2800
|
}
|
|
2801
2801
|
var credentialEpochs = /* @__PURE__ */ new Map();
|
|
2802
2802
|
async function credentialStore(url, clientId) {
|
|
@@ -2821,6 +2821,17 @@ async function credentialStore(url, clientId) {
|
|
|
2821
2821
|
throw failure("credential_store_unavailable", { operation: "auth" });
|
|
2822
2822
|
}
|
|
2823
2823
|
}
|
|
2824
|
+
function clientMetadata(options) {
|
|
2825
|
+
return {
|
|
2826
|
+
client_name: "Pi MCP Client",
|
|
2827
|
+
redirect_uris: [callbackUrl(options)],
|
|
2828
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
2829
|
+
response_types: ["code"],
|
|
2830
|
+
token_endpoint_auth_method: "none",
|
|
2831
|
+
application_type: "native",
|
|
2832
|
+
...options.oauthScopes ? { scope: options.oauthScopes.join(" ") } : {}
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2824
2835
|
var OAuthProvider = class {
|
|
2825
2836
|
constructor(url, store, redirect, clientId, options = {}) {
|
|
2826
2837
|
this.url = url;
|
|
@@ -2828,15 +2839,7 @@ var OAuthProvider = class {
|
|
|
2828
2839
|
this.redirect = redirect;
|
|
2829
2840
|
this.clientId = clientId;
|
|
2830
2841
|
this.redirectUrl = callbackUrl(options);
|
|
2831
|
-
this.clientMetadata =
|
|
2832
|
-
client_name: "Pi MCP Client",
|
|
2833
|
-
redirect_uris: [this.redirectUrl],
|
|
2834
|
-
grant_types: ["authorization_code", "refresh_token"],
|
|
2835
|
-
response_types: ["code"],
|
|
2836
|
-
token_endpoint_auth_method: "none",
|
|
2837
|
-
application_type: "native",
|
|
2838
|
-
...options.oauthScopes ? { scope: options.oauthScopes.join(" ") } : {}
|
|
2839
|
-
};
|
|
2842
|
+
this.clientMetadata = clientMetadata(options);
|
|
2840
2843
|
this.key = credentialKey(url, clientId);
|
|
2841
2844
|
this.epoch = credentialEpochs.get(this.key) ?? 0;
|
|
2842
2845
|
const raw = store.read();
|
|
@@ -2876,7 +2879,7 @@ var OAuthProvider = class {
|
|
|
2876
2879
|
const stored = Object.hasOwn(this.data.clients, ctx.issuer) ? this.data.clients[ctx.issuer] : void 0;
|
|
2877
2880
|
if (!this.clientId) {
|
|
2878
2881
|
const registration = this.data.registrations?.[ctx.issuer];
|
|
2879
|
-
if (this.redirect && stored && (
|
|
2882
|
+
if (this.redirect && stored && (registration?.redirect !== this.redirectUrl || registration?.scope !== this.clientMetadata.scope))
|
|
2880
2883
|
return void 0;
|
|
2881
2884
|
return stored;
|
|
2882
2885
|
}
|
|
@@ -2943,6 +2946,49 @@ var OAuthProvider = class {
|
|
|
2943
2946
|
this.save();
|
|
2944
2947
|
}
|
|
2945
2948
|
};
|
|
2949
|
+
async function connectionAuthProvider(config, storeFactory = credentialStore) {
|
|
2950
|
+
if (!usesOAuth(config)) return void 0;
|
|
2951
|
+
let initialized = false;
|
|
2952
|
+
const create = async () => {
|
|
2953
|
+
const provider = new OAuthProvider(
|
|
2954
|
+
config.url,
|
|
2955
|
+
await storeFactory(config.url, config.oauthClientId),
|
|
2956
|
+
void 0,
|
|
2957
|
+
config.oauthClientId,
|
|
2958
|
+
config
|
|
2959
|
+
);
|
|
2960
|
+
initialized = true;
|
|
2961
|
+
return provider;
|
|
2962
|
+
};
|
|
2963
|
+
let pending;
|
|
2964
|
+
const get = () => pending ??= create();
|
|
2965
|
+
return {
|
|
2966
|
+
redirectUrl: callbackUrl(config),
|
|
2967
|
+
clientMetadata: clientMetadata(config),
|
|
2968
|
+
state: async () => (await get()).state(),
|
|
2969
|
+
clientInformation: async (ctx) => {
|
|
2970
|
+
const info = (await get()).clientInformation(ctx);
|
|
2971
|
+
if (!info) throw failure("authentication_required", { operation: "connect", oauth: true });
|
|
2972
|
+
return info;
|
|
2973
|
+
},
|
|
2974
|
+
saveClientInformation: async (info, ctx) => (await get()).saveClientInformation(info, ctx),
|
|
2975
|
+
tokens: async (ctx) => {
|
|
2976
|
+
try {
|
|
2977
|
+
return (await get()).tokens(ctx);
|
|
2978
|
+
} catch (error) {
|
|
2979
|
+
if (!initialized && !ctx && error instanceof DiagnosticError && error.diagnostic.code === "credential_store_unavailable") return void 0;
|
|
2980
|
+
throw error;
|
|
2981
|
+
}
|
|
2982
|
+
},
|
|
2983
|
+
saveTokens: async (tokens2) => (await get()).saveTokens(tokens2),
|
|
2984
|
+
saveCodeVerifier: async (value) => (await get()).saveCodeVerifier(value),
|
|
2985
|
+
codeVerifier: async () => (await get()).codeVerifier(),
|
|
2986
|
+
redirectToAuthorization: async (url) => (await get()).redirectToAuthorization(url),
|
|
2987
|
+
saveDiscoveryState: async (value) => (await get()).saveDiscoveryState(value),
|
|
2988
|
+
discoveryState: async () => pending ? (await pending).discoveryState() : void 0,
|
|
2989
|
+
invalidateCredentials: async (scope) => (await get()).invalidateCredentials(scope)
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2946
2992
|
async function logout(url, store, signal, clientId) {
|
|
2947
2993
|
let tokens2;
|
|
2948
2994
|
let client;
|
|
@@ -3175,11 +3221,11 @@ ${parameter.description}` : ""}`
|
|
|
3175
3221
|
].join("\n\n");
|
|
3176
3222
|
}
|
|
3177
3223
|
function oauthSettings(config, cwd) {
|
|
3178
|
-
const resolved = resolveServer({ url: config.url,
|
|
3224
|
+
const resolved = resolveServer({ url: config.url, oauthClientId: config.oauthClientId }, cwd);
|
|
3179
3225
|
return { url: resolved.url, clientId: resolved.oauthClientId };
|
|
3180
3226
|
}
|
|
3181
3227
|
async function authenticationSummary(config, cwd, storeFactory = credentialStore) {
|
|
3182
|
-
if (!config
|
|
3228
|
+
if (!usesOAuth(config)) return config.command ? "Server-managed (stdio)" : "Headers (externally managed)";
|
|
3183
3229
|
const method = config.oauthClientId === void 0 ? "OAuth" : "OAuth (pre-registered public client)";
|
|
3184
3230
|
try {
|
|
3185
3231
|
const { url, clientId } = oauthSettings(config, cwd);
|
|
@@ -3195,8 +3241,8 @@ function inspectServer(name, config, status, authentication) {
|
|
|
3195
3241
|
`Server: ${name}`,
|
|
3196
3242
|
`Transport: ${config.command ? "stdio" : "HTTP"}`,
|
|
3197
3243
|
`Protocol: ${config.protocol ?? "auto"}`,
|
|
3198
|
-
`OAuth: ${config
|
|
3199
|
-
...config
|
|
3244
|
+
`OAuth: ${usesOAuth(config) ? "automatic" : "not used"}`,
|
|
3245
|
+
...usesOAuth(config) ? [
|
|
3200
3246
|
`Requested scopes: ${config.oauthScopes?.map(line).join(", ") ?? "SDK/server defaults"}`,
|
|
3201
3247
|
`OAuth callback: http://127.0.0.1:${config.oauthCallbackPort ?? 19847}/callback`
|
|
3202
3248
|
] : [],
|
|
@@ -3341,7 +3387,7 @@ function waitFor2(promise, signal) {
|
|
|
3341
3387
|
if (signal.aborted) abort();
|
|
3342
3388
|
});
|
|
3343
3389
|
}
|
|
3344
|
-
var
|
|
3390
|
+
var createSdkConnector = (storeFactory = credentialStore) => async (_name, config, signal, onToolsChanged, onResourcesChanged) => {
|
|
3345
3391
|
const timeout = config.timeoutMs ?? 15e3;
|
|
3346
3392
|
const client = new Client(
|
|
3347
3393
|
{ name: "pi-mcp-client", version: "0.1.0" },
|
|
@@ -3372,7 +3418,7 @@ var connectSdk = async (_name, config, signal, onToolsChanged, onResourcesChange
|
|
|
3372
3418
|
stderr: "pipe"
|
|
3373
3419
|
}) : new StreamableHTTPClientTransport(new URL(config.url), {
|
|
3374
3420
|
requestInit: { headers: config.headers },
|
|
3375
|
-
authProvider:
|
|
3421
|
+
authProvider: await connectionAuthProvider(config, storeFactory),
|
|
3376
3422
|
// Bound HTTP responses (including OAuth), but not established SSE streams.
|
|
3377
3423
|
// The SDK bounds ordinary MCP requests with their request timeout.
|
|
3378
3424
|
fetch: async (input, init) => {
|
|
@@ -3418,6 +3464,7 @@ var connectSdk = async (_name, config, signal, onToolsChanged, onResourcesChange
|
|
|
3418
3464
|
throw error;
|
|
3419
3465
|
}
|
|
3420
3466
|
};
|
|
3467
|
+
var connectSdk = createSdkConnector();
|
|
3421
3468
|
var McpRuntime = class {
|
|
3422
3469
|
constructor(config, cwd, cacheDir, connect = connectSdk) {
|
|
3423
3470
|
this.config = config;
|
|
@@ -3543,7 +3590,7 @@ var McpRuntime = class {
|
|
|
3543
3590
|
diagnose(error, {
|
|
3544
3591
|
server: name,
|
|
3545
3592
|
operation: "connect",
|
|
3546
|
-
oauth: config
|
|
3593
|
+
oauth: usesOAuth(config),
|
|
3547
3594
|
signal: this.lifetime.signal
|
|
3548
3595
|
})
|
|
3549
3596
|
);
|
|
@@ -3776,7 +3823,7 @@ var McpRuntime = class {
|
|
|
3776
3823
|
this.state(name).error = void 0;
|
|
3777
3824
|
return result;
|
|
3778
3825
|
} catch (error) {
|
|
3779
|
-
const value = diagnose(error, { server: name, operation: "read", oauth: config
|
|
3826
|
+
const value = diagnose(error, { server: name, operation: "read", oauth: usesOAuth(config), signal: combined });
|
|
3780
3827
|
this.state(name).error = value;
|
|
3781
3828
|
throw new DiagnosticError(value);
|
|
3782
3829
|
}
|
|
@@ -3876,7 +3923,7 @@ var McpRuntime = class {
|
|
|
3876
3923
|
const value = diagnose(error, {
|
|
3877
3924
|
server: tool.server,
|
|
3878
3925
|
operation: "call",
|
|
3879
|
-
oauth: config
|
|
3926
|
+
oauth: usesOAuth(config),
|
|
3880
3927
|
signal
|
|
3881
3928
|
});
|
|
3882
3929
|
this.state(tool.server).error = value;
|
|
@@ -3974,7 +4021,7 @@ var McpRuntime = class {
|
|
|
3974
4021
|
return error instanceof ToolContractError ? diagnostic("tool_changed", { server: name, operation: "search" }) : diagnose(error, {
|
|
3975
4022
|
server: name,
|
|
3976
4023
|
operation: "search",
|
|
3977
|
-
oauth: this.config[name]
|
|
4024
|
+
oauth: this.config[name] && usesOAuth(this.config[name]),
|
|
3978
4025
|
signal: this.lifetime.signal
|
|
3979
4026
|
});
|
|
3980
4027
|
}
|
|
@@ -4253,7 +4300,7 @@ function formatBlock(text, block, theme) {
|
|
|
4253
4300
|
).join("");
|
|
4254
4301
|
}
|
|
4255
4302
|
function formatOutput(text, blocks, theme) {
|
|
4256
|
-
if (!blocks) return
|
|
4303
|
+
if (!blocks) return plain(text);
|
|
4257
4304
|
let cursor = 0;
|
|
4258
4305
|
let extraBytes = 0;
|
|
4259
4306
|
let extraLines = 0;
|
|
@@ -4449,7 +4496,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
4449
4496
|
return errorResult(error, {
|
|
4450
4497
|
server: tool.server,
|
|
4451
4498
|
operation: "call",
|
|
4452
|
-
oauth: config[tool.server]
|
|
4499
|
+
oauth: usesOAuth(config[tool.server]),
|
|
4453
4500
|
signal: ctx.signal?.aborted ? ctx.signal : signal
|
|
4454
4501
|
});
|
|
4455
4502
|
}
|
|
@@ -4501,7 +4548,8 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
4501
4548
|
const next = new McpRuntime(
|
|
4502
4549
|
nextConfig,
|
|
4503
4550
|
ctx.cwd,
|
|
4504
|
-
join4(agentDir, "cache", "pi-mcp-client")
|
|
4551
|
+
join4(agentDir, "cache", "pi-mcp-client"),
|
|
4552
|
+
createSdkConnector(storeFactory)
|
|
4505
4553
|
);
|
|
4506
4554
|
const active = new Set(pi.getActiveTools());
|
|
4507
4555
|
const retained = [...exposure.definitions.values()].filter((tool) => {
|
|
@@ -4528,7 +4576,7 @@ Text output is limited to 2000 lines or 50 KiB; larger results are saved to a pr
|
|
|
4528
4576
|
configError = void 0;
|
|
4529
4577
|
try {
|
|
4530
4578
|
config = await loadConfig(agentDir, ctx.cwd, ctx.isProjectTrusted());
|
|
4531
|
-
runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"));
|
|
4579
|
+
runtime = new McpRuntime(config, ctx.cwd, join4(agentDir, "cache", "pi-mcp-client"), createSdkConnector(storeFactory));
|
|
4532
4580
|
resourceNotifications(runtime, ctx);
|
|
4533
4581
|
} catch (error) {
|
|
4534
4582
|
configError = new DiagnosticError(diagnose(error, { operation: "configuration" }));
|
|
@@ -4754,7 +4802,7 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
|
|
|
4754
4802
|
const result = errorResult(error, {
|
|
4755
4803
|
server: args.complete?.server ?? args.read?.server ?? args.server,
|
|
4756
4804
|
operation: hasComplete ? "complete" : hasRead ? "read" : "search",
|
|
4757
|
-
oauth: config[args.complete?.server ?? args.read?.server ?? args.server ?? ""]
|
|
4805
|
+
oauth: usesOAuth(config[args.complete?.server ?? args.read?.server ?? args.server ?? ""]),
|
|
4758
4806
|
signal: ctx.signal?.aborted ? ctx.signal : signal
|
|
4759
4807
|
});
|
|
4760
4808
|
if (args.read) {
|
|
@@ -4854,7 +4902,7 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
|
|
|
4854
4902
|
return;
|
|
4855
4903
|
}
|
|
4856
4904
|
if (action === "logout" && server && !extra.length && Object.hasOwn(config, server)) {
|
|
4857
|
-
if (!config[server]
|
|
4905
|
+
if (!usesOAuth(config[server])) {
|
|
4858
4906
|
if (ctx.hasUI) ctx.ui.notify(
|
|
4859
4907
|
`${server}: no managed OAuth credentials. Header and server credentials are externally managed; configuration was not changed.`,
|
|
4860
4908
|
"info"
|
|
@@ -4866,7 +4914,7 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
|
|
|
4866
4914
|
if (generation !== sessionGeneration)
|
|
4867
4915
|
throw new CommandUsageError("The Pi session changed during logout.");
|
|
4868
4916
|
const related = Object.keys(config).filter((name) => {
|
|
4869
|
-
if (!config[name]
|
|
4917
|
+
if (!usesOAuth(config[name])) return false;
|
|
4870
4918
|
try {
|
|
4871
4919
|
const other = oauthSettings(config[name], ctx.cwd);
|
|
4872
4920
|
return other.url === url && other.clientId === clientId;
|
|
@@ -4938,11 +4986,13 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
|
|
|
4938
4986
|
throw new CommandUsageError(
|
|
4939
4987
|
"OAuth requires an interactive session. Use an Authorization header for headless access."
|
|
4940
4988
|
);
|
|
4941
|
-
if (!config[server].
|
|
4942
|
-
throw new CommandUsageError(
|
|
4943
|
-
|
|
4944
|
-
);
|
|
4989
|
+
if (!config[server].url)
|
|
4990
|
+
throw new CommandUsageError("OAuth login requires an HTTP server; stdio authentication is managed by the server.");
|
|
4991
|
+
if (Object.keys(config[server].headers ?? {}).some((name) => name.toLowerCase() === "authorization"))
|
|
4992
|
+
throw new CommandUsageError("This server uses an Authorization header. Remove it from the server definition before using /mcp login, or keep using header authentication.");
|
|
4945
4993
|
const { url, clientId } = oauthSettings(config[server], ctx.cwd);
|
|
4994
|
+
const options2 = config[server];
|
|
4995
|
+
const loginRuntime = current();
|
|
4946
4996
|
const open = async (target) => {
|
|
4947
4997
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
4948
4998
|
const result = await pi.exec(command, [target], { timeout: 5e3 }).catch(() => void 0);
|
|
@@ -4954,8 +5004,10 @@ Variables: ${(candidate.variables ?? []).join(", ") || "none"}. Supply known val
|
|
|
4954
5004
|
loginController = controller;
|
|
4955
5005
|
const signal = AbortSignal.any([controller.signal, ...ctx.signal ? [ctx.signal] : []]);
|
|
4956
5006
|
try {
|
|
5007
|
+
signal.throwIfAborted();
|
|
4957
5008
|
const store = await storeFactory(url, clientId);
|
|
4958
|
-
|
|
5009
|
+
if (generation !== sessionGeneration || current() !== loginRuntime)
|
|
5010
|
+
throw failure("cancelled", { server, operation: "auth" });
|
|
4959
5011
|
const summary = `Requested scopes: ${options2.oauthScopes?.join(", ") ?? "SDK/server defaults"}
|
|
4960
5012
|
Callback: http://127.0.0.1:${options2.oauthCallbackPort ?? 19847}/callback`;
|
|
4961
5013
|
if (extra[0] === "--no-browser") {
|
|
@@ -4999,8 +5051,8 @@ Opening browser. Esc to cancel.`
|
|
|
4999
5051
|
if (!ok)
|
|
5000
5052
|
throw authError ?? failure("cancelled", { server, operation: "auth" });
|
|
5001
5053
|
} else await authenticate(url, open, signal, store, clientId, options2);
|
|
5002
|
-
if (generation !== sessionGeneration) throw failure("cancelled", { server, operation: "auth" });
|
|
5003
|
-
await
|
|
5054
|
+
if (generation !== sessionGeneration || current() !== loginRuntime) throw failure("cancelled", { server, operation: "auth" });
|
|
5055
|
+
await loginRuntime.reconnect(server);
|
|
5004
5056
|
} finally {
|
|
5005
5057
|
if (loginController === controller) loginController = void 0;
|
|
5006
5058
|
}
|
|
@@ -5023,7 +5075,7 @@ Opening browser. Esc to cancel.`
|
|
|
5023
5075
|
diagnose(error, {
|
|
5024
5076
|
server,
|
|
5025
5077
|
operation: ["reload", "get", "enable", "disable", "add", "remove"].includes(action) ? "configuration" : action === "tools" ? "search" : ["login", "logout"].includes(action) ? "auth" : ["subscribe", "unsubscribe"].includes(action) ? "subscribe" : action === "refresh" ? "refresh" : "reconnect",
|
|
5026
|
-
oauth: config[server]
|
|
5078
|
+
oauth: usesOAuth(config[server]),
|
|
5027
5079
|
signal: ctx.signal
|
|
5028
5080
|
})
|
|
5029
5081
|
);
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
[Back to the README](../README.md)
|
|
4
|
+
|
|
5
|
+
You manage authentication through `/mcp login` and `/mcp logout`. The assistant
|
|
6
|
+
can't initiate an OAuth login: only your explicit login command opens the browser.
|
|
7
|
+
For externally managed bearer tokens, use
|
|
8
|
+
[headers and secret commands](configuration.md#secret-commands) instead.
|
|
9
|
+
|
|
10
|
+
## Sign in with OAuth
|
|
11
|
+
|
|
12
|
+
Add the HTTP server, then log in:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
/mcp add --scope global slack https://mcp.slack.com/mcp
|
|
16
|
+
/mcp login slack
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Login uses the effective server definition (the trusted project override, if
|
|
20
|
+
present; otherwise the global definition) and starts the SDK's OAuth discovery
|
|
21
|
+
and authorization flow. It doesn't change configuration files. Adding a server
|
|
22
|
+
still doesn't connect or open a browser.
|
|
23
|
+
|
|
24
|
+
HTTP servers use automatic authentication by default:
|
|
25
|
+
|
|
26
|
+
- Configured Authorization headers take precedence over OAuth.
|
|
27
|
+
- Otherwise, connections reuse stored OAuth tokens when available. The SDK
|
|
28
|
+
handles authentication challenges and refreshes existing grants.
|
|
29
|
+
- Without a grant, an authentication challenge asks you to run `/mcp login`.
|
|
30
|
+
Discovery and tool calls never register a new client or open a browser.
|
|
31
|
+
- A missing or locked keyring doesn't block public servers. If the server
|
|
32
|
+
requires OAuth, an unavailable keyring is an error; no credentials are stored
|
|
33
|
+
outside the OS credential store.
|
|
34
|
+
|
|
35
|
+
The `oauth` configuration field and `--oauth` switch aren't supported. Remove
|
|
36
|
+
these from existing definitions and commands; HTTP authentication is automatic.
|
|
37
|
+
|
|
38
|
+
If the server uses an Authorization header, login asks you to remove that header
|
|
39
|
+
before switching to OAuth; it never replaces existing header credentials.
|
|
40
|
+
Stdio servers manage their own authentication and don't support OAuth login.
|
|
41
|
+
|
|
42
|
+
The extension supports public clients with dynamic registration or a
|
|
43
|
+
pre-registered client ID. Both use PKCE and a loopback callback at
|
|
44
|
+
`http://127.0.0.1:19847/callback` by default. Normal login opens a local listener
|
|
45
|
+
that your browser must be able to reach.
|
|
46
|
+
|
|
47
|
+
Authentication times out after two minutes; you can cancel it with Escape in the
|
|
48
|
+
terminal UI. Explicit login always starts a fresh authorization flow, even if a
|
|
49
|
+
refresh token is already stored. The browser callback page identifies **Pi MCP
|
|
50
|
+
Client** and asks you to return to Pi; receiving a callback doesn't yet mean the
|
|
51
|
+
token exchange succeeded.
|
|
52
|
+
|
|
53
|
+
OAuth tokens and client registrations are stored in the operating system
|
|
54
|
+
credential store, bound to the server URL, configured client ID (if any), and
|
|
55
|
+
authorization-server issuer. There is no plaintext credential fallback. PKCE
|
|
56
|
+
verifiers and callback state stay in memory. Linux requires a working Secret
|
|
57
|
+
Service/keyring session.
|
|
58
|
+
|
|
59
|
+
### Upgrade from earlier versions
|
|
60
|
+
|
|
61
|
+
Credentials now use an identity based only on the server URL and optional client
|
|
62
|
+
ID. Earlier credential-store entries aren't migrated or deleted. Run
|
|
63
|
+
`/mcp login <server>` again after upgrading; revoke old grants at the service if
|
|
64
|
+
needed. Changing scopes or the callback port doesn't select a different store.
|
|
65
|
+
|
|
66
|
+
## Use a pre-registered client
|
|
67
|
+
|
|
68
|
+
For a server without dynamic registration, register a **public/native** client
|
|
69
|
+
with the service, using the exact callback URL and token endpoint authentication
|
|
70
|
+
method `none`. Then configure its client ID:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"example": {
|
|
76
|
+
"url": "https://mcp.example.com/mcp",
|
|
77
|
+
"oauthClientId": "${EXAMPLE_OAUTH_CLIENT_ID}"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run `/mcp reload`, then `/mcp login example`. The configured ID is used for login,
|
|
84
|
+
token refresh, and revocation; the extension never falls back to dynamic
|
|
85
|
+
registration if it is rejected. `/mcp get example` identifies the client as
|
|
86
|
+
pre-registered without printing the ID.
|
|
87
|
+
|
|
88
|
+
Changing the client ID selects separate credentials and requires a new login.
|
|
89
|
+
Log out before changing or removing the ID if you want to delete its old
|
|
90
|
+
credentials. After the first successful grant, a pre-registered client is pinned
|
|
91
|
+
to its authorization-server issuer. If that issuer changes, verify the server
|
|
92
|
+
configuration before logging out and logging in again to trust the replacement.
|
|
93
|
+
|
|
94
|
+
## Set scopes and callback ports
|
|
95
|
+
|
|
96
|
+
Configure scopes and a callback port in the server definition:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"mcpServers": {
|
|
101
|
+
"example": {
|
|
102
|
+
"url": "https://mcp.example.com/mcp",
|
|
103
|
+
"oauthScopes": ["read", "write"],
|
|
104
|
+
"oauthCallbackPort": 19848
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or set them when adding the server:
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
/mcp add --scope global --oauth-scope read --oauth-scope write --oauth-callback-port 19848 example https://mcp.example.com/mcp
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The callback becomes `http://127.0.0.1:19848/callback`. Pre-registered clients must
|
|
117
|
+
allow that exact URL. The listener stays bound to loopback; arbitrary callback
|
|
118
|
+
hosts and paths aren't supported. If the port is occupied, choose another port or
|
|
119
|
+
use manual login.
|
|
120
|
+
|
|
121
|
+
Scopes are case-sensitive OAuth tokens, each up to 256 characters, without spaces,
|
|
122
|
+
quotes, or backslashes. Omit `oauthScopes` to retain SDK/server-driven selection;
|
|
123
|
+
an empty array is rejected. The SDK may also request `offline_access` when the
|
|
124
|
+
service advertises refresh-token support. Requested scopes aren't a guarantee of
|
|
125
|
+
granted permissions or a per-tool permission policy.
|
|
126
|
+
|
|
127
|
+
After changing these options, run `/mcp reload`, then `/mcp login example`.
|
|
128
|
+
Changing configuration never starts authorization or revokes existing grants.
|
|
129
|
+
Scopes and callback ports don't select separate credential stores: definitions
|
|
130
|
+
sharing a URL and client ID still share credentials. Explicit login renews a
|
|
131
|
+
dynamic registration when its requested options change. `/mcp get example` shows
|
|
132
|
+
the requested scopes and callback address without connecting.
|
|
133
|
+
|
|
134
|
+
## Sign in remotely or without launching a browser
|
|
135
|
+
|
|
136
|
+
When Pi runs over SSH, or you don't want it to launch a browser, use:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
/mcp login example --no-browser
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
1. Open the authorization URL shown in Pi's interactive dialog in your browser.
|
|
143
|
+
2. Complete sign-in. The browser may show a connection error at the loopback
|
|
144
|
+
callback address; this is expected when the browser and Pi run on different
|
|
145
|
+
machines.
|
|
146
|
+
3. Copy the full callback URL from the browser's address bar and paste it into
|
|
147
|
+
the **Callback URL** dialog in Pi, not into chat or a slash command.
|
|
148
|
+
|
|
149
|
+
Manual login doesn't open a browser or bind a callback port. The extension
|
|
150
|
+
validates the callback address, state, and authorization response before
|
|
151
|
+
exchanging the code. It doesn't write authorization URLs or pasted callbacks to
|
|
152
|
+
session entries, catalogs, notifications, or logs. Treat the callback URL as
|
|
153
|
+
sensitive; your browser history and clipboard may still contain it.
|
|
154
|
+
|
|
155
|
+
`--no-browser` still requires an interactive UI and an available OS credential
|
|
156
|
+
store. It isn't unattended authentication: print and JSON modes refuse OAuth
|
|
157
|
+
login. Use externally managed bearer headers for unattended access. Confidential
|
|
158
|
+
clients requiring a client secret aren't supported yet.
|
|
159
|
+
|
|
160
|
+
## Sign out
|
|
161
|
+
|
|
162
|
+
Run `/mcp logout <server>` to remove stored tokens and client registrations. Logout
|
|
163
|
+
also closes connections and deactivates tools for OAuth servers sharing
|
|
164
|
+
the same URL and configured client ID, since they share credentials. Configuration
|
|
165
|
+
and enabled state stay unchanged. Disabled servers accept logout too. Header and
|
|
166
|
+
server-managed credentials remain untouched.
|
|
167
|
+
|
|
168
|
+
Local removal happens before a bounded attempt to revoke tokens at the original
|
|
169
|
+
authorization server. The result distinguishes accepted revocation, unsupported
|
|
170
|
+
revocation, and unconfirmed revocation. When revocation isn't confirmed, remove the
|
|
171
|
+
grant at the service if needed. Repeating logout is safe. Other running Pi sessions
|
|
172
|
+
may need to reconnect; logout cannot recall requests already sent to a server.
|
|
173
|
+
|
|
174
|
+
Removing a server definition doesn't remove its credentials. Log out before
|
|
175
|
+
[removing the definition](commands.md#add-and-remove-servers) if you want both.
|