@fitchmultz/pi-mcp-adapter 8.0.0 → 8.0.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 +36 -7
- package/LICENSE +1 -0
- package/OAUTH.md +3 -3
- package/README.md +43 -858
- package/dist/app-bridge.bundle.js +14 -14
- package/dist/client-operations.d.ts +5 -0
- package/dist/client-operations.js +19 -0
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +3 -1
- package/dist/config.js +1 -1
- package/dist/connection-runtime.d.ts +503 -0
- package/dist/connection-runtime.js +98 -0
- package/dist/direct-tools.js +1 -1
- package/dist/host-html-template.js +24 -12
- package/dist/index.js +36 -29
- package/dist/init.d.ts +1 -1
- package/dist/init.js +66 -38
- package/dist/lifecycle.d.ts +1 -1
- package/dist/lifecycle.js +1 -1
- package/dist/mcp-auth-flow.d.ts +2 -43
- package/dist/mcp-auth-flow.js +3 -138
- package/dist/mcp-callback-server.js +1 -1
- package/dist/mcp-code.d.ts +0 -1
- package/dist/mcp-code.js +20 -7
- package/dist/mcp-oauth-callback-config.d.ts +8 -0
- package/dist/mcp-oauth-callback-config.js +28 -0
- package/dist/mcp-oauth-provider.d.ts +1 -8
- package/dist/mcp-oauth-provider.js +2 -28
- package/dist/mcp-oauth-runtime.d.ts +83 -0
- package/dist/mcp-oauth-runtime.js +145 -0
- package/dist/mcp-output-guard.d.ts +1 -1
- package/dist/mcp-output-guard.js +5 -108
- package/dist/mcp-output-preview.d.ts +29 -0
- package/dist/mcp-output-preview.js +220 -0
- package/dist/mcp-panel.js +1 -2
- package/dist/mcp-script-worker.mjs +14 -2
- package/dist/mcp-setup-panel.js +1 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +56 -14
- package/dist/plain-result-reader.d.ts +7 -0
- package/dist/plain-result-reader.js +201 -0
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +1 -1
- package/dist/proxy-modes.js +14 -5
- package/dist/public-types.d.ts +2 -0
- package/dist/public-types.js +2 -0
- package/dist/resource-tools.d.ts +0 -1
- package/dist/resource-tools.js +0 -13
- package/dist/server-manager.d.ts +4 -4
- package/dist/server-manager.js +25 -125
- package/dist/tool-loader.js +10 -4
- package/dist/tool-metadata.js +20 -3
- package/dist/tool-result-renderer.d.ts +1 -1
- package/dist/tool-result-renderer.js +10 -3
- package/dist/types.d.ts +1 -1
- package/dist/types.js +0 -3
- package/dist/ui-message-history.js +3 -2
- package/dist/ui-resource-handler.d.ts +2 -1
- package/dist/ui-resource-handler.js +4 -3
- package/dist/ui-server.js +2 -1
- package/dist/ui-session.js +2 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +1 -0
- package/package.json +10 -13
package/dist/mcp-auth-flow.js
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
import { auth as runSdkAuth, extractWWWAuthenticateParams, UnauthorizedError, Client, StreamableHTTPClientTransport, validateAuthorizationResponseIssuer, InsufficientScopeError, SdkHttpError, computeScopeUnion, isStrictScopeSuperset, } from "@modelcontextprotocol/client";
|
|
7
7
|
import open from "open";
|
|
8
8
|
import { McpOAuthProvider, issuersMatch, loopbackRedirectsMatch, validateOAuthClientMetadataUrl, validateOAuthPrivateKeyJwt, validateOAuthCrossAppAccess } from "./mcp-oauth-provider.js";
|
|
9
|
-
import { ensureCallbackServer, waitForCallback, cancelPendingCallback,
|
|
9
|
+
import { ensureCallbackServer, waitForCallback, cancelPendingCallback, releaseCallbackServer, takeCallbackResult, } from "./mcp-callback-server.js";
|
|
10
10
|
import { getAuthForUrl, clearAllCredentials, clearClientInfo, clearTokens, clearCodeVerifier, getOAuthState, clearOAuthState, getAuthBaseDir, } from "./mcp-auth.js";
|
|
11
11
|
import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig } from "./types.js";
|
|
12
12
|
import { ADAPTER_VERSION, formatTerminalError, interpolateEnvRecord, interpolateEnvVars, normalizeRequestTimeoutMs } from "./utils.js";
|
|
13
13
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
14
14
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
|
|
15
|
+
import { clearPendingAuth, getPendingAuthKey, getRuntime, getRuntimeState, legacyRuntime, } from "./mcp-oauth-runtime.js";
|
|
16
|
+
export { createOAuthRuntime, hasPendingAuth, initializeOAuth, shutdownOAuth, supportsOAuth, } from "./mcp-oauth-runtime.js";
|
|
15
17
|
function applyConfiguredScope(discovery, config) {
|
|
16
18
|
return {
|
|
17
19
|
...discovery,
|
|
@@ -34,45 +36,6 @@ function validateCallbackIssuer(serverName, context, response) {
|
|
|
34
36
|
throw new Error(`The OAuth authorization response issuer does not match the discovered issuer for ${serverName}.`);
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
const runtimeStates = new WeakMap();
|
|
38
|
-
const activeRuntimes = new Set();
|
|
39
|
-
export function createOAuthRuntime(signal) {
|
|
40
|
-
const controller = new AbortController();
|
|
41
|
-
const runtime = { signal: combineAbortSignals(signal, controller.signal) };
|
|
42
|
-
runtimeStates.set(runtime, {
|
|
43
|
-
controller,
|
|
44
|
-
generation: 0,
|
|
45
|
-
pendingAuths: new Map(),
|
|
46
|
-
pendingAuthStates: new Map(),
|
|
47
|
-
pendingAuthCleanupTimers: new Map(),
|
|
48
|
-
pendingAuthentications: new Map(),
|
|
49
|
-
requests: new Map(),
|
|
50
|
-
});
|
|
51
|
-
activeRuntimes.add(runtime);
|
|
52
|
-
return runtime;
|
|
53
|
-
}
|
|
54
|
-
let legacyRuntime = createOAuthRuntime();
|
|
55
|
-
activeRuntimes.delete(legacyRuntime);
|
|
56
|
-
function getRuntime(options) {
|
|
57
|
-
if (options?.runtime) {
|
|
58
|
-
options.runtime.signal.throwIfAborted();
|
|
59
|
-
activeRuntimes.add(options.runtime);
|
|
60
|
-
return options.runtime;
|
|
61
|
-
}
|
|
62
|
-
if (legacyRuntime.signal.aborted)
|
|
63
|
-
legacyRuntime = createOAuthRuntime();
|
|
64
|
-
activeRuntimes.add(legacyRuntime);
|
|
65
|
-
return legacyRuntime;
|
|
66
|
-
}
|
|
67
|
-
function getRuntimeState(runtime) {
|
|
68
|
-
const state = runtimeStates.get(runtime);
|
|
69
|
-
if (!state)
|
|
70
|
-
throw new Error("Unknown OAuth runtime");
|
|
71
|
-
return state;
|
|
72
|
-
}
|
|
73
|
-
function getPendingAuthKey(serverName, options) {
|
|
74
|
-
return JSON.stringify([serverName, getAuthBaseDir(options)]);
|
|
75
|
-
}
|
|
76
39
|
export function isOAuthChallenge(error) {
|
|
77
40
|
return error instanceof InsufficientScopeError || error instanceof UnauthorizedError
|
|
78
41
|
|| (error instanceof SdkHttpError && error.status === 401);
|
|
@@ -105,13 +68,6 @@ export function bindOAuthRequestIssuer(serverName, serverUrl, storageBase, issue
|
|
|
105
68
|
}
|
|
106
69
|
request.issuer = issuer;
|
|
107
70
|
}
|
|
108
|
-
export function hasPendingAuth(serverName, options, runtime) {
|
|
109
|
-
const state = getRuntimeState(runtime ?? legacyRuntime);
|
|
110
|
-
if (options) {
|
|
111
|
-
return state.pendingAuths.has(getPendingAuthKey(serverName, options));
|
|
112
|
-
}
|
|
113
|
-
return Array.from(state.pendingAuths.values()).some(pendingAuth => pendingAuth.serverName === serverName);
|
|
114
|
-
}
|
|
115
71
|
/** Timeout for manual auth completion (5 minutes) */
|
|
116
72
|
const MANUAL_AUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
117
73
|
/**
|
|
@@ -446,31 +402,6 @@ async function setPendingAuth(runtime, serverName, pendingAuth, oauthState, sign
|
|
|
446
402
|
cleanupTimer.unref?.();
|
|
447
403
|
state.pendingAuthCleanupTimers.set(key, cleanupTimer);
|
|
448
404
|
}
|
|
449
|
-
async function clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions = {}) {
|
|
450
|
-
const state = getRuntimeState(runtime);
|
|
451
|
-
const key = getPendingAuthKey(serverName, fallbackStorageOptions);
|
|
452
|
-
const pendingAuth = state.pendingAuths.get(key);
|
|
453
|
-
const authStorageOptions = pendingAuth?.authStorageOptions ?? fallbackStorageOptions;
|
|
454
|
-
const pendingState = state.pendingAuthStates.get(key);
|
|
455
|
-
if (oauthState && pendingState && pendingState !== oauthState)
|
|
456
|
-
return;
|
|
457
|
-
const timer = state.pendingAuthCleanupTimers.get(key);
|
|
458
|
-
if (timer) {
|
|
459
|
-
clearTimeout(timer);
|
|
460
|
-
state.pendingAuthCleanupTimers.delete(key);
|
|
461
|
-
}
|
|
462
|
-
pendingAuth?.authProvider.deactivate();
|
|
463
|
-
state.pendingAuths.delete(key);
|
|
464
|
-
state.pendingAuthStates.delete(key);
|
|
465
|
-
const stateToRelease = pendingState ?? oauthState;
|
|
466
|
-
if (stateToRelease) {
|
|
467
|
-
cancelPendingCallback(stateToRelease);
|
|
468
|
-
const storedState = await getOAuthState(serverName, authStorageOptions);
|
|
469
|
-
if (storedState === stateToRelease) {
|
|
470
|
-
await clearOAuthState(serverName, authStorageOptions);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
405
|
function getSearchParamsFromInput(input) {
|
|
475
406
|
try {
|
|
476
407
|
const url = new URL(input);
|
|
@@ -727,69 +658,3 @@ export async function removeAuth(serverName, options = {}) {
|
|
|
727
658
|
throwIfAborted(signal);
|
|
728
659
|
console.log(`MCP Auth: Removed credentials for ${serverName}`);
|
|
729
660
|
}
|
|
730
|
-
/**
|
|
731
|
-
* Check if OAuth is supported for a server configuration.
|
|
732
|
-
* OAuth is supported for HTTP servers unless explicitly disabled.
|
|
733
|
-
*
|
|
734
|
-
* @param definition - The server definition
|
|
735
|
-
* @returns True if OAuth is supported
|
|
736
|
-
*/
|
|
737
|
-
export function supportsOAuth(definition) {
|
|
738
|
-
// OAuth requires a URL
|
|
739
|
-
if (!definition.url)
|
|
740
|
-
return false;
|
|
741
|
-
// Explicitly disabled via auth: false or oauth: false
|
|
742
|
-
if (definition.auth === false)
|
|
743
|
-
return false;
|
|
744
|
-
if (definition.oauth === false)
|
|
745
|
-
return false;
|
|
746
|
-
if (definition.auth === "oauth")
|
|
747
|
-
return true;
|
|
748
|
-
// Configured custom headers take precedence over implicit OAuth auto-detection.
|
|
749
|
-
if (definition.headers && Object.keys(definition.headers).length > 0)
|
|
750
|
-
return false;
|
|
751
|
-
// OAuth is enabled when auth is not specified (auto-detect)
|
|
752
|
-
return definition.auth === undefined;
|
|
753
|
-
}
|
|
754
|
-
/**
|
|
755
|
-
* Initialize the OAuth system on startup.
|
|
756
|
-
* OAuth callback binding is lazy and starts from startAuth() only.
|
|
757
|
-
*/
|
|
758
|
-
export async function initializeOAuth(runtimeOrSignal) {
|
|
759
|
-
if (runtimeOrSignal && "signal" in runtimeOrSignal) {
|
|
760
|
-
runtimeOrSignal.signal.throwIfAborted();
|
|
761
|
-
activeRuntimes.add(runtimeOrSignal);
|
|
762
|
-
return runtimeOrSignal;
|
|
763
|
-
}
|
|
764
|
-
await shutdownOAuth(legacyRuntime);
|
|
765
|
-
legacyRuntime = createOAuthRuntime(runtimeOrSignal);
|
|
766
|
-
return legacyRuntime;
|
|
767
|
-
}
|
|
768
|
-
/**
|
|
769
|
-
* Shutdown one OAuth runtime. The callback server remains process-shared while
|
|
770
|
-
* another runtime has pending/reserved callback state or is still active.
|
|
771
|
-
*/
|
|
772
|
-
export async function shutdownOAuth(runtime = legacyRuntime) {
|
|
773
|
-
const state = getRuntimeState(runtime);
|
|
774
|
-
if (state.controller.signal.aborted)
|
|
775
|
-
return;
|
|
776
|
-
state.generation += 1;
|
|
777
|
-
state.controller.abort(new Error("OAuth runtime stopped"));
|
|
778
|
-
for (const callbackState of Array.from(state.pendingAuthStates.values()))
|
|
779
|
-
cancelPendingCallback(callbackState);
|
|
780
|
-
const results = await Promise.allSettled(Array.from(state.pendingAuths.values(), pendingAuth => clearPendingAuth(runtime, pendingAuth.serverName, undefined, pendingAuth.authStorageOptions)));
|
|
781
|
-
const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
|
|
782
|
-
state.pendingAuthentications.clear();
|
|
783
|
-
state.requests.clear();
|
|
784
|
-
activeRuntimes.delete(runtime);
|
|
785
|
-
if (activeRuntimes.size === 0) {
|
|
786
|
-
try {
|
|
787
|
-
await stopCallbackServer();
|
|
788
|
-
}
|
|
789
|
-
catch (error) {
|
|
790
|
-
failures.push(error);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
if (failures.length)
|
|
794
|
-
throw new AggregateError(failures, "MCP OAuth cleanup failed");
|
|
795
|
-
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Uses Node.js http module for compatibility.
|
|
6
6
|
*/
|
|
7
7
|
import { createServer } from "http";
|
|
8
|
-
import { DEFAULT_OAUTH_CALLBACK_PATH, getConfiguredOAuthCallbackPort, getOAuthCallbackPath, getOAuthCallbackPort, setOAuthCallbackPath, setOAuthCallbackPort, } from "./mcp-oauth-
|
|
8
|
+
import { DEFAULT_OAUTH_CALLBACK_PATH, getConfiguredOAuthCallbackPort, getOAuthCallbackPath, getOAuthCallbackPort, setOAuthCallbackPath, setOAuthCallbackPort, } from "./mcp-oauth-callback-config.js";
|
|
9
9
|
// HTML templates for callback responses
|
|
10
10
|
const HTML_SUCCESS = `<!DOCTYPE html>
|
|
11
11
|
<html>
|
package/dist/mcp-code.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { McpExtensionState } from "./state.ts";
|
|
3
3
|
import { type ContentBlock, type McpOperationContext } from "./types.ts";
|
|
4
|
-
export declare const DEFAULT_MCP_SCRIPT_TIMEOUT_MS = 30000;
|
|
5
4
|
export declare const runMcpScript: typeof runMcpScriptOperation;
|
|
6
5
|
declare function runMcpScriptOperation(state: McpExtensionState, code: string, timeoutMs?: number | null, getPiTools?: () => ToolInfo[], signal?: AbortSignal, toolCallId?: string, beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>): Promise<{
|
|
7
6
|
content: ContentBlock[];
|
package/dist/mcp-code.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { formatWithOptions } from "node:util";
|
|
2
2
|
import { Worker } from "node:worker_threads";
|
|
3
|
-
import {
|
|
3
|
+
import { guardedMcpDetails, resolveMcpOutputGuardOptions, readMcpResult, formatMcpResultReference, formatMcpPayloadFile } from "./mcp-output-guard.js";
|
|
4
|
+
import { createMcpOutputSpool } from "./mcp-output-preview.js";
|
|
4
5
|
import { executeCall, executeConnect } from "./proxy-modes.js";
|
|
5
6
|
import { executeResourceList, executeResourceRead } from "./resource-tools.js";
|
|
6
7
|
import { combineAbortSignals } from "./runtime-owner.js";
|
|
7
8
|
import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.js";
|
|
8
9
|
import { catalogCoverage, toToolDescriptor } from "./tool-metadata.js";
|
|
9
10
|
import { isServerDisabled } from "./types.js";
|
|
10
|
-
|
|
11
|
+
import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS } from "./utils.js";
|
|
12
|
+
/** UTF-16 units of emitted output, plus 256 per emit, the worker may queue before it waits for the host. */
|
|
13
|
+
const SCRIPT_OUTPUT_WINDOW = 4 * 1024 * 1024;
|
|
11
14
|
class McpScriptTimeoutError extends Error {
|
|
12
15
|
constructor(timeoutMs) {
|
|
13
16
|
super(`mcp_script timed out after ${timeoutMs}ms`);
|
|
@@ -71,8 +74,9 @@ function parseWorkerMessage(value) {
|
|
|
71
74
|
if (typeof value !== "object" || value === null)
|
|
72
75
|
return null;
|
|
73
76
|
const message = value;
|
|
77
|
+
// An older worker file, such as one restored by a rollback under a running Pi, neither charges the window nor sends a size.
|
|
74
78
|
if (message.type === "emit" && "block" in message)
|
|
75
|
-
return { type: "emit", block: message.block };
|
|
79
|
+
return { type: "emit", block: message.block, size: typeof message.size === "number" ? message.size : 0 };
|
|
76
80
|
if (message.type === "call" && typeof message.id === "number" && typeof message.path === "string") {
|
|
77
81
|
return { type: "call", id: message.id, path: message.path, ...(message.args !== undefined ? { args: message.args } : {}), ...(typeof message.server === "string" ? { server: message.server } : {}) };
|
|
78
82
|
}
|
|
@@ -110,7 +114,7 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
|
|
|
110
114
|
const resolvedTimeoutMs = timeoutMs === null ? null : Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
111
115
|
? Math.floor(timeoutMs)
|
|
112
116
|
: DEFAULT_MCP_SCRIPT_TIMEOUT_MS;
|
|
113
|
-
const output =
|
|
117
|
+
const output = createMcpOutputSpool(resolveMcpOutputGuardOptions(state.config.settings, state.outputDirectory));
|
|
114
118
|
const externalSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
115
119
|
const timeoutController = new AbortController();
|
|
116
120
|
const callSignal = combineAbortSignals(externalSignal, timeoutController.signal);
|
|
@@ -273,8 +277,9 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
|
|
|
273
277
|
if (externalSignal?.aborted) {
|
|
274
278
|
throw abortReasonError(externalSignal.reason);
|
|
275
279
|
}
|
|
280
|
+
const outputFlow = new Int32Array(new SharedArrayBuffer(4));
|
|
276
281
|
worker = new Worker(new URL("./mcp-script-worker.mjs", import.meta.url), {
|
|
277
|
-
workerData: { code },
|
|
282
|
+
workerData: { code, outputFlow, outputWindow: SCRIPT_OUTPUT_WINDOW },
|
|
278
283
|
env: {},
|
|
279
284
|
});
|
|
280
285
|
const activeWorker = worker;
|
|
@@ -286,6 +291,9 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
|
|
|
286
291
|
return;
|
|
287
292
|
if (message.type === "emit") {
|
|
288
293
|
output.push(toContentBlock(message.block));
|
|
294
|
+
const queued = Atomics.sub(outputFlow, 0, message.size) - message.size;
|
|
295
|
+
if (queued <= SCRIPT_OUTPUT_WINDOW / 2)
|
|
296
|
+
Atomics.notify(outputFlow, 0);
|
|
289
297
|
return;
|
|
290
298
|
}
|
|
291
299
|
if (message.type === "done") {
|
|
@@ -395,8 +403,13 @@ async function runMcpScriptOperation(state, code, timeoutMs = DEFAULT_MCP_SCRIPT
|
|
|
395
403
|
await worker?.terminate();
|
|
396
404
|
}
|
|
397
405
|
const callsSnapshot = snapshotCalls();
|
|
398
|
-
|
|
399
|
-
|
|
406
|
+
if (output.empty)
|
|
407
|
+
output.push({ type: "text", text: "(no output)" });
|
|
408
|
+
for (const text of payloadNotices)
|
|
409
|
+
output.push({ type: "text", text });
|
|
410
|
+
for (const ref of resultRefs)
|
|
411
|
+
output.push({ type: "text", text: formatMcpResultReference(ref) });
|
|
412
|
+
const guarded = output.finish();
|
|
400
413
|
return {
|
|
401
414
|
content: guarded.content,
|
|
402
415
|
details: {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** SDK-free process-shared OAuth callback endpoint configuration. */
|
|
2
|
+
export declare const DEFAULT_OAUTH_CALLBACK_PORT = 19876;
|
|
3
|
+
export declare const DEFAULT_OAUTH_CALLBACK_PATH = "/callback";
|
|
4
|
+
export declare function getConfiguredOAuthCallbackPort(): number;
|
|
5
|
+
export declare function getOAuthCallbackPort(): number;
|
|
6
|
+
export declare function setOAuthCallbackPort(port: number): void;
|
|
7
|
+
export declare function getOAuthCallbackPath(): string;
|
|
8
|
+
export declare function setOAuthCallbackPath(path: string): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** SDK-free process-shared OAuth callback endpoint configuration. */
|
|
2
|
+
// Callback server configuration
|
|
3
|
+
export const DEFAULT_OAUTH_CALLBACK_PORT = 19876;
|
|
4
|
+
export const DEFAULT_OAUTH_CALLBACK_PATH = "/callback";
|
|
5
|
+
let configuredOAuthCallbackPort = DEFAULT_OAUTH_CALLBACK_PORT;
|
|
6
|
+
if (process.env.MCP_OAUTH_CALLBACK_PORT) {
|
|
7
|
+
const parsedPort = Number.parseInt(process.env.MCP_OAUTH_CALLBACK_PORT, 10);
|
|
8
|
+
if (Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535) {
|
|
9
|
+
configuredOAuthCallbackPort = parsedPort;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
let oauthCallbackPort = configuredOAuthCallbackPort;
|
|
13
|
+
let oauthCallbackPath = DEFAULT_OAUTH_CALLBACK_PATH;
|
|
14
|
+
export function getConfiguredOAuthCallbackPort() {
|
|
15
|
+
return configuredOAuthCallbackPort;
|
|
16
|
+
}
|
|
17
|
+
export function getOAuthCallbackPort() {
|
|
18
|
+
return oauthCallbackPort;
|
|
19
|
+
}
|
|
20
|
+
export function setOAuthCallbackPort(port) {
|
|
21
|
+
oauthCallbackPort = port;
|
|
22
|
+
}
|
|
23
|
+
export function getOAuthCallbackPath() {
|
|
24
|
+
return oauthCallbackPath;
|
|
25
|
+
}
|
|
26
|
+
export function setOAuthCallbackPath(path) {
|
|
27
|
+
oauthCallbackPath = path.startsWith("/") ? path : `/${path}`;
|
|
28
|
+
}
|
|
@@ -13,13 +13,6 @@ export declare function validateOAuthClientMetadataUrl(value: string | false | u
|
|
|
13
13
|
/** Only the port may differ; keep raw host, path and query spelling exact. */
|
|
14
14
|
export declare function loopbackRedirectsMatch(first: string, second: unknown): boolean;
|
|
15
15
|
export declare function issuersMatch(first: string, second: string): boolean;
|
|
16
|
-
declare const DEFAULT_OAUTH_CALLBACK_PORT = 19876;
|
|
17
|
-
declare const DEFAULT_OAUTH_CALLBACK_PATH = "/callback";
|
|
18
|
-
export declare function getConfiguredOAuthCallbackPort(): number;
|
|
19
|
-
export declare function getOAuthCallbackPort(): number;
|
|
20
|
-
export declare function setOAuthCallbackPort(port: number): void;
|
|
21
|
-
export declare function getOAuthCallbackPath(): string;
|
|
22
|
-
export declare function setOAuthCallbackPath(path: string): void;
|
|
23
16
|
/** Configuration options for OAuth */
|
|
24
17
|
export interface McpOAuthConfig {
|
|
25
18
|
skipIssuerMetadataValidation?: boolean;
|
|
@@ -151,4 +144,4 @@ export declare class McpOAuthProvider implements OAuthClientProvider {
|
|
|
151
144
|
private prepareCrossAppRequest;
|
|
152
145
|
prepareTokenRequest(scope?: string): URLSearchParams | Promise<URLSearchParams> | undefined;
|
|
153
146
|
}
|
|
154
|
-
export { DEFAULT_OAUTH_CALLBACK_PORT, DEFAULT_OAUTH_CALLBACK_PATH };
|
|
147
|
+
export { DEFAULT_OAUTH_CALLBACK_PORT, DEFAULT_OAUTH_CALLBACK_PATH, getConfiguredOAuthCallbackPort, getOAuthCallbackPath, getOAuthCallbackPort, setOAuthCallbackPath, setOAuthCallbackPort, } from "./mcp-oauth-callback-config.ts";
|
|
@@ -10,6 +10,7 @@ import { getAuthForUrl, updateTokens, updateClientInfo, clearAllCredentials, cle
|
|
|
10
10
|
import { resolveCommandSecret } from "./utils.js";
|
|
11
11
|
import { isNonInteractiveOAuth } from "./types.js";
|
|
12
12
|
import { combineAbortSignals } from "./runtime-owner.js";
|
|
13
|
+
import { getOAuthCallbackPath, getOAuthCallbackPort } from "./mcp-oauth-callback-config.js";
|
|
13
14
|
import sharedClientMetadata from "./docs/client-metadata.json" with { type: "json" };
|
|
14
15
|
/** Validate explicit document URLs without echoing possible credentials in errors. */
|
|
15
16
|
export function validateOAuthClientMetadataUrl(value) {
|
|
@@ -50,33 +51,6 @@ export function issuersMatch(first, second) {
|
|
|
50
51
|
|| (first.endsWith("/") && first.slice(0, -1) === second)
|
|
51
52
|
|| (second.endsWith("/") && second.slice(0, -1) === first);
|
|
52
53
|
}
|
|
53
|
-
// Callback server configuration
|
|
54
|
-
const DEFAULT_OAUTH_CALLBACK_PORT = 19876;
|
|
55
|
-
const DEFAULT_OAUTH_CALLBACK_PATH = "/callback";
|
|
56
|
-
let configuredOAuthCallbackPort = DEFAULT_OAUTH_CALLBACK_PORT;
|
|
57
|
-
if (process.env.MCP_OAUTH_CALLBACK_PORT) {
|
|
58
|
-
const parsedPort = Number.parseInt(process.env.MCP_OAUTH_CALLBACK_PORT, 10);
|
|
59
|
-
if (Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535) {
|
|
60
|
-
configuredOAuthCallbackPort = parsedPort;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
let oauthCallbackPort = configuredOAuthCallbackPort;
|
|
64
|
-
let oauthCallbackPath = DEFAULT_OAUTH_CALLBACK_PATH;
|
|
65
|
-
export function getConfiguredOAuthCallbackPort() {
|
|
66
|
-
return configuredOAuthCallbackPort;
|
|
67
|
-
}
|
|
68
|
-
export function getOAuthCallbackPort() {
|
|
69
|
-
return oauthCallbackPort;
|
|
70
|
-
}
|
|
71
|
-
export function setOAuthCallbackPort(port) {
|
|
72
|
-
oauthCallbackPort = port;
|
|
73
|
-
}
|
|
74
|
-
export function getOAuthCallbackPath() {
|
|
75
|
-
return oauthCallbackPath;
|
|
76
|
-
}
|
|
77
|
-
export function setOAuthCallbackPath(path) {
|
|
78
|
-
oauthCallbackPath = path.startsWith("/") ? path : `/${path}`;
|
|
79
|
-
}
|
|
80
54
|
function isRecord(value) {
|
|
81
55
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
82
56
|
}
|
|
@@ -669,4 +643,4 @@ export class McpOAuthProvider {
|
|
|
669
643
|
return params;
|
|
670
644
|
}
|
|
671
645
|
}
|
|
672
|
-
export { DEFAULT_OAUTH_CALLBACK_PORT, DEFAULT_OAUTH_CALLBACK_PATH };
|
|
646
|
+
export { DEFAULT_OAUTH_CALLBACK_PORT, DEFAULT_OAUTH_CALLBACK_PATH, getConfiguredOAuthCallbackPort, getOAuthCallbackPath, getOAuthCallbackPort, setOAuthCallbackPath, setOAuthCallbackPort, } from "./mcp-oauth-callback-config.js";
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK-free OAuth runtime ownership and pending-flow cleanup.
|
|
3
|
+
* Native auth/protocol implementation stays in mcp-auth-flow.ts and is loaded
|
|
4
|
+
* only by operations that need it; all callers share the state owned here.
|
|
5
|
+
*/
|
|
6
|
+
import type { McpOAuthProvider } from "./mcp-oauth-provider.ts";
|
|
7
|
+
import { type AuthStorageOptions } from "./mcp-auth.ts";
|
|
8
|
+
import type { ServerEntry } from "./types.ts";
|
|
9
|
+
/** Auth status for a server */
|
|
10
|
+
export type AuthStatus = "authenticated" | "expired" | "not_authenticated";
|
|
11
|
+
export interface McpOAuthRuntime {
|
|
12
|
+
readonly signal: AbortSignal;
|
|
13
|
+
}
|
|
14
|
+
export interface AuthenticateOptions {
|
|
15
|
+
onAuthorizationUrl?: (authorizationUrl: string) => void | Promise<void>;
|
|
16
|
+
authStorageOptions?: AuthStorageOptions;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
runtime?: McpOAuthRuntime;
|
|
19
|
+
}
|
|
20
|
+
export type AuthDiscovery = {
|
|
21
|
+
resourceMetadataUrl?: URL;
|
|
22
|
+
scope?: string;
|
|
23
|
+
skipIssuerMetadataValidation?: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type CallbackIssuer = {
|
|
26
|
+
expectedIssuer?: string | undefined;
|
|
27
|
+
fallbackIssuer?: string;
|
|
28
|
+
issParameterSupported?: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type PendingAuth = {
|
|
31
|
+
serverName: string;
|
|
32
|
+
authProvider: McpOAuthProvider;
|
|
33
|
+
serverUrl: string;
|
|
34
|
+
authorizationUrl: string;
|
|
35
|
+
discovery: AuthDiscovery;
|
|
36
|
+
callbackIssuer: CallbackIssuer;
|
|
37
|
+
request?: OAuthRequest;
|
|
38
|
+
authStorageOptions: AuthStorageOptions;
|
|
39
|
+
};
|
|
40
|
+
export type RuntimeState = {
|
|
41
|
+
controller: AbortController;
|
|
42
|
+
generation: number;
|
|
43
|
+
pendingAuths: Map<string, PendingAuth>;
|
|
44
|
+
pendingAuthStates: Map<string, string>;
|
|
45
|
+
pendingAuthCleanupTimers: Map<string, ReturnType<typeof setTimeout>>;
|
|
46
|
+
pendingAuthentications: Map<string, Promise<AuthStatus>>;
|
|
47
|
+
requests: Map<string, OAuthRequest>;
|
|
48
|
+
};
|
|
49
|
+
export declare function createOAuthRuntime(signal?: AbortSignal): McpOAuthRuntime;
|
|
50
|
+
export declare let legacyRuntime: McpOAuthRuntime;
|
|
51
|
+
export declare function getRuntime(options?: AuthenticateOptions): McpOAuthRuntime;
|
|
52
|
+
export declare function getRuntimeState(runtime: McpOAuthRuntime): RuntimeState;
|
|
53
|
+
export declare function getPendingAuthKey(serverName: string, options: AuthStorageOptions): string;
|
|
54
|
+
export type OAuthRequest = {
|
|
55
|
+
serverName: string;
|
|
56
|
+
serverUrl: string;
|
|
57
|
+
requestedScope?: string;
|
|
58
|
+
challenge?: {
|
|
59
|
+
requiredScope?: string;
|
|
60
|
+
resourceMetadataUrl?: URL;
|
|
61
|
+
};
|
|
62
|
+
issuer?: string;
|
|
63
|
+
};
|
|
64
|
+
export declare function hasPendingAuth(serverName: string, options?: AuthStorageOptions, runtime?: McpOAuthRuntime): boolean;
|
|
65
|
+
export declare function clearPendingAuth(runtime: McpOAuthRuntime, serverName: string, oauthState?: string, fallbackStorageOptions?: AuthStorageOptions): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Check if OAuth is supported for a server configuration.
|
|
68
|
+
* OAuth is supported for HTTP servers unless explicitly disabled.
|
|
69
|
+
*
|
|
70
|
+
* @param definition - The server definition
|
|
71
|
+
* @returns True if OAuth is supported
|
|
72
|
+
*/
|
|
73
|
+
export declare function supportsOAuth(definition: ServerEntry): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Initialize the OAuth system on startup.
|
|
76
|
+
* OAuth callback binding is lazy and starts from startAuth() only.
|
|
77
|
+
*/
|
|
78
|
+
export declare function initializeOAuth(runtimeOrSignal?: McpOAuthRuntime | AbortSignal): Promise<McpOAuthRuntime>;
|
|
79
|
+
/**
|
|
80
|
+
* Shutdown one OAuth runtime. The callback server remains process-shared while
|
|
81
|
+
* another runtime has pending/reserved callback state or is still active.
|
|
82
|
+
*/
|
|
83
|
+
export declare function shutdownOAuth(runtime?: McpOAuthRuntime): Promise<void>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK-free OAuth runtime ownership and pending-flow cleanup.
|
|
3
|
+
* Native auth/protocol implementation stays in mcp-auth-flow.ts and is loaded
|
|
4
|
+
* only by operations that need it; all callers share the state owned here.
|
|
5
|
+
*/
|
|
6
|
+
import { cancelPendingCallback, stopCallbackServer } from "./mcp-callback-server.js";
|
|
7
|
+
import { getAuthBaseDir, getOAuthState, clearOAuthState } from "./mcp-auth.js";
|
|
8
|
+
import { combineAbortSignals } from "./runtime-owner.js";
|
|
9
|
+
const runtimeStates = new WeakMap();
|
|
10
|
+
const activeRuntimes = new Set();
|
|
11
|
+
export function createOAuthRuntime(signal) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const runtime = { signal: combineAbortSignals(signal, controller.signal) };
|
|
14
|
+
runtimeStates.set(runtime, {
|
|
15
|
+
controller,
|
|
16
|
+
generation: 0,
|
|
17
|
+
pendingAuths: new Map(),
|
|
18
|
+
pendingAuthStates: new Map(),
|
|
19
|
+
pendingAuthCleanupTimers: new Map(),
|
|
20
|
+
pendingAuthentications: new Map(),
|
|
21
|
+
requests: new Map(),
|
|
22
|
+
});
|
|
23
|
+
activeRuntimes.add(runtime);
|
|
24
|
+
return runtime;
|
|
25
|
+
}
|
|
26
|
+
export let legacyRuntime = createOAuthRuntime();
|
|
27
|
+
activeRuntimes.delete(legacyRuntime);
|
|
28
|
+
export function getRuntime(options) {
|
|
29
|
+
if (options?.runtime) {
|
|
30
|
+
options.runtime.signal.throwIfAborted();
|
|
31
|
+
activeRuntimes.add(options.runtime);
|
|
32
|
+
return options.runtime;
|
|
33
|
+
}
|
|
34
|
+
if (legacyRuntime.signal.aborted)
|
|
35
|
+
legacyRuntime = createOAuthRuntime();
|
|
36
|
+
activeRuntimes.add(legacyRuntime);
|
|
37
|
+
return legacyRuntime;
|
|
38
|
+
}
|
|
39
|
+
export function getRuntimeState(runtime) {
|
|
40
|
+
const state = runtimeStates.get(runtime);
|
|
41
|
+
if (!state)
|
|
42
|
+
throw new Error("Unknown OAuth runtime");
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
export function getPendingAuthKey(serverName, options) {
|
|
46
|
+
return JSON.stringify([serverName, getAuthBaseDir(options)]);
|
|
47
|
+
}
|
|
48
|
+
export function hasPendingAuth(serverName, options, runtime) {
|
|
49
|
+
const state = getRuntimeState(runtime ?? legacyRuntime);
|
|
50
|
+
if (options) {
|
|
51
|
+
return state.pendingAuths.has(getPendingAuthKey(serverName, options));
|
|
52
|
+
}
|
|
53
|
+
return Array.from(state.pendingAuths.values()).some(pendingAuth => pendingAuth.serverName === serverName);
|
|
54
|
+
}
|
|
55
|
+
export async function clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions = {}) {
|
|
56
|
+
const state = getRuntimeState(runtime);
|
|
57
|
+
const key = getPendingAuthKey(serverName, fallbackStorageOptions);
|
|
58
|
+
const pendingAuth = state.pendingAuths.get(key);
|
|
59
|
+
const authStorageOptions = pendingAuth?.authStorageOptions ?? fallbackStorageOptions;
|
|
60
|
+
const pendingState = state.pendingAuthStates.get(key);
|
|
61
|
+
if (oauthState && pendingState && pendingState !== oauthState)
|
|
62
|
+
return;
|
|
63
|
+
const timer = state.pendingAuthCleanupTimers.get(key);
|
|
64
|
+
if (timer) {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
state.pendingAuthCleanupTimers.delete(key);
|
|
67
|
+
}
|
|
68
|
+
pendingAuth?.authProvider.deactivate();
|
|
69
|
+
state.pendingAuths.delete(key);
|
|
70
|
+
state.pendingAuthStates.delete(key);
|
|
71
|
+
const stateToRelease = pendingState ?? oauthState;
|
|
72
|
+
if (stateToRelease) {
|
|
73
|
+
cancelPendingCallback(stateToRelease);
|
|
74
|
+
const storedState = await getOAuthState(serverName, authStorageOptions);
|
|
75
|
+
if (storedState === stateToRelease) {
|
|
76
|
+
await clearOAuthState(serverName, authStorageOptions);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Check if OAuth is supported for a server configuration.
|
|
82
|
+
* OAuth is supported for HTTP servers unless explicitly disabled.
|
|
83
|
+
*
|
|
84
|
+
* @param definition - The server definition
|
|
85
|
+
* @returns True if OAuth is supported
|
|
86
|
+
*/
|
|
87
|
+
export function supportsOAuth(definition) {
|
|
88
|
+
// OAuth requires a URL
|
|
89
|
+
if (!definition.url)
|
|
90
|
+
return false;
|
|
91
|
+
// Explicitly disabled via auth: false or oauth: false
|
|
92
|
+
if (definition.auth === false)
|
|
93
|
+
return false;
|
|
94
|
+
if (definition.oauth === false)
|
|
95
|
+
return false;
|
|
96
|
+
if (definition.auth === "oauth")
|
|
97
|
+
return true;
|
|
98
|
+
// Configured custom headers take precedence over implicit OAuth auto-detection.
|
|
99
|
+
if (definition.headers && Object.keys(definition.headers).length > 0)
|
|
100
|
+
return false;
|
|
101
|
+
// OAuth is enabled when auth is not specified (auto-detect)
|
|
102
|
+
return definition.auth === undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Initialize the OAuth system on startup.
|
|
106
|
+
* OAuth callback binding is lazy and starts from startAuth() only.
|
|
107
|
+
*/
|
|
108
|
+
export async function initializeOAuth(runtimeOrSignal) {
|
|
109
|
+
if (runtimeOrSignal && "signal" in runtimeOrSignal) {
|
|
110
|
+
runtimeOrSignal.signal.throwIfAborted();
|
|
111
|
+
activeRuntimes.add(runtimeOrSignal);
|
|
112
|
+
return runtimeOrSignal;
|
|
113
|
+
}
|
|
114
|
+
await shutdownOAuth(legacyRuntime);
|
|
115
|
+
legacyRuntime = createOAuthRuntime(runtimeOrSignal);
|
|
116
|
+
return legacyRuntime;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Shutdown one OAuth runtime. The callback server remains process-shared while
|
|
120
|
+
* another runtime has pending/reserved callback state or is still active.
|
|
121
|
+
*/
|
|
122
|
+
export async function shutdownOAuth(runtime = legacyRuntime) {
|
|
123
|
+
const state = getRuntimeState(runtime);
|
|
124
|
+
if (state.controller.signal.aborted)
|
|
125
|
+
return;
|
|
126
|
+
state.generation += 1;
|
|
127
|
+
state.controller.abort(new Error("OAuth runtime stopped"));
|
|
128
|
+
for (const callbackState of Array.from(state.pendingAuthStates.values()))
|
|
129
|
+
cancelPendingCallback(callbackState);
|
|
130
|
+
const results = await Promise.allSettled(Array.from(state.pendingAuths.values(), pendingAuth => clearPendingAuth(runtime, pendingAuth.serverName, undefined, pendingAuth.authStorageOptions)));
|
|
131
|
+
const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
|
|
132
|
+
state.pendingAuthentications.clear();
|
|
133
|
+
state.requests.clear();
|
|
134
|
+
activeRuntimes.delete(runtime);
|
|
135
|
+
if (activeRuntimes.size === 0) {
|
|
136
|
+
try {
|
|
137
|
+
await stopCallbackServer();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
failures.push(error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (failures.length)
|
|
144
|
+
throw new AggregateError(failures, "MCP OAuth cleanup failed");
|
|
145
|
+
}
|
|
@@ -61,7 +61,7 @@ export interface McpPayloadFile {
|
|
|
61
61
|
error?: string;
|
|
62
62
|
}
|
|
63
63
|
export type RetainedMcpResult = Pick<GuardedMcpOutput, "mcpResult" | "resultRef" | "resultWriteError" | "payloadFiles">;
|
|
64
|
-
export declare function resolveMcpOutputGuardOptions(settings?: McpSettings, outputDirectory?: string): Pick<McpOutputGuardOptions, "enabled" | "
|
|
64
|
+
export declare function resolveMcpOutputGuardOptions(settings?: McpSettings, outputDirectory?: string): Pick<McpOutputGuardOptions, "enabled" | "detailsMaxBytes" | "outputDirectory"> & Required<Pick<McpOutputGuardOptions, "maxBytes" | "maxLines">>;
|
|
65
65
|
/** Spread helper for tool-result details: includes mcpResult/outputGuard only when present. */
|
|
66
66
|
export declare function guardedMcpDetails(guarded: GuardedMcpOutput): Record<string, unknown>;
|
|
67
67
|
/**
|