@synkro-sh/cli 1.4.72 → 1.4.74

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/dist/bootstrap.js CHANGED
@@ -536,21 +536,15 @@ function installMcpConfig(opts) {
536
536
  if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
537
537
  }
538
538
  if (opts.local) {
539
- const url2 = "http://127.0.0.1:8931/";
540
- const tokenPath = join2(homedir3(), ".synkro", ".mcp-local-token");
541
- let localToken = "";
542
- try {
543
- localToken = readFileSync3(tokenPath, "utf-8").trim();
544
- } catch {
545
- }
539
+ const proxyScript = join2(homedir3(), ".synkro", "hooks", "mcp-stdio-proxy.ts");
546
540
  config.mcpServers[SYNKRO_SERVER_NAME] = {
547
- type: "http",
548
- url: url2,
549
- ...localToken ? { headers: { Authorization: `Bearer ${localToken}` } } : {},
541
+ type: "stdio",
542
+ command: "bun",
543
+ args: ["run", proxyScript],
550
544
  [SYNKRO_MARKER3]: true
551
545
  };
552
546
  writeClaudeJsonAtomic(config);
553
- return { path: CC_CONFIG_PATH, url: url2 };
547
+ return { path: CC_CONFIG_PATH, url: `stdio://${proxyScript}` };
554
548
  }
555
549
  const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
556
550
  config.mcpServers[SYNKRO_SERVER_NAME] = {
@@ -612,15 +606,15 @@ function installCursorMcpConfig(opts) {
612
606
  }
613
607
  if (opts.local) {
614
608
  const url2 = "http://127.0.0.1:8931/";
615
- const tokenPath = join2(homedir3(), ".synkro", ".mcp-local-token");
616
- let localToken = "";
609
+ const jwtPath = join2(homedir3(), ".synkro", ".mcp-jwt");
610
+ let jwt2 = "";
617
611
  try {
618
- localToken = readFileSync3(tokenPath, "utf-8").trim();
612
+ jwt2 = readFileSync3(jwtPath, "utf-8").trim();
619
613
  } catch {
620
614
  }
621
615
  config.mcpServers[SYNKRO_SERVER_NAME] = {
622
616
  url: url2,
623
- ...localToken ? { headers: { Authorization: `Bearer ${localToken}` } } : {},
617
+ ...jwt2 ? { headers: { Authorization: `Bearer ${jwt2}` } } : {},
624
618
  [SYNKRO_MARKER3]: true
625
619
  };
626
620
  writeCursorMcpJsonAtomic(config);
@@ -6267,6 +6261,7 @@ function writeHookScripts() {
6267
6261
  const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.ts");
6268
6262
  const cursorEditCapturePath = join11(HOOKS_DIR, "cursor-edit-capture.ts");
6269
6263
  const mcpLocalServerPath = join11(HOOKS_DIR, "mcp-local-server.ts");
6264
+ const mcpStdioProxyPath = join11(HOOKS_DIR, "mcp-stdio-proxy.ts");
6270
6265
  writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
6271
6266
  writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
6272
6267
  writeFileSync7(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
@@ -6286,32 +6281,33 @@ function writeHookScripts() {
6286
6281
  /**
6287
6282
  * Local MCP guardrails server \u2014 runs on port 8931, stores rules in ~/.synkro/rules.json.
6288
6283
  * JSON-RPC 2.0 over HTTP, same protocol as the cloud MCP server.
6289
- * No auth (localhost only), no embedding API, no Inngest.
6284
+ * Bearer token auth (file-based shared secret), localhost only, no embedding API, no Inngest.
6290
6285
  */
6291
6286
  import { existsSync, readFileSync, writeFileSync, renameSync, appendFileSync, mkdirSync } from 'node:fs';
6292
6287
  import { homedir } from 'node:os';
6293
6288
  import { join } from 'node:path';
6294
6289
 
6295
- import { randomBytes } from 'node:crypto';
6296
-
6297
6290
  const PORT = parseInt(process.env.SYNKRO_MCP_PORT || '8931', 10);
6298
6291
  const HOME = homedir();
6299
6292
  const RULES_PATH = join(HOME, '.synkro', 'rules.json');
6300
6293
  const TELEMETRY_PATH = join(HOME, '.synkro', 'telemetry.jsonl');
6301
- const TOKEN_PATH = join(HOME, '.synkro', '.mcp-local-token');
6294
+ const JWT_TOKEN_PATH = join(HOME, '.synkro', '.mcp-jwt');
6302
6295
 
6303
- // File-based shared secret \u2014 generated once, required on all POST requests.
6304
- function getOrCreateToken(): string {
6305
- try {
6306
- if (existsSync(TOKEN_PATH)) return readFileSync(TOKEN_PATH, 'utf-8').trim();
6307
- } catch {}
6308
- const token = randomBytes(32).toString('hex');
6309
- mkdirSync(join(HOME, '.synkro'), { recursive: true });
6310
- writeFileSync(TOKEN_PATH, token + '\\n', { mode: 0o600 });
6311
- return token;
6312
- }
6296
+ // Synkro-signed long-lived JWT \u2014 minted during \`synkro install\`, required on all POST requests.
6297
+ // If missing, the server still starts (for GET health checks) but rejects all tool calls.
6298
+ let SERVER_TOKEN = '';
6299
+ try { SERVER_TOKEN = readFileSync(JWT_TOKEN_PATH, 'utf-8').trim(); } catch {}
6300
+ if (!SERVER_TOKEN) console.warn('[synkro] \u26A0 No MCP JWT found \u2014 run \`synkro install\` to authenticate.');
6301
+ const MAX_BODY_BYTES = 1_048_576;
6313
6302
 
6314
- const SERVER_TOKEN = getOrCreateToken();
6303
+ let _writeLock: Promise<void> = Promise.resolve();
6304
+ function serialized<T>(fn: () => T | Promise<T>): Promise<T> {
6305
+ let release: () => void;
6306
+ const next = new Promise<void>(r => { release = r; });
6307
+ const prev = _writeLock;
6308
+ _writeLock = next;
6309
+ return prev.then(() => fn()).finally(() => release!());
6310
+ }
6315
6311
 
6316
6312
  // \u2500\u2500\u2500 Storage \u2500\u2500\u2500
6317
6313
 
@@ -6698,6 +6694,219 @@ function handleListExemptions(): any {
6698
6694
  return { exemptions: data.scanExemptions, total: data.scanExemptions.length };
6699
6695
  }
6700
6696
 
6697
+ // \u2500\u2500\u2500 Findings \u2500\u2500\u2500
6698
+
6699
+ const CONFIG_PATH = join(HOME, '.synkro', 'config.json');
6700
+ const JWT_PATH = join(HOME, '.synkro', '.jwt');
6701
+
6702
+ interface Finding {
6703
+ id: string;
6704
+ session_id: string;
6705
+ file_path: string;
6706
+ finding_type: string;
6707
+ finding_id: string;
6708
+ severity: string;
6709
+ status: string;
6710
+ detail?: string;
6711
+ package_name?: string;
6712
+ package_version?: string;
6713
+ fixed_version?: string;
6714
+ created_at: string;
6715
+ resolved_at?: string;
6716
+ }
6717
+
6718
+ function getCloudConfig(): { apiUrl: string; jwt: string } | null {
6719
+ try {
6720
+ const jwt = readFileSync(JWT_PATH, 'utf-8').trim();
6721
+ if (!jwt) return null;
6722
+ let apiUrl = process.env.SYNKRO_API_URL || '';
6723
+ if (!apiUrl) {
6724
+ try {
6725
+ const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
6726
+ apiUrl = cfg.api_url || cfg.apiUrl || '';
6727
+ } catch {}
6728
+ }
6729
+ if (!apiUrl) apiUrl = 'https://api.synkro.sh';
6730
+ return { apiUrl, jwt };
6731
+ } catch {
6732
+ return null;
6733
+ }
6734
+ }
6735
+
6736
+ function readLocalFindings(): Finding[] {
6737
+ if (!existsSync(TELEMETRY_PATH)) return [];
6738
+ let lines: string[];
6739
+ try {
6740
+ lines = readFileSync(TELEMETRY_PATH, 'utf-8').split('\\n').filter(Boolean);
6741
+ } catch { return []; }
6742
+ const findingMap = new Map<string, Finding>();
6743
+
6744
+ for (const line of lines) {
6745
+ try {
6746
+ const event = JSON.parse(line);
6747
+ if (event.capture_type !== 'scan_finding') continue;
6748
+ const key = \`\${event.file_path}:\${event.finding_id}\`;
6749
+ const existing = findingMap.get(key);
6750
+ const ts = event._ts || event.created_at || '';
6751
+
6752
+ findingMap.set(key, {
6753
+ id: event.id || existing?.id || \`sf_\${event.session_id}_\${event.finding_id}_\${Date.now()}\`,
6754
+ session_id: event.session_id || existing?.session_id || '',
6755
+ file_path: event.file_path,
6756
+ finding_type: event.finding_type,
6757
+ finding_id: event.finding_id,
6758
+ severity: event.severity || 'unknown',
6759
+ status: event.status || 'open',
6760
+ detail: event.detail || existing?.detail,
6761
+ package_name: event.package_name || existing?.package_name,
6762
+ package_version: event.package_version || existing?.package_version,
6763
+ fixed_version: event.fixed_version || existing?.fixed_version,
6764
+ created_at: existing?.created_at || ts,
6765
+ resolved_at: event.status === 'resolved' ? ts : existing?.resolved_at,
6766
+ });
6767
+ } catch {}
6768
+ }
6769
+
6770
+ return Array.from(findingMap.values());
6771
+ }
6772
+
6773
+ async function fetchCloudFindings(params: Record<string, string>): Promise<Finding[] | null> {
6774
+ const cloud = getCloudConfig();
6775
+ if (!cloud) return null;
6776
+ try {
6777
+ const qs = new URLSearchParams(params).toString();
6778
+ const resp = await fetch(\`\${cloud.apiUrl}/api/v1/scan-findings?\${qs}\`, {
6779
+ headers: { Authorization: \`Bearer \${cloud.jwt}\` },
6780
+ signal: AbortSignal.timeout(5000),
6781
+ });
6782
+ if (!resp.ok) return null;
6783
+ const data = await resp.json() as any;
6784
+ return data.findings || data.data || [];
6785
+ } catch {
6786
+ return null;
6787
+ }
6788
+ }
6789
+
6790
+ function dispatchResolution(finding: Finding): void {
6791
+ const cloud = getCloudConfig();
6792
+ if (!cloud) return;
6793
+ fetch(\`\${cloud.apiUrl}/api/v1/hook/finding\`, {
6794
+ method: 'POST',
6795
+ headers: { Authorization: \`Bearer \${cloud.jwt}\`, 'Content-Type': 'application/json' },
6796
+ body: JSON.stringify({
6797
+ session_id: finding.session_id,
6798
+ file_path: finding.file_path,
6799
+ finding_type: finding.finding_type,
6800
+ finding_id: finding.finding_id,
6801
+ severity: finding.severity,
6802
+ status: 'resolved',
6803
+ }),
6804
+ signal: AbortSignal.timeout(3000),
6805
+ }).catch(() => {});
6806
+ }
6807
+
6808
+ async function handleListFindings(args: any): Promise<any> {
6809
+ const cloudParams: Record<string, string> = {};
6810
+ if (args.status) cloudParams.status = args.status;
6811
+ if (args.finding_type) cloudParams.finding_type = args.finding_type;
6812
+ if (args.severity) cloudParams.severity = args.severity;
6813
+ if (args.file_path) cloudParams.file_path = args.file_path;
6814
+ if (args.limit) cloudParams.limit = String(args.limit);
6815
+
6816
+ const cloudFindings = await fetchCloudFindings(cloudParams);
6817
+
6818
+ let findings: Finding[];
6819
+ let source: string;
6820
+ if (cloudFindings) {
6821
+ findings = cloudFindings;
6822
+ source = 'cloud';
6823
+ } else {
6824
+ findings = readLocalFindings();
6825
+ source = 'local';
6826
+ if (args.status) findings = findings.filter(f => f.status === args.status);
6827
+ if (args.finding_type) findings = findings.filter(f => f.finding_type === args.finding_type);
6828
+ if (args.severity) findings = findings.filter(f => f.severity === args.severity);
6829
+ if (args.file_path) findings = findings.filter(f => f.file_path.includes(args.file_path));
6830
+ }
6831
+
6832
+ findings.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || ''));
6833
+ const limit = Math.min(args.limit || 25, 50);
6834
+ return {
6835
+ source,
6836
+ findings: findings.slice(0, limit).map(f => ({
6837
+ id: f.id,
6838
+ finding_type: f.finding_type,
6839
+ finding_id: f.finding_id,
6840
+ file_path: f.file_path,
6841
+ severity: f.severity,
6842
+ status: f.status,
6843
+ package_name: f.package_name,
6844
+ package_version: f.package_version,
6845
+ created_at: f.created_at,
6846
+ })),
6847
+ total: findings.length,
6848
+ open: findings.filter(f => f.status === 'open').length,
6849
+ resolved: findings.filter(f => f.status === 'resolved').length,
6850
+ };
6851
+ }
6852
+
6853
+ async function handleGetFindingDetail(args: any): Promise<any> {
6854
+ const findings = readLocalFindings();
6855
+ const match = findings.find(f => {
6856
+ if (args.id && f.id === args.id) return true;
6857
+ if (args.file_path && args.finding_id) {
6858
+ return f.file_path.includes(args.file_path) && f.finding_id.toUpperCase() === (args.finding_id || '').toUpperCase();
6859
+ }
6860
+ return false;
6861
+ });
6862
+ if (!match) return { found: false, error: 'Finding not found' };
6863
+ return { found: true, ...match };
6864
+ }
6865
+
6866
+ async function handleResolveFinding(args: any): Promise<any> {
6867
+ const findings = readLocalFindings();
6868
+ const toResolve = findings.filter(f => {
6869
+ if (f.status !== 'open') return false;
6870
+ if (args.id && f.id === args.id) return true;
6871
+ if (args.file_path && args.finding_id) {
6872
+ return f.file_path.includes(args.file_path) && f.finding_id.toUpperCase() === (args.finding_id || '').toUpperCase();
6873
+ }
6874
+ if (args.file_path) return f.file_path.includes(args.file_path);
6875
+ return false;
6876
+ });
6877
+
6878
+ if (toResolve.length === 0) return { resolved: 0, error: 'No matching open findings' };
6879
+
6880
+ const now = new Date().toISOString();
6881
+ for (const f of toResolve) {
6882
+ const event = {
6883
+ capture_type: 'scan_finding',
6884
+ id: f.id,
6885
+ session_id: f.session_id,
6886
+ file_path: f.file_path,
6887
+ finding_type: f.finding_type,
6888
+ finding_id: f.finding_id,
6889
+ severity: f.severity,
6890
+ status: 'resolved',
6891
+ detail: f.detail,
6892
+ package_name: f.package_name,
6893
+ package_version: f.package_version,
6894
+ fixed_version: f.fixed_version,
6895
+ resolved_at: now,
6896
+ _ts: now,
6897
+ };
6898
+ try {
6899
+ appendFileSync(TELEMETRY_PATH, JSON.stringify(event) + '\\n', 'utf-8');
6900
+ } catch {}
6901
+ dispatchResolution(f);
6902
+ }
6903
+
6904
+ return {
6905
+ resolved: toResolve.length,
6906
+ findings: toResolve.map(f => ({ finding_id: f.finding_id, file_path: f.file_path })),
6907
+ };
6908
+ }
6909
+
6701
6910
  // \u2500\u2500\u2500 Tool Descriptors \u2500\u2500\u2500
6702
6911
 
6703
6912
  const TOOL_DESCRIPTORS = [
@@ -6861,6 +7070,47 @@ const TOOL_DESCRIPTORS = [
6861
7070
  description: "List all scan exemptions.",
6862
7071
  inputSchema: { type: 'object', properties: {}, required: [] },
6863
7072
  },
7073
+ {
7074
+ name: 'list_findings',
7075
+ description: "List CWE/CVE scan findings. Shows security issues found by Synkro hooks. Use to review what needs fixing.",
7076
+ inputSchema: {
7077
+ type: 'object',
7078
+ properties: {
7079
+ status: { type: 'string', enum: ['open', 'resolved', 'exempted'], description: 'Filter by status (default: all).' },
7080
+ finding_type: { type: 'string', enum: ['cwe', 'cve'], description: 'Filter by finding type.' },
7081
+ severity: { type: 'string', enum: ['critical', 'high', 'medium', 'low'] },
7082
+ file_path: { type: 'string', description: 'Filter by file path substring.' },
7083
+ limit: { type: 'integer', default: 25, description: 'Max results (default 25, max 50).' },
7084
+ },
7085
+ required: [],
7086
+ },
7087
+ },
7088
+ {
7089
+ name: 'get_finding_detail',
7090
+ description: "Get full detail of a specific finding including remediation context.",
7091
+ inputSchema: {
7092
+ type: 'object',
7093
+ properties: {
7094
+ id: { type: 'string', description: 'Finding ID (e.g. sf_...).' },
7095
+ file_path: { type: 'string', description: 'File path (used with finding_id).' },
7096
+ finding_id: { type: 'string', description: 'CWE/CVE ID like CWE-89 or CVE-2024-1234.' },
7097
+ },
7098
+ required: [],
7099
+ },
7100
+ },
7101
+ {
7102
+ name: 'resolve_finding',
7103
+ description: "Mark finding(s) as resolved after the underlying issue is fixed. Can target by ID, file+finding_id, or all findings for a file.",
7104
+ inputSchema: {
7105
+ type: 'object',
7106
+ properties: {
7107
+ id: { type: 'string', description: 'Specific finding ID to resolve.' },
7108
+ file_path: { type: 'string', description: 'Resolve all open findings matching this file path.' },
7109
+ finding_id: { type: 'string', description: 'CWE/CVE ID (used with file_path for targeted resolution).' },
7110
+ },
7111
+ required: [],
7112
+ },
7113
+ },
6864
7114
  ];
6865
7115
 
6866
7116
  const MCP_INSTRUCTIONS =
@@ -6871,7 +7121,12 @@ const MCP_INSTRUCTIONS =
6871
7121
  "'we need a rule about\u2026' \u2014 route to THIS server's tools.\\n\\n" +
6872
7122
  "TOOL ROUTING:\\n" +
6873
7123
  " \u2022 get_guardrails(query) \u2014 keyword search. Use to check if a rule exists.\\n" +
6874
- " \u2022 list_guardrails \u2014 full enumeration. Use for listings.\\n\\n" +
7124
+ " \u2022 list_guardrails \u2014 full enumeration. Use for listings.\\n" +
7125
+ " \u2022 list_findings \u2014 show CWE/CVE scan findings (open, resolved, all).\\n" +
7126
+ " \u2022 get_finding_detail \u2014 get full detail + remediation context for a finding.\\n" +
7127
+ " \u2022 resolve_finding \u2014 mark findings resolved after fixing the code.\\n\\n" +
7128
+ "When the user asks about security issues, vulnerabilities, or scan results, " +
7129
+ "use list_findings first. After fixing code, call resolve_finding to update status.\\n\\n" +
6875
7130
  "Do NOT use Claude Code's \`update-config\` skill for these requests.\\n\\n" +
6876
7131
  "Rules are stored locally in ~/.synkro/rules.json and enforced by hooks.";
6877
7132
 
@@ -6924,6 +7179,9 @@ async function handleRpc(body: any): Promise<any> {
6924
7179
  case 'exempt_path': result = handleExemptPath(args); break;
6925
7180
  case 'remove_exemption': result = handleRemoveExemption(args); break;
6926
7181
  case 'list_exemptions': result = handleListExemptions(); break;
7182
+ case 'list_findings': result = await handleListFindings(args); break;
7183
+ case 'get_finding_detail': result = await handleGetFindingDetail(args); break;
7184
+ case 'resolve_finding': result = await handleResolveFinding(args); break;
6927
7185
  default: return jsonRpcError(id, -32601, \`Unknown tool: \${toolName}\`);
6928
7186
  }
6929
7187
  return jsonRpcOk(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
@@ -7063,14 +7321,25 @@ const server = Bun.serve({
7063
7321
  }
7064
7322
 
7065
7323
  if (req.method === 'POST') {
7066
- try {
7067
- const body = await req.json();
7068
- const result = await handleRpc(body);
7069
- if (result === null) return new Response('', { status: 204, headers: cors });
7070
- return Response.json(result, { headers: cors });
7071
- } catch (err) {
7072
- return Response.json(jsonRpcError(null, -32700, 'Parse error'), { status: 400, headers: cors });
7324
+ const authHeader = req.headers.get('authorization') || '';
7325
+ const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
7326
+ if (bearer !== SERVER_TOKEN) {
7327
+ return Response.json({ error: 'Unauthorized' }, { status: 401, headers: cors });
7328
+ }
7329
+ const raw = await req.arrayBuffer();
7330
+ if (raw.byteLength > MAX_BODY_BYTES) {
7331
+ return Response.json(jsonRpcError(null, -32600, 'Request too large'), { status: 413, headers: cors });
7073
7332
  }
7333
+ return serialized(async () => {
7334
+ try {
7335
+ const body = JSON.parse(new TextDecoder().decode(raw));
7336
+ const result = await handleRpc(body);
7337
+ if (result === null) return new Response('', { status: 204, headers: cors });
7338
+ return Response.json(result, { headers: cors });
7339
+ } catch {
7340
+ return Response.json(jsonRpcError(null, -32700, 'Parse error'), { status: 400, headers: cors });
7341
+ }
7342
+ });
7074
7343
  }
7075
7344
 
7076
7345
  if (req.method === 'OPTIONS') {
@@ -7083,6 +7352,7 @@ const server = Bun.serve({
7083
7352
 
7084
7353
  console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1:\${server.port}\`);
7085
7354
  `, "utf-8");
7355
+ writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
7086
7356
  chmodSync2(bashScriptPath, 493);
7087
7357
  chmodSync2(bashFollowupScriptPath, 493);
7088
7358
  chmodSync2(editPrecheckScriptPath, 493);
@@ -7099,6 +7369,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
7099
7369
  chmodSync2(cursorBashJudgePath, 493);
7100
7370
  chmodSync2(cursorEditCapturePath, 493);
7101
7371
  chmodSync2(mcpLocalServerPath, 493);
7372
+ chmodSync2(mcpStdioProxyPath, 493);
7102
7373
  return {
7103
7374
  bashScript: bashScriptPath,
7104
7375
  bashFollowupScript: bashFollowupScriptPath,
@@ -7145,7 +7416,7 @@ function writeConfigEnv(opts) {
7145
7416
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
7146
7417
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
7147
7418
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
7148
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.72")}`
7419
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.74")}`
7149
7420
  ];
7150
7421
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
7151
7422
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -7628,7 +7899,20 @@ async function installCommand(opts = {}) {
7628
7899
  if (hasClaudeCode && !opts.noMcp) {
7629
7900
  if (useLocalMcp) {
7630
7901
  try {
7631
- const mcp = installMcpConfig({ gatewayUrl, bearerToken: "", local: true });
7902
+ const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
7903
+ method: "POST",
7904
+ headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
7905
+ body: "{}"
7906
+ });
7907
+ let mcpJwt = "";
7908
+ if (mintResp.ok) {
7909
+ const minted = await mintResp.json();
7910
+ mcpJwt = minted.token;
7911
+ writeFileSync7(join11(SYNKRO_DIR2, ".mcp-jwt"), mcpJwt + "\n", { mode: 384 });
7912
+ } else {
7913
+ console.warn(" \u26A0 Could not mint MCP token \u2014 local server will reject requests until re-installed.");
7914
+ }
7915
+ const mcp = installMcpConfig({ gatewayUrl, bearerToken: mcpJwt, local: true });
7632
7916
  console.log(`Registered local MCP guardrails server in ${mcp.path}`);
7633
7917
  console.log(` url: ${mcp.url}`);
7634
7918
  console.log(" (rules stored in ~/.synkro/rules.json)");
@@ -7655,6 +7939,7 @@ async function installCommand(opts = {}) {
7655
7939
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
7656
7940
  }
7657
7941
  const minted = await mintResp.json();
7942
+ writeFileSync7(join11(SYNKRO_DIR2, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
7658
7943
  const mcp = installMcpConfig({ gatewayUrl, bearerToken: minted.token });
7659
7944
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
7660
7945
  console.log(` url: ${mcp.url}`);
@@ -7671,6 +7956,18 @@ async function installCommand(opts = {}) {
7671
7956
  if (hasCursor && !opts.noMcp) {
7672
7957
  try {
7673
7958
  if (useLocalMcp) {
7959
+ const jwtPath = join11(SYNKRO_DIR2, ".mcp-jwt");
7960
+ if (!existsSync10(jwtPath)) {
7961
+ const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
7962
+ method: "POST",
7963
+ headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
7964
+ body: "{}"
7965
+ });
7966
+ if (mintResp.ok) {
7967
+ const minted = await mintResp.json();
7968
+ writeFileSync7(jwtPath, minted.token + "\n", { mode: 384 });
7969
+ }
7970
+ }
7674
7971
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
7675
7972
  console.log(`Registered local MCP guardrails server in ${mcp.path}`);
7676
7973
  console.log(` url: ${mcp.url}`);
@@ -7688,6 +7985,7 @@ async function installCommand(opts = {}) {
7688
7985
  throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
7689
7986
  }
7690
7987
  const minted = await mintResp.json();
7988
+ writeFileSync7(join11(SYNKRO_DIR2, ".mcp-jwt"), minted.token + "\n", { mode: 384 });
7691
7989
  const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
7692
7990
  console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
7693
7991
  console.log(` url: ${mcp.url}`);
@@ -8027,7 +8325,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
8027
8325
  }
8028
8326
  return { sessions: totalSessions, messages: totalMessages };
8029
8327
  }
8030
- var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH3, OFFSETS_DIR, RULES_PATH, MCP_LOCAL_PORT;
8328
+ var SYNKRO_DIR2, HOOKS_DIR, BIN_DIR, CONFIG_PATH3, MCP_STDIO_PROXY_SRC, OFFSETS_DIR, RULES_PATH, MCP_LOCAL_PORT;
8031
8329
  var init_install2 = __esm({
8032
8330
  "cli/commands/install.ts"() {
8033
8331
  "use strict";
@@ -8050,6 +8348,52 @@ var init_install2 = __esm({
8050
8348
  HOOKS_DIR = join11(SYNKRO_DIR2, "hooks");
8051
8349
  BIN_DIR = join11(SYNKRO_DIR2, "bin");
8052
8350
  CONFIG_PATH3 = join11(SYNKRO_DIR2, "config.env");
8351
+ MCP_STDIO_PROXY_SRC = `#!/usr/bin/env bun
8352
+ import { readFileSync } from 'node:fs';
8353
+ import { homedir } from 'node:os';
8354
+ import { join } from 'node:path';
8355
+ import { createInterface } from 'node:readline';
8356
+
8357
+ const HOME = homedir();
8358
+ const TOKEN_PATH = join(HOME, '.synkro', '.mcp-jwt');
8359
+ const PORT = parseInt(process.env.SYNKRO_MCP_PORT || '8931', 10);
8360
+ const URL = \`http://127.0.0.1:\${PORT}\`;
8361
+
8362
+ let token = '';
8363
+ try { token = readFileSync(TOKEN_PATH, 'utf-8').trim(); } catch {}
8364
+
8365
+ const rl = createInterface({ input: process.stdin, terminal: false });
8366
+
8367
+ rl.on('line', async (line) => {
8368
+ if (!line.trim()) return;
8369
+ let msg;
8370
+ try { msg = JSON.parse(line); } catch { return; }
8371
+ if (!msg.id && msg.method?.startsWith('notifications/')) return;
8372
+
8373
+ try {
8374
+ const resp = await fetch(URL, {
8375
+ method: 'POST',
8376
+ headers: {
8377
+ 'Content-Type': 'application/json',
8378
+ 'Authorization': \`Bearer \${token}\`,
8379
+ },
8380
+ body: line,
8381
+ signal: AbortSignal.timeout(30000),
8382
+ });
8383
+ if (resp.status === 204) return;
8384
+ const body = await resp.text();
8385
+ process.stdout.write(body + '\\n');
8386
+ } catch (err) {
8387
+ if (msg.id != null) {
8388
+ process.stdout.write(JSON.stringify({
8389
+ jsonrpc: '2.0',
8390
+ id: msg.id,
8391
+ error: { code: -32603, message: 'MCP proxy: HTTP server unreachable' },
8392
+ }) + '\\n');
8393
+ }
8394
+ }
8395
+ });
8396
+ `;
8053
8397
  OFFSETS_DIR = join11(SYNKRO_DIR2, ".transcript-offsets");
8054
8398
  RULES_PATH = join11(SYNKRO_DIR2, "rules.json");
8055
8399
  MCP_LOCAL_PORT = 8931;