@geraldmaron/construct 1.3.0 → 1.3.2

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.
@@ -53,11 +53,16 @@ export function validateAllGoldenArtifactGates({ rootDir } = {}) {
53
53
  return { pass: errors.length === 0, results, errors, matrix: artifactGateMatrix({ rootDir: root }) };
54
54
  }
55
55
 
56
+ // The committed gate matrix is a deterministic projection of the rootDir; no
57
+ // wall-clock field, so regenerating it (the artifact-gates test writes it on every
58
+ // run) yields a byte-identical file instead of a timestamp-only diff on a tracked
59
+ // artifact. The matrix content is the signal; when it changes, the diff is real.
60
+
56
61
  export function writeArtifactGateMatrixDoc({ rootDir } = {}) {
57
62
  const root = findConstructRoot(rootDir);
58
63
  const out = path.join(root, 'tests', 'certification', 'artifacts', 'gate-matrix.json');
59
64
  fs.mkdirSync(path.dirname(out), { recursive: true });
60
- const payload = { generatedAt: new Date().toISOString(), matrix: artifactGateMatrix({ rootDir: root }) };
65
+ const payload = { matrix: artifactGateMatrix({ rootDir: root }) };
61
66
  fs.writeFileSync(out, `${JSON.stringify(payload, null, 2)}\n`);
62
67
  return out;
63
68
  }
@@ -101,16 +101,26 @@ export async function runCli(args) {
101
101
  }
102
102
 
103
103
  if (sub === 'consistency') {
104
+ const strict = args.some((a) => a === '--strict' || a === '--all' || a === '--debug');
104
105
  const { runAllChecks } = await import('./watchers/consistency.mjs');
105
106
  const result = await runAllChecks();
106
107
  const blocking = result.findings.filter((f) => f.severity === 'blocking');
107
108
  const warnings = result.findings.filter((f) => f.severity === 'warning');
108
109
 
110
+ // Default output carries only user-actionable signal; package/maintainer drift
111
+ // (tier 'internal') is gated behind --strict so a clean install reads clean.
112
+
113
+ const visibleWarnings = strict ? warnings : warnings.filter((f) => f.tier !== 'internal');
114
+ const internalWarnings = warnings.filter((f) => f.tier === 'internal');
115
+
109
116
  for (const p of result.passed) console.log(` ✓ ${p.category.padEnd(18)} ${p.summary}`);
110
- for (const w of warnings) console.log(` ⚠ ${w.category.padEnd(18)} ${w.summary}`);
117
+ for (const w of visibleWarnings) console.log(` ⚠ ${w.category.padEnd(18)} ${w.summary}`);
111
118
  for (const b of blocking) console.log(` ✗ ${b.category.padEnd(18)} ${b.summary}`);
112
119
 
113
- console.log(`\n${result.passed.length} category(s) clean, ${warnings.length} warning(s), ${blocking.length} blocking finding(s)`);
120
+ console.log(`\n${result.passed.length} category(s) clean, ${visibleWarnings.length} warning(s), ${blocking.length} blocking finding(s)`);
121
+ if (!strict && internalWarnings.length > 0) {
122
+ console.log(` (+${internalWarnings.length} package-internal diagnostic(s) — run \`construct doctor consistency --strict\` to view)`);
123
+ }
114
124
  return blocking.length > 0 ? 1 : 0;
115
125
  }
116
126
 
@@ -19,7 +19,7 @@
19
19
  * directory listings, no network.
20
20
  */
21
21
 
22
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
22
+ import { existsSync, readFileSync, statSync } from 'node:fs';
23
23
  import { loadRegistry } from '../../registry/loader.mjs';
24
24
  import { join, dirname, resolve } from 'node:path';
25
25
  import os from 'node:os';
@@ -97,15 +97,32 @@ export async function runAllChecks({ repoRoot = REPO_ROOT } = {}) {
97
97
  return { findings, passed };
98
98
  }
99
99
 
100
+ // Drift categories split into two operator tiers. `actionable` findings are
101
+ // project state a user can fix and surface by default. `internal` findings are
102
+ // package/maintainer diagnostics about Construct's own registry and MCP wiring —
103
+ // never user-actionable in a consumer project (this watcher resolves REPO_ROOT to
104
+ // the installed package), so they stay behind `doctor consistency --strict`.
105
+
106
+ const CATEGORY_TIERS = {
107
+ 'mcp-drift': 'internal',
108
+ 'roles-drift': 'internal',
109
+ };
110
+
111
+ function tierFor(category) {
112
+ return CATEGORY_TIERS[category] || 'actionable';
113
+ }
114
+
100
115
  function collect(result, findings, passed, defaultCategory) {
101
116
  if (result.violations.length === 0) {
102
- passed.push({ category: defaultCategory, summary: result.summary });
117
+ passed.push({ category: defaultCategory, summary: result.summary, tier: tierFor(defaultCategory) });
103
118
  return;
104
119
  }
105
120
  for (const v of result.violations) {
121
+ const category = v.category || defaultCategory;
106
122
  findings.push({
107
- category: v.category || defaultCategory,
123
+ category,
108
124
  severity: v.severity || 'warning',
125
+ tier: v.tier || tierFor(category),
109
126
  summary: v.summary,
110
127
  target: v.target || null,
111
128
  details: v.details || null,
@@ -184,38 +201,51 @@ function checkMcpDrift({ repoRoot }) {
184
201
  }
185
202
 
186
203
  const serverSource = readFileSync(serverPath, 'utf8');
187
- const dispatchedTools = new Set();
188
- for (const m of serverSource.matchAll(/name === ['"]([a-z][a-z0-9_]*)['"]/g)) {
189
- dispatchedTools.add(m[1]);
190
- }
191
204
 
192
- const exportedTools = new Set();
193
- const entries = readdirSync(toolsDir, { withFileTypes: true });
194
- for (const entry of entries) {
195
- if (!entry.isFile() || !entry.name.endsWith('.mjs')) continue;
196
- const src = readFileSync(join(toolsDir, entry.name), 'utf8');
197
- for (const m of src.matchAll(/export (?:async )?function (\w+)/g)) {
198
- exportedTools.add(camelToSnake(m[1]));
205
+ // Candidate tool handlers are exactly the identifiers server.mjs imports from
206
+ // ./tools/*.mjs. Tool modules also export private helpers (exec, readJson) that
207
+ // are never MCP tools; scanning every `export function` swept those into the
208
+ // signal. Scoping to the server's own imports is the real contract surface.
209
+
210
+ const importedHandlers = new Set();
211
+ for (const m of serverSource.matchAll(/import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*['"]\.\/tools\/[^'"]+['"]/g)) {
212
+ for (const ident of m[1].split(',')) {
213
+ const local = ident.trim().split(/\s+as\s+/).pop().trim();
214
+ if (local) importedHandlers.add(local);
199
215
  }
200
216
  }
201
217
 
202
- for (const tool of exportedTools) {
203
- if (KNOWN_NON_DISPATCH_TOOLS.has(tool)) continue;
204
- if (!dispatchedTools.has(tool)) {
218
+ // A handler is wired when the dispatcher invokes it, regardless of the
219
+ // registered tool name. Handlers follow xxxTool→'xxx' and construct_xxx naming
220
+ // conventions, so matching exported names against `name === '<tool>'` strings is
221
+ // unreliable; matching the invoked identifier is convention-agnostic.
222
+
223
+ const invoked = new Set();
224
+ for (const m of serverSource.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) {
225
+ invoked.add(m[1]);
226
+ }
227
+
228
+ for (const handler of importedHandlers) {
229
+ const snake = camelToSnake(handler);
230
+ if (KNOWN_NON_DISPATCH_TOOLS.has(handler) || KNOWN_NON_DISPATCH_TOOLS.has(snake)) continue;
231
+ if (!invoked.has(handler)) {
205
232
  violations.push({
206
233
  category: 'mcp-drift',
207
234
  severity: 'warning',
208
- target: tool,
209
- summary: `MCP tool exported but not dispatched: ${tool}`,
235
+ tier: 'internal',
236
+ target: snake,
237
+ summary: `MCP tool handler imported but never dispatched: ${handler}`,
210
238
  });
211
239
  }
212
240
  }
213
241
 
214
- return { summary: `mcp: ${dispatchedTools.size} dispatched, ${exportedTools.size} exported, ${violations.length} drift`, violations };
242
+ return { summary: `mcp: ${importedHandlers.size} handlers, ${violations.length} drift`, violations };
215
243
  }
216
244
 
245
+ // Handlers the server may import for re-export or test surface but intentionally
246
+ // never dispatches. Matched by camelCase identifier or snake_case form.
247
+
217
248
  const KNOWN_NON_DISPATCH_TOOLS = new Set([
218
- // Internal helpers exported for tests but never wired through MCP.
219
249
  'workflow_status_bound',
220
250
  'create_needs_main_input_packet',
221
251
  ]);
@@ -263,21 +293,28 @@ function personaOwnersFromRegistry(registry) {
263
293
  owners.get(personaId).add(owner);
264
294
  }
265
295
 
296
+ // One registry entity owns a persona once. A specialist's id and its own name
297
+ // normalize to the same persona by design (cx-architect / architect), so they
298
+ // must share a single owner token keyed on the entity — otherwise every
299
+ // specialist self-collides and reports a spurious "ambiguous" drift. Genuine
300
+ // ambiguity (two distinct specialists, or a specialist and the orchestrator,
301
+ // normalizing to the same persona) still yields owners.size > 1.
302
+
266
303
  for (const [specId, spec] of Object.entries(registry?.specialists || {})) {
267
304
  addOwner(specId, `specialist:${specId}`);
268
- addOwner(spec?.name, `specialist:${specId}:name`);
305
+ addOwner(spec?.name, `specialist:${specId}`);
269
306
  }
270
307
 
271
308
  if (Array.isArray(registry?.specialists)) {
272
309
  for (const spec of registry.specialists) {
273
310
  const owner = `specialist:${spec?.id || spec?.name || 'unknown'}`;
274
311
  addOwner(spec?.id, owner);
275
- addOwner(spec?.name, `${owner}:name`);
312
+ addOwner(spec?.name, owner);
276
313
  }
277
314
  }
278
315
 
279
- addOwner(registry?.orchestrator?.id, 'orchestrator:id');
280
- addOwner(registry?.orchestrator?.name, 'orchestrator:name');
316
+ addOwner(registry?.orchestrator?.id, 'orchestrator');
317
+ addOwner(registry?.orchestrator?.name, 'orchestrator');
281
318
  return owners;
282
319
  }
283
320
 
@@ -38,7 +38,7 @@ import {
38
38
  } from './tools/storage.mjs';
39
39
  import {
40
40
  listSkills, getSkill, searchSkills, getTemplate, listTemplates,
41
- agentContract, brokerCheck, orchestrationPolicy, workerRun, listTeams, getTeam, suggestSkillsTool,
41
+ agentContract, brokerCheck, orchestrationPolicy, workerRun, listTeams, suggestSkillsTool,
42
42
  } from './tools/skills.mjs';
43
43
  import {
44
44
  workflowInit, workflowAddTask, workflowUpdateTask,
@@ -339,35 +339,33 @@ export async function cmdMcpAdd(id) {
339
339
  }
340
340
  }
341
341
 
342
- // GitHub: source token from gh CLI, env, or prompt (--token flag)
343
- if (id === 'github') {
342
+ // GitHub's hosted remote server uses one-click OAuth by default: the host runs
343
+ // the browser flow and stores the token in its own secure store, so we wire the
344
+ // URL only and keep no credential. `--token`/`--pat` opts into the PAT fallback
345
+ // (headless/CI), sourced from gh CLI or env and emitted as an env reference.
346
+ let authMode = id === 'github' ? 'oauth' : 'pat';
347
+ if (id === 'github' && (flags.token || flags.pat)) {
348
+ authMode = 'pat';
344
349
  const envToken = process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
345
- const forceToken = flags.token;
346
-
347
350
  let ghCliToken = '';
348
351
  try {
349
352
  execSync('gh auth status -h github.com', { stdio: 'ignore' });
350
353
  ghCliToken = execSync('gh auth token -h github.com 2>/dev/null', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
351
354
  } catch { /* gh not installed or not authed */ }
352
355
 
353
- if (forceToken) {
356
+ const best = envToken || ghCliToken;
357
+ if (process.stdin.isTTY && typeof flags.token !== 'string') {
354
358
  const rl = createInterface({ input: process.stdin, output: process.stdout });
355
- const best = envToken || ghCliToken;
356
359
  const hint = best ? ' [press Enter to use detected token]' : '';
357
360
  const value = await prompt(rl, `\n > GITHUB_TOKEN${hint}: `);
358
361
  rl.close();
359
362
  envValues.GITHUB_TOKEN = value.trim() || best || '';
360
- } else if (ghCliToken) {
361
- console.log('\n ✓ GitHub token sourced from gh CLI (gh auth login).');
362
- envValues.GITHUB_TOKEN = ghCliToken;
363
- } else if (envToken) {
364
- console.log('\n ✓ GitHub token detected in environment.');
365
- envValues.GITHUB_TOKEN = envToken;
366
363
  } else {
367
- console.log('\n ✗ No GitHub token found.');
368
- console.log(' Run: gh auth login -h github.com (opens browser, stores token automatically)');
369
- console.log(' Then re-run: construct mcp add github');
370
- console.log(' Or provide a PAT: construct mcp add github --token');
364
+ envValues.GITHUB_TOKEN = (typeof flags.token === 'string' ? flags.token : '') || best || '';
365
+ }
366
+ if (!envValues.GITHUB_TOKEN) {
367
+ console.log('\n --token requested but no PAT found (gh CLI / env / prompt).');
368
+ console.log(' Run `gh auth login -h github.com`, or omit --token to use OAuth.');
371
369
  process.exit(1);
372
370
  }
373
371
  }
@@ -418,7 +416,7 @@ export async function cmdMcpAdd(id) {
418
416
  const settings = loadSettings();
419
417
  if (!settings.mcpServers) settings.mcpServers = {};
420
418
  if (isManagedOnHost(mcp, 'claude')) {
421
- settings.mcpServers[id] = buildClaudeMcpEntry(id, mcp, resolvedValues);
419
+ settings.mcpServers[id] = buildClaudeMcpEntry(id, mcp, resolvedValues, { auth: authMode });
422
420
  } else {
423
421
  delete settings.mcpServers[id];
424
422
  }
@@ -431,7 +429,7 @@ export async function cmdMcpAdd(id) {
431
429
  delete oc.mcp[id];
432
430
  if (id === 'memory') delete oc.mcp.cass;
433
431
  if (isManagedOnHost(mcp, 'opencode')) {
434
- oc.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcp, resolvedValues).entry;
432
+ oc.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcp, resolvedValues, { auth: authMode }).entry;
435
433
  } else {
436
434
  delete oc.mcp[openCodeId];
437
435
  }
@@ -451,8 +449,15 @@ export async function cmdMcpAdd(id) {
451
449
 
452
450
  console.log(`\n✓ ${mcp.name} successfully wired into Claude Code, OpenCode, and Codex.`);
453
451
  console.log(`✓ Setup mode: ${setupMode}`);
452
+ console.log(`✓ Auth: ${id === 'github' ? authMode : 'managed'}`);
454
453
  console.log(`✓ Services synchronized.`);
455
454
  console.log(`\nNext: Restart OpenCode, Claude Code, or Codex to activate the new tools.`);
455
+ if (id === 'github' && authMode === 'oauth') {
456
+ console.log(`\nAuthenticate (one-click OAuth, no token stored on disk):`);
457
+ console.log(` Claude Code: \`claude mcp login github\` · OpenCode: \`opencode mcp auth github\``);
458
+ console.log(` VS Code / Cursor: approve the sign-in prompt on first use.`);
459
+ console.log(` PAT fallback (headless/CI): \`construct mcp add github --token\``);
460
+ }
456
461
  for (const host of ['claude', 'opencode', 'codex']) {
457
462
  const support = getHostSupport(mcp, host);
458
463
  const hostLabel = host === 'claude' ? 'Claude Code' : host === 'opencode' ? 'OpenCode' : 'Codex';
@@ -40,14 +40,24 @@ function resolveArgs(args, resolvedValues) {
40
40
  );
41
41
  }
42
42
 
43
+ // Remote MCP header values carry secrets (e.g. `Authorization: Bearer __GITHUB_TOKEN__`).
44
+ // Emit a host-resolved env reference rather than the literal token value, so a live
45
+ // credential never lands in a config file on disk. Each host expands a different
46
+ // reference syntax; the secret stays in one place (config.env / the shell env) and the
47
+ // host resolves it at launch.
48
+
49
+ const SECRET_REF = {
50
+ claude: (name) => `\${${name}}`,
51
+ vscode: (name) => `\${env:${name}}`,
52
+ opencode: (name) => `{env:${name}}`,
53
+ };
54
+
43
55
  function buildLocalEnvironment(mcpDef, resolvedValues) {
44
56
  return stripUnresolvedValues(resolveTemplateObject(mcpDef.env ?? {}, resolvedValues));
45
57
  }
46
58
 
47
- function buildRemoteHeaders(mcpDef, resolvedValues) {
48
- return stripUnresolvedValues(
49
- resolveTemplateObject(mcpDef.headers ?? {}, resolvedValues, (name) => `{env:${name}}`),
50
- );
59
+ function buildRemoteHeaders(mcpDef, secretRef) {
60
+ return resolveTemplateObject(mcpDef.headers ?? {}, {}, secretRef);
51
61
  }
52
62
 
53
63
  function withMemoryDefaults(id, values) {
@@ -56,11 +66,18 @@ function withMemoryDefaults(id, values) {
56
66
  return { ...values, MEMORY_PORT: "8765" };
57
67
  }
58
68
 
59
- export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}) {
69
+ // auth defaults to "oauth": remote MCP servers (e.g. GitHub's hosted server) are
70
+ // configured by URL only, and the host runs the browser OAuth flow and stores the
71
+ // token in its own secure store — nothing secret touches the config file. "pat"
72
+ // is the opt-in fallback for non-OAuth contexts (headless/CI), emitting an env
73
+ // reference for the credential, never the literal value.
74
+
75
+ export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}, { host = "claude", auth = "oauth" } = {}) {
60
76
  const values = withMemoryDefaults(id, { CX_TOOLKIT_DIR: ROOT_DIR, ...resolvedValues });
77
+ const secretRef = SECRET_REF[host] ?? SECRET_REF.claude;
61
78
 
62
79
  if (mcpDef.type === "url") {
63
- const headers = buildRemoteHeaders(mcpDef, values);
80
+ const headers = auth === "pat" ? buildRemoteHeaders(mcpDef, secretRef) : {};
64
81
  const url =
65
82
  id === "memory" && values.MEMORY_PORT
66
83
  ? `http://127.0.0.1:${values.MEMORY_PORT}/`
@@ -80,7 +97,7 @@ export function buildClaudeMcpEntry(id, mcpDef, resolvedValues = {}) {
80
97
  };
81
98
  }
82
99
 
83
- export function buildOpenCodeMcpEntry(id, mcpDef, resolvedValues = {}) {
100
+ export function buildOpenCodeMcpEntry(id, mcpDef, resolvedValues = {}, { auth = "oauth" } = {}) {
84
101
  const runtimeValues = withMemoryDefaults(id, {
85
102
  CX_TOOLKIT_DIR: ROOT_DIR,
86
103
  ...resolvedValues,
@@ -92,12 +109,13 @@ export function buildOpenCodeMcpEntry(id, mcpDef, resolvedValues = {}) {
92
109
  id === "memory" && runtimeValues.MEMORY_PORT
93
110
  ? `http://127.0.0.1:${runtimeValues.MEMORY_PORT}/`
94
111
  : resolveTemplateString(mcpDef.url, runtimeValues);
112
+ const headers = auth === "pat" ? buildRemoteHeaders(mcpDef, SECRET_REF.opencode) : {};
95
113
  return {
96
114
  id: openCodeId,
97
115
  entry: {
98
116
  type: "remote",
99
117
  url,
100
- ...(Object.keys(buildRemoteHeaders(mcpDef, runtimeValues)).length > 0 ? { headers: buildRemoteHeaders(mcpDef, runtimeValues) } : {}),
118
+ ...(Object.keys(headers).length > 0 ? { headers } : {}),
101
119
  },
102
120
  };
103
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "type": "module",
5
5
  "packageManager": "npm@11.5.1",
6
6
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
@@ -1002,13 +1002,20 @@ ${buildPrompt(entry, allEntries, "claude")}
1002
1002
  * Rewrite the home-mode hook command pattern
1003
1003
  * node "$HOME/.config/construct/lib/hooks/<name>.mjs"
1004
1004
  * into the project-portable form
1005
- * node .construct/run.mjs hook <name>
1005
+ * node "${CLAUDE_PROJECT_DIR:-.}/.construct/run.mjs" hook <name>
1006
1006
  * so the resulting settings.json works on any clone where the project ships
1007
1007
  * the .construct/ launcher (committed by `npm install`'s postinstall or by
1008
1008
  * `construct init`). The launcher resolves Construct via node_modules → npx
1009
1009
  * → globally-installed CLI → cached binary → docker, in that order, so it
1010
1010
  * works for non-Node ecosystems too. Other commands (inline node -e
1011
1011
  * snippets, npx block-no-verify@…) are left untouched.
1012
+ *
1013
+ * The `${CLAUDE_PROJECT_DIR:-.}` anchor matters: Claude Code invokes hooks with
1014
+ * a working directory that is not guaranteed to be the project root (observed:
1015
+ * $HOME), and a bare relative `.construct/run.mjs` then fails with MODULE_NOT_FOUND
1016
+ * at node:internal/modules/cjs/loader. CLAUDE_PROJECT_DIR is the project root Claude
1017
+ * Code exports to every hook (same var lib/hooks/*.mjs already read); the `:-.`
1018
+ * fallback preserves the prior relative behavior wherever the var is unset.
1012
1019
  */
1013
1020
  function makeHooksPortable(hooksJson) {
1014
1021
  // Operate on the in-memory object so we don't fight JSON string escaping.
@@ -1017,7 +1024,7 @@ function makeHooksPortable(hooksJson) {
1017
1024
  const m = cmd.match(/^node\s+"?\$HOME\/\.config\/construct\/lib\/hooks\/([a-z0-9-]+)\.mjs"?\s*(.*)$/);
1018
1025
  if (!m) return cmd;
1019
1026
  const [, name, rest] = m;
1020
- return `node .construct/run.mjs hook ${name}${rest ? ' ' + rest.trim() : ''}`;
1027
+ return `node "\${CLAUDE_PROJECT_DIR:-.}/.construct/run.mjs" hook ${name}${rest ? ' ' + rest.trim() : ''}`;
1021
1028
  };
1022
1029
 
1023
1030
  const walk = (node) => {
@@ -1474,7 +1481,7 @@ function syncVSCode(targetDir = null, wants = true) {
1474
1481
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1475
1482
  const transportMismatch = registryWantsCommand && existingIsRemote;
1476
1483
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1477
- config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1484
+ config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1478
1485
  }
1479
1486
  if (!DRY_RUN) {
1480
1487
  mkdirp(path.dirname(mcpPath));
@@ -1498,7 +1505,7 @@ function syncVSCode(targetDir = null, wants = true) {
1498
1505
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1499
1506
  const transportMismatch = registryWantsCommand && existingIsRemote;
1500
1507
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1501
- config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1508
+ config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1502
1509
  }
1503
1510
  if (!DRY_RUN) fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
1504
1511
  synced = true;
@@ -1568,7 +1575,7 @@ function syncCursor(targetDir = null, wants = true) {
1568
1575
  const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
1569
1576
  const transportMismatch = registryWantsCommand && existingIsRemote;
1570
1577
  if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
1571
- config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
1578
+ config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
1572
1579
  }
1573
1580
  if (!DRY_RUN) {
1574
1581
  mkdirp(path.dirname(cursorMcpPath));