@sonoma-security/mcp-gateway 0.1.2 → 0.1.5

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.
Files changed (67) hide show
  1. package/README.md +104 -45
  2. package/dist/__tests__/config.test.js +28 -0
  3. package/dist/__tests__/config.test.js.map +1 -1
  4. package/dist/__tests__/ssrf-protection.test.d.ts +2 -0
  5. package/dist/__tests__/ssrf-protection.test.d.ts.map +1 -0
  6. package/dist/__tests__/ssrf-protection.test.js +389 -0
  7. package/dist/__tests__/ssrf-protection.test.js.map +1 -0
  8. package/dist/auth/client.d.ts +2 -0
  9. package/dist/auth/client.d.ts.map +1 -1
  10. package/dist/auth/client.js +17 -15
  11. package/dist/auth/client.js.map +1 -1
  12. package/dist/auth/crypto.d.ts +23 -0
  13. package/dist/auth/crypto.d.ts.map +1 -0
  14. package/dist/auth/crypto.js +78 -0
  15. package/dist/auth/crypto.js.map +1 -0
  16. package/dist/auth/index.d.ts +4 -1
  17. package/dist/auth/index.d.ts.map +1 -1
  18. package/dist/auth/index.js +4 -1
  19. package/dist/auth/index.js.map +1 -1
  20. package/dist/auth/server.d.ts +2 -0
  21. package/dist/auth/server.d.ts.map +1 -1
  22. package/dist/auth/server.js +337 -59
  23. package/dist/auth/server.js.map +1 -1
  24. package/dist/auth/storage.d.ts.map +1 -1
  25. package/dist/auth/storage.js +2 -72
  26. package/dist/auth/storage.js.map +1 -1
  27. package/dist/auth/upstream-oauth-provider.d.ts +41 -0
  28. package/dist/auth/upstream-oauth-provider.d.ts.map +1 -0
  29. package/dist/auth/upstream-oauth-provider.js +88 -0
  30. package/dist/auth/upstream-oauth-provider.js.map +1 -0
  31. package/dist/auth/upstream-oauth.d.ts +31 -0
  32. package/dist/auth/upstream-oauth.d.ts.map +1 -0
  33. package/dist/auth/upstream-oauth.js +79 -0
  34. package/dist/auth/upstream-oauth.js.map +1 -0
  35. package/dist/auth/upstream-token-store.d.ts +27 -0
  36. package/dist/auth/upstream-token-store.d.ts.map +1 -0
  37. package/dist/auth/upstream-token-store.js +103 -0
  38. package/dist/auth/upstream-token-store.js.map +1 -0
  39. package/dist/cli.js +115 -86
  40. package/dist/cli.js.map +1 -1
  41. package/dist/config.d.ts +30 -1
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +203 -9
  44. package/dist/config.js.map +1 -1
  45. package/dist/gateway.d.ts +23 -1
  46. package/dist/gateway.d.ts.map +1 -1
  47. package/dist/gateway.js +224 -35
  48. package/dist/gateway.js.map +1 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/pattern-matcher.d.ts +47 -0
  54. package/dist/pattern-matcher.d.ts.map +1 -0
  55. package/dist/pattern-matcher.js +98 -0
  56. package/dist/pattern-matcher.js.map +1 -0
  57. package/dist/sonoma-client.d.ts +21 -5
  58. package/dist/sonoma-client.d.ts.map +1 -1
  59. package/dist/sonoma-client.js +42 -2
  60. package/dist/sonoma-client.js.map +1 -1
  61. package/dist/ssrf-protection.d.ts +59 -0
  62. package/dist/ssrf-protection.d.ts.map +1 -0
  63. package/dist/ssrf-protection.js +253 -0
  64. package/dist/ssrf-protection.js.map +1 -0
  65. package/dist/types.d.ts +6 -2
  66. package/dist/types.d.ts.map +1 -1
  67. package/package.json +2 -2
