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' }, });