Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project are documented here. The format is based on

### Changed

- **The MCP server runs on SDK v2 and serves the `2026-07-28` revision alongside the 2025 era.** `@modelcontextprotocol/sdk` ^1.29.0 is replaced by `@modelcontextprotocol/server` + `/core` 2.0.0 (the `latest` line; the v1 package is no longer in the tree, and 80 transitive packages the v1 line dragged in — express, hono, ajv, cors, … — go with it). The codemod rewrote the imports and `McpError`/`ErrorCode` → `ProtocolError`/`ProtocolErrorCode` (the four JSON-RPC codes the server emits, -32600/-32601/-32602/-32603, are unchanged) and `setRequestHandler(Schema, …)` → `setRequestHandler("tools/list" | "tools/call", …)`; the two-handler dispatch and the 22 tools are untouched. The stdio entry is now the SDK's `serveStdio`, which owns the era decision per connection: a client that opens with `initialize` is pinned to a 2025-era instance and served exactly as before (every revision the v1 line accepted — `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, `2024-10-07` — is still accepted and echoed; `@modelcontextprotocol/server-legacy` is SSE + OAuth and is not needed); a client that probes with `server/discover` is pinned to a `2026-07-28` instance, on which the SDK itself answers the probe (`supportedVersions: ["2026-07-28"]`, capabilities, instructions), stamps `resultType: "complete"` and `serverInfo` onto every result, and emits the caching hints — `tools/list` is declared `ttlMs: 86400000, cacheScope: "public"` because the inventory is a module constant. A 2025-era response never carries any of the 2026 vocabulary. `server.json`'s `$schema` stays at `2025-12-11`: it is the only registry schema published (`…/schemas/2026-07-28/server.schema.json` is 404 and the 2026-07-28 spec's registry docs reference `2025-12-11`). `tests/mcp-protocol-2026-07-28.test.mjs` drives the shipped binary over stdio in both eras, and `scripts/smoke-mcp.mjs` now runs 12 checks: the original nine through `initialize`, then three through the v2 client's `versionNegotiation: { mode: "auto" }` probe. One wire-visible detail changed: `ProtocolError.message` no longer carries the v1 `MCP error <code>: ` prefix (the code is in `error.code`; four tests that matched the prefix now match the message body and keep asserting the code). Not yet done: a real round trip through Claude Code, Codex and Hermes per CONTRIBUTING's surface verification — an SDK-era swap is exactly the change that step exists to catch, and it is the pre-merge requirement for this entry (#185).

- **Ten test expectations no longer assume POSIX paths.** The `windows-latest` job's first run reported ten failures; two were product defects (#393, #394) and the other eight were expectations written for `/`-separated paths: an `outputDir`'s last segment taken with `split("/")`, which does not split a backslash path; four comparisons against a raw `library.path` or `source_repo` line, where a path containing backslashes is correctly emitted as a quoted YAML scalar with those backslashes escaped; a path interpolated into a `RegExp` source, where backslashes read as escapes and `\b` becomes a word boundary; a refusal message matched against a `/`-rooted pattern; and a joined path compared against a `/`-joined literal. Each now compares a parsed value, a `basename`, a `path.join` on both sides, or a literal prefix — and one `doesNotMatch` in the same family, which a quoted backslash path would have satisfied vacuously rather than failing, became a parsed-value comparison too. Fixing those eight exposed two more of the same kind — a third raw `library.path` comparison and a second `/`-rooted `source_repo` pattern — because a test stops at its first failing assertion, so the run could only report the first one in each: the issue's list of eight was what was visible, not the whole set. A sweep for every instance of these shapes across `tests/` (raw-line comparisons, a path interpolated into a pattern, `split("/")` on a path) finds no others; what remains is writes that feed the parser, URLs, which are always `/`-separated, and patterns that already escape their input. No product code changed, and `continue-on-error` stays on the `test-windows` job until a run reports it green (#395).

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ The current parallel-sub-agent design landed in 0.2.0 and has been incrementally

The same framework is packaged as a [Model Context Protocol](https://modelcontextprotocol.io) server. The MCP path returns prompt text for the host to dispatch and never runs sub-agents itself, so the Pi-only orchestration features (sub-agents, live widget, dashboard, usage tracking) don't apply — but phase prompts and validation are byte-identical with the Pi path because both import the same `core/`. v0.9.0 also exposes experimental library tools so MCP-capable hosts can publish, list, and reindex reusable `reimplementation-spec.md` artifacts.

Implements MCP spec revision [`2025-11-25`](https://modelcontextprotocol.io/specification/2025-11-25) via `@modelcontextprotocol/sdk` ≥ 1.29.0. The negotiated `protocolVersion` reflects whatever the connecting client requests; the server accepts every revision the SDK supports (currently `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, `2024-10-07`).
Implements MCP spec revisions [`2026-07-28`](https://modelcontextprotocol.io/specification/2026-07-28) and [`2025-11-25`](https://modelcontextprotocol.io/specification/2025-11-25) via `@modelcontextprotocol/server` ≥ 2.0.0. The opening message selects the era: a client that sends `initialize` is served the 2025-era handshake and the negotiated `protocolVersion` reflects whatever it requests (the server accepts every legacy revision the SDK supports: `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, `2024-10-07`); a client that probes with `server/discover` is served the `2026-07-28` era (per-request `_meta` envelope, `resultType` on every result, `tools/list` cache hints `ttlMs`/`cacheScope`, `serverInfo` stamped on every response). Both eras come from the same process and the same tool handlers.

| Tool | Pi equivalent |
|---|---|
Expand Down
30 changes: 15 additions & 15 deletions mcp-server/engineering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

import { createHash } from "node:crypto";

import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";

import {
buildChangeBrief,
Expand Down Expand Up @@ -124,12 +124,12 @@ function asCallerError(error: unknown): unknown {
case "idempotency-conflict":
// Retrying verbatim will never succeed: the key is already bound to
// different bytes. Say so rather than looking like a blip.
return new McpError(ErrorCode.InvalidParams, `${message}; use a new request_id or resend the original payload`);
return new ProtocolError(ProtocolErrorCode.InvalidParams, `${message}; use a new request_id or resend the original payload`);
case "invalid-enum":
case "invalid-value":
case "invalid-request":
case "stale-revision":
return new McpError(ErrorCode.InvalidParams, message);
return new ProtocolError(ProtocolErrorCode.InvalidParams, message);
default:
return error;
}
Expand All @@ -148,7 +148,7 @@ function displayText(value: string): string {
}

function invalid(message: string): never {
throw new McpError(ErrorCode.InvalidParams, message);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, message);
}

/** A non-empty string argument, or a refusal naming the field. */
Expand Down Expand Up @@ -214,7 +214,7 @@ export function createChangeHandler(deps: {
invalid(`action is required; one of ${CHANGE_ACTIONS.join(", ")}`);
}
if (Object.hasOwn(REFUSED_ACTIONS, action)) {
throw new McpError(ErrorCode.InvalidParams, `${action} is not available through this surface: ${REFUSED_ACTIONS[action]}`);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `${action} is not available through this surface: ${REFUSED_ACTIONS[action]}`);
}
if (!(CHANGE_ACTIONS as readonly string[]).includes(action)) {
invalid(`unknown action ${JSON.stringify(action)}; one of ${CHANGE_ACTIONS.join(", ")}`);
Expand Down Expand Up @@ -336,8 +336,8 @@ async function updateChange(store: EngineeringStore, args: ChangeArgs, textResul
// leaves the approval validating against a change it no longer describes.
// The record would then state an outcome nobody approved.
if (TERMINAL_CHANGE_STATES.has(record.state)) {
throw new McpError(
ErrorCode.InvalidRequest,
throw new ProtocolError(
ProtocolErrorCode.InvalidRequest,
`change ${changeId} is ${record.state} and cannot be edited: an approval records agreement to a specific title and outcome, ` +
`so changing them would leave the approval describing bytes nobody approved. Open a new change instead.`,
);
Expand All @@ -349,18 +349,18 @@ async function updateChange(store: EngineeringStore, args: ChangeArgs, textResul
// second writer's work vanished with no error reported to anyone.
const revision = args.revision;
if (revision === undefined) {
throw new McpError(
ErrorCode.InvalidParams,
throw new ProtocolError(
ProtocolErrorCode.InvalidParams,
`update requires the revision you last read (change ${changeId} is at revision ${record.revision}), ` +
`so a concurrent writer's work cannot be overwritten silently`,
);
}
if (typeof revision !== "number" || !Number.isInteger(revision)) {
throw new McpError(ErrorCode.InvalidParams, `revision must be an integer, got ${JSON.stringify(revision)}`);
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `revision must be an integer, got ${JSON.stringify(revision)}`);
}
if (revision !== record.revision) {
throw new McpError(
ErrorCode.InvalidParams,
throw new ProtocolError(
ProtocolErrorCode.InvalidParams,
`stale revision ${String(revision)}: change ${changeId} is at revision ${record.revision}; re-read it and retry`,
);
}
Expand Down Expand Up @@ -415,8 +415,8 @@ async function planChange(store: EngineeringStore, args: ChangeArgs, textResult:
references: record.references,
});
if (!brief.ok || !brief.markdown) {
throw new McpError(
ErrorCode.InvalidParams,
throw new ProtocolError(
ProtocolErrorCode.InvalidParams,
`this change cannot be planned yet: ${(brief.errors ?? []).map((e) => e.message).join("; ") || "the brief could not be built"}`,
);
}
Expand Down Expand Up @@ -452,7 +452,7 @@ async function recordProof(store: EngineeringStore, args: ChangeArgs, textResult
provenance: { source: "mcp:tool-call", attested_by: "caller" },
});
if (ingested.ok === false) {
throw new McpError(ErrorCode.InvalidParams, ingested.errors.map((e) => e.message).join("; "));
throw new ProtocolError(ProtocolErrorCode.InvalidParams, ingested.errors.map((e) => e.message).join("; "));
}
return textResult(`Recorded proof ${ingested.proof.id} (${ingested.proof.result}, authority ${ingested.authority}).`, {
proof_id: ingested.proof.id,
Expand Down
Loading
Loading