@@ -0,0 +1,88 @@
1
+ /**
2
+ * OAuthClientProvider implementation for upstream MCP servers.
3
+ *
4
+ * Implements the MCP SDK's OAuthClientProvider interface so the SDK's
5
+ * `auth()` orchestrator can handle the full OAuth flow (discovery, DCR,
6
+ * PKCE, authorization, token exchange, refresh) for any upstream server.
7
+ *
8
+ * Design: `redirectToAuthorization()` captures the URL instead of opening
9
+ * the browser directly. The caller (gateway) controls when/how to open it,
10
+ * enabling serial auth flows across multiple servers.
11
+ */
12
+ const CLIENT_NAME = "Sonoma MCP Gateway";
13
+ export class UpstreamOAuthProvider {
14
+ serverUrl;
15
+ store;
16
+ callbackPort;
17
+ _debug;
18
+ /** Captured authorization URL after `redirectToAuthorization` is called */
19
+ pendingAuthorizationUrl;
20
+ constructor(options) {
21
+ this.serverUrl = options.serverUrl;
22
+ this.store = options.store;
23
+ this.callbackPort = options.callbackPort;
24
+ this._debug = options.debug ?? false;
25
+ }
26
+ get redirectUrl() {
27
+ return `http://localhost:${this.callbackPort}/callback`;
28
+ }
29
+ get clientMetadata() {
30
+ return {
31
+ client_name: CLIENT_NAME,
32
+ redirect_uris: [this.redirectUrl],
33
+ grant_types: ["authorization_code", "refresh_token"],
34
+ response_types: ["code"],
35
+ token_endpoint_auth_method: "none",
36
+ };
37
+ }
38
+ async clientInformation() {
39
+ return this.store.getClientInfo(this.serverUrl);
40
+ }
41
+ async saveClientInformation(info) {
42
+ this.store.saveClientInfo(this.serverUrl, info);
43
+ }
44
+ async tokens() {
45
+ return this.store.getTokens(this.serverUrl);
46
+ }
47
+ async saveTokens(tokens) {
48
+ this.store.saveTokens(this.serverUrl, tokens);
49
+ }
50
+ async redirectToAuthorization(authorizationUrl) {
51
+ // Capture the URL; caller will open the browser
52
+ this.pendingAuthorizationUrl = authorizationUrl;
53
+ this.log("Authorization URL captured (caller will open browser)");
54
+ }
55
+ async saveCodeVerifier(verifier) {
56
+ this.store.saveCodeVerifier(this.serverUrl, verifier);
57
+ }
58
+ async codeVerifier() {
59
+ const verifier = this.store.getCodeVerifier(this.serverUrl);
60
+ if (!verifier) {
61
+ throw new Error("No code verifier found for this server");
62
+ }
63
+ return verifier;
64
+ }
65
+ async invalidateCredentials(scope) {
66
+ switch (scope) {
67
+ case "all":
68
+ this.store.clearAll(this.serverUrl);
69
+ break;
70
+ case "client":
71
+ this.store.clearClientInfo(this.serverUrl);
72
+ break;
73
+ case "tokens":
74
+ this.store.clearTokens(this.serverUrl);
75
+ break;
76
+ case "verifier":
77
+ // Code verifier is transient; clearing it is optional
78
+ break;
79
+ }
80
+ }
81
+ log(msg) {
82
+ if (this._debug) {
83
+ // Security: msg is from developer code, not user input
84
+ console.error(`[upstream-oauth] ${msg}`); // nosemgrep: unsafe-formatstring
85
+ }
86
+ }
87
+ }
88
+ //# sourceMappingURL=upstream-oauth-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-oauth-provider.js","sourceRoot":"","sources":["../../src/auth/upstream-oauth-provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * OAuth flow orchestrator for upstream MCP servers.
3
+ *
4
+ * Uses the SDK's `auth()` function to handle discovery, DCR, PKCE,
5
+ * authorization, token exchange, and refresh. Opens the browser for
6
+ * user consent when needed, using a separate callback port from
7
+ * Sonoma's own auth (19843 vs 19842).
8
+ */
9
+ import { UpstreamOAuthProvider } from "./upstream-oauth-provider.js";
10
+ import type { UpstreamTokenStore } from "./upstream-token-store.js";
11
+ export interface AuthenticateUpstreamOptions {
12
+ serverUrl: string;
13
+ serverName: string;
14
+ store: UpstreamTokenStore;
15
+ debug?: boolean;
16
+ }
17
+ /**
18
+ * Authenticate with an upstream MCP server that requires OAuth.
19
+ *
20
+ * Flow:
21
+ * 1. Create an OAuthClientProvider for this server
22
+ * 2. Call `auth()` which checks existing tokens / tries refresh
23
+ * 3. If "AUTHORIZED", tokens are ready
24
+ * 4. If "REDIRECT", open browser for user consent, wait for callback,
25
+ * then complete the auth code exchange
26
+ *
27
+ * Returns the provider (which can be passed to transports for mid-session refresh).
28
+ * Throws if authentication fails.
29
+ */
30
+ export declare function authenticateUpstream(options: AuthenticateUpstreamOptions): Promise<UpstreamOAuthProvider>;
31
+ //# sourceMappingURL=upstream-oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-oauth.d.ts","sourceRoot":"","sources":["../../src/auth/upstream-oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAMpE,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC,CA+DhC"}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * OAuth flow orchestrator for upstream MCP servers.
3
+ *
4
+ * Uses the SDK's `auth()` function to handle discovery, DCR, PKCE,
5
+ * authorization, token exchange, and refresh. Opens the browser for
6
+ * user consent when needed, using a separate callback port from
7
+ * Sonoma's own auth (19843 vs 19842).
8
+ */
9
+ import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
10
+ import { UpstreamOAuthProvider } from "./upstream-oauth-provider.js";
11
+ import { openBrowser } from "./client.js";
12
+ import { startCallbackServer } from "./server.js";
13
+ const UPSTREAM_CALLBACK_PORT = 19843;
14
+ /**
15
+ * Authenticate with an upstream MCP server that requires OAuth.
16
+ *
17
+ * Flow:
18
+ * 1. Create an OAuthClientProvider for this server
19
+ * 2. Call `auth()` which checks existing tokens / tries refresh
20
+ * 3. If "AUTHORIZED", tokens are ready
21
+ * 4. If "REDIRECT", open browser for user consent, wait for callback,
22
+ * then complete the auth code exchange
23
+ *
24
+ * Returns the provider (which can be passed to transports for mid-session refresh).
25
+ * Throws if authentication fails.
26
+ */
27
+ export async function authenticateUpstream(options) {
28
+ const { serverUrl, serverName, store, debug = false } = options;
29
+ const log = (msg) => {
30
+ if (debug) {
31
+ // Security: msg is from developer code, not user input
32
+ console.error(`[upstream-oauth] ${msg}`); // nosemgrep: unsafe-formatstring
33
+ }
34
+ };
35
+ store.setServerName(serverUrl, serverName);
36
+ const provider = new UpstreamOAuthProvider({
37
+ serverUrl,
38
+ store,
39
+ callbackPort: UPSTREAM_CALLBACK_PORT,
40
+ debug,
41
+ });
42
+ // First auth() call: checks existing tokens, tries refresh, or initiates redirect
43
+ log(`Authenticating with ${serverName} (${serverUrl})...`);
44
+ const result = await auth(provider, { serverUrl });
45
+ if (result === "AUTHORIZED") {
46
+ log(`Already authorized with ${serverName}`);
47
+ return provider;
48
+ }
49
+ // result === "REDIRECT": provider captured the authorization URL
50
+ if (!provider.pendingAuthorizationUrl) {
51
+ throw new Error(`Auth flow returned REDIRECT but no authorization URL was captured for ${serverName}`);
52
+ }
53
+ const authUrl = provider.pendingAuthorizationUrl.toString();
54
+ console.error(`\nOpening browser to authenticate with ${serverName}...`);
55
+ console.error(`If browser doesn't open, visit:\n${authUrl}\n`);
56
+ // Start callback server and open browser
57
+ const callbackPromise = startCallbackServer({
58
+ port: UPSTREAM_CALLBACK_PORT,
59
+ debug,
60
+ serverName,
61
+ });
62
+ await openBrowser(authUrl);
63
+ // Wait for the authorization code
64
+ const callbackResult = await callbackPromise;
65
+ // State validation is handled internally by the MCP SDK's auth() function.
66
+ // The provider's codeVerifier storage implicitly binds the session.
67
+ log(`Received authorization code from ${serverName}`);
68
+ // Second auth() call: exchange the authorization code for tokens
69
+ const exchangeResult = await auth(provider, {
70
+ serverUrl,
71
+ authorizationCode: callbackResult.code,
72
+ });
73
+ if (exchangeResult !== "AUTHORIZED") {
74
+ throw new Error(`Token exchange failed for ${serverName}: unexpected result "${exchangeResult}"`);
75
+ }
76
+ log(`Successfully authenticated with ${serverName}`);
77
+ return provider;
78
+ }
79
+ //# sourceMappingURL=upstream-oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-oauth.js","sourceRoot":"","sources":["../../src/auth/upstream-oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,0CAA0C,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,sBAAsB,GAAG,KAAK,CAAC;AASrC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAoC;IAEpC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,EAAE,GAAG,OAAO,CAAC;IAEhE,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE;QAC1B,IAAI,KAAK,EAAE,CAAC;YACV,uDAAuD;YACvD,OAAO,CAAC,KAAK,CAAC,oBAAoB,GAAG,EAAE,CAAC,CAAC,CAAC,iCAAiC;QAC7E,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,CAAC,aAAa,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAE3C,MAAM,QAAQ,GAAG,IAAI,qBAAqB,CAAC;QACzC,SAAS;QACT,KAAK;QACL,YAAY,EAAE,sBAAsB;QACpC,KAAK;KACN,CAAC,CAAC;IAEH,kFAAkF;IAClF,GAAG,CAAC,uBAAuB,UAAU,KAAK,SAAS,MAAM,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IAEnD,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;QAC5B,GAAG,CAAC,2BAA2B,UAAU,EAAE,CAAC,CAAC;QAC7C,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,iEAAiE;IACjE,IAAI,CAAC,QAAQ,CAAC,uBAAuB,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,yEAAyE,UAAU,EAAE,CAAC,CAAC;IACzG,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,uBAAuB,CAAC,QAAQ,EAAE,CAAC;IAC5D,OAAO,CAAC,KAAK,CAAC,0CAA0C,UAAU,KAAK,CAAC,CAAC;IACzE,OAAO,CAAC,KAAK,CAAC,oCAAoC,OAAO,IAAI,CAAC,CAAC;IAE/D,yCAAyC;IACzC,MAAM,eAAe,GAAG,mBAAmB,CAAC;QAC1C,IAAI,EAAE,sBAAsB;QAC5B,KAAK;QACL,UAAU;KACX,CAAC,CAAC;IACH,MAAM,WAAW,CAAC,OAAO,CAAC,CAAC;IAE3B,kCAAkC;IAClC,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC;IAC7C,2EAA2E;IAC3E,oEAAoE;IACpE,GAAG,CAAC,oCAAoC,UAAU,EAAE,CAAC,CAAC;IAEtD,iEAAiE;IACjE,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE;QAC1C,SAAS;QACT,iBAAiB,EAAE,cAAc,CAAC,IAAI;KACvC,CAAC,CAAC;IAEH,IAAI,cAAc,KAAK,YAAY,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,6BAA6B,UAAU,wBAAwB,cAAc,GAAG,CAAC,CAAC;IACpG,CAAC;IAED,GAAG,CAAC,mCAAmC,UAAU,EAAE,CAAC,CAAC;IACrD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Per-server token storage for upstream MCP servers that require OAuth.
3
+ *
4
+ * Stores credentials in ~/.sonoma/upstream-credentials.json, encrypted with
5
+ * the same AES-256-GCM scheme used for Sonoma gateway credentials.
6
+ * Each server is keyed by its URL origin (e.g., "https://mcp.sentry.dev").
7
+ */
8
+ import type { OAuthTokens, OAuthClientInformationMixed } from "@modelcontextprotocol/sdk/shared/auth.js";
9
+ export declare class UpstreamTokenStore {
10
+ private store;
11
+ private load;
12
+ private save;
13
+ private getServer;
14
+ getTokens(serverUrl: string): OAuthTokens | undefined;
15
+ saveTokens(serverUrl: string, tokens: OAuthTokens): void;
16
+ clearTokens(serverUrl: string): void;
17
+ getClientInfo(serverUrl: string): OAuthClientInformationMixed | undefined;
18
+ saveClientInfo(serverUrl: string, info: OAuthClientInformationMixed): void;
19
+ clearClientInfo(serverUrl: string): void;
20
+ getCodeVerifier(serverUrl: string): string | undefined;
21
+ saveCodeVerifier(serverUrl: string, verifier: string): void;
22
+ setServerName(serverUrl: string, name: string): void;
23
+ clearAll(serverUrl: string): void;
24
+ /** Clear credentials for every upstream server. */
25
+ clearAllServers(): void;
26
+ }
27
+ //# sourceMappingURL=upstream-token-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-token-store.d.ts","sourceRoot":"","sources":["../../src/auth/upstream-token-store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;AAsBzG,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAwC;IAErD,OAAO,CAAC,IAAI;IAoBZ,OAAO,CAAC,IAAI;IAOZ,OAAO,CAAC,SAAS;IASjB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAIrD,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI;IAOxD,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAMpC,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,2BAA2B,GAAG,SAAS;IAIzE,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,2BAA2B,GAAG,IAAI;IAM1E,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAMxC,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAItD,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAM3D,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAMpD,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAOjC,mDAAmD;IACnD,eAAe,IAAI,IAAI;CAIxB"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Per-server token storage for upstream MCP servers that require OAuth.
3
+ *
4
+ * Stores credentials in ~/.sonoma/upstream-credentials.json, encrypted with
5
+ * the same AES-256-GCM scheme used for Sonoma gateway credentials.
6
+ * Each server is keyed by its URL origin (e.g., "https://mcp.sentry.dev").
7
+ */
8
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { SONOMA_DIR, ensureSonomaDir, encrypt, decrypt } from "./crypto.js";
11
+ const UPSTREAM_CREDENTIALS_PATH = join(SONOMA_DIR, "upstream-credentials.json");
12
+ function getServerKey(serverUrl) {
13
+ const url = new URL(serverUrl);
14
+ return url.origin;
15
+ }
16
+ export class UpstreamTokenStore {
17
+ store = null;
18
+ load() {
19
+ if (this.store)
20
+ return this.store;
21
+ if (!existsSync(UPSTREAM_CREDENTIALS_PATH)) {
22
+ this.store = { servers: {} };
23
+ return this.store;
24
+ }
25
+ try {
26
+ const encryptedData = readFileSync(UPSTREAM_CREDENTIALS_PATH, "utf-8");
27
+ const decrypted = decrypt(encryptedData);
28
+ this.store = JSON.parse(decrypted);
29
+ return this.store;
30
+ }
31
+ catch {
32
+ // If decryption fails (e.g., machine changed), start fresh
33
+ this.store = { servers: {} };
34
+ return this.store;
35
+ }
36
+ }
37
+ save() {
38
+ ensureSonomaDir();
39
+ const data = JSON.stringify(this.load(), null, 2);
40
+ const encrypted = encrypt(data);
41
+ writeFileSync(UPSTREAM_CREDENTIALS_PATH, encrypted, { mode: 0o600 });
42
+ }
43
+ getServer(serverUrl) {
44
+ const store = this.load();
45
+ const key = getServerKey(serverUrl);
46
+ if (!store.servers[key]) {
47
+ store.servers[key] = {};
48
+ }
49
+ return store.servers[key];
50
+ }
51
+ getTokens(serverUrl) {
52
+ return this.getServer(serverUrl).tokens;
53
+ }
54
+ saveTokens(serverUrl, tokens) {
55
+ const server = this.getServer(serverUrl);
56
+ server.tokens = tokens;
57
+ server.lastAuthenticated = Date.now();
58
+ this.save();
59
+ }
60
+ clearTokens(serverUrl) {
61
+ const server = this.getServer(serverUrl);
62
+ delete server.tokens;
63
+ this.save();
64
+ }
65
+ getClientInfo(serverUrl) {
66
+ return this.getServer(serverUrl).clientInfo;
67
+ }
68
+ saveClientInfo(serverUrl, info) {
69
+ const server = this.getServer(serverUrl);
70
+ server.clientInfo = info;
71
+ this.save();
72
+ }
73
+ clearClientInfo(serverUrl) {
74
+ const server = this.getServer(serverUrl);
75
+ delete server.clientInfo;
76
+ this.save();
77
+ }
78
+ getCodeVerifier(serverUrl) {
79
+ return this.getServer(serverUrl).codeVerifier;
80
+ }
81
+ saveCodeVerifier(serverUrl, verifier) {
82
+ const server = this.getServer(serverUrl);
83
+ server.codeVerifier = verifier;
84
+ this.save();
85
+ }
86
+ setServerName(serverUrl, name) {
87
+ const server = this.getServer(serverUrl);
88
+ server.serverName = name;
89
+ this.save();
90
+ }
91
+ clearAll(serverUrl) {
92
+ const store = this.load();
93
+ const key = getServerKey(serverUrl);
94
+ delete store.servers[key];
95
+ this.save();
96
+ }
97
+ /** Clear credentials for every upstream server. */
98
+ clearAllServers() {
99
+ this.store = { servers: {} };
100
+ this.save();
101
+ }
102
+ }
103
+ //# sourceMappingURL=upstream-token-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-token-store.js","sourceRoot":"","sources":["../../src/auth/upstream-token-store.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -10,8 +10,8 @@
10
10
  import { parseArgs } from "node:util";
