@xsreality/mcp-gateway 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abhinav Sonkar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # mcp-gateway
2
+
3
+ ![CI](https://github.com/xsreality/mcp-gateway/actions/workflows/ci.yml/badge.svg)
4
+
5
+ A small CLI that exposes a **local STDIO MCP endpoint** and proxies it to a **remote Streamable-HTTP MCP
6
+ server** — handling **OAuth 2.1** (including **Dynamic Client Registration**) on your behalf.
7
+
8
+ Use it to connect stdio-only MCP clients (Claude Desktop, IDE MCP integrations, anything that launches an
9
+ MCP server as a subprocess) to remote, OAuth-protected MCP servers that those clients can't reach directly.
10
+
11
+ ```
12
+ ┌──────────────┐ stdio ┌──────────────┐ Streamable HTTP + OAuth ┌──────────────┐
13
+ │ MCP client │ ─────────▶ │ mcp-gateway │ ─────────────────────────▶ │ Remote MCP │
14
+ │ (Claude etc.)│ ◀───────── │ │ ◀───────────────────────── │ server │
15
+ └──────────────┘ └──────────────┘ └──────────────┘
16
+ ```
17
+
18
+ The gateway is transparent: it forwards raw MCP messages both ways, so every tool, resource, prompt, and
19
+ notification the remote server offers passes straight through.
20
+
21
+ ## Requirements
22
+
23
+ - Node.js ≥ 20
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install -g @xsreality/mcp-gateway
29
+ ```
30
+
31
+ Or run without installing:
32
+
33
+ ```bash
34
+ npx @xsreality/mcp-gateway --url https://mcp.example.com/mcp
35
+ ```
36
+
37
+ ### From source
38
+
39
+ ```bash
40
+ git clone https://github.com/xsreality/mcp-gateway.git && cd mcp-gateway
41
+ npm install
42
+ npm run build
43
+ npm link # puts `mcp-gateway` on your PATH
44
+ ```
45
+
46
+ To remove the global link: `npm rm -g @xsreality/mcp-gateway`.
47
+
48
+ ## Usage
49
+
50
+ ```bash
51
+ mcp-gateway --url https://mcp.example.com/mcp
52
+ ```
53
+
54
+ On first connection to a protected server, the gateway opens your browser to authorize. After you approve,
55
+ tokens are cached locally and reused on subsequent runs (and refreshed automatically when they expire).
56
+ Servers that don't require auth work with no extra flags.
57
+
58
+ ### Use from an MCP client
59
+
60
+ Point your client at the `mcp-gateway` command:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "remote": {
66
+ "command": "npx",
67
+ "args": ["-y", "@xsreality/mcp-gateway", "--url", "https://mcp.example.com/mcp", "--scope", "read write"]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Options
74
+
75
+ | Flag | Description | Default |
76
+ |------|-------------|---------|
77
+ | `--url <url>` | **(required)** Remote Streamable-HTTP MCP server endpoint | `$MCP_GATEWAY_URL` |
78
+ | `--header <k:v>` | Static header forwarded upstream, `"Key: value"` (repeatable) | — |
79
+ | `--scope <scopes>` | OAuth scopes to request | `$MCP_GATEWAY_SCOPE` |
80
+ | `--client-name <name>` | `client_name` used during Dynamic Client Registration | `mcp-gateway` |
81
+ | `--client-id <id>` | Pre-registered OAuth client id (skips DCR) | `$MCP_GATEWAY_CLIENT_ID` |
82
+ | `--client-secret <secret>` | Pre-registered OAuth client secret (confidential client) | `$MCP_GATEWAY_CLIENT_SECRET` |
83
+ | `--no-dcr` | Disable Dynamic Client Registration (requires `--client-id`) | DCR enabled |
84
+ | `--callback-port <port>` | Fixed loopback port for the OAuth redirect | auto (persisted) |
85
+ | `--auth-timeout <seconds>` | How long to wait for you to finish authorizing in the browser | `300` |
86
+ | `--token-store <dir>` | Where tokens + client registration are stored | `~/.mcp-gateway` |
87
+ | `--no-browser` | Print the authorization URL instead of opening a browser (headless) | opens browser |
88
+ | `--log-level <level>` | `trace` `debug` `info` `warn` `error` `silent` (stderr/file only) | `info` |
89
+ | `--log-file <path>` | Write logs to a file instead of stderr | stderr |
90
+
91
+ Every flag has a `MCP_GATEWAY_*` environment-variable fallback where shown, so it drops cleanly into client
92
+ config blocks.
93
+
94
+ ## Authentication
95
+
96
+ - **Standards:** OAuth 2.1 authorization-code flow with mandatory PKCE, RFC 9728 protected-resource metadata
97
+ discovery, RFC 8414 authorization-server metadata, RFC 7591 Dynamic Client Registration, and the RFC 8707
98
+ `resource` indicator.
99
+ - **No setup needed for DCR servers:** the gateway registers itself automatically and caches the client id.
100
+ - **Servers without DCR:** pass `--client-id` (and `--client-secret` if it's a confidential client) and
101
+ `--no-dcr`.
102
+ - **Headless / remote machines:** use `--no-browser`; the gateway prints the URL to open, and listens on a
103
+ loopback port for the redirect. (You'll need to be able to reach that loopback port — e.g. over an SSH
104
+ tunnel — to complete the flow.)
105
+
106
+ ### Where credentials live
107
+
108
+ Tokens, the registered client, and the chosen callback port are stored as one JSON file per server (keyed by
109
+ the server's canonical URL) under `--token-store` (default `~/.mcp-gateway`), written with `0600` permissions.
110
+ Delete that directory to force re-authorization.
111
+
112
+ ## Logging
113
+
114
+ The stdio channel (stdout) carries the MCP protocol, so **all logs go to stderr** (or `--log-file`). If your
115
+ client shows the gateway's diagnostics mixed into its logs, lower `--log-level` (e.g. `warn`) or redirect to
116
+ a file.
117
+
118
+ ## Troubleshooting
119
+
120
+ - **Browser didn't open** — copy the URL printed on stderr, or run with `--no-browser`.
121
+ - **`authorization timed out`** — you didn't finish within `--auth-timeout`; just reconnect to retry.
122
+ - **Re-authorize from scratch** — delete the server's file under `~/.mcp-gateway` (or the whole directory).
123
+ - **Corporate proxy / extra auth** — forward static headers with repeated `--header "Key: value"`.
124
+ - **Stuck after the server changed its auth** — clear the token store; cached discovery/registration may be stale.
125
+
126
+ ## License
127
+
128
+ [MIT](LICENSE) © Abhinav Sonkar
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { Command, InvalidArgumentError, Option } from "commander";
3
+ import { ConfigError, defaultTokenStoreDir, parseHeaders, parseUrl, } from "./config.js";
4
+ import { createLogger } from "./log.js";
5
+ import { Gateway } from "./gateway.js";
6
+ const LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "silent"];
7
+ function collect(value, previous) {
8
+ return [...previous, value];
9
+ }
10
+ function intArg(name) {
11
+ return (value) => {
12
+ const n = Number(value);
13
+ if (!Number.isInteger(n) || n < 0) {
14
+ throw new InvalidArgumentError(`${name} must be a non-negative integer`);
15
+ }
16
+ return n;
17
+ };
18
+ }
19
+ function buildProgram() {
20
+ return new Command()
21
+ .name("mcp-gateway")
22
+ .description("Expose a local STDIO MCP endpoint that proxies a remote Streamable-HTTP MCP server (OAuth 2.1 + DCR).")
23
+ .requiredOption("--url <url>", "Remote Streamable-HTTP MCP server endpoint", process.env.MCP_GATEWAY_URL)
24
+ .option("--header <k:v>", 'Static header forwarded upstream, "Key: value" (repeatable)', collect, [])
25
+ .option("--scope <scopes>", "OAuth scopes to request", process.env.MCP_GATEWAY_SCOPE)
26
+ .option("--client-name <name>", "client_name used in Dynamic Client Registration", "mcp-gateway")
27
+ .option("--client-id <id>", "Pre-registered OAuth client id (skips DCR)", process.env.MCP_GATEWAY_CLIENT_ID)
28
+ .option("--client-secret <secret>", "Pre-registered OAuth client secret", process.env.MCP_GATEWAY_CLIENT_SECRET)
29
+ .option("--no-dcr", "Disable Dynamic Client Registration")
30
+ .option("--callback-port <port>", "Fixed loopback OAuth callback port", intArg("--callback-port"))
31
+ .option("--auth-timeout <seconds>", "Max wait for browser authorization", intArg("--auth-timeout"), 300)
32
+ .option("--token-store <dir>", "Directory for tokens + client registration", defaultTokenStoreDir())
33
+ .option("--no-browser", "Print the authorization URL instead of opening a browser")
34
+ .addOption(new Option("--log-level <level>", "Log verbosity (stderr/file only)")
35
+ .choices(LOG_LEVELS)
36
+ .default(process.env.MCP_GATEWAY_LOG_LEVEL ?? "info"))
37
+ .option("--log-file <path>", "Write logs to a file instead of stderr", process.env.MCP_GATEWAY_LOG_FILE);
38
+ }
39
+ function resolveConfig(opts) {
40
+ if (!opts.url) {
41
+ throw new ConfigError("--url is required (or set MCP_GATEWAY_URL)");
42
+ }
43
+ if (!LOG_LEVELS.includes(opts.logLevel)) {
44
+ throw new InvalidArgumentError(`invalid --log-level "${opts.logLevel}"`);
45
+ }
46
+ if (!opts.dcr && !opts.clientId) {
47
+ throw new ConfigError("--no-dcr requires --client-id to be provided");
48
+ }
49
+ return {
50
+ url: parseUrl(opts.url),
51
+ headers: parseHeaders(opts.header),
52
+ scope: opts.scope,
53
+ clientName: opts.clientName,
54
+ clientId: opts.clientId,
55
+ clientSecret: opts.clientSecret,
56
+ dcr: opts.dcr,
57
+ callbackPort: opts.callbackPort,
58
+ authTimeoutSec: opts.authTimeout,
59
+ tokenStoreDir: opts.tokenStore,
60
+ openBrowser: opts.browser,
61
+ logLevel: opts.logLevel,
62
+ logFile: opts.logFile,
63
+ };
64
+ }
65
+ async function main() {
66
+ const program = buildProgram();
67
+ program.parse(process.argv);
68
+ const opts = program.opts();
69
+ const config = resolveConfig(opts);
70
+ const log = createLogger({ level: config.logLevel, file: config.logFile });
71
+ const gateway = await Gateway.create(config, log);
72
+ const shutdown = (signal) => {
73
+ log.info({ signal }, "received signal, shutting down");
74
+ void gateway.close().finally(() => process.exit(0));
75
+ };
76
+ process.on("SIGINT", () => shutdown("SIGINT"));
77
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
78
+ try {
79
+ await gateway.run();
80
+ process.exit(0);
81
+ }
82
+ catch (err) {
83
+ log.error({ err }, "gateway failed");
84
+ process.exit(1);
85
+ }
86
+ }
87
+ main().catch((err) => {
88
+ // Last-resort handler; logger may not exist yet. stderr only.
89
+ process.stderr.write(`fatal: ${err instanceof Error ? err.stack : String(err)}\n`);
90
+ process.exit(1);
91
+ });
92
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { LogLevel } from "./log.js";
2
+ /**
3
+ * Resolved gateway configuration.
4
+ */
5
+ export interface Config {
6
+ /** Remote Streamable-HTTP MCP server endpoint. */
7
+ url: URL;
8
+ /** Static headers forwarded on every upstream request (e.g. routing, non-OAuth auth). */
9
+ headers: Record<string, string>;
10
+ logLevel: LogLevel;
11
+ logFile?: string;
12
+ /** OAuth scopes to request. */
13
+ scope?: string;
14
+ /** client_name used in Dynamic Client Registration. */
15
+ clientName: string;
16
+ /** Pre-registered client id (skips DCR for the id). */
17
+ clientId?: string;
18
+ /** Pre-registered client secret (confidential client). */
19
+ clientSecret?: string;
20
+ /** Whether Dynamic Client Registration is permitted. */
21
+ dcr: boolean;
22
+ /** Fixed loopback callback port; when undefined a free port is chosen and persisted. */
23
+ callbackPort?: number;
24
+ /** Max seconds to wait for the user to complete browser authorization. */
25
+ authTimeoutSec: number;
26
+ /** Directory holding per-server tokens + registration. */
27
+ tokenStoreDir: string;
28
+ /** Open the system browser automatically (false => print the URL). */
29
+ openBrowser: boolean;
30
+ }
31
+ export declare class ConfigError extends Error {
32
+ }
33
+ /** Parse repeated `--header "Key: value"` flags into a header map. */
34
+ export declare function parseHeaders(raw: string[]): Record<string, string>;
35
+ export declare function parseUrl(raw: string): URL;
36
+ export declare function defaultTokenStoreDir(): string;
package/dist/config.js ADDED
@@ -0,0 +1,38 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export class ConfigError extends Error {
4
+ }
5
+ /** Parse repeated `--header "Key: value"` flags into a header map. */
6
+ export function parseHeaders(raw) {
7
+ const headers = {};
8
+ for (const entry of raw) {
9
+ const idx = entry.indexOf(":");
10
+ if (idx === -1) {
11
+ throw new ConfigError(`Invalid --header "${entry}", expected "Key: value"`);
12
+ }
13
+ const key = entry.slice(0, idx).trim();
14
+ const value = entry.slice(idx + 1).trim();
15
+ if (!key) {
16
+ throw new ConfigError(`Invalid --header "${entry}", empty header name`);
17
+ }
18
+ headers[key] = value;
19
+ }
20
+ return headers;
21
+ }
22
+ export function parseUrl(raw) {
23
+ let url;
24
+ try {
25
+ url = new URL(raw);
26
+ }
27
+ catch {
28
+ throw new ConfigError(`Invalid --url "${raw}"`);
29
+ }
30
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
31
+ throw new ConfigError(`--url must be http(s), got "${url.protocol}"`);
32
+ }
33
+ return url;
34
+ }
35
+ export function defaultTokenStoreDir() {
36
+ return path.join(os.homedir(), ".mcp-gateway");
37
+ }
38
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAmC7B,MAAM,OAAO,WAAY,SAAQ,KAAK;CAAG;AAEzC,sEAAsE;AACtE,MAAM,UAAU,YAAY,CAAC,GAAa;IACxC,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YACf,MAAM,IAAI,WAAW,CAAC,qBAAqB,KAAK,0BAA0B,CAAC,CAAC;QAC9E,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1C,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,IAAI,WAAW,CAAC,qBAAqB,KAAK,sBAAsB,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACvB,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,WAAW,CAAC,kBAAkB,GAAG,GAAG,CAAC,CAAC;IAClD,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1D,MAAM,IAAI,WAAW,CAAC,+BAA+B,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;AACjD,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { Config } from "./config.js";
2
+ import type { Logger } from "./log.js";
3
+ /**
4
+ * The gateway transparently relays raw JSON-RPC messages between a local stdio
5
+ * MCP client (downstream) and a remote Streamable-HTTP MCP server (upstream).
6
+ *
7
+ * It deliberately does NOT re-declare tools/resources/prompts: by forwarding
8
+ * messages verbatim it passes through every current and future MCP capability.
9
+ *
10
+ * OAuth is lazy and transport-driven: the SDK transport attempts each upstream
11
+ * request with whatever token it has; on a 401 it runs discovery/DCR and invokes
12
+ * the provider's `redirectToAuthorization`. The first forwarded send then rejects
13
+ * with `UnauthorizedError`, at which point the gateway waits for the browser
14
+ * redirect, finishes the token exchange, and retries the send. Servers that need
15
+ * no auth never trigger any of this.
16
+ */
17
+ export declare class Gateway {
18
+ private readonly config;
19
+ private readonly log;
20
+ private readonly provider;
21
+ private readonly downstream;
22
+ private readonly upstream;
23
+ private closing;
24
+ private onClosed?;
25
+ private authInFlight?;
26
+ private constructor();
27
+ static create(config: Config, log: Logger): Promise<Gateway>;
28
+ /** Resolves when either side closes the connection. */
29
+ run(): Promise<void>;
30
+ private wire;
31
+ /** Send to upstream, completing the OAuth flow on a 401 and retrying once. */
32
+ private sendUpstream;
33
+ /** Wait for the browser redirect and finish token exchange; shared + reusable. */
34
+ private completeAuthorization;
35
+ private forward;
36
+ /** Tears down both transports; idempotent. */
37
+ close(): Promise<void>;
38
+ }
@@ -0,0 +1,134 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
3
+ import { createUpstreamTransport } from "./upstream.js";
4
+ import { GatewayOAuthProvider } from "./oauth/provider.js";
5
+ /**
6
+ * The gateway transparently relays raw JSON-RPC messages between a local stdio
7
+ * MCP client (downstream) and a remote Streamable-HTTP MCP server (upstream).
8
+ *
9
+ * It deliberately does NOT re-declare tools/resources/prompts: by forwarding
10
+ * messages verbatim it passes through every current and future MCP capability.
11
+ *
12
+ * OAuth is lazy and transport-driven: the SDK transport attempts each upstream
13
+ * request with whatever token it has; on a 401 it runs discovery/DCR and invokes
14
+ * the provider's `redirectToAuthorization`. The first forwarded send then rejects
15
+ * with `UnauthorizedError`, at which point the gateway waits for the browser
16
+ * redirect, finishes the token exchange, and retries the send. Servers that need
17
+ * no auth never trigger any of this.
18
+ */
19
+ export class Gateway {
20
+ config;
21
+ log;
22
+ provider;
23
+ downstream;
24
+ upstream;
25
+ closing = false;
26
+ onClosed;
27
+ authInFlight;
28
+ constructor(config, log, provider) {
29
+ this.config = config;
30
+ this.log = log;
31
+ this.provider = provider;
32
+ this.downstream = new StdioServerTransport();
33
+ this.upstream = createUpstreamTransport(config, provider);
34
+ }
35
+ static async create(config, log) {
36
+ const provider = await GatewayOAuthProvider.create(config, log);
37
+ return new Gateway(config, log, provider);
38
+ }
39
+ /** Resolves when either side closes the connection. */
40
+ async run() {
41
+ this.wire();
42
+ // Bind the loopback callback up front: the SDK reads redirect_uri (needing the
43
+ // bound port) during DCR, before redirectToAuthorization runs. Kept open for
44
+ // the session so token-expiry re-auth works without rebinding.
45
+ await this.provider.callback.listen();
46
+ await this.upstream.start(); // no network call yet; auth happens on first send
47
+ await this.downstream.start();
48
+ this.log.info({ url: this.config.url.href }, "gateway ready (stdio <-> streamable-http)");
49
+ await new Promise((resolve) => {
50
+ this.onClosed = resolve;
51
+ });
52
+ }
53
+ wire() {
54
+ // downstream (local client) -> upstream (remote server): may need auth.
55
+ this.downstream.onmessage = (msg) => {
56
+ if (this.closing)
57
+ return;
58
+ this.log.debug({ dir: "client→remote", msg }, "relay");
59
+ void this.sendUpstream(msg).catch((err) => {
60
+ this.log.error({ err }, "upstream relay failed");
61
+ void this.close();
62
+ });
63
+ };
64
+ // upstream (remote server) -> downstream (local client).
65
+ this.upstream.onmessage = (msg) => {
66
+ this.forward(this.downstream, msg, "remote→client");
67
+ };
68
+ this.downstream.onerror = (err) => this.log.error({ err }, "downstream error");
69
+ this.upstream.onerror = (err) => {
70
+ // 401s are reported out-of-band here too, but sendUpstream handles them.
71
+ if (err instanceof UnauthorizedError)
72
+ this.log.debug({ err }, "upstream auth challenge");
73
+ else
74
+ this.log.error({ err }, "upstream error");
75
+ };
76
+ this.downstream.onclose = () => {
77
+ this.log.info("downstream closed");
78
+ void this.close();
79
+ };
80
+ this.upstream.onclose = () => {
81
+ this.log.info("upstream closed");
82
+ void this.close();
83
+ };
84
+ }
85
+ /** Send to upstream, completing the OAuth flow on a 401 and retrying once. */
86
+ async sendUpstream(msg) {
87
+ try {
88
+ await this.upstream.send(msg);
89
+ }
90
+ catch (err) {
91
+ if (!(err instanceof UnauthorizedError))
92
+ throw err;
93
+ await this.completeAuthorization();
94
+ await this.upstream.send(msg);
95
+ }
96
+ }
97
+ /** Wait for the browser redirect and finish token exchange; shared + reusable. */
98
+ completeAuthorization() {
99
+ if (!this.authInFlight) {
100
+ this.authInFlight = (async () => {
101
+ this.log.info("authorization required; waiting for browser approval");
102
+ const code = await this.provider.callback.waitForCode(this.config.authTimeoutSec * 1000);
103
+ await this.upstream.finishAuth(code);
104
+ this.log.info("authorization complete");
105
+ })().finally(() => {
106
+ this.authInFlight = undefined; // allow re-auth on later token expiry
107
+ });
108
+ }
109
+ return this.authInFlight;
110
+ }
111
+ forward(to, msg, dir) {
112
+ if (this.closing)
113
+ return;
114
+ this.log.debug({ dir, msg }, "relay");
115
+ void to.send(msg).catch((err) => {
116
+ this.log.error({ err, dir }, "relay send failed");
117
+ void this.close();
118
+ });
119
+ }
120
+ /** Tears down both transports; idempotent. */
121
+ async close() {
122
+ if (this.closing)
123
+ return;
124
+ this.closing = true;
125
+ this.log.info("shutting down gateway");
126
+ await Promise.allSettled([
127
+ this.downstream.close(),
128
+ this.upstream.close(),
129
+ this.provider.callback.close(),
130
+ ]);
131
+ this.onClosed?.();
132
+ }
133
+ }
134
+ //# sourceMappingURL=gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.js","sourceRoot":"","sources":["../src/gateway.ts"],"names":[],"mappings":"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"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import pino from "pino";
2
+ /**
3
+ * Logger writes to **stderr only**. stdout is reserved for the MCP JSON-RPC
4
+ * stream on the stdio transport — writing anything else there corrupts the
5
+ * protocol. Never use console.log in this project.
6
+ */
7
+ export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "silent";
8
+ export interface LogOptions {
9
+ level?: LogLevel;
10
+ /** Optional file path. When set, logs go to the file instead of stderr. */
11
+ file?: string;
12
+ }
13
+ export declare function createLogger(opts?: LogOptions): pino.Logger;
14
+ export type Logger = pino.Logger;
package/dist/log.js ADDED
@@ -0,0 +1,9 @@
1
+ import pino from "pino";
2
+ export function createLogger(opts = {}) {
3
+ const level = opts.level ?? "info";
4
+ const destination = opts.file
5
+ ? pino.destination({ dest: opts.file, sync: false })
6
+ : pino.destination(2); // fd 2 = stderr
7
+ return pino({ level }, destination);
8
+ }
9
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAexB,MAAM,UAAU,YAAY,CAAC,OAAmB,EAAE;IAChD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;IACnC,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI;QAC3B,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACpD,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,gBAAgB;IACzC,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,WAAW,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Logger } from "../log.js";
2
+ /**
3
+ * Loopback HTTP server (127.0.0.1 only) that captures the OAuth authorization
4
+ * code redirect. Bound to a fixed port so the registered redirect_uri stays
5
+ * byte-identical across runs.
6
+ */
7
+ export declare class CallbackServer {
8
+ private readonly preferredPort;
9
+ private readonly expectedState;
10
+ private readonly log;
11
+ private server?;
12
+ private pending?;
13
+ private boundPort?;
14
+ constructor(preferredPort: number | undefined, expectedState: () => string | undefined, log: Logger);
15
+ /** Begin listening. Resolves to the actual bound port. */
16
+ listen(): Promise<number>;
17
+ get port(): number;
18
+ get redirectUrl(): string;
19
+ /** Resolves with the authorization code once the browser redirect arrives. */
20
+ waitForCode(timeoutMs: number): Promise<string>;
21
+ /** Settle the pending waitForCode exactly once, clearing its timeout. */
22
+ private settle;
23
+ private handle;
24
+ private fail;
25
+ close(): Promise<void>;
26
+ }
27
+ /** Open a URL in the system browser without an extra dependency. */
28
+ export declare function openBrowser(url: string, log: Logger): void;
@@ -0,0 +1,131 @@
1
+ import http from "node:http";
2
+ import { spawn } from "node:child_process";
3
+ const CALLBACK_PATH = "/callback";
4
+ const SUCCESS_HTML = "<!doctype html><meta charset=utf-8><title>Authorized</title>" +
5
+ "<body style='font-family:system-ui;padding:3rem;text-align:center'>" +
6
+ "<h2>Authorization complete</h2><p>You can close this tab and return to the terminal.</p>";
7
+ /**
8
+ * Loopback HTTP server (127.0.0.1 only) that captures the OAuth authorization
9
+ * code redirect. Bound to a fixed port so the registered redirect_uri stays
10
+ * byte-identical across runs.
11
+ */
12
+ export class CallbackServer {
13
+ preferredPort;
14
+ expectedState;
15
+ log;
16
+ server;
17
+ pending;
18
+ boundPort;
19
+ constructor(preferredPort, expectedState, log) {
20
+ this.preferredPort = preferredPort;
21
+ this.expectedState = expectedState;
22
+ this.log = log;
23
+ }
24
+ /** Begin listening. Resolves to the actual bound port. */
25
+ async listen() {
26
+ if (this.server)
27
+ return this.boundPort;
28
+ const server = http.createServer((req, res) => this.handle(req, res));
29
+ // Prefer an explicit/stored port; otherwise reuse the port from a previous
30
+ // bind in this process so the registered redirect_uri stays stable.
31
+ const target = this.preferredPort ?? this.boundPort ?? 0;
32
+ await new Promise((resolve, reject) => {
33
+ server.once("error", reject);
34
+ server.listen(target, "127.0.0.1", resolve);
35
+ });
36
+ this.server = server;
37
+ this.boundPort = server.address().port;
38
+ this.log.debug({ port: this.boundPort }, "callback server listening");
39
+ return this.boundPort;
40
+ }
41
+ get port() {
42
+ if (this.boundPort === undefined)
43
+ throw new Error("callback server not listening");
44
+ return this.boundPort;
45
+ }
46
+ get redirectUrl() {
47
+ return `http://127.0.0.1:${this.port}${CALLBACK_PATH}`;
48
+ }
49
+ /** Resolves with the authorization code once the browser redirect arrives. */
50
+ waitForCode(timeoutMs) {
51
+ return new Promise((resolve, reject) => {
52
+ const timer = setTimeout(() => {
53
+ this.settle(new Error(`timed out after ${Math.round(timeoutMs / 1000)}s waiting for authorization`));
54
+ }, timeoutMs);
55
+ timer.unref();
56
+ this.pending = { resolve, reject, timer };
57
+ });
58
+ }
59
+ /** Settle the pending waitForCode exactly once, clearing its timeout. */
60
+ settle(result) {
61
+ const p = this.pending;
62
+ if (!p)
63
+ return;
64
+ this.pending = undefined;
65
+ clearTimeout(p.timer);
66
+ if (result instanceof Error)
67
+ p.reject(result);
68
+ else
69
+ p.resolve(result.code);
70
+ }
71
+ handle(req, res) {
72
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${this.boundPort}`);
73
+ if (url.pathname !== CALLBACK_PATH) {
74
+ res.writeHead(404).end();
75
+ return;
76
+ }
77
+ const error = url.searchParams.get("error");
78
+ const code = url.searchParams.get("code");
79
+ const state = url.searchParams.get("state");
80
+ const expected = this.expectedState();
81
+ if (error) {
82
+ const desc = url.searchParams.get("error_description") ?? "";
83
+ this.fail(res, 400, `Authorization failed: ${error} ${desc}`.trim());
84
+ return;
85
+ }
86
+ if (expected !== undefined && state !== expected) {
87
+ this.fail(res, 400, "State mismatch — possible CSRF, request rejected.");
88
+ return;
89
+ }
90
+ if (!code) {
91
+ this.fail(res, 400, "Missing authorization code.");
92
+ return;
93
+ }
94
+ res.writeHead(200, { "content-type": "text/html" }).end(SUCCESS_HTML);
95
+ this.settle({ code });
96
+ }
97
+ fail(res, status, message) {
98
+ res.writeHead(status, { "content-type": "text/plain" }).end(message);
99
+ this.settle(new Error(message));
100
+ }
101
+ async close() {
102
+ // Don't leave a waitForCode() awaiter hanging if we shut down mid-flow.
103
+ this.settle(new Error("callback server closed before authorization completed"));
104
+ if (!this.server)
105
+ return;
106
+ await new Promise((resolve) => this.server.close(() => resolve()));
107
+ this.server = undefined;
108
+ }
109
+ }
110
+ /** Open a URL in the system browser without an extra dependency. */
111
+ export function openBrowser(url, log) {
112
+ const platform = process.platform;
113
+ const isWin = platform === "win32";
114
+ const cmd = platform === "darwin" ? "open" : isWin ? "cmd" : "xdg-open";
115
+ // On Windows the URL must be quoted: cmd.exe treats `&` (always present in
116
+ // OAuth query strings) as a command separator, which would truncate the URL.
117
+ const args = isWin ? ["/c", "start", '""', `"${url}"`] : [url];
118
+ try {
119
+ const child = spawn(cmd, args, {
120
+ stdio: "ignore",
121
+ detached: true,
122
+ windowsVerbatimArguments: isWin,
123
+ });
124
+ child.on("error", (err) => log.warn({ err }, "failed to launch browser"));
125
+ child.unref();
126
+ }
127
+ catch (err) {
128
+ log.warn({ err }, "failed to launch browser");
129
+ }
130
+ }
131
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/oauth/callback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * RFC 8707 canonical resource URI for an MCP server.
3
+ *
4
+ * Lowercase scheme + host, no fragment, default ports dropped, and no trailing
5
+ * slash on the root path. Used both as the `resource` indicator value and as the
6
+ * key for per-server credential storage.
7
+ */
8
+ export declare function canonicalResourceUri(url: URL): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * RFC 8707 canonical resource URI for an MCP server.
3
+ *
4
+ * Lowercase scheme + host, no fragment, default ports dropped, and no trailing
5
+ * slash on the root path. Used both as the `resource` indicator value and as the
6
+ * key for per-server credential storage.
7
+ */
8
+ export function canonicalResourceUri(url) {
9
+ const u = new URL(url.href);
10
+ u.hash = "";
11
+ u.username = "";
12
+ u.password = "";
13
+ u.protocol = u.protocol.toLowerCase();
14
+ u.hostname = u.hostname.toLowerCase();
15
+ // Drop default ports.
16
+ if ((u.protocol === "http:" && u.port === "80") ||
17
+ (u.protocol === "https:" && u.port === "443")) {
18
+ u.port = "";
19
+ }
20
+ let out = u.toString();
21
+ // Drop a lone trailing slash on the root (but keep meaningful paths intact).
22
+ if (u.pathname === "/" && !u.search) {
23
+ out = out.replace(/\/$/, "");
24
+ }
25
+ return out;
26
+ }
27
+ //# sourceMappingURL=canonical.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.js","sourceRoot":"","sources":["../../src/oauth/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAQ;IAC3C,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IACZ,CAAC,CAAC,QAAQ,GAAG,EAAE,CAAC;IAChB,CAAC,CAAC,QAAQ,GAAG,EAAE,CAAC;IAChB,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACtC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACtC,sBAAsB;IACtB,IACE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;QAC3C,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,EAC7C,CAAC;QACD,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IACd,CAAC;IACD,IAAI,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;IACvB,6EAA6E;IAC7E,IAAI,CAAC,CAAC,QAAQ,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QACpC,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
2
+ import type { OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
3
+ import type { Config } from "../config.js";
4
+ import type { Logger } from "../log.js";
5
+ import { CallbackServer } from "./callback.js";
6
+ /**
7
+ * OAuth 2.1 client provider for one remote MCP server. The SDK's `auth()` helper
8
+ * drives discovery (RFC 9728 → RFC 8414), Dynamic Client Registration (RFC 7591),
9
+ * PKCE, and token exchange; this class supplies persistence, the redirect URL,
10
+ * and the browser hand-off. RFC 8707 `resource` binding is handled by the SDK.
11
+ */
12
+ export declare class GatewayOAuthProvider implements OAuthClientProvider {
13
+ private readonly config;
14
+ private readonly store;
15
+ readonly callback: CallbackServer;
16
+ private readonly log;
17
+ private currentState?;
18
+ private constructor();
19
+ /** Async factory: loads persisted state and binds the loopback callback port. */
20
+ static create(config: Config, log: Logger): Promise<GatewayOAuthProvider>;
21
+ get redirectUrl(): string;
22
+ get clientMetadata(): OAuthClientMetadata;
23
+ state(): string;
24
+ clientInformation(): Promise<OAuthClientInformationFull | undefined>;
25
+ saveClientInformation(info: OAuthClientInformationFull): Promise<void>;
26
+ tokens(): Promise<OAuthTokens | undefined>;
27
+ saveTokens(tokens: OAuthTokens): Promise<void>;
28
+ saveCodeVerifier(codeVerifier: string): Promise<void>;
29
+ codeVerifier(): Promise<string>;
30
+ redirectToAuthorization(authorizationUrl: URL): Promise<void>;
31
+ invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery"): Promise<void>;
32
+ }
@@ -0,0 +1,105 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { canonicalResourceUri } from "./canonical.js";
3
+ import { CallbackServer, openBrowser } from "./callback.js";
4
+ import { AuthStore } from "./store.js";
5
+ /**
6
+ * OAuth 2.1 client provider for one remote MCP server. The SDK's `auth()` helper
7
+ * drives discovery (RFC 9728 → RFC 8414), Dynamic Client Registration (RFC 7591),
8
+ * PKCE, and token exchange; this class supplies persistence, the redirect URL,
9
+ * and the browser hand-off. RFC 8707 `resource` binding is handled by the SDK.
10
+ */
11
+ export class GatewayOAuthProvider {
12
+ config;
13
+ store;
14
+ callback;
15
+ log;
16
+ currentState;
17
+ constructor(config, store, callback, log) {
18
+ this.config = config;
19
+ this.store = store;
20
+ this.callback = callback;
21
+ this.log = log;
22
+ }
23
+ /** Async factory: loads persisted state and binds the loopback callback port. */
24
+ static async create(config, log) {
25
+ const canonical = canonicalResourceUri(config.url);
26
+ const store = new AuthStore(config.tokenStoreDir, canonical, log);
27
+ const stored = await store.load();
28
+ const port = config.callbackPort ?? stored.redirectPort;
29
+ let provider;
30
+ const callback = new CallbackServer(port, () => provider.currentState, log);
31
+ provider = new GatewayOAuthProvider(config, store, callback, log);
32
+ return provider;
33
+ }
34
+ get redirectUrl() {
35
+ return this.callback.redirectUrl;
36
+ }
37
+ get clientMetadata() {
38
+ return {
39
+ redirect_uris: [this.callback.redirectUrl],
40
+ token_endpoint_auth_method: this.config.clientSecret ? "client_secret_post" : "none",
41
+ grant_types: ["authorization_code", "refresh_token"],
42
+ response_types: ["code"],
43
+ client_name: this.config.clientName,
44
+ ...(this.config.scope ? { scope: this.config.scope } : {}),
45
+ };
46
+ }
47
+ state() {
48
+ this.currentState = randomUUID();
49
+ return this.currentState;
50
+ }
51
+ async clientInformation() {
52
+ // Statically configured client takes precedence over any DCR result.
53
+ if (this.config.clientId) {
54
+ return {
55
+ client_id: this.config.clientId,
56
+ ...(this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}),
57
+ ...this.clientMetadata,
58
+ };
59
+ }
60
+ return (await this.store.load()).clientInformation;
61
+ }
62
+ async saveClientInformation(info) {
63
+ if (!this.config.dcr) {
64
+ throw new Error("Dynamic Client Registration is disabled (--no-dcr) and no --client-id was provided");
65
+ }
66
+ this.log.info({ clientId: info.client_id }, "registered OAuth client via DCR");
67
+ await this.store.patch({ clientInformation: info, redirectPort: this.callback.port });
68
+ }
69
+ async tokens() {
70
+ return (await this.store.load()).tokens;
71
+ }
72
+ async saveTokens(tokens) {
73
+ this.log.info("stored OAuth tokens");
74
+ await this.store.patch({ tokens });
75
+ }
76
+ async saveCodeVerifier(codeVerifier) {
77
+ await this.store.patch({ codeVerifier });
78
+ }
79
+ async codeVerifier() {
80
+ const verifier = (await this.store.load()).codeVerifier;
81
+ if (!verifier)
82
+ throw new Error("missing PKCE code verifier");
83
+ return verifier;
84
+ }
85
+ async redirectToAuthorization(authorizationUrl) {
86
+ // Start the loopback listener before launching the browser so the redirect
87
+ // can never arrive before we are listening.
88
+ await this.callback.listen();
89
+ if (this.config.openBrowser) {
90
+ this.log.info("opening browser for authorization");
91
+ openBrowser(authorizationUrl.href, this.log);
92
+ // stderr so the local client's stdout protocol stream stays clean.
93
+ process.stderr.write(`\nIf your browser did not open, visit:\n${authorizationUrl.href}\n\n`);
94
+ }
95
+ else {
96
+ process.stderr.write(`\nAuthorize this gateway by visiting:\n${authorizationUrl.href}\n\n`);
97
+ }
98
+ }
99
+ async invalidateCredentials(scope) {
100
+ if (scope === "discovery")
101
+ return; // discovery state isn't persisted here
102
+ await this.store.clear(scope);
103
+ }
104
+ }
105
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/oauth/provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,OAAO,oBAAoB;IAIZ;IACA;IACR;IACQ;IANX,YAAY,CAAU;IAE9B,YACmB,MAAc,EACd,KAAgB,EACxB,QAAwB,EAChB,GAAW;QAHX,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAW;QACxB,aAAQ,GAAR,QAAQ,CAAgB;QAChB,QAAG,GAAH,GAAG,CAAQ;IAC3B,CAAC;IAEJ,iFAAiF;IACjF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,GAAW;QAC7C,MAAM,SAAS,GAAG,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,aAAa,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,YAAY,CAAC;QACxD,IAAI,QAA8B,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QAC5E,QAAQ,GAAG,IAAI,oBAAoB,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;QAClE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;IACnC,CAAC;IAED,IAAI,cAAc;QAChB,OAAO;YACL,aAAa,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;YAC1C,0BAA0B,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,MAAM;YACpF,WAAW,EAAE,CAAC,oBAAoB,EAAE,eAAe,CAAC;YACpD,cAAc,EAAE,CAAC,MAAM,CAAC;YACxB,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU;YACnC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D,CAAC;IACJ,CAAC;IAED,KAAK;QACH,IAAI,CAAC,YAAY,GAAG,UAAU,EAAE,CAAC;QACjC,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,qEAAqE;QACrE,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACzB,OAAO;gBACL,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAC/B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChF,GAAG,IAAI,CAAC,cAAc;aACvB,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,iBAAiB,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,IAAgC;QAC1D,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,oFAAoF,CAAC,CAAC;QACxG,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,iCAAiC,CAAC,CAAC;QAC/E,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACxF,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,MAAmB;QAClC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QACrC,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,YAAoB;QACzC,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC;QACxD,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QAC7D,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,gBAAqB;QACjD,2EAA2E;QAC3E,4CAA4C;QAC5C,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;YAC5B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;YACnD,WAAW,CAAC,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YAC7C,mEAAmE;YACnE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2CAA2C,gBAAgB,CAAC,IAAI,MAAM,CAAC,CAAC;QAC/F,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0CAA0C,gBAAgB,CAAC,IAAI,MAAM,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,KAA6D;QACvF,IAAI,KAAK,KAAK,WAAW;YAAE,OAAO,CAAC,uCAAuC;QAC1E,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;CACF"}
@@ -0,0 +1,35 @@
1
+ import type { OAuthClientInformationFull, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
2
+ import type { Logger } from "../log.js";
3
+ /** Everything persisted for one remote MCP server, keyed by its canonical URI. */
4
+ export interface StoredAuth {
5
+ /** DCR result (or full info for a statically configured client). */
6
+ clientInformation?: OAuthClientInformationFull;
7
+ tokens?: OAuthTokens;
8
+ /** PKCE verifier persisted between redirect and token exchange. */
9
+ codeVerifier?: string;
10
+ /**
11
+ * Loopback callback port used for the registered redirect_uri. Persisted so
12
+ * the redirect_uri stays byte-identical across runs (exact-match requirement).
13
+ */
14
+ redirectPort?: number;
15
+ }
16
+ /**
17
+ * File-backed credential store. One JSON file per server under a 0700 directory,
18
+ * each written 0600. Tolerates a missing/corrupt file by treating it as empty.
19
+ */
20
+ export declare class AuthStore {
21
+ private readonly dir;
22
+ private readonly log;
23
+ private readonly file;
24
+ private cache;
25
+ /** Serializes read-modify-write cycles so concurrent patches don't lose updates. */
26
+ private writeChain;
27
+ constructor(dir: string, canonicalUri: string, log: Logger);
28
+ load(): Promise<StoredAuth>;
29
+ private save;
30
+ /** Run a read-modify-write cycle serialized against all other mutations. */
31
+ private mutate;
32
+ patch(update: Partial<StoredAuth>): Promise<void>;
33
+ /** Remove credentials by scope; used by invalidateCredentials. */
34
+ clear(scope: "all" | "client" | "tokens" | "verifier"): Promise<void>;
35
+ }
@@ -0,0 +1,68 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ /**
5
+ * File-backed credential store. One JSON file per server under a 0700 directory,
6
+ * each written 0600. Tolerates a missing/corrupt file by treating it as empty.
7
+ */
8
+ export class AuthStore {
9
+ dir;
10
+ log;
11
+ file;
12
+ cache;
13
+ /** Serializes read-modify-write cycles so concurrent patches don't lose updates. */
14
+ writeChain = Promise.resolve();
15
+ constructor(dir, canonicalUri, log) {
16
+ this.dir = dir;
17
+ this.log = log;
18
+ const key = createHash("sha256").update(canonicalUri).digest("hex").slice(0, 32);
19
+ this.file = path.join(dir, `${key}.json`);
20
+ }
21
+ async load() {
22
+ if (this.cache)
23
+ return this.cache;
24
+ try {
25
+ const raw = await fs.readFile(this.file, "utf8");
26
+ this.cache = JSON.parse(raw);
27
+ }
28
+ catch (err) {
29
+ if (err.code !== "ENOENT") {
30
+ this.log.warn({ err, file: this.file }, "ignoring unreadable auth store file");
31
+ }
32
+ this.cache = {};
33
+ }
34
+ return this.cache;
35
+ }
36
+ async save() {
37
+ await fs.mkdir(this.dir, { recursive: true, mode: 0o700 });
38
+ const tmp = `${this.file}.tmp`;
39
+ await fs.writeFile(tmp, JSON.stringify(this.cache ?? {}, null, 2), { mode: 0o600 });
40
+ await fs.rename(tmp, this.file);
41
+ }
42
+ /** Run a read-modify-write cycle serialized against all other mutations. */
43
+ mutate(fn) {
44
+ const next = this.writeChain.then(async () => {
45
+ this.cache = fn(await this.load());
46
+ await this.save();
47
+ });
48
+ // Keep the chain alive even if one mutation rejects.
49
+ this.writeChain = next.catch(() => { });
50
+ return next;
51
+ }
52
+ async patch(update) {
53
+ return this.mutate((current) => ({ ...current, ...update }));
54
+ }
55
+ /** Remove credentials by scope; used by invalidateCredentials. */
56
+ async clear(scope) {
57
+ return this.mutate((current) => {
58
+ if (scope === "all")
59
+ return {};
60
+ if (scope === "client")
61
+ return { ...current, clientInformation: undefined };
62
+ if (scope === "tokens")
63
+ return { ...current, tokens: undefined };
64
+ return { ...current, codeVerifier: undefined };
65
+ });
66
+ }
67
+ }
68
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
3
+ import type { Config } from "./config.js";
4
+ /**
5
+ * Builds the upstream Streamable-HTTP client transport that talks to the remote
6
+ * MCP server. When an `authProvider` is supplied, the SDK drives OAuth discovery,
7
+ * DCR, token refresh, and the RFC 8707 resource parameter automatically.
8
+ */
9
+ export declare function createUpstreamTransport(config: Config, authProvider?: OAuthClientProvider): StreamableHTTPClientTransport;
@@ -0,0 +1,15 @@
1
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2
+ /**
3
+ * Builds the upstream Streamable-HTTP client transport that talks to the remote
4
+ * MCP server. When an `authProvider` is supplied, the SDK drives OAuth discovery,
5
+ * DCR, token refresh, and the RFC 8707 resource parameter automatically.
6
+ */
7
+ export function createUpstreamTransport(config, authProvider) {
8
+ return new StreamableHTTPClientTransport(config.url, {
9
+ authProvider,
10
+ requestInit: {
11
+ headers: config.headers,
12
+ },
13
+ });
14
+ }
15
+ //# sourceMappingURL=upstream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream.js","sourceRoot":"","sources":["../src/upstream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAInG;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAc,EACd,YAAkC;IAElC,OAAO,IAAI,6BAA6B,CAAC,MAAM,CAAC,GAAG,EAAE;QACnD,YAAY;QACZ,WAAW,EAAE;YACX,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB;KACF,CAAC,CAAC;AACL,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@xsreality/mcp-gateway",
3
+ "version": "0.1.0",
4
+ "description": "CLI gateway that exposes a local STDIO MCP endpoint and proxies a remote Streamable-HTTP MCP server (OAuth 2.1 + DCR).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Abhinav Sonkar",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/xsreality/mcp-gateway.git"
11
+ },
12
+ "homepage": "https://github.com/xsreality/mcp-gateway#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/xsreality/mcp-gateway/issues"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "gateway",
20
+ "proxy",
21
+ "stdio",
22
+ "streamable-http",
23
+ "oauth",
24
+ "oauth2",
25
+ "dynamic-client-registration",
26
+ "cli"
27
+ ],
28
+ "bin": {
29
+ "mcp-gateway": "dist/cli.js"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "scripts": {
41
+ "prepare": "npm run build",
42
+ "build": "tsc -p tsconfig.json",
43
+ "dev": "tsc -p tsconfig.json --watch",
44
+ "start": "node dist/cli.js",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit",
46
+ "test": "npm run build && node test/manual-relay.mjs && node test/manual-oauth.mjs"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.29.0",
50
+ "commander": "^13.0.0",
51
+ "pino": "^9.6.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.10.0",
55
+ "typescript": "^5.7.0"
56
+ }
57
+ }