@fitchmultz/pi-mcp-adapter 8.0.5 → 8.0.7
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 +140 -7
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.d.ts +3 -3
- package/dist/tool-result-renderer.js +65 -44
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +29 -5
- package/skills/mcp-scripting/SKILL.md +6 -6
package/dist/mcp-auth.js
CHANGED
|
@@ -9,46 +9,47 @@
|
|
|
9
9
|
* when set, otherwise <Pi agent dir>/fitch-mcp-adapter/mcp-oauth/sha256-<server-hash>/tokens.json,
|
|
10
10
|
* then the plaintext file is removed.
|
|
11
11
|
*/
|
|
12
|
-
import { spawnSync } from
|
|
13
|
-
import { createHash } from
|
|
14
|
-
import { createRequire } from
|
|
15
|
-
import { readFileSync, existsSync, rmSync } from
|
|
16
|
-
import { dirname, join } from
|
|
17
|
-
import { fileURLToPath } from
|
|
18
|
-
import { getAdapterPath, getAgentPath } from
|
|
19
|
-
import { resolveConfiguredOAuthDir } from
|
|
12
|
+
import { spawnSync } from "child_process";
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
import { createRequire } from "module";
|
|
15
|
+
import { readFileSync, existsSync, rmSync } from "fs";
|
|
16
|
+
import { dirname, join } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import { getAdapterPath, getAgentPath } from "./agent-dir.js";
|
|
19
|
+
import { resolveConfiguredOAuthDir } from "./config.js";
|
|
20
20
|
const require = createRequire(import.meta.url);
|
|
21
|
-
const AUTH_SECRET_SERVICE =
|
|
22
|
-
const LEGACY_AUTH_SECRET_SERVICE =
|
|
23
|
-
const TEST_AUTH_STORE_ENV =
|
|
21
|
+
const AUTH_SECRET_SERVICE = "fitch-mcp-adapter.oauth";
|
|
22
|
+
const LEGACY_AUTH_SECRET_SERVICE = "pi-mcp-adapter.oauth";
|
|
23
|
+
const TEST_AUTH_STORE_ENV = "PI_MCP_ADAPTER_TEST_AUTH_STORE";
|
|
24
24
|
const AUTH_SECRET_CHUNK_SIZE = 1800;
|
|
25
|
-
const KEYRING_RECOVERY_DISABLED_ENV =
|
|
26
|
-
const KEYRING_RECOVERY_KEYCTL_ENV =
|
|
27
|
-
const KEYRING_RECOVERY_NODE_ENV =
|
|
28
|
-
const KEYRING_RECOVERY_HELPER_ENV =
|
|
29
|
-
const TEST_LINUX_KEYRING_RECOVERY_ENV =
|
|
25
|
+
const KEYRING_RECOVERY_DISABLED_ENV = "PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY";
|
|
26
|
+
const KEYRING_RECOVERY_KEYCTL_ENV = "PI_MCP_ADAPTER_KEYRING_RECOVERY_KEYCTL";
|
|
27
|
+
const KEYRING_RECOVERY_NODE_ENV = "PI_MCP_ADAPTER_KEYRING_RECOVERY_NODE";
|
|
28
|
+
const KEYRING_RECOVERY_HELPER_ENV = "PI_MCP_ADAPTER_KEYRING_RECOVERY_HELPER";
|
|
29
|
+
const TEST_LINUX_KEYRING_RECOVERY_ENV = "PI_MCP_ADAPTER_TEST_LINUX_KEYRING_RECOVERY";
|
|
30
30
|
const KEYRING_RECOVERY_TIMEOUT_MS = 10_000;
|
|
31
|
-
const AUTH_CHUNK_MANIFEST_KEY =
|
|
31
|
+
const AUTH_CHUNK_MANIFEST_KEY = "__piMcpAdapterOAuthChunked";
|
|
32
32
|
export class OAuthCredentialStoreError extends Error {
|
|
33
|
-
code =
|
|
33
|
+
code = "OAUTH_CREDENTIAL_STORE_UNAVAILABLE";
|
|
34
34
|
operation;
|
|
35
35
|
constructor(message, operation, cause) {
|
|
36
36
|
super(isAndroidKeyringLoadFailure(cause)
|
|
37
37
|
? `${message}. Android/Termux: @napi-rs/keyring could not load a native binding. Persistent OAuth requires a supported platform with an OS credential store; no plaintext fallback is used.`
|
|
38
38
|
: message, { cause });
|
|
39
39
|
this.operation = operation;
|
|
40
|
-
this.name =
|
|
40
|
+
this.name = "OAuthCredentialStoreError";
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
function causeChainContains(error, pattern) {
|
|
44
44
|
const seen = new Set();
|
|
45
45
|
let current = error;
|
|
46
|
-
while ((typeof current ===
|
|
47
|
-
if (seen.has(current))
|
|
46
|
+
while ((typeof current === "object" && current !== null) || typeof current === "function") {
|
|
47
|
+
if (seen.has(current)) {
|
|
48
48
|
break;
|
|
49
|
+
}
|
|
49
50
|
seen.add(current);
|
|
50
51
|
const candidate = current;
|
|
51
|
-
if ([candidate.name, candidate.message, candidate.code].some(value => typeof value ===
|
|
52
|
+
if ([candidate.name, candidate.message, candidate.code].some((value) => typeof value === "string" && pattern.test(value))) {
|
|
52
53
|
return true;
|
|
53
54
|
}
|
|
54
55
|
current = candidate.cause;
|
|
@@ -56,15 +57,18 @@ function causeChainContains(error, pattern) {
|
|
|
56
57
|
return false;
|
|
57
58
|
}
|
|
58
59
|
function isAndroidKeyringLoadFailure(error) {
|
|
59
|
-
return process.platform ===
|
|
60
|
+
return (process.platform === "android" &&
|
|
61
|
+
causeChainContains(error, /^Failed to load @napi-rs\/keyring;/));
|
|
60
62
|
}
|
|
61
63
|
export function formatOAuthCredentialStoreUnavailable(error) {
|
|
62
|
-
if (isAndroidKeyringLoadFailure(error))
|
|
64
|
+
if (isAndroidKeyringLoadFailure(error)) {
|
|
63
65
|
return error.message;
|
|
64
|
-
if (process.platform === 'linux' && causeChainContains(error, /key\s*(?:has been\s*)?revoked|keyrevoked/i)) {
|
|
65
|
-
return 'OAuth credential store unavailable: the Linux session keyring may be revoked. Start Pi from a fresh login/keyring session and retry.';
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
if (process.platform === "linux" &&
|
|
68
|
+
causeChainContains(error, /key\s*(?:has been\s*)?revoked|keyrevoked/i)) {
|
|
69
|
+
return "OAuth credential store unavailable: the Linux session keyring may be revoked. Start Pi from a fresh login/keyring session and retry.";
|
|
70
|
+
}
|
|
71
|
+
return "OAuth credential store unavailable. Configure or unlock the OS credential store and retry.";
|
|
68
72
|
}
|
|
69
73
|
let KeyringEntryClass;
|
|
70
74
|
const memoryAuthEntries = new Map();
|
|
@@ -96,17 +100,19 @@ function keyringAuthSecretStore(service) {
|
|
|
96
100
|
}
|
|
97
101
|
const unavailableAuthSecretStore = {
|
|
98
102
|
read() {
|
|
99
|
-
throw new Error(
|
|
103
|
+
throw new Error("simulated secure credential store unavailable");
|
|
100
104
|
},
|
|
101
105
|
write() {
|
|
102
|
-
throw new Error(
|
|
106
|
+
throw new Error("simulated secure credential store unavailable");
|
|
103
107
|
},
|
|
104
108
|
remove() {
|
|
105
|
-
throw new Error(
|
|
109
|
+
throw new Error("simulated secure credential store unavailable");
|
|
106
110
|
},
|
|
107
111
|
};
|
|
108
112
|
function createKeyRevokedTestError() {
|
|
109
|
-
return new Error("Couldn't access platform storage: KeyRevoked", {
|
|
113
|
+
return new Error("Couldn't access platform storage: KeyRevoked", {
|
|
114
|
+
cause: new Error("KeyRevoked"),
|
|
115
|
+
});
|
|
110
116
|
}
|
|
111
117
|
const keyRevokedAuthSecretStore = {
|
|
112
118
|
read() {
|
|
@@ -120,12 +126,15 @@ const keyRevokedAuthSecretStore = {
|
|
|
120
126
|
},
|
|
121
127
|
};
|
|
122
128
|
function getAuthSecretStore(service = AUTH_SECRET_SERVICE) {
|
|
123
|
-
if (process.env[TEST_AUTH_STORE_ENV] ===
|
|
129
|
+
if (process.env[TEST_AUTH_STORE_ENV] === "memory") {
|
|
124
130
|
return memoryAuthSecretStore(service);
|
|
125
|
-
|
|
131
|
+
}
|
|
132
|
+
if (process.env[TEST_AUTH_STORE_ENV] === "unavailable") {
|
|
126
133
|
return unavailableAuthSecretStore;
|
|
127
|
-
|
|
134
|
+
}
|
|
135
|
+
if (process.env[TEST_AUTH_STORE_ENV] === "keyrevoked") {
|
|
128
136
|
return keyRevokedAuthSecretStore;
|
|
137
|
+
}
|
|
129
138
|
return keyringAuthSecretStore(service);
|
|
130
139
|
}
|
|
131
140
|
function getKeyringEntry(account, service) {
|
|
@@ -134,12 +143,12 @@ function getKeyringEntry(account, service) {
|
|
|
134
143
|
return new KeyringEntryClass(service, account);
|
|
135
144
|
}
|
|
136
145
|
catch (error) {
|
|
137
|
-
throw new Error(
|
|
146
|
+
throw new Error("OAuth secure credential storage is unavailable. Configure the OS credential store and retry authentication.", { cause: error });
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
function loadKeyringEntryClass(keyringRequire = require, platform = process.platform, arch = process.arch) {
|
|
141
150
|
try {
|
|
142
|
-
return keyringRequire(
|
|
151
|
+
return keyringRequire("@napi-rs/keyring").Entry;
|
|
143
152
|
}
|
|
144
153
|
catch (loaderError) {
|
|
145
154
|
try {
|
|
@@ -170,157 +179,178 @@ function loadKeyringNativeBindingFallback(keyringRequire, platform, arch) {
|
|
|
170
179
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
171
180
|
}
|
|
172
181
|
function getKeyringNativeBindingTargets(platform, arch) {
|
|
173
|
-
return getKeyringNativeBindingSuffixes(platform, arch).map(suffix => ({
|
|
182
|
+
return getKeyringNativeBindingSuffixes(platform, arch).map((suffix) => ({
|
|
174
183
|
packageName: `@napi-rs/keyring-${suffix}`,
|
|
175
184
|
bindingFile: `keyring.${suffix}.node`,
|
|
176
185
|
}));
|
|
177
186
|
}
|
|
178
187
|
function getKeyringNativeBindingSuffixes(platform, arch) {
|
|
179
|
-
if (platform ===
|
|
180
|
-
if (arch ===
|
|
181
|
-
return [
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (arch ===
|
|
189
|
-
return [
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (arch ===
|
|
195
|
-
return [
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
188
|
+
if (platform === "darwin") {
|
|
189
|
+
if (arch === "arm64") {
|
|
190
|
+
return ["darwin-arm64"];
|
|
191
|
+
}
|
|
192
|
+
if (arch === "x64") {
|
|
193
|
+
return ["darwin-x64"];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (platform === "win32") {
|
|
197
|
+
if (arch === "arm64") {
|
|
198
|
+
return ["win32-arm64-msvc"];
|
|
199
|
+
}
|
|
200
|
+
if (arch === "x64") {
|
|
201
|
+
return ["win32-x64-msvc"];
|
|
202
|
+
}
|
|
203
|
+
if (arch === "ia32") {
|
|
204
|
+
return ["win32-ia32-msvc"];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (platform === "linux") {
|
|
208
|
+
if (arch === "arm64") {
|
|
209
|
+
return ["linux-arm64-gnu", "linux-arm64-musl"];
|
|
210
|
+
}
|
|
211
|
+
if (arch === "arm") {
|
|
212
|
+
return ["linux-arm-gnueabihf"];
|
|
213
|
+
}
|
|
214
|
+
if (arch === "riscv64") {
|
|
215
|
+
return ["linux-riscv64-gnu"];
|
|
216
|
+
}
|
|
217
|
+
if (arch === "x64") {
|
|
218
|
+
return ["linux-x64-gnu", "linux-x64-musl"];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (platform === "freebsd" && arch === "x64") {
|
|
222
|
+
return ["freebsd-x64"];
|
|
223
|
+
}
|
|
205
224
|
return [];
|
|
206
225
|
}
|
|
207
226
|
function formatErrorMessage(error) {
|
|
208
227
|
return error instanceof Error ? error.message : String(error);
|
|
209
228
|
}
|
|
210
229
|
function isLinuxKeyringRecoveryEnabled() {
|
|
211
|
-
if (process.env[KEYRING_RECOVERY_DISABLED_ENV] ===
|
|
230
|
+
if (process.env[KEYRING_RECOVERY_DISABLED_ENV] === "1") {
|
|
212
231
|
return false;
|
|
213
|
-
|
|
232
|
+
}
|
|
233
|
+
return process.platform === "linux" || process.env[TEST_LINUX_KEYRING_RECOVERY_ENV] === "1";
|
|
214
234
|
}
|
|
215
235
|
function shouldAttemptLinuxKeyringRecovery(error) {
|
|
216
|
-
return isLinuxKeyringRecoveryEnabled()
|
|
217
|
-
|
|
236
|
+
return (isLinuxKeyringRecoveryEnabled() &&
|
|
237
|
+
causeChainContains(error, /key\s*(?:has been\s*)?revoked|keyrevoked/i));
|
|
218
238
|
}
|
|
219
239
|
function runLinuxKeyringRecoveryOperation(operation, account, payload, service) {
|
|
220
|
-
const keyctl = process.env[KEYRING_RECOVERY_KEYCTL_ENV]?.trim() ||
|
|
221
|
-
const node = process.env[KEYRING_RECOVERY_NODE_ENV]?.trim() ||
|
|
222
|
-
const helper = process.env[KEYRING_RECOVERY_HELPER_ENV]?.trim()
|
|
223
|
-
|
|
240
|
+
const keyctl = process.env[KEYRING_RECOVERY_KEYCTL_ENV]?.trim() || "keyctl";
|
|
241
|
+
const node = process.env[KEYRING_RECOVERY_NODE_ENV]?.trim() || "node";
|
|
242
|
+
const helper = process.env[KEYRING_RECOVERY_HELPER_ENV]?.trim() ||
|
|
243
|
+
fileURLToPath(new URL("./mcp-keyring-helper.cjs", import.meta.url));
|
|
224
244
|
const request = JSON.stringify({ operation, service, account, payload });
|
|
225
|
-
const result = spawnSync(keyctl, [
|
|
245
|
+
const result = spawnSync(keyctl, ["session", "-", node, helper], {
|
|
226
246
|
input: `${request}\n`,
|
|
227
|
-
encoding:
|
|
247
|
+
encoding: "utf8",
|
|
228
248
|
maxBuffer: 1024 * 1024,
|
|
229
249
|
timeout: KEYRING_RECOVERY_TIMEOUT_MS,
|
|
230
250
|
windowsHide: true,
|
|
231
251
|
});
|
|
232
252
|
if (result.error) {
|
|
233
|
-
throw new Error(`Linux keyring recovery helper could not start: ${result.error.message}`, {
|
|
253
|
+
throw new Error(`Linux keyring recovery helper could not start: ${result.error.message}`, {
|
|
254
|
+
cause: result.error,
|
|
255
|
+
});
|
|
234
256
|
}
|
|
235
257
|
if (result.status !== 0) {
|
|
236
|
-
throw new Error(`Linux keyring recovery helper failed with exit code ${result.status ??
|
|
258
|
+
throw new Error(`Linux keyring recovery helper failed with exit code ${result.status ?? "unknown"}`);
|
|
237
259
|
}
|
|
238
260
|
let response;
|
|
239
261
|
try {
|
|
240
262
|
response = JSON.parse(result.stdout.trim());
|
|
241
263
|
}
|
|
242
264
|
catch (error) {
|
|
243
|
-
throw new Error(
|
|
265
|
+
throw new Error("Linux keyring recovery helper returned invalid JSON", { cause: error });
|
|
244
266
|
}
|
|
245
|
-
if (typeof response !==
|
|
246
|
-
|
|
267
|
+
if (typeof response !== "object" ||
|
|
268
|
+
response === null ||
|
|
269
|
+
typeof response.ok !== "boolean") {
|
|
270
|
+
throw new Error("Linux keyring recovery helper returned an invalid response");
|
|
247
271
|
}
|
|
248
272
|
const typedResponse = response;
|
|
249
|
-
if (typedResponse.ok
|
|
250
|
-
throw new Error(typedResponse.error ||
|
|
273
|
+
if (!typedResponse.ok) {
|
|
274
|
+
throw new Error(typedResponse.error || "Linux keyring recovery helper failed");
|
|
251
275
|
}
|
|
252
|
-
if (operation ===
|
|
253
|
-
|
|
276
|
+
if (operation === "read" &&
|
|
277
|
+
typedResponse.found === true &&
|
|
278
|
+
typeof typedResponse.value !== "string") {
|
|
279
|
+
throw new Error("Linux keyring recovery helper returned an invalid read response");
|
|
254
280
|
}
|
|
255
281
|
return typedResponse;
|
|
256
282
|
}
|
|
257
283
|
function linuxKeyringRecoveryAuthSecretStore(service = AUTH_SECRET_SERVICE) {
|
|
258
284
|
return {
|
|
259
285
|
read(account) {
|
|
260
|
-
const response = runLinuxKeyringRecoveryOperation(
|
|
286
|
+
const response = runLinuxKeyringRecoveryOperation("read", account, undefined, service);
|
|
261
287
|
return response.ok && response.found === true ? response.value : undefined;
|
|
262
288
|
},
|
|
263
289
|
write(account, payload) {
|
|
264
|
-
runLinuxKeyringRecoveryOperation(
|
|
290
|
+
runLinuxKeyringRecoveryOperation("write", account, payload, service);
|
|
265
291
|
},
|
|
266
292
|
remove(account) {
|
|
267
|
-
runLinuxKeyringRecoveryOperation(
|
|
293
|
+
runLinuxKeyringRecoveryOperation("remove", account, undefined, service);
|
|
268
294
|
},
|
|
269
295
|
};
|
|
270
296
|
}
|
|
271
297
|
export function getAuthStorageOptions(oauthDir, cwd = process.cwd()) {
|
|
272
298
|
const baseDir = resolveConfiguredOAuthDir(oauthDir, cwd);
|
|
273
|
-
return baseDir ? { baseDir: join(baseDir,
|
|
299
|
+
return baseDir ? { baseDir: join(baseDir, "fitch-mcp-adapter") } : {};
|
|
274
300
|
}
|
|
275
301
|
export function getAuthBaseDir(options = {}) {
|
|
276
302
|
const override = process.env.FITCH_MCP_OAUTH_DIR?.trim();
|
|
277
|
-
if (override)
|
|
303
|
+
if (override) {
|
|
278
304
|
return override;
|
|
279
|
-
|
|
305
|
+
}
|
|
306
|
+
return options.baseDir ?? getAdapterPath("mcp-oauth");
|
|
280
307
|
}
|
|
281
308
|
/**
|
|
282
309
|
* Get the legacy server-specific directory path.
|
|
283
310
|
*/
|
|
284
311
|
function getServerDir(serverName, options) {
|
|
285
|
-
if (typeof serverName !==
|
|
312
|
+
if (typeof serverName !== "string") {
|
|
286
313
|
throw new Error(`Invalid MCP server name: ${JSON.stringify(serverName)}`);
|
|
287
314
|
}
|
|
288
315
|
const storageKey = getAuthEntryAccount(serverName);
|
|
289
316
|
return join(getAuthBaseDir(options), storageKey);
|
|
290
317
|
}
|
|
291
318
|
function getAuthEntryAccount(serverName) {
|
|
292
|
-
if (typeof serverName !==
|
|
319
|
+
if (typeof serverName !== "string") {
|
|
293
320
|
throw new Error(`Invalid MCP server name: ${JSON.stringify(serverName)}`);
|
|
294
321
|
}
|
|
295
|
-
return `sha256-${createHash(
|
|
322
|
+
return `sha256-${createHash("sha256").update(serverName, "utf8").digest("hex")}`;
|
|
296
323
|
}
|
|
297
324
|
/**
|
|
298
325
|
* Get the legacy plaintext tokens file path for a server.
|
|
299
326
|
*/
|
|
300
327
|
export function getAuthEntryFilePath(serverName, options) {
|
|
301
|
-
return join(getServerDir(serverName, options),
|
|
328
|
+
return join(getServerDir(serverName, options), "tokens.json");
|
|
302
329
|
}
|
|
303
330
|
function parseJsonPayload(serverName, payload, source) {
|
|
304
331
|
try {
|
|
305
332
|
return JSON.parse(payload);
|
|
306
333
|
}
|
|
307
334
|
catch (error) {
|
|
308
|
-
throw new Error(`Failed to parse OAuth credentials for ${serverName} from ${source}`, {
|
|
335
|
+
throw new Error(`Failed to parse OAuth credentials for ${serverName} from ${source}`, {
|
|
336
|
+
cause: error,
|
|
337
|
+
});
|
|
309
338
|
}
|
|
310
339
|
}
|
|
311
340
|
function parseAuthEntryPayload(serverName, payload, source) {
|
|
312
341
|
return parseJsonPayload(serverName, payload, source);
|
|
313
342
|
}
|
|
314
343
|
function isAuthEntryChunkManifest(value) {
|
|
315
|
-
if (typeof value !==
|
|
344
|
+
if (typeof value !== "object" || value === null) {
|
|
316
345
|
return false;
|
|
346
|
+
}
|
|
317
347
|
const manifest = value;
|
|
318
|
-
return manifest[AUTH_CHUNK_MANIFEST_KEY] === 1
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
348
|
+
return (manifest[AUTH_CHUNK_MANIFEST_KEY] === 1 &&
|
|
349
|
+
typeof manifest.chunkCount === "number" &&
|
|
350
|
+
Number.isInteger(manifest.chunkCount) &&
|
|
351
|
+
manifest.chunkCount > 0 &&
|
|
352
|
+
typeof manifest.chunkDigest === "string" &&
|
|
353
|
+
/^[a-f0-9]{16}$/.test(manifest.chunkDigest));
|
|
324
354
|
}
|
|
325
355
|
function getAuthEntryChunkAccount(account, manifest, index) {
|
|
326
356
|
return `${account}.chunk.${manifest.chunkDigest}.${index}`;
|
|
@@ -335,7 +365,9 @@ function readChunkManifestFromPayload(serverName, payload, source) {
|
|
|
335
365
|
function readExistingChunkManifest(store, serverName, account) {
|
|
336
366
|
try {
|
|
337
367
|
const payload = store.read(account);
|
|
338
|
-
return payload === undefined
|
|
368
|
+
return payload === undefined
|
|
369
|
+
? undefined
|
|
370
|
+
: readChunkManifestFromPayload(serverName, payload, "OS secure credential store");
|
|
339
371
|
}
|
|
340
372
|
catch {
|
|
341
373
|
return undefined;
|
|
@@ -347,8 +379,9 @@ function removeChunkPayloads(store, account, manifest) {
|
|
|
347
379
|
}
|
|
348
380
|
}
|
|
349
381
|
function tryRemoveChunkPayloads(store, account, manifest) {
|
|
350
|
-
if (!manifest)
|
|
382
|
+
if (!manifest) {
|
|
351
383
|
return;
|
|
384
|
+
}
|
|
352
385
|
try {
|
|
353
386
|
removeChunkPayloads(store, account, manifest);
|
|
354
387
|
}
|
|
@@ -360,7 +393,7 @@ function createChunkManifest(payload) {
|
|
|
360
393
|
return {
|
|
361
394
|
[AUTH_CHUNK_MANIFEST_KEY]: 1,
|
|
362
395
|
chunkCount: Math.ceil(payload.length / AUTH_SECRET_CHUNK_SIZE),
|
|
363
|
-
chunkDigest: createHash(
|
|
396
|
+
chunkDigest: createHash("sha256").update(payload, "utf8").digest("hex").slice(0, 16),
|
|
364
397
|
};
|
|
365
398
|
}
|
|
366
399
|
function readChunkedAuthEntry(store, serverName, account, manifest) {
|
|
@@ -373,22 +406,24 @@ function readChunkedAuthEntry(store, serverName, account, manifest) {
|
|
|
373
406
|
return chunk;
|
|
374
407
|
}
|
|
375
408
|
catch (error) {
|
|
376
|
-
throw new OAuthCredentialStoreError(`Failed to read OAuth credentials for ${serverName} from the OS secure credential store`,
|
|
409
|
+
throw new OAuthCredentialStoreError(`Failed to read OAuth credentials for ${serverName} from the OS secure credential store`, "read", error);
|
|
377
410
|
}
|
|
378
411
|
});
|
|
379
|
-
return parseAuthEntryPayload(serverName, chunks.join(
|
|
412
|
+
return parseAuthEntryPayload(serverName, chunks.join(""), "OS secure credential store chunks");
|
|
380
413
|
}
|
|
381
414
|
function readLegacyAuthEntry(serverName, options) {
|
|
382
415
|
const filePath = getAuthEntryFilePath(serverName, options);
|
|
383
|
-
if (!existsSync(filePath))
|
|
416
|
+
if (!existsSync(filePath)) {
|
|
384
417
|
return undefined;
|
|
385
|
-
|
|
418
|
+
}
|
|
419
|
+
const data = readFileSync(filePath, "utf-8");
|
|
386
420
|
return parseAuthEntryPayload(serverName, data, filePath);
|
|
387
421
|
}
|
|
388
422
|
function removeLegacyAuthEntry(serverName, options) {
|
|
389
423
|
const filePath = getAuthEntryFilePath(serverName, options);
|
|
390
|
-
if (!existsSync(filePath))
|
|
424
|
+
if (!existsSync(filePath)) {
|
|
391
425
|
return;
|
|
426
|
+
}
|
|
392
427
|
try {
|
|
393
428
|
rmSync(filePath, { force: true });
|
|
394
429
|
}
|
|
@@ -426,7 +461,7 @@ function writeSecureAuthEntryToStore(store, serverName, entry) {
|
|
|
426
461
|
}
|
|
427
462
|
catch (error) {
|
|
428
463
|
tryRemoveChunkPayloads(store, account, manifest);
|
|
429
|
-
throw new OAuthCredentialStoreError(`Failed to write OAuth credentials for ${serverName} to the OS secure credential store`,
|
|
464
|
+
throw new OAuthCredentialStoreError(`Failed to write OAuth credentials for ${serverName} to the OS secure credential store`, "write", error);
|
|
430
465
|
}
|
|
431
466
|
}
|
|
432
467
|
function writeSecureAuthEntry(serverName, entry) {
|
|
@@ -434,8 +469,9 @@ function writeSecureAuthEntry(serverName, entry) {
|
|
|
434
469
|
writeSecureAuthEntryToStore(getAuthSecretStore(), serverName, entry);
|
|
435
470
|
}
|
|
436
471
|
catch (error) {
|
|
437
|
-
if (!shouldAttemptLinuxKeyringRecovery(error))
|
|
472
|
+
if (!shouldAttemptLinuxKeyringRecovery(error)) {
|
|
438
473
|
throw error;
|
|
474
|
+
}
|
|
439
475
|
writeSecureAuthEntryToStore(linuxKeyringRecoveryAuthSecretStore(), serverName, entry);
|
|
440
476
|
}
|
|
441
477
|
}
|
|
@@ -446,58 +482,64 @@ function readSecureAuthEntryFromStore(store, serverName) {
|
|
|
446
482
|
payload = store.read(account);
|
|
447
483
|
}
|
|
448
484
|
catch (error) {
|
|
449
|
-
throw new OAuthCredentialStoreError(`Failed to read OAuth credentials for ${serverName} from the OS secure credential store`,
|
|
485
|
+
throw new OAuthCredentialStoreError(`Failed to read OAuth credentials for ${serverName} from the OS secure credential store`, "read", error);
|
|
450
486
|
}
|
|
451
|
-
if (payload === undefined)
|
|
487
|
+
if (payload === undefined) {
|
|
452
488
|
return undefined;
|
|
453
|
-
|
|
489
|
+
}
|
|
490
|
+
const manifest = readChunkManifestFromPayload(serverName, payload, "OS secure credential store");
|
|
454
491
|
return manifest
|
|
455
492
|
? readChunkedAuthEntry(store, serverName, account, manifest)
|
|
456
|
-
: parseAuthEntryPayload(serverName, payload,
|
|
493
|
+
: parseAuthEntryPayload(serverName, payload, "OS secure credential store");
|
|
457
494
|
}
|
|
458
495
|
function readSecureAuthEntry(serverName, service = AUTH_SECRET_SERVICE) {
|
|
459
496
|
try {
|
|
460
497
|
return readSecureAuthEntryFromStore(getAuthSecretStore(service), serverName);
|
|
461
498
|
}
|
|
462
499
|
catch (error) {
|
|
463
|
-
if (!shouldAttemptLinuxKeyringRecovery(error))
|
|
500
|
+
if (!shouldAttemptLinuxKeyringRecovery(error)) {
|
|
464
501
|
throw error;
|
|
502
|
+
}
|
|
465
503
|
return readSecureAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore(service), serverName);
|
|
466
504
|
}
|
|
467
505
|
}
|
|
468
506
|
function readAuthEntry(serverName, options, behavior = {}) {
|
|
469
507
|
const entry = readSecureAuthEntry(serverName);
|
|
470
508
|
if (entry !== undefined) {
|
|
471
|
-
if (behavior.migrateLegacy !== false)
|
|
509
|
+
if (behavior.migrateLegacy !== false) {
|
|
472
510
|
removeLegacyAuthEntry(serverName, options);
|
|
511
|
+
}
|
|
473
512
|
return entry;
|
|
474
513
|
}
|
|
475
514
|
const legacyEntry = readLegacyAuthEntry(serverName, options);
|
|
476
|
-
if (!legacyEntry || behavior.migrateLegacy === false)
|
|
515
|
+
if (!legacyEntry || behavior.migrateLegacy === false) {
|
|
477
516
|
return legacyEntry;
|
|
517
|
+
}
|
|
478
518
|
writeSecureAuthEntry(serverName, legacyEntry);
|
|
479
519
|
removeLegacyAuthEntry(serverName, options);
|
|
480
520
|
return legacyEntry;
|
|
481
521
|
}
|
|
482
522
|
/** Explicit copy-only upgrade. Normal reads and logout never access the old service. */
|
|
483
|
-
export function migrateLegacyAuthEntry(serverName, legacyDirectory = process.env.MCP_OAUTH_DIR?.trim() || getAgentPath(
|
|
484
|
-
if (readSecureAuthEntry(serverName) !== undefined)
|
|
485
|
-
return
|
|
523
|
+
export function migrateLegacyAuthEntry(serverName, legacyDirectory = process.env.MCP_OAUTH_DIR?.trim() || getAgentPath("mcp-oauth")) {
|
|
524
|
+
if (readSecureAuthEntry(serverName) !== undefined) {
|
|
525
|
+
return "existing";
|
|
526
|
+
}
|
|
486
527
|
let entry = readSecureAuthEntry(serverName, LEGACY_AUTH_SECRET_SERVICE);
|
|
487
528
|
if (entry === undefined) {
|
|
488
|
-
const path = join(legacyDirectory, getAuthEntryAccount(serverName),
|
|
489
|
-
if (!existsSync(path))
|
|
490
|
-
return
|
|
491
|
-
|
|
529
|
+
const path = join(legacyDirectory, getAuthEntryAccount(serverName), "tokens.json");
|
|
530
|
+
if (!existsSync(path)) {
|
|
531
|
+
return "absent";
|
|
532
|
+
}
|
|
533
|
+
entry = parseAuthEntryPayload(serverName, readFileSync(path, "utf8"), path);
|
|
492
534
|
}
|
|
493
|
-
if (!entry || typeof entry !==
|
|
535
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
494
536
|
throw new Error(`Invalid legacy OAuth entry for ${serverName}`);
|
|
495
537
|
}
|
|
496
538
|
writeSecureAuthEntry(serverName, entry);
|
|
497
539
|
if (JSON.stringify(readSecureAuthEntry(serverName)) !== JSON.stringify(entry)) {
|
|
498
540
|
throw new Error(`Failed to verify migrated OAuth credentials for ${serverName}`);
|
|
499
541
|
}
|
|
500
|
-
return
|
|
542
|
+
return "copied";
|
|
501
543
|
}
|
|
502
544
|
/**
|
|
503
545
|
* Get auth entry for a server.
|
|
@@ -511,14 +553,17 @@ export function getAuthEntry(serverName, options) {
|
|
|
511
553
|
*/
|
|
512
554
|
export function getAuthForUrl(serverName, serverUrl, options) {
|
|
513
555
|
const entry = getAuthEntry(serverName, options);
|
|
514
|
-
if (!entry)
|
|
556
|
+
if (!entry) {
|
|
515
557
|
return undefined;
|
|
558
|
+
}
|
|
516
559
|
// If no serverUrl is stored, this is from an old version - consider it invalid
|
|
517
|
-
if (!entry.serverUrl)
|
|
560
|
+
if (!entry.serverUrl) {
|
|
518
561
|
return undefined;
|
|
562
|
+
}
|
|
519
563
|
// If URL has changed, credentials are invalid
|
|
520
|
-
if (entry.serverUrl !== serverUrl)
|
|
564
|
+
if (entry.serverUrl !== serverUrl) {
|
|
521
565
|
return undefined;
|
|
566
|
+
}
|
|
522
567
|
return entry;
|
|
523
568
|
}
|
|
524
569
|
/**
|
|
@@ -529,14 +574,16 @@ export function getAuthForUrl(serverName, serverUrl, options) {
|
|
|
529
574
|
export function inspectAuthForUrl(serverName, serverUrl, options) {
|
|
530
575
|
try {
|
|
531
576
|
const entry = readAuthEntry(serverName, options, { migrateLegacy: false });
|
|
532
|
-
if (!entry?.serverUrl || entry.serverUrl !== serverUrl)
|
|
533
|
-
return { status:
|
|
534
|
-
|
|
577
|
+
if (!entry?.serverUrl || entry.serverUrl !== serverUrl) {
|
|
578
|
+
return { status: "absent" };
|
|
579
|
+
}
|
|
580
|
+
return { status: "present", entry };
|
|
535
581
|
}
|
|
536
582
|
catch (error) {
|
|
537
|
-
if (!(error instanceof OAuthCredentialStoreError))
|
|
583
|
+
if (!(error instanceof OAuthCredentialStoreError)) {
|
|
538
584
|
throw error;
|
|
539
|
-
|
|
585
|
+
}
|
|
586
|
+
return { status: "unavailable", message: formatOAuthCredentialStoreUnavailable(error) };
|
|
540
587
|
}
|
|
541
588
|
}
|
|
542
589
|
/**
|
|
@@ -557,13 +604,16 @@ function removeAuthEntryFromStore(store, serverName) {
|
|
|
557
604
|
const account = getAuthEntryAccount(serverName);
|
|
558
605
|
try {
|
|
559
606
|
const payload = store.read(account);
|
|
560
|
-
const manifest = payload === undefined
|
|
561
|
-
|
|
607
|
+
const manifest = payload === undefined
|
|
608
|
+
? undefined
|
|
609
|
+
: readChunkManifestFromPayload(serverName, payload, "OS secure credential store");
|
|
610
|
+
if (manifest) {
|
|
562
611
|
removeChunkPayloads(store, account, manifest);
|
|
612
|
+
}
|
|
563
613
|
store.remove(account);
|
|
564
614
|
}
|
|
565
615
|
catch (error) {
|
|
566
|
-
throw new OAuthCredentialStoreError(`Failed to remove OAuth credentials for ${serverName} from the OS secure credential store`,
|
|
616
|
+
throw new OAuthCredentialStoreError(`Failed to remove OAuth credentials for ${serverName} from the OS secure credential store`, "remove", error);
|
|
567
617
|
}
|
|
568
618
|
}
|
|
569
619
|
export function removeAuthEntry(serverName, options) {
|
|
@@ -571,8 +621,9 @@ export function removeAuthEntry(serverName, options) {
|
|
|
571
621
|
removeAuthEntryFromStore(getAuthSecretStore(), serverName);
|
|
572
622
|
}
|
|
573
623
|
catch (error) {
|
|
574
|
-
if (!shouldAttemptLinuxKeyringRecovery(error))
|
|
624
|
+
if (!shouldAttemptLinuxKeyringRecovery(error)) {
|
|
575
625
|
throw error;
|
|
626
|
+
}
|
|
576
627
|
removeAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore(), serverName);
|
|
577
628
|
}
|
|
578
629
|
removeLegacyAuthEntry(serverName, options);
|