11
11
  import { readFileSync, existsSync } from "node:fs";
12
12
  import { McpGateway } from "./gateway.js";
13
- import { loadConfig, findClaudeDesktopConfig, loadFromParentConfig } from "./config.js";
14
- import { login, logout, getAuthStatus, ensureValidToken } from "./auth/index.js";
13
+ import { loadConfig, findClaudeDesktopConfig, loadFromParentConfig, autoDetectConfig } from "./config.js";
14
+ import { login, logout, getAuthStatus, ensureValidToken, UpstreamTokenStore } from "./auth/index.js";
15
15
  import { SonomaClient } from "./sonoma-client.js";
16
16
  const DEFAULT_SONOMA_ENDPOINT = "https://app.sonoma.dev";
17
17
  const MDM_CONFIG_PATH = "/usr/local/etc/sonoma/config";
@@ -121,6 +121,7 @@ async function main() {
121
121
  login: { type: "boolean" },
122
122
  logout: { type: "boolean" },
123
123
  status: { type: "boolean" },
124
+ reauth: { type: "boolean" },
124
125
  endpoint: { type: "string", short: "e" },
125
126
  },
126
127
  strict: true,
@@ -155,6 +156,12 @@ async function main() {
155
156
  }
156
157
  process.exit(0);
157
158
  }
