From 9b3aea9a89f585a178be5e59e2f2977242523865 Mon Sep 17 00:00:00 2001 From: Gunnar Nelson Date: Tue, 15 Sep 2026 20:58:57 -0700 Subject: [PATCH] Install the published package instead of trusting npx, and make npm the default 0.1.1 is on the registry, so the installation notice and the release-tarball detour come out and npx becomes the documented path again. The walkthrough had a real bug that only appeared once npm was the spec. It ran `npx --yes `, and npx resolves a LOCAL package first. Run from inside this repository, whose package.json declares this very bin, npx tried the working tree and died with "command not found". The README tells you to run it from the clone, so that was the normal case, and it silently defeated the one property the walkthrough exists to guarantee: that it exercises what a developer installs. It now installs the spec into a throwaway prefix and runs that binary directly. The guarantee is real rather than dependent on npx's resolution order. Verified against the published npm artifact from inside the clone: server reports 0.1.1, untampered returns requires_approval with the correct violation, tampered is rejected. The 0.1.0 pin warning stays. That version is still on the registry and still has the first-use defect. --- README.md | 36 +++++++++++++------------- SHA256SUMS.txt | 1 + examples/verify-mandate.mjs | 51 ++++++++++++++++++++++++------------- 3 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 SHA256SUMS.txt diff --git a/README.md b/README.md index da82c8c..03cc26d 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,6 @@ does not check a signature. --- -> **Installation notice.** The npm registry currently serves **0.1.0, which has a known -> first-use defect**: its input schema accepted any object for `mandate`, so a call built -> from the keyless demo's shape returned `HTTP 400 invalid_request`. Use the tested -> GitHub Release below until `0.1.1` is on npm. This notice is removed once the registry -> artifact is validated. - ## Run the walkthrough ```bash @@ -38,8 +32,9 @@ npm ci AGENT_MANDATE_API_KEY=your_key node examples/verify-mandate.mjs ``` -The example runner comes from this repository; the **MCP server it starts is the -released 0.1.1 artifact**, downloaded from the release below, not your working tree. +The example runner comes from this repository, but it installs +`@api-disk-integrations/agent-mandate-mcp@0.1.1` **from npm into a throwaway prefix** +and runs that, so it exercises the published artifact rather than your working tree. Omit `AGENT_MANDATE_API_KEY` and it prompts without echoing. ### What it prints @@ -71,23 +66,18 @@ correct result, not a failure. ## Install the server -Until `0.1.1` is on npm, install from the release tarball: - ```bash -curl -fsSLO https://github.com/API-Disk-Integrations/agent-mandate-mcp/releases/download/v0.1.1/api-disk-integrations-agent-mandate-mcp-0.1.1.tgz -shasum -a 256 api-disk-integrations-agent-mandate-mcp-0.1.1.tgz -npm install -g ./api-disk-integrations-agent-mandate-mcp-0.1.1.tgz +npx --yes @api-disk-integrations/agent-mandate-mcp@0.1.1 ``` -Compare the checksum against the one published on the release page before installing. - A generic stdio client configuration: ```json { "mcpServers": { "agent-mandate": { - "command": "agent-mandate-mcp", + "command": "npx", + "args": ["--yes", "@api-disk-integrations/agent-mandate-mcp@0.1.1"], "env": { "AGENT_MANDATE_API_KEY": "${AGENT_MANDATE_API_KEY}" } @@ -100,8 +90,18 @@ A generic stdio client configuration: documented secret facility if its syntax differs. The package uses stdio and reads exactly that environment variable. It has no remote `/mcp` endpoint. -Once `0.1.1` is published, the command becomes -`npx --yes @api-disk-integrations/agent-mandate-mcp@0.1.1`. +**Pin the version.** `0.1.0` is still on the registry and has a first-use defect: its +input schema accepted any object for `mandate`, so a call built from the keyless +demo's shape returned `HTTP 400`. + +If you would rather verify a checksummed artifact, every release also attaches a +tarball and its SHA-256: + +```bash +curl -fsSLO https://github.com/API-Disk-Integrations/agent-mandate-mcp/releases/download/v0.1.1/api-disk-integrations-agent-mandate-mcp-0.1.1.tgz +shasum -a 256 api-disk-integrations-agent-mandate-mcp-0.1.1.tgz # compare with the release page +npm install -g ./api-disk-integrations-agent-mandate-mcp-0.1.1.tgz +``` ## The two shapes, which is the thing that trips people up diff --git a/SHA256SUMS.txt b/SHA256SUMS.txt new file mode 100644 index 0000000..c3b1349 --- /dev/null +++ b/SHA256SUMS.txt @@ -0,0 +1 @@ +3bccda202e8a4b24f5a6cd9317207eef4852b8e825551624fb3d8017c6431fda api-disk-integrations-agent-mandate-mcp-0.1.1.tgz diff --git a/examples/verify-mandate.mjs b/examples/verify-mandate.mjs index 840ea22..54302f4 100755 --- a/examples/verify-mandate.mjs +++ b/examples/verify-mandate.mjs @@ -20,22 +20,16 @@ * printed, never written to disk, and never passed on a command line. The * signature is printed only as a short prefix. */ -import { spawn } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import { createInterface } from 'node:readline'; -import { mkdtempSync, rmSync } from 'node:fs'; +import { existsSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const API = process.env.AGENT_MANDATE_API_URL ?? 'https://agentmandate-api.com'; -/** - * Defaults to the GitHub Release tarball because npm currently serves 0.1.0, - * which has the first-use defect this version fixes. Override to test another - * artifact. Once 0.1.1 is on npm this becomes - * '@api-disk-integrations/agent-mandate-mcp@0.1.1'. - */ -const SPEC = process.env.AGENT_MANDATE_MCP_SPEC ?? - 'https://github.com/API-Disk-Integrations/agent-mandate-mcp/releases/download/v0.1.1/api-disk-integrations-agent-mandate-mcp-0.1.1.tgz'; +/** The published package. Override to test a release tarball or a local build. */ +const SPEC = process.env.AGENT_MANDATE_MCP_SPEC ?? '@api-disk-integrations/agent-mandate-mcp@0.1.1'; /** The grant allows up to 100,000 minor units but requires approval above 25,000. */ const CLAIMS = { @@ -88,11 +82,31 @@ async function createMandate(key) { return body; } -/** A minimal stdio MCP client. Keeps the walkthrough dependency-free. */ -function startServer(key, cache) { - const child = spawn('npx', ['--yes', SPEC], { +/** + * Installs the published artifact into a throwaway prefix and runs its binary + * directly. + * + * WHY NOT `npx ` + * npx resolves a LOCAL package first. Run from inside this repository, whose + * package.json declares this very bin, npx tries the working tree instead of the + * published package and fails with "command not found". Since the README tells you + * to run this from the clone, that is the normal case, and it would silently defeat + * the point of testing what a developer actually installs. + */ +function installAndStart(key, dir) { + const install = spawnSync('npm', ['install', '--no-save', '--no-audit', '--no-fund', '--loglevel', 'error', '--prefix', dir, SPEC], { + encoding: 'utf8', + env: { ...process.env, npm_config_cache: join(dir, '.npm'), npm_config_update_notifier: 'false' }, + }); + if (install.status !== 0) { + throw new Error(`could not install ${SPEC}: ${(install.stderr || install.stdout || '').trim().slice(0, 400)}`); + } + const bin = join(dir, 'node_modules', '.bin', 'agent-mandate-mcp'); + if (!existsSync(bin)) throw new Error(`the installed package did not provide the agent-mandate-mcp binary at ${bin}`); + + const child = spawn(bin, [], { stdio: ['pipe', 'pipe', 'pipe'], - env: { ...process.env, AGENT_MANDATE_API_KEY: key, npm_config_cache: cache, CI: '1' }, + env: { ...process.env, AGENT_MANDATE_API_KEY: key }, }); const pending = new Map(); let id = 0, buf = '', stderr = ''; @@ -112,10 +126,10 @@ function startServer(key, cache) { const n = ++id; pending.set(n, { resolve, reject }); child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: n, method, params }) + '\n'); - setTimeout(() => { if (pending.has(n)) { pending.delete(n); reject(new Error(`${method} timed out. stderr: ${stderr.slice(0, 200)}`)); } }, 60000); + setTimeout(() => { if (pending.has(n)) { pending.delete(n); reject(new Error(`${method} timed out. stderr: ${stderr.slice(0, 300)}`)); } }, 60000); }); const notify = (method) => child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method }) + '\n'); - return { child, call, notify, stderr: () => stderr }; + return { child, call, notify }; } function describe(result) { @@ -142,9 +156,10 @@ try { console.log(`signature: ${String(envelope.signature).slice(0, 12)}… (${String(envelope.signature).length} chars)`); console.log('That whole body is the envelope. Pass it through unchanged.'); - console.log(`\n=== 2. Start the MCP server ===`); + console.log(`\n=== 2. Install and start the published MCP server ===`); console.log(`spec: ${SPEC}`); - server = startServer(key, cache); + console.log('installing into a throwaway prefix, so this tests the published artifact'); + server = installAndStart(key, cache); const info = await server.call('initialize', { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'verify-mandate-walkthrough', version: '1.0.0' }, });