@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.
- package/README.md +104 -45
- package/dist/__tests__/config.test.js +28 -0
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/ssrf-protection.test.d.ts +2 -0
- package/dist/__tests__/ssrf-protection.test.d.ts.map +1 -0
- package/dist/__tests__/ssrf-protection.test.js +389 -0
- package/dist/__tests__/ssrf-protection.test.js.map +1 -0
- package/dist/auth/client.d.ts +2 -0
- package/dist/auth/client.d.ts.map +1 -1
- package/dist/auth/client.js +17 -15
- package/dist/auth/client.js.map +1 -1
- package/dist/auth/crypto.d.ts +23 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +78 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/index.d.ts +4 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +4 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/server.d.ts +2 -0
- package/dist/auth/server.d.ts.map +1 -1
- package/dist/auth/server.js +337 -59
- package/dist/auth/server.js.map +1 -1
- package/dist/auth/storage.d.ts.map +1 -1
- package/dist/auth/storage.js +2 -72
- package/dist/auth/storage.js.map +1 -1
- package/dist/auth/upstream-oauth-provider.d.ts +41 -0
- package/dist/auth/upstream-oauth-provider.d.ts.map +1 -0
- package/dist/auth/upstream-oauth-provider.js +88 -0
- package/dist/auth/upstream-oauth-provider.js.map +1 -0
- package/dist/auth/upstream-oauth.d.ts +31 -0
- package/dist/auth/upstream-oauth.d.ts.map +1 -0
- package/dist/auth/upstream-oauth.js +79 -0
- package/dist/auth/upstream-oauth.js.map +1 -0
- package/dist/auth/upstream-token-store.d.ts +27 -0
- package/dist/auth/upstream-token-store.d.ts.map +1 -0
- package/dist/auth/upstream-token-store.js +103 -0
- package/dist/auth/upstream-token-store.js.map +1 -0
- package/dist/cli.js +115 -86
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +30 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +203 -9
- package/dist/config.js.map +1 -1
- package/dist/gateway.d.ts +23 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +224 -35
- package/dist/gateway.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pattern-matcher.d.ts +47 -0
- package/dist/pattern-matcher.d.ts.map +1 -0
- package/dist/pattern-matcher.js +98 -0
- package/dist/pattern-matcher.js.map +1 -0
- package/dist/sonoma-client.d.ts +21 -5
- package/dist/sonoma-client.d.ts.map +1 -1
- package/dist/sonoma-client.js +42 -2
- package/dist/sonoma-client.js.map +1 -1
- package/dist/ssrf-protection.d.ts +59 -0
- package/dist/ssrf-protection.d.ts.map +1 -0
- package/dist/ssrf-protection.js +253 -0
- package/dist/ssrf-protection.js.map +1 -0
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- 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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
197
|
-
// Note:
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
//
|
|
212
|
-
// This
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
#
|
|
347
|
-
npx @sonoma/mcp-gateway
|
|
375
|
+
# Zero-config (auto-detects ~/.cursor/mcp.json or Claude Desktop)
|
|
376
|
+
npx @sonoma-security/mcp-gateway
|
|
348
377
|
|
|
349
|
-
#
|
|
350
|
-
npx @sonoma/mcp-gateway --
|
|
378
|
+
# Explicit path (if auto-detect doesn't work)
|
|
379
|
+
npx @sonoma-security/mcp-gateway --mcp-json-path ~/.cursor/mcp.json
|
|
351
380
|
|
|
352
|
-
#
|
|
353
|
-
npx @sonoma/mcp-gateway --
|
|
381
|
+
# Separate config file
|
|
382
|
+
npx @sonoma-security/mcp-gateway --config ./my-servers.json
|
|
354
383
|
`);
|
|
355
384
|
}
|
|
356
385
|
main().catch((error) => {
|