159
+ // Clear upstream OAuth tokens (forces re-authentication on next connect)
160
+ if (values.reauth) {
161
+ const store = new UpstreamTokenStore();
162
+ store.clearAllServers();
163
+ console.error("Cleared all upstream OAuth credentials. Servers will re-authenticate on connect.");
164
+ }
158
165
  // Gateway mode
159
166
  let config;
160
167
  if (values["mcp-json-path"]) {
@@ -176,9 +183,18 @@ async function main() {
176
183
  config = loadConfig(claudeConfig);
177
184
  }
178
185
  else {
179
- console.error("No config specified. Use --config, --mcp-json-path, or --auto");
180
- printHelp();
181
- process.exit(1);
186
+ // Auto-detect: search common MCP config locations for a Sonoma gateway entry
187
+ const detected = autoDetectConfig(values.debug);
188
+ if (detected) {
189
+ console.error(`Auto-detected config: ${detected.path} (gateway: ${detected.gatewayName})`);
190
+ config = loadFromParentConfig(detected.path, detected.gatewayName);
191
+ }
192
+ else {
193
+ console.error("No config found. Searched: ~/.cursor/mcp.json, Claude Desktop config");
194
+ console.error("Configure servers in your MCP config, or use --mcp-json-path <path>");
195
+ printHelp();
196
+ process.exit(1);
197
+ }
182
198
  }
183
199
  if (values.debug) {
184
200
  config.debug = true;
@@ -193,10 +209,12 @@ async function main() {
193
209
  gatewayEnabled: mdmConfig.gatewayEnabled,
194
210
  }));
