@xsreality/mcp-gateway 0.1.0 → 0.2.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/README.md +13 -6
- package/dist/cli.js +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -1
- package/dist/config.js.map +1 -1
- package/dist/oauth/keychain.d.ts +30 -0
- package/dist/oauth/keychain.js +46 -0
- package/dist/oauth/keychain.js.map +1 -0
- package/dist/oauth/provider.js +2 -2
- package/dist/oauth/provider.js.map +1 -1
- package/dist/oauth/store-factory.d.ts +14 -0
- package/dist/oauth/store-factory.js +48 -0
- package/dist/oauth/store-factory.js.map +1 -0
- package/dist/oauth/store.d.ts +34 -5
- package/dist/oauth/store.js +64 -23
- package/dist/oauth/store.js.map +1 -1
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Use it to connect stdio-only MCP clients (Claude Desktop, IDE MCP integrations,
|
|
|
9
9
|
MCP server as a subprocess) to remote, OAuth-protected MCP servers that those clients can't reach directly.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
┌──────────────┐ stdio ┌──────────────┐
|
|
12
|
+
┌──────────────┐ stdio ┌──────────────┐ Streamable HTTP + OAuth ┌──────────────┐
|
|
13
13
|
│ MCP client │ ─────────▶ │ mcp-gateway │ ─────────────────────────▶ │ Remote MCP │
|
|
14
14
|
│ (Claude etc.)│ ◀───────── │ │ ◀───────────────────────── │ server │
|
|
15
15
|
└──────────────┘ └──────────────┘ └──────────────┘
|
|
@@ -83,7 +83,8 @@ Point your client at the `mcp-gateway` command:
|
|
|
83
83
|
| `--no-dcr` | Disable Dynamic Client Registration (requires `--client-id`) | DCR enabled |
|
|
84
84
|
| `--callback-port <port>` | Fixed loopback port for the OAuth redirect | auto (persisted) |
|
|
85
85
|
| `--auth-timeout <seconds>` | How long to wait for you to finish authorizing in the browser | `300` |
|
|
86
|
-
| `--
|
|
86
|
+
| `--credential-store <mode>` | Where credentials persist: `keychain` (OS keychain), `file` (on-disk), or `auto` | `auto` (`$MCP_GATEWAY_CREDENTIAL_STORE`) |
|
|
87
|
+
| `--token-store <dir>` | Directory for `file`-stored tokens + client registration | `~/.mcp-gateway` |
|
|
87
88
|
| `--no-browser` | Print the authorization URL instead of opening a browser (headless) | opens browser |
|
|
88
89
|
| `--log-level <level>` | `trace` `debug` `info` `warn` `error` `silent` (stderr/file only) | `info` |
|
|
89
90
|
| `--log-file <path>` | Write logs to a file instead of stderr | stderr |
|
|
@@ -105,9 +106,14 @@ config blocks.
|
|
|
105
106
|
|
|
106
107
|
### Where credentials live
|
|
107
108
|
|
|
108
|
-
Tokens, the registered client, and the chosen callback port are stored
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
Tokens, the registered client, and the chosen callback port are stored per server, keyed by the server's
|
|
110
|
+
canonical URL. By default (`--credential-store auto`) they go into the **OS keychain** (macOS Keychain,
|
|
111
|
+
Windows Credential Manager, Linux Secret Service) under the service name `mcp-gateway`, falling back to
|
|
112
|
+
on-disk JSON when no keychain is reachable (headless Linux, CI). The first time the keychain is used, any
|
|
113
|
+
existing `~/.mcp-gateway` file for that server is migrated into it and the plaintext file deleted.
|
|
114
|
+
|
|
115
|
+
Force a backend with `--credential-store keychain` or `--credential-store file`. In `file` mode the blob is
|
|
116
|
+
one JSON file per server under `--token-store` (default `~/.mcp-gateway`), written `0600`.
|
|
111
117
|
|
|
112
118
|
## Logging
|
|
113
119
|
|
|
@@ -119,7 +125,8 @@ a file.
|
|
|
119
125
|
|
|
120
126
|
- **Browser didn't open** — copy the URL printed on stderr, or run with `--no-browser`.
|
|
121
127
|
- **`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
|
|
128
|
+
- **Re-authorize from scratch** — in `file` mode delete the server's file under `~/.mcp-gateway`; in keychain
|
|
129
|
+
mode delete the `mcp-gateway` entry for that server URL from your OS keychain.
|
|
123
130
|
- **Corporate proxy / extra auth** — forward static headers with repeated `--header "Key: value"`.
|
|
124
131
|
- **Stuck after the server changed its auth** — clear the token store; cached discovery/registration may be stale.
|
|
125
132
|
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ConfigError, defaultTokenStoreDir, parseHeaders, parseUrl, } from "./co
|
|
|
4
4
|
import { createLogger } from "./log.js";
|
|
5
5
|
import { Gateway } from "./gateway.js";
|
|
6
6
|
const LOG_LEVELS = ["trace", "debug", "info", "warn", "error", "silent"];
|
|
7
|
+
const CREDENTIAL_STORES = ["auto", "keychain", "file"];
|
|
7
8
|
function collect(value, previous) {
|
|
8
9
|
return [...previous, value];
|
|
9
10
|
}
|
|
@@ -29,7 +30,10 @@ function buildProgram() {
|
|
|
29
30
|
.option("--no-dcr", "Disable Dynamic Client Registration")
|
|
30
31
|
.option("--callback-port <port>", "Fixed loopback OAuth callback port", intArg("--callback-port"))
|
|
31
32
|
.option("--auth-timeout <seconds>", "Max wait for browser authorization", intArg("--auth-timeout"), 300)
|
|
32
|
-
.
|
|
33
|
+
.addOption(new Option("--credential-store <mode>", "Where to persist credentials: keychain (OS keychain), file (on-disk), or auto")
|
|
34
|
+
.choices(CREDENTIAL_STORES)
|
|
35
|
+
.default(process.env.MCP_GATEWAY_CREDENTIAL_STORE ?? "auto"))
|
|
36
|
+
.option("--token-store <dir>", "Directory for file-stored tokens + client registration", defaultTokenStoreDir())
|
|
33
37
|
.option("--no-browser", "Print the authorization URL instead of opening a browser")
|
|
34
38
|
.addOption(new Option("--log-level <level>", "Log verbosity (stderr/file only)")
|
|
35
39
|
.choices(LOG_LEVELS)
|
|
@@ -43,6 +47,9 @@ function resolveConfig(opts) {
|
|
|
43
47
|
if (!LOG_LEVELS.includes(opts.logLevel)) {
|
|
44
48
|
throw new InvalidArgumentError(`invalid --log-level "${opts.logLevel}"`);
|
|
45
49
|
}
|
|
50
|
+
if (!CREDENTIAL_STORES.includes(opts.credentialStore)) {
|
|
51
|
+
throw new InvalidArgumentError(`invalid --credential-store "${opts.credentialStore}"`);
|
|
52
|
+
}
|
|
46
53
|
if (!opts.dcr && !opts.clientId) {
|
|
47
54
|
throw new ConfigError("--no-dcr requires --client-id to be provided");
|
|
48
55
|
}
|
|
@@ -56,6 +63,7 @@ function resolveConfig(opts) {
|
|
|
56
63
|
dcr: opts.dcr,
|
|
57
64
|
callbackPort: opts.callbackPort,
|
|
58
65
|
authTimeoutSec: opts.authTimeout,
|
|
66
|
+
credentialStore: opts.credentialStore,
|
|
59
67
|
tokenStoreDir: opts.tokenStore,
|
|
60
68
|
openBrowser: opts.browser,
|
|
61
69
|
logLevel: opts.logLevel,
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAEL,WAAW,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAEL,WAAW,EAEX,oBAAoB,EACpB,YAAY,EACZ,QAAQ,GACT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAiB,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAmBvC,MAAM,UAAU,GAAe,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AACrF,MAAM,iBAAiB,GAAsB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;AAE1E,SAAS,OAAO,CAAC,KAAa,EAAE,QAAkB;IAChD,OAAO,CAAC,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,MAAM,CAAC,IAAY;IAC1B,OAAO,CAAC,KAAa,EAAU,EAAE;QAC/B,MAAM,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,oBAAoB,CAAC,GAAG,IAAI,iCAAiC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,YAAY;IACnB,OAAO,IAAI,OAAO,EAAE;SACjB,IAAI,CAAC,aAAa,CAAC;SACnB,WAAW,CACV,uGAAuG,CACxG;SACA,cAAc,CACb,aAAa,EACb,4CAA4C,EAC5C,OAAO,CAAC,GAAG,CAAC,eAAe,CAC5B;SACA,MAAM,CAAC,gBAAgB,EAAE,6DAA6D,EAAE,OAAO,EAAE,EAAE,CAAC;SACpG,MAAM,CAAC,kBAAkB,EAAE,yBAAyB,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;SACpF,MAAM,CAAC,sBAAsB,EAAE,iDAAiD,EAAE,aAAa,CAAC;SAChG,MAAM,CAAC,kBAAkB,EAAE,4CAA4C,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC;SAC3G,MAAM,CAAC,0BAA0B,EAAE,oCAAoC,EAAE,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;SAC/G,MAAM,CAAC,UAAU,EAAE,qCAAqC,CAAC;SACzD,MAAM,CAAC,wBAAwB,EAAE,oCAAoC,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;SACjG,MAAM,CAAC,0BAA0B,EAAE,oCAAoC,EAAE,MAAM,CAAC,gBAAgB,CAAC,EAAE,GAAG,CAAC;SACvG,SAAS,CACR,IAAI,MAAM,CACR,2BAA2B,EAC3B,+EAA+E,CAChF;SACE,OAAO,CAAC,iBAAiB,CAAC;SAC1B,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,4BAA4B,IAAI,MAAM,CAAC,CAC/D;SACA,MAAM,CAAC,qBAAqB,EAAE,wDAAwD,EAAE,oBAAoB,EAAE,CAAC;SAC/G,MAAM,CAAC,cAAc,EAAE,0DAA0D,CAAC;SAClF,SAAS,CACR,IAAI,MAAM,CAAC,qBAAqB,EAAE,kCAAkC,CAAC;SAClE,OAAO,CAAC,UAAU,CAAC;SACnB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,MAAM,CAAC,CACxD;SACA,MAAM,CAAC,mBAAmB,EAAE,wCAAwC,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAC7G,CAAC;AAED,SAAS,aAAa,CAAC,IAAgB;IACrC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,WAAW,CAAC,4CAA4C,CAAC,CAAC;IACtE,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAoB,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,oBAAoB,CAAC,wBAAwB,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,IAAI,CAAC,eAAkC,CAAC,EAAE,CAAC;QACzE,MAAM,IAAI,oBAAoB,CAAC,+BAA+B,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChC,MAAM,IAAI,WAAW,CAAC,8CAA8C,CAAC,CAAC;IACxE,CAAC;IACD,OAAO;QACL,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC;QACvB,OAAO,EAAE,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC;QAClC,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,cAAc,EAAE,IAAI,CAAC,WAAW;QAChC,eAAe,EAAE,IAAI,CAAC,eAAkC;QACxD,aAAa,EAAE,IAAI,CAAC,UAAU;QAC9B,WAAW,EAAE,IAAI,CAAC,OAAO;QACzB,QAAQ,EAAE,IAAI,CAAC,QAAoB;QACnC,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;IAC/B,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAc,CAAC;IAExC,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3E,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAElD,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAE,EAAE;QAClC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,gCAAgC,CAAC,CAAC;QACvD,KAAK,OAAO,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IAEjD,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,gBAAgB,CAAC,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,8DAA8D;IAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { LogLevel } from "./log.js";
|
|
2
|
+
/**
|
|
3
|
+
* Where credentials are persisted:
|
|
4
|
+
* - `auto` — OS keychain when available, else file storage (default).
|
|
5
|
+
* - `keychain` — OS keychain only; error out if it's unavailable.
|
|
6
|
+
* - `file` — on-disk JSON under `tokenStoreDir`.
|
|
7
|
+
*/
|
|
8
|
+
export type CredentialStore = "auto" | "keychain" | "file";
|
|
2
9
|
/**
|
|
3
10
|
* Resolved gateway configuration.
|
|
4
11
|
*/
|
|
@@ -23,7 +30,9 @@ export interface Config {
|
|
|
23
30
|
callbackPort?: number;
|
|
24
31
|
/** Max seconds to wait for the user to complete browser authorization. */
|
|
25
32
|
authTimeoutSec: number;
|
|
26
|
-
/**
|
|
33
|
+
/** Where credentials are persisted (OS keychain vs. on-disk file). */
|
|
34
|
+
credentialStore: CredentialStore;
|
|
35
|
+
/** Directory holding per-server tokens + registration when using file storage. */
|
|
27
36
|
tokenStoreDir: string;
|
|
28
37
|
/** Open the system browser automatically (false => print the URL). */
|
|
29
38
|
openBrowser: boolean;
|
package/dist/config.js.map
CHANGED
|
@@ -1 +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;
|
|
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;AA6C7B,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,30 @@
|
|
|
1
|
+
import type { Entry as KeyringEntry } from "@napi-rs/keyring";
|
|
2
|
+
import type { Logger } from "../log.js";
|
|
3
|
+
import type { SecretBackend } from "./store.js";
|
|
4
|
+
/** Thrown when the platform keychain can't be loaded or reached. */
|
|
5
|
+
export declare class KeychainUnavailable extends Error {
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Stores one server's credential blob in the OS keychain (macOS Keychain,
|
|
9
|
+
* Windows Credential Manager, Linux Secret Service) via `@napi-rs/keyring`.
|
|
10
|
+
* The account is the server's canonical URI, so entries are recognizable in
|
|
11
|
+
* tools like Keychain Access. A missing entry reads as `undefined`.
|
|
12
|
+
*/
|
|
13
|
+
export declare class KeychainBackend implements SecretBackend {
|
|
14
|
+
private readonly entry;
|
|
15
|
+
constructor(entry: KeyringEntry);
|
|
16
|
+
read(): Promise<string | undefined>;
|
|
17
|
+
write(data: string): Promise<void>;
|
|
18
|
+
remove(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Open the keychain backend for one server. The native module is imported
|
|
22
|
+
* lazily so a missing/broken binary (or an unreachable Secret Service) never
|
|
23
|
+
* breaks file-only users — it surfaces as {@link KeychainUnavailable}. A probe
|
|
24
|
+
* read both proves the backend works and returns the current blob, sparing the
|
|
25
|
+
* caller a second read for migration.
|
|
26
|
+
*/
|
|
27
|
+
export declare function openKeychain(canonicalUri: string, log: Logger): Promise<{
|
|
28
|
+
backend: KeychainBackend;
|
|
29
|
+
current: string | undefined;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Service name under which every server's blob is filed in the OS keychain. */
|
|
2
|
+
const SERVICE = "mcp-gateway";
|
|
3
|
+
/** Thrown when the platform keychain can't be loaded or reached. */
|
|
4
|
+
export class KeychainUnavailable extends Error {
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Stores one server's credential blob in the OS keychain (macOS Keychain,
|
|
8
|
+
* Windows Credential Manager, Linux Secret Service) via `@napi-rs/keyring`.
|
|
9
|
+
* The account is the server's canonical URI, so entries are recognizable in
|
|
10
|
+
* tools like Keychain Access. A missing entry reads as `undefined`.
|
|
11
|
+
*/
|
|
12
|
+
export class KeychainBackend {
|
|
13
|
+
entry;
|
|
14
|
+
constructor(entry) {
|
|
15
|
+
this.entry = entry;
|
|
16
|
+
}
|
|
17
|
+
async read() {
|
|
18
|
+
return this.entry.getPassword() ?? undefined;
|
|
19
|
+
}
|
|
20
|
+
async write(data) {
|
|
21
|
+
this.entry.setPassword(data);
|
|
22
|
+
}
|
|
23
|
+
async remove() {
|
|
24
|
+
this.entry.deletePassword();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Open the keychain backend for one server. The native module is imported
|
|
29
|
+
* lazily so a missing/broken binary (or an unreachable Secret Service) never
|
|
30
|
+
* breaks file-only users — it surfaces as {@link KeychainUnavailable}. A probe
|
|
31
|
+
* read both proves the backend works and returns the current blob, sparing the
|
|
32
|
+
* caller a second read for migration.
|
|
33
|
+
*/
|
|
34
|
+
export async function openKeychain(canonicalUri, log) {
|
|
35
|
+
try {
|
|
36
|
+
const { Entry } = await import("@napi-rs/keyring");
|
|
37
|
+
const backend = new KeychainBackend(new Entry(SERVICE, canonicalUri));
|
|
38
|
+
const current = await backend.read();
|
|
39
|
+
return { backend, current };
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
log.debug({ err }, "OS keychain unavailable");
|
|
43
|
+
throw new KeychainUnavailable("OS keychain is unavailable", { cause: err });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=keychain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../src/oauth/keychain.ts"],"names":[],"mappings":"AAIA,gFAAgF;AAChF,MAAM,OAAO,GAAG,aAAa,CAAC;AAE9B,oEAAoE;AACpE,MAAM,OAAO,mBAAoB,SAAQ,KAAK;CAAG;AAEjD;;;;;GAKG;AACH,MAAM,OAAO,eAAe;IACG;IAA7B,YAA6B,KAAmB;QAAnB,UAAK,GAAL,KAAK,CAAc;IAAG,CAAC;IAEpD,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,SAAS,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;IAC9B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,YAAoB,EACpB,GAAW;IAEX,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,IAAI,eAAe,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;QACtE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;QACrC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,yBAAyB,CAAC,CAAC;QAC9C,MAAM,IAAI,mBAAmB,CAAC,4BAA4B,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC9E,CAAC;AACH,CAAC"}
|
package/dist/oauth/provider.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { canonicalResourceUri } from "./canonical.js";
|
|
3
3
|
import { CallbackServer, openBrowser } from "./callback.js";
|
|
4
|
-
import {
|
|
4
|
+
import { createAuthStore } from "./store-factory.js";
|
|
5
5
|
/**
|
|
6
6
|
* OAuth 2.1 client provider for one remote MCP server. The SDK's `auth()` helper
|
|
7
7
|
* drives discovery (RFC 9728 → RFC 8414), Dynamic Client Registration (RFC 7591),
|
|
@@ -23,7 +23,7 @@ export class GatewayOAuthProvider {
|
|
|
23
23
|
/** Async factory: loads persisted state and binds the loopback callback port. */
|
|
24
24
|
static async create(config, log) {
|
|
25
25
|
const canonical = canonicalResourceUri(config.url);
|
|
26
|
-
const store =
|
|
26
|
+
const store = await createAuthStore(config, canonical, log);
|
|
27
27
|
const stored = await store.load();
|
|
28
28
|
const port = config.callbackPort ?? stored.redirectPort;
|
|
29
29
|
let provider;
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/oauth/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Config } from "../config.js";
|
|
2
|
+
import type { Logger } from "../log.js";
|
|
3
|
+
import { AuthStore } from "./store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Build the credential store for one server, honoring `config.credentialStore`:
|
|
6
|
+
*
|
|
7
|
+
* - `file` — on-disk JSON under `--token-store` (legacy behavior).
|
|
8
|
+
* - `keychain` — OS keychain only; fail loud if it's unavailable.
|
|
9
|
+
* - `auto` — keychain when reachable, otherwise fall back to file storage.
|
|
10
|
+
*
|
|
11
|
+
* When the keychain is used and holds nothing yet, any legacy on-disk blob for
|
|
12
|
+
* this server is migrated into it (and the plaintext file removed) on first use.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createAuthStore(config: Config, canonicalUri: string, log: Logger): Promise<AuthStore>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { KeychainBackend, KeychainUnavailable, openKeychain } from "./keychain.js";
|
|
2
|
+
import { AuthStore, FileBackend } from "./store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build the credential store for one server, honoring `config.credentialStore`:
|
|
5
|
+
*
|
|
6
|
+
* - `file` — on-disk JSON under `--token-store` (legacy behavior).
|
|
7
|
+
* - `keychain` — OS keychain only; fail loud if it's unavailable.
|
|
8
|
+
* - `auto` — keychain when reachable, otherwise fall back to file storage.
|
|
9
|
+
*
|
|
10
|
+
* When the keychain is used and holds nothing yet, any legacy on-disk blob for
|
|
11
|
+
* this server is migrated into it (and the plaintext file removed) on first use.
|
|
12
|
+
*/
|
|
13
|
+
export async function createAuthStore(config, canonicalUri, log) {
|
|
14
|
+
if (config.credentialStore === "file") {
|
|
15
|
+
return AuthStore.file(config.tokenStoreDir, canonicalUri, log);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const { backend, current } = await openKeychain(canonicalUri, log);
|
|
19
|
+
if (current === undefined) {
|
|
20
|
+
await migrateLegacyFile(config, canonicalUri, backend, log);
|
|
21
|
+
}
|
|
22
|
+
return new AuthStore(backend, log);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
// Explicit `--credential-store keychain` must surface the failure.
|
|
26
|
+
if (config.credentialStore === "keychain" || !(err instanceof KeychainUnavailable)) {
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
log.warn({ err }, "OS keychain unavailable; falling back to file storage");
|
|
30
|
+
return AuthStore.file(config.tokenStoreDir, canonicalUri, log);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Move a pre-existing on-disk blob into the keychain. Best-effort: never fatal. */
|
|
34
|
+
async function migrateLegacyFile(config, canonicalUri, backend, log) {
|
|
35
|
+
try {
|
|
36
|
+
const file = new FileBackend(config.tokenStoreDir, canonicalUri);
|
|
37
|
+
const legacy = await file.read();
|
|
38
|
+
if (legacy === undefined)
|
|
39
|
+
return;
|
|
40
|
+
await backend.write(legacy);
|
|
41
|
+
await file.remove();
|
|
42
|
+
log.info("migrated credentials from disk to OS keychain");
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
log.warn({ err }, "failed to migrate on-disk credentials to OS keychain");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=store-factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-factory.js","sourceRoot":"","sources":["../../src/oauth/store-factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,YAAoB,EACpB,GAAW;IAEX,IAAI,MAAM,CAAC,eAAe,KAAK,MAAM,EAAE,CAAC;QACtC,OAAO,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,YAAY,EAAE,GAAG,CAAC,CAAC;IACjE,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,MAAM,YAAY,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QACnE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,iBAAiB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QAC9D,CAAC;QACD,OAAO,IAAI,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,mEAAmE;QACnE,IAAI,MAAM,CAAC,eAAe,KAAK,UAAU,IAAI,CAAC,CAAC,GAAG,YAAY,mBAAmB,CAAC,EAAE,CAAC;YACnF,MAAM,GAAG,CAAC;QACZ,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,uDAAuD,CAAC,CAAC;QAC3E,OAAO,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,YAAY,EAAE,GAAG,CAAC,CAAC;IACjE,CAAC;AACH,CAAC;AAED,oFAAoF;AACpF,KAAK,UAAU,iBAAiB,CAC9B,MAAc,EACd,YAAoB,EACpB,OAAwB,EACxB,GAAW;IAEX,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,aAAa,EAAE,YAAY,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACjC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO;QACjC,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5B,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;QACpB,GAAG,CAAC,IAAI,CAAC,+CAA+C,CAAC,CAAC;IAC5D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,sDAAsD,CAAC,CAAC;IAC5E,CAAC;AACH,CAAC"}
|
package/dist/oauth/store.d.ts
CHANGED
|
@@ -14,19 +14,48 @@ export interface StoredAuth {
|
|
|
14
14
|
redirectPort?: number;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Persistence primitive for one server's credential blob. The store layers JSON
|
|
18
|
+
* parse/merge/cache/serialization on top; a backend only moves an opaque string
|
|
19
|
+
* in and out of some medium (a file, the OS keychain, ...). `read()` returns
|
|
20
|
+
* `undefined` when nothing is stored yet.
|
|
19
21
|
*/
|
|
20
|
-
export
|
|
22
|
+
export interface SecretBackend {
|
|
23
|
+
read(): Promise<string | undefined>;
|
|
24
|
+
write(data: string): Promise<void>;
|
|
25
|
+
remove(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** Derive the 32-hex storage key shared by the file name and keychain account. */
|
|
28
|
+
export declare function storeKey(canonicalUri: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* File backend: one JSON file per server under a 0700 directory, written 0600
|
|
31
|
+
* via a temp file + atomic rename. A missing file reads as `undefined`.
|
|
32
|
+
*/
|
|
33
|
+
export declare class FileBackend implements SecretBackend {
|
|
21
34
|
private readonly dir;
|
|
22
|
-
private readonly log;
|
|
23
35
|
private readonly file;
|
|
36
|
+
constructor(dir: string, canonicalUri: string);
|
|
37
|
+
read(): Promise<string | undefined>;
|
|
38
|
+
write(data: string): Promise<void>;
|
|
39
|
+
remove(): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Credential store over a {@link SecretBackend}. Holds the in-memory cache,
|
|
43
|
+
* serializes read-modify-write cycles, and tolerates an unreadable/corrupt blob
|
|
44
|
+
* by treating it as empty.
|
|
45
|
+
*/
|
|
46
|
+
export declare class AuthStore {
|
|
47
|
+
private readonly backend;
|
|
48
|
+
private readonly log;
|
|
24
49
|
private cache;
|
|
25
50
|
/** Serializes read-modify-write cycles so concurrent patches don't lose updates. */
|
|
26
51
|
private writeChain;
|
|
27
|
-
constructor(
|
|
52
|
+
constructor(backend: SecretBackend, log: Logger);
|
|
53
|
+
/** Convenience constructor for the on-disk backend. */
|
|
54
|
+
static file(dir: string, canonicalUri: string, log: Logger): AuthStore;
|
|
28
55
|
load(): Promise<StoredAuth>;
|
|
29
56
|
private save;
|
|
57
|
+
/** Append a unit of work to the serialized chain, surfacing its result to the caller. */
|
|
58
|
+
private enqueue;
|
|
30
59
|
/** Run a read-modify-write cycle serialized against all other mutations. */
|
|
31
60
|
private mutate;
|
|
32
61
|
patch(update: Partial<StoredAuth>): Promise<void>;
|
package/dist/oauth/store.js
CHANGED
|
@@ -1,62 +1,103 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
/** Derive the 32-hex storage key shared by the file name and keychain account. */
|
|
5
|
+
export function storeKey(canonicalUri) {
|
|
6
|
+
return createHash("sha256").update(canonicalUri).digest("hex").slice(0, 32);
|
|
7
|
+
}
|
|
4
8
|
/**
|
|
5
|
-
* File
|
|
6
|
-
*
|
|
9
|
+
* File backend: one JSON file per server under a 0700 directory, written 0600
|
|
10
|
+
* via a temp file + atomic rename. A missing file reads as `undefined`.
|
|
7
11
|
*/
|
|
8
|
-
export class
|
|
12
|
+
export class FileBackend {
|
|
9
13
|
dir;
|
|
10
|
-
log;
|
|
11
14
|
file;
|
|
15
|
+
constructor(dir, canonicalUri) {
|
|
16
|
+
this.dir = dir;
|
|
17
|
+
this.file = path.join(dir, `${storeKey(canonicalUri)}.json`);
|
|
18
|
+
}
|
|
19
|
+
async read() {
|
|
20
|
+
try {
|
|
21
|
+
return await fs.readFile(this.file, "utf8");
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err.code === "ENOENT")
|
|
25
|
+
return undefined;
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async write(data) {
|
|
30
|
+
await fs.mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
31
|
+
const tmp = `${this.file}.tmp`;
|
|
32
|
+
await fs.writeFile(tmp, data, { mode: 0o600 });
|
|
33
|
+
await fs.rename(tmp, this.file);
|
|
34
|
+
}
|
|
35
|
+
async remove() {
|
|
36
|
+
await fs.rm(this.file, { force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Credential store over a {@link SecretBackend}. Holds the in-memory cache,
|
|
41
|
+
* serializes read-modify-write cycles, and tolerates an unreadable/corrupt blob
|
|
42
|
+
* by treating it as empty.
|
|
43
|
+
*/
|
|
44
|
+
export class AuthStore {
|
|
45
|
+
backend;
|
|
46
|
+
log;
|
|
12
47
|
cache;
|
|
13
48
|
/** Serializes read-modify-write cycles so concurrent patches don't lose updates. */
|
|
14
49
|
writeChain = Promise.resolve();
|
|
15
|
-
constructor(
|
|
16
|
-
this.
|
|
50
|
+
constructor(backend, log) {
|
|
51
|
+
this.backend = backend;
|
|
17
52
|
this.log = log;
|
|
18
|
-
|
|
19
|
-
|
|
53
|
+
}
|
|
54
|
+
/** Convenience constructor for the on-disk backend. */
|
|
55
|
+
static file(dir, canonicalUri, log) {
|
|
56
|
+
return new AuthStore(new FileBackend(dir, canonicalUri), log);
|
|
20
57
|
}
|
|
21
58
|
async load() {
|
|
22
59
|
if (this.cache)
|
|
23
60
|
return this.cache;
|
|
24
61
|
try {
|
|
25
|
-
const raw = await
|
|
26
|
-
this.cache = JSON.parse(raw);
|
|
62
|
+
const raw = await this.backend.read();
|
|
63
|
+
this.cache = raw ? JSON.parse(raw) : {};
|
|
27
64
|
}
|
|
28
65
|
catch (err) {
|
|
29
|
-
|
|
30
|
-
this.log.warn({ err, file: this.file }, "ignoring unreadable auth store file");
|
|
31
|
-
}
|
|
66
|
+
this.log.warn({ err }, "ignoring unreadable auth store");
|
|
32
67
|
this.cache = {};
|
|
33
68
|
}
|
|
34
69
|
return this.cache;
|
|
35
70
|
}
|
|
36
71
|
async save() {
|
|
37
|
-
await
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
72
|
+
await this.backend.write(JSON.stringify(this.cache ?? {}, null, 2));
|
|
73
|
+
}
|
|
74
|
+
/** Append a unit of work to the serialized chain, surfacing its result to the caller. */
|
|
75
|
+
enqueue(fn) {
|
|
76
|
+
const next = this.writeChain.then(fn);
|
|
77
|
+
// Keep the chain alive even if one unit rejects.
|
|
78
|
+
this.writeChain = next.catch(() => { });
|
|
79
|
+
return next;
|
|
41
80
|
}
|
|
42
81
|
/** Run a read-modify-write cycle serialized against all other mutations. */
|
|
43
82
|
mutate(fn) {
|
|
44
|
-
|
|
83
|
+
return this.enqueue(async () => {
|
|
45
84
|
this.cache = fn(await this.load());
|
|
46
85
|
await this.save();
|
|
47
86
|
});
|
|
48
|
-
// Keep the chain alive even if one mutation rejects.
|
|
49
|
-
this.writeChain = next.catch(() => { });
|
|
50
|
-
return next;
|
|
51
87
|
}
|
|
52
88
|
async patch(update) {
|
|
53
89
|
return this.mutate((current) => ({ ...current, ...update }));
|
|
54
90
|
}
|
|
55
91
|
/** Remove credentials by scope; used by invalidateCredentials. */
|
|
56
92
|
async clear(scope) {
|
|
93
|
+
if (scope === "all") {
|
|
94
|
+
// Drop the backing entry entirely so no empty blob lingers.
|
|
95
|
+
return this.enqueue(async () => {
|
|
96
|
+
this.cache = {};
|
|
97
|
+
await this.backend.remove();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
57
100
|
return this.mutate((current) => {
|
|
58
|
-
if (scope === "all")
|
|
59
|
-
return {};
|
|
60
101
|
if (scope === "client")
|
|
61
102
|
return { ...current, clientInformation: undefined };
|
|
62
103
|
if (scope === "tokens")
|
package/dist/oauth/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth/store.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xsreality/mcp-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CLI gateway that exposes a local STDIO MCP endpoint and proxies a remote Streamable-HTTP MCP server (OAuth 2.1 + DCR).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,15 +43,23 @@
|
|
|
43
43
|
"dev": "tsc -p tsconfig.json --watch",
|
|
44
44
|
"start": "node dist/cli.js",
|
|
45
45
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
|
-
"test": "
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"test:unit": "vitest run test/unit",
|
|
49
|
+
"test:e2e": "vitest run test/e2e",
|
|
50
|
+
"coverage": "vitest run --coverage"
|
|
47
51
|
},
|
|
48
52
|
"dependencies": {
|
|
49
53
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
54
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
50
55
|
"commander": "^13.0.0",
|
|
51
56
|
"pino": "^9.6.0"
|
|
52
57
|
},
|
|
53
58
|
"devDependencies": {
|
|
54
59
|
"@types/node": "^22.10.0",
|
|
55
|
-
"
|
|
60
|
+
"@vitest/coverage-v8": "^3.2.6",
|
|
61
|
+
"typescript": "^5.7.0",
|
|
62
|
+
"vitest": "^3.2.6",
|
|
63
|
+
"zod": "^4.4.3"
|
|
56
64
|
}
|
|
57
65
|
}
|