@pharaoh-so/mcp 0.3.23 → 0.3.25
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/dist/auth.js +7 -0
- package/dist/backend.d.ts +3 -2
- package/dist/backend.js +9 -4
- package/dist/gateway.js +25 -6
- package/dist/proxy-server.js +9 -4
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -52,6 +52,13 @@ export async function pollOnce(baseUrl, deviceCode) {
|
|
|
52
52
|
const token = (await res.json());
|
|
53
53
|
return { status: "authorized", token };
|
|
54
54
|
}
|
|
55
|
+
// 5xx is an upstream/transport issue (CDN 503, Render cold-start 502,
|
|
56
|
+
// gateway 504 with an HTML error page). Treat as network_error so the
|
|
57
|
+
// gateway keeps the pending cycle alive and retries on the next tool call
|
|
58
|
+
// — a transient hiccup must not surface to the user as "auth denied".
|
|
59
|
+
if (res.status >= 500) {
|
|
60
|
+
return { status: "network_error", error: `HTTP ${res.status}` };
|
|
61
|
+
}
|
|
55
62
|
const body = (await res.json().catch(() => ({ error: "unknown" })));
|
|
56
63
|
if (body.error === "authorization_pending") {
|
|
57
64
|
return { status: "pending" };
|
package/dist/backend.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export declare class AuthRequiredBackend implements Backend {
|
|
|
51
51
|
listTools(): Promise<Tool[]>;
|
|
52
52
|
callTool(_name: string, _args: Record<string, unknown>): Promise<CallToolResult>;
|
|
53
53
|
}
|
|
54
|
+
/** Backoff schedule for reconnect attempts (milliseconds). Exported for tests. */
|
|
55
|
+
export declare const BACKOFF_MS: number[];
|
|
54
56
|
/**
|
|
55
57
|
* Real SSE-backed backend. Owns an MCP Client + SSEClientTransport and a
|
|
56
58
|
* reconnect loop. Forwards listTools/callTool to the remote server.
|
|
@@ -62,7 +64,7 @@ export declare class AuthRequiredBackend implements Backend {
|
|
|
62
64
|
* let this loop create a fresh transport for the next attempt. 5 retries
|
|
63
65
|
* (2s, 4s, 8s, 16s, 32s) before giving up.
|
|
64
66
|
*/
|
|
65
|
-
declare class SseProxyBackend implements Backend {
|
|
67
|
+
export declare class SseProxyBackend implements Backend {
|
|
66
68
|
private readonly sseUrl;
|
|
67
69
|
private readonly token;
|
|
68
70
|
readonly name: "sse";
|
|
@@ -93,4 +95,3 @@ declare class SseProxyBackend implements Backend {
|
|
|
93
95
|
* depend on its internal reconnect state.
|
|
94
96
|
*/
|
|
95
97
|
export declare function createSseBackend(sseUrl: string, token: string): SseProxyBackend;
|
|
96
|
-
export {};
|
package/dist/backend.js
CHANGED
|
@@ -81,8 +81,8 @@ export class AuthRequiredBackend {
|
|
|
81
81
|
return response;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
/** Backoff schedule for reconnect attempts (milliseconds). */
|
|
85
|
-
const BACKOFF_MS = [2000, 4000, 8000, 16000, 32000];
|
|
84
|
+
/** Backoff schedule for reconnect attempts (milliseconds). Exported for tests. */
|
|
85
|
+
export const BACKOFF_MS = [2000, 4000, 8000, 16000, 32000];
|
|
86
86
|
/**
|
|
87
87
|
* Build the SSEClientTransport with Bearer token auth. Injects the
|
|
88
88
|
* Authorization header into both the SSE connection (via custom fetch) and
|
|
@@ -117,7 +117,7 @@ function sleep(ms) {
|
|
|
117
117
|
* let this loop create a fresh transport for the next attempt. 5 retries
|
|
118
118
|
* (2s, 4s, 8s, 16s, 32s) before giving up.
|
|
119
119
|
*/
|
|
120
|
-
class SseProxyBackend {
|
|
120
|
+
export class SseProxyBackend {
|
|
121
121
|
sseUrl;
|
|
122
122
|
token;
|
|
123
123
|
name = "sse";
|
|
@@ -207,7 +207,12 @@ class SseProxyBackend {
|
|
|
207
207
|
if (err instanceof TokenExpiredError || err instanceof TenantSuspendedError) {
|
|
208
208
|
throw err;
|
|
209
209
|
}
|
|
210
|
-
attempt
|
|
210
|
+
// Do NOT increment attempt here — the try block already incremented
|
|
211
|
+
// it before calling reconnectAfter. A second bump would halve the
|
|
212
|
+
// effective retry budget so only ~3 of the 5 BACKOFF_MS slots
|
|
213
|
+
// would fire (skipping the 4s and 16s delays). The guard below
|
|
214
|
+
// catches the max-retries case where reconnectAfter's own throw
|
|
215
|
+
// arrives after we've already spent the budget in the try block.
|
|
211
216
|
if (attempt > BACKOFF_MS.length) {
|
|
212
217
|
throw err instanceof Error
|
|
213
218
|
? err
|
package/dist/gateway.js
CHANGED
|
@@ -17,17 +17,31 @@
|
|
|
17
17
|
import { openBrowser, pollOnce, requestDeviceCode, } from "./auth.js";
|
|
18
18
|
import { readCredentials, writeCredentials } from "./credentials.js";
|
|
19
19
|
import { resolveSseUrl, tokenToCredentials } from "./helpers.js";
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* True when the credentials' access token is at or past its expiry.
|
|
22
|
+
*
|
|
23
|
+
* Takes `now` as a parameter so boundary tests (Date.now() === expires_at
|
|
24
|
+
* exactly) work naturally with the gateway's injected clock — previously
|
|
25
|
+
* this used Date.now() directly, which couldn't be controlled from tests.
|
|
26
|
+
*/
|
|
27
|
+
function isAccessExpired(creds, now) {
|
|
21
28
|
const expiresAt = new Date(creds.expires_at).getTime();
|
|
22
29
|
if (Number.isNaN(expiresAt))
|
|
23
30
|
return true;
|
|
24
|
-
return
|
|
31
|
+
return now() >= expiresAt;
|
|
25
32
|
}
|
|
26
33
|
function buildAuthResponse(device) {
|
|
34
|
+
// Stryker disable next-line StringLiteral: user-facing UX copy; wording
|
|
35
|
+
// changes are deliberate and covered by manual review, not mutation testing.
|
|
27
36
|
const prefix = device.errorMessage ? `${device.errorMessage}\n\n` : "";
|
|
28
|
-
const text =
|
|
37
|
+
const text =
|
|
38
|
+
// Stryker disable next-line StringLiteral: user-facing UX copy
|
|
39
|
+
`${prefix}Pharaoh authentication required.\n\n` +
|
|
40
|
+
// Stryker disable next-line StringLiteral: user-facing UX copy
|
|
29
41
|
` 1. Visit ${device.verification_uri_complete}\n` +
|
|
42
|
+
// Stryker disable next-line StringLiteral: user-facing UX copy
|
|
30
43
|
` 2. Enter code: ${device.user_code}\n` +
|
|
44
|
+
// Stryker disable next-line StringLiteral: user-facing UX copy
|
|
31
45
|
` 3. Re-run this tool after authorizing.`;
|
|
32
46
|
return {
|
|
33
47
|
content: [{ type: "text", text }],
|
|
@@ -45,6 +59,12 @@ function buildAuthResponse(device) {
|
|
|
45
59
|
*/
|
|
46
60
|
export function createGateway(opts) {
|
|
47
61
|
const now = opts.now ?? (() => Date.now());
|
|
62
|
+
// Stryker disable next-line LogicalOperator: the `??` fallback is exercised
|
|
63
|
+
// at runtime when no opts.openBrowser is passed, but tests vi.mock auth.js's
|
|
64
|
+
// openBrowser to a noop, so a mutated operator can't be distinguished via
|
|
65
|
+
// observable behavior. vi.mock is the safety net; the fallback is defense
|
|
66
|
+
// in depth for production. See packages/mcp-proxy/tests/*.test.ts for the
|
|
67
|
+
// module-level mock that keeps real browser-open calls out of CI.
|
|
48
68
|
const openBrowserImpl = opts.openBrowser ?? openBrowser;
|
|
49
69
|
let pending = null;
|
|
50
70
|
let lastCreds = null;
|
|
@@ -91,8 +111,6 @@ export function createGateway(opts) {
|
|
|
91
111
|
}
|
|
92
112
|
/** Decide whether the cached response is still valid without polling. */
|
|
93
113
|
function shouldSkipPoll(state) {
|
|
94
|
-
if (state.last_poll_at === null)
|
|
95
|
-
return false;
|
|
96
114
|
return now() - state.last_poll_at < state.interval_ms;
|
|
97
115
|
}
|
|
98
116
|
/**
|
|
@@ -110,6 +128,7 @@ export function createGateway(opts) {
|
|
|
110
128
|
// inline so the agent sees why; the user will naturally retry.
|
|
111
129
|
pending = null;
|
|
112
130
|
return {
|
|
131
|
+
// Stryker disable next-line StringLiteral: user-facing UX copy
|
|
113
132
|
content: [{ type: "text", text: `Pharaoh auth denied: ${result.error}` }],
|
|
114
133
|
isError: true,
|
|
115
134
|
_meta: {
|
|
@@ -153,7 +172,7 @@ export function createGateway(opts) {
|
|
|
153
172
|
if (!creds)
|
|
154
173
|
return false;
|
|
155
174
|
lastCreds = creds;
|
|
156
|
-
return !isAccessExpired(creds);
|
|
175
|
+
return !isAccessExpired(creds, now);
|
|
157
176
|
}
|
|
158
177
|
function getCredentials() {
|
|
159
178
|
return lastCreds;
|
package/dist/proxy-server.js
CHANGED
|
@@ -87,12 +87,17 @@ export class ProxyServer {
|
|
|
87
87
|
*/
|
|
88
88
|
async setBackend(next) {
|
|
89
89
|
const prev = this.backend;
|
|
90
|
-
|
|
90
|
+
// Start the new backend BEFORE swapping the reference. If start() rejects
|
|
91
|
+
// (e.g. SSE connect fails), `prev` stays active — otherwise the proxy
|
|
92
|
+
// would be wedged with a not-yet-started backend that throws on every
|
|
93
|
+
// list/call until the caller tried again. This ordering also closes the
|
|
94
|
+
// race window where an in-flight request could arrive between the
|
|
95
|
+
// reference swap and a slow SSE handshake.
|
|
91
96
|
if (next.start)
|
|
92
97
|
await next.start();
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
// new catalog.
|
|
98
|
+
this.backend = next;
|
|
99
|
+
// Fire the notification after the swap so a client that races a list
|
|
100
|
+
// request against it sees the new catalog.
|
|
96
101
|
await this.server.sendToolListChanged();
|
|
97
102
|
// Fire-and-forget stop of the old backend. Errors here don't affect
|
|
98
103
|
// the running session — the swap has already happened.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pharaoh-so/mcp",
|
|
3
3
|
"mcpName": "so.pharaoh/pharaoh",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.25",
|
|
5
5
|
"description": "MCP proxy for Pharaoh — maps codebases into queryable knowledge graphs for AI agents. Enables Claude Code in headless environments (VPS, SSH, CI) via device flow auth.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|