195
211
  }
196
- // Set endpoint priority: CLI flag > MDM gateway endpoint > default
197
- // Note: sonomaEndpoint was set from --endpoint flag, defaults to DEFAULT_SONOMA_ENDPOINT
198
- if (!config.sonomaEndpoint) {
199
- config.sonomaEndpoint = values.endpoint || mdmConfig.gatewayEndpoint || sonomaEndpoint;
212
+ // Set endpoint priority: CLI flag > MDM gateway endpoint
213
+ // Note: We intentionally don't fall back to DEFAULT_SONOMA_ENDPOINT here
214
+ // If no endpoint is explicitly configured, the gateway runs in offline mode without auth
215
+ const explicitEndpoint = values.endpoint || mdmConfig.gatewayEndpoint || config.sonomaEndpoint;
216
+ if (explicitEndpoint) {
217
+ config.sonomaEndpoint = explicitEndpoint;
200
218
  }
201
219
  // Store org API key in config for gateway
202
220
  if (mdmConfig.apiKey) {
@@ -208,73 +226,83 @@ async function main() {
208
226
  // Determine auth mode from MDM config or policy
209
227
  // Default: user_id (prompt for OAuth login)
210
228
  let requiredAuthMode = mdmConfig.gatewayAuthMode || "user_id";
211
- // Try to ensure we have a valid token (refreshes if expired)
212
- // This handles the case where user has old/expired credentials
213
- const hasValidToken = await ensureValidToken({
214
- sonomaEndpoint: config.sonomaEndpoint,
215
- debug: values.debug,
216
- });
217
- if (values.debug) {
218
- console.error(`[cli] Token validation: ${hasValidToken ? "valid" : "invalid/missing"}`);
219
- }
220
- // Check auth status (now accurate after ensureValidToken)
221
- let authStatus = getAuthStatus(config.sonomaEndpoint);
222
- // Fetch policy to check if admin overrides auth mode
223
- if (config.sonomaEndpoint && config.sonomaApiKey) {
224
- const tempClient = new SonomaClient({
225
- endpoint: config.sonomaEndpoint,
226
- orgApiKey: config.sonomaApiKey,
229
+ // Only attempt authentication if a Sonoma endpoint is explicitly configured
230
+ // This allows the gateway to run in "offline" mode for local testing
231
+ let authStatus = { loggedIn: false, hasRefreshToken: false };
232
+ if (config.sonomaEndpoint) {
233
+ // Try to ensure we have a valid token (refreshes if expired)
234
+ // This handles the case where user has old/expired credentials
235
+ const hasValidToken = await ensureValidToken({
236
+ sonomaEndpoint: config.sonomaEndpoint,
227
237
  debug: values.debug,
228
238
  });
229
- try {
230
- const policy = await tempClient.fetchPolicy();
231
- if (values.debug) {
232
- console.error(`[cli] Policy: mode=${policy.mode}, authMode=${policy.gatewayAuthMode}`);
239
+ if (values.debug) {
240
+ console.error(`[cli] Token validation: ${hasValidToken ? "valid" : "invalid/missing"}`);
241
+ }
242
+ // Check auth status (now accurate after ensureValidToken)
243
+ authStatus = getAuthStatus(config.sonomaEndpoint);
244
+ // Fetch policy to check if admin overrides auth mode
245
+ if (config.sonomaApiKey) {
246
+ const tempClient = new SonomaClient({
247
+ endpoint: config.sonomaEndpoint,
248
+ orgApiKey: config.sonomaApiKey,
249
+ debug: values.debug,
250
+ });
251
+ try {
252
+ const policy = await tempClient.fetchPolicy();
253
+ if (values.debug) {
254
+ console.error(`[cli] Policy: mode=${policy.mode}, authMode=${policy.gatewayAuthMode}`);
255
+ }
256
+ // Admin-set policy overrides MDM config
257
+ if (policy.gatewayAuthMode) {
258
+ requiredAuthMode = policy.gatewayAuthMode;
259
+ }
233
260
  }
234
- // Admin-set policy overrides MDM config
235
- if (policy.gatewayAuthMode) {
236
- requiredAuthMode = policy.gatewayAuthMode;
261
+ catch (error) {
262
+ if (values.debug) {
263
+ console.error("[cli] Failed to fetch policy:", error);
264
+ }
265
+ // Continue anyway - will use MDM config or default
237
266
  }
238
267
  }
239
- catch (error) {
240
- if (values.debug) {
241
- console.error("[cli] Failed to fetch policy:", error);
242
- }
243
- // Continue anyway - will use MDM config or default
268
+ if (values.debug) {
269
+ console.error(`[cli] Required auth mode: ${requiredAuthMode}`);
244
270
  }
245
- }
246
- if (values.debug) {
247
- console.error(`[cli] Required auth mode: ${requiredAuthMode}`);
248
- }
249
- // Handle authentication based on required mode
250
- if (requiredAuthMode === "user_id" && !authStatus.loggedIn) {
251
- // User mode: require OAuth login
252
- console.error("User authentication required for MCP visibility.");
253
- console.error("Opening browser to login...");
254
- console.error("");
255
- await login({ sonomaEndpoint: config.sonomaEndpoint, debug: values.debug });
256
- authStatus = getAuthStatus();
257
- if (!authStatus.loggedIn) {
258
- console.error("Login required but not completed. Exiting.");
259
- process.exit(1);
271
+ // Handle authentication based on required mode
272
+ if (requiredAuthMode === "user_id" && !authStatus.loggedIn) {
273
+ // User mode: require OAuth login
274
+ console.error("User authentication required for MCP visibility.");
275
+ console.error("Opening browser to login...");
276
+ console.error("");
277
+ await login({ sonomaEndpoint: config.sonomaEndpoint, debug: values.debug });
278
+ authStatus = getAuthStatus();
279
+ if (!authStatus.loggedIn) {
280
+ console.error("Login required but not completed. Exiting.");
281
+ process.exit(1);
282
+ }
260
283
  }
261
- }
262
- else if (requiredAuthMode === "org_key" && !config.sonomaApiKey && !authStatus.loggedIn) {
263
- // Org key mode but no key available - fall back to OAuth
264
- console.error("No API key found. Opening browser to login...");
265
- console.error("");
266
- await login({ sonomaEndpoint: config.sonomaEndpoint, debug: values.debug });
267
- authStatus = getAuthStatus();
268
- }
269
- if (values.debug) {
270
- if (authStatus.loggedIn) {
271
- console.error("[cli] Using OAuth token (user-linked telemetry)");
284
+ else if (requiredAuthMode === "org_key" && !config.sonomaApiKey && !authStatus.loggedIn) {
285
+ // Org key mode but no key available - fall back to OAuth
286
+ console.error("No API key found. Opening browser to login...");
287
+ console.error("");
288
+ await login({ sonomaEndpoint: config.sonomaEndpoint, debug: values.debug });
289
+ authStatus = getAuthStatus();
272
290
  }
273
- else if (config.sonomaApiKey) {
274
- console.error("[cli] Using org API key (device-level telemetry)");
291
+ if (values.debug) {
292
+ if (authStatus.loggedIn) {
293
+ console.error("[cli] Using OAuth token (user-linked telemetry)");
294
+ }
295
+ else if (config.sonomaApiKey) {
296
+ console.error("[cli] Using org API key (device-level telemetry)");
297
+ }
298
+ else {
299
+ console.error("[cli] Warning: No authentication available");
300
+ }
275
301
  }
276
- else {
277
- console.error("[cli] Warning: No authentication available");
302
+ }
303
+ else {
304
+ if (values.debug) {
305
+ console.error("[cli] No Sonoma endpoint configured - running in offline mode");
278
306
  }
279
307
  }
280
308
  const gateway = new McpGateway(config);
@@ -300,12 +328,18 @@ function printHelp() {
300
328
  @sonoma/mcp-gateway - Local MCP Gateway for tool-level visibility
301
329
 
302
330
  USAGE:
303
- npx @sonoma/mcp-gateway [OPTIONS]
331
+ npx @sonoma-security/mcp-gateway [OPTIONS]
332
+
333
+ By default (no arguments), auto-detects config from:
334
+ - ~/.claude.json (Claude Code CLI)
335
+ - ~/.cursor/mcp.json (Cursor)
336
+ - ~/Library/Application Support/Claude/claude_desktop_config.json (Claude Desktop)
337
+ - ~/.codeium/windsurf/mcp_config.json (Windsurf)
304
338
 
305
339
  OPTIONS:
306
340
  -c, --config <path> Path to gateway config JSON file
307
341
  --mcp-json-path <path> Read servers from parent MCP config (single-file mode)
308
- -a, --auto Auto-detect Claude Desktop config
342
+ -a, --auto Auto-detect Claude Desktop config only
309
343
  -d, --debug Enable debug logging
310
344
  -h, --help Show this help message
311
345
  -v, --version Show version
@@ -314,16 +348,16 @@ AUTH OPTIONS:
314
348
  --login Authenticate with Sonoma (opens browser)
315
349
  --logout Clear stored credentials
316
350
  --status Show authentication status
351
+ --reauth Clear upstream OAuth tokens and re-authenticate
317
352
  -e, --endpoint <url> Sonoma API endpoint (default: https://app.sonoma.dev)
318
353
 
319
- SINGLE-FILE CONFIG (recommended):
320
- Configure in your Claude Desktop / Cursor config with nested "servers":
354
+ QUICKSTART (zero-config):
355
+ Add to your ~/.cursor/mcp.json or Claude Desktop config:
321
356
  {
322
357
  "mcpServers": {
323
358
  "sonoma": {
324
359
  "command": "npx",
325
- "args": ["@sonoma/mcp-gateway", "--mcp-json-path", "~/.cursor/mcp.json"],
326
- "env": { "SONOMA_API_KEY": "your-key" },
360
+ "args": ["@sonoma-security/mcp-gateway"],
327
361
  "servers": {
328
362
  "filesystem": {
329
363
  "command": "npx",
@@ -334,23 +368,18 @@ SINGLE-FILE CONFIG (recommended):
334
368
  }
335
369
  }
336
370
 
337
- SEPARATE CONFIG FILE:
338
- {
339
- "servers": [
340
- { "name": "filesystem", "command": "npx", "args": ["@modelcontextprotocol/server-filesystem", "/tmp"] }
341
- ],
342
- "sonomaEndpoint": "https://app.sonoma.dev"
343
- }
371
+ The gateway auto-detects its config location - no --mcp-json-path needed!
372
+ On first run, it prompts for OAuth login. That's it.
344
373
 
345
374
  EXAMPLES:
346
- # Single-file config (servers nested in Claude Desktop config)
347
- npx @sonoma/mcp-gateway --mcp-json-path ~/.cursor/mcp.json
375
+ # Zero-config (auto-detects ~/.cursor/mcp.json or Claude Desktop)
376
+ npx @sonoma-security/mcp-gateway
348
377
 
349
- # Separate config file
350
- npx @sonoma/mcp-gateway --config ./my-servers.json
378
+ # Explicit path (if auto-detect doesn't work)
379
+ npx @sonoma-security/mcp-gateway --mcp-json-path ~/.cursor/mcp.json
351
380
 
352
- # Auto-detect Claude Desktop config
353
- npx @sonoma/mcp-gateway --auto
381
+ # Separate config file
382
+ npx @sonoma-security/mcp-gateway --config ./my-servers.json
354
383
  `);
355
384
  }
356
385
  main().catch((error) => {