@pharaoh-so/mcp 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -1
- package/dist/auth.js +0 -1
- package/dist/credentials.js +0 -1
- package/dist/helpers.d.ts +36 -0
- package/dist/helpers.js +124 -0
- package/dist/index.js +60 -139
- package/dist/proxy.d.ts +4 -0
- package/dist/proxy.js +14 -18
- package/package.json +31 -27
- package/dist/auth.js.map +0 -1
- package/dist/credentials.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/proxy.js.map +0 -1
- package/src/auth.ts +0 -143
- package/src/credentials.ts +0 -92
- package/src/index.ts +0 -203
- package/src/proxy.ts +0 -168
- package/tsconfig.json +0 -18
package/README.md
CHANGED
|
@@ -4,11 +4,28 @@ Stdio-to-SSE proxy for [Pharaoh](https://pharaoh.so) — enables Claude Code in
|
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
|
+
**Step 1 — Authenticate** (run in your terminal):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @pharaoh-so/mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This shows a device code and URL. Open the URL on any device (phone, laptop) to authorize. Credentials are saved to `~/.pharaoh/credentials.json` (valid for 7 days).
|
|
14
|
+
|
|
15
|
+
**Step 2 — Add to Claude Code:**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
claude mcp add pharaoh -- npx @pharaoh-so/mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If you previously added pharaoh as SSE, remove it first:
|
|
22
|
+
|
|
7
23
|
```bash
|
|
24
|
+
claude mcp remove pharaoh
|
|
8
25
|
claude mcp add pharaoh -- npx @pharaoh-so/mcp
|
|
9
26
|
```
|
|
10
27
|
|
|
11
|
-
|
|
28
|
+
Verify with `claude mcp list` — it should show `pharaoh` as a **stdio** server, not SSE.
|
|
12
29
|
|
|
13
30
|
## How It Works
|
|
14
31
|
|
package/dist/auth.js
CHANGED
package/dist/credentials.js
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper functions for the mcp-proxy CLI — no side effects on import.
|
|
3
|
+
* Separated from index.ts so tests can import without triggering main().
|
|
4
|
+
*/
|
|
5
|
+
import type { TokenResponse } from "./auth.js";
|
|
6
|
+
import type { Credentials } from "./credentials.js";
|
|
7
|
+
/** Write one or more lines to stderr. */
|
|
8
|
+
export declare function printLines(...lines: string[]): void;
|
|
9
|
+
/** Parse CLI arguments. */
|
|
10
|
+
export declare function parseArgs(argv?: string[]): {
|
|
11
|
+
server: string;
|
|
12
|
+
logout: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function printUsage(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Validate that a server-supplied SSE URL shares the same origin as the configured server.
|
|
17
|
+
* Prevents a compromised auth response from redirecting the Bearer token to an attacker's host.
|
|
18
|
+
* Falls back to `${server}/sse` if the URL is missing, malformed, or cross-origin.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveSseUrl(tokenSseUrl: string | undefined, server: string): string;
|
|
21
|
+
/** Convert a token response to storable credentials. */
|
|
22
|
+
export declare function tokenToCredentials(token: TokenResponse, sseUrl: string): Credentials;
|
|
23
|
+
/** Format remaining TTL as human-readable string (e.g. "5d 12h"). */
|
|
24
|
+
export declare function formatTtl(expiresAt: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Print setup instructions for Claude Code. Called in interactive mode
|
|
27
|
+
* after auth completes (or when credentials already exist).
|
|
28
|
+
*/
|
|
29
|
+
export declare function printSetupInstructions(): void;
|
|
30
|
+
/** Format a credential identity string (e.g. "alice (my-org)"). */
|
|
31
|
+
export declare function formatIdentity(creds: Credentials): string;
|
|
32
|
+
/**
|
|
33
|
+
* Determine if credentials are valid for proxy use.
|
|
34
|
+
* Returns a diagnostic message if not, null if valid.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateCredentials(creds: Credentials | null): string | null;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { isExpired } from "./credentials.js";
|
|
2
|
+
const DEFAULT_SERVER = "https://mcp.pharaoh.so";
|
|
3
|
+
/** Write one or more lines to stderr. */
|
|
4
|
+
export function printLines(...lines) {
|
|
5
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
6
|
+
}
|
|
7
|
+
/** Parse CLI arguments. */
|
|
8
|
+
export function parseArgs(argv = process.argv.slice(2)) {
|
|
9
|
+
let server = DEFAULT_SERVER;
|
|
10
|
+
let logout = false;
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
if (argv[i] === "--server" && argv[i + 1]) {
|
|
13
|
+
server = argv[i + 1];
|
|
14
|
+
i++;
|
|
15
|
+
}
|
|
16
|
+
else if (argv[i] === "--logout") {
|
|
17
|
+
logout = true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Strip trailing slash
|
|
21
|
+
server = server.replace(/\/+$/, "");
|
|
22
|
+
// Reject non-HTTPS servers (allow loopback addresses for local dev)
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(server);
|
|
25
|
+
const isLoopback = parsed.hostname === "localhost" ||
|
|
26
|
+
parsed.hostname === "127.0.0.1" ||
|
|
27
|
+
parsed.hostname === "[::1]";
|
|
28
|
+
if (parsed.protocol !== "https:" && !isLoopback) {
|
|
29
|
+
printLines(`Pharaoh: --server must use HTTPS (got ${parsed.protocol}//…). Use https:// or http://localhost for local dev.`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (parsed.protocol !== "https:" && isLoopback) {
|
|
33
|
+
printLines("⚠ Warning: using insecure HTTP over loopback — do not use in production.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
printLines(`Pharaoh: --server is not a valid URL: ${server}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
return { server, logout };
|
|
41
|
+
}
|
|
42
|
+
export function printUsage() {
|
|
43
|
+
printLines("Usage: pharaoh-mcp [options]", "", "Options:", " --server <url> Pharaoh server URL (default: https://mcp.pharaoh.so)", " --logout Clear stored credentials and exit", " --help, -h Show this help", "", "Add to Claude Code:", " claude mcp add pharaoh -- npx @pharaoh-so/mcp", "");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate that a server-supplied SSE URL shares the same origin as the configured server.
|
|
47
|
+
* Prevents a compromised auth response from redirecting the Bearer token to an attacker's host.
|
|
48
|
+
* Falls back to `${server}/sse` if the URL is missing, malformed, or cross-origin.
|
|
49
|
+
*/
|
|
50
|
+
export function resolveSseUrl(tokenSseUrl, server) {
|
|
51
|
+
const fallback = `${server}/sse`;
|
|
52
|
+
if (!tokenSseUrl)
|
|
53
|
+
return fallback;
|
|
54
|
+
try {
|
|
55
|
+
const sseOrigin = new URL(tokenSseUrl).origin;
|
|
56
|
+
const serverOrigin = new URL(server).origin;
|
|
57
|
+
if (sseOrigin !== serverOrigin) {
|
|
58
|
+
printLines(`Pharaoh: ignoring cross-origin sse_url (${sseOrigin} ≠ ${serverOrigin})`);
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
return tokenSseUrl;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Convert a token response to storable credentials. */
|
|
68
|
+
export function tokenToCredentials(token, sseUrl) {
|
|
69
|
+
return {
|
|
70
|
+
version: 1,
|
|
71
|
+
access_token: token.access_token,
|
|
72
|
+
expires_at: new Date(Date.now() + token.expires_in * 1000).toISOString(),
|
|
73
|
+
expires_in: token.expires_in,
|
|
74
|
+
sse_url: sseUrl,
|
|
75
|
+
github_login: token.github_login ?? null,
|
|
76
|
+
tenant_name: token.tenant_name ?? null,
|
|
77
|
+
repos: token.repos ?? [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Format remaining TTL as human-readable string (e.g. "5d 12h"). */
|
|
81
|
+
export function formatTtl(expiresAt) {
|
|
82
|
+
const remainingMs = new Date(expiresAt).getTime() - Date.now();
|
|
83
|
+
if (remainingMs <= 0)
|
|
84
|
+
return "expired";
|
|
85
|
+
const hours = Math.floor(remainingMs / 3_600_000);
|
|
86
|
+
const days = Math.floor(hours / 24);
|
|
87
|
+
const remHours = hours % 24;
|
|
88
|
+
if (days > 0)
|
|
89
|
+
return `${days}d ${remHours}h`;
|
|
90
|
+
if (hours > 0)
|
|
91
|
+
return `${hours}h`;
|
|
92
|
+
return `${Math.floor(remainingMs / 60_000)}m`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Print setup instructions for Claude Code. Called in interactive mode
|
|
96
|
+
* after auth completes (or when credentials already exist).
|
|
97
|
+
*/
|
|
98
|
+
export function printSetupInstructions() {
|
|
99
|
+
printLines("", "┌───────────────────────────────────────────────────────┐", "│ Next step: add Pharaoh to Claude Code │", "│ │", "│ If pharaoh is already registered (e.g. as SSE): │", "│ claude mcp remove pharaoh │", "│ │", "│ Then add as stdio proxy: │", "│ claude mcp add pharaoh -- npx @pharaoh-so/mcp │", "│ │", "│ Verify with: │", "│ claude mcp list │", "└───────────────────────────────────────────────────────┘", "");
|
|
100
|
+
}
|
|
101
|
+
/** Format a credential identity string (e.g. "alice (my-org)"). */
|
|
102
|
+
export function formatIdentity(creds) {
|
|
103
|
+
return [
|
|
104
|
+
creds.github_login ?? "unknown",
|
|
105
|
+
creds.tenant_name ? `(${creds.tenant_name})` : null,
|
|
106
|
+
]
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join(" ");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Determine if credentials are valid for proxy use.
|
|
112
|
+
* Returns a diagnostic message if not, null if valid.
|
|
113
|
+
*/
|
|
114
|
+
export function validateCredentials(creds) {
|
|
115
|
+
if (!creds || isExpired(creds)) {
|
|
116
|
+
return [
|
|
117
|
+
"Pharaoh: no valid credentials — cannot start proxy.",
|
|
118
|
+
"Run this command first to authenticate:",
|
|
119
|
+
" npx @pharaoh-so/mcp",
|
|
120
|
+
"",
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,171 +5,92 @@
|
|
|
5
5
|
* Presents as an MCP server on stdio (for Claude Code) and relays messages
|
|
6
6
|
* to a remote Pharaoh SSE server. Authenticates via RFC 8628 device flow
|
|
7
7
|
* so the user can authorize on any device with a browser.
|
|
8
|
+
*
|
|
9
|
+
* Two modes determined by whether stdin is a TTY:
|
|
10
|
+
* - Interactive (TTY): authenticate, print setup instructions, exit.
|
|
11
|
+
* - Proxy (pipe): require pre-existing credentials, bridge stdio ↔ SSE.
|
|
8
12
|
*/
|
|
9
13
|
import { printActivationPrompt, printAuthSuccess, pollForToken, requestDeviceCode, } from "./auth.js";
|
|
10
|
-
import { deleteCredentials, isExpired, readCredentials, writeCredentials
|
|
14
|
+
import { deleteCredentials, isExpired, readCredentials, writeCredentials } from "./credentials.js";
|
|
15
|
+
import { formatIdentity, formatTtl, parseArgs, printLines, printSetupInstructions, printUsage, resolveSseUrl, tokenToCredentials, } from "./helpers.js";
|
|
11
16
|
import { TokenExpiredError, TenantSuspendedError, startProxy } from "./proxy.js";
|
|
12
|
-
|
|
13
|
-
/** Parse CLI arguments. */
|
|
14
|
-
function parseArgs() {
|
|
17
|
+
async function main() {
|
|
15
18
|
const args = process.argv.slice(2);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (args[i] === "--server" && args[i + 1]) {
|
|
20
|
-
server = args[i + 1];
|
|
21
|
-
i++;
|
|
22
|
-
}
|
|
23
|
-
else if (args[i] === "--logout") {
|
|
24
|
-
logout = true;
|
|
25
|
-
}
|
|
26
|
-
else if (args[i] === "--help" || args[i] === "-h") {
|
|
27
|
-
printUsage();
|
|
28
|
-
process.exit(0);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// Strip trailing slash
|
|
32
|
-
server = server.replace(/\/+$/, "");
|
|
33
|
-
return { server, logout };
|
|
34
|
-
}
|
|
35
|
-
function printUsage() {
|
|
36
|
-
process.stderr.write([
|
|
37
|
-
"Usage: pharaoh-mcp [options]",
|
|
38
|
-
"",
|
|
39
|
-
"Options:",
|
|
40
|
-
" --server <url> Pharaoh server URL (default: https://mcp.pharaoh.so)",
|
|
41
|
-
" --logout Clear stored credentials and exit",
|
|
42
|
-
" --help, -h Show this help",
|
|
43
|
-
"",
|
|
44
|
-
"Add to Claude Code:",
|
|
45
|
-
" claude mcp add pharaoh -- npx @pharaoh-so/mcp",
|
|
46
|
-
"",
|
|
47
|
-
].join("\n") + "\n");
|
|
48
|
-
}
|
|
49
|
-
/** Run the device flow and return a token response. */
|
|
50
|
-
async function authenticate(server) {
|
|
51
|
-
const deviceCode = await requestDeviceCode(server);
|
|
52
|
-
printActivationPrompt(deviceCode.user_code, deviceCode.verification_uri);
|
|
53
|
-
return pollForToken(server, deviceCode.device_code, deviceCode.interval);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Validate that a server-supplied SSE URL shares the same origin as the configured server.
|
|
57
|
-
* Prevents a compromised auth response from redirecting the Bearer token to an attacker's host.
|
|
58
|
-
* Falls back to `${server}/sse` if the URL is missing, malformed, or cross-origin.
|
|
59
|
-
*/
|
|
60
|
-
function resolveSseUrl(tokenSseUrl, server) {
|
|
61
|
-
const fallback = `${server}/sse`;
|
|
62
|
-
if (!tokenSseUrl)
|
|
63
|
-
return fallback;
|
|
64
|
-
try {
|
|
65
|
-
const sseOrigin = new URL(tokenSseUrl).origin;
|
|
66
|
-
const serverOrigin = new URL(server).origin;
|
|
67
|
-
if (sseOrigin !== serverOrigin) {
|
|
68
|
-
process.stderr.write(`Pharaoh: ignoring cross-origin sse_url (${sseOrigin} ≠ ${serverOrigin})\n`);
|
|
69
|
-
return fallback;
|
|
70
|
-
}
|
|
71
|
-
return tokenSseUrl;
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return fallback;
|
|
19
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
20
|
+
printUsage();
|
|
21
|
+
process.exit(0);
|
|
75
22
|
}
|
|
76
|
-
}
|
|
77
|
-
/** Convert a token response to storable credentials. */
|
|
78
|
-
function tokenToCredentials(token, sseUrl) {
|
|
79
|
-
return {
|
|
80
|
-
version: 1,
|
|
81
|
-
access_token: token.access_token,
|
|
82
|
-
expires_at: new Date(Date.now() + token.expires_in * 1000).toISOString(),
|
|
83
|
-
expires_in: token.expires_in,
|
|
84
|
-
sse_url: sseUrl,
|
|
85
|
-
github_login: token.github_login ?? null,
|
|
86
|
-
tenant_name: token.tenant_name ?? null,
|
|
87
|
-
repos: token.repos ?? [],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
/** Format remaining TTL as human-readable string (e.g. "5d 12h"). */
|
|
91
|
-
function formatTtl(expiresAt) {
|
|
92
|
-
const remainingMs = new Date(expiresAt).getTime() - Date.now();
|
|
93
|
-
if (remainingMs <= 0)
|
|
94
|
-
return "expired";
|
|
95
|
-
const hours = Math.floor(remainingMs / 3_600_000);
|
|
96
|
-
const days = Math.floor(hours / 24);
|
|
97
|
-
const remHours = hours % 24;
|
|
98
|
-
if (days > 0)
|
|
99
|
-
return `${days}d ${remHours}h`;
|
|
100
|
-
if (hours > 0)
|
|
101
|
-
return `${hours}h`;
|
|
102
|
-
return `${Math.floor(remainingMs / 60_000)}m`;
|
|
103
|
-
}
|
|
104
|
-
async function main() {
|
|
105
|
-
const { server, logout } = parseArgs();
|
|
106
|
-
// --logout: clear credentials and exit
|
|
23
|
+
const { server, logout } = parseArgs(args);
|
|
107
24
|
if (logout) {
|
|
108
25
|
deleteCredentials();
|
|
109
|
-
|
|
26
|
+
printLines("Pharaoh: credentials cleared");
|
|
110
27
|
process.exit(0);
|
|
111
28
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
process.stderr.write("Pharaoh: token rejected by server — re-authenticating\n");
|
|
123
|
-
deleteCredentials();
|
|
124
|
-
creds = null;
|
|
125
|
-
}
|
|
126
|
-
else if (err instanceof TenantSuspendedError) {
|
|
127
|
-
process.stderr.write(`Pharaoh: ${err.message}\n`);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
throw err;
|
|
132
|
-
}
|
|
29
|
+
const creds = readCredentials();
|
|
30
|
+
const isInteractive = Boolean(process.stdin.isTTY);
|
|
31
|
+
// ── Interactive mode (user running in a terminal) ──
|
|
32
|
+
// Authenticate if needed, print setup instructions, and exit.
|
|
33
|
+
// The proxy is useless without Claude Code on the other end of stdin.
|
|
34
|
+
if (isInteractive) {
|
|
35
|
+
if (creds && !isExpired(creds)) {
|
|
36
|
+
printLines(`Pharaoh: authenticated as ${formatIdentity(creds)} — token valid for ${formatTtl(creds.expires_at)}, ${creds.repos.length} repo${creds.repos.length === 1 ? "" : "s"} connected`);
|
|
37
|
+
printSetupInstructions();
|
|
38
|
+
process.exit(0);
|
|
133
39
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const token = await
|
|
40
|
+
// No valid credentials — run device flow
|
|
41
|
+
printLines("Pharaoh: no valid credentials — starting device authorization");
|
|
42
|
+
const deviceCode = await requestDeviceCode(server);
|
|
43
|
+
printActivationPrompt(deviceCode.user_code, deviceCode.verification_uri);
|
|
44
|
+
const token = await pollForToken(server, deviceCode.device_code, deviceCode.interval);
|
|
139
45
|
if (token.provisional) {
|
|
140
|
-
|
|
46
|
+
printLines(`Pharaoh: provisional access — install the GitHub App to map your repos: ${token.install_url ?? ""}`);
|
|
141
47
|
}
|
|
142
48
|
const sseUrl = resolveSseUrl(token.sse_url, server);
|
|
143
|
-
|
|
144
|
-
writeCredentials(
|
|
49
|
+
const newCreds = tokenToCredentials(token, sseUrl);
|
|
50
|
+
writeCredentials(newCreds);
|
|
145
51
|
printAuthSuccess(token.github_login ?? null, token.tenant_name ?? null, token.repos?.length ?? 0);
|
|
52
|
+
printSetupInstructions();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
// ── Proxy mode (Claude Code spawned us as a stdio MCP server) ──
|
|
56
|
+
// If no credentials, we can't run the device flow (no TTY for user interaction).
|
|
57
|
+
if (!creds || isExpired(creds)) {
|
|
58
|
+
printLines("Pharaoh: no valid credentials — cannot start proxy.", "Run this command first to authenticate:", " npx @pharaoh-so/mcp", "");
|
|
59
|
+
process.exit(1);
|
|
146
60
|
}
|
|
147
|
-
//
|
|
61
|
+
// Valid credentials — start the proxy
|
|
62
|
+
printLines(`Pharaoh: token valid for ${formatTtl(creds.expires_at)} — connecting`);
|
|
148
63
|
try {
|
|
149
64
|
await startProxy(creds.sse_url, creds.access_token);
|
|
150
65
|
}
|
|
151
66
|
catch (err) {
|
|
152
67
|
if (err instanceof TokenExpiredError) {
|
|
153
|
-
|
|
154
|
-
process.stderr.write("Pharaoh: session expired — re-authenticating\n");
|
|
68
|
+
printLines("Pharaoh: token expired or revoked.", "Run this command to re-authenticate:", " npx @pharaoh-so/mcp", "");
|
|
155
69
|
deleteCredentials();
|
|
156
|
-
const token = await authenticate(server);
|
|
157
|
-
const sseUrl = resolveSseUrl(token.sse_url, server);
|
|
158
|
-
creds = tokenToCredentials(token, sseUrl);
|
|
159
|
-
writeCredentials(creds);
|
|
160
|
-
await startProxy(creds.sse_url, creds.access_token);
|
|
161
|
-
}
|
|
162
|
-
else if (err instanceof TenantSuspendedError) {
|
|
163
|
-
process.stderr.write(`Pharaoh: ${err.message}\n`);
|
|
164
70
|
process.exit(1);
|
|
165
71
|
}
|
|
166
|
-
|
|
167
|
-
|
|
72
|
+
if (err instanceof TenantSuspendedError) {
|
|
73
|
+
printLines(`Pharaoh: ${err.message}`);
|
|
74
|
+
process.exit(1);
|
|
168
75
|
}
|
|
76
|
+
throw err;
|
|
169
77
|
}
|
|
170
78
|
}
|
|
171
79
|
main().catch((err) => {
|
|
172
|
-
|
|
80
|
+
// Default: print only error name/code to avoid leaking tokens, internal URLs,
|
|
81
|
+
// or stack fragments that persist in CI logs. Full message behind PHARAOH_DEBUG.
|
|
82
|
+
if (process.env.PHARAOH_DEBUG === "1" && err instanceof Error) {
|
|
83
|
+
// Use main's comprehensive redaction patterns when showing the full message
|
|
84
|
+
const safeMsg = err.message
|
|
85
|
+
.replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]")
|
|
86
|
+
.replace(/(?:phat|phrt|ghp|gho|ghs|ghu)_\S+/gi, "[REDACTED_TOKEN]")
|
|
87
|
+
.replace(/[?&](?:code|token|key|secret|password|state)=[^&\s]+/gi, "?[REDACTED_PARAM]")
|
|
88
|
+
.replace(/\b[0-9a-f]{32,}\b/gi, "[REDACTED_HEX]");
|
|
89
|
+
printLines(`Pharaoh: fatal — ${safeMsg}`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const label = err instanceof Error ? err.name : "Error";
|
|
93
|
+
printLines(`Pharaoh: fatal — ${label}. Set PHARAOH_DEBUG=1 for details.`);
|
|
94
|
+
}
|
|
173
95
|
process.exit(1);
|
|
174
96
|
});
|
|
175
|
-
//# sourceMappingURL=index.js.map
|
package/dist/proxy.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export declare function classifySSEError(err: Error): void;
|
|
|
18
18
|
* Claude Code → stdin → StdioServerTransport.onmessage → SSEClientTransport.send → POST /message
|
|
19
19
|
* Pharaoh → SSE stream → SSEClientTransport.onmessage → StdioServerTransport.send → stdout
|
|
20
20
|
*
|
|
21
|
+
* CRITICAL ordering: SSE must be connected BEFORE stdio starts reading stdin.
|
|
22
|
+
* Otherwise the MCP `initialize` message arrives before the remote transport is ready,
|
|
23
|
+
* gets permanently lost ("send error — Not connected"), and the client times out.
|
|
24
|
+
*
|
|
21
25
|
* On SSE disconnect, attempts reconnect with exponential backoff (5 retries, ~62s total).
|
|
22
26
|
* Throws TokenExpiredError on 401, TenantSuspendedError on 403.
|
|
23
27
|
*/
|
package/dist/proxy.js
CHANGED
|
@@ -67,6 +67,10 @@ function createSSETransport(sseUrl, token) {
|
|
|
67
67
|
* Claude Code → stdin → StdioServerTransport.onmessage → SSEClientTransport.send → POST /message
|
|
68
68
|
* Pharaoh → SSE stream → SSEClientTransport.onmessage → StdioServerTransport.send → stdout
|
|
69
69
|
*
|
|
70
|
+
* CRITICAL ordering: SSE must be connected BEFORE stdio starts reading stdin.
|
|
71
|
+
* Otherwise the MCP `initialize` message arrives before the remote transport is ready,
|
|
72
|
+
* gets permanently lost ("send error — Not connected"), and the client times out.
|
|
73
|
+
*
|
|
70
74
|
* On SSE disconnect, attempts reconnect with exponential backoff (5 retries, ~62s total).
|
|
71
75
|
* Throws TokenExpiredError on 401, TenantSuspendedError on 403.
|
|
72
76
|
*/
|
|
@@ -74,6 +78,7 @@ export async function startProxy(sseUrl, token) {
|
|
|
74
78
|
const stdio = new StdioServerTransport();
|
|
75
79
|
let sse = createSSETransport(sseUrl, token);
|
|
76
80
|
let reconnectAttempt = 0;
|
|
81
|
+
let stdioStarted = false;
|
|
77
82
|
/** Wire up bidirectional message relay between the two transports. */
|
|
78
83
|
function bridge(sseTransport) {
|
|
79
84
|
stdio.onmessage = (msg) => {
|
|
@@ -89,22 +94,7 @@ export async function startProxy(sseUrl, token) {
|
|
|
89
94
|
}
|
|
90
95
|
/** Handle SSE errors — delegates to classifySSEError for 401/403 detection. */
|
|
91
96
|
const handleSSEError = classifySSEError;
|
|
92
|
-
//
|
|
93
|
-
bridge(sse);
|
|
94
|
-
// Handle SSE connection drops with reconnect
|
|
95
|
-
sse.onerror = (err) => {
|
|
96
|
-
try {
|
|
97
|
-
handleSSEError(err);
|
|
98
|
-
}
|
|
99
|
-
catch (typed) {
|
|
100
|
-
// TokenExpiredError or TenantSuspendedError — propagate via close
|
|
101
|
-
sse.close().catch(() => { });
|
|
102
|
-
throw typed;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
// Start both transports
|
|
106
|
-
await stdio.start();
|
|
107
|
-
// Connect SSE with reconnect loop
|
|
97
|
+
// Connect SSE with reconnect loop — stdio starts AFTER first successful connection
|
|
108
98
|
await connectWithReconnect();
|
|
109
99
|
/** Attempt SSE connection with exponential backoff on failure. */
|
|
110
100
|
async function connectWithReconnect() {
|
|
@@ -115,10 +105,17 @@ export async function startProxy(sseUrl, token) {
|
|
|
115
105
|
process.stderr.write(`Pharaoh: reconnecting in ${delay / 1000}s (attempt ${reconnectAttempt}/${BACKOFF_MS.length})...\n`);
|
|
116
106
|
await sleep(delay);
|
|
117
107
|
sse = createSSETransport(sseUrl, token);
|
|
118
|
-
bridge(sse);
|
|
119
108
|
}
|
|
120
109
|
await sse.start();
|
|
121
110
|
reconnectAttempt = 0; // Reset on successful connection
|
|
111
|
+
// Bridge AFTER SSE is connected — messages can now be forwarded
|
|
112
|
+
bridge(sse);
|
|
113
|
+
// Start stdio AFTER first SSE connection — prevents initialize race
|
|
114
|
+
if (!stdioStarted) {
|
|
115
|
+
process.stderr.write("Pharaoh: connected\n");
|
|
116
|
+
await stdio.start();
|
|
117
|
+
stdioStarted = true;
|
|
118
|
+
}
|
|
122
119
|
// Wait for close — this promise resolves when SSE disconnects
|
|
123
120
|
await new Promise((resolve, reject) => {
|
|
124
121
|
sse.onclose = () => resolve();
|
|
@@ -154,4 +151,3 @@ export async function startProxy(sseUrl, token) {
|
|
|
154
151
|
function sleep(ms) {
|
|
155
152
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
156
153
|
}
|
|
157
|
-
//# sourceMappingURL=proxy.js.map
|
package/package.json
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
2
|
+
"name": "@pharaoh-so/mcp",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Stdio-to-SSE proxy for Pharaoh MCP — enables headless environments (VPS, SSH, CI) via device flow auth",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pharaoh-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.26.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@biomejs/biome": "^2.3.15",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vitest": "^4.0.18"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc && node -e \"const fs=require('fs'),f='dist/index.js',c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c);fs.chmodSync(f,0o755)\"",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"lint": "biome check src/"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/auth.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/credentials.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,wEAAwE;AACxE,MAAM,cAAc,GAAG,CAAC,CAAC;AAgBzB,oCAAoC;AACpC,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,kBAAkB,CAAC,CAAC;AAEzE;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAI,GAAG,gBAAgB;IACtD,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACxC,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7C,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAkB,EAAE,IAAI,GAAG,gBAAgB;IAC3E,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAClD,CAAC;IACD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACzE,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,iBAAiB,CAAC,IAAI,GAAG,gBAAgB;IACxD,IAAI,CAAC;QACJ,UAAU,CAAC,IAAI,CAAC,CAAC;IAClB,CAAC;IAAC,MAAM,CAAC;QACR,yCAAyC;IAC1C,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAkB;IAC3C,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC;IACvD,IAAI,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC;IAC3C,MAAM,QAAQ,GAAG,UAAU,GAAG,GAAG,CAAC;IAElC,OAAO,GAAG,IAAI,SAAS,GAAG,QAAQ,CAAC;AACpC,CAAC;AAED,+CAA+C;AAC/C,SAAS,kBAAkB,CAAC,KAAc;IACzC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,OAAO,CACN,GAAG,CAAC,OAAO,KAAK,cAAc;QAC9B,OAAO,GAAG,CAAC,YAAY,KAAK,QAAQ;QACpC,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ;QAClC,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ;QAClC,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ;QAC/B,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CACxB,CAAC;AACH,CAAC"}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;GAMG;AACH,OAAO,EACN,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,GAEjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAEN,iBAAiB,EACjB,SAAS,EACT,eAAe,EACf,gBAAgB,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEjF,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAEhD,2BAA2B;AAC3B,SAAS,SAAS;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,IAAI,MAAM,GAAG,cAAc,CAAC;IAC5B,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC3C,MAAM,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACrB,CAAC,EAAE,CAAC;QACL,CAAC;aAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,UAAU,EAAE,CAAC;YACnC,MAAM,GAAG,IAAI,CAAC;QACf,CAAC;aAAM,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YACrD,UAAU,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjB,CAAC;IACF,CAAC;IAED,uBAAuB;IACvB,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEpC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC;AAED,SAAS,UAAU;IAClB,OAAO,CAAC,MAAM,CAAC,KAAK,CACnB;QACC,8BAA8B;QAC9B,EAAE;QACF,UAAU;QACV,wEAAwE;QACxE,qDAAqD;QACrD,kCAAkC;QAClC,EAAE;QACF,qBAAqB;QACrB,iDAAiD;QACjD,EAAE;KACF,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CACnB,CAAC;AACH,CAAC;AAED,uDAAuD;AACvD,KAAK,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnD,qBAAqB,CAAC,UAAU,CAAC,SAAS,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC;IACzE,OAAO,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,WAAW,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;AAC1E,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,WAA+B,EAAE,MAAc;IACrE,MAAM,QAAQ,GAAG,GAAG,MAAM,MAAM,CAAC;IACjC,IAAI,CAAC,WAAW;QAAE,OAAO,QAAQ,CAAC;IAClC,IAAI,CAAC;QACJ,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;QAC9C,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;QAC5C,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;YAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CACnB,2CAA2C,SAAS,MAAM,YAAY,KAAK,CAC3E,CAAC;YACF,OAAO,QAAQ,CAAC;QACjB,CAAC;QACD,OAAO,WAAW,CAAC;IACpB,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,QAAQ,CAAC;IACjB,CAAC;AACF,CAAC;AAED,wDAAwD;AACxD,SAAS,kBAAkB,CAAC,KAAoB,EAAE,MAAc;IAC/D,OAAO;QACN,OAAO,EAAE,CAAC;QACV,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,UAAU,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;QACxE,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,OAAO,EAAE,MAAM;QACf,YAAY,EAAE,KAAK,CAAC,YAAY,IAAI,IAAI;QACxC,WAAW,EAAE,KAAK,CAAC,WAAW,IAAI,IAAI;QACtC,KAAK,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE;KACxB,CAAC;AACH,CAAC;AAED,qEAAqE;AACrE,SAAS,SAAS,CAAC,SAAiB;IACnC,MAAM,WAAW,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC/D,IAAI,WAAW,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACvC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG,KAAK,GAAG,EAAE,CAAC;IAC5B,IAAI,IAAI,GAAG,CAAC;QAAE,OAAO,GAAG,IAAI,KAAK,QAAQ,GAAG,CAAC;IAC7C,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,GAAG,KAAK,GAAG,CAAC;IAClC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC;AAC/C,CAAC;AAED,KAAK,UAAU,IAAI;IAClB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;IAEvC,uCAAuC;IACvC,IAAI,MAAM,EAAE,CAAC;QACZ,iBAAiB,EAAE,CAAC;QACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IAED,IAAI,KAAK,GAAG,eAAe,EAAE,CAAC;IAE9B,wDAAwD;IACxD,IAAI,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4BAA4B,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC;QAC/F,IAAI,CAAC;YACJ,MAAM,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;YACpD,OAAO;QACR,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,GAAG,YAAY,iBAAiB,EAAE,CAAC;gBACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;gBAChF,iBAAiB,EAAE,CAAC;gBACpB,KAAK,GAAG,IAAI,CAAC;YACd,CAAC;iBAAM,IAAI,GAAG,YAAY,oBAAoB,EAAE,CAAC;gBAChD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;gBAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACjB,CAAC;iBAAM,CAAC;gBACP,MAAM,GAAG,CAAC;YACX,CAAC;QACF,CAAC;IACF,CAAC;IAED,yCAAyC;IACzC,IAAI,CAAC,KAAK,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iEAAiE,CAAC,CAAC;QACxF,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC;QAEzC,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;YACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CACnB,2EAA2E,KAAK,CAAC,WAAW,IAAI,EAAE,IAAI,CACtG,CAAC;QACH,CAAC;QAED,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACpD,KAAK,GAAG,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC1C,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAExB,gBAAgB,CACf,KAAK,CAAC,YAAY,IAAI,IAAI,EAC1B,KAAK,CAAC,WAAW,IAAI,IAAI,EACzB,KAAK,CAAC,KAAK,EAAE,MAAM,IAAI,CAAC,CACxB,CAAC;IACH,CAAC;IAED,qCAAqC;IACrC,IAAI,CAAC;QACJ,MAAM,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,GAAG,YAAY,iBAAiB,EAAE,CAAC;YACtC,0DAA0D;YAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,gDAAgD,CAAC,CAAC;YACvE,iBAAiB,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YACpD,KAAK,GAAG,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;YAC1C,gBAAgB,CAAC,KAAK,CAAC,CAAC;YACxB,MAAM,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;QACrD,CAAC;aAAM,IAAI,GAAG,YAAY,oBAAoB,EAAE,CAAC;YAChD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;YAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjB,CAAC;aAAM,CAAC;YACP,MAAM,GAAG,CAAC;QACX,CAAC;IACF,CAAC;AACF,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
|
package/dist/proxy.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
package/src/auth.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Device flow client for Pharaoh MCP proxy (RFC 8628).
|
|
3
|
-
* All user-visible output goes to stderr — stdout is reserved for JSON-RPC.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** Response from POST /device. */
|
|
7
|
-
export interface DeviceCodeResponse {
|
|
8
|
-
device_code: string;
|
|
9
|
-
user_code: string;
|
|
10
|
-
verification_uri: string;
|
|
11
|
-
verification_uri_complete: string;
|
|
12
|
-
expires_in: number;
|
|
13
|
-
interval: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Successful response from POST /device/token. */
|
|
17
|
-
export interface TokenResponse {
|
|
18
|
-
access_token: string;
|
|
19
|
-
token_type: string;
|
|
20
|
-
expires_in: number;
|
|
21
|
-
sse_url: string;
|
|
22
|
-
github_login?: string;
|
|
23
|
-
tenant_name?: string;
|
|
24
|
-
repos?: string[];
|
|
25
|
-
provisional?: boolean;
|
|
26
|
-
install_url?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Error response from POST /device/token while still waiting. */
|
|
30
|
-
interface DeviceTokenError {
|
|
31
|
-
error: string;
|
|
32
|
-
error_description?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Request a device code from the Pharaoh server (RFC 8628 §3.1).
|
|
37
|
-
* Returns the device code, user code, and verification URIs.
|
|
38
|
-
*/
|
|
39
|
-
export async function requestDeviceCode(baseUrl: string): Promise<DeviceCodeResponse> {
|
|
40
|
-
const res = await fetch(`${baseUrl}/device`, {
|
|
41
|
-
method: "POST",
|
|
42
|
-
headers: { "Content-Type": "application/json" },
|
|
43
|
-
body: JSON.stringify({}),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
if (!res.ok) {
|
|
47
|
-
const text = await res.text().catch(() => "");
|
|
48
|
-
throw new Error(`Device code request failed (${res.status}): ${text}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return (await res.json()) as DeviceCodeResponse;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Poll for device authorization completion (RFC 8628 §3.5).
|
|
56
|
-
* Blocks until the user authorizes, the code expires, or an error occurs.
|
|
57
|
-
* Adds 1s jitter to the server-specified interval to avoid thundering herd.
|
|
58
|
-
*/
|
|
59
|
-
export async function pollForToken(
|
|
60
|
-
baseUrl: string,
|
|
61
|
-
deviceCode: string,
|
|
62
|
-
interval: number,
|
|
63
|
-
): Promise<TokenResponse> {
|
|
64
|
-
const grantType = "urn:ietf:params:oauth:grant-type:device_code";
|
|
65
|
-
|
|
66
|
-
while (true) {
|
|
67
|
-
// Wait interval + 0–1s jitter before each poll
|
|
68
|
-
const jitterMs = Math.random() * 1000;
|
|
69
|
-
await sleep(interval * 1000 + jitterMs);
|
|
70
|
-
|
|
71
|
-
const res = await fetch(`${baseUrl}/device/token`, {
|
|
72
|
-
method: "POST",
|
|
73
|
-
headers: { "Content-Type": "application/json" },
|
|
74
|
-
body: JSON.stringify({ device_code: deviceCode, grant_type: grantType }),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (res.ok) {
|
|
78
|
-
return (await res.json()) as TokenResponse;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const body = (await res.json().catch(() => ({ error: "unknown" }))) as DeviceTokenError;
|
|
82
|
-
|
|
83
|
-
if (body.error === "authorization_pending") {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (body.error === "slow_down") {
|
|
88
|
-
// RFC 8628 §3.5: increase interval by 5 seconds
|
|
89
|
-
interval += 5;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (body.error === "expired_token") {
|
|
94
|
-
throw new Error("Device code expired. Please try again.");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (body.error === "access_denied") {
|
|
98
|
-
throw new Error("Authorization was denied.");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
throw new Error(`Device token error: ${body.error} — ${body.error_description ?? ""}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Print the device activation prompt to stderr.
|
|
107
|
-
* Shows the user code and verification URI in a visible box.
|
|
108
|
-
*/
|
|
109
|
-
export function printActivationPrompt(userCode: string, verificationUri: string): void {
|
|
110
|
-
const lines = [
|
|
111
|
-
"",
|
|
112
|
-
"┌───────────────────────────────────────────┐",
|
|
113
|
-
"│ Pharaoh — Device Authorization │",
|
|
114
|
-
"│ │",
|
|
115
|
-
`│ Code: ${userCode.padEnd(33)}│`,
|
|
116
|
-
`│ URL: ${verificationUri.padEnd(33)}│`,
|
|
117
|
-
"│ │",
|
|
118
|
-
"│ Open the URL and enter the code to sign │",
|
|
119
|
-
"│ in. You can do this on any device. │",
|
|
120
|
-
"└───────────────────────────────────────────┘",
|
|
121
|
-
"",
|
|
122
|
-
];
|
|
123
|
-
process.stderr.write(lines.join("\n") + "\n");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Print authentication success message to stderr.
|
|
128
|
-
*/
|
|
129
|
-
export function printAuthSuccess(
|
|
130
|
-
login: string | null,
|
|
131
|
-
tenant: string | null,
|
|
132
|
-
repoCount: number,
|
|
133
|
-
): void {
|
|
134
|
-
const parts: string[] = ["Pharaoh: authenticated"];
|
|
135
|
-
if (login) parts.push(`as ${login}`);
|
|
136
|
-
if (tenant) parts.push(`(${tenant})`);
|
|
137
|
-
parts.push(`— ${repoCount} repo${repoCount === 1 ? "" : "s"} connected`);
|
|
138
|
-
process.stderr.write(`${parts.join(" ")}\n`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function sleep(ms: number): Promise<void> {
|
|
142
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
143
|
-
}
|
package/src/credentials.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credential storage for Pharaoh MCP proxy — persists device flow tokens to ~/.pharaoh/credentials.json.
|
|
3
|
-
* File permissions: 0600 (credentials.json), 0700 (~/.pharaoh/).
|
|
4
|
-
*/
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
8
|
-
|
|
9
|
-
/** Current credential schema version — bump when the format changes. */
|
|
10
|
-
const SCHEMA_VERSION = 1;
|
|
11
|
-
|
|
12
|
-
/** Stored credential shape. */
|
|
13
|
-
export interface Credentials {
|
|
14
|
-
version: typeof SCHEMA_VERSION;
|
|
15
|
-
access_token: string;
|
|
16
|
-
/** ISO 8601 timestamp when the token expires. */
|
|
17
|
-
expires_at: string;
|
|
18
|
-
/** Original TTL in seconds (from the token response). */
|
|
19
|
-
expires_in: number;
|
|
20
|
-
sse_url: string;
|
|
21
|
-
github_login: string | null;
|
|
22
|
-
tenant_name: string | null;
|
|
23
|
-
repos: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Default credential file path. */
|
|
27
|
-
const CREDENTIALS_PATH = join(homedir(), ".pharaoh", "credentials.json");
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Read and parse stored credentials. Returns null if missing, unreadable,
|
|
31
|
-
* malformed, or wrong schema version.
|
|
32
|
-
*/
|
|
33
|
-
export function readCredentials(path = CREDENTIALS_PATH): Credentials | null {
|
|
34
|
-
try {
|
|
35
|
-
const raw = readFileSync(path, "utf-8");
|
|
36
|
-
const parsed: unknown = JSON.parse(raw);
|
|
37
|
-
if (!isValidCredentials(parsed)) return null;
|
|
38
|
-
return parsed;
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Write credentials to disk with restrictive permissions.
|
|
46
|
-
* Creates the parent directory (0700) if it doesn't exist.
|
|
47
|
-
*/
|
|
48
|
-
export function writeCredentials(creds: Credentials, path = CREDENTIALS_PATH): void {
|
|
49
|
-
const dir = dirname(path);
|
|
50
|
-
if (!existsSync(dir)) {
|
|
51
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
52
|
-
}
|
|
53
|
-
writeFileSync(path, JSON.stringify(creds, null, "\t"), { mode: 0o600 });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Delete the credential file. No-op if it doesn't exist. */
|
|
57
|
-
export function deleteCredentials(path = CREDENTIALS_PATH): void {
|
|
58
|
-
try {
|
|
59
|
-
unlinkSync(path);
|
|
60
|
-
} catch {
|
|
61
|
-
// File doesn't exist — nothing to delete
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Check if credentials are expired or within 10% of expiry.
|
|
67
|
-
* The 10% buffer prevents connecting with a token that will expire mid-session.
|
|
68
|
-
*/
|
|
69
|
-
export function isExpired(creds: Credentials): boolean {
|
|
70
|
-
const expiresAt = new Date(creds.expires_at).getTime();
|
|
71
|
-
if (Number.isNaN(expiresAt)) return true;
|
|
72
|
-
|
|
73
|
-
const now = Date.now();
|
|
74
|
-
const totalTtlMs = creds.expires_in * 1000;
|
|
75
|
-
const bufferMs = totalTtlMs * 0.1;
|
|
76
|
-
|
|
77
|
-
return now >= expiresAt - bufferMs;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Type guard for valid credential objects. */
|
|
81
|
-
function isValidCredentials(value: unknown): value is Credentials {
|
|
82
|
-
if (typeof value !== "object" || value === null) return false;
|
|
83
|
-
const obj = value as Record<string, unknown>;
|
|
84
|
-
return (
|
|
85
|
-
obj.version === SCHEMA_VERSION &&
|
|
86
|
-
typeof obj.access_token === "string" &&
|
|
87
|
-
typeof obj.expires_at === "string" &&
|
|
88
|
-
typeof obj.expires_in === "number" &&
|
|
89
|
-
typeof obj.sse_url === "string" &&
|
|
90
|
-
Array.isArray(obj.repos)
|
|
91
|
-
);
|
|
92
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* @pharaoh-so/mcp — Stdio-to-SSE proxy for headless MCP environments.
|
|
4
|
-
*
|
|
5
|
-
* Presents as an MCP server on stdio (for Claude Code) and relays messages
|
|
6
|
-
* to a remote Pharaoh SSE server. Authenticates via RFC 8628 device flow
|
|
7
|
-
* so the user can authorize on any device with a browser.
|
|
8
|
-
*/
|
|
9
|
-
import {
|
|
10
|
-
printActivationPrompt,
|
|
11
|
-
printAuthSuccess,
|
|
12
|
-
pollForToken,
|
|
13
|
-
requestDeviceCode,
|
|
14
|
-
type TokenResponse,
|
|
15
|
-
} from "./auth.js";
|
|
16
|
-
import {
|
|
17
|
-
type Credentials,
|
|
18
|
-
deleteCredentials,
|
|
19
|
-
isExpired,
|
|
20
|
-
readCredentials,
|
|
21
|
-
writeCredentials,
|
|
22
|
-
} from "./credentials.js";
|
|
23
|
-
import { TokenExpiredError, TenantSuspendedError, startProxy } from "./proxy.js";
|
|
24
|
-
|
|
25
|
-
const DEFAULT_SERVER = "https://mcp.pharaoh.so";
|
|
26
|
-
|
|
27
|
-
/** Parse CLI arguments. */
|
|
28
|
-
function parseArgs(): { server: string; logout: boolean } {
|
|
29
|
-
const args = process.argv.slice(2);
|
|
30
|
-
let server = DEFAULT_SERVER;
|
|
31
|
-
let logout = false;
|
|
32
|
-
|
|
33
|
-
for (let i = 0; i < args.length; i++) {
|
|
34
|
-
if (args[i] === "--server" && args[i + 1]) {
|
|
35
|
-
server = args[i + 1];
|
|
36
|
-
i++;
|
|
37
|
-
} else if (args[i] === "--logout") {
|
|
38
|
-
logout = true;
|
|
39
|
-
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
40
|
-
printUsage();
|
|
41
|
-
process.exit(0);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Strip trailing slash
|
|
46
|
-
server = server.replace(/\/+$/, "");
|
|
47
|
-
|
|
48
|
-
return { server, logout };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function printUsage(): void {
|
|
52
|
-
process.stderr.write(
|
|
53
|
-
[
|
|
54
|
-
"Usage: pharaoh-mcp [options]",
|
|
55
|
-
"",
|
|
56
|
-
"Options:",
|
|
57
|
-
" --server <url> Pharaoh server URL (default: https://mcp.pharaoh.so)",
|
|
58
|
-
" --logout Clear stored credentials and exit",
|
|
59
|
-
" --help, -h Show this help",
|
|
60
|
-
"",
|
|
61
|
-
"Add to Claude Code:",
|
|
62
|
-
" claude mcp add pharaoh -- npx @pharaoh-so/mcp",
|
|
63
|
-
"",
|
|
64
|
-
].join("\n") + "\n",
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Run the device flow and return a token response. */
|
|
69
|
-
async function authenticate(server: string): Promise<TokenResponse> {
|
|
70
|
-
const deviceCode = await requestDeviceCode(server);
|
|
71
|
-
printActivationPrompt(deviceCode.user_code, deviceCode.verification_uri);
|
|
72
|
-
return pollForToken(server, deviceCode.device_code, deviceCode.interval);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Validate that a server-supplied SSE URL shares the same origin as the configured server.
|
|
77
|
-
* Prevents a compromised auth response from redirecting the Bearer token to an attacker's host.
|
|
78
|
-
* Falls back to `${server}/sse` if the URL is missing, malformed, or cross-origin.
|
|
79
|
-
*/
|
|
80
|
-
function resolveSseUrl(tokenSseUrl: string | undefined, server: string): string {
|
|
81
|
-
const fallback = `${server}/sse`;
|
|
82
|
-
if (!tokenSseUrl) return fallback;
|
|
83
|
-
try {
|
|
84
|
-
const sseOrigin = new URL(tokenSseUrl).origin;
|
|
85
|
-
const serverOrigin = new URL(server).origin;
|
|
86
|
-
if (sseOrigin !== serverOrigin) {
|
|
87
|
-
process.stderr.write(
|
|
88
|
-
`Pharaoh: ignoring cross-origin sse_url (${sseOrigin} ≠ ${serverOrigin})\n`,
|
|
89
|
-
);
|
|
90
|
-
return fallback;
|
|
91
|
-
}
|
|
92
|
-
return tokenSseUrl;
|
|
93
|
-
} catch {
|
|
94
|
-
return fallback;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Convert a token response to storable credentials. */
|
|
99
|
-
function tokenToCredentials(token: TokenResponse, sseUrl: string): Credentials {
|
|
100
|
-
return {
|
|
101
|
-
version: 1,
|
|
102
|
-
access_token: token.access_token,
|
|
103
|
-
expires_at: new Date(Date.now() + token.expires_in * 1000).toISOString(),
|
|
104
|
-
expires_in: token.expires_in,
|
|
105
|
-
sse_url: sseUrl,
|
|
106
|
-
github_login: token.github_login ?? null,
|
|
107
|
-
tenant_name: token.tenant_name ?? null,
|
|
108
|
-
repos: token.repos ?? [],
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Format remaining TTL as human-readable string (e.g. "5d 12h"). */
|
|
113
|
-
function formatTtl(expiresAt: string): string {
|
|
114
|
-
const remainingMs = new Date(expiresAt).getTime() - Date.now();
|
|
115
|
-
if (remainingMs <= 0) return "expired";
|
|
116
|
-
const hours = Math.floor(remainingMs / 3_600_000);
|
|
117
|
-
const days = Math.floor(hours / 24);
|
|
118
|
-
const remHours = hours % 24;
|
|
119
|
-
if (days > 0) return `${days}d ${remHours}h`;
|
|
120
|
-
if (hours > 0) return `${hours}h`;
|
|
121
|
-
return `${Math.floor(remainingMs / 60_000)}m`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function main(): Promise<void> {
|
|
125
|
-
const { server, logout } = parseArgs();
|
|
126
|
-
|
|
127
|
-
// --logout: clear credentials and exit
|
|
128
|
-
if (logout) {
|
|
129
|
-
deleteCredentials();
|
|
130
|
-
process.stderr.write("Pharaoh: credentials cleared\n");
|
|
131
|
-
process.exit(0);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let creds = readCredentials();
|
|
135
|
-
|
|
136
|
-
// If we have valid credentials, try to connect directly
|
|
137
|
-
if (creds && !isExpired(creds)) {
|
|
138
|
-
process.stderr.write(`Pharaoh: token valid for ${formatTtl(creds.expires_at)} — connecting\n`);
|
|
139
|
-
try {
|
|
140
|
-
await startProxy(creds.sse_url, creds.access_token);
|
|
141
|
-
return;
|
|
142
|
-
} catch (err) {
|
|
143
|
-
if (err instanceof TokenExpiredError) {
|
|
144
|
-
process.stderr.write("Pharaoh: token rejected by server — re-authenticating\n");
|
|
145
|
-
deleteCredentials();
|
|
146
|
-
creds = null;
|
|
147
|
-
} else if (err instanceof TenantSuspendedError) {
|
|
148
|
-
process.stderr.write(`Pharaoh: ${err.message}\n`);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
} else {
|
|
151
|
-
throw err;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// No valid credentials — run device flow
|
|
157
|
-
if (!creds || isExpired(creds)) {
|
|
158
|
-
process.stderr.write("Pharaoh: no valid credentials — starting device authorization\n");
|
|
159
|
-
const token = await authenticate(server);
|
|
160
|
-
|
|
161
|
-
if (token.provisional) {
|
|
162
|
-
process.stderr.write(
|
|
163
|
-
`Pharaoh: provisional access — install the GitHub App to map your repos: ${token.install_url ?? ""}\n`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const sseUrl = resolveSseUrl(token.sse_url, server);
|
|
168
|
-
creds = tokenToCredentials(token, sseUrl);
|
|
169
|
-
writeCredentials(creds);
|
|
170
|
-
|
|
171
|
-
printAuthSuccess(
|
|
172
|
-
token.github_login ?? null,
|
|
173
|
-
token.tenant_name ?? null,
|
|
174
|
-
token.repos?.length ?? 0,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Start proxy with fresh credentials
|
|
179
|
-
try {
|
|
180
|
-
await startProxy(creds.sse_url, creds.access_token);
|
|
181
|
-
} catch (err) {
|
|
182
|
-
if (err instanceof TokenExpiredError) {
|
|
183
|
-
// Token expired during session — delete creds and re-auth
|
|
184
|
-
process.stderr.write("Pharaoh: session expired — re-authenticating\n");
|
|
185
|
-
deleteCredentials();
|
|
186
|
-
const token = await authenticate(server);
|
|
187
|
-
const sseUrl = resolveSseUrl(token.sse_url, server);
|
|
188
|
-
creds = tokenToCredentials(token, sseUrl);
|
|
189
|
-
writeCredentials(creds);
|
|
190
|
-
await startProxy(creds.sse_url, creds.access_token);
|
|
191
|
-
} else if (err instanceof TenantSuspendedError) {
|
|
192
|
-
process.stderr.write(`Pharaoh: ${err.message}\n`);
|
|
193
|
-
process.exit(1);
|
|
194
|
-
} else {
|
|
195
|
-
throw err;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
main().catch((err) => {
|
|
201
|
-
process.stderr.write(`Pharaoh: fatal — ${err instanceof Error ? err.message : String(err)}\n`);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
});
|
package/src/proxy.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stdio ↔ SSE proxy — bridges a local MCP stdio connection to a remote Pharaoh SSE server.
|
|
3
|
-
* Uses SDK transports on both ends: StdioServerTransport (local) and SSEClientTransport (remote).
|
|
4
|
-
*/
|
|
5
|
-
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
6
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
-
|
|
8
|
-
/** Thrown when the remote server returns 401 — token is expired or revoked. */
|
|
9
|
-
export class TokenExpiredError extends Error {
|
|
10
|
-
constructor() {
|
|
11
|
-
super("Token expired or revoked (401)");
|
|
12
|
-
this.name = "TokenExpiredError";
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Thrown when the remote server returns 403 — tenant is suspended. */
|
|
17
|
-
export class TenantSuspendedError extends Error {
|
|
18
|
-
constructor(message: string) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.name = "TenantSuspendedError";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Backoff schedule for reconnect attempts (milliseconds). */
|
|
25
|
-
const BACKOFF_MS = [2000, 4000, 8000, 16000, 32000];
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Classify an SSE error — detect 401/403 and throw typed errors.
|
|
29
|
-
* Exported for testability; used internally by the reconnect loop.
|
|
30
|
-
*/
|
|
31
|
-
export function classifySSEError(err: Error): void {
|
|
32
|
-
const msg = err.message ?? "";
|
|
33
|
-
// SseError from SDK includes the HTTP status code
|
|
34
|
-
if ("code" in err) {
|
|
35
|
-
const code = (err as { code?: number }).code;
|
|
36
|
-
if (code === 401) throw new TokenExpiredError();
|
|
37
|
-
if (code === 403) throw new TenantSuspendedError(msg || "Tenant suspended");
|
|
38
|
-
}
|
|
39
|
-
// Fallback: check message for status codes
|
|
40
|
-
if (msg.includes("401")) throw new TokenExpiredError();
|
|
41
|
-
if (msg.includes("403")) throw new TenantSuspendedError(msg || "Tenant suspended");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Create an SSEClientTransport with Bearer token auth.
|
|
46
|
-
* Injects the Authorization header into both the SSE connection (via custom fetch)
|
|
47
|
-
* and POST requests (via requestInit).
|
|
48
|
-
*/
|
|
49
|
-
function createSSETransport(sseUrl: string, token: string): SSEClientTransport {
|
|
50
|
-
const authHeader = `Bearer ${token}`;
|
|
51
|
-
return new SSEClientTransport(new URL(sseUrl), {
|
|
52
|
-
eventSourceInit: {
|
|
53
|
-
fetch: (url, init) => {
|
|
54
|
-
const h = new Headers(init.headers as HeadersInit);
|
|
55
|
-
h.set("Authorization", authHeader);
|
|
56
|
-
return fetch(url, { ...init, headers: h } as RequestInit);
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
requestInit: {
|
|
60
|
-
headers: { Authorization: authHeader },
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Start the stdio ↔ SSE proxy. Blocks until the connection is permanently closed.
|
|
67
|
-
*
|
|
68
|
-
* Message flow:
|
|
69
|
-
* Claude Code → stdin → StdioServerTransport.onmessage → SSEClientTransport.send → POST /message
|
|
70
|
-
* Pharaoh → SSE stream → SSEClientTransport.onmessage → StdioServerTransport.send → stdout
|
|
71
|
-
*
|
|
72
|
-
* On SSE disconnect, attempts reconnect with exponential backoff (5 retries, ~62s total).
|
|
73
|
-
* Throws TokenExpiredError on 401, TenantSuspendedError on 403.
|
|
74
|
-
*/
|
|
75
|
-
export async function startProxy(sseUrl: string, token: string): Promise<void> {
|
|
76
|
-
const stdio = new StdioServerTransport();
|
|
77
|
-
let sse = createSSETransport(sseUrl, token);
|
|
78
|
-
let reconnectAttempt = 0;
|
|
79
|
-
|
|
80
|
-
/** Wire up bidirectional message relay between the two transports. */
|
|
81
|
-
function bridge(sseTransport: SSEClientTransport): void {
|
|
82
|
-
stdio.onmessage = (msg) => {
|
|
83
|
-
sseTransport.send(msg).catch((err) => {
|
|
84
|
-
process.stderr.write(`Pharaoh: send error — ${err instanceof Error ? err.message : String(err)}\n`);
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
sseTransport.onmessage = (msg) => {
|
|
88
|
-
stdio.send(msg).catch((err) => {
|
|
89
|
-
process.stderr.write(`Pharaoh: stdout error — ${err instanceof Error ? err.message : String(err)}\n`);
|
|
90
|
-
});
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Handle SSE errors — delegates to classifySSEError for 401/403 detection. */
|
|
95
|
-
const handleSSEError = classifySSEError;
|
|
96
|
-
|
|
97
|
-
// Wire up initial bridge
|
|
98
|
-
bridge(sse);
|
|
99
|
-
|
|
100
|
-
// Handle SSE connection drops with reconnect
|
|
101
|
-
sse.onerror = (err) => {
|
|
102
|
-
try {
|
|
103
|
-
handleSSEError(err);
|
|
104
|
-
} catch (typed) {
|
|
105
|
-
// TokenExpiredError or TenantSuspendedError — propagate via close
|
|
106
|
-
sse.close().catch(() => {});
|
|
107
|
-
throw typed;
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// Start both transports
|
|
112
|
-
await stdio.start();
|
|
113
|
-
|
|
114
|
-
// Connect SSE with reconnect loop
|
|
115
|
-
await connectWithReconnect();
|
|
116
|
-
|
|
117
|
-
/** Attempt SSE connection with exponential backoff on failure. */
|
|
118
|
-
async function connectWithReconnect(): Promise<void> {
|
|
119
|
-
while (reconnectAttempt <= BACKOFF_MS.length) {
|
|
120
|
-
try {
|
|
121
|
-
if (reconnectAttempt > 0) {
|
|
122
|
-
const delay = BACKOFF_MS[reconnectAttempt - 1];
|
|
123
|
-
process.stderr.write(
|
|
124
|
-
`Pharaoh: reconnecting in ${delay / 1000}s (attempt ${reconnectAttempt}/${BACKOFF_MS.length})...\n`,
|
|
125
|
-
);
|
|
126
|
-
await sleep(delay);
|
|
127
|
-
sse = createSSETransport(sseUrl, token);
|
|
128
|
-
bridge(sse);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
await sse.start();
|
|
132
|
-
reconnectAttempt = 0; // Reset on successful connection
|
|
133
|
-
|
|
134
|
-
// Wait for close — this promise resolves when SSE disconnects
|
|
135
|
-
await new Promise<void>((resolve, reject) => {
|
|
136
|
-
sse.onclose = () => resolve();
|
|
137
|
-
sse.onerror = (err) => {
|
|
138
|
-
try {
|
|
139
|
-
handleSSEError(err);
|
|
140
|
-
} catch (typed) {
|
|
141
|
-
reject(typed);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
// Non-fatal error — SSE will auto-close, onclose will fire
|
|
145
|
-
};
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// SSE closed — attempt reconnect
|
|
149
|
-
reconnectAttempt++;
|
|
150
|
-
} catch (err) {
|
|
151
|
-
if (err instanceof TokenExpiredError || err instanceof TenantSuspendedError) {
|
|
152
|
-
await stdio.close().catch(() => {});
|
|
153
|
-
throw err;
|
|
154
|
-
}
|
|
155
|
-
reconnectAttempt++;
|
|
156
|
-
if (reconnectAttempt > BACKOFF_MS.length) {
|
|
157
|
-
process.stderr.write("Pharaoh: max reconnect attempts reached — exiting\n");
|
|
158
|
-
await stdio.close().catch(() => {});
|
|
159
|
-
throw new Error("SSE connection failed after maximum retries");
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function sleep(ms: number): Promise<void> {
|
|
167
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
168
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"rootDir": "src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"declaration": true,
|
|
13
|
-
"sourceMap": true,
|
|
14
|
-
"resolveJsonModule": true
|
|
15
|
-
},
|
|
16
|
-
"include": ["src/**/*.ts"],
|
|
17
|
-
"exclude": ["node_modules", "dist"]
|
|
18
|
-
}
|