From e0a40d413c9df3d9a46440c3d8270ad5f47741b6 Mon Sep 17 00:00:00 2001 From: calvin-archastro Date: Wed, 19 Aug 2026 13:31:15 -0700 Subject: [PATCH] Follow latest Intern MCP release --- README.md | 21 ++++++++----- scripts/harness-install-smoke.mjs | 15 +++++++++- src/index.ts | 7 +++-- src/setup.test.ts | 46 +++++++++++++++++++++++++++++ src/setup.ts | 49 +++++++++++++++++++++++++++---- 5 files changed, 123 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 071b9e2..ea5aef7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It requires Node.js 22 or newer. ### Codex ```sh -npx --yes @archastro/intern-mcp@0.1.0 setup --host codex +npx --yes @archastro/intern-mcp@latest setup --host codex ``` Restart Codex after adding the server. Codex stores the stdio command in its @@ -20,7 +20,7 @@ user configuration and starts the package when a session needs the server. ### Claude Code ```sh -npx --yes @archastro/intern-mcp@0.1.0 setup --host claude +npx --yes @archastro/intern-mcp@latest setup --host claude ``` Add `--verbose` to either setup command to print redacted request lifecycle @@ -47,10 +47,12 @@ setup with a new token, restart the host, and revoke the old token on the Connect page. Revocation blocks new API calls and SSH certificates immediately; a Git certificate already issued can remain valid until its five-minute expiry. -Upgrades are explicit: rerun the corresponding setup command with the reviewed -version replacing `0.1.0`. The installer updates only the user-level `intern` -registration. The host never executes a newly published package version merely -because it restarted. +The setup command resolves npm's stable `latest` release and saves a launcher +that checks that channel whenever the MCP host starts. The saved launcher uses +`--prefer-online` to refresh stale package metadata instead of trusting its npx +cache; the one-time setup command does not expose that runtime policy. To pin a +reviewed build instead, run setup with `INTERN_MCP_PACKAGE` set to a complete +package spec such as `@archastro/intern-mcp@0.1.1`, or to a package tarball. The repository is private; the package is public on npm. @@ -59,9 +61,14 @@ that local mapping for this public package: ```sh npx --yes --@archastro:registry=https://registry.npmjs.org \ - @archastro/intern-mcp@0.1.0 setup --host codex + @archastro/intern-mcp@latest setup --host codex \ + --registry https://registry.npmjs.org ``` +The first registry option lets npx find the setup executable. The setup +`--registry` option saves the same scoped override in the Codex or Claude +launcher so later host restarts continue resolving the public package. + Maintainers run the manual **release** workflow from `main` and choose a patch, minor, or major bump. It verifies the package, commits the version change on a release branch, rebase-merges the version-only PR, tags that exact merged commit diff --git a/scripts/harness-install-smoke.mjs b/scripts/harness-install-smoke.mjs index 8a889ba..e689f2f 100644 --- a/scripts/harness-install-smoke.mjs +++ b/scripts/harness-install-smoke.mjs @@ -103,6 +103,8 @@ try { "--host", "codex", "--verbose", + "--registry", + publicRegistry, ]); if (!codexSetup.stdout.includes("Intern connected to Codex as Harness · admin")) { throw new Error("packaged setup did not validate and configure Codex"); @@ -120,12 +122,21 @@ try { if ( !codex.stdout.includes("intern-mcp") || !codex.stdout.includes("launch") || + !codex.stdout.includes("--prefer-online") || + !codex.stdout.includes(`--@archastro:registry=${publicRegistry}`) || codex.stdout.includes("harness-proof-token") ) { throw new Error("Codex did not persist the packaged Intern MCP profile launcher"); } - const claudeSetup = await run("npx", [...setupCommand, "setup", "--host", "claude"]); + const claudeSetup = await run("npx", [ + ...setupCommand, + "setup", + "--host", + "claude", + "--registry", + publicRegistry, + ]); if ( !claudeSetup.stdout.includes("Intern connected to Claude Code as Harness · admin") ) { @@ -135,6 +146,8 @@ try { if ( !claude.stdout.includes("intern-mcp") || !claude.stdout.includes("launch") || + !claude.stdout.includes("--prefer-online") || + !claude.stdout.includes(`--@archastro:registry=${publicRegistry}`) || claude.stdout.includes("harness-proof-token") || !claude.stdout.includes("Connected") ) { diff --git a/src/index.ts b/src/index.ts index ab5faf7..30da4d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,7 +34,10 @@ switch (command) { case "setup": try { const options = parseSetupOptions(process.argv.slice(3)); - await runSetup(config, options.host, { verbose: options.verbose }); + await runSetup(config, options.host, { + verbose: options.verbose, + registry: options.registry, + }); } catch (error) { process.stderr.write( `Intern setup failed: ${error instanceof Error ? error.message : "request failed"}\n`, @@ -44,7 +47,7 @@ switch (command) { break; default: process.stderr.write( - "Usage: intern-mcp serve|launch|status|setup --host codex|claude [--verbose]\n", + "Usage: intern-mcp serve|launch|status|setup --host codex|claude [--verbose] [--registry URL]\n", ); process.exitCode = 2; } diff --git a/src/setup.test.ts b/src/setup.test.ts index ba99685..a7bf8bd 100644 --- a/src/setup.test.ts +++ b/src/setup.test.ts @@ -81,6 +81,7 @@ describe("Intern MCP setup", () => { args: expect.arrayContaining([ "add", "INTERN_WORKSPACE_ROOT=/tmp/Intern", + "--prefer-online", "--package=/tmp/intern-mcp.tgz", "launch", ]), @@ -94,6 +95,37 @@ describe("Intern MCP setup", () => { expect(output.join("")).not.toContain("secret-token"); }); + it("saves a launcher that refreshes the latest stable package", async () => { + const calls: Array<{ command: string; args: string[] }> = []; + await runSetup(config, "codex", { + token: "secret-token", + env, + registry: "https://registry.npmjs.org", + session: async () => session, + run: async (command, args) => { + calls.push({ command, args }); + return { + status: 0, + stdout: args.includes("get") + ? '{"command": "npx", "args": ["intern-mcp", "launch"]}' + : "ok", + }; + }, + write: () => {}, + }); + + expect(calls[0]).toMatchObject({ + command: "codex", + args: expect.arrayContaining([ + "--prefer-online", + "--@archastro:registry=https://registry.npmjs.org", + "--package=@archastro/intern-mcp@latest", + "intern-mcp", + "launch", + ]), + }); + }); + it("replaces only Claude's user-scoped Intern entry", async () => { const calls: string[][] = []; await runSetup(config, "claude", { @@ -244,8 +276,22 @@ describe("Intern MCP setup", () => { host: "claude", verbose: true, }); + expect( + parseSetupOptions(["--host=codex", "--registry", "https://registry.npmjs.org/"]), + ).toEqual({ + host: "codex", + verbose: false, + registry: "https://registry.npmjs.org", + }); expect(() => parseSetupOptions(["--host", "cursor"])).toThrow("Usage:"); expect(() => parseSetupOptions(["--host", "codex", "--debug"])).toThrow("Usage:"); + expect(() => + parseSetupOptions([ + "--host", + "codex", + "--registry=https://user:secret@example.com", + ]), + ).toThrow("Usage:"); }); it("reads a piped token without echoing it", async () => { diff --git a/src/setup.ts b/src/setup.ts index 5f35c22..1c003ff 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -12,7 +12,7 @@ import type { InternSession } from "./api.js"; import { PACKAGE_VERSION, type InternConfig } from "./config.js"; const exec = promisify(execFile); -const defaultPackage = `@archastro/intern-mcp@${PACKAGE_VERSION}`; +const defaultPackage = "@archastro/intern-mcp@latest"; export type SetupHost = "codex" | "claude"; @@ -40,6 +40,7 @@ interface SetupDependencies { write?: (message: string) => void; env?: NodeJS.ProcessEnv; verbose?: boolean; + registry?: string; } class HostConfigurationCommittedError extends Error {} @@ -47,15 +48,25 @@ class HostConfigurationCommittedError extends Error {} export function parseSetupOptions(args: string[]): { host: SetupHost; verbose: boolean; + registry?: string; } { let host: SetupHost | undefined; let verbose = false; + let registry: string | undefined; for (let index = 0; index < args.length; index += 1) { const value = args[index]; if (value === "--verbose") { verbose = true; continue; } + if (value === "--registry" || value.startsWith("--registry=")) { + if (registry !== undefined) throw new Error(setupUsage()); + const registryValue = value.startsWith("--registry=") + ? value.slice("--registry=".length) + : args[++index]; + registry = parseRegistry(registryValue); + continue; + } const hostValue = value.startsWith("--host=") ? value.slice("--host=".length) : value === "--host" @@ -71,7 +82,7 @@ export function parseSetupOptions(args: string[]): { host = hostValue; } if (!host) throw new Error(setupUsage()); - return { host, verbose }; + return { host, verbose, ...(registry ? { registry } : {}) }; } export async function runSetup( @@ -82,6 +93,7 @@ export async function runSetup( const promptToken = dependencies.promptToken ?? promptAccessToken; const env = dependencies.env ?? process.env; const verbose = dependencies.verbose ?? false; + const registry = dependencies.registry; const token = ( dependencies.token ?? env.INTERN_ACCESS_TOKEN ?? @@ -101,7 +113,7 @@ export async function runSetup( const previousToken = await snapshotFile(tokenFile); await writeAccessToken(config, token); try { - await configureHost(host, packageSpec, env, run); + await configureHost(host, packageSpec, registry, env, run); } catch (error) { if (!(error instanceof HostConfigurationCommittedError)) { await restoreFile(tokenFile, previousToken); @@ -124,10 +136,19 @@ export async function runSetup( async function configureHost( host: SetupHost, packageSpec: string, + registry: string | undefined, env: NodeJS.ProcessEnv, run: (command: string, args: string[]) => Promise, ): Promise { - const launcher = ["npx", "--yes", `--package=${packageSpec}`, "intern-mcp", "launch"]; + const launcher = [ + "npx", + "--yes", + "--prefer-online", + ...(registry ? [`--@archastro:registry=${registry}`] : []), + `--package=${packageSpec}`, + "intern-mcp", + "launch", + ]; const environmentArgs = [ "INTERN_BASE_URL", "INTERN_WORKSPACE_ROOT", @@ -324,7 +345,25 @@ function question(lines: Interface): Promise { } function setupUsage(): string { - return "Usage: intern-mcp setup --host codex|claude [--verbose]"; + return "Usage: intern-mcp setup --host codex|claude [--verbose] [--registry URL]"; +} + +function parseRegistry(value: string | undefined): string { + try { + const registry = new URL(value ?? ""); + if ( + registry.protocol !== "https:" || + registry.username || + registry.password || + registry.search || + registry.hash + ) { + throw new Error("unsafe registry"); + } + return registry.toString().replace(/\/$/, ""); + } catch { + throw new Error(setupUsage()); + } } export async function readStoredAccessToken(config: InternConfig): Promise {