@geraldmaron/construct 1.3.1 → 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 = {
|
|
65
|
+
const payload = { matrix: artifactGateMatrix({ rootDir: root }) };
|
|
61
66
|
fs.writeFileSync(out, `${JSON.stringify(payload, null, 2)}\n`);
|
|
62
67
|
return out;
|
|
63
68
|
}
|
package/lib/mcp-manager.mjs
CHANGED
|
@@ -339,35 +339,33 @@ export async function cmdMcpAdd(id) {
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
// GitHub
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
console.log('
|
|
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,
|
|
48
|
-
return
|
|
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
|
-
|
|
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,
|
|
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(
|
|
118
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
101
119
|
},
|
|
102
120
|
};
|
|
103
121
|
}
|
package/package.json
CHANGED
|
@@ -1481,7 +1481,7 @@ function syncVSCode(targetDir = null, wants = true) {
|
|
|
1481
1481
|
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
1482
1482
|
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1483
1483
|
if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
|
|
1484
|
-
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
1484
|
+
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
|
|
1485
1485
|
}
|
|
1486
1486
|
if (!DRY_RUN) {
|
|
1487
1487
|
mkdirp(path.dirname(mcpPath));
|
|
@@ -1505,7 +1505,7 @@ function syncVSCode(targetDir = null, wants = true) {
|
|
|
1505
1505
|
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
1506
1506
|
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1507
1507
|
if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
|
|
1508
|
-
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
1508
|
+
config.servers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
|
|
1509
1509
|
}
|
|
1510
1510
|
if (!DRY_RUN) fs.writeFileSync(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
1511
1511
|
synced = true;
|
|
@@ -1575,7 +1575,7 @@ function syncCursor(targetDir = null, wants = true) {
|
|
|
1575
1575
|
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
1576
1576
|
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1577
1577
|
if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
|
|
1578
|
-
config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
1578
|
+
config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env, { host: "vscode" });
|
|
1579
1579
|
}
|
|
1580
1580
|
if (!DRY_RUN) {
|
|
1581
1581
|
mkdirp(path.dirname(cursorMcpPath));
|