@fitchmultz/pi-mcp-adapter 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +750 -0
- package/LICENSE +21 -0
- package/OAUTH.md +462 -0
- package/README.md +820 -0
- package/abort.ts +34 -0
- package/agent-dir.ts +24 -0
- package/app-bridge.bundle.js +67 -0
- package/banner.png +0 -0
- package/checkpoint.ts +55 -0
- package/cli.js +222 -0
- package/commands.ts +619 -0
- package/config.ts +1204 -0
- package/consent-manager.ts +25 -0
- package/direct-tools.ts +408 -0
- package/dist/abort.d.ts +2 -0
- package/dist/abort.js +35 -0
- package/dist/agent-dir.d.ts +3 -0
- package/dist/agent-dir.js +21 -0
- package/dist/app-bridge.bundle.js +67 -0
- package/dist/checkpoint.d.ts +18 -0
- package/dist/checkpoint.js +53 -0
- package/dist/commands.d.ts +20 -0
- package/dist/commands.js +521 -0
- package/dist/config.d.ts +109 -0
- package/dist/config.js +1013 -0
- package/dist/consent-manager.d.ts +8 -0
- package/dist/consent-manager.js +21 -0
- package/dist/direct-tools.d.ts +10 -0
- package/dist/direct-tools.js +350 -0
- package/dist/docs/client-metadata.json +14 -0
- package/dist/elicitation-handler.d.ts +17 -0
- package/dist/elicitation-handler.js +308 -0
- package/dist/error-signal.d.ts +15 -0
- package/dist/error-signal.js +21 -0
- package/dist/glimpse-ui.d.ts +9 -0
- package/dist/glimpse-ui.js +75 -0
- package/dist/host-html-template.d.ts +15 -0
- package/dist/host-html-template.js +429 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +840 -0
- package/dist/init.d.ts +25 -0
- package/dist/init.js +562 -0
- package/dist/legacy-migration.d.ts +18 -0
- package/dist/legacy-migration.js +45 -0
- package/dist/lifecycle.d.ts +40 -0
- package/dist/lifecycle.js +167 -0
- package/dist/logger.d.ts +38 -0
- package/dist/logger.js +102 -0
- package/dist/mcp-auth-flow.d.ts +113 -0
- package/dist/mcp-auth-flow.js +834 -0
- package/dist/mcp-auth.d.ts +127 -0
- package/dist/mcp-auth.js +658 -0
- package/dist/mcp-callback-server.d.ts +56 -0
- package/dist/mcp-callback-server.js +371 -0
- package/dist/mcp-code.d.ts +51 -0
- package/dist/mcp-code.js +366 -0
- package/dist/mcp-keyring-helper.cjs +89 -0
- package/dist/mcp-oauth-provider.d.ts +154 -0
- package/dist/mcp-oauth-provider.js +672 -0
- package/dist/mcp-output-guard.d.ts +59 -0
- package/dist/mcp-output-guard.js +332 -0
- package/dist/mcp-panel.d.ts +66 -0
- package/dist/mcp-panel.js +889 -0
- package/dist/mcp-script-worker.mjs +128 -0
- package/dist/mcp-setup-panel.d.ts +84 -0
- package/dist/mcp-setup-panel.js +561 -0
- package/dist/mcp-status.d.ts +11 -0
- package/dist/mcp-status.js +96 -0
- package/dist/mcp-trace.d.ts +95 -0
- package/dist/mcp-trace.js +233 -0
- package/dist/metadata-cache.d.ts +18 -0
- package/dist/metadata-cache.js +277 -0
- package/dist/npx-resolver.d.ts +6 -0
- package/dist/npx-resolver.js +448 -0
- package/dist/onboarding-state.d.ts +11 -0
- package/dist/onboarding-state.js +47 -0
- package/dist/panel-keys.d.ts +16 -0
- package/dist/panel-keys.js +15 -0
- package/dist/prompts.d.ts +56 -0
- package/dist/prompts.js +284 -0
- package/dist/proxy-modes.d.ts +38 -0
- package/dist/proxy-modes.js +1235 -0
- package/dist/resource-tools.d.ts +1 -0
- package/dist/resource-tools.js +14 -0
- package/dist/runtime-owner.d.ts +18 -0
- package/dist/runtime-owner.js +117 -0
- package/dist/sampling-handler.d.ts +15 -0
- package/dist/sampling-handler.js +211 -0
- package/dist/search-ranking.d.ts +20 -0
- package/dist/search-ranking.js +180 -0
- package/dist/server-manager.d.ts +114 -0
- package/dist/server-manager.js +1175 -0
- package/dist/session-recovery.d.ts +31 -0
- package/dist/session-recovery.js +310 -0
- package/dist/state.d.ts +63 -0
- package/dist/state.js +1 -0
- package/dist/tool-approval.d.ts +10 -0
- package/dist/tool-approval.js +42 -0
- package/dist/tool-metadata.d.ts +9 -0
- package/dist/tool-metadata.js +193 -0
- package/dist/tool-registrar.d.ts +10 -0
- package/dist/tool-registrar.js +64 -0
- package/dist/tool-result-renderer.d.ts +45 -0
- package/dist/tool-result-renderer.js +224 -0
- package/dist/ts-shape.d.ts +2 -0
- package/dist/ts-shape.js +147 -0
- package/dist/types.d.ts +516 -0
- package/dist/types.js +168 -0
- package/dist/ui-resource-handler.d.ts +17 -0
- package/dist/ui-resource-handler.js +209 -0
- package/dist/ui-server.d.ts +51 -0
- package/dist/ui-server.js +669 -0
- package/dist/ui-session.d.ts +39 -0
- package/dist/ui-session.js +463 -0
- package/dist/ui-stream-types.d.ts +104 -0
- package/dist/ui-stream-types.js +58 -0
- package/dist/ui-tool-visibility.d.ts +4 -0
- package/dist/ui-tool-visibility.js +26 -0
- package/dist/unix-socket-transport.d.ts +15 -0
- package/dist/unix-socket-transport.js +84 -0
- package/dist/utils.d.ts +30 -0
- package/dist/utils.js +276 -0
- package/docs/client-metadata.json +14 -0
- package/elicitation-handler.ts +349 -0
- package/error-signal.ts +21 -0
- package/glimpse-ui.ts +80 -0
- package/host-html-template.ts +456 -0
- package/index.ts +900 -0
- package/init.ts +627 -0
- package/legacy-migration.ts +47 -0
- package/lifecycle.ts +165 -0
- package/logger.ts +127 -0
- package/mcp-auth-flow.ts +975 -0
- package/mcp-auth.ts +793 -0
- package/mcp-callback-server.ts +450 -0
- package/mcp-code.ts +386 -0
- package/mcp-keyring-helper.cjs +89 -0
- package/mcp-oauth-provider.ts +774 -0
- package/mcp-output-guard.ts +412 -0
- package/mcp-panel.ts +978 -0
- package/mcp-script-worker.mjs +128 -0
- package/mcp-setup-panel.ts +666 -0
- package/mcp-status.ts +109 -0
- package/mcp-trace.ts +295 -0
- package/metadata-cache.ts +333 -0
- package/npx-resolver.ts +488 -0
- package/onboarding-state.ts +58 -0
- package/package.json +182 -0
- package/panel-keys.ts +37 -0
- package/prompts.ts +337 -0
- package/proxy-modes.ts +1361 -0
- package/resource-tools.ts +17 -0
- package/runtime-owner.ts +123 -0
- package/sampling-handler.ts +273 -0
- package/scripts/build.mjs +131 -0
- package/scripts/prepare.mjs +102 -0
- package/search-ranking.ts +196 -0
- package/server-manager.ts +1320 -0
- package/session-recovery.ts +316 -0
- package/skills/mcp-scripting/SKILL.md +38 -0
- package/state.ts +64 -0
- package/tool-approval.ts +78 -0
- package/tool-metadata.ts +235 -0
- package/tool-registrar.ts +69 -0
- package/tool-result-renderer.ts +286 -0
- package/ts-shape.ts +145 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +16 -0
- package/types.ts +810 -0
- package/ui-resource-handler.ts +249 -0
- package/ui-server.ts +808 -0
- package/ui-session.ts +568 -0
- package/ui-stream-types.ts +89 -0
- package/ui-tool-visibility.ts +25 -0
- package/unix-socket-transport.ts +85 -0
- package/utils.ts +309 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP OAuth Callback Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP server that handles OAuth callbacks from the authorization server.
|
|
5
|
+
* Uses Node.js http module for compatibility.
|
|
6
|
+
*/
|
|
7
|
+
/** Result of a successful OAuth callback */
|
|
8
|
+
export interface OAuthCallbackResult {
|
|
9
|
+
code: string;
|
|
10
|
+
/** RFC 9207 `iss` authorization response parameter, when provided */
|
|
11
|
+
iss?: string;
|
|
12
|
+
}
|
|
13
|
+
export type ValidateOAuthCallback = (response: {
|
|
14
|
+
iss?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
}) => void;
|
|
17
|
+
type ReservedAuth = {
|
|
18
|
+
validate: ValidateOAuthCallback | undefined;
|
|
19
|
+
result?: OAuthCallbackResult | {
|
|
20
|
+
error: string;
|
|
21
|
+
iss?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
interface EnsureCallbackServerOptions {
|
|
25
|
+
strictPort?: boolean;
|
|
26
|
+
port?: number;
|
|
27
|
+
callbackHost?: string;
|
|
28
|
+
callbackPath?: string;
|
|
29
|
+
oauthState?: string;
|
|
30
|
+
reserveState?: boolean;
|
|
31
|
+
validate?: ValidateOAuthCallback;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Ensure the callback server is running.
|
|
35
|
+
* If strictPort is true, requires binding on the configured callback port.
|
|
36
|
+
* If strictPort is false, asks the OS for an available local port.
|
|
37
|
+
*/
|
|
38
|
+
export declare function ensureCallbackServer(options?: EnsureCallbackServerOptions): Promise<void>;
|
|
39
|
+
export declare function releaseCallbackServer(oauthState: string): void;
|
|
40
|
+
/** Consume a validated manual callback without releasing the flow's reservation. */
|
|
41
|
+
export declare function takeCallbackResult(oauthState: string): ReservedAuth["result"];
|
|
42
|
+
/**
|
|
43
|
+
* Wait for a callback with the given OAuth state.
|
|
44
|
+
* Returns a promise that resolves with the authorization code and, when the
|
|
45
|
+
* authorization server sends one, the RFC 9207 `iss` parameter.
|
|
46
|
+
*/
|
|
47
|
+
export declare function waitForCallback(oauthState: string): Promise<OAuthCallbackResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Cancel a pending authorization by state.
|
|
50
|
+
*/
|
|
51
|
+
export declare function cancelPendingCallback(oauthState: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Stop the callback server and reject all pending authorizations.
|
|
54
|
+
*/
|
|
55
|
+
export declare function stopCallbackServer(): Promise<void>;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP OAuth Callback Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP server that handles OAuth callbacks from the authorization server.
|
|
5
|
+
* Uses Node.js http module for compatibility.
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from "http";
|
|
8
|
+
import { DEFAULT_OAUTH_CALLBACK_PATH, getConfiguredOAuthCallbackPort, getOAuthCallbackPath, getOAuthCallbackPort, setOAuthCallbackPath, setOAuthCallbackPort, } from "./mcp-oauth-provider.js";
|
|
9
|
+
// HTML templates for callback responses
|
|
10
|
+
const HTML_SUCCESS = `<!DOCTYPE html>
|
|
11
|
+
<html>
|
|
12
|
+
<head>
|
|
13
|
+
<title>Pi - Authorization Successful</title>
|
|
14
|
+
<style>
|
|
15
|
+
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
|
|
16
|
+
.container { text-align: center; padding: 2rem; }
|
|
17
|
+
h1 { color: #4ade80; margin-bottom: 1rem; }
|
|
18
|
+
p { color: #aaa; }
|
|
19
|
+
</style>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<div class="container">
|
|
23
|
+
<h1>Authorization Successful</h1>
|
|
24
|
+
<p>You can close this window and return to Pi.</p>
|
|
25
|
+
</div>
|
|
26
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>`;
|
|
29
|
+
const HTML_MANUAL_SUCCESS = `<!DOCTYPE html>
|
|
30
|
+
<html>
|
|
31
|
+
<head>
|
|
32
|
+
<title>Pi - Authorization Received</title>
|
|
33
|
+
<style>
|
|
34
|
+
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
|
|
35
|
+
.container { text-align: center; padding: 2rem; }
|
|
36
|
+
h1 { color: #4ade80; margin-bottom: 1rem; }
|
|
37
|
+
p { color: #aaa; }
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div class="container">
|
|
42
|
+
<h1>Authorization Received</h1>
|
|
43
|
+
<p>Return to Pi and run auth-complete for this server. No callback URL or code is needed.</p>
|
|
44
|
+
</div>
|
|
45
|
+
</body>
|
|
46
|
+
</html>`;
|
|
47
|
+
function escapeHtml(value) {
|
|
48
|
+
return value
|
|
49
|
+
.replace(/&/g, "&")
|
|
50
|
+
.replace(/</g, "<")
|
|
51
|
+
.replace(/>/g, ">")
|
|
52
|
+
.replace(/"/g, """)
|
|
53
|
+
.replace(/'/g, "'");
|
|
54
|
+
}
|
|
55
|
+
const HTML_ERROR = (error) => `<!DOCTYPE html>
|
|
56
|
+
<html>
|
|
57
|
+
<head>
|
|
58
|
+
<title>Pi - Authorization Failed</title>
|
|
59
|
+
<style>
|
|
60
|
+
body { font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
|
|
61
|
+
.container { text-align: center; padding: 2rem; }
|
|
62
|
+
h1 { color: #f87171; margin-bottom: 1rem; }
|
|
63
|
+
p { color: #aaa; }
|
|
64
|
+
.error { color: #fca5a5; font-family: monospace; margin-top: 1rem; padding: 1rem; background: rgba(248,113,113,0.1); border-radius: 0.5rem; }
|
|
65
|
+
</style>
|
|
66
|
+
</head>
|
|
67
|
+
<body>
|
|
68
|
+
<div class="container">
|
|
69
|
+
<h1>Authorization Failed</h1>
|
|
70
|
+
<p>An error occurred during authorization.</p>
|
|
71
|
+
<div class="error">${escapeHtml(error)}</div>
|
|
72
|
+
</div>
|
|
73
|
+
</body>
|
|
74
|
+
</html>`;
|
|
75
|
+
/** Server singleton state */
|
|
76
|
+
let server;
|
|
77
|
+
let bindingPromise;
|
|
78
|
+
let stoppingPromise;
|
|
79
|
+
let callbackGeneration = 0;
|
|
80
|
+
const pendingAuths = new Map();
|
|
81
|
+
const reservedAuthStates = new Map();
|
|
82
|
+
/** Timeout for callback completion (5 minutes) */
|
|
83
|
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
84
|
+
const DEFAULT_OAUTH_CALLBACK_HOST = "localhost";
|
|
85
|
+
let callbackServerHost = DEFAULT_OAUTH_CALLBACK_HOST;
|
|
86
|
+
/**
|
|
87
|
+
* Handle incoming HTTP requests to the callback server.
|
|
88
|
+
*/
|
|
89
|
+
function handleRequest(req, res) {
|
|
90
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
91
|
+
// Only handle the callback path
|
|
92
|
+
if (url.pathname !== getOAuthCallbackPath()) {
|
|
93
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
94
|
+
res.end("Not found");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const code = url.searchParams.get("code");
|
|
98
|
+
const iss = url.searchParams.get("iss");
|
|
99
|
+
const state = url.searchParams.get("state");
|
|
100
|
+
const error = url.searchParams.get("error");
|
|
101
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
102
|
+
// Enforce state parameter presence for CSRF protection
|
|
103
|
+
if (!state) {
|
|
104
|
+
const errorMsg = "Missing required state parameter - potential CSRF attack";
|
|
105
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
106
|
+
res.end(HTML_ERROR(errorMsg));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const pending = pendingAuths.get(state);
|
|
110
|
+
const reserved = reservedAuthStates.get(state);
|
|
111
|
+
if (!pending && !reserved) {
|
|
112
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
113
|
+
res.end(HTML_ERROR("Invalid or expired state parameter - potential CSRF attack"));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const validate = pending?.validate ?? reserved?.validate;
|
|
118
|
+
if (validate)
|
|
119
|
+
validate({ ...(iss !== null ? { iss } : {}), ...(error ? { error } : {}) });
|
|
120
|
+
else if (error)
|
|
121
|
+
throw new Error("Cannot verify the OAuth error callback issuer");
|
|
122
|
+
}
|
|
123
|
+
catch (failure) {
|
|
124
|
+
const message = failure instanceof Error ? failure.message : "Invalid OAuth callback";
|
|
125
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
126
|
+
res.end(HTML_ERROR(message));
|
|
127
|
+
if (pending) {
|
|
128
|
+
clearTimeout(pending.timeout);
|
|
129
|
+
pendingAuths.delete(state);
|
|
130
|
+
reservedAuthStates.delete(state);
|
|
131
|
+
pending.reject(new Error(message));
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Provider error text is only trusted after state and issuer validation.
|
|
136
|
+
if (error) {
|
|
137
|
+
const errorMsg = errorDescription || error;
|
|
138
|
+
// Send HTTP response first before rejecting promise
|
|
139
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
140
|
+
res.end(HTML_ERROR(errorMsg));
|
|
141
|
+
// Reject promise after response is sent (defer to allow test to attach handler)
|
|
142
|
+
if (pending) {
|
|
143
|
+
reservedAuthStates.delete(state);
|
|
144
|
+
clearTimeout(pending.timeout);
|
|
145
|
+
pendingAuths.delete(state);
|
|
146
|
+
setTimeout(() => pending.reject(new Error(errorMsg)), 0);
|
|
147
|
+
}
|
|
148
|
+
else if (reserved) {
|
|
149
|
+
// Keep provider-controlled callback payloads out of tool output.
|
|
150
|
+
reserved.result = { error: "OAuth authorization was denied or failed. Finish authorization in the browser, then retry auth-complete.", ...(iss !== null ? { iss } : {}) };
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Require authorization code
|
|
155
|
+
if (!code) {
|
|
156
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
157
|
+
res.end(HTML_ERROR("No authorization code provided"));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!pending) {
|
|
161
|
+
if (reserved)
|
|
162
|
+
reserved.result = { code, ...(iss !== null ? { iss } : {}) };
|
|
163
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
164
|
+
res.end(HTML_MANUAL_SUCCESS);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Clear timeout and resolve the pending promise
|
|
168
|
+
clearTimeout(pending.timeout);
|
|
169
|
+
pendingAuths.delete(state);
|
|
170
|
+
pending.resolve({ code, ...(iss !== null ? { iss } : {}) });
|
|
171
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
172
|
+
res.end(HTML_SUCCESS);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Ensure the callback server is running.
|
|
176
|
+
* If strictPort is true, requires binding on the configured callback port.
|
|
177
|
+
* If strictPort is false, asks the OS for an available local port.
|
|
178
|
+
*/
|
|
179
|
+
export async function ensureCallbackServer(options = {}) {
|
|
180
|
+
if (stoppingPromise) {
|
|
181
|
+
throw new Error("OAuth callback server stopped");
|
|
182
|
+
}
|
|
183
|
+
const generation = callbackGeneration;
|
|
184
|
+
while (bindingPromise) {
|
|
185
|
+
await bindingPromise;
|
|
186
|
+
if (generation !== callbackGeneration) {
|
|
187
|
+
throw new Error("OAuth callback server stopped");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (generation !== callbackGeneration) {
|
|
191
|
+
throw new Error("OAuth callback server stopped");
|
|
192
|
+
}
|
|
193
|
+
const operation = ensureCallbackServerLocked(options);
|
|
194
|
+
bindingPromise = operation;
|
|
195
|
+
try {
|
|
196
|
+
await operation;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
if (bindingPromise === operation) {
|
|
200
|
+
bindingPromise = undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function ensureCallbackServerLocked(options = {}) {
|
|
205
|
+
const requiredPort = options.port ?? getConfiguredOAuthCallbackPort();
|
|
206
|
+
const strictPort = options.strictPort === true;
|
|
207
|
+
const requestedHost = options.callbackHost ?? DEFAULT_OAUTH_CALLBACK_HOST;
|
|
208
|
+
const rawRequestedPath = options.callbackPath ?? DEFAULT_OAUTH_CALLBACK_PATH;
|
|
209
|
+
const requestedPath = rawRequestedPath.startsWith("/") ? rawRequestedPath : `/${rawRequestedPath}`;
|
|
210
|
+
if (options.reserveState && !options.oauthState) {
|
|
211
|
+
throw new Error("OAuth callback reservation requires an oauthState");
|
|
212
|
+
}
|
|
213
|
+
let reservedState;
|
|
214
|
+
const previousServer = server;
|
|
215
|
+
const needsStrictRebind = Boolean(previousServer && strictPort && getOAuthCallbackPort() !== requiredPort);
|
|
216
|
+
const needsHostSwitch = Boolean(previousServer && callbackServerHost !== requestedHost);
|
|
217
|
+
const needsPathSwitch = Boolean(previousServer && getOAuthCallbackPath() !== requestedPath);
|
|
218
|
+
if (previousServer) {
|
|
219
|
+
if (!needsStrictRebind && !needsHostSwitch) {
|
|
220
|
+
if (needsPathSwitch) {
|
|
221
|
+
if (pendingAuths.size > 0 || reservedAuthStates.size > 0) {
|
|
222
|
+
throw new Error(`OAuth callback server is using path ${getOAuthCallbackPath()}, but callback path ${requestedPath} is required and cannot be switched while authorizations are pending`);
|
|
223
|
+
}
|
|
224
|
+
setOAuthCallbackPath(requestedPath);
|
|
225
|
+
}
|
|
226
|
+
if (options.reserveState && options.oauthState) {
|
|
227
|
+
reservedAuthStates.set(options.oauthState, { validate: options.validate });
|
|
228
|
+
reservedState = options.oauthState;
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (pendingAuths.size > 0 || reservedAuthStates.size > 0) {
|
|
233
|
+
throw new Error(`OAuth callback server is running on ${callbackServerHost}:${getOAuthCallbackPort()}, but strict callback endpoint ${requestedHost}:${requiredPort} is required and cannot be switched while authorizations are pending`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const candidateServer = createServer(handleRequest);
|
|
237
|
+
const listenPort = strictPort ? requiredPort : 0;
|
|
238
|
+
try {
|
|
239
|
+
await new Promise((resolve, reject) => {
|
|
240
|
+
candidateServer.once("error", (err) => {
|
|
241
|
+
reject(err);
|
|
242
|
+
});
|
|
243
|
+
candidateServer.listen(listenPort, requestedHost, () => {
|
|
244
|
+
resolve();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
if (strictPort) {
|
|
248
|
+
setOAuthCallbackPort(requiredPort);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const address = candidateServer.address();
|
|
252
|
+
if (!address || typeof address === "string" || typeof address.port !== "number") {
|
|
253
|
+
throw new Error("OAuth callback server did not report an assigned port");
|
|
254
|
+
}
|
|
255
|
+
setOAuthCallbackPort(address.port);
|
|
256
|
+
}
|
|
257
|
+
if (previousServer && (needsStrictRebind || needsHostSwitch)) {
|
|
258
|
+
await new Promise((resolve) => {
|
|
259
|
+
previousServer.close(() => resolve());
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
callbackServerHost = requestedHost;
|
|
263
|
+
setOAuthCallbackPath(requestedPath);
|
|
264
|
+
server = candidateServer;
|
|
265
|
+
if (options.reserveState && options.oauthState) {
|
|
266
|
+
reservedAuthStates.set(options.oauthState, { validate: options.validate });
|
|
267
|
+
reservedState = options.oauthState;
|
|
268
|
+
}
|
|
269
|
+
server.unref();
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (reservedState) {
|
|
273
|
+
reservedAuthStates.delete(reservedState);
|
|
274
|
+
}
|
|
275
|
+
const nodeError = error;
|
|
276
|
+
await new Promise((resolve) => {
|
|
277
|
+
candidateServer.close(() => resolve());
|
|
278
|
+
});
|
|
279
|
+
if (strictPort && nodeError.code === "EADDRINUSE") {
|
|
280
|
+
throw new Error(`OAuth callback port ${requiredPort} is already in use. Pre-registered OAuth clients require an exact redirect URI; set MCP_OAUTH_CALLBACK_PORT to your registered port or free port ${requiredPort}`, { cause: error });
|
|
281
|
+
}
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
export function releaseCallbackServer(oauthState) {
|
|
286
|
+
reservedAuthStates.delete(oauthState);
|
|
287
|
+
}
|
|
288
|
+
/** Consume a validated manual callback without releasing the flow's reservation. */
|
|
289
|
+
export function takeCallbackResult(oauthState) {
|
|
290
|
+
const reserved = reservedAuthStates.get(oauthState);
|
|
291
|
+
const result = reserved?.result;
|
|
292
|
+
if (reserved)
|
|
293
|
+
delete reserved.result;
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Wait for a callback with the given OAuth state.
|
|
298
|
+
* Returns a promise that resolves with the authorization code and, when the
|
|
299
|
+
* authorization server sends one, the RFC 9207 `iss` parameter.
|
|
300
|
+
*/
|
|
301
|
+
export function waitForCallback(oauthState) {
|
|
302
|
+
const reserved = reservedAuthStates.get(oauthState);
|
|
303
|
+
reservedAuthStates.delete(oauthState);
|
|
304
|
+
if (reserved?.result) {
|
|
305
|
+
return "error" in reserved.result
|
|
306
|
+
? Promise.reject(new Error(reserved.result.error))
|
|
307
|
+
: Promise.resolve(reserved.result);
|
|
308
|
+
}
|
|
309
|
+
const validate = reserved?.validate;
|
|
310
|
+
return new Promise((resolve, reject) => {
|
|
311
|
+
const timeout = setTimeout(() => {
|
|
312
|
+
if (pendingAuths.has(oauthState)) {
|
|
313
|
+
pendingAuths.delete(oauthState);
|
|
314
|
+
reject(new Error("OAuth callback timeout - authorization took too long"));
|
|
315
|
+
}
|
|
316
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
317
|
+
pendingAuths.set(oauthState, { resolve, reject, timeout, ...(validate ? { validate } : {}) });
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Cancel a pending authorization by state.
|
|
322
|
+
*/
|
|
323
|
+
export function cancelPendingCallback(oauthState) {
|
|
324
|
+
reservedAuthStates.delete(oauthState);
|
|
325
|
+
const pending = pendingAuths.get(oauthState);
|
|
326
|
+
if (pending) {
|
|
327
|
+
clearTimeout(pending.timeout);
|
|
328
|
+
pendingAuths.delete(oauthState);
|
|
329
|
+
pending.reject(new Error("Authorization cancelled"));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Stop the callback server and reject all pending authorizations.
|
|
334
|
+
*/
|
|
335
|
+
export function stopCallbackServer() {
|
|
336
|
+
if (stoppingPromise)
|
|
337
|
+
return stoppingPromise;
|
|
338
|
+
callbackGeneration += 1;
|
|
339
|
+
const cleanup = (async () => {
|
|
340
|
+
while (bindingPromise) {
|
|
341
|
+
await bindingPromise.catch(() => { });
|
|
342
|
+
}
|
|
343
|
+
if (server) {
|
|
344
|
+
await new Promise((resolve) => {
|
|
345
|
+
server.close(() => {
|
|
346
|
+
resolve();
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
server = undefined;
|
|
350
|
+
}
|
|
351
|
+
setOAuthCallbackPort(getConfiguredOAuthCallbackPort());
|
|
352
|
+
callbackServerHost = DEFAULT_OAUTH_CALLBACK_HOST;
|
|
353
|
+
setOAuthCallbackPath(DEFAULT_OAUTH_CALLBACK_PATH);
|
|
354
|
+
// Reject all pending auths (defer to allow any pending operations to complete)
|
|
355
|
+
const pendingList = Array.from(pendingAuths.entries());
|
|
356
|
+
pendingAuths.clear();
|
|
357
|
+
reservedAuthStates.clear();
|
|
358
|
+
setTimeout(() => {
|
|
359
|
+
for (const [, pending] of pendingList) {
|
|
360
|
+
clearTimeout(pending.timeout);
|
|
361
|
+
pending.reject(new Error("OAuth callback server stopped"));
|
|
362
|
+
}
|
|
363
|
+
}, 0);
|
|
364
|
+
})();
|
|
365
|
+
const operation = cleanup.finally(() => {
|
|
366
|
+
if (stoppingPromise === operation)
|
|
367
|
+
stoppingPromise = undefined;
|
|
368
|
+
});
|
|
369
|
+
stoppingPromise = operation;
|
|
370
|
+
return operation;
|
|
371
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.ts";
|
|
3
|
+
import type { ContentBlock, McpOperationContext } from "./types.ts";
|
|
4
|
+
export declare const DEFAULT_MCP_SCRIPT_TIMEOUT_MS = 30000;
|
|
5
|
+
export declare const runMcpScript: typeof runMcpScriptOperation;
|
|
6
|
+
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
|
+
content: ContentBlock[];
|
|
8
|
+
details: {
|
|
9
|
+
calls?: ({
|
|
10
|
+
operation: "call";
|
|
11
|
+
path: string;
|
|
12
|
+
ok: true;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
} | {
|
|
15
|
+
operation: "call";
|
|
16
|
+
path: string;
|
|
17
|
+
ok: false;
|
|
18
|
+
error: string;
|
|
19
|
+
durationMs: number;
|
|
20
|
+
recovery?: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
operation: "search";
|
|
23
|
+
query: string;
|
|
24
|
+
ok: true;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
} | {
|
|
27
|
+
operation: "search";
|
|
28
|
+
query: string;
|
|
29
|
+
ok: false;
|
|
30
|
+
error: string;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
} | {
|
|
33
|
+
operation: "describe";
|
|
34
|
+
path: string;
|
|
35
|
+
ok: true;
|
|
36
|
+
durationMs: number;
|
|
37
|
+
} | {
|
|
38
|
+
operation: "describe";
|
|
39
|
+
path: string;
|
|
40
|
+
ok: false;
|
|
41
|
+
error: string;
|
|
42
|
+
durationMs: number;
|
|
43
|
+
})[];
|
|
44
|
+
recovery?: {} | null;
|
|
45
|
+
timeoutMs: number | null;
|
|
46
|
+
error?: "aborted" | "timeout" | "call_capture_failed" | "script_error";
|
|
47
|
+
message?: string | undefined;
|
|
48
|
+
mode: string;
|
|
49
|
+
};
|
|
50
|
+
}>;
|
|
51
|
+
export {};
|