@synchronex/mcp-proxy
Advanced tools
@@ -15,4 +15,6 @@ export type CredentialMap = Record<string, string>; | ||
| category: 'desler' | 'third-party'; | ||
| command: string; | ||
| args: string[]; | ||
| serverType?: 'stdio' | 'http'; | ||
| command?: string; | ||
| args?: string[]; | ||
| url?: string; | ||
| credentialKeys: string[]; | ||
@@ -19,0 +21,0 @@ multiAccount: boolean; |
+33
-9
@@ -6,2 +6,3 @@ import fs from 'node:fs'; | ||
| import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; | ||
| import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; | ||
| import { resolveProfile } from './resolver.js'; | ||
@@ -41,3 +42,3 @@ import { getCredentials } from './vault.js'; | ||
| } | ||
| function buildEnv(server, profile) { | ||
| function requiredCredentials(server, profile) { | ||
| const credentials = getCredentials(profile, server.id); | ||
@@ -47,8 +48,37 @@ if (!credentials) { | ||
| } | ||
| return credentials; | ||
| } | ||
| function buildEnv(server, profile) { | ||
| return { | ||
| ...process.env, | ||
| ...(server.baseEnv ?? {}), | ||
| ...credentials, | ||
| ...requiredCredentials(server, profile), | ||
| }; | ||
| } | ||
| // For http servers, credentialKeys are literal HTTP header names (e.g. | ||
| // 'Authorization', 'X-Sendinel-Project-Id') — the credential map's values go | ||
| // straight onto the request, unlike stdio's process-env merge in buildEnv. | ||
| function buildHttpHeaders(server, profile) { | ||
| return { ...requiredCredentials(server, profile) }; | ||
| } | ||
| function buildTransport(server, profile) { | ||
| if (server.serverType === 'http') { | ||
| if (!server.url) { | ||
| throw new Error(`Server "${server.id}" is serverType "http" but has no url configured`); | ||
| } | ||
| return new StreamableHTTPClientTransport(new URL(server.url), { | ||
| requestInit: { headers: buildHttpHeaders(server, profile) }, | ||
| }); | ||
| } | ||
| if (!server.command || !server.args) { | ||
| throw new Error(`Server "${server.id}" is serverType "stdio" but has no command/args configured`); | ||
| } | ||
| return new StdioClientTransport({ | ||
| command: server.command, | ||
| args: server.args, | ||
| env: buildEnv(server, profile), | ||
| cwd: process.cwd(), | ||
| stderr: 'pipe', | ||
| }); | ||
| } | ||
| async function getClient(serverName, profile) { | ||
@@ -63,9 +93,3 @@ cleanupIdleClients(); | ||
| const server = getServer(serverName); | ||
| const transport = new StdioClientTransport({ | ||
| command: server.command, | ||
| args: server.args, | ||
| env: buildEnv(server, profile), | ||
| cwd: process.cwd(), | ||
| stderr: 'pipe', | ||
| }); | ||
| const transport = buildTransport(server, profile); | ||
| const client = new Client({ name: 'synchronex-mcp-proxy', version: '1.0.0' }); | ||
@@ -72,0 +96,0 @@ await client.connect(transport); |
@@ -32,3 +32,8 @@ import fs from 'node:fs'; | ||
| export function deriveMasterKey(passphrase) { | ||
| return scryptSync(passphrase, 'synchronex-mcp-vault', KEY_BYTES); | ||
| // SYNC-1536: random per-derivation salt, not a fixed constant — the derived | ||
| // key is written to disk and reused directly (readMasterKey), never | ||
| // re-derived from the passphrase later, so the salt need not persist. | ||
| // A fixed salt let an attacker precompute rainbow tables once and reuse | ||
| // them against every Synchronex install. | ||
| return scryptSync(passphrase, randomBytes(16), KEY_BYTES); | ||
| } | ||
@@ -39,3 +44,3 @@ export function writeMasterKey(key, dryRun = false) { | ||
| return; | ||
| fs.writeFileSync(MASTER_KEY_PATH, key); | ||
| fs.writeFileSync(MASTER_KEY_PATH, key, { mode: 0o600 }); | ||
| } | ||
@@ -72,3 +77,3 @@ export function encryptVault(data, masterKey) { | ||
| return; | ||
| fs.writeFileSync(VAULT_PATH, encryptVault(data, readMasterKey())); | ||
| fs.writeFileSync(VAULT_PATH, encryptVault(data, readMasterKey()), { mode: 0o600 }); | ||
| } | ||
@@ -79,3 +84,3 @@ export function initializeVault(passphrase, dryRun = false) { | ||
| if (!dryRun && !fs.existsSync(VAULT_PATH)) { | ||
| fs.writeFileSync(VAULT_PATH, encryptVault({ profiles: {}, shared: {} }, key)); | ||
| fs.writeFileSync(VAULT_PATH, encryptVault({ profiles: {}, shared: {} }, key), { mode: 0o600 }); | ||
| } | ||
@@ -82,0 +87,0 @@ } |
+3
-3
| { | ||
| "name": "@synchronex/mcp-proxy", | ||
| "version": "1.0.1", | ||
| "version": "1.1.0", | ||
| "mcpName": "ai.synchronex/mcp-proxy", | ||
@@ -38,4 +38,4 @@ "description": "Credential-aware MCP proxy - schema caching plus per-project credential routing", | ||
| "devDependencies": { | ||
| "@types/node": "^25.5.2", | ||
| "tsx": "^4.22.4", | ||
| "@types/node": "^25.9.5", | ||
| "tsx": "^4.23.1", | ||
| "typescript": "^5.9.3", | ||
@@ -42,0 +42,0 @@ "vitest": "^4.1.2" |
+13
-0
@@ -27,2 +27,15 @@ # @synchronex/mcp-proxy | ||
| ## Upstream servers: stdio and http | ||
| Each `servers.json` entry declares `serverType: 'stdio' | 'http'` (defaults to | ||
| `'stdio'` for entries written before this existed): | ||
| - `stdio` spawns `command`/`args` as a local process; `credentialKeys` become | ||
| env vars merged into the child process's environment. | ||
| - `http` connects via MCP Streamable HTTP to `url`; `credentialKeys` are | ||
| literal HTTP header names (e.g. `Authorization`, `X-Sendinel-Project-Id`) | ||
| sent as-is from the vault credential map — no local process, no npx. | ||
| See `docs/brain/ops/mcp-proxy.md` for server-classification guidance. | ||
| ## Terminal session attribution | ||
@@ -29,0 +42,0 @@ |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
63672
3.52%1668
1.89%59
28.26%16
6.67%