@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 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
- On first run, the proxy shows a device code and URL. Open the URL on any device (phone, laptop) to authorize. The proxy stores credentials at `~/.pharaoh/credentials.json` and reuses them until they expire (7 days).
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
@@ -90,4 +90,3 @@ export function printAuthSuccess(login, tenant, repoCount) {
90
90
  function sleep(ms) {
91
91
  return new Promise((resolve) => setTimeout(resolve, ms));
92
92
  }
93
- //# sourceMappingURL=auth.js.map
@@ -70,4 +70,3 @@ function isValidCredentials(value) {
70
70
  typeof obj.sse_url === "string" &&
71
71
  Array.isArray(obj.repos));
72
72
  }
73
- //# sourceMappingURL=credentials.js.map
@@ -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;
@@ -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, } from "./credentials.js";
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
- const DEFAULT_SERVER = "https://mcp.pharaoh.so";
13
- /** Parse CLI arguments. */
14
- function parseArgs() {
17
+ async function main() {
15
18
  const args = process.argv.slice(2);
16
- let server = DEFAULT_SERVER;
17
- let logout = false;
18
- for (let i = 0; i < args.length; i++) {
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
- process.stderr.write("Pharaoh: credentials cleared\n");
26
+ printLines("Pharaoh: credentials cleared");
110
27
  process.exit(0);
111
28
  }
112
- let creds = readCredentials();
113
- // If we have valid credentials, try to connect directly
114
- if (creds && !isExpired(creds)) {
115
- process.stderr.write(`Pharaoh: token valid for ${formatTtl(creds.expires_at)} connecting\n`);
116
- try {
117
- await startProxy(creds.sse_url, creds.access_token);
118
- return;
119
- }
120
- catch (err) {
121
- if (err instanceof TokenExpiredError) {
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
- // No valid credentials — run device flow
136
- if (!creds || isExpired(creds)) {
137
- process.stderr.write("Pharaoh: no valid credentials — starting device authorization\n");
138
- const token = await authenticate(server);
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
- process.stderr.write(`Pharaoh: provisional access — install the GitHub App to map your repos: ${token.install_url ?? ""}\n`);
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
- creds = tokenToCredentials(token, sseUrl);
144
- writeCredentials(creds);
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
- // Start proxy with fresh credentials
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
- // Token expired during session delete creds and re-auth
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
- else {
167
- throw err;
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
- process.stderr.write(`Pharaoh: fatal ${err instanceof Error ? err.message : String(err)}\n`);
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
- // Wire up initial bridge
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
- "name": "@pharaoh-so/mcp",
3
- "version": "0.1.0",
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
- "scripts": {
11
- "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)\"",
12
- "typecheck": "tsc --noEmit",
13
- "test": "vitest run",
14
- "lint": "biome check src/"
15
- },
16
- "license": "MIT",
17
- "engines": {
18
- "node": ">=18"
19
- },
20
- "dependencies": {
21
- "@modelcontextprotocol/sdk": "^1.26.0"
22
- },
23
- "devDependencies": {
24
- "@biomejs/biome": "^2.3.15",
25
- "typescript": "^5.9.3",
26
- "vitest": "^4.0.18"
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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"}
@@ -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":"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"}
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
- }
@@ -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
- }