@sonoma-security/mcp-gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/dist/auth/client.d.ts +35 -0
- package/dist/auth/client.d.ts.map +1 -0
- package/dist/auth/client.js +217 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/index.d.ts +7 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/server.d.ts +26 -0
- package/dist/auth/server.d.ts.map +1 -0
- package/dist/auth/server.js +155 -0
- package/dist/auth/server.js.map +1 -0
- package/dist/auth/storage.d.ts +58 -0
- package/dist/auth/storage.d.ts.map +1 -0
- package/dist/auth/storage.js +181 -0
- package/dist/auth/storage.js.map +1 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +134 -0
- package/dist/config.js.map +1 -0
- package/dist/gateway.d.ts +66 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +346 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/sonoma-client.d.ts +55 -0
- package/dist/sonoma-client.d.ts.map +1 -0
- package/dist/sonoma-client.js +144 -0
- package/dist/sonoma-client.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure token storage for MCP Gateway OAuth credentials
|
|
3
|
+
*
|
|
4
|
+
* Stores tokens in ~/.sonoma/gateway-credentials.json with AES-256-GCM encryption.
|
|
5
|
+
* The encryption key is derived from a machine-specific identifier to prevent
|
|
6
|
+
* tokens from being copied between machines.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
9
|
+
import { homedir, hostname } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
12
|
+
const SONOMA_DIR = join(homedir(), ".sonoma");
|
|
13
|
+
const CREDENTIALS_PATH = join(SONOMA_DIR, "gateway-credentials.json");
|
|
14
|
+
const DEVICE_ID_PATH = join(SONOMA_DIR, "device-id");
|
|
15
|
+
// Encryption settings
|
|
16
|
+
const ALGORITHM = "aes-256-gcm";
|
|
17
|
+
const KEY_LENGTH = 32;
|
|
18
|
+
const IV_LENGTH = 16;
|
|
19
|
+
const SALT_LENGTH = 32;
|
|
20
|
+
/**
|
|
21
|
+
* Get or create a stable device ID for this machine
|
|
22
|
+
*/
|
|
23
|
+
function getDeviceId() {
|
|
24
|
+
ensureSonomaDir();
|
|
25
|
+
if (existsSync(DEVICE_ID_PATH)) {
|
|
26
|
+
return readFileSync(DEVICE_ID_PATH, "utf-8").trim();
|
|
27
|
+
}
|
|
28
|
+
// Generate new device ID: dev_{random_hex}
|
|
29
|
+
const id = `dev_${randomBytes(16).toString("hex")}`;
|
|
30
|
+
writeFileSync(DEVICE_ID_PATH, id);
|
|
31
|
+
return id;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Derive an encryption key from machine-specific data
|
|
35
|
+
*/
|
|
36
|
+
function deriveKey(salt) {
|
|
37
|
+
// Use device ID and hostname as key material
|
|
38
|
+
const deviceId = getDeviceId();
|
|
39
|
+
const keyMaterial = `${deviceId}:${hostname()}:sonoma-mcp-gateway`;
|
|
40
|
+
return scryptSync(keyMaterial, salt, KEY_LENGTH);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Encrypt data using AES-256-GCM
|
|
44
|
+
*/
|
|
45
|
+
function encrypt(data) {
|
|
46
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
47
|
+
const key = deriveKey(salt);
|
|
48
|
+
const iv = randomBytes(IV_LENGTH);
|
|
49
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
50
|
+
let encrypted = cipher.update(data, "utf8", "hex");
|
|
51
|
+
encrypted += cipher.final("hex");
|
|
52
|
+
const authTag = cipher.getAuthTag();
|
|
53
|
+
// Format: salt:iv:authTag:encrypted (all hex)
|
|
54
|
+
return [salt.toString("hex"), iv.toString("hex"), authTag.toString("hex"), encrypted].join(":");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Decrypt data using AES-256-GCM
|
|
58
|
+
*/
|
|
59
|
+
function decrypt(encryptedData) {
|
|
60
|
+
const [saltHex, ivHex, authTagHex, encrypted] = encryptedData.split(":");
|
|
61
|
+
if (!saltHex || !ivHex || !authTagHex || !encrypted) {
|
|
62
|
+
throw new Error("Invalid encrypted data format");
|
|
63
|
+
}
|
|
64
|
+
const salt = Buffer.from(saltHex, "hex");
|
|
65
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
66
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
67
|
+
const key = deriveKey(salt);
|
|
68
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
69
|
+
decipher.setAuthTag(authTag);
|
|
70
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
71
|
+
decrypted += decipher.final("utf8");
|
|
72
|
+
return decrypted;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Ensure the ~/.sonoma directory exists
|
|
76
|
+
*/
|
|
77
|
+
function ensureSonomaDir() {
|
|
78
|
+
if (!existsSync(SONOMA_DIR)) {
|
|
79
|
+
mkdirSync(SONOMA_DIR, { recursive: true, mode: 0o700 });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Load stored credentials from disk
|
|
84
|
+
*/
|
|
85
|
+
export function loadCredentials() {
|
|
86
|
+
if (!existsSync(CREDENTIALS_PATH)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const encryptedData = readFileSync(CREDENTIALS_PATH, "utf-8");
|
|
91
|
+
const decrypted = decrypt(encryptedData);
|
|
92
|
+
return JSON.parse(decrypted);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// If decryption fails (e.g., machine changed), return null
|
|
96
|
+
console.error("[auth] Failed to load credentials:", error);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Save credentials to disk (encrypted)
|
|
102
|
+
*/
|
|
103
|
+
export function saveCredentials(credentials) {
|
|
104
|
+
ensureSonomaDir();
|
|
105
|
+
const data = JSON.stringify(credentials, null, 2);
|
|
106
|
+
const encrypted = encrypt(data);
|
|
107
|
+
writeFileSync(CREDENTIALS_PATH, encrypted, { mode: 0o600 });
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Clear all stored credentials
|
|
111
|
+
*/
|
|
112
|
+
export function clearCredentials() {
|
|
113
|
+
if (existsSync(CREDENTIALS_PATH)) {
|
|
114
|
+
unlinkSync(CREDENTIALS_PATH);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Convert stored credentials to MCP SDK OAuthTokens format
|
|
119
|
+
*/
|
|
120
|
+
export function toOAuthTokens(creds) {
|
|
121
|
+
if (!creds.accessToken) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
access_token: creds.accessToken,
|
|
126
|
+
token_type: creds.tokenType || "Bearer",
|
|
127
|
+
refresh_token: creds.refreshToken,
|
|
128
|
+
expires_in: creds.expiresAt ? Math.floor((creds.expiresAt - Date.now()) / 1000) : undefined,
|
|
129
|
+
scope: creds.scope,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Convert MCP SDK OAuthTokens to stored credentials format
|
|
134
|
+
*/
|
|
135
|
+
export function fromOAuthTokens(tokens, existing) {
|
|
136
|
+
const expiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1000 : undefined;
|
|
137
|
+
return {
|
|
138
|
+
...existing,
|
|
139
|
+
accessToken: tokens.access_token,
|
|
140
|
+
refreshToken: tokens.refresh_token || existing?.refreshToken,
|
|
141
|
+
expiresAt,
|
|
142
|
+
tokenType: tokens.token_type,
|
|
143
|
+
scope: tokens.scope,
|
|
144
|
+
lastRefreshed: Date.now(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Convert stored credentials to MCP SDK client information format
|
|
149
|
+
*/
|
|
150
|
+
export function toClientInformation(creds) {
|
|
151
|
+
if (!creds.clientId) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
client_id: creds.clientId,
|
|
156
|
+
client_secret: creds.clientSecret,
|
|
157
|
+
client_secret_expires_at: creds.clientSecretExpiresAt,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Check if stored tokens are expired or about to expire
|
|
162
|
+
*/
|
|
163
|
+
export function isTokenExpired(creds, bufferMs = 60000) {
|
|
164
|
+
if (!creds.accessToken || !creds.expiresAt) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return Date.now() + bufferMs >= creds.expiresAt;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Check if we have a valid refresh token
|
|
171
|
+
*/
|
|
172
|
+
export function hasRefreshToken(creds) {
|
|
173
|
+
return !!creds.refreshToken;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get device ID for telemetry
|
|
177
|
+
*/
|
|
178
|
+
export function getStoredDeviceId() {
|
|
179
|
+
return getDeviceId();
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/auth/storage.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for MCP Gateway
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @sonoma/mcp-gateway --config ./config.json
|
|
7
|
+
* npx @sonoma/mcp-gateway --auto # Auto-detect Claude Desktop config
|
|
8
|
+
* npx @sonoma/mcp-gateway --login # Authenticate with Sonoma
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry point for MCP Gateway
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @sonoma/mcp-gateway --config ./config.json
|
|
7
|
+
* npx @sonoma/mcp-gateway --auto # Auto-detect Claude Desktop config
|
|
8
|
+
* npx @sonoma/mcp-gateway --login # Authenticate with Sonoma
|
|
9
|
+
*/
|
|
10
|
+
import { parseArgs } from "node:util";
|
|
11
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { McpGateway } from "./gateway";
|
|
13
|
+
import { loadConfig, findClaudeDesktopConfig, loadFromParentConfig } from "./config";
|
|
14
|
+
import { login, logout, getAuthStatus } from "./auth";
|
|
15
|
+
import { SonomaClient } from "./sonoma-client";
|
|
16
|
+
const DEFAULT_SONOMA_ENDPOINT = "https://app.sonoma.dev";
|
|
17
|
+
const MDM_CONFIG_PATH = "/usr/local/etc/sonoma/config";
|
|
18
|
+
/**
|
|
19
|
+
* Read org API key from MDM config file or environment
|
|
20
|
+
* Priority: 1. SONOMA_API_KEY env var, 2. /usr/local/etc/sonoma/config
|
|
21
|
+
*/
|
|
22
|
+
function getOrgApiKey() {
|
|
23
|
+
// Check env var first
|
|
24
|
+
if (process.env.SONOMA_API_KEY) {
|
|
25
|
+
return {
|
|
26
|
+
apiKey: process.env.SONOMA_API_KEY,
|
|
27
|
+
endpoint: process.env.SONOMA_ENDPOINT || null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Check MDM config file
|
|
31
|
+
if (existsSync(MDM_CONFIG_PATH)) {
|
|
32
|
+
try {
|
|
33
|
+
const content = readFileSync(MDM_CONFIG_PATH, "utf-8");
|
|
34
|
+
const lines = content.split("\n");
|
|
35
|
+
let apiKey = null;
|
|
36
|
+
let endpoint = null;
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const [key, ...valueParts] = line.split("=");
|
|
39
|
+
const value = valueParts.join("=").trim();
|
|
40
|
+
if (key?.trim() === "API_KEY")
|
|
41
|
+
apiKey = value;
|
|
42
|
+
if (key?.trim() === "ENDPOINT")
|
|
43
|
+
endpoint = value;
|
|
44
|
+
}
|
|
45
|
+
return { apiKey, endpoint };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Ignore read errors
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { apiKey: null, endpoint: null };
|
|
52
|
+
}
|
|
53
|
+
async function main() {
|
|
54
|
+
const { values } = parseArgs({
|
|
55
|
+
options: {
|
|
56
|
+
config: { type: "string", short: "c" },
|
|
57
|
+
auto: { type: "boolean", short: "a" },
|
|
58
|
+
// Read servers from parent config file (like Lasso MCP Gateway)
|
|
59
|
+
// This enables single-file config where servers are nested under our entry
|
|
60
|
+
"mcp-json-path": { type: "string" },
|
|
61
|
+
debug: { type: "boolean", short: "d" },
|
|
62
|
+
help: { type: "boolean", short: "h" },
|
|
63
|
+
version: { type: "boolean", short: "v" },
|
|
64
|
+
// Auth commands
|
|
65
|
+
login: { type: "boolean" },
|
|
66
|
+
logout: { type: "boolean" },
|
|
67
|
+
status: { type: "boolean" },
|
|
68
|
+
endpoint: { type: "string", short: "e" },
|
|
69
|
+
},
|
|
70
|
+
strict: true,
|
|
71
|
+
});
|
|
72
|
+
if (values.help) {
|
|
73
|
+
printHelp();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
if (values.version) {
|
|
77
|
+
console.error("@sonoma/mcp-gateway v0.1.0");
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
// Auth commands
|
|
81
|
+
const sonomaEndpoint = values.endpoint || DEFAULT_SONOMA_ENDPOINT;
|
|
82
|
+
if (values.login) {
|
|
83
|
+
await login({ sonomaEndpoint, debug: values.debug });
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
if (values.logout) {
|
|
87
|
+
logout();
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
if (values.status) {
|
|
91
|
+
const status = getAuthStatus();
|
|
92
|
+
if (status.loggedIn) {
|
|
93
|
+
console.error("Logged in to:", status.endpoint);
|
|
94
|
+
console.error("Token expires:", status.expiresAt?.toISOString() || "unknown");
|
|
95
|
+
console.error("Has refresh token:", status.hasRefreshToken);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.error("Not logged in. Run: sonoma-gateway --login");
|
|
99
|
+
}
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
// Gateway mode
|
|
103
|
+
let config;
|
|
104
|
+
if (values["mcp-json-path"]) {
|
|
105
|
+
// Single-file config pattern (like Lasso MCP Gateway)
|
|
106
|
+
// Reads nested "servers" from parent config file
|
|
107
|
+
console.error(`Reading servers from: ${values["mcp-json-path"]}`);
|
|
108
|
+
config = loadFromParentConfig(values["mcp-json-path"]);
|
|
109
|
+
}
|
|
110
|
+
else if (values.config) {
|
|
111
|
+
config = loadConfig(values.config);
|
|
112
|
+
}
|
|
113
|
+
else if (values.auto) {
|
|
114
|
+
const claudeConfig = findClaudeDesktopConfig();
|
|
115
|
+
if (!claudeConfig) {
|
|
116
|
+
console.error("Could not find Claude Desktop config");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
console.error(`Using Claude Desktop config: ${claudeConfig}`);
|
|
120
|
+
config = loadConfig(claudeConfig);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.error("No config specified. Use --config, --mcp-json-path, or --auto");
|
|
124
|
+
printHelp();
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
if (values.debug) {
|
|
128
|
+
config.debug = true;
|
|
129
|
+
}
|
|
130
|
+
// Set endpoint from CLI or config or MDM config
|
|
131
|
+
const orgConfig = getOrgApiKey();
|
|
132
|
+
if (!config.sonomaEndpoint) {
|
|
133
|
+
config.sonomaEndpoint = orgConfig.endpoint || sonomaEndpoint;
|
|
134
|
+
}
|
|
135
|
+
// Store org API key in config for gateway
|
|
136
|
+
if (orgConfig.apiKey) {
|
|
137
|
+
config.sonomaApiKey = orgConfig.apiKey;
|
|
138
|
+
if (values.debug) {
|
|
139
|
+
console.error("[cli] Found org API key from", process.env.SONOMA_API_KEY ? "env" : MDM_CONFIG_PATH);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Check auth status and handle auto-login if needed
|
|
143
|
+
let authStatus = getAuthStatus();
|
|
144
|
+
if (!authStatus.loggedIn && !config.sonomaApiKey) {
|
|
145
|
+
// No OAuth token and no org API key - need to login
|
|
146
|
+
console.error("No authentication found. Opening browser to login...");
|
|
147
|
+
console.error("");
|
|
148
|
+
await login({ sonomaEndpoint: config.sonomaEndpoint, debug: values.debug });
|
|
149
|
+
authStatus = getAuthStatus();
|
|
150
|
+
}
|
|
151
|
+
// Fetch policy to check gatewayAuthMode
|
|
152
|
+
// Admin can require user_id mode which forces OAuth even if org key exists
|
|
153
|
+
if (config.sonomaEndpoint) {
|
|
154
|
+
const tempClient = new SonomaClient({
|
|
155
|
+
endpoint: config.sonomaEndpoint,
|
|
156
|
+
orgApiKey: config.sonomaApiKey,
|
|
157
|
+
debug: values.debug,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
const policy = await tempClient.fetchPolicy();
|
|
161
|
+
if (values.debug) {
|
|
162
|
+
console.error(`[cli] Policy: mode=${policy.mode}, authMode=${policy.gatewayAuthMode}`);
|
|
163
|
+
}
|
|
164
|
+
// If admin requires user_id mode but we only have org key, force OAuth login
|
|
165
|
+
if (policy.gatewayAuthMode === "user_id" && !authStatus.loggedIn) {
|
|
166
|
+
console.error("Organization requires user authentication for MCP visibility.");
|
|
167
|
+
console.error("Opening browser to login...");
|
|
168
|
+
console.error("");
|
|
169
|
+
await login({ sonomaEndpoint: config.sonomaEndpoint, debug: values.debug });
|
|
170
|
+
authStatus = getAuthStatus();
|
|
171
|
+
if (!authStatus.loggedIn) {
|
|
172
|
+
console.error("Login required but not completed. Exiting.");
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (values.debug) {
|
|
179
|
+
console.error("[cli] Failed to fetch policy:", error);
|
|
180
|
+
}
|
|
181
|
+
// Continue anyway - gateway will retry policy fetch
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (values.debug) {
|
|
185
|
+
if (authStatus.loggedIn) {
|
|
186
|
+
console.error("[cli] Using OAuth token (user-linked telemetry)");
|
|
187
|
+
}
|
|
188
|
+
else if (config.sonomaApiKey) {
|
|
189
|
+
console.error("[cli] Using org API key (device-level telemetry)");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const gateway = new McpGateway(config);
|
|
193
|
+
// Handle shutdown gracefully
|
|
194
|
+
const shutdown = async () => {
|
|
195
|
+
console.error("\nShutting down...");
|
|
196
|
+
await gateway.stop();
|
|
197
|
+
process.exit(0);
|
|
198
|
+
};
|
|
199
|
+
process.on("SIGINT", shutdown);
|
|
200
|
+
process.on("SIGTERM", shutdown);
|
|
201
|
+
try {
|
|
202
|
+
await gateway.start();
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error("Failed to start gateway:", error);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function printHelp() {
|
|
210
|
+
// Use stderr for help output - stdout is reserved for MCP JSON-RPC
|
|
211
|
+
console.error(`
|
|
212
|
+
@sonoma/mcp-gateway - Local MCP Gateway for tool-level visibility
|
|
213
|
+
|
|
214
|
+
USAGE:
|
|
215
|
+
npx @sonoma/mcp-gateway [OPTIONS]
|
|
216
|
+
|
|
217
|
+
OPTIONS:
|
|
218
|
+
-c, --config <path> Path to gateway config JSON file
|
|
219
|
+
--mcp-json-path <path> Read servers from parent MCP config (single-file mode)
|
|
220
|
+
-a, --auto Auto-detect Claude Desktop config
|
|
221
|
+
-d, --debug Enable debug logging
|
|
222
|
+
-h, --help Show this help message
|
|
223
|
+
-v, --version Show version
|
|
224
|
+
|
|
225
|
+
AUTH OPTIONS:
|
|
226
|
+
--login Authenticate with Sonoma (opens browser)
|
|
227
|
+
--logout Clear stored credentials
|
|
228
|
+
--status Show authentication status
|
|
229
|
+
-e, --endpoint <url> Sonoma API endpoint (default: https://app.sonoma.dev)
|
|
230
|
+
|
|
231
|
+
SINGLE-FILE CONFIG (recommended):
|
|
232
|
+
Configure in your Claude Desktop / Cursor config with nested "servers":
|
|
233
|
+
{
|
|
234
|
+
"mcpServers": {
|
|
235
|
+
"sonoma": {
|
|
236
|
+
"command": "npx",
|
|
237
|
+
"args": ["@sonoma/mcp-gateway", "--mcp-json-path", "~/.cursor/mcp.json"],
|
|
238
|
+
"env": { "SONOMA_API_KEY": "your-key" },
|
|
239
|
+
"servers": {
|
|
240
|
+
"filesystem": {
|
|
241
|
+
"command": "npx",
|
|
242
|
+
"args": ["@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
SEPARATE CONFIG FILE:
|
|
250
|
+
{
|
|
251
|
+
"servers": [
|
|
252
|
+
{ "name": "filesystem", "command": "npx", "args": ["@modelcontextprotocol/server-filesystem", "/tmp"] }
|
|
253
|
+
],
|
|
254
|
+
"sonomaEndpoint": "https://app.sonoma.dev"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
EXAMPLES:
|
|
258
|
+
# Single-file config (servers nested in Claude Desktop config)
|
|
259
|
+
npx @sonoma/mcp-gateway --mcp-json-path ~/.cursor/mcp.json
|
|
260
|
+
|
|
261
|
+
# Separate config file
|
|
262
|
+
npx @sonoma/mcp-gateway --config ./my-servers.json
|
|
263
|
+
|
|
264
|
+
# Auto-detect Claude Desktop config
|
|
265
|
+
npx @sonoma/mcp-gateway --auto
|
|
266
|
+
`);
|
|
267
|
+
}
|
|
268
|
+
main().catch((error) => {
|
|
269
|
+
console.error("Fatal error:", error);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loader for MCP Gateway
|
|
3
|
+
* Parses Claude Desktop / Cursor style MCP configs
|
|
4
|
+
*/
|
|
5
|
+
import type { GatewayConfig } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Load gateway config from a JSON file
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadConfig(configPath: string): GatewayConfig;
|
|
10
|
+
/**
|
|
11
|
+
* Load servers from parent MCP config file (single-file config pattern)
|
|
12
|
+
*
|
|
13
|
+
* This enables the Lasso MCP Gateway pattern where you configure:
|
|
14
|
+
* {
|
|
15
|
+
* "mcpServers": {
|
|
16
|
+
* "sonoma": {
|
|
17
|
+
* "command": "npx",
|
|
18
|
+
* "args": ["@sonoma/mcp-gateway", "--mcp-json-path", "~/.cursor/mcp.json"],
|
|
19
|
+
* "servers": {
|
|
20
|
+
* "filesystem": { "command": "npx", "args": ["@modelcontextprotocol/server-filesystem", "."] }
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* The gateway reads the parent config, finds its own entry, and extracts the nested "servers".
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadFromParentConfig(configPath: string, gatewayName?: string): GatewayConfig;
|
|
29
|
+
/**
|
|
30
|
+
* Find Claude Desktop config file
|
|
31
|
+
*/
|
|
32
|
+
export declare function findClaudeDesktopConfig(): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Create a minimal test config
|
|
35
|
+
*/
|
|
36
|
+
export declare function createTestConfig(): GatewayConfig;
|
|
37
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,SAAS,CAAC;AAc9D;;GAEG;AACH,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,CAe5D;AAsBD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,SAAW,GAAG,aAAa,CAyD9F;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,GAAG,IAAI,CAwBvD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,aAAa,CAKhD"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loader for MCP Gateway
|
|
3
|
+
* Parses Claude Desktop / Cursor style MCP configs
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
/**
|
|
9
|
+
* Load gateway config from a JSON file
|
|
10
|
+
*/
|
|
11
|
+
export function loadConfig(configPath) {
|
|
12
|
+
if (!existsSync(configPath)) {
|
|
13
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
14
|
+
}
|
|
15
|
+
const content = readFileSync(configPath, "utf-8");
|
|
16
|
+
const raw = JSON.parse(content);
|
|
17
|
+
// Check if this is a Claude Desktop style config
|
|
18
|
+
if ("mcpServers" in raw && raw.mcpServers) {
|
|
19
|
+
return convertClaudeConfig(raw);
|
|
20
|
+
}
|
|
21
|
+
// Otherwise treat as native gateway config
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Convert Claude Desktop config format to gateway config
|
|
26
|
+
*/
|
|
27
|
+
function convertClaudeConfig(config) {
|
|
28
|
+
const servers = [];
|
|
29
|
+
if (config.mcpServers) {
|
|
30
|
+
for (const [name, server] of Object.entries(config.mcpServers)) {
|
|
31
|
+
servers.push({
|
|
32
|
+
name,
|
|
33
|
+
command: server.command,
|
|
34
|
+
args: server.args,
|
|
35
|
+
env: server.env,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { servers };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Load servers from parent MCP config file (single-file config pattern)
|
|
43
|
+
*
|
|
44
|
+
* This enables the Lasso MCP Gateway pattern where you configure:
|
|
45
|
+
* {
|
|
46
|
+
* "mcpServers": {
|
|
47
|
+
* "sonoma": {
|
|
48
|
+
* "command": "npx",
|
|
49
|
+
* "args": ["@sonoma/mcp-gateway", "--mcp-json-path", "~/.cursor/mcp.json"],
|
|
50
|
+
* "servers": {
|
|
51
|
+
* "filesystem": { "command": "npx", "args": ["@modelcontextprotocol/server-filesystem", "."] }
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* The gateway reads the parent config, finds its own entry, and extracts the nested "servers".
|
|
58
|
+
*/
|
|
59
|
+
export function loadFromParentConfig(configPath, gatewayName = "sonoma") {
|
|
60
|
+
const expandedPath = configPath.replace(/^~/, homedir());
|
|
61
|
+
if (!existsSync(expandedPath)) {
|
|
62
|
+
throw new Error(`Parent config file not found: ${expandedPath}`);
|
|
63
|
+
}
|
|
64
|
+
const content = readFileSync(expandedPath, "utf-8");
|
|
65
|
+
const raw = JSON.parse(content);
|
|
66
|
+
if (!raw.mcpServers) {
|
|
67
|
+
throw new Error(`No mcpServers found in ${expandedPath}`);
|
|
68
|
+
}
|
|
69
|
+
// Find our gateway entry - try common names
|
|
70
|
+
const gatewayNames = [gatewayName, "sonoma", "sonoma-gateway", "mcp-gateway"];
|
|
71
|
+
let gatewayEntry;
|
|
72
|
+
for (const name of gatewayNames) {
|
|
73
|
+
if (raw.mcpServers[name]) {
|
|
74
|
+
gatewayEntry = raw.mcpServers[name];
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!gatewayEntry?.servers) {
|
|
79
|
+
// Fall back to loading all servers except our own gateway
|
|
80
|
+
const servers = [];
|
|
81
|
+
for (const [name, server] of Object.entries(raw.mcpServers)) {
|
|
82
|
+
// Skip gateway entries (they have --mcp-json-path in args)
|
|
83
|
+
const isGateway = server.args?.some(arg => arg.includes("mcp-gateway") || arg.includes("--mcp-json-path"));
|
|
84
|
+
if (!isGateway) {
|
|
85
|
+
servers.push({
|
|
86
|
+
name,
|
|
87
|
+
command: server.command,
|
|
88
|
+
args: server.args,
|
|
89
|
+
env: server.env,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { servers };
|
|
94
|
+
}
|
|
95
|
+
// Extract nested servers from gateway entry
|
|
96
|
+
const servers = [];
|
|
97
|
+
for (const [name, server] of Object.entries(gatewayEntry.servers)) {
|
|
98
|
+
servers.push({
|
|
99
|
+
name,
|
|
100
|
+
command: server.command,
|
|
101
|
+
args: server.args,
|
|
102
|
+
env: server.env,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { servers };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Find Claude Desktop config file
|
|
109
|
+
*/
|
|
110
|
+
export function findClaudeDesktopConfig() {
|
|
111
|
+
const platform = process.platform;
|
|
112
|
+
let configPath;
|
|
113
|
+
if (platform === "darwin") {
|
|
114
|
+
configPath = join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
115
|
+
}
|
|
116
|
+
else if (platform === "win32") {
|
|
117
|
+
configPath = join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Linux
|
|
121
|
+
configPath = join(homedir(), ".config", "claude", "claude_desktop_config.json");
|
|
122
|
+
}
|
|
123
|
+
return existsSync(configPath) ? configPath : null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Create a minimal test config
|
|
127
|
+
*/
|
|
128
|
+
export function createTestConfig() {
|
|
129
|
+
return {
|
|
130
|
+
servers: [],
|
|
131
|
+
debug: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|