@synapsor/runner 1.6.4 → 1.6.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/dist/runner.mjs CHANGED
@@ -22743,7 +22743,7 @@ var require_svgPath = __commonJS({
22743
22743
  ["Z", 0],
22744
22744
  ["z", 0]
22745
22745
  ]);
22746
- var parse = function(path30) {
22746
+ var parse2 = function(path30) {
22747
22747
  var cmd;
22748
22748
  var ret = [];
22749
22749
  var args = [];
@@ -23065,7 +23065,7 @@ var require_svgPath = __commonJS({
23065
23065
  return result;
23066
23066
  };
23067
23067
  exports.svgPathToOperators = function(path30) {
23068
- return apply2(parse(path30));
23068
+ return apply2(parse2(path30));
23069
23069
  };
23070
23070
  }
23071
23071
  });
@@ -50839,15 +50839,16 @@ import path16 from "node:path";
50839
50839
  init_src();
50840
50840
  init_src2();
50841
50841
 
50842
- // apps/runner/src/cursor-project.ts
50842
+ // apps/runner/src/managed-mcp-project.ts
50843
50843
  import crypto7 from "node:crypto";
50844
50844
  import fs3 from "node:fs/promises";
50845
50845
  import path3 from "node:path";
50846
+ import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
50846
50847
 
50847
50848
  // apps/runner/package.json
50848
50849
  var package_default = {
50849
50850
  name: "@synapsor/runner",
50850
- version: "1.6.4",
50851
+ version: "1.6.5",
50851
50852
  description: "Open-source MCP runtime for governed Postgres/MySQL access with reviewed capabilities, proposals, guarded writes, receipts, and replay.",
50852
50853
  license: "Apache-2.0",
50853
50854
  type: "module",
@@ -50949,6 +50950,7 @@ var package_default = {
50949
50950
  "@modelcontextprotocol/sdk": "1.29.0",
50950
50951
  "@synapsor/spec": "workspace:^1.7.0",
50951
50952
  jose: "6.2.3",
50953
+ "jsonc-parser": "3.3.1",
50952
50954
  mysql2: "^3.11.0",
50953
50955
  "pdf-lib": "1.17.1",
50954
50956
  pg: "^8.13.0",
@@ -50976,61 +50978,102 @@ var package_default = {
50976
50978
  }
50977
50979
  };
50978
50980
 
50979
- // apps/runner/src/cursor-project.ts
50980
- var markerVersion = "synapsor.cursor-project.v1";
50981
+ // apps/runner/src/managed-mcp-project.ts
50981
50982
  var serverName = "synapsor";
50982
- async function previewCursorProjectInstall(input = {}) {
50983
- const paths = await resolveInstallPaths(input);
50984
- const existing = await readOptionalJson(paths.destination);
50985
- const marker = await readMarker(paths.marker);
50986
- const entry = cursorEntry(paths.configArgument, paths.storeArgument, input.packageSpec, input.authoring === true);
50987
- assertSecretFree(entry);
50988
- if (existing?.mcpServers !== void 0 && !isRecord7(existing.mcpServers)) {
50989
- throw new Error(`${displayPath(paths.projectRoot, paths.destination)} must contain an object at mcpServers.`);
50990
- }
50991
- const existingServers = isRecord7(existing?.mcpServers) ? existing.mcpServers : {};
50983
+ var managedClients = {
50984
+ cursor: {
50985
+ client: "cursor",
50986
+ displayName: "Cursor",
50987
+ destination: ".cursor/mcp.json",
50988
+ marker: ".synapsor/cursor-project.json",
50989
+ markerVersion: "synapsor.cursor-project.v1",
50990
+ serversKey: "mcpServers",
50991
+ reloadInstruction: "Restart or reload Cursor"
50992
+ },
50993
+ "claude-code": {
50994
+ client: "claude-code",
50995
+ displayName: "Claude Code",
50996
+ destination: ".mcp.json",
50997
+ marker: ".synapsor/claude-code-project.json",
50998
+ markerVersion: "synapsor.claude-code-project.v1",
50999
+ serversKey: "mcpServers",
51000
+ reloadInstruction: "Restart Claude Code or begin a new project session, then approve the project MCP server when Claude Code prompts"
51001
+ },
51002
+ vscode: {
51003
+ client: "vscode",
51004
+ displayName: "VS Code",
51005
+ destination: ".vscode/mcp.json",
51006
+ marker: ".synapsor/vscode-project.json",
51007
+ markerVersion: "synapsor.vscode-project.v1",
51008
+ serversKey: "servers",
51009
+ reloadInstruction: "Reload the VS Code window and start the Synapsor MCP server"
51010
+ }
51011
+ };
51012
+ function parseManagedMcpProjectClient(value) {
51013
+ if (value === "cursor") return "cursor";
51014
+ if (value === "claude-code" || value === "claude") return "claude-code";
51015
+ if (value === "vscode" || value === "vs-code") return "vscode";
51016
+ throw new Error("managed MCP project client must be cursor, claude-code, or vscode");
51017
+ }
51018
+ function managedMcpProjectDefinition(client) {
51019
+ return managedClients[client];
51020
+ }
51021
+ async function previewManagedMcpProjectInstall(input) {
51022
+ const definition = managedClients[input.client];
51023
+ const paths = await resolveInstallPaths(input, definition);
51024
+ const existing = await readOptionalJsonDocument(paths.destination);
51025
+ const marker = await readMarker(paths.marker, definition);
51026
+ const entry = managedEntry(paths.configArgument, paths.storeArgument, input.packageSpec, input.authoring === true);
51027
+ assertSecretFree(entry, definition);
51028
+ const existingServersValue = existing?.value[definition.serversKey];
51029
+ if (existingServersValue !== void 0 && !isRecord7(existingServersValue)) {
51030
+ throw new Error(`${displayPath(paths.projectRoot, paths.destination)} must contain an object at ${definition.serversKey}.`);
51031
+ }
51032
+ const existingServers = isRecord7(existingServersValue) ? existingServersValue : {};
50992
51033
  const existingEntry = isRecord7(existingServers[serverName]) ? existingServers[serverName] : void 0;
50993
51034
  if (marker && !existingEntry) {
50994
- throw new Error(`Runner ownership marker exists, but the Cursor ${serverName} MCP entry is missing. Review the files manually.`);
51035
+ throw new Error(`Runner ownership marker exists, but the ${definition.displayName} ${serverName} MCP entry is missing. Review the files manually.`);
50995
51036
  }
50996
51037
  if (existingEntry && !marker) {
50997
51038
  if (!deepEqual(existingEntry, entry)) {
50998
- throw new Error(`Cursor project already has an unowned ${serverName} MCP entry at ${displayPath(paths.projectRoot, paths.destination)}. Rename it or remove it explicitly before installing.`);
51039
+ throw new Error(`${definition.displayName} project already has an unowned ${serverName} MCP entry at ${displayPath(paths.projectRoot, paths.destination)}. Rename it or remove it explicitly before installing.`);
50999
51040
  }
51000
- throw new Error(`Cursor project has a matching but unowned ${serverName} MCP entry. Remove it explicitly or restore ${displayPath(paths.projectRoot, paths.marker)} before Runner manages it.`);
51041
+ throw new Error(`${definition.displayName} project has a matching but unowned ${serverName} MCP entry. Remove it explicitly or restore ${displayPath(paths.projectRoot, paths.marker)} before Runner manages it.`);
51001
51042
  }
51002
51043
  if (existingEntry && marker && digest3(existingEntry) !== marker.entry_digest) {
51003
- throw new Error(`Cursor ${serverName} MCP entry changed after Runner installed it. Refusing to overwrite user edits; review ${displayPath(paths.projectRoot, paths.destination)}.`);
51044
+ throw new Error(`${definition.displayName} ${serverName} MCP entry changed after Runner installed it. Refusing to overwrite user edits; review ${displayPath(paths.projectRoot, paths.destination)}.`);
51004
51045
  }
51005
51046
  const merged = {
51006
- ...existing ?? {},
51007
- mcpServers: { ...existingServers, [serverName]: entry }
51047
+ ...existing?.value ?? {},
51048
+ [definition.serversKey]: { ...existingServers, [serverName]: entry }
51008
51049
  };
51009
51050
  const unchanged = Boolean(existingEntry && marker && deepEqual(existingEntry, entry));
51010
51051
  return {
51011
51052
  state: unchanged ? "installed" : existingEntry ? "installed" : "not_installed",
51012
51053
  paths,
51013
51054
  entry,
51014
- message: unchanged ? "Cursor project MCP entry already matches the reviewed Runner wiring." : "Cursor project MCP entry is ready to install.",
51055
+ message: unchanged ? `${definition.displayName} project MCP entry already matches the reviewed Runner wiring.` : `${definition.displayName} project MCP entry is ready to install.`,
51015
51056
  action: unchanged ? "unchanged" : existingEntry ? "update" : "install",
51016
- merged
51057
+ merged,
51058
+ updatedDocument: setManagedEntry(existing?.text, definition, entry)
51017
51059
  };
51018
51060
  }
51019
- async function installCursorProject(input = {}) {
51020
- const preview = await previewCursorProjectInstall(input);
51061
+ async function installManagedMcpProject(input) {
51062
+ const definition = managedClients[input.client];
51063
+ const preview = await previewManagedMcpProjectInstall(input);
51021
51064
  if (preview.action === "unchanged") return preview;
51022
51065
  const hadDestination = await pathExists(preview.paths.destination);
51023
51066
  const backup = hadDestination ? await backupFile(preview.paths.destination, input.now) : void 0;
51024
51067
  const marker = {
51025
- schema_version: markerVersion,
51068
+ schema_version: definition.markerVersion,
51026
51069
  server_name: serverName,
51027
- destination: ".cursor/mcp.json",
51070
+ destination: definition.destination,
51028
51071
  entry_digest: digest3(preview.entry),
51029
51072
  config_path: preview.paths.configArgument,
51030
51073
  store_path: preview.paths.storeArgument,
51031
51074
  installed_at: input.now ?? (/* @__PURE__ */ new Date()).toISOString()
51032
51075
  };
51033
- await writeJsonAtomic(preview.paths.destination, preview.merged);
51076
+ await writeTextAtomic(preview.paths.destination, preview.updatedDocument);
51034
51077
  try {
51035
51078
  await writeJsonAtomic(preview.paths.marker, marker);
51036
51079
  } catch (error) {
@@ -51040,49 +51083,76 @@ async function installCursorProject(input = {}) {
51040
51083
  }
51041
51084
  return { ...preview, state: "installed", ...backup ? { backup } : {} };
51042
51085
  }
51043
- async function uninstallCursorProject(input = {}) {
51044
- const base = await resolveBasePaths(input.projectRoot);
51045
- const existing = await readOptionalJson(base.destination);
51046
- const marker = await readMarker(base.marker);
51047
- const paths = marker ? await pathsFromMarker(base, marker) : defaultPaths(base);
51086
+ async function uninstallManagedMcpProject(input) {
51087
+ const definition = managedClients[input.client];
51088
+ const base = await resolveBasePaths(input.projectRoot, definition);
51089
+ const existing = await readOptionalJsonDocument(base.destination);
51090
+ const marker = await readMarker(base.marker, definition);
51091
+ const paths = marker ? await pathsFromMarker(base, marker, definition) : defaultPaths(base);
51048
51092
  if (!existing && !marker) return { changed: false, paths };
51049
- if (!marker) throw new Error(`Refusing to uninstall an unowned Cursor ${serverName} entry without ${displayPath(paths.projectRoot, paths.marker)}.`);
51050
- if (existing?.mcpServers !== void 0 && !isRecord7(existing.mcpServers)) {
51051
- throw new Error(`${displayPath(paths.projectRoot, paths.destination)} must contain an object at mcpServers.`);
51093
+ if (!marker) {
51094
+ throw new Error(`Refusing to uninstall an unowned ${definition.displayName} ${serverName} entry without ${displayPath(paths.projectRoot, paths.marker)}.`);
51095
+ }
51096
+ const serversValue = existing?.value[definition.serversKey];
51097
+ if (serversValue !== void 0 && !isRecord7(serversValue)) {
51098
+ throw new Error(`${displayPath(paths.projectRoot, paths.destination)} must contain an object at ${definition.serversKey}.`);
51052
51099
  }
51053
- const servers = isRecord7(existing?.mcpServers) ? existing.mcpServers : {};
51100
+ const servers = isRecord7(serversValue) ? serversValue : {};
51054
51101
  const entry = isRecord7(servers[serverName]) ? servers[serverName] : void 0;
51055
- if (!entry) throw new Error(`Runner ownership marker exists, but Cursor ${serverName} entry is missing. Review the files manually.`);
51102
+ if (!entry) {
51103
+ throw new Error(`Runner ownership marker exists, but ${definition.displayName} ${serverName} entry is missing. Review the files manually.`);
51104
+ }
51056
51105
  if (digest3(entry) !== marker.entry_digest) {
51057
- throw new Error(`Cursor ${serverName} entry changed after installation. Refusing to remove user edits.`);
51106
+ throw new Error(`${definition.displayName} ${serverName} entry changed after installation. Refusing to remove user edits.`);
51058
51107
  }
51059
- const remainingServers = { ...servers };
51060
- delete remainingServers[serverName];
51061
- const updated = { ...existing, mcpServers: remainingServers };
51062
51108
  const backup = await backupFile(paths.destination, input.now);
51063
- await writeJsonAtomic(paths.destination, updated);
51109
+ const updatedDocument = removeManagedEntry(existing?.text ?? "{}\n", definition);
51110
+ await writeTextAtomic(paths.destination, updatedDocument);
51064
51111
  await fs3.rm(paths.marker, { force: true });
51065
51112
  return { changed: true, paths, backup };
51066
51113
  }
51067
- async function cursorProjectStatus(projectRoot = process.cwd()) {
51068
- const base = await resolveBasePaths(projectRoot);
51069
- const existing = await readOptionalJson(base.destination);
51070
- const marker = await readMarker(base.marker);
51071
- const paths = marker ? await pathsFromMarker(base, marker) : defaultPaths(base);
51072
- if (existing?.mcpServers !== void 0 && !isRecord7(existing.mcpServers)) {
51073
- return { state: "tampered", paths, message: "Cursor mcpServers is not a JSON object." };
51074
- }
51075
- const servers = isRecord7(existing?.mcpServers) ? existing.mcpServers : {};
51114
+ async function managedMcpProjectStatus(client, projectRoot = process.cwd()) {
51115
+ const definition = managedClients[client];
51116
+ const base = await resolveBasePaths(projectRoot, definition);
51117
+ const existing = await readOptionalJsonDocument(base.destination);
51118
+ const marker = await readMarker(base.marker, definition);
51119
+ const paths = marker ? await pathsFromMarker(base, marker, definition) : defaultPaths(base);
51120
+ const serversValue = existing?.value[definition.serversKey];
51121
+ if (serversValue !== void 0 && !isRecord7(serversValue)) {
51122
+ return { state: "tampered", paths, message: `${definition.displayName} ${definition.serversKey} is not a JSON object.` };
51123
+ }
51124
+ const servers = isRecord7(serversValue) ? serversValue : {};
51076
51125
  const entry = isRecord7(servers[serverName]) ? servers[serverName] : void 0;
51077
- if (!entry && !marker) return { state: "not_installed", paths, message: "No Runner-owned Cursor project entry is installed." };
51078
- if (!entry || !marker) return { state: "unowned", paths, ...entry ? { entry } : {}, message: "Cursor entry and Runner ownership marker do not agree." };
51079
- if (digest3(entry) !== marker.entry_digest) return { state: "tampered", paths, entry, message: "Cursor entry changed after Runner installation." };
51080
- return { state: "installed", paths, entry, message: "Runner-owned Cursor project entry is intact." };
51081
- }
51082
- async function resolveInstallPaths(input) {
51083
- const base = await resolveBasePaths(input.projectRoot);
51084
- const config = resolveContained(base.projectRoot, input.configPath ?? "./synapsor.runner.json", "Runner config");
51085
- const store = resolveContained(base.projectRoot, input.storePath ?? "./.synapsor/local.db", "Runner store");
51126
+ if (!entry && !marker) {
51127
+ return { state: "not_installed", paths, message: `No Runner-owned ${definition.displayName} project entry is installed.` };
51128
+ }
51129
+ if (!entry || !marker) {
51130
+ return {
51131
+ state: "unowned",
51132
+ paths,
51133
+ ...entry ? { entry } : {},
51134
+ message: `${definition.displayName} entry and Runner ownership marker do not agree.`
51135
+ };
51136
+ }
51137
+ if (digest3(entry) !== marker.entry_digest) {
51138
+ return {
51139
+ state: "tampered",
51140
+ paths,
51141
+ entry,
51142
+ message: `${definition.displayName} entry changed after Runner installation.`
51143
+ };
51144
+ }
51145
+ return {
51146
+ state: "installed",
51147
+ paths,
51148
+ entry,
51149
+ message: `Runner-owned ${definition.displayName} project entry is intact.`
51150
+ };
51151
+ }
51152
+ async function resolveInstallPaths(input, definition) {
51153
+ const base = await resolveBasePaths(input.projectRoot, definition);
51154
+ const config = resolveContained(base.projectRoot, input.configPath ?? "./synapsor.runner.json", "Runner config", definition);
51155
+ const store = resolveContained(base.projectRoot, input.storePath ?? "./.synapsor/local.db", "Runner store", definition);
51086
51156
  await rejectSymlinkChain(base.projectRoot, config, "Runner config");
51087
51157
  await rejectSymlinkChain(base.projectRoot, store, "Runner store");
51088
51158
  if (!input.authoring) await requireRegularFile(config, "Runner config");
@@ -51092,14 +51162,14 @@ async function resolveInstallPaths(input) {
51092
51162
  storeArgument: projectArgument(base.projectRoot, store)
51093
51163
  };
51094
51164
  }
51095
- async function resolveBasePaths(projectRootInput) {
51165
+ async function resolveBasePaths(projectRootInput, definition) {
51096
51166
  const projectRoot = path3.resolve(projectRootInput ?? process.cwd());
51097
- await requireRealDirectory(projectRoot, "Cursor project root");
51098
- const destination = path3.join(projectRoot, ".cursor/mcp.json");
51099
- const marker = path3.join(projectRoot, ".synapsor/cursor-project.json");
51100
- await rejectSymlinkChain(projectRoot, destination, "Cursor project config");
51101
- await rejectSymlinkChain(projectRoot, marker, "Cursor ownership marker");
51102
- return { projectRoot, destination, marker };
51167
+ await requireRealDirectory(projectRoot, `${definition.displayName} project root`);
51168
+ const destination = path3.join(projectRoot, definition.destination);
51169
+ const marker = path3.join(projectRoot, definition.marker);
51170
+ await rejectSymlinkChain(projectRoot, destination, `${definition.displayName} project config`);
51171
+ await rejectSymlinkChain(projectRoot, marker, `${definition.displayName} ownership marker`);
51172
+ return { client: definition.client, projectRoot, destination, marker };
51103
51173
  }
51104
51174
  function defaultPaths(base) {
51105
51175
  return {
@@ -51108,9 +51178,9 @@ function defaultPaths(base) {
51108
51178
  storeArgument: "./.synapsor/local.db"
51109
51179
  };
51110
51180
  }
51111
- async function pathsFromMarker(base, marker) {
51112
- const config = resolveContained(base.projectRoot, marker.config_path, "Recorded Runner config");
51113
- const store = resolveContained(base.projectRoot, marker.store_path, "Recorded Runner store");
51181
+ async function pathsFromMarker(base, marker, definition) {
51182
+ const config = resolveContained(base.projectRoot, marker.config_path, "Recorded Runner config", definition);
51183
+ const store = resolveContained(base.projectRoot, marker.store_path, "Recorded Runner store", definition);
51114
51184
  await rejectSymlinkChain(base.projectRoot, config, "Recorded Runner config");
51115
51185
  await rejectSymlinkChain(base.projectRoot, store, "Recorded Runner store");
51116
51186
  return {
@@ -51119,10 +51189,10 @@ async function pathsFromMarker(base, marker) {
51119
51189
  storeArgument: projectArgument(base.projectRoot, store)
51120
51190
  };
51121
51191
  }
51122
- function cursorEntry(configPath, storePath, packageSpec, authoring) {
51192
+ function managedEntry(configPath, storePath, packageSpec, authoring) {
51123
51193
  const resolvedPackage = packageSpec ?? `@synapsor/runner@${package_default.version}`;
51124
51194
  if (!resolvedPackage.trim() || resolvedPackage.length > 2048 || /[\u0000-\u001f\u007f]/.test(resolvedPackage)) {
51125
- throw new Error("Cursor Runner package spec must be a non-empty package reference without control characters");
51195
+ throw new Error("Runner package spec must be a non-empty package reference without control characters");
51126
51196
  }
51127
51197
  return {
51128
51198
  type: "stdio",
@@ -51130,11 +51200,24 @@ function cursorEntry(configPath, storePath, packageSpec, authoring) {
51130
51200
  args: authoring ? ["-y", resolvedPackage, "mcp", "serve", "--authoring", "--project-root", "."] : ["-y", resolvedPackage, "mcp", "serve", "--config", configPath, "--store", storePath]
51131
51201
  };
51132
51202
  }
51133
- function resolveContained(root, value, label) {
51203
+ function setManagedEntry(existingText, definition, entry) {
51204
+ const source = existingText ?? "{}\n";
51205
+ const edits = modify(source, [definition.serversKey, serverName], entry, {
51206
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }
51207
+ });
51208
+ return finalNewline(applyEdits(source, edits));
51209
+ }
51210
+ function removeManagedEntry(existingText, definition) {
51211
+ const edits = modify(existingText, [definition.serversKey, serverName], void 0, {
51212
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" }
51213
+ });
51214
+ return finalNewline(applyEdits(existingText, edits));
51215
+ }
51216
+ function resolveContained(root, value, label, definition) {
51134
51217
  const resolved = path3.resolve(root, value);
51135
51218
  const relative2 = path3.relative(root, resolved);
51136
51219
  if (relative2 === "" || !relative2.startsWith("..") && !path3.isAbsolute(relative2)) return resolved;
51137
- throw new Error(`${label} must stay inside the project for project-scoped Cursor installation: ${value}`);
51220
+ throw new Error(`${label} must stay inside the project for project-scoped ${definition.displayName} installation: ${value}`);
51138
51221
  }
51139
51222
  function projectArgument(root, value) {
51140
51223
  const relative2 = path3.relative(root, value).split(path3.sep).join("/");
@@ -51162,26 +51245,33 @@ async function rejectSymlinkChain(root, value, label) {
51162
51245
  }
51163
51246
  }
51164
51247
  }
51165
- async function readOptionalJson(value) {
51248
+ async function readOptionalJsonDocument(value) {
51166
51249
  try {
51167
- const parsed = JSON.parse(await fs3.readFile(value, "utf8"));
51250
+ const text = await fs3.readFile(value, "utf8");
51251
+ const errors = [];
51252
+ const parsed = parse(text, errors, { allowTrailingComma: true, disallowComments: false });
51253
+ if (errors.length > 0) {
51254
+ const first = errors[0];
51255
+ throw new Error(`${value} contains invalid JSON/JSONC at offset ${first.offset}: ${printParseErrorCode(first.error)}`);
51256
+ }
51168
51257
  if (!isRecord7(parsed)) throw new Error(`${value} must contain a JSON object`);
51169
- return parsed;
51258
+ return { value: parsed, text };
51170
51259
  } catch (error) {
51171
51260
  if (error.code === "ENOENT") return void 0;
51172
51261
  throw error;
51173
51262
  }
51174
51263
  }
51175
- async function readMarker(value) {
51176
- const parsed = await readOptionalJson(value);
51264
+ async function readMarker(value, definition) {
51265
+ const document2 = await readOptionalJsonDocument(value);
51266
+ const parsed = document2?.value;
51177
51267
  if (!parsed) return void 0;
51178
- if (parsed.schema_version !== markerVersion || parsed.server_name !== serverName || parsed.destination !== ".cursor/mcp.json" || typeof parsed.entry_digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(parsed.entry_digest) || typeof parsed.config_path !== "string" || typeof parsed.store_path !== "string" || typeof parsed.installed_at !== "string") {
51179
- throw new Error(`Invalid Cursor ownership marker: ${value}`);
51268
+ if (parsed.schema_version !== definition.markerVersion || parsed.server_name !== serverName || parsed.destination !== definition.destination || typeof parsed.entry_digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(parsed.entry_digest) || typeof parsed.config_path !== "string" || typeof parsed.store_path !== "string" || typeof parsed.installed_at !== "string") {
51269
+ throw new Error(`Invalid ${definition.displayName} ownership marker: ${value}`);
51180
51270
  }
51181
51271
  return {
51182
- schema_version: markerVersion,
51272
+ schema_version: definition.markerVersion,
51183
51273
  server_name: serverName,
51184
- destination: ".cursor/mcp.json",
51274
+ destination: definition.destination,
51185
51275
  entry_digest: parsed.entry_digest,
51186
51276
  config_path: parsed.config_path,
51187
51277
  store_path: parsed.store_path,
@@ -51189,11 +51279,14 @@ async function readMarker(value) {
51189
51279
  };
51190
51280
  }
51191
51281
  async function writeJsonAtomic(destination, value) {
51282
+ await writeTextAtomic(destination, `${JSON.stringify(value, null, 2)}
51283
+ `);
51284
+ }
51285
+ async function writeTextAtomic(destination, value) {
51192
51286
  await fs3.mkdir(path3.dirname(destination), { recursive: true });
51193
51287
  const temporary = `${destination}.tmp-${process.pid}-${crypto7.randomBytes(6).toString("hex")}`;
51194
51288
  try {
51195
- await fs3.writeFile(temporary, `${JSON.stringify(value, null, 2)}
51196
- `, { encoding: "utf8", mode: 384 });
51289
+ await fs3.writeFile(temporary, value, { encoding: "utf8", mode: 384 });
51197
51290
  await fs3.rename(temporary, destination);
51198
51291
  } finally {
51199
51292
  await fs3.rm(temporary, { force: true });
@@ -51218,10 +51311,10 @@ function stableJson(value) {
51218
51311
  function deepEqual(left, right) {
51219
51312
  return stableJson(left) === stableJson(right);
51220
51313
  }
51221
- function assertSecretFree(value) {
51314
+ function assertSecretFree(value, definition) {
51222
51315
  const text = JSON.stringify(value);
51223
51316
  if (/postgres(?:ql)?:\/\/|mysql:\/\/|password|bearer\s+[a-z0-9._~+/=-]+|syn_wbr_|api[_-]?key/i.test(text)) {
51224
- throw new Error("Cursor MCP configuration must contain command paths only, never credentials or database URLs");
51317
+ throw new Error(`${definition.displayName} MCP configuration must contain command paths only, never credentials or database URLs`);
51225
51318
  }
51226
51319
  }
51227
51320
  function displayPath(root, value) {
@@ -51236,10 +51329,19 @@ async function pathExists(value) {
51236
51329
  throw error;
51237
51330
  }
51238
51331
  }
51332
+ function finalNewline(value) {
51333
+ return value.endsWith("\n") ? value : `${value}
51334
+ `;
51335
+ }
51239
51336
  function isRecord7(value) {
51240
51337
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
51241
51338
  }
51242
51339
 
51340
+ // apps/runner/src/cursor-project.ts
51341
+ function cursorProjectStatus(projectRoot = process.cwd()) {
51342
+ return managedMcpProjectStatus("cursor", projectRoot);
51343
+ }
51344
+
51243
51345
  // apps/runner/src/workbench-syntax.ts
51244
51346
  var synapsorDslKeywordValues = [
51245
51347
  "ABSOLUTE",
@@ -53296,12 +53398,12 @@ function renderBoundaryWorkbench(csrfToken) {
53296
53398
  }
53297
53399
  }
53298
53400
 
53299
- function renderClientConfigs(){
53300
- const command="npx -y @synapsor/runner mcp serve --authoring --project-root .";
53301
- const config={mcpServers:{synapsor_authoring:{command:"npx",args:["-y","@synapsor/runner","mcp","serve","--authoring","--project-root","."]}}};
53302
- const codex='[mcp_servers.synapsor_authoring]\\ncommand = "npx"\\nargs = '+JSON.stringify(config.mcpServers.synapsor_authoring.args);
53303
- byId("client-configs").innerHTML='<p>All clients receive the same two local authoring tools and no approval or commit tool.</p><h3>Generic stdio MCP</h3><pre>'+esc(JSON.stringify(config,null,2))+'</pre><h3>Cursor project</h3><p>Save the same JSON as <code>.cursor/mcp.json</code>, or use the consent-gated installer.</p><pre>'+esc("synapsor-runner mcp install cursor --project --authoring --project-root . --yes")+'</pre><h3>Claude-compatible local MCP</h3><p>Use the same stdio server command in the client MCP configuration. No model API key is needed by Runner.</p><pre>'+esc(command)+'</pre><h3>Codex</h3><pre>'+esc(codex)+'</pre>';
53304
- }
53401
+ function renderClientConfigs(){
53402
+ const command="npx -y @synapsor/runner mcp serve --authoring --project-root .";
53403
+ const config={mcpServers:{synapsor_authoring:{command:"npx",args:["-y","@synapsor/runner","mcp","serve","--authoring","--project-root","."]}}};
53404
+ const codex='[mcp_servers.synapsor_authoring]\\ncommand = "npx"\\nargs = '+JSON.stringify(config.mcpServers.synapsor_authoring.args);
53405
+ byId("client-configs").innerHTML='<p>Every path receives the same two local authoring tools and no approval or commit tool. Runner never puts database credentials in these files.</p><h3>Managed project installers</h3><p>Each command previews and owns only the <code>synapsor</code> entry, preserves other project settings, and creates a backup before editing an existing file.</p><pre>'+esc("synapsor-runner mcp install cursor --project --authoring --project-root . --yes\\nsynapsor-runner mcp install claude-code --project --authoring --project-root . --yes\\nsynapsor-runner mcp install vscode --project --authoring --project-root . --yes")+'</pre><h3>Generic stdio MCP</h3><pre>'+esc(JSON.stringify(config,null,2))+'</pre><h3>Direct server command</h3><p>Use this in another local MCP client. No model API key is needed by Runner.</p><pre>'+esc(command)+'</pre><h3>Codex</h3><pre>'+esc(codex)+'</pre>';
53406
+ }
53305
53407
 
53306
53408
  async function loadProtect(preferredRef=preferredProtectQueryRef){
53307
53409
  const status=byId("protect-message");
@@ -57485,7 +57587,7 @@ function protectedQueryTests(capability, plan, boundaryDigest) {
57485
57587
  async function writeDraftArtifacts(input) {
57486
57588
  await fs9.mkdir(input.outputRoot, { recursive: true, mode: 448 });
57487
57589
  const markerPath = path9.join(input.outputRoot, ".synapsor-protected-query.json");
57488
- const existing = await readOptionalJson2(markerPath);
57590
+ const existing = await readOptionalJson(markerPath);
57489
57591
  if (existing && existing.schema_version !== PROTECTED_QUERY_VERSION) {
57490
57592
  throw new Error(`Refusing to overwrite unmanaged protected-query directory ${input.outputRoot}.`);
57491
57593
  }
@@ -57497,7 +57599,7 @@ async function writeDraftArtifacts(input) {
57497
57599
  await writeAtomic(markerPath, json3({ schema_version: PROTECTED_QUERY_VERSION, capability: input.draft.capability }), 384);
57498
57600
  }
57499
57601
  async function addProtectedContractToRuntimeConfig(input) {
57500
- const existing = await readOptionalJson2(input.configPath);
57602
+ const existing = await readOptionalJson(input.configPath);
57501
57603
  const relativeContract = relativeConfigPath(path9.dirname(input.configPath), input.contractPath);
57502
57604
  const config = existing ?? {
57503
57605
  version: 1,
@@ -57710,7 +57812,7 @@ async function writeAtomic(filePath, content, mode) {
57710
57812
  throw error;
57711
57813
  }
57712
57814
  }
57713
- async function readOptionalJson2(filePath) {
57815
+ async function readOptionalJson(filePath) {
57714
57816
  try {
57715
57817
  return JSON.parse(await fs9.readFile(filePath, "utf8"));
57716
57818
  } catch (error) {
@@ -58560,14 +58662,14 @@ async function guidedActionDraftDetails(projectRootInput, capabilityName2) {
58560
58662
  }
58561
58663
  async function guidedActionStatus(projectRootInput) {
58562
58664
  const projectRoot = path11.resolve(projectRootInput);
58563
- const index = await readOptionalJson3(path11.join(projectRoot, ".synapsor/guided-action-drafts.json"));
58665
+ const index = await readOptionalJson2(path11.join(projectRoot, ".synapsor/guided-action-drafts.json"));
58564
58666
  const drafts = Array.isArray(index?.drafts) ? index.drafts.filter((item) => isRecord13(item) && item.schema_version === GUIDED_ACTION_VERSION && item.state === "disabled" && typeof item.capability === "string") : [];
58565
58667
  const activeRoot = path11.join(projectRoot, GUIDED_ACTION_ROOT, "active");
58566
58668
  const activations = [];
58567
58669
  try {
58568
58670
  for (const entry of await fs11.readdir(activeRoot, { withFileTypes: true })) {
58569
58671
  if (!entry.isFile() || !entry.name.endsWith(".active.json")) continue;
58570
- const parsed = await readOptionalJson3(path11.join(activeRoot, entry.name));
58672
+ const parsed = await readOptionalJson2(path11.join(activeRoot, entry.name));
58571
58673
  if (parsed && parsed.schema_version === GUIDED_ACTION_VERSION && parsed.state === "active" && typeof parsed.capability === "string") {
58572
58674
  activations.push(parsed);
58573
58675
  }
@@ -58966,7 +59068,7 @@ function guidedActionReview(draft, action, resource) {
58966
59068
  }
58967
59069
  async function writeGuidedActionArtifacts(input) {
58968
59070
  const markerPath = path11.join(input.outputRoot, ".synapsor-guided-action.json");
58969
- const existing = await readOptionalJson3(markerPath);
59071
+ const existing = await readOptionalJson2(markerPath);
58970
59072
  if (existing && (existing.schema_version !== GUIDED_ACTION_VERSION || existing.capability !== input.draft.capability)) {
58971
59073
  throw new Error(`GUIDED_ACTION_OUTPUT_UNMANAGED: refusing to overwrite ${input.outputRoot}.`);
58972
59074
  }
@@ -58981,7 +59083,7 @@ async function writeGuidedActionArtifacts(input) {
58981
59083
  }
58982
59084
  async function updateActionIndex(projectRoot, draft) {
58983
59085
  const indexPath = path11.join(projectRoot, ".synapsor/guided-action-drafts.json");
58984
- const existing = await readOptionalJson3(indexPath);
59086
+ const existing = await readOptionalJson2(indexPath);
58985
59087
  if (existing && existing.schema_version !== "synapsor.guided-action-drafts.v1" && existing.schema_version !== GUIDED_ACTION_INDEX_VERSION) {
58986
59088
  throw new Error("GUIDED_ACTION_INDEX_UNMANAGED: refusing to replace an unknown action index.");
58987
59089
  }
@@ -59131,7 +59233,7 @@ function relativeConfigPath2(configDirectory, target2) {
59131
59233
  const relative2 = path11.relative(configDirectory, target2).replace(/\\/g, "/");
59132
59234
  return relative2 === "." || relative2.startsWith("./") || relative2.startsWith("../") ? relative2 : `./${relative2}`;
59133
59235
  }
59134
- async function readOptionalJson3(filePath) {
59236
+ async function readOptionalJson2(filePath) {
59135
59237
  try {
59136
59238
  const parsed = JSON.parse(await fs11.readFile(filePath, "utf8"));
59137
59239
  if (!isRecord13(parsed)) throw new Error(`Expected a JSON object at ${filePath}.`);
@@ -60670,7 +60772,7 @@ async function activateSafeActionDraft(input) {
60670
60772
  activated_at: activatedAt
60671
60773
  });
60672
60774
  } catch (error) {
60673
- await writeTextAtomic(configPath, previousConfigText).catch(() => void 0);
60775
+ await writeTextAtomic2(configPath, previousConfigText).catch(() => void 0);
60674
60776
  throw error;
60675
60777
  }
60676
60778
  return active;
@@ -61190,10 +61292,10 @@ async function writeDigestArtifact(filePath, content, immutable = false) {
61190
61292
  if (immutable) await fs15.chmod(filePath, 256).catch(() => void 0);
61191
61293
  }
61192
61294
  async function writeJsonAtomic2(filePath, value) {
61193
- await writeTextAtomic(filePath, `${JSON.stringify(value, null, 2)}
61295
+ await writeTextAtomic2(filePath, `${JSON.stringify(value, null, 2)}
61194
61296
  `);
61195
61297
  }
61196
- async function writeTextAtomic(filePath, value) {
61298
+ async function writeTextAtomic2(filePath, value) {
61197
61299
  await fs15.mkdir(path15.dirname(filePath), { recursive: true, mode: 448 });
61198
61300
  const temporary = path15.join(path15.dirname(filePath), `.${path15.basename(filePath)}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
61199
61301
  try {
@@ -61213,7 +61315,7 @@ async function writeNewText(filePath, value, force) {
61213
61315
  } catch (error) {
61214
61316
  if (error.code !== "ENOENT") throw error;
61215
61317
  }
61216
- await writeTextAtomic(filePath, value);
61318
+ await writeTextAtomic2(filePath, value);
61217
61319
  }
61218
61320
  function relativeProjectPath3(projectRoot, filePath) {
61219
61321
  const relative2 = normalizePath(path15.relative(projectRoot, filePath));
@@ -62786,7 +62888,7 @@ function createScopedExploreMcpServer(runtime) {
62786
62888
  }).strict().optional()
62787
62889
  }).strict();
62788
62890
  const server = new McpServer2(
62789
- { name: "synapsor-runner-authoring", version: "1.6.4" },
62891
+ { name: "synapsor-runner-authoring", version: package_default.version },
62790
62892
  { capabilities: { tools: {} } }
62791
62893
  );
62792
62894
  server.registerTool(SCOPED_EXPLORE_DESCRIBE_TOOL, {
@@ -63005,7 +63107,7 @@ async function connectAuthoringSurface(input) {
63005
63107
  async function connectSurface(kind, server, closeRuntime) {
63006
63108
  const client = new Client({
63007
63109
  name: `synapsor-workbench-ask-${kind}`,
63008
- version: "1.6.4"
63110
+ version: package_default.version
63009
63111
  });
63010
63112
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
63011
63113
  try {
@@ -63229,7 +63331,7 @@ async function handleRequest(input) {
63229
63331
  const candidate = progress?.candidate ?? recommendedWorkbenchCandidate(draft);
63230
63332
  const reviewDecisions = boundaryReviewDecisions(candidate);
63231
63333
  const confirmedDecisions = progress?.confirmed_decisions ?? [];
63232
- const active = await readOptionalJson4(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"));
63334
+ const active = await readOptionalJson3(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"));
63233
63335
  const instantCandidate = instantWorkbenchCandidate(draft);
63234
63336
  sendJson(response, 200, {
63235
63337
  ok: true,
@@ -63444,7 +63546,7 @@ async function handleRequest(input) {
63444
63546
  path16.join(boundaryRoot, "exploration-boundary.draft.json"),
63445
63547
  "utf8"
63446
63548
  ));
63447
- const active = await readOptionalJson4(
63549
+ const active = await readOptionalJson3(
63448
63550
  path16.join(projectRoot, ".synapsor/exploration-boundary.active.json")
63449
63551
  );
63450
63552
  if (!active || active.activation.digest !== expectedActiveDigest) {
@@ -63549,7 +63651,7 @@ async function handleRequest(input) {
63549
63651
  );
63550
63652
  const previousProgress = await readBoundaryReviewProgress(projectRoot, previousDraft);
63551
63653
  const previousActive = activeBoundaryEventMetadata(
63552
- await readOptionalJson4(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
63654
+ await readOptionalJson3(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
63553
63655
  );
63554
63656
  const overrides = applyManagedBoundaryReviewDecision(
63555
63657
  await loadAutoBoundaryReviewOverrides(projectRoot),
@@ -63706,7 +63808,7 @@ async function handleRequest(input) {
63706
63808
  throw new Error("Schema or review inputs changed after preview; preview the rescan again.");
63707
63809
  }
63708
63810
  const previousActive = activeBoundaryEventMetadata(
63709
- await readOptionalJson4(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
63811
+ await readOptionalJson3(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
63710
63812
  );
63711
63813
  const previousDraft = JSON.parse(
63712
63814
  await fs16.readFile(path16.join(boundaryRoot, "exploration-boundary.draft.json"), "utf8")
@@ -63802,7 +63904,7 @@ async function handleRequest(input) {
63802
63904
  throw new Error("Start over requires the exact confirmation START OVER REVIEW.");
63803
63905
  }
63804
63906
  const previousActive = activeBoundaryEventMetadata(
63805
- await readOptionalJson4(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
63907
+ await readOptionalJson3(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
63806
63908
  );
63807
63909
  const prepared = await prepareAutoBoundaryRescan({
63808
63910
  projectRoot,
@@ -64030,7 +64132,7 @@ async function handleRequest(input) {
64030
64132
  throw new Error("Protected-capability activation requires capability_name, expected_digest, confirmation, and actor.");
64031
64133
  }
64032
64134
  const previousExploreAuthority = activeBoundaryEventMetadata(
64033
- await readOptionalJson4(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
64135
+ await readOptionalJson3(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
64034
64136
  );
64035
64137
  const active = await activateProtectedQuery({
64036
64138
  projectRoot,
@@ -64096,7 +64198,7 @@ async function handleRequest(input) {
64096
64198
  return;
64097
64199
  }
64098
64200
  const previousActive = activeBoundaryEventMetadata(
64099
- await readOptionalJson4(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
64201
+ await readOptionalJson3(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"))
64100
64202
  );
64101
64203
  const disabled = await disableScopedExplore(projectRoot);
64102
64204
  if (disabled.disabled && previousActive) {
@@ -66564,13 +66666,13 @@ function workbenchReceiptRecord(record) {
66564
66666
  }
66565
66667
  async function resolveWorkbenchDeploymentProfile(projectRoot, configured) {
66566
66668
  if (configured) return configured;
66567
- const active = await readOptionalJson4(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"));
66568
- const draft = await readOptionalJson4(path16.join(projectRoot, "synapsor/generated/exploration-boundary.draft.json"));
66669
+ const active = await readOptionalJson3(path16.join(projectRoot, ".synapsor/exploration-boundary.active.json"));
66670
+ const draft = await readOptionalJson3(path16.join(projectRoot, "synapsor/generated/exploration-boundary.draft.json"));
66569
66671
  const profile = isRecord18(active) ? active.deployment_profile : isRecord18(draft) ? draft.deployment_profile : void 0;
66570
66672
  return profile === "development" || profile === "staging" || profile === "production" ? profile : "unknown";
66571
66673
  }
66572
66674
  async function workbenchOperatorPosture(configPath) {
66573
- const rawConfig = await readOptionalJson4(path16.resolve(configPath));
66675
+ const rawConfig = await readOptionalJson3(path16.resolve(configPath));
66574
66676
  const operator = isRecord18(rawConfig) ? asRecord4(rawConfig.operator_identity) : {};
66575
66677
  const provider = operator.provider === "signed_key" || operator.provider === "jwt_oidc" ? operator.provider : "dev_env";
66576
66678
  const applyRoles = Array.isArray(operator.apply_roles) ? operator.apply_roles.filter((role) => typeof role === "string") : [];
@@ -66622,7 +66724,7 @@ async function readJsonBody(request) {
66622
66724
  if (!isRecord18(parsed)) throw new Error("request body must be a JSON object");
66623
66725
  return parsed;
66624
66726
  }
66625
- async function readOptionalJson4(filePath) {
66727
+ async function readOptionalJson3(filePath) {
66626
66728
  try {
66627
66729
  return JSON.parse(await fs16.readFile(filePath, "utf8"));
66628
66730
  } catch (error) {
@@ -66687,7 +66789,7 @@ function instantActivationActor() {
66687
66789
  }
66688
66790
  async function readBoundaryReviewProgress(projectRoot, draft) {
66689
66791
  const filePath = path16.join(projectRoot, ".synapsor/boundary-review-progress.json");
66690
- const raw = await readOptionalJson4(filePath);
66792
+ const raw = await readOptionalJson3(filePath);
66691
66793
  if (raw === null) return void 0;
66692
66794
  if (!isRecord18(raw) || raw.schema_version !== BOUNDARY_REVIEW_PROGRESS_VERSION && raw.schema_version !== LEGACY_BOUNDARY_REVIEW_PROGRESS_VERSION || !isRecord18(raw.candidate) || !Array.isArray(raw.confirmed_decisions) || raw.confirmed_decisions.some((decision) => typeof decision !== "string") || typeof raw.updated_at !== "string") {
66693
66795
  throw new Error("Saved boundary-review progress is invalid; use explicit Rescan or Start over rather than trusting it.");
@@ -67025,11 +67127,11 @@ function askWorkbenchAccessFailure(profile, host) {
67025
67127
  return void 0;
67026
67128
  }
67027
67129
  async function workbenchAskAuthority(input) {
67028
- const active = await readOptionalJson4(path16.join(input.projectRoot, ".synapsor/exploration-boundary.active.json"));
67130
+ const active = await readOptionalJson3(path16.join(input.projectRoot, ".synapsor/exploration-boundary.active.json"));
67029
67131
  const activation2 = isRecord18(active) ? asRecord4(active.activation) : {};
67030
67132
  const activeBoundaryDigest = typeof activation2.digest === "string" && /^sha256:[a-f0-9]{64}$/.test(activation2.digest) ? activation2.digest : void 0;
67031
67133
  const toolSurfaceDigest = askToolSurfaceDigest(input.tools);
67032
- const runtimeConfig = await readOptionalJson4(input.configPath);
67134
+ const runtimeConfig = await readOptionalJson3(input.configPath);
67033
67135
  if (!isRecord18(runtimeConfig)) {
67034
67136
  throw new AskError("ASK_RUNTIME_CONFIG_UNAVAILABLE", "Ask could not bind consent to the active Runner configuration.", 409);
67035
67137
  }
@@ -75156,7 +75258,7 @@ async function writeGeneratedOnboardingFiles(output, generated, selection, force
75156
75258
  process2.stdout.write(`created onboarding manifest ${displayPath2(manifestPath)}
75157
75259
  `);
75158
75260
  if (options.printNext !== false) {
75159
- process2.stdout.write(`Next: set the referenced environment variables, run \`${cliCommandName()} config validate --config ${configArgument}\`, then add the reviewed tools to Cursor with \`${cliCommandName()} mcp install cursor --project --config ${configArgument}\`.
75261
+ process2.stdout.write(`Next: set the referenced environment variables, run \`${cliCommandName()} config validate --config ${configArgument}\`, then add the reviewed tools to a project client with \`${cliCommandName()} mcp install <cursor|claude-code|vscode> --project --config ${configArgument}\`.
75160
75262
  `);
75161
75263
  }
75162
75264
  return artifacts;
@@ -77756,7 +77858,7 @@ async function localDoctor(args) {
77756
77858
  level: forbiddenTools.length === 0 ? "pass" : "fail",
77757
77859
  message: forbiddenTools.length === 0 ? "MCP tool catalog is semantic-only." : `Forbidden model-facing tools: ${forbiddenTools.join(", ")}`
77758
77860
  });
77759
- checks.push(...await cursorProjectDoctorChecks(configPath, tools2, args));
77861
+ checks.push(...await managedMcpProjectDoctorChecks(configPath, tools2, args));
77760
77862
  const report = {
77761
77863
  ok: checks.every((check) => check.level !== "fail"),
77762
77864
  mode: String(parsed.mode),
@@ -77781,78 +77883,100 @@ async function localDoctor(args) {
77781
77883
  }
77782
77884
  return report.ok ? 0 : 1;
77783
77885
  }
77784
- async function cursorProjectDoctorChecks(configPath, expectedTools, args) {
77886
+ async function managedMcpProjectDoctorChecks(configPath, expectedTools, args) {
77785
77887
  const projectRoot = path29.resolve(optionalArg(args, "--project-root") ?? path29.dirname(path29.resolve(configPath)));
77786
- try {
77787
- const status = await cursorProjectStatus(projectRoot);
77788
- if (status.state === "not_installed") {
77789
- return [{
77790
- name: "cursor-project:installation",
77791
- ok: true,
77792
- level: "warn",
77793
- message: `No Runner-owned project Cursor entry is installed. Preview it with ${cliCommandName()} mcp install cursor --project --project-root ${projectRoot} --dry-run.`
77794
- }];
77795
- }
77796
- if (status.state !== "installed") {
77797
- return [{
77798
- name: "cursor-project:installation",
77799
- ok: false,
77800
- level: "fail",
77801
- message: status.message
77802
- }];
77803
- }
77804
- const authoring = isCursorAuthoringEntry(status.entry);
77805
- const authoringTools = ["app.describe_data", "app.explore_data"];
77806
- const reviewedTools = authoring ? authoringTools : expectedTools;
77807
- const recordedConfig = path29.resolve(projectRoot, status.paths.configArgument);
77808
- const configMatches = recordedConfig === path29.resolve(configPath);
77809
- const checks = [{
77810
- name: "cursor-project:installation",
77811
- ok: authoring || configMatches,
77812
- level: authoring || configMatches ? "pass" : "fail",
77813
- message: authoring ? "Runner owns an intact local authoring Cursor entry; it does not depend on a production Runner config." : configMatches ? `Runner owns an intact project Cursor entry for ${status.paths.configArgument}.` : `Cursor entry points to ${status.paths.configArgument}, but doctor inspected ${path29.resolve(configPath)}.`
77814
- }, {
77815
- name: "cursor-project:model-tools",
77816
- ok: reviewedTools.length > 0,
77817
- level: reviewedTools.length > 0 ? "pass" : "fail",
77818
- message: authoring ? `Authoring tools: ${authoringTools.join(", ")}. Scoped Explore remains local development/staging only; activation, approval, apply, revert, credentials, and trusted identity remain outside MCP.` : `Reviewed model-facing tools: ${expectedTools.join(", ") || "none"}. Approval, apply, revert, policy, credentials, and trusted identity remain outside MCP.`
77888
+ const clients = ["cursor", "claude-code", "vscode"];
77889
+ const requestedLaunch = args.includes("--check-cursor") ? "cursor" : optionalArg(args, "--check-mcp-client");
77890
+ const launchClient = requestedLaunch === void 0 ? void 0 : parseManagedMcpProjectClient(requestedLaunch);
77891
+ const statuses = await Promise.all(clients.map(async (client) => ({
77892
+ client,
77893
+ definition: managedMcpProjectDefinition(client),
77894
+ status: await managedMcpProjectStatus(client, projectRoot)
77895
+ })));
77896
+ const relevant = statuses.filter(({ status, client }) => status.state !== "not_installed" || client === launchClient);
77897
+ if (relevant.length === 0) {
77898
+ return [{
77899
+ name: "mcp-project:installation",
77900
+ ok: true,
77901
+ level: "warn",
77902
+ message: `No Runner-owned project MCP entry is installed. Preview one with ${cliCommandName()} mcp install <cursor|claude-code|vscode> --project --project-root ${projectRoot} --dry-run.`
77819
77903
  }];
77820
- if (!args.includes("--check-cursor")) {
77904
+ }
77905
+ const checks = [];
77906
+ for (const { client, definition, status } of relevant) {
77907
+ const prefix = `${client}-project`;
77908
+ try {
77909
+ if (status.state === "not_installed") {
77910
+ checks.push({
77911
+ name: `${prefix}:installation`,
77912
+ ok: false,
77913
+ level: "fail",
77914
+ message: `No Runner-owned ${definition.displayName} project entry is installed, so the requested launch check cannot run.`
77915
+ });
77916
+ continue;
77917
+ }
77918
+ if (status.state !== "installed") {
77919
+ checks.push({
77920
+ name: `${prefix}:installation`,
77921
+ ok: false,
77922
+ level: "fail",
77923
+ message: status.message
77924
+ });
77925
+ continue;
77926
+ }
77927
+ const authoring = isManagedAuthoringEntry(status.entry);
77928
+ const authoringTools = ["app.describe_data", "app.explore_data"];
77929
+ const reviewedTools = authoring ? authoringTools : expectedTools;
77930
+ const recordedConfig = path29.resolve(projectRoot, status.paths.configArgument);
77931
+ const configMatches = recordedConfig === path29.resolve(configPath);
77821
77932
  checks.push({
77822
- name: "cursor-project:launch",
77823
- ok: true,
77824
- level: "warn",
77825
- message: `Cursor command was not launched. Rerun with --check-cursor to perform a real stdio initialize + tools/list handshake.`
77933
+ name: `${prefix}:installation`,
77934
+ ok: authoring || configMatches,
77935
+ level: authoring || configMatches ? "pass" : "fail",
77936
+ message: authoring ? `Runner owns an intact local authoring ${definition.displayName} entry; it does not depend on a production Runner config.` : configMatches ? `Runner owns an intact ${definition.displayName} project entry for ${status.paths.configArgument}.` : `${definition.displayName} entry points to ${status.paths.configArgument}, but doctor inspected ${path29.resolve(configPath)}.`
77937
+ }, {
77938
+ name: `${prefix}:model-tools`,
77939
+ ok: reviewedTools.length > 0,
77940
+ level: reviewedTools.length > 0 ? "pass" : "fail",
77941
+ message: authoring ? `Authoring tools: ${authoringTools.join(", ")}. Scoped Explore remains local development/staging only; activation, approval, apply, revert, credentials, and trusted identity remain outside MCP.` : `Reviewed model-facing tools: ${expectedTools.join(", ") || "none"}. Approval, apply, revert, policy, credentials, and trusted identity remain outside MCP.`
77826
77942
  });
77827
- return checks;
77828
- }
77829
- const entry = status.entry;
77830
- const command = typeof entry?.command === "string" ? entry.command : "";
77831
- const commandArgs = Array.isArray(entry?.args) && entry.args.every((value) => typeof value === "string") ? entry.args : [];
77832
- if (!command) {
77833
- checks.push({ name: "cursor-project:launch", ok: false, level: "fail", message: "Cursor Synapsor entry has no valid command." });
77834
- return checks;
77835
- }
77836
- const timeoutMs = Number(optionalArg(args, "--timeout-ms") ?? "10000");
77837
- if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 12e4) throw new Error("--timeout-ms must be an integer from 100 to 120000");
77838
- const response = await fetchStdioMcpToolsCommand(command, commandArgs, timeoutMs, projectRoot);
77839
- const liveTools = mcpAuditToolNames(response);
77840
- const matches = stableStringArray(liveTools).join("\n") === stableStringArray(reviewedTools).join("\n");
77841
- checks.push({
77842
- name: "cursor-project:launch",
77843
- ok: matches,
77844
- level: matches ? "pass" : "fail",
77845
- message: matches ? `Configured Cursor command started and exposed exactly ${liveTools.length} reviewed tool(s).` : `Configured command exposed ${liveTools.join(", ") || "no tools"}; expected ${reviewedTools.join(", ") || "no tools"}.`
77846
- });
77847
- return checks;
77848
- } catch (error) {
77849
- return [{
77850
- name: "cursor-project:installation",
77851
- ok: false,
77852
- level: "fail",
77853
- message: `Cursor project verification failed: ${safeErrorMessage(error)}`
77854
- }];
77943
+ if (launchClient !== client) {
77944
+ checks.push({
77945
+ name: `${prefix}:launch`,
77946
+ ok: true,
77947
+ level: "warn",
77948
+ message: `${definition.displayName} command was not launched. Rerun with --check-mcp-client ${client} to perform a real stdio initialize + tools/list handshake.`
77949
+ });
77950
+ continue;
77951
+ }
77952
+ const entry = status.entry;
77953
+ const command = typeof entry?.command === "string" ? entry.command : "";
77954
+ const commandArgs = Array.isArray(entry?.args) && entry.args.every((value) => typeof value === "string") ? entry.args : [];
77955
+ if (!command) {
77956
+ checks.push({ name: `${prefix}:launch`, ok: false, level: "fail", message: `${definition.displayName} Synapsor entry has no valid command.` });
77957
+ continue;
77958
+ }
77959
+ const timeoutMs = Number(optionalArg(args, "--timeout-ms") ?? "10000");
77960
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 12e4) throw new Error("--timeout-ms must be an integer from 100 to 120000");
77961
+ const response = await fetchStdioMcpToolsCommand(command, commandArgs, timeoutMs, projectRoot);
77962
+ const liveTools = mcpAuditToolNames(response);
77963
+ const matches = stableStringArray(liveTools).join("\n") === stableStringArray(reviewedTools).join("\n");
77964
+ checks.push({
77965
+ name: `${prefix}:launch`,
77966
+ ok: matches,
77967
+ level: matches ? "pass" : "fail",
77968
+ message: matches ? `Configured ${definition.displayName} command started and exposed exactly ${liveTools.length} reviewed tool(s).` : `Configured command exposed ${liveTools.join(", ") || "no tools"}; expected ${reviewedTools.join(", ") || "no tools"}.`
77969
+ });
77970
+ } catch (error) {
77971
+ checks.push({
77972
+ name: `${prefix}:installation`,
77973
+ ok: false,
77974
+ level: "fail",
77975
+ message: `${definition.displayName} project verification failed: ${safeErrorMessage(error)}`
77976
+ });
77977
+ }
77855
77978
  }
77979
+ return checks;
77856
77980
  }
77857
77981
  async function httpSecurityDoctorChecks(config, args) {
77858
77982
  const requestedTransport = optionalArg(args, "--transport") ?? "stdio";
@@ -81851,11 +81975,12 @@ async function mcp(args) {
81851
81975
  return 2;
81852
81976
  }
81853
81977
  async function mcpProjectInstall(args) {
81854
- const [client, ...rest] = args;
81855
- if (client !== "cursor") throw new Error("mcp install currently supports cursor only");
81856
- assertKnownOptions(rest, /* @__PURE__ */ new Set(["--project", "--project-root", "--config", "--store", "--authoring", "--dry-run", "--yes", "--json"]), "mcp install cursor");
81978
+ const [clientValue, ...rest] = args;
81979
+ const client = parseManagedMcpProjectClient(clientValue);
81980
+ const definition = managedMcpProjectDefinition(client);
81981
+ assertKnownOptions(rest, /* @__PURE__ */ new Set(["--project", "--project-root", "--config", "--store", "--authoring", "--dry-run", "--yes", "--json"]), `mcp install ${client}`);
81857
81982
  if (!rest.includes("--project")) {
81858
- throw new Error("mcp install cursor requires --project so Runner changes only the current project's .cursor/mcp.json");
81983
+ throw new Error(`mcp install ${client} requires --project so Runner changes only the current project's ${definition.destination}`);
81859
81984
  }
81860
81985
  const projectRoot = path29.resolve(optionalArg(rest, "--project-root") ?? process2.cwd());
81861
81986
  const authoring = rest.includes("--authoring");
@@ -81870,42 +81995,44 @@ async function mcpProjectInstall(args) {
81870
81995
  await readRuntimeConfig(absoluteConfig);
81871
81996
  const boundary = await inspectMcpToolBoundary(["--config", absoluteConfig, "--store", ":memory:"]);
81872
81997
  if (!boundary.ok) {
81873
- throw new Error(`Cursor install refused because the reviewed model-facing boundary failed: ${boundary.checks.filter((check) => !check.ok).map((check) => check.name).join(", ")}`);
81998
+ throw new Error(`${definition.displayName} install refused because the reviewed model-facing boundary failed: ${boundary.checks.filter((check) => !check.ok).map((check) => check.name).join(", ")}`);
81874
81999
  }
81875
82000
  toolNames = boundary.names;
81876
82001
  }
81877
- const preview = await previewCursorProjectInstall({ projectRoot, configPath, storePath, authoring });
81878
- const report = cursorProjectLifecycleReport(preview, toolNames, authoring);
82002
+ const preview = await previewManagedMcpProjectInstall({ client, projectRoot, configPath, storePath, authoring });
82003
+ const report = managedMcpProjectLifecycleReport(client, preview, toolNames, authoring);
81879
82004
  const json7 = rest.includes("--json");
81880
82005
  if (rest.includes("--dry-run") || preview.action === "unchanged") {
81881
82006
  if (json7) process2.stdout.write(`${JSON.stringify(report, null, 2)}
81882
82007
  `);
81883
- else process2.stdout.write(formatCursorProjectPreview(report));
82008
+ else process2.stdout.write(formatManagedMcpProjectPreview(report));
81884
82009
  return 0;
81885
82010
  }
81886
- if (!json7) process2.stdout.write(formatCursorProjectPreview(report));
82011
+ if (!json7) process2.stdout.write(formatManagedMcpProjectPreview(report));
81887
82012
  await confirmDangerousAction(rest, `Install the reviewed Synapsor MCP entry in ${path29.relative(projectRoot, preview.paths.destination)}?`);
81888
- const installed = await installCursorProject({ projectRoot, configPath, storePath, authoring });
82013
+ const installed = await installManagedMcpProject({ client, projectRoot, configPath, storePath, authoring });
81889
82014
  if (json7) {
81890
82015
  process2.stdout.write(`${JSON.stringify({ ...report, action: installed.action, installed: true, backup: installed.backup ?? null }, null, 2)}
81891
82016
  `);
81892
82017
  } else {
81893
- process2.stdout.write(`Cursor project MCP entry installed at ${path29.relative(projectRoot, installed.paths.destination)}.
82018
+ process2.stdout.write(`${definition.displayName} project MCP entry installed at ${path29.relative(projectRoot, installed.paths.destination)}.
81894
82019
  `);
81895
82020
  if (installed.backup) process2.stdout.write(`Backup: ${installed.backup}
81896
82021
  `);
81897
- process2.stdout.write("Restart or reload Cursor, then run `synapsor-runner mcp status cursor --project` to verify the reviewed tool boundary.\n");
82022
+ process2.stdout.write(`${definition.reloadInstruction}, then run \`synapsor-runner mcp status ${client} --project\` to verify the reviewed tool boundary.
82023
+ `);
81898
82024
  }
81899
82025
  return 0;
81900
82026
  }
81901
82027
  async function mcpProjectStatus(args) {
81902
- const [client, ...rest] = args;
81903
- if (client !== "cursor") throw new Error("mcp status currently supports cursor only");
81904
- assertKnownOptions(rest, /* @__PURE__ */ new Set(["--project", "--project-root", "--json", "--check-launch", "--timeout-ms"]), "mcp status cursor");
81905
- if (!rest.includes("--project")) throw new Error("mcp status cursor requires --project");
82028
+ const [clientValue, ...rest] = args;
82029
+ const client = parseManagedMcpProjectClient(clientValue);
82030
+ const definition = managedMcpProjectDefinition(client);
82031
+ assertKnownOptions(rest, /* @__PURE__ */ new Set(["--project", "--project-root", "--json", "--check-launch", "--timeout-ms"]), `mcp status ${client}`);
82032
+ if (!rest.includes("--project")) throw new Error(`mcp status ${client} requires --project`);
81906
82033
  const projectRoot = path29.resolve(optionalArg(rest, "--project-root") ?? process2.cwd());
81907
- const status = await cursorProjectStatus(projectRoot);
81908
- const authoring = status.state === "installed" && isCursorAuthoringEntry(status.entry);
82034
+ const status = await managedMcpProjectStatus(client, projectRoot);
82035
+ const authoring = status.state === "installed" && isManagedAuthoringEntry(status.entry);
81909
82036
  let tools2 = [];
81910
82037
  let launch = {
81911
82038
  checked: false,
@@ -81929,7 +82056,7 @@ async function mcpProjectStatus(args) {
81929
82056
  const entry = status.entry;
81930
82057
  const command = typeof entry?.command === "string" ? entry.command : "";
81931
82058
  const commandArgs = Array.isArray(entry?.args) && entry.args.every((value) => typeof value === "string") ? entry.args : [];
81932
- if (!command) throw new Error("Cursor Synapsor entry does not contain a valid command");
82059
+ if (!command) throw new Error(`${definition.displayName} Synapsor entry does not contain a valid command`);
81933
82060
  const timeoutMs = Number(optionalArg(rest, "--timeout-ms") ?? "10000");
81934
82061
  if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 12e4) throw new Error("--timeout-ms must be an integer from 100 to 120000");
81935
82062
  const response = await fetchStdioMcpToolsCommand(command, commandArgs, timeoutMs, projectRoot);
@@ -81938,12 +82065,14 @@ async function mcpProjectStatus(args) {
81938
82065
  launch = {
81939
82066
  checked: true,
81940
82067
  ok: matches,
81941
- message: matches ? `Cursor command started and exposed exactly ${liveNames.length} reviewed tool(s).` : `Started command exposed ${liveNames.join(", ") || "no tools"}; expected ${tools2.join(", ") || "no tools"}.`
82068
+ message: matches ? `${definition.displayName} command started and exposed exactly ${liveNames.length} reviewed tool(s).` : `Started command exposed ${liveNames.join(", ") || "no tools"}; expected ${tools2.join(", ") || "no tools"}.`
81942
82069
  };
81943
82070
  }
81944
82071
  }
81945
82072
  const report = {
81946
82073
  ok: status.state === "installed" && launch.ok,
82074
+ client,
82075
+ client_name: definition.displayName,
81947
82076
  state: status.state,
81948
82077
  mode: authoring ? "authoring" : "runtime",
81949
82078
  message: status.message,
@@ -81956,18 +82085,21 @@ async function mcpProjectStatus(args) {
81956
82085
  };
81957
82086
  if (rest.includes("--json")) process2.stdout.write(`${JSON.stringify(report, null, 2)}
81958
82087
  `);
81959
- else process2.stdout.write(formatCursorProjectStatus(report));
82088
+ else process2.stdout.write(formatManagedMcpProjectStatus(report));
81960
82089
  return report.ok ? 0 : 1;
81961
82090
  }
81962
82091
  async function mcpProjectUninstall(args) {
81963
- const [client, ...rest] = args;
81964
- if (client !== "cursor") throw new Error("mcp uninstall currently supports cursor only");
81965
- assertKnownOptions(rest, /* @__PURE__ */ new Set(["--project", "--project-root", "--dry-run", "--yes", "--json"]), "mcp uninstall cursor");
81966
- if (!rest.includes("--project")) throw new Error("mcp uninstall cursor requires --project");
82092
+ const [clientValue, ...rest] = args;
82093
+ const client = parseManagedMcpProjectClient(clientValue);
82094
+ const definition = managedMcpProjectDefinition(client);
82095
+ assertKnownOptions(rest, /* @__PURE__ */ new Set(["--project", "--project-root", "--dry-run", "--yes", "--json"]), `mcp uninstall ${client}`);
82096
+ if (!rest.includes("--project")) throw new Error(`mcp uninstall ${client} requires --project`);
81967
82097
  const projectRoot = path29.resolve(optionalArg(rest, "--project-root") ?? process2.cwd());
81968
- const status = await cursorProjectStatus(projectRoot);
82098
+ const status = await managedMcpProjectStatus(client, projectRoot);
81969
82099
  const preview = {
81970
82100
  ok: status.state === "installed" || status.state === "not_installed",
82101
+ client,
82102
+ client_name: definition.displayName,
81971
82103
  state: status.state,
81972
82104
  changed: status.state === "installed",
81973
82105
  destination: path29.relative(projectRoot, status.paths.destination),
@@ -81986,45 +82118,50 @@ ${preview.changed ? `Will remove only the Runner-owned Synapsor entry from ${pre
81986
82118
  if (!json7) process2.stdout.write(`${preview.message}
81987
82119
  Will remove only the Runner-owned Synapsor entry from ${preview.destination}.
81988
82120
  `);
81989
- if (!preview.ok) throw new Error("Cursor project entry is unowned or changed; refusing to remove it automatically");
82121
+ if (!preview.ok) throw new Error(`${definition.displayName} project entry is unowned or changed; refusing to remove it automatically`);
81990
82122
  await confirmDangerousAction(rest, `Remove only the Runner-owned Synapsor MCP entry from ${preview.destination}?`);
81991
- const removed = await uninstallCursorProject({ projectRoot });
82123
+ const removed = await uninstallManagedMcpProject({ client, projectRoot });
81992
82124
  if (json7) process2.stdout.write(`${JSON.stringify({ ...preview, changed: removed.changed, backup: removed.backup ?? null }, null, 2)}
81993
82125
  `);
81994
- else process2.stdout.write(`Removed Runner-owned Cursor project MCP entry. Backup: ${removed.backup}
82126
+ else process2.stdout.write(`Removed Runner-owned ${definition.displayName} project MCP entry. Backup: ${removed.backup}
81995
82127
  `);
81996
82128
  return 0;
81997
82129
  }
81998
- function cursorProjectLifecycleReport(preview, tools2, authoring = false) {
82130
+ function managedMcpProjectLifecycleReport(client, preview, tools2, authoring = false) {
82131
+ const definition = managedMcpProjectDefinition(client);
81999
82132
  return {
82000
82133
  ok: true,
82134
+ client,
82135
+ client_name: definition.displayName,
82001
82136
  action: preview.action,
82002
82137
  mode: authoring ? "authoring" : "runtime",
82003
82138
  destination: path29.relative(preview.paths.projectRoot, preview.paths.destination),
82004
82139
  config: authoring ? null : preview.paths.configArgument,
82005
82140
  store: authoring ? null : preview.paths.storeArgument,
82006
82141
  preserves_other_servers: true,
82007
- credentials_in_cursor_config: false,
82142
+ credentials_in_client_config: false,
82143
+ ...client === "cursor" ? { credentials_in_cursor_config: false } : {},
82008
82144
  tools: tools2,
82009
82145
  not_exposed_to_mcp: defaultBlockedToolSurface()
82010
82146
  };
82011
82147
  }
82012
- function formatCursorProjectPreview(report) {
82148
+ function formatManagedMcpProjectPreview(report) {
82013
82149
  const tools2 = Array.isArray(report.tools) ? report.tools.join(", ") : "";
82150
+ const clientName = String(report.client_name);
82014
82151
  return [
82015
- `Cursor project MCP ${String(report.action)} preview`,
82152
+ `${clientName} project MCP ${String(report.action)} preview`,
82016
82153
  `Mode: ${String(report.mode)}`,
82017
82154
  `Destination: ${String(report.destination)}`,
82018
82155
  ...report.mode === "authoring" ? ["Project root: .", "Transport: local stdio only"] : [`Runner config: ${String(report.config)}`, `Runner store: ${String(report.store)}`],
82019
82156
  `Model-facing tools: ${tools2 || "none"}`,
82020
82157
  "Approval, apply, revert, policy, credentials, and trusted identity stay outside MCP.",
82021
- "Other Cursor MCP servers and project settings are preserved.",
82158
+ `Other ${clientName} MCP servers and project settings are preserved.`,
82022
82159
  ""
82023
82160
  ].join("\n");
82024
82161
  }
82025
- function formatCursorProjectStatus(report) {
82162
+ function formatManagedMcpProjectStatus(report) {
82026
82163
  return [
82027
- `Cursor project MCP: ${report.state}`,
82164
+ `${report.client_name} project MCP: ${report.state}`,
82028
82165
  report.message,
82029
82166
  `Mode: ${report.mode}`,
82030
82167
  `Destination: ${report.destination}`,
@@ -82035,7 +82172,7 @@ function formatCursorProjectStatus(report) {
82035
82172
  ""
82036
82173
  ].join("\n");
82037
82174
  }
82038
- function isCursorAuthoringEntry(entry) {
82175
+ function isManagedAuthoringEntry(entry) {
82039
82176
  return Array.isArray(entry?.args) && entry.args.every((value) => typeof value === "string") && entry.args.includes("--authoring");
82040
82177
  }
82041
82178
  function stableStringArray(values) {
@@ -91605,7 +91742,7 @@ the local reviewed contract and proposal before writeback.
91605
91742
  ${cmd} boundary activate --headless --review-bundle boundary-review.json --config ./synapsor/synapsor.runner.json --confirm "ACTIVATE sha256:..." --identity reviewer --identity-key ./reviewer.pem --required-role boundary_reviewer --reason "Reviewed staging authority"
91606
91743
  ${cmd} boundary status [--project-root .] [--json]
91607
91744
  ${cmd} boundary diff [--project-root .] [--json]
91608
- ${cmd} mcp install cursor --project --authoring --project-root . --yes
91745
+ ${cmd} mcp install <cursor|claude-code|vscode> --project --authoring --project-root . --yes
91609
91746
 
91610
91747
  Draft the whole deterministic application boundary without opening a browser,
91611
91748
  inspect/export its disabled review state, and compare the generation lock with
@@ -91618,7 +91755,8 @@ exact digest confirmation, a short-lived nonce-bound decision, and a configured
91618
91755
  signed_key or jwt_oidc operator identity carrying the required role. --yes and
91619
91756
  an actor string are never sufficient. Workbench and CLI converge on the same
91620
91757
  activation checks. After activation, --authoring installs exactly
91621
- app.describe_data and app.explore_data in the current Cursor project. Scoped
91758
+ app.describe_data and app.explore_data in the selected Cursor, Claude Code, or
91759
+ VS Code project. Scoped
91622
91760
  Explore remains local stdio only and is absent from production and remote HTTP.
91623
91761
  `,
91624
91762
  action: `Usage:
@@ -91681,10 +91819,10 @@ Drizzle input is parsed as a bounded TypeScript AST and is never imported or run
91681
91819
  ${cmd} mcp serve-http --config ./synapsor.runner.json --store ./.synapsor/local.db --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
91682
91820
  ${cmd} mcp config --absolute-paths --config ./synapsor.runner.json --store ./.synapsor/local.db
91683
91821
  ${cmd} mcp client-config --client openai-agents --config ./synapsor.runner.json --store ./.synapsor/local.db
91684
- ${cmd} mcp install cursor --project --authoring [--project-root .] [--dry-run]
91685
- ${cmd} mcp install cursor --project [--dry-run] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
91686
- ${cmd} mcp status cursor --project [--check-launch]
91687
- ${cmd} mcp uninstall cursor --project [--dry-run]
91822
+ ${cmd} mcp install <cursor|claude-code|vscode> --project --authoring [--project-root .] [--dry-run]
91823
+ ${cmd} mcp install <cursor|claude-code|vscode> --project [--dry-run] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db]
91824
+ ${cmd} mcp status <cursor|claude-code|vscode> --project [--check-launch]
91825
+ ${cmd} mcp uninstall <cursor|claude-code|vscode> --project [--dry-run]
91688
91826
  ${cmd} mcp audit --example dangerous-db-mcp
91689
91827
  ${cmd} mcp audit ./tools-list.json
91690
91828
  ${cmd} mcp audit generate ./tools-list.json --output ./synapsor-audit-candidates
@@ -91694,10 +91832,11 @@ Stdio opens no network socket and needs no HTTP credential. Networked MCP is aut
91694
91832
  MCP clients see semantic tools. They do not receive raw SQL, write credentials, approval tools, or commit tools.
91695
91833
  `,
91696
91834
  "mcp install": `Usage:
91697
- ${cmd} mcp install cursor --project --authoring [--project-root .] [--dry-run] [--yes]
91698
- ${cmd} mcp install cursor --project [--project-root .] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db] [--dry-run] [--yes]
91835
+ ${cmd} mcp install <cursor|claude-code|vscode> --project --authoring [--project-root .] [--dry-run] [--yes]
91836
+ ${cmd} mcp install <cursor|claude-code|vscode> --project [--project-root .] [--config ./synapsor.runner.json] [--store ./.synapsor/local.db] [--dry-run] [--yes]
91699
91837
 
91700
- Preview, confirm, and merge a project-scoped Synapsor entry into .cursor/mcp.json.
91838
+ Preview, confirm, and merge a project-scoped Synapsor entry into
91839
+ .cursor/mcp.json, .mcp.json, or .vscode/mcp.json.
91701
91840
  Runner preserves other servers/settings, creates a backup before changing an
91702
91841
  existing file, records explicit ownership, and never writes database URLs,
91703
91842
  credentials, trusted identity, approval, apply, revert, or policy authority.
@@ -91708,16 +91847,16 @@ runtime config. Re-run without --authoring after Protect activates a production
91708
91847
  named capability.
91709
91848
  `,
91710
91849
  "mcp status": `Usage:
91711
- ${cmd} mcp status cursor --project [--project-root .] [--check-launch] [--timeout-ms 10000] [--json]
91850
+ ${cmd} mcp status <cursor|claude-code|vscode> --project [--project-root .] [--check-launch] [--timeout-ms 10000] [--json]
91712
91851
 
91713
- Verify Runner's project-scoped Cursor ownership marker and print the exact
91852
+ Verify Runner's project-scoped client ownership marker and print the exact
91714
91853
  reviewed model-facing tools. --check-launch performs a real stdio initialize +
91715
91854
  tools/list handshake with the configured command.
91716
91855
  `,
91717
91856
  "mcp uninstall": `Usage:
91718
- ${cmd} mcp uninstall cursor --project [--project-root .] [--dry-run] [--yes] [--json]
91857
+ ${cmd} mcp uninstall <cursor|claude-code|vscode> --project [--project-root .] [--dry-run] [--yes] [--json]
91719
91858
 
91720
- Remove only the Runner-owned Synapsor entry. Other Cursor MCP servers and
91859
+ Remove only the Runner-owned Synapsor entry. Other client MCP servers and
91721
91860
  project settings are preserved, and edited/unowned entries fail closed.
91722
91861
  `,
91723
91862
  tools: `Usage:
@@ -91922,6 +92061,7 @@ security guarantee.
91922
92061
  ${cmd} doctor --config synapsor.runner.json --check-handlers
91923
92062
  ${cmd} doctor --config synapsor.runner.json --check-writeback
91924
92063
  ${cmd} doctor --config synapsor.runner.json --check-rls
92064
+ ${cmd} doctor --config synapsor.runner.json --check-mcp-client <cursor|claude-code|vscode>
91925
92065
  ${cmd} doctor --config synapsor.runner.json --transport streamable-http --host 127.0.0.1 --auth-token-env SYNAPSOR_RUNNER_HTTP_TOKEN
91926
92066
  ${cmd} doctor --config synapsor.runner.json --transport streamable-http --host 0.0.0.0 --trusted-tls-proxy
91927
92067
  ${cmd} doctor --config synapsor.runner.json --transport streamable-http --host 0.0.0.0 --tls-cert-env SYNAPSOR_TLS_CERT_PEM --tls-key-env SYNAPSOR_TLS_KEY_PEM