diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c5d293..1ccd024 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 : ` 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 diff --git a/README.md b/README.md index 478cae7..0d0e4fc 100644 --- a/README.md +++ b/README.md @@ -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 | |---|---| diff --git a/mcp-server/engineering.ts b/mcp-server/engineering.ts index 0b19c48..c429fa6 100644 --- a/mcp-server/engineering.ts +++ b/mcp-server/engineering.ts @@ -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, @@ -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; } @@ -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. */ @@ -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(", ")}`); @@ -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.`, ); @@ -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`, ); } @@ -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"}`, ); } @@ -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, diff --git a/mcp-server/server.ts b/mcp-server/server.ts index 99a4957..80e3373 100644 --- a/mcp-server/server.ts +++ b/mcp-server/server.ts @@ -13,14 +13,8 @@ // Tools that produce phase or skill text return it inline as the tool result; // the host decides how to surface it (display, feed to the agent, etc.). -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ErrorCode, - ListToolsRequestSchema, - McpError, -} from "@modelcontextprotocol/sdk/types.js"; +import { type ListToolsResult, ProtocolError, ProtocolErrorCode, Server } from "@modelcontextprotocol/server"; +import { serveStdio } from "@modelcontextprotocol/server/stdio"; import { cp, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises"; import { basename, isAbsolute, join } from "node:path"; @@ -130,20 +124,20 @@ import { type CodecartoConfig, describeConfigProblems, loadUserConfig, resolveUs function requireOptionalPhase(phase: unknown): string | undefined { if (phase === undefined || phase === null) return undefined; if (typeof phase !== "string") { - throw new McpError(ErrorCode.InvalidParams, `phase must be a string when provided, got ${typeof phase}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `phase must be a string when provided, got ${typeof phase}`); } return phase.trim() || undefined; } async function validateCwd(cwd: unknown): Promise { if (typeof cwd !== "string" || !cwd.trim()) { - throw new McpError(ErrorCode.InvalidParams, "cwd is required"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "cwd is required"); } if (!isAbsolute(cwd)) { - throw new McpError(ErrorCode.InvalidParams, `cwd must be an absolute path, got: ${cwd}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `cwd must be an absolute path, got: ${cwd}`); } if (!(await pathExists(cwd))) { - throw new McpError(ErrorCode.InvalidParams, `cwd does not exist: ${cwd}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `cwd does not exist: ${cwd}`); } return cwd; } @@ -160,7 +154,7 @@ async function validateCwd(cwd: unknown): Promise { async function optionalCwd(cwd: unknown): Promise { if (cwd === undefined || cwd === null) return null; if (typeof cwd !== "string") { - throw new McpError(ErrorCode.InvalidParams, "cwd must be a string when given"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "cwd must be a string when given"); } if (cwd.trim() === "") return null; return validateCwd(cwd.trim()); @@ -172,11 +166,11 @@ async function requireWorkspace(cwd: string): Promise { // — so it is InvalidRequest, not the InternalError a host would retry or // report as a server bug (self-audit mech 2.8). const state = await getWorkspaceState(cwd).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); if (!state) { - throw new McpError( - ErrorCode.InvalidRequest, + throw new ProtocolError( + ProtocolErrorCode.InvalidRequest, `No CodeCartographer workspace at ${cwd}. Call codecarto_init first.`, ); } @@ -206,7 +200,7 @@ async function buildMcpPhasePrompt( return await buildPhasePrompt(state, phase, forced, { auto }); } catch (error) { if (error instanceof PhasePreflightError) { - throw new McpError(ErrorCode.InvalidRequest, error.message); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error.message); } throw error; } @@ -218,11 +212,11 @@ export async function handleInit(args: { cwd: string; pipeline?: string; force?: const cwd = await validateCwd(args.cwd); const pipelineChoice = args.pipeline ? resolvePipelineChoice(args.pipeline) : null; if (args.pipeline && !pipelineChoice) { - throw new McpError(ErrorCode.InvalidParams, `Unknown pipeline: ${args.pipeline}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown pipeline: ${args.pipeline}`); } if (!(await pathExists(packagedWorkspaceDir))) { - throw new McpError(ErrorCode.InternalError, "Packaged .codecarto assets are missing on the MCP server."); + throw new ProtocolError(ProtocolErrorCode.InternalError, "Packaged .codecarto assets are missing on the MCP server."); } const targetWorkspaceDir = join(cwd, ".codecarto"); @@ -246,8 +240,8 @@ export async function handleInit(args: { cwd: string; pipeline?: string; force?: if (targetExists && !broadsideOnly) { if (!args.force) { - throw new McpError( - ErrorCode.InvalidRequest, + throw new ProtocolError( + ProtocolErrorCode.InvalidRequest, sameWorkspace ? `The .codecarto/ at ${targetWorkspaceDir} is CodeCartographer's own packaged template (a checkout install), and it holds workspace state. Pass force: true to move that state — status, findings, handoffs, usage data, closeouts, dashboard — to a .codecarto-backup-TIMESTAMP/ directory and reinitialize; the framework files stay in place. Consider codecarto_open to reattach without resetting.` : `A .codecarto/ directory already exists at ${targetWorkspaceDir}. Pass force: true to back it up and reinitialize. Warning: this moves all existing findings, handoffs, usage data, closeouts, and phase progress to a .codecarto-backup-TIMESTAMP/ directory.`, @@ -281,7 +275,7 @@ export async function handleInit(args: { cwd: string; pipeline?: string; force?: const resolvedPipelinePath = join(targetWorkspaceDir, selectedPipelinePath); if (!(await pathExists(resolvedPipelinePath))) { - throw new McpError(ErrorCode.InvalidParams, `Pipeline file not found: ${selectedPipelinePath}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Pipeline file not found: ${selectedPipelinePath}`); } const pipeline = await loadYamlFile(resolvedPipelinePath); @@ -377,7 +371,7 @@ export async function handleSwitchPipeline(args: { cwd: string; pipeline: string const pipelineChoice = resolvePipelineChoice(args.pipeline); if (!pipelineChoice) { - throw new McpError(ErrorCode.InvalidRequest, `Unknown pipeline: ${args.pipeline}`); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown pipeline: ${args.pipeline}`); } if (state.status.pipeline === pipelineChoice) { @@ -417,7 +411,7 @@ export async function handleNext(args: { cwd: string; unattended?: boolean }) { // Not a result: a host looping on codecarto_next would read a text // answer as "done" (#228). There is no prompt to hand out until the // pipeline file is fixed. - throw new McpError(ErrorCode.InvalidRequest, describeStuckPipeline(outcome.blocked)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, describeStuckPipeline(outcome.blocked)); } if (outcome.kind === "complete") { return textResult("All CodeCartographer phases are complete. Run codecarto_skill for post-pipeline work.", { @@ -434,13 +428,13 @@ export async function handleNext(args: { cwd: string; unattended?: boolean }) { export async function handlePhase(args: { cwd: string; phase: string; unattended?: boolean }) { if (typeof args.phase !== "string" || !args.phase.trim()) { - throw new McpError(ErrorCode.InvalidParams, "phase is required"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "phase is required"); } const cwd = await validateCwd(args.cwd); const state = await requireWorkspace(cwd); const phase = resolvePhase(state, args.phase); if (!phase) { - throw new McpError(ErrorCode.InvalidParams, `Unknown phase: ${args.phase}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown phase: ${args.phase}`); } const unattended = args.unattended === true; const prompt = await buildMcpPhasePrompt(state, phase, true, unattended); @@ -451,7 +445,7 @@ export async function handleValidate(args: { cwd: string; phase?: string }) { const cwd = await validateCwd(args.cwd); const state = await requireWorkspace(cwd); const validation = await validatePhaseOutput(state, requireOptionalPhase(args.phase)).catch((error) => { - throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); }); const summary = buildValidationSummary(validation).join("\n"); return textResult(summary, { @@ -471,17 +465,17 @@ export async function handleComplete(args: { cwd: string; phase?: string }) { const cwd = await validateCwd(args.cwd); const initialState = await requireWorkspace(cwd); const validation = await validatePhaseOutput(initialState, requireOptionalPhase(args.phase)).catch((error) => { - throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); }); if (validation.overall === "FAIL" || validation.overall === "MISSING") { - throw new McpError( - ErrorCode.InvalidRequest, + throw new ProtocolError( + ProtocolErrorCode.InvalidRequest, `Cannot complete ${validation.phaseId}: validation is ${validation.overall}.\n${buildValidationSummary(validation).join("\n")}`, ); } const { updatedState, closeoutNotice, orchestratorCheckpoint, warnings } = await completeValidatedPhase(cwd, validation, "codecarto_complete").catch((error) => { - throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); }); // Record the run in the usage log (issue #100). MCP hosts execute phases in @@ -536,7 +530,7 @@ export async function handleComplete(args: { cwd: string; phase?: string }) { export async function handleSkill(args: { cwd: string; name: string }) { if (typeof args.name !== "string" || !args.name.trim()) { - throw new McpError(ErrorCode.InvalidParams, "name is required"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "name is required"); } const cwd = await validateCwd(args.cwd); @@ -545,7 +539,7 @@ export async function handleSkill(args: { cwd: string; name: string }) { // repository with no workspace at all, so it is served ahead of both gates. if (args.name.trim() === BROADSIDE_SKILL_NAME) { const skill = await readBroadsideSkill(cwd).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); return textResult(skill.content, { skill: BROADSIDE_SKILL_NAME, path: skill.path, postPipeline: false }); } @@ -563,7 +557,7 @@ export async function handleSkill(args: { cwd: string; name: string }) { const state = await requireWorkspace(cwd); const skillName = await resolveSkillName(state.workspaceDir, ENGINEERING_SKILL_NAME); if (!skillName) { - throw new McpError(ErrorCode.InvalidRequest, `Unknown skill: ${ENGINEERING_SKILL_NAME}.`); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Unknown skill: ${ENGINEERING_SKILL_NAME}.`); } const prompt = await buildSkillPrompt(state, skillName, { postPipeline: false }); return textResult(prompt, { skill: skillName, postPipeline: false }); @@ -572,13 +566,13 @@ export async function handleSkill(args: { cwd: string; name: string }) { const state = await requireWorkspace(cwd); const outcome = resolvePipelineOutcome(state); if (outcome.kind === "eligible") { - throw new McpError( - ErrorCode.InvalidRequest, + throw new ProtocolError( + ProtocolErrorCode.InvalidRequest, `Cannot run skill: pipeline is not complete (next phase: ${outcome.phase.id}). Finish the pipeline first.`, ); } if (outcome.kind === "stuck") { - throw new McpError(ErrorCode.InvalidRequest, `Cannot run skill: the pipeline is not complete. ${describeStuckPipeline(outcome.blocked)}`); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, `Cannot run skill: the pipeline is not complete. ${describeStuckPipeline(outcome.blocked)}`); } // Resolve against the installed list only: the name is never joined onto a // path, so a traversal like `../findings/architecture` cannot splice a @@ -587,8 +581,8 @@ export async function handleSkill(args: { cwd: string; name: string }) { if (!skillName) { const available = await listSkillNames(state.workspaceDir); const hint = available.length > 0 ? ` Available: ${available.join(", ")}.` : " No skills installed."; - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, `Unknown skill: ${args.name.trim()}.${hint} The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`, ); } @@ -604,13 +598,13 @@ function resolveLibraryPath(args: { library_path?: unknown }, config: CodecartoC : null; if (explicit) { if (!isAbsolute(explicit)) { - throw new McpError(ErrorCode.InvalidParams, `library_path must be absolute, got: ${explicit}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `library_path must be absolute, got: ${explicit}`); } return explicit; } if (config.library.path) return config.library.path; - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, "library_path is required (pass it explicitly, or pass cwd and configure library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml).", ); } @@ -636,20 +630,20 @@ async function loadEffectiveConfig(cwd: string | null): Promise */ function refuseOnConfigProblems(config: CodecartoConfig, tool: string): void { if (config.problems.length === 0) return; - throw new McpError( - ErrorCode.InvalidRequest, + throw new ProtocolError( + ProtocolErrorCode.InvalidRequest, [`${tool} refused: the configuration has problems. Fix or remove the offending file, then retry.`, ...describeConfigProblems(config)].join("\n"), ); } function asStringArray(value: unknown, fieldName: string): string[] { if (!Array.isArray(value)) { - throw new McpError(ErrorCode.InvalidParams, `${fieldName} must be an array of strings`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `${fieldName} must be an array of strings`); } const out: string[] = []; for (const v of value) { if (typeof v !== "string") { - throw new McpError(ErrorCode.InvalidParams, `${fieldName} must contain only strings`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `${fieldName} must contain only strings`); } out.push(v); } @@ -671,7 +665,7 @@ function buildGenerationFromArg(model_metadata: unknown): EntryGeneration { }; if (model_metadata === undefined || model_metadata === null) return defaults; if (typeof model_metadata !== "object") { - throw new McpError(ErrorCode.InvalidParams, "model_metadata must be an object"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "model_metadata must be an object"); } const m = model_metadata as Record; const out = { ...defaults }; @@ -699,16 +693,16 @@ export async function readSpecArg( if (typeof args.spec === "string" && args.spec.length > 0) return args.spec; if (typeof args.spec_path === "string" && args.spec_path.length > 0) { if (!isAbsolute(args.spec_path)) { - throw new McpError(ErrorCode.InvalidParams, `spec_path must be absolute, got: ${args.spec_path}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `spec_path must be absolute, got: ${args.spec_path}`); } if (!Array.isArray(allowedRoots) || allowedRoots.length === 0) { - throw new McpError( - ErrorCode.InternalError, + throw new ProtocolError( + ProtocolErrorCode.InternalError, "refusing to read spec_path without a containment root — this is a caller bug, not a client error", ); } if (!(await pathExists(args.spec_path))) { - throw new McpError(ErrorCode.InvalidParams, `spec_path does not exist: ${args.spec_path}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `spec_path does not exist: ${args.spec_path}`); } // Enforce path containment: spec_path must be within an allowed root // (cwd's .codecarto/ or the configured library path) to prevent @@ -718,8 +712,8 @@ export async function readSpecArg( allowedRoots.map((root) => isWithinPathResolved(resolvedSpecPath, root)), ); if (!withinAllowed.some((result) => result)) { - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, `spec_path must be within the workspace (.codecarto/) or the configured library path. Got: ${args.spec_path}`, ); } @@ -728,7 +722,7 @@ export async function readSpecArg( // the read; the canonical one cannot (#362). return readFile(resolvedSpecPath, "utf8"); } - throw new McpError(ErrorCode.InvalidParams, "Either spec (inline content) or spec_path (absolute file path) is required"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "Either spec (inline content) or spec_path (absolute file path) is required"); } async function resolveDefaultsFromWorkspace( @@ -806,8 +800,8 @@ export async function handlePublish(args: Record) { const libraryPath = resolveLibraryPath(args, config); const marker = await discoverLibrary(libraryPath); if (!marker) { - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker). Create one before publishing.`, ); } @@ -818,10 +812,10 @@ export async function handlePublish(args: Record) { const spec = await readSpecArg(args, allowedRoots); if (typeof args.source_repo !== "string" || args.source_repo.trim() === "") { - throw new McpError(ErrorCode.InvalidParams, "source_repo is required"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "source_repo is required"); } if (typeof args.headline !== "string" || args.headline.trim() === "") { - throw new McpError(ErrorCode.InvalidParams, "headline is required"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "headline is required"); } const tags = asStringArray(args.tags ?? [], "tags"); const capabilities = asStringArray(args.capabilities ?? [], "capabilities"); @@ -830,8 +824,8 @@ export async function handlePublish(args: Record) { const slugInput = typeof args.slug === "string" && args.slug.trim() !== "" ? args.slug.trim() : null; const slug = slugInput ?? deriveSlug(sourceRepo); if (!isValidSlug(slug)) { - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, `Resolved slug "${slug}" is invalid. Provide an explicit slug (lowercase ASCII, starts with a letter, max 64 chars).`, ); } @@ -848,8 +842,8 @@ export async function handlePublish(args: Record) { : undefined; if (marker.namespaced && !namespace) { - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, "Library is namespaced — namespace argument is required (or set library.namespace in config.yaml).", ); } @@ -885,8 +879,8 @@ export async function handlePublish(args: Record) { const specSource = typeof args.spec_path === "string" && args.spec_path.trim() !== "" ? args.spec_path.trim() : `inline (${spec.length} characters)`; - throw new McpError( - ErrorCode.InvalidRequest, + throw new ProtocolError( + ProtocolErrorCode.InvalidRequest, [ "Publish not performed: library.publish_confirm is set and this call did not carry confirm: true. Nothing was written.", `Would publish ${label} to ${libraryPath}`, @@ -959,8 +953,8 @@ export async function handleLibraryList(args: Record) { const libraryPath = resolveLibraryPath(args, config); const marker = await discoverLibrary(libraryPath); if (!marker) { - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker).`, ); } @@ -1011,8 +1005,8 @@ export async function handleLibraryReindex(args: Record) { const libraryPath = resolveLibraryPath(args, config); const marker = await discoverLibrary(libraryPath); if (!marker) { - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker).`, ); } @@ -1035,13 +1029,13 @@ export async function handleLibraryReindex(args: Record) { export async function handleLibraryInit(args: { library_path: string; name?: string; namespace?: string }) { if (!args.library_path || typeof args.library_path !== "string") { - throw new McpError(ErrorCode.InvalidParams, "library_path is required."); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "library_path is required."); } // Same rule as resolveLibraryPath for the other library tools: a relative // path would resolve against the MCP server process's cwd and then be // persisted verbatim into the user-global config (#134). if (!isAbsolute(args.library_path)) { - throw new McpError(ErrorCode.InvalidParams, `library_path must be absolute, got: ${args.library_path}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `library_path must be absolute, got: ${args.library_path}`); } const libraryPath = args.library_path; @@ -1049,8 +1043,8 @@ export async function handleLibraryInit(args: { library_path: string; name?: str // The rule codecarto_publish applies to the namespace later, applied // before it is written into the config. if (namespaced && !isValidSlug(args.namespace!)) { - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, `Invalid namespace "${args.namespace}" (lowercase ASCII, starts with a letter, max 64 chars).`, ); } @@ -1067,7 +1061,7 @@ export async function handleLibraryInit(args: { library_path: string; name?: str // does not switch the codecarto_publish confirm gate on (#244). A config // file that cannot be parsed is left alone and reported. await writeLibraryConfig(configPath, libraryPath, args.namespace ?? null).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); const written = args.namespace ? "library.path and library.namespace" : "library.path"; @@ -1091,14 +1085,14 @@ export async function handleVision(args: { cwd: string; raw_text: string }) { // synthesize a vision brief from. Every sibling handler validates its // required string argument; this one did not. if (typeof args.raw_text !== "string" || !args.raw_text.trim()) { - throw new McpError(ErrorCode.InvalidParams, "raw_text is required (the user's raw product description)"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "raw_text is required (the user's raw product description)"); } const workspaceDir = join(cwd, ".codecarto"); const interviewPath = join(workspaceDir, "findings", "vision-capture", "INTERVIEW.md"); const visionPath = join(workspaceDir, "inputs", "vision.md"); if (!(await pathExists(interviewPath))) { - throw new McpError(ErrorCode.InvalidRequest, "Vision interview skill not found. Run codecarto_init with the synthesis pipeline first."); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, "Vision interview skill not found. Run codecarto_init with the synthesis pipeline first."); } const interviewSkill = await readFile(interviewPath, "utf8"); @@ -1168,7 +1162,7 @@ export async function handleOpen(args: { cwd: string }) { const cwd = await validateCwd(args.cwd); const workspaceDir = join(cwd, ".codecarto"); if (!(await pathExists(join(workspaceDir, "workflow", "status.yaml")))) { - throw new McpError(ErrorCode.InvalidRequest, "No existing CodeCartographer workspace found. Run codecarto_init first."); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, "No existing CodeCartographer workspace found. Run codecarto_init first."); } const state = await requireWorkspace(cwd); const outcome = resolvePipelineOutcome(state); @@ -1218,7 +1212,7 @@ export async function handleDashboard(args: { cwd: string }) { const cwd = await validateCwd(args.cwd); await requireWorkspace(cwd); if (!(await writeDashboard(cwd, PACKAGE_VERSION))) { - throw new McpError(ErrorCode.InvalidRequest, "Dashboard render failed: the workspace state could not be gathered or .codecarto/dashboard.html is not writable."); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, "Dashboard render failed: the workspace state could not be gathered or .codecarto/dashboard.html is not writable."); } return textResult("Dashboard regenerated: .codecarto/dashboard.html", { path: ".codecarto/dashboard.html" }); } @@ -1247,7 +1241,7 @@ export async function handleRefreshScaffold(args: { cwd: string }) { const cwd = await validateCwd(args.cwd); await requireWorkspace(cwd); const result = await refreshScaffold(cwd).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); const shown = result.written.slice(0, 20); const lines = [ @@ -1267,12 +1261,12 @@ export async function handleRefreshScaffold(args: { cwd: string }) { export async function handleAmend(args: { cwd: string; name: string }) { if (typeof args.name !== "string" || !args.name.trim()) { - throw new McpError(ErrorCode.InvalidParams, "name is required (the amendment file's slug under .codecarto/scratch/amendments/)"); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "name is required (the amendment file's slug under .codecarto/scratch/amendments/)"); } const cwd = await validateCwd(args.cwd); await requireWorkspace(cwd); const { applied, closeoutNotice } = await applyAmendment(cwd, args.name).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); // An amendment exists precisely to change the numbers the dashboard shows @@ -1304,8 +1298,8 @@ function resolveBroadsideApiKey(explicit: string | undefined, config: { apiKey: const fromEnv = process.env.OPENROUTER_API_KEY?.trim(); if (fromEnv) return fromEnv; if (config.apiKey) return config.apiKey; - throw new McpError( - ErrorCode.InvalidParams, + throw new ProtocolError( + ProtocolErrorCode.InvalidParams, "No OpenRouter API key found. Pass api_key, set the OPENROUTER_API_KEY environment variable, or add api_key to .codecarto/broadside/config.yaml.", ); } @@ -1332,7 +1326,7 @@ export async function handleBroadside(args: { const cwd = await validateCwd(args.cwd); const action = args.action ?? "submit"; if (!["submit", "collect", "status", "models", "verify"].includes(action)) { - throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}. Valid actions: submit, collect, status, models, verify.`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown action: ${action}. Valid actions: submit, collect, status, models, verify.`); } // A config.yaml that exists but cannot be read refuses every action that @@ -1345,7 +1339,7 @@ export async function handleBroadside(args: { config = await loadBroadsideConfig(broadsideDirFor(cwd)); } catch (error) { if (!(error instanceof BroadsideConfigError) || action !== "status") { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); } config = defaultBroadsideConfig(); configWarning = error.message; @@ -1353,7 +1347,7 @@ export async function handleBroadside(args: { if (action === "status") { const { state } = await runBroadsideStatus(cwd).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); const lines = [statusText(state)]; if (configWarning) lines.push(`Warning: ${configWarning}`); @@ -1378,7 +1372,7 @@ export async function handleBroadside(args: { const { entries, benchmarks, endpoints } = await listBatchModels(broadsideDirFor(cwd), config, apiKey, { includeBenchmarks: args.include_benchmarks === true, }).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); return textResult(modelsText(entries, { benchmarks, defaultModel: config.model, endpoints }), { models: entries, @@ -1396,7 +1390,7 @@ export async function handleBroadside(args: { if (args.lenses && args.lenses.length > 0) { const unknown = args.lenses.filter((l) => !BROADSIDE_LENS_IDS.includes(l as BroadsideLensId)); if (unknown.length > 0) { - throw new McpError(ErrorCode.InvalidParams, `Unknown lens(es): ${unknown.join(", ")}. Valid: ${BROADSIDE_LENS_IDS.join(", ")}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown lens(es): ${unknown.join(", ")}. Valid: ${BROADSIDE_LENS_IDS.join(", ")}`); } lenses = args.lenses as BroadsideLensId[]; } else { @@ -1414,19 +1408,19 @@ export async function handleBroadside(args: { // model's ceiling — so a wrong id fails before anything is submitted. const model = typeof args.model === "string" && args.model.trim() ? args.model.trim() : config.model; if (args.model !== undefined && !(typeof args.model === "string" && args.model.trim())) { - throw new McpError(ErrorCode.InvalidParams, "model must be a non-empty OpenRouter batch model id (see action 'models')."); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "model must be a non-empty OpenRouter batch model id (see action 'models')."); } const lensModels: Partial> = {}; if (args.lens_models !== undefined) { if (!args.lens_models || typeof args.lens_models !== "object" || Array.isArray(args.lens_models)) { - throw new McpError(ErrorCode.InvalidParams, "lens_models must be an object mapping lens ids to batch model ids."); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "lens_models must be an object mapping lens ids to batch model ids."); } for (const [lensId, value] of Object.entries(args.lens_models)) { if (!BROADSIDE_LENS_IDS.includes(lensId as BroadsideLensId)) { - throw new McpError(ErrorCode.InvalidParams, `lens_models: unknown lens "${lensId}". Valid: ${BROADSIDE_LENS_IDS.join(", ")}`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `lens_models: unknown lens "${lensId}". Valid: ${BROADSIDE_LENS_IDS.join(", ")}`); } if (typeof value !== "string" || !value.trim()) { - throw new McpError(ErrorCode.InvalidParams, `lens_models.${lensId} must be a non-empty OpenRouter batch model id.`); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `lens_models.${lensId} must be a non-empty OpenRouter batch model id.`); } lensModels[lensId as BroadsideLensId] = value.trim(); } @@ -1440,7 +1434,7 @@ export async function handleBroadside(args: { force: args.force === true, incremental, }).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); const lines = [estimateSubmitText(result, lenses.map(getLens))]; @@ -1460,7 +1454,7 @@ export async function handleBroadside(args: { // The `collect` action normalizes this same call; without it here, // a failure during submit-with-wait reached the client as an // opaque InternalError instead of naming its cause. - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); lines.push("", collectResultText(collect)); } @@ -1482,10 +1476,10 @@ export async function handleBroadside(args: { // call's cost is known only when it returns — so the pass stops before // the next finding once reached; absent, config.yaml's cap applies. if (args.top !== undefined && !(typeof args.top === "number" && Number.isInteger(args.top) && args.top >= 1)) { - throw new McpError(ErrorCode.InvalidParams, "top must be a positive integer."); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "top must be a positive integer."); } if (args.model !== undefined && !(typeof args.model === "string" && args.model.trim())) { - throw new McpError(ErrorCode.InvalidParams, "model must be a non-empty OpenRouter model id."); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "model must be a non-empty OpenRouter model id."); } const maxCost = typeof args.max_cost === "number" && args.max_cost >= 0 ? args.max_cost : config.maxCost; const verified = await runBroadsideVerify(cwd, apiKey, { @@ -1495,7 +1489,7 @@ export async function handleBroadside(args: { maxCost, signal: serverLifetime?.signal, }).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); return textResult(verifyResultText(verified), { runId: verified.runId, @@ -1512,10 +1506,10 @@ export async function handleBroadside(args: { // action === "collect" if (args.regenerate_post_passes !== undefined && typeof args.regenerate_post_passes !== "boolean") { - throw new McpError(ErrorCode.InvalidParams, "regenerate_post_passes must be a boolean."); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "regenerate_post_passes must be a boolean."); } if (args.regenerate_post_passes && !includeSynthesis && !includeTriage) { - throw new McpError(ErrorCode.InvalidParams, "regenerate_post_passes needs at least one of include_synthesis and include_triage."); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, "regenerate_post_passes needs at least one of include_synthesis and include_triage."); } const collect = await runBroadsideCollect(cwd, apiKey, { waitMs, @@ -1526,7 +1520,7 @@ export async function handleBroadside(args: { ...(runId && { runId }), ...(args.regenerate_post_passes && { regeneratePostPasses: true }), }).catch((error) => { - throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error)); }); return textResult(collectResultText(collect), { runId: collect.runId, @@ -1991,7 +1985,7 @@ const HANDLERS: Record Promise> = { export async function handleGuide(args: { topic?: string }) { const topics = await listGuideTopics(); const document: GuideDocument = await readGuide(args.topic).catch((error) => { - throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); + throw new ProtocolError(ProtocolErrorCode.InvalidParams, error instanceof Error ? error.message : String(error)); }); const other = topics.filter((name) => name !== document.topic); const footer = other.length > 0 @@ -2002,30 +1996,44 @@ export async function handleGuide(args: { topic?: string }) { // ---------- server bootstrap ---------- +// The inventory is fixed for the life of the process (TOOLS and +// ENGINEERING_TOOLS are module constants), so a 2026-07-28 client may cache +// `tools/list` for a long time and share it across sessions. `server/discover` +// is left at the SDK default (ttlMs 0, private) because its answer is what a +// client should re-probe on every connection. Hints are emitted only on +// 2026-07-28-era responses; a 2025-era response never carries them. +const TOOLS_LIST_CACHE_HINT = { ttlMs: 24 * 60 * 60 * 1000, cacheScope: "public" as const }; + export function buildServer() { const server = new Server( { name: "codecartographer", version: PACKAGE_VERSION }, - { capabilities: { tools: {} } }, + { + capabilities: { tools: {} }, + cacheHints: { "tools/list": TOOLS_LIST_CACHE_HINT }, + }, ); // The engineering surface is appended rather than interleaved: it is // experimental, and a host diffing the inventory should see exactly one // addition at the end rather than a reshuffle of the analysis tools. - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools: [...TOOLS, ...ENGINEERING_TOOLS] as unknown as typeof TOOLS[number][], + // TOOLS is `as const` (readonly tuples); the SDK's ListToolsResult wants + // mutable arrays. The cast crosses that gap only — the objects are + // identical on the wire. + server.setRequestHandler("tools/list", async () => ({ + tools: [...TOOLS, ...ENGINEERING_TOOLS] as unknown as ListToolsResult["tools"], })); - server.setRequestHandler(CallToolRequestSchema, async (request) => { + server.setRequestHandler("tools/call", async (request) => { const handler = HANDLERS[request.params.name]; if (!handler) { - throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); + throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } try { return (await handler(request.params.arguments ?? {})) as Awaited>; } catch (error) { - if (error instanceof McpError) throw error; - throw new McpError( - ErrorCode.InternalError, + if (error instanceof ProtocolError) throw error; + throw new ProtocolError( + ProtocolErrorCode.InternalError, error instanceof Error ? error.message : String(error), ); } @@ -2044,11 +2052,23 @@ export function buildServer() { let serverLifetime: AbortController | null = null; export async function startStdioServer() { - const server = buildServer(); - const transport = new StdioServerTransport(); serverLifetime = new AbortController(); const lifetime = serverLifetime; - server.onclose = () => lifetime.abort(); + // `serveStdio` owns the era decision for the connection: a 2025-era opening + // (`initialize`) pins a legacy-era instance and is served exactly as the SDK + // v1 line served it; a 2026-07-28 opening (an envelope-bearing + // `server/discover` or request) pins a modern-era instance, on which the + // SDK itself answers `server/discover` and stamps `resultType`, the caching + // hints and `serverInfo` onto every result. The factory is called once per + // connection, so one process still serves one client. + serveStdio( + () => { + const server = buildServer(); + server.onclose = () => lifetime.abort(); + return server; + }, + { onerror: (error) => console.error(`codecarto-mcp: ${error.message}`) }, + ); // The SDK's stdio transport listens for stdin `data` and `error` only — it // never sees the end of the stream — so a client that exits mid-request // leaves the server polling with nobody to answer to (#322, observed: a @@ -2057,5 +2077,4 @@ export async function startStdioServer() { const gone = () => lifetime.abort(); process.stdin.once("end", gone); process.stdin.once("close", gone); - await server.connect(transport); } diff --git a/package-lock.json b/package-lock.json index 17600e5..7707b5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,14 @@ "version": "0.26.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0" }, "bin": { "codecarto-mcp": "dist/mcp-server/bin.mjs" }, "devDependencies": { + "@modelcontextprotocol/client": "^2.0.0", "typescript": "^5.9.3" }, "engines": { @@ -1101,18 +1103,6 @@ } } }, - "node_modules/@hono/node-server": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", - "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "hono": "^4" - } - }, "node_modules/@mariozechner/clipboard": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", @@ -1318,44 +1308,48 @@ "node": ">= 10" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", + "dev": true, "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", + "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" + "zod": "^4.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", + "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "zod": "^4.2.0" }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } + "engines": { + "node": ">=20" } }, "node_modules/@protobufjs/aspromise": { @@ -1590,19 +1584,6 @@ "license": "MIT", "peer": true }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1613,39 +1594,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "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/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1687,43 +1635,6 @@ "node": "*" } }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", @@ -1751,44 +1662,6 @@ "license": "BSD-3-Clause", "peer": true }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1802,63 +1675,6 @@ "url": "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/chalk/chalk?sponsor=1" } }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1888,6 +1704,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1900,15 +1717,6 @@ } } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/diff": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", @@ -1919,20 +1727,6 @@ "node": ">=0.3.1" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1943,51 +1737,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -2030,25 +1779,11 @@ "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/eventsource": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -2058,76 +1793,15 @@ } }, "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz", - "integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "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/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2135,12 +1809,6 @@ "license": "MIT", "peer": true }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, "node_modules/fast-sha256": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", @@ -2148,22 +1816,6 @@ "license": "Unlicense", "peer": true }, - "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", - "funding": [ - { - "type": "github", - "url": "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/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -2188,27 +1840,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -2222,33 +1853,6 @@ "node": ">=12.20.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "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/sponsors/ljharb" - } - }, "node_modules/gaxios": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", @@ -2292,43 +1896,6 @@ "url": "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/sponsors/sindresorhus" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/google-auth-library": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", @@ -2357,18 +1924,6 @@ "node": ">=14" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2386,30 +1941,6 @@ "node": ">=18" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -2420,15 +1951,6 @@ "node": "*" } }, - "node_modules/hono": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", - "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, "node_modules/hosted-git-info": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", @@ -2442,26 +1964,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -2490,22 +1992,6 @@ "node": ">= 14" } }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -2516,36 +2002,6 @@ "node": ">= 4" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2563,9 +2019,10 @@ } }, "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "dev": true, "license": "MIT", "funding": { "url": "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/sponsors/panva" @@ -2595,18 +2052,6 @@ "node": ">=16" } }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2660,61 +2105,6 @@ "node": ">= 20" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "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/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2735,16 +2125,8 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", - "engines": { - "node": ">= 0.6" - } + "peer": true }, "node_modules/node-domexception": { "version": "1.0.0", @@ -2786,48 +2168,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/openai": { "version": "6.40.0", "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", @@ -2861,15 +2201,6 @@ "node": ">=8" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", @@ -2886,20 +2217,11 @@ "node": ">=8" } }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -2951,72 +2273,6 @@ "node": ">=12.0.0" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -3027,22 +2283,6 @@ "node": ">= 4" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3064,12 +2304,6 @@ "license": "MIT", "peer": true }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", @@ -3083,57 +2317,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3155,78 +2338,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3245,24 +2356,6 @@ "fast-sha256": "^1.3.0" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -3277,37 +2370,6 @@ "license": "0BSD", "peer": true }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/typebox": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", @@ -3346,24 +2408,6 @@ "license": "MIT", "peer": true }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -3389,12 +2433,6 @@ "node": ">= 8" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, "node_modules/ws": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", @@ -3441,15 +2479,6 @@ "funding": { "url": "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/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } } } } diff --git a/package.json b/package.json index 5521352..ef5a6ee 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,8 @@ "demo:synthesis": "npm run build && node scripts/create-synthesis-demo.mjs" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0" }, "peerDependencies": { "@earendil-works/pi-coding-agent": ">=0.84.0", @@ -74,6 +75,7 @@ ] }, "devDependencies": { + "@modelcontextprotocol/client": "^2.0.0", "typescript": "^5.9.3" }, "overrides": { diff --git a/scripts/smoke-mcp.mjs b/scripts/smoke-mcp.mjs index 160844d..6c4238d 100644 --- a/scripts/smoke-mcp.mjs +++ b/scripts/smoke-mcp.mjs @@ -1,11 +1,13 @@ #!/usr/bin/env node // End-to-end smoke test for the published codecartographer-pi MCP server. // Installs the package from npm into a temp dir, drives the bin via the -// MCP SDK's stdio client, and runs nine TAP-style assertions covering the -// happy path and key negative cases. +// MCP SDK's stdio client, and runs TAP-style assertions covering the happy +// path and key negative cases — first through the 2025-era `initialize` +// handshake (what every shipping host sends today), then through the +// 2026-07-28 `server/discover` negotiation (#185). -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import assert from "node:assert/strict"; import { execFile as execFileCb } from "node:child_process"; import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; @@ -224,7 +226,46 @@ async function main() { }); await client.close(); - console.log("1..9"); + + // The same binary, opened in the 2026-07-28 era: the client probes with + // `server/discover` and, on a modern verdict, every request carries the + // per-request `_meta` envelope. `mode: 'auto'` is what a negotiating host + // does; a modern verdict here proves the server answered the probe. + const modernTransport = new StdioClientTransport({ command: binPath, args: [], stderr: "pipe" }); + modernTransport.stderr?.on("data", (c) => stderrChunks.push(c)); + const modern = new Client( + { name: "codecarto-smoke-2026", version: "0.0.0" }, + { capabilities: {}, versionNegotiation: { mode: "auto" } }, + ); + + await step("2026-07-28: server/discover negotiation reaches the modern era", async () => { + await Promise.race([ + modern.connect(modernTransport), + new Promise((_, rej) => setTimeout(() => rej(new Error("connect timeout 10s")), 10_000)), + ]); + assert.equal(modern.getServerVersion()?.name, "codecartographer"); + assert.deepEqual(Object.keys(modern.getServerCapabilities() ?? {}), ["tools"]); + }); + + await step("2026-07-28: tools/list carries the same inventory and the caching hints", async () => { + const listed = await modern.listTools(); + assert.deepEqual(listed.tools.map((t) => t.name).sort(), EXPECTED_TOOLS); + assert.equal(listed.cacheScope, "public", "the static inventory is shareable"); + assert.ok(listed.ttlMs >= 60_000, `expected a long ttlMs, got ${listed.ttlMs}`); + }); + + await step("2026-07-28: tools/call keeps both content halves and standard error codes", async () => { + const result = await modern.callTool({ name: "codecarto_status", arguments: { cwd: target } }); + assert.equal(result.structuredContent?.currentPhase, "architecture"); + assert.ok((result.content?.[0]?.text ?? "").length > 0, "text content present"); + await expectReject(modern.callTool({ name: "codecarto_status", arguments: {} }), { + code: -32602, + message: /cwd is required/, + }); + }); + + await modern.close(); + console.log("1..12"); } try { diff --git a/tests/broadside-guardrails.test.mjs b/tests/broadside-guardrails.test.mjs index d7cb9c2..f2f5dad 100644 --- a/tests/broadside-guardrails.test.mjs +++ b/tests/broadside-guardrails.test.mjs @@ -18,7 +18,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const core = await import(pathToFileURL(`${REPO_ROOT}/core/broadside.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { default: codeCartographerExtension } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/index.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const { BROADSIDE_DEFAULT_MAX_COST, BroadsideConfigError, BroadsideStateError, broadsideDirFor, defaultBroadsideConfig, loadBroadsideConfig, loadBroadsideState, persistBroadsideRun, runBroadsideCollect, runBroadsideSubmit, saveBroadsideState } = core; process.env.OPENROUTER_API_KEY = "sk-fake"; @@ -173,8 +173,8 @@ test("a config.yaml that cannot be parsed refuses submit, collect, and models on for (const action of ["submit", "collect", "models"]) { await assert.rejects(server.handleBroadside({ cwd: dir, action, api_key: "sk-fake" }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /Broad-Side config .*could not be parsed/); return true; }, action); @@ -215,7 +215,7 @@ test("a corrupt state.json refuses submit and collect, is preserved, and is neve await writeFile(statePath, corrupt, "utf8"); const refused = (error) => { - assert.ok(error instanceof BroadsideStateError || error instanceof McpError, String(error)); + assert.ok(error instanceof BroadsideStateError || error instanceof ProtocolError, String(error)); assert.match(error.message, /Broad-Side state .*state\.json could not be parsed .*A copy is preserved at .*state\.json\.corrupt-[0-9a-f]{8}; the file is not overwritten\. Repair state\.json from the copy/); return true; }; diff --git a/tests/broadside-model-selection.test.mjs b/tests/broadside-model-selection.test.mjs index cd5a6d3..6df7b80 100644 --- a/tests/broadside-model-selection.test.mjs +++ b/tests/broadside-model-selection.test.mjs @@ -25,7 +25,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const core = await import(pathToFileURL(`${REPO_ROOT}/core/broadside.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { default: codeCartographerExtension } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/index.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const { BROADSIDE_ENDPOINTS_FILE, BROADSIDE_MODEL, @@ -272,7 +272,7 @@ test("codecarto_broadside takes model and lens_models, validates them, and passe ]) { await assert.rejects( server.handleBroadside({ cwd: dir, action: "submit", lenses: ["architecture"], max_cost: 0, ...args }), - (error) => error instanceof McpError && error.code === ErrorCode.InvalidParams && pattern.test(error.message), + (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams && pattern.test(error.message), `${JSON.stringify(args)} must be refused as InvalidParams`, ); } diff --git a/tests/broadside-post-pass-verdicts.test.mjs b/tests/broadside-post-pass-verdicts.test.mjs index dc2b3a4..4e04a36 100644 --- a/tests/broadside-post-pass-verdicts.test.mjs +++ b/tests/broadside-post-pass-verdicts.test.mjs @@ -11,7 +11,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; -import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; +import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server"; const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const core = await import(pathToFileURL(`${REPO_ROOT}/core/broadside.ts`).href); @@ -257,11 +257,11 @@ test("codecarto_broadside collect: regenerate_post_passes is validated and repor await writeFile(join(runDir, "verified.json"), JSON.stringify(VERIFIED), "utf8"); await assert.rejects( server.handleBroadside({ cwd: dir, action: "collect", regenerate_post_passes: "yes" }), - (error) => error instanceof McpError && error.code === ErrorCode.InvalidParams && /must be a boolean/.test(error.message), + (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams && /must be a boolean/.test(error.message), ); await assert.rejects( server.handleBroadside({ cwd: dir, action: "collect", regenerate_post_passes: true, include_synthesis: false, include_triage: false }), - (error) => error instanceof McpError && error.code === ErrorCode.InvalidParams && /needs at least one of/.test(error.message), + (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams && /needs at least one of/.test(error.message), ); const result = await server.handleBroadside({ cwd: dir, action: "collect", regenerate_post_passes: true, wait_seconds: 5 }); assert.deepEqual(result.structuredContent.regenerated, ["synthesis", "triage"]); diff --git a/tests/broadside-verify.test.mjs b/tests/broadside-verify.test.mjs index e084a48..13d6388 100644 --- a/tests/broadside-verify.test.mjs +++ b/tests/broadside-verify.test.mjs @@ -13,7 +13,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const core = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { default: codeCartographerExtension } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/index.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const { BROADSIDE_CHAT_URL, BROADSIDE_MODEL, BROADSIDE_VERIFY_MAX_TOOL_CALLS, BROADSIDE_VERIFY_SYSTEM_PROMPT, broadsideDirFor, createRepoReader, loadBroadsideState, rankVerifiableFindings, runBroadsideVerify, saveBroadsideState, syncModelFor, verifyResultText, @@ -284,7 +284,7 @@ test("codecarto_broadside verify: parameters validated, verdicts in text and str for (const [args, pattern] of [[{ top: 0 }, /top must be a positive integer/], [{ top: 1.5 }, /top must be a positive integer/], [{ model: " " }, /model must be a non-empty/]]) { await assert.rejects( server.handleBroadside({ cwd: dir, action: "verify", ...args }), - (error) => error instanceof McpError && error.code === ErrorCode.InvalidParams && pattern.test(error.message), + (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams && pattern.test(error.message), ); } const result = await server.handleBroadside({ cwd: dir, action: "verify", top: 2, max_cost: 0 }); diff --git a/tests/config-problems.test.mjs b/tests/config-problems.test.mjs index b8e02d4..27e1e14 100644 --- a/tests/config-problems.test.mjs +++ b/tests/config-problems.test.mjs @@ -22,7 +22,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const core = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { default: codeCartographerExtension } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/index.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const { CONFIG_RELATIVE_PATH, describeConfigProblems, loadCodecartoConfig, loadUserConfig, mergeConfig, writeLibraryConfig } = core; @@ -256,8 +256,8 @@ test("codecarto_library_init does not switch the publish gate on, and says what await assert.rejects( server.handleLibraryInit({ library_path: join(dir, "bad-library"), namespace: "Team Alpha" }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /Invalid namespace "Team Alpha" \(lowercase ASCII, starts with a letter, max 64 chars\)/); return true; }, @@ -272,8 +272,8 @@ test("codecarto_library_init refuses to rewrite an unparseable user config", asy await assert.rejects( server.handleLibraryInit({ library_path: join(dir, "new-library") }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /Refusing to rewrite .*could not be parsed/); return true; }, @@ -295,9 +295,9 @@ test("codecarto_config lists the problems and the library tools refuse while any assert.equal(shown.structuredContent.libraryPath, libraryPath, "the user-global path is what is in effect"); const refused = (tool) => (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest); - assert.match(error.message, new RegExp(`^MCP error -32600: ${tool} refused: the configuration has problems`)); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); + assert.match(error.message, new RegExp(`^${tool} refused: the configuration has problems`)); assert.match(error.message, /library\.path must be absolute/); return true; }; diff --git a/tests/init-same-workspace.test.mjs b/tests/init-same-workspace.test.mjs index db315f0..951b34d 100644 --- a/tests/init-same-workspace.test.mjs +++ b/tests/init-same-workspace.test.mjs @@ -20,7 +20,7 @@ const core = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { ENGINEERING_NAMESPACE } = await import(pathToFileURL(`${REPO_ROOT}/core/engineering/ids.ts`).href); const { default: codeCartographerExtension } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/index.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); async function withTemp(fn) { const dir = await mkdtemp(join(tmpdir(), "cc-init-same-")); @@ -192,8 +192,8 @@ test("codecarto_init refuses to reset the packaged template without force", asyn await assert.rejects( server.handleInit({ cwd }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /is CodeCartographer's own packaged template \(a checkout install\)/); assert.match(error.message, /Pass force: true to move that state/); assert.match(error.message, /the framework files stay in place/); diff --git a/tests/low-hygiene.test.mjs b/tests/low-hygiene.test.mjs index c47cb10..1d0e655 100644 --- a/tests/low-hygiene.test.mjs +++ b/tests/low-hygiene.test.mjs @@ -12,7 +12,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const core = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { phaseIdFromSessionName } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/phase-compaction.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); function response(status, body) { return { status, ok: status < 300, json: async () => body, text: async () => JSON.stringify(body) }; @@ -27,12 +27,12 @@ test("a status.yaml the workspace loader cannot use is InvalidRequest, not Inter const statusPath = join(cwd, ".codecarto", "workflow", "status.yaml"); await writeFile(statusPath, "pipeline: workflow/no-such-pipeline.yaml\nschema_version: 1\n", "utf8"); await assert.rejects(server.handleStatus({ cwd }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest, "a config problem is the caller's to fix, not a server bug"); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest, "a config problem is the caller's to fix, not a server bug"); return true; }); await writeFile(statusPath, "pipeline: workflow/pipeline-lite.yaml\n bad: indent\n", "utf8"); - await assert.rejects(server.handleNext({ cwd }), (error) => error instanceof McpError && error.code === ErrorCode.InvalidRequest && /YAML line 2/.test(error.message)); + await assert.rejects(server.handleNext({ cwd }), (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidRequest && /YAML line 2/.test(error.message)); } finally { await rm(cwd, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); } diff --git a/tests/mcp-engineering.test.mjs b/tests/mcp-engineering.test.mjs index a52449b..4e95bd5 100644 --- a/tests/mcp-engineering.test.mjs +++ b/tests/mcp-engineering.test.mjs @@ -31,7 +31,7 @@ import { fileURLToPath, pathToFileURL } from "node:url"; const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const engineeringMcp = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/engineering.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const { openStore } = await import(pathToFileURL(`${REPO_ROOT}/core/engineering/index.ts`).href); const { ENGINEERING_TOOLS } = engineeringMcp; @@ -86,7 +86,7 @@ test("an unknown action is refused, not treated as a default", async () => { await withWorkspace(async (cwd) => { await assert.rejects( () => handleChange({ cwd, action: "delete_everything" }), - (error) => error instanceof McpError && error.code === ErrorCode.InvalidParams, + (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams, ); }); }); @@ -95,7 +95,7 @@ test("a missing action is refused", async () => { await withWorkspace(async (cwd) => { await assert.rejects( () => handleChange({ cwd }), - (error) => error instanceof McpError && error.code === ErrorCode.InvalidParams, + (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams, ); }); }); @@ -124,7 +124,7 @@ test("an agent cannot approve its own work through ordinary fields", async () => ]) { await assert.rejects( () => handleChange({ cwd, change_id: changeId, ...forged }), - (error) => error instanceof McpError, + (error) => error instanceof ProtocolError, `${JSON.stringify(forged)} was not refused`, ); } @@ -175,7 +175,7 @@ test("a stale revision is refused rather than silently overwriting", async () => await handleChange({ cwd, action: "update", change_id: changeId, outcome: "first write", revision: stale }); await assert.rejects( () => handleChange({ cwd, action: "update", change_id: changeId, outcome: "second write", revision: stale }), - (error) => error instanceof McpError && /revision|conflict|stale/i.test(error.message), + (error) => error instanceof ProtocolError && /revision|conflict|stale/i.test(error.message), ); }); }); @@ -265,7 +265,7 @@ test("an explicitly refused action says why, rather than reading as a typo", asy await assert.rejects( () => handleChange({ cwd, action }), (error) => { - assert.ok(error instanceof McpError, `${action} did not raise an McpError`); + assert.ok(error instanceof ProtocolError, `${action} did not raise an ProtocolError`); assert.match(error.message, /not available through this surface/, `${action} was not refused with a reason`); return true; }, @@ -280,7 +280,7 @@ test("a caller cannot supply a derived field on create", async () => { for (const field of ["decision", "authority", "discharges", "approved_by", "accepted_at"]) { await assert.rejects( () => handleChange({ cwd, action: "create", title: "t", outcome: "o", [field]: "anything" }), - (error) => error instanceof McpError && new RegExp(`${field} is derived`).test(error.message), + (error) => error instanceof ProtocolError && new RegExp(`${field} is derived`).test(error.message), `${field} was accepted from a caller`, ); } @@ -410,7 +410,7 @@ test("a blank or whitespace-only required field is refused", async () => { for (const blank of ["", " ", "\t", "\n"]) { await assert.rejects( () => handleChange({ cwd, action: "create", title: blank, outcome: "something" }), - (error) => error instanceof McpError && /title/.test(error.message), + (error) => error instanceof ProtocolError && /title/.test(error.message), `a title of ${JSON.stringify(blank)} was accepted`, ); } @@ -436,7 +436,7 @@ test("a proof that is not an object is refused", async () => { for (const bad of ["a string", 42, true, ["an", "array"], null]) { await assert.rejects( () => handleChange({ cwd, action: "record_proof", change_id: created.structuredContent.change_id, proof: bad }), - (error) => error instanceof McpError && /proof/.test(error.message), + (error) => error instanceof ProtocolError && /proof/.test(error.message), `a proof of ${JSON.stringify(bad)} was accepted`, ); } @@ -464,7 +464,7 @@ test("an accepted change cannot be rewritten through update", async () => { await store.put({ ...change, state, revision: nextRevision }, current ? { ifRevision: current.record.revision } : undefined); await assert.rejects( () => handleChange({ cwd, action: "update", change_id: id, revision: nextRevision, title: "MUTATED AFTER ACCEPTANCE" }), - (error) => error instanceof McpError && new RegExp(state).test(error.message), + (error) => error instanceof ProtocolError && new RegExp(state).test(error.message), `a ${state} change was editable`, ); const after = (await store.get("change", id)).record; @@ -483,21 +483,21 @@ test("an update without the revision it read is refused, not applied blindly", a const id = created.structuredContent.change_id; await assert.rejects( () => handleChange({ cwd, action: "update", change_id: id, title: "BLIND OVERWRITE" }), - (error) => error instanceof McpError && /requires the revision you last read/.test(error.message), + (error) => error instanceof ProtocolError && /requires the revision you last read/.test(error.message), "a blind update was applied", ); const shown = await handleChange({ cwd, action: "show", change_id: id }); assert.equal(shown.structuredContent.title, "Original", "the record changed despite the refusal"); // A non-integer revision is a caller error, not a silent coercion. The - // message must name the TYPE problem: asserting only "some McpError" + // message must name the TYPE problem: asserting only "some ProtocolError" // let the integer check be deleted, because the undefined check above // already rejects "1" and null for a different reason. for (const bad of ["1", 1.5, null, {}, Number.NaN]) { await assert.rejects( () => handleChange({ cwd, action: "update", change_id: id, revision: bad, title: "x" }), (error) => { - assert.ok(error instanceof McpError, `a revision of ${JSON.stringify(bad)} did not raise an McpError`); + assert.ok(error instanceof ProtocolError, `a revision of ${JSON.stringify(bad)} did not raise an ProtocolError`); assert.match( error.message, /revision must be an integer|requires the revision you last read/, @@ -511,7 +511,7 @@ test("an update without the revision it read is refused, not applied blindly", a // 1.5 and NaN are defined, so they reach the integer check specifically. await assert.rejects( () => handleChange({ cwd, action: "update", change_id: id, revision: 1.5, title: "x" }), - (error) => error instanceof McpError && /revision must be an integer/.test(error.message), + (error) => error instanceof ProtocolError && /revision must be an integer/.test(error.message), "a fractional revision was not refused as a type error", ); }); @@ -576,8 +576,8 @@ test("a bad argument is a caller error, not an internal one", async () => { await assert.rejects( () => handleChange({ cwd, action: "create", title: "t", outcome: "o", ...args }), (error) => { - assert.ok(error instanceof McpError, `${why} did not raise an McpError`); - assert.equal(error.code, ErrorCode.InvalidParams, `${why} was reported as an internal error (${error.code})`); + assert.ok(error instanceof ProtocolError, `${why} did not raise an ProtocolError`); + assert.equal(error.code, ProtocolErrorCode.InvalidParams, `${why} was reported as an internal error (${error.code})`); return true; }, `${why} was accepted`, @@ -589,7 +589,7 @@ test("a bad argument is a caller error, not an internal one", async () => { await handleChange({ ...args }); await assert.rejects( () => handleChange({ ...args, title: "Beta" }), - (error) => error instanceof McpError && error.code === ErrorCode.InvalidParams && /new request_id/.test(error.message), + (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams && /new request_id/.test(error.message), "a conflicting retry was reported as an internal error", ); }); @@ -604,7 +604,7 @@ test("an inherited property name is not treated as a refused action", async () = await assert.rejects( () => handleChange({ cwd, action }), (error) => { - assert.ok(error instanceof McpError); + assert.ok(error instanceof ProtocolError); assert.ok(!/native code|\[object Object\]/.test(error.message), `${action} leaked engine internals: ${error.message}`); assert.match(error.message, /unknown action/); return true; diff --git a/tests/mcp-library.test.mjs b/tests/mcp-library.test.mjs index 9661dbf..d7d7a8b 100644 --- a/tests/mcp-library.test.mjs +++ b/tests/mcp-library.test.mjs @@ -14,7 +14,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const { handlePublish, handleLibraryList, handleLibraryReindex } = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { writeMarker, LIBRARY_MARKER_FILE, LIBRARY_INDEX_FILE, ENTRIES_DIR, METADATA_FILE, SPEC_FILE, ConfidentialityMismatchError } = await import(pathToFileURL(`${REPO_ROOT}/core/library.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); // handlePublish reads the user-global config for its publish_confirm gate. // Point that at a path which does not exist so the developer's real @@ -159,8 +159,8 @@ test("handlePublish rejects missing source_repo", async () => { await assert.rejects( handlePublish(args), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /source_repo/); return true; }, @@ -178,8 +178,8 @@ test("handlePublish rejects missing headline", async () => { await assert.rejects( handlePublish(args), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /headline/); return true; }, @@ -195,8 +195,8 @@ test("handlePublish rejects when library_path is not a library", async () => { await assert.rejects( handlePublish(basePublishArgs(dir)), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /missing \.codecarto-library/); return true; }, @@ -214,8 +214,8 @@ test("handlePublish requires namespace for namespaced libraries", async () => { await assert.rejects( handlePublish(args), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /namespace/); return true; }, @@ -242,8 +242,8 @@ test("handlePublish rejects without library_path or cwd config", async () => { await assert.rejects( handlePublish({ source_repo: "x", headline: "y", spec: "z" }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /library_path is required/); return true; }, @@ -259,7 +259,7 @@ test("handlePublish refuses an internal entry into a public library and writes n handlePublish(basePublishArgs(libraryPath, { confidentiality: "internal" })), (error) => { // Surfaced as the core's typed error, not rewrapped: the server's - // CallTool handler turns any non-McpError into an InternalError that + // CallTool handler turns any non-ProtocolError into an InternalError that // keeps this message, and the message names the override. assert.ok(error instanceof ConfidentialityMismatchError); assert.equal(error.entryConfidentiality, "internal"); @@ -357,8 +357,8 @@ function assertPublishConfirmRefusal(error) { // The shape codecarto_broadside's spend gate has: an InvalidRequest whose // message is the whole story, so a host that reads only the error text // still sees the preview and the way forward. - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /library\.publish_confirm is set/); assert.match(error.message, /Nothing was written/); assert.match(error.message, /confirm: true/); @@ -541,8 +541,8 @@ test("the gate runs after argument validation, so a refusal previews a publish t const args = basePublishArgs(libraryPath); delete args.headline; await assert.rejects(handlePublish(args), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams, "a bad argument is reported as such, not hidden behind the gate"); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams, "a bad argument is reported as such, not hidden behind the gate"); assert.match(error.message, /headline/); return true; }); @@ -649,8 +649,8 @@ test("handleLibraryReindex rejects on missing marker", async () => { await assert.rejects( handleLibraryReindex({ library_path: dir }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); return true; }, ); diff --git a/tests/mcp-protocol-2026-07-28.test.mjs b/tests/mcp-protocol-2026-07-28.test.mjs new file mode 100644 index 0000000..a7e8914 --- /dev/null +++ b/tests/mcp-protocol-2026-07-28.test.mjs @@ -0,0 +1,268 @@ +// Protocol-era contract of the shipped stdio server (#185). +// +// Drives dist/mcp-server/bin.mjs over raw newline-delimited JSON-RPC, the way +// a host does, and pins two things at once: +// +// 1. NON-REGRESSION: every 2025-era `initialize` the SDK v1 line accepted is +// still accepted and negotiated exactly as before. The issue's hard +// acceptance item is "today the server accepts every revision the SDK +// supports, and that behavior must not regress". +// 2. THE 2026-07-28 ERA: a client opening with the per-request `_meta` +// envelope (`server/discover`, then envelope-bearing `tools/list` / +// `tools/call`) is answered in that era: supportedVersions, resultType, +// caching hints, serverInfo stamping. +// +// Both eras are served from one process; the opening message selects the era. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const BIN = join(REPO_ROOT, "dist/mcp-server/bin.mjs"); + +const MODERN_REVISION = "2026-07-28"; +// The legacy-era revisions the SDK v1 line (1.30.0) accepted on `initialize`. +// Removing one from this list is the regression the issue forbids. +const LEGACY_REVISIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"]; + +const PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion"; +const CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo"; +const CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities"; +const SERVER_INFO_META_KEY = "io.modelcontextprotocol/serverInfo"; + +/** The 2026-07-28 per-request envelope: every request carries its own era claim. */ +function envelope(params = {}) { + return { + ...params, + _meta: { + [PROTOCOL_VERSION_META_KEY]: MODERN_REVISION, + [CLIENT_INFO_META_KEY]: { name: "protocol-2026-test", version: "0" }, + [CLIENT_CAPABILITIES_META_KEY]: {}, + }, + }; +} + +async function withSession(fn) { + const child = spawn(process.execPath, [BIN], { + cwd: REPO_ROOT, + stdio: ["pipe", "pipe", "pipe"], + env: { ...process.env, NO_COLOR: "1" }, + }); + let buffer = ""; + const pending = new Map(); + let nextId = 1; + child.stdout.on("data", (chunk) => { + buffer += chunk.toString(); + let index; + while ((index = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, index).trim(); + buffer = buffer.slice(index + 1); + if (!line) continue; + let message; + try { + message = JSON.parse(line); + } catch { + continue; + } + const waiter = pending.get(message.id); + if (waiter) { + pending.delete(message.id); + waiter(message); + } + } + }); + const stderr = []; + child.stderr.on("data", (chunk) => stderr.push(chunk.toString())); + + function send(method, params) { + const id = nextId++; + const frame = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n"; + return new Promise((resolvePromise, rejectPromise) => { + const timer = setTimeout(() => { + pending.delete(id); + rejectPromise(new Error(`timed out waiting for ${method}; stderr: ${stderr.join("")}`)); + }, 30_000); + pending.set(id, (message) => { + clearTimeout(timer); + resolvePromise(message); + }); + child.stdin.write(frame); + }); + } + function notify(method, params) { + child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n"); + } + try { + return await fn({ send, notify }); + } finally { + child.stdin.end(); + child.kill(); + } +} + +const initialize = (send, protocolVersion) => + send("initialize", { protocolVersion, capabilities: {}, clientInfo: { name: "protocol-2026-test", version: "0" } }); + +// ---------- 1. non-regression: the 2025-era handshake ---------- + +for (const revision of LEGACY_REVISIONS) { + test(`initialize with ${revision} is accepted and echoed back (non-regression)`, async () => { + await withSession(async ({ send, notify }) => { + const reply = await initialize(send, revision); + assert.ok(!reply.error, JSON.stringify(reply.error)); + assert.equal(reply.result.protocolVersion, revision); + assert.equal(reply.result.serverInfo.name, "codecartographer"); + assert.deepEqual(Object.keys(reply.result.capabilities), ["tools"]); + notify("notifications/initialized"); + const listed = await send("tools/list", {}); + assert.ok(listed.result.tools.length >= 22, `expected the full inventory, got ${listed.result.tools.length}`); + // A 2025-era response carries none of the 2026-era result vocabulary. + assert.equal(listed.result.resultType, undefined); + assert.equal(listed.result.ttlMs, undefined); + }); + }); +} + +test("initialize with an unknown revision negotiates down to the newest legacy revision", async () => { + await withSession(async ({ send }) => { + // A client that names the modern revision through the LEGACY handshake is + // still a legacy client (the modern era opens with server/discover, not + // initialize): the SDK v1 and v2 lines both answer with the newest + // revision they serve on initialize. + const reply = await initialize(send, MODERN_REVISION); + assert.ok(!reply.error, JSON.stringify(reply.error)); + assert.equal(reply.result.protocolVersion, "2025-11-25"); + }); +}); + +// ---------- 2. the 2026-07-28 era ---------- + +test("server/discover answers with the modern revision, capabilities and instructions", async () => { + await withSession(async ({ send }) => { + const reply = await send("server/discover", envelope()); + assert.ok(!reply.error, `server/discover failed: ${JSON.stringify(reply.error)}`); + assert.deepEqual(reply.result.supportedVersions, [MODERN_REVISION]); + assert.deepEqual(Object.keys(reply.result.capabilities), ["tools"]); + assert.equal(reply.result.resultType, "complete"); + assert.equal(typeof reply.result.ttlMs, "number"); + assert.ok(["public", "private"].includes(reply.result.cacheScope)); + assert.equal(reply.result._meta[SERVER_INFO_META_KEY].name, "codecartographer"); + }); +}); + +test("tools/list in the modern era carries resultType, caching hints and serverInfo", async () => { + await withSession(async ({ send }) => { + await send("server/discover", envelope()); + const listed = await send("tools/list", envelope()); + assert.ok(!listed.error, JSON.stringify(listed.error)); + assert.equal(listed.result.resultType, "complete"); + // The inventory is static for the life of the process: cache it long + // and share it, as the issue's P1 item specifies. + assert.equal(listed.result.cacheScope, "public"); + assert.ok(listed.result.ttlMs >= 60_000, `ttlMs ${listed.result.ttlMs} is not a long TTL`); + assert.equal(listed.result._meta[SERVER_INFO_META_KEY].name, "codecartographer"); + assert.ok(listed.result.tools.length >= 22); + }); +}); + +test("tools/call in the modern era returns resultType 'complete' with both content halves", async () => { + await withSession(async ({ send }) => { + await send("server/discover", envelope()); + const called = await send("tools/call", envelope({ name: "codecarto_guide", arguments: {} })); + assert.ok(!called.error, JSON.stringify(called.error)); + assert.equal(called.result.resultType, "complete"); + assert.equal(called.result.content[0].type, "text"); + // issue #94's failure mode must survive the new era's projection too. + assert.equal(typeof called.result.structuredContent.text, "string"); + assert.equal(called.result._meta[SERVER_INFO_META_KEY].name, "codecartographer"); + }); +}); + +test("a tool refusal in the modern era is still a JSON-RPC error with the standard code", async () => { + await withSession(async ({ send }) => { + await send("server/discover", envelope()); + const refused = await send("tools/call", envelope({ name: "codecarto_status", arguments: { cwd: "relative/path" } })); + assert.ok(refused.error, "a bad cwd must be a JSON-RPC error"); + assert.equal(refused.error.code, -32602, "InvalidParams keeps the JSON-RPC standard code"); + const unknown = await send("tools/call", envelope({ name: "codecarto_no_such_tool", arguments: {} })); + assert.equal(unknown.error.code, -32601, "an unknown tool is MethodNotFound"); + }); +}); + +test("a client that probes with server/discover and then falls back to initialize is served the 2025 era", async () => { + await withSession(async ({ send, notify }) => { + // A probe alone does not pin the connection: a negotiating client that + // discovers, decides it prefers the legacy handshake, and sends + // `initialize` must still be served (the SDK discards the probe instance + // and pins a legacy one). + await send("server/discover", envelope()); + const fallback = await initialize(send, "2025-11-25"); + assert.ok(!fallback.error, JSON.stringify(fallback.error)); + assert.equal(fallback.result.protocolVersion, "2025-11-25"); + notify("notifications/initialized"); + const listed = await send("tools/list", {}); + assert.ok(listed.result.tools.length >= 22); + assert.equal(listed.result.resultType, undefined, "a legacy-pinned connection carries no 2026 vocabulary"); + }); +}); + +test("a 2025-era initialize on a modern-PINNED connection is refused, not mis-served", async () => { + await withSession(async ({ send }) => { + await send("server/discover", envelope()); + // An envelope-bearing request after the probe pins the modern era. + const listed = await send("tools/list", envelope()); + assert.equal(listed.result.resultType, "complete"); + const late = await initialize(send, "2025-11-25"); + assert.ok(late.error, "the era is decided by the opening exchange"); + assert.deepEqual(late.error.data.supported, [MODERN_REVISION]); + assert.equal(late.error.data.requested, "2025-11-25"); + }); +}); + +// ---------- 3. verify item: inputSchema under JSON Schema 2020-12 ---------- + +// Keywords that Draft-07 accepted and 2020-12 removed or renamed. Under the +// 2020-12 default a validator silently ignores them, which turns a constraint +// into no constraint. +const DRAFT07_ONLY_KEYWORDS = new Set(["definitions", "dependencies", "additionalItems", "id"]); + +function* walk(node, path = "") { + if (Array.isArray(node)) { + for (const [i, child] of node.entries()) yield* walk(child, `${path}[${i}]`); + return; + } + if (node === null || typeof node !== "object") return; + yield [path, node]; + for (const [key, child] of Object.entries(node)) { + // Property NAMES under `properties` are not keywords. + if (key === "properties" && child && typeof child === "object") { + for (const [name, sub] of Object.entries(child)) yield* walk(sub, `${path}.properties.${name}`); + continue; + } + yield* walk(child, `${path}.${key}`); + } +} + +test("every advertised inputSchema is valid under JSON Schema 2020-12 (no Draft-07-only keywords)", async () => { + await withSession(async ({ send, notify }) => { + await initialize(send, "2025-11-25"); + notify("notifications/initialized"); + const listed = await send("tools/list", {}); + const problems = []; + for (const tool of listed.result.tools) { + assert.equal(tool.inputSchema.type, "object", `${tool.name}: inputSchema.type`); + for (const [path, node] of walk(tool.inputSchema, tool.name)) { + for (const key of Object.keys(node)) { + if (DRAFT07_ONLY_KEYWORDS.has(key)) problems.push(`${path}: ${key}`); + if (key === "$ref" && String(node[key]).includes("#/definitions/")) problems.push(`${path}: $ref into definitions`); + } + // Draft-07 tuple form `items: [...]` became `prefixItems` in 2020-12. + if (Array.isArray(node.items)) problems.push(`${path}: items as array`); + } + } + assert.deepEqual(problems, []); + }); +}); diff --git a/tests/mcp-server.test.mjs b/tests/mcp-server.test.mjs index ae4821a..50ec913 100644 --- a/tests/mcp-server.test.mjs +++ b/tests/mcp-server.test.mjs @@ -1,7 +1,7 @@ // MCP server smoke tests. Drives the core workflow handlers directly // (without spawning a stdio transport) against a fresh temp workspace // initialized via handleInit. Confirms that each tool returns the expected -// content shape, that error cases throw McpError with the right code, and +// content shape, that error cases throw ProtocolError with the right code, and // that the phase prompt the MCP server returns is byte-identical to the // one core/prompts.ts produces (since both Pi and MCP must emit the same // text). @@ -28,7 +28,7 @@ const { handleConfig, } = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { buildPhasePrompt, getNextEligiblePhase, getWorkspaceState } = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); let WORKSPACE; @@ -87,8 +87,8 @@ test("handleComplete refuses when validation is MISSING", async () => { await assert.rejects( handleComplete({ cwd: WORKSPACE }), (error) => { - assert.ok(error instanceof McpError, "expected McpError"); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError, "expected ProtocolError"); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /Cannot complete .*MISSING/); return true; }, @@ -99,8 +99,8 @@ test("handleSkill refuses while pipeline is incomplete", async () => { await assert.rejects( handleSkill({ cwd: WORKSPACE, name: "spec-delta-application" }), (error) => { - assert.ok(error instanceof McpError, "expected McpError"); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError, "expected ProtocolError"); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /pipeline is not complete/); return true; }, @@ -131,7 +131,7 @@ test("an unknown skill name points at the Broad-Side exemption", async () => { await assert.rejects( handleSkill({ cwd: WORKSPACE, name: "no-such-skill" }), (error) => { - assert.ok(error instanceof McpError, "expected McpError"); + assert.ok(error instanceof ProtocolError, "expected ProtocolError"); // The completion gate fires first for a workspace mid-pipeline; either // message is acceptable, but the name must never dead-end silently. assert.match(error.message, /pipeline is not complete|Unknown skill/); @@ -152,8 +152,8 @@ test("handleInit refuses to overwrite without force", async () => { await assert.rejects( handleInit({ cwd: WORKSPACE }), (error) => { - assert.ok(error instanceof McpError, "expected McpError"); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError, "expected ProtocolError"); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /already exists/); assert.match(error.message, /force: true/); return true; @@ -171,8 +171,8 @@ test("validation rejects non-absolute cwd", async () => { await assert.rejects( handleStatus({ cwd: "relative/path" }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /absolute path/); return true; }, @@ -183,8 +183,8 @@ test("validation rejects missing cwd", async () => { await assert.rejects( handleStatus({ cwd: "" }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); return true; }, ); @@ -196,8 +196,8 @@ test("requireWorkspace error: missing .codecarto/", async () => { await assert.rejects( handleStatus({ cwd: empty }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /codecarto_init/); return true; }, diff --git a/tests/mcp-uncovered-handlers.test.mjs b/tests/mcp-uncovered-handlers.test.mjs index 8f2d1a3..50a0d2c 100644 --- a/tests/mcp-uncovered-handlers.test.mjs +++ b/tests/mcp-uncovered-handlers.test.mjs @@ -26,7 +26,7 @@ const { handleBroadside, } = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { getWorkspaceState } = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); async function withWorkspace(fn, { init = "lite" } = {}) { const cwd = await mkdtemp(join(tmpdir(), "cc-mcp-uncovered-")); @@ -55,7 +55,7 @@ test("open refuses a directory with no workspace", async () => { await assert.rejects( () => handleOpen({ cwd }), (error) => { - assert.ok(error instanceof McpError); + assert.ok(error instanceof ProtocolError); assert.match(error.message, /codecarto_init|No existing/i); return true; }, @@ -88,7 +88,7 @@ test("switch_pipeline rejects an unknown variant without touching status", async await assert.rejects( () => handleSwitchPipeline({ cwd, pipeline: "turbo" }), (error) => { - assert.ok(error instanceof McpError); + assert.ok(error instanceof ProtocolError); assert.match(error.message, /Unknown pipeline: turbo/); return true; }, @@ -144,8 +144,8 @@ test("broadside rejects an unknown action and names the valid ones", async () => await assert.rejects( () => handleBroadside({ cwd, action: "obliterate" }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /Unknown action: obliterate/); assert.match(error.message, /submit, collect, status, models/); return true; @@ -190,8 +190,8 @@ test("broadside rejects an unknown lens before spending anything", async () => { await assert.rejects( () => handleBroadside({ cwd, action: "submit", api_key: "sk-not-used", lenses: ["architecture", "vibes"] }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidParams); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); assert.match(error.message, /Unknown lens\(es\): vibes/); return true; }, diff --git a/tests/publish-path-containment.test.mjs b/tests/publish-path-containment.test.mjs index ca9a011..ae5bc04 100644 --- a/tests/publish-path-containment.test.mjs +++ b/tests/publish-path-containment.test.mjs @@ -138,11 +138,11 @@ test("readSpecArg accepts inline spec regardless of allowedRoots", async () => { // way the workflow tools validate a required one, first. const { handleLibraryList, handleLibraryReindex } = await import("../mcp-server/server.ts"); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const isInvalidParams = (pattern) => (err) => { - assert.ok(err instanceof McpError, "expected McpError"); - assert.equal(err.code, ErrorCode.InvalidParams); + assert.ok(err instanceof ProtocolError, "expected ProtocolError"); + assert.equal(err.code, ProtocolErrorCode.InvalidParams); assert.match(err.message, pattern); return true; }; @@ -163,7 +163,7 @@ test("codecarto_publish refuses a relative cwd before spec_path is read through headline: "Test spec", spec_path: secretPath, }), - isInvalidParams(/^MCP error -32602: cwd must be an absolute path, got: target$/), + isInvalidParams(/^cwd must be an absolute path, got: target$/), ); } finally { await cleanup(); diff --git a/tests/self-scan-triage.test.mjs b/tests/self-scan-triage.test.mjs index 4e0a655..a2031fe 100644 --- a/tests/self-scan-triage.test.mjs +++ b/tests/self-scan-triage.test.mjs @@ -35,7 +35,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const core = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); const { phaseCompactionExtension } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/phase-compaction.ts`).href); const { handleInit, handleLibraryInit } = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const PIPELINE = "workflow/pipeline-architecture-only.yaml"; const UNTERMINATED_LOG = "# Thread Log\n\n- 2026-01-01 — init — seeded without a trailing newline"; @@ -158,7 +158,7 @@ test("lead 5: codecarto_library_init refuses a relative library_path like its si try { await assert.rejects( () => handleLibraryInit({ library_path: "relative/library" }), - (error) => error instanceof McpError && error.code === ErrorCode.InvalidParams && /absolute/.test(error.message), + (error) => error instanceof ProtocolError && error.code === ProtocolErrorCode.InvalidParams && /absolute/.test(error.message), ); assert.equal(await core.pathExists(configPath), false, "a refused path must not reach the user-global config"); diff --git a/tests/skill-name-resolution.test.mjs b/tests/skill-name-resolution.test.mjs index edd3882..f4eca4d 100644 --- a/tests/skill-name-resolution.test.mjs +++ b/tests/skill-name-resolution.test.mjs @@ -18,7 +18,7 @@ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const core = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { default: codeCartographerExtension } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/index.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const PASSING_REPORT = [ "# Map", @@ -151,9 +151,9 @@ test("codecarto_skill refuses a traversal name on a completed pipeline (probe P5 await assert.rejects( server.handleSkill({ cwd, name: TRAVERSAL }), (error) => { - assert.ok(error instanceof McpError, "expected McpError"); - assert.equal(error.code, ErrorCode.InvalidParams); - assert.match(error.message, /^MCP error -32602: Unknown skill: \.\.\/findings\/architecture\./); + assert.ok(error instanceof ProtocolError, "expected ProtocolError"); + assert.equal(error.code, ProtocolErrorCode.InvalidParams); + assert.match(error.message, /^Unknown skill: \.\.\/findings\/architecture\./); assert.match(error.message, /Available: /, "the refusal lists what is installed"); return true; }, diff --git a/tests/stuck-pipeline.test.mjs b/tests/stuck-pipeline.test.mjs index c40fca4..53386bc 100644 --- a/tests/stuck-pipeline.test.mjs +++ b/tests/stuck-pipeline.test.mjs @@ -19,7 +19,7 @@ const core = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); const server = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); const { default: codeCartographerExtension } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/index.ts`).href); const { buildAutoSummary } = await import(pathToFileURL(`${REPO_ROOT}/extensions/codecarto/auto-runner.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); const REPORT = "# A\n\n## Validation\n\n| # | c | r | e |\n|---|---|---|---|\n| 1 | c | PASS | e |\n\n**Overall:** PASS\n"; const STUCK = /^Pipeline is stuck: b depends on nope, which is not in this pipeline\. No phase can run until the pipeline file's depends_on is fixed \(or switch pipelines with codecarto_switch_pipeline \/ \/codecarto-switch-pipeline\)\.$/m; @@ -124,13 +124,13 @@ test("codecarto_status reports stuck (probe P6), and next / skill / amend refuse assert.deepEqual(status.structuredContent.stuck, [{ phaseId: "b", missing: [{ dependencyId: "nope", reason: "not-in-pipeline" }] }]); const refused = (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /Pipeline is stuck: b depends on nope, which is not in this pipeline/); return true; }; await assert.rejects(server.handleNext({ cwd }), refused); - await assert.rejects(server.handleSkill({ cwd, name: "spec-delta-application" }), (error) => refused(error) && /^MCP error -32600: Cannot run skill: the pipeline is not complete\./.test(error.message)); + await assert.rejects(server.handleSkill({ cwd, name: "spec-delta-application" }), (error) => refused(error) && /^Cannot run skill: the pipeline is not complete\./.test(error.message)); await mkdir(join(codecarto, "scratch", "amendments"), { recursive: true }); await writeFile(join(codecarto, "scratch", "amendments", "x.yaml"), "schema_version: 1\nopen_question_closures:\n - q1\n", "utf8"); await assert.rejects(server.handleAmend({ cwd, name: "x" }), (error) => refused(error) && /Cannot amend: the pipeline is not complete\./.test(error.message)); diff --git a/tests/synthesis.test.mjs b/tests/synthesis.test.mjs index ba96c41..f85cd7f 100644 --- a/tests/synthesis.test.mjs +++ b/tests/synthesis.test.mjs @@ -19,7 +19,7 @@ const { writeMarker, } = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); const { handleInit, handlePhase } = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); -const { McpError, ErrorCode } = await import("@modelcontextprotocol/sdk/types.js"); +const { ProtocolError, ProtocolErrorCode } = await import("@modelcontextprotocol/server"); let workspace; let library; @@ -194,8 +194,8 @@ test("MCP maps confirmation failures to InvalidRequest", async () => { await assert.rejects( handlePhase({ cwd: workspace, phase: "spec-merge" }), (error) => { - assert.ok(error instanceof McpError); - assert.equal(error.code, ErrorCode.InvalidRequest); + assert.ok(error instanceof ProtocolError); + assert.equal(error.code, ProtocolErrorCode.InvalidRequest); assert.match(error.message, /no library entries are confirmed/); return true; },