@pharaoh-so/mcp 0.1.2 → 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
 
@@ -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,170 +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
  });
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pharaoh-so/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Stdio-to-SSE proxy for Pharaoh MCP — enables headless environments (VPS, SSH, CI) via device flow auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",