From 8253a0ac27fb2f2d30e558d3006b62fc6ad84a29 Mon Sep 17 00:00:00 2001 From: Nader Helmy Date: Wed, 29 Jul 2026 02:21:35 -0500 Subject: [PATCH] fix: install Zod v4 in standalone MCP servers --- .changeset/warm-tools-negotiate.md | 6 + .github/workflows/ci.yml | 2 + CLAUDE.md | 2 + package.json | 4 +- pnpm-lock.yaml | 3 + scripts/check-mcp-v2-owned-surfaces.mjs | 11 +- scripts/check-mcp-v2-packed-runtime.mjs | 282 ++++++++++++++++++++++++ services/atrib-attest/package.json | 2 +- services/atrib-summarize/package.json | 2 +- 9 files changed, 305 insertions(+), 9 deletions(-) create mode 100644 .changeset/warm-tools-negotiate.md create mode 100644 scripts/check-mcp-v2-packed-runtime.mjs diff --git a/.changeset/warm-tools-negotiate.md b/.changeset/warm-tools-negotiate.md new file mode 100644 index 000000000..486a1ecd5 --- /dev/null +++ b/.changeset/warm-tools-negotiate.md @@ -0,0 +1,6 @@ +--- +'@atrib/attest': patch +'@atrib/summarize': patch +--- + +Install Zod v4 with the standalone MCP servers so fresh npm installs can register their tools and negotiate MCP 2026-07-28. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f9603be3..edd72f45c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,8 @@ jobs: - run: pnpm -r build + - run: pnpm mcp-v2:packed-runtime + - run: pnpm -r typecheck - run: pnpm lint diff --git a/CLAUDE.md b/CLAUDE.md index 9f195455a..f6db2ea8f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,6 +81,8 @@ atrib/ refresh-global-emit-cli.mjs # D082/D128-class updater and guard for the global atrib-emit-cli hook signer. Reinstalls @atrib/emit from the registry, fails on npm-link drift into a checkout, and gates on a signed smoke envelope. collect-npm-downloads.mjs # METRICS.md Tier 2 collector. Writes metrics/npm-downloads-.json with last-week npm downloads per designed-public package (current/deprecated from package.json descriptions) plus the PyPI atrib distribution. check-release-publish-readiness.mjs # Release preflight. Verifies every Changesets-publishable workspace package already exists on npm, while first-publish-pending packages stay in `.changeset/config.json` ignore. + check-mcp-v2-owned-surfaces.mjs # MCP v2 source and publish-archive inventory gate. Uses pnpm pack so workspace dependencies match the publish-ready manifest. + check-mcp-v2-packed-runtime.mjs # Fresh-installs every published MCP v2 surface and its atrib dependency closure from local pnpm archives, then requires modern 2026-07-28 negotiation and tools/list from each stdio binary. metrics/ # Dated JSON snapshots from `pnpm --filter @atrib/log-node metrics` and `pnpm metrics:npm-downloads` (npm + PyPI download counts per package, current/deprecated tagged) packages/ mcp/ # @atrib/mcp: MCP server middleware (public) diff --git a/package.json b/package.json index 9c791a016..745a4fd02 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,13 @@ "typos": "typos .", "clean": "turbo clean", "version-packages": "changeset version", - "release": "node scripts/check-release-publish-readiness.mjs && pnpm -r build && node scripts/check-mcp-v2-owned-surfaces.mjs --require-built --packed && changeset publish" + "mcp-v2:packed-runtime": "node scripts/check-mcp-v2-packed-runtime.mjs", + "release": "node scripts/check-release-publish-readiness.mjs && pnpm -r build && node scripts/check-mcp-v2-owned-surfaces.mjs --require-built --packed && pnpm mcp-v2:packed-runtime && changeset publish" }, "devDependencies": { "@changesets/cli": "^2.31.1", "@eslint/js": "^10.0.1", + "@modelcontextprotocol/client": "^2.0.0", "eslint": "^10.8.0", "eslint-config-prettier": "^10.1.8", "globals": "^17.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c49037464..8fefa5670 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,9 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.8.0) + '@modelcontextprotocol/client': + specifier: ^2.0.0 + version: 2.0.0 eslint: specifier: ^10.8.0 version: 10.8.0 diff --git a/scripts/check-mcp-v2-owned-surfaces.mjs b/scripts/check-mcp-v2-owned-surfaces.mjs index b3b43d435..e7f6d8ab4 100644 --- a/scripts/check-mcp-v2-owned-surfaces.mjs +++ b/scripts/check-mcp-v2-owned-surfaces.mjs @@ -136,7 +136,7 @@ function packPublishedPackages(inventory) { try { for (const [packageName, entry] of packages) { - const packed = spawnSync('npm', ['pack', '--silent', '--pack-destination', tempDir], { + const packed = spawnSync('pnpm', ['pack', '--pack-destination', tempDir], { cwd: join(ROOT, entry.workspace), encoding: 'utf8', env: process.env, @@ -147,13 +147,12 @@ function packPublishedPackages(inventory) { packed.stderr.trim() || packed.stdout.trim() || `exit status ${packed.status}` - errors.push(`${packageName}: npm pack failed: ${diagnostic}`) + errors.push(`${packageName}: pnpm pack failed: ${diagnostic}`) continue } - const archiveName = packed.stdout.trim().split('\n').at(-1) - const archivePath = join(tempDir, archiveName) - if (!archiveName || !existsSync(archivePath)) { - errors.push(`${packageName}: npm pack did not produce an archive`) + const archivePath = packed.stdout.trim().split('\n').at(-1) + if (!archivePath || !existsSync(archivePath)) { + errors.push(`${packageName}: pnpm pack did not produce an archive`) continue } diff --git a/scripts/check-mcp-v2-packed-runtime.mjs b/scripts/check-mcp-v2-packed-runtime.mjs new file mode 100644 index 000000000..7725c9abf --- /dev/null +++ b/scripts/check-mcp-v2-packed-runtime.mjs @@ -0,0 +1,282 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from 'node:child_process' +import { + mkdtempSync, + mkdirSync, + readdirSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync, +} from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, isAbsolute, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { randomBytes } from 'node:crypto' +import { Client } from '@modelcontextprotocol/client' +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio' + +const ROOT = fileURLToPath(new URL('..', import.meta.url)) +const INVENTORY_PATH = join(ROOT, 'scripts', 'mcp-v2-owned-surfaces.json') +const EXPECTED_PROTOCOL_VERSION = '2026-07-28' +const PROBE_TIMEOUT_MS = 30_000 + +function readJson(path) { + return JSON.parse(readFileSync(path, 'utf8')) +} + +function workspaceManifests() { + const manifests = new Map() + const pending = [join(ROOT, 'packages'), join(ROOT, 'services')] + + while (pending.length > 0) { + const directory = pending.pop() + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === 'dist') continue + const path = join(directory, entry.name) + if (entry.isDirectory()) { + pending.push(path) + } else if (entry.isFile() && entry.name === 'package.json') { + const manifest = readJson(path) + if (typeof manifest.name === 'string') { + manifests.set(manifest.name, { directory: dirname(path), manifest }) + } + } + } + } + + return manifests +} + +function requiredWorkspacePackages(inventory, manifests) { + const required = new Set( + inventory.surfaces.filter((surface) => surface.published).map((surface) => surface.package), + ) + const pending = [...required] + + while (pending.length > 0) { + const packageName = pending.pop() + const entry = manifests.get(packageName) + if (!entry) throw new Error(`Missing workspace manifest for ${packageName}`) + + for (const dependencyName of Object.keys(entry.manifest.dependencies ?? {})) { + if (!manifests.has(dependencyName) || required.has(dependencyName)) continue + required.add(dependencyName) + pending.push(dependencyName) + } + } + + return [...required].sort() +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + encoding: 'utf8', + env: process.env, + ...options, + }) + if (result.status !== 0) { + const diagnostic = + result.error?.message || + result.stderr.trim() || + result.stdout.trim() || + `exit status ${result.status}` + throw new Error(`${command} ${args.join(' ')} failed: ${diagnostic}`) + } + return result.stdout.trim() +} + +function packPackages(packageNames, manifests, packDirectory) { + const archives = [] + for (const packageName of packageNames) { + const output = run('pnpm', ['pack', '--pack-destination', packDirectory], { + cwd: manifests.get(packageName).directory, + }) + const archivePath = output.split('\n').at(-1) + if (!archivePath) throw new Error(`${packageName}: pnpm pack returned no archive`) + archives.push(isAbsolute(archivePath) ? archivePath : join(packDirectory, archivePath)) + } + return archives +} + +function installPackedPackages(archives, installDirectory) { + writeFileSync( + join(installDirectory, 'package.json'), + JSON.stringify({ name: 'atrib-mcp-v2-packed-runtime-proof', private: true }), + ) + run( + 'npm', + [ + 'install', + '--ignore-scripts', + '--no-audit', + '--no-fund', + '--no-package-lock', + '--save=false', + ...archives, + ], + { cwd: installDirectory }, + ) +} + +function installedEntrypoint(installDirectory, surface) { + const packageDirectory = join(installDirectory, 'node_modules', ...surface.package.split('/')) + const manifest = readJson(join(packageDirectory, 'package.json')) + const relativeEntrypoint = manifest.bin?.[surface.bin] + if (typeof relativeEntrypoint !== 'string') { + throw new Error(`${surface.id}: installed package is missing bin ${surface.bin}`) + } + return realpathSync(join(packageDirectory, relativeEntrypoint)) +} + +function writeWrapperFixture(installDirectory) { + const fixturePath = join(installDirectory, 'echo-server.mjs') + writeFileSync( + fixturePath, + `import { McpServer } from '@modelcontextprotocol/server' +import { serveStdio } from '@modelcontextprotocol/server/stdio' + +const server = new McpServer({ name: 'packed-wrapper-fixture', version: '0.0.0' }) +server.registerTool('echo', { inputSchema: {} }, async () => ({ + content: [{ type: 'text', text: 'echo' }], +})) +serveStdio(() => server) +`, + ) + return fixturePath +} + +function writeWrapperConfig(installDirectory, fixturePath) { + const configPath = join(installDirectory, 'wrap-config.json') + writeFileSync( + configPath, + JSON.stringify({ + name: 'packed-v2-proof', + agent: 'release-check', + upstream: { + command: process.execPath, + args: [fixturePath], + }, + serverUrl: 'mcp://packed-v2-proof.local', + logEndpoint: 'http://127.0.0.1:1/v1/entries', + recordFile: join(installDirectory, 'wrapper-records.jsonl'), + logFile: join(installDirectory, 'wrapper.log'), + }), + ) + return configPath +} + +async function withTimeout(promise, label) { + let timer + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} timed out`)), PROBE_TIMEOUT_MS) + }), + ]) + } finally { + clearTimeout(timer) + } +} + +async function probeSurface(surface, installDirectory, wrapperConfigPath) { + const args = [installedEntrypoint(installDirectory, surface)] + if (surface.id === 'mcp-wrap-stdio') args.push(wrapperConfigPath) + + const transport = new StdioClientTransport({ + command: process.execPath, + args, + env: { + ...process.env, + ATRIB_PRIVATE_KEY: randomBytes(32).toString('base64url'), + ATRIB_RECORD_FILE: join(installDirectory, `${surface.id}-records.jsonl`), + }, + stderr: 'pipe', + }) + let stderr = '' + transport.stderr?.on('data', (chunk) => { + stderr += String(chunk) + }) + const client = new Client( + { name: 'atrib-packed-runtime-proof', version: '0.0.0' }, + { versionNegotiation: { mode: { pin: EXPECTED_PROTOCOL_VERSION } } }, + ) + + try { + await withTimeout(client.connect(transport), `${surface.id} connect`) + const listed = await withTimeout(client.listTools(), `${surface.id} tools/list`) + if (client.getProtocolEra() !== 'modern') { + throw new Error(`${surface.id}: expected modern protocol era`) + } + if (client.getNegotiatedProtocolVersion() !== EXPECTED_PROTOCOL_VERSION) { + throw new Error( + `${surface.id}: negotiated ${client.getNegotiatedProtocolVersion() ?? 'nothing'}`, + ) + } + if (listed.tools.length === 0) { + throw new Error(`${surface.id}: tools/list returned no tools`) + } + if (surface.id === 'mcp-wrap-stdio' && !listed.tools.some((tool) => tool.name === 'echo')) { + throw new Error('mcp-wrap-stdio: tools/list did not include the upstream echo tool') + } + return { id: surface.id, package: surface.package, tools: listed.tools.length } + } catch (error) { + const diagnostic = stderr.trim() + const message = `${surface.id}: ${error instanceof Error ? error.message : String(error)}` + throw new Error(diagnostic ? `${message}\nstderr:\n${diagnostic}` : message, { + cause: error, + }) + } finally { + await client.close().catch(() => {}) + } +} + +async function main() { + const inventory = readJson(INVENTORY_PATH) + if (inventory.protocol_version !== EXPECTED_PROTOCOL_VERSION) { + throw new Error(`Inventory protocol version is ${inventory.protocol_version}`) + } + + const tempDirectory = mkdtempSync(join(tmpdir(), 'atrib-mcp-v2-packed-runtime-')) + const packDirectory = join(tempDirectory, 'packs') + const installDirectory = join(tempDirectory, 'install') + mkdirSync(packDirectory) + mkdirSync(installDirectory) + + try { + const manifests = workspaceManifests() + const packageNames = requiredWorkspacePackages(inventory, manifests) + const archives = packPackages(packageNames, manifests, packDirectory) + installPackedPackages(archives, installDirectory) + + const wrapperFixturePath = writeWrapperFixture(installDirectory) + const wrapperConfigPath = writeWrapperConfig(installDirectory, wrapperFixturePath) + const surfaces = inventory.surfaces.filter( + (surface) => surface.published && surface.transport === 'stdio', + ) + const results = [] + for (const surface of surfaces) { + results.push(await probeSurface(surface, installDirectory, wrapperConfigPath)) + } + + process.stdout.write( + `MCP v2 packed-runtime proof passed: ${results.length} stdio surfaces from ${packageNames.length} fresh package archives negotiated ${EXPECTED_PROTOCOL_VERSION} and listed tools.\n`, + ) + } finally { + if (process.env.ATRIB_MCP_V2_KEEP_TEMP === '1') { + process.stderr.write(`MCP v2 packed-runtime temp preserved at ${tempDirectory}\n`) + } else { + rmSync(tempDirectory, { recursive: true, force: true }) + } + } +} + +main().catch((error) => { + process.stderr.write( + `MCP v2 packed-runtime proof failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`, + ) + process.exit(1) +}) diff --git a/services/atrib-attest/package.json b/services/atrib-attest/package.json index 2f7b67102..e8d27ff1e 100644 --- a/services/atrib-attest/package.json +++ b/services/atrib-attest/package.json @@ -52,7 +52,7 @@ "@noble/ed25519": "^3.1.0", "@noble/hashes": "^2.2.0", "canonicalize": "^3.0.0", - "zod": "^3.25.76" + "zod": "^4.4.3" }, "devDependencies": { "@types/node": "^26.1.1", diff --git a/services/atrib-summarize/package.json b/services/atrib-summarize/package.json index 8a84e9b74..c1a468725 100644 --- a/services/atrib-summarize/package.json +++ b/services/atrib-summarize/package.json @@ -31,7 +31,7 @@ "dependencies": { "@atrib/mcp": "workspace:*", "@modelcontextprotocol/server": "^2.0.0", - "zod": "^3.25.76" + "zod": "^4.4.3" }, "devDependencies": { "@types/node": "^26.1.1",