From f350c0fca601a3ee0f6e8b5200480de7f0c05901 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 25 Jul 2026 17:56:07 -0700 Subject: [PATCH 1/3] Assert on structured output in the smoke tests The smoke tests connected to each server, listed its tools and stopped there, so they could not catch a broken structured result. mcp-test-client now calls every tool that declares an outputSchema and checks the answer against it: the result must carry structuredContent, and a tool with an array-rooted schema must return a top-level JSON array rather than an object wrapping one. Tools with no output schema are left alone, so this is not a new requirement on any example. The client negotiates with mode "auto", so it exercises servers of either protocol era. Tool calls reach the live NWS API; an error result is reported as a skip rather than a failure. mock-mcp-server moves to the low-level Server and advertises one array-rooted and one object-rooted tool, with the schemas written out literally. A client that compiles every declared outputSchema at connect time fails against the array-rooted one if it assumes output schemas are always objects. utils.sh no longer swallows build output. A failed build left no binary behind and the test reported a downstream spawn ENOENT instead of the compiler error, which is what makes a broken build hard to diagnose from CI logs; #143 is the current example. check_dependency returns instead of exiting, so a missing toolchain fails its own test and the suite still prints its summary. Binaries resolve through a helper that accounts for the .exe suffix. The test helpers move to the 2.0 MCP SDK packages, where the 2026-07-28 support lives; the examples themselves are untouched. Those require Node 20 or newer, so CI moves off Node 18 to Node 24, the Active LTS - Node 20 reached end of life in April 2026. The Go examples and the Rust client stay uncovered here. Both clients abort when no .env file is present, so that coverage lands with those examples. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 4 +- tests/README.md | 30 +- tests/helpers/mcp-test-client.ts | 106 ++- tests/helpers/mock-mcp-server.ts | 125 +++- tests/helpers/package-lock.json | 1073 ++---------------------------- tests/helpers/package.json | 8 +- tests/smoke-test.sh | 38 +- tests/utils.sh | 64 +- 8 files changed, 338 insertions(+), 1110 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c92444e5..c6e7ab6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + # Node 24 is the Active LTS. The 2.0 MCP SDK packages require >=20, but + # Node 20 reached end of life in April 2026. - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: "18" + node-version: "24" - name: Set up Python uses: actions/setup-python@v5 diff --git a/tests/README.md b/tests/README.md index 30415090..d917db66 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,13 +1,26 @@ # MCP Quickstart Smoke Tests -This directory contains smoke tests for the MCP quickstart examples. These tests verify that all example servers and clients can start and respond correctly, without calling external APIs. +This directory contains smoke tests for the MCP quickstart examples. These tests verify that all example servers and clients can start and respond correctly. ## Overview The smoke tests verify: -- **Servers**: Each weather server (Python, TypeScript, Rust) can start and respond to MCP protocol requests -- **Clients**: Each MCP client (Python, TypeScript) can connect to a mock server and list tools +- **Servers**: Each weather server (Python, TypeScript, Rust, Go) can start, respond to MCP protocol requests, and honour the output schemas it advertises +- **Clients**: Each MCP client (Python, TypeScript, Go, Rust) can connect to a mock server and list tools + +The Ruby examples are not covered: the `mcp` gem cannot negotiate protocol revision `2026-07-28`. + +## Structured output + +Listing tools is not enough to catch a broken structured result, so each server test also **calls** every tool that declares an `outputSchema` and checks the answer: + +- the result must carry `structuredContent`, and it must conform to the declared schema (the SDK validates this and throws on a mismatch); +- a tool with an array-rooted schema must return a **top-level JSON array**, and one with an object-rooted schema must return an object. + +The array case is the one worth guarding. A server that advertises `{"type": "array"}` and then answers `{"result": [...]}` passes a tools/list-only test and fails this one. + +Tool calls reach the live NWS API. When it is unreachable the tools return an error result, which the test reports as a skip rather than a failure — someone else's outage should not fail the build. ## Running Tests @@ -23,6 +36,7 @@ The smoke tests verify: - **uv** (Python package manager) - **Rust** stable - **Cargo** (for Rust builds) +- **Go** 1.25+ ## How It Works @@ -30,10 +44,10 @@ The smoke tests verify: Each server test: -1. Builds/prepares the server if needed +1. Builds/prepares the server if needed (a failed build prints its compiler output rather than swallowing it) 2. Uses `mcp-test-client.ts` to connect to the server via stdio -3. Sends MCP initialize and `tools/list` requests -4. Verifies the server responds with a valid tool list +3. Negotiates a protocol era with `mode: "auto"` — one `server/discover` probe, falling back to the `2025-11-25` `initialize` handshake +4. Lists tools, then calls each tool that declares an `outputSchema` and checks the structured result against it 5. Reports pass/fail ### Client Tests @@ -68,7 +82,9 @@ node tests/helpers/build/mcp-test-client.js python weather.py ### mock-mcp-server.ts -A minimal MCP server that verifies clients call the `tools/list` method and returns an empty tool list. Used to test clients without requiring a real weather server. Exits with an error if the client doesn't call `tools/list`. +A minimal MCP server that verifies clients call the `tools/list` method. Used to test clients without requiring a real weather server. Exits with an error if the client doesn't call `tools/list`. + +It advertises two tools whose output schemas cover both shapes a structured result can take: an object root, and an array root. The array-rooted one is deliberate — a client that compiles every declared `outputSchema` up front, as the Go and Rust quickstart clients do, will fail here if it assumes an output schema is always `{"type": "object"}`. **Usage**: diff --git a/tests/helpers/mcp-test-client.ts b/tests/helpers/mcp-test-client.ts index cf92b1cf..2903881a 100644 --- a/tests/helpers/mcp-test-client.ts +++ b/tests/helpers/mcp-test-client.ts @@ -1,40 +1,112 @@ #!/usr/bin/env node /** * Minimal MCP Test Client for testing servers - * Connects to a server, initializes, and lists tools + * + * Connects to a server, initializes, lists tools, and then checks the tools + * actually honour what they advertise: + * + * - every tool that declares an `outputSchema` is called, and the result must + * carry `structuredContent` (the SDK validates it against the schema for us + * and throws on a mismatch); + * - a tool whose `outputSchema` is array-rooted must answer with a top-level + * JSON array, not an object wrapping one. + * + * The second check is the point of the exercise. Listing tools alone would not + * notice a server that advertises `{"type": "array"}` and then returns + * `{"result": [...]}`. */ -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"; + +/** Arguments to call a tool with, chosen by matching its name. */ +const TOOL_ARGUMENTS: { match: RegExp; args: Record }[] = [ + { match: /alert/i, args: { state: "CA" } }, + { match: /forecast/i, args: { latitude: 38.5816, longitude: -121.4944 } }, +]; + +function argumentsFor(name: string): Record | undefined { + return TOOL_ARGUMENTS.find(({ match }) => match.test(name))?.args; +} + +/** The root `type` of a JSON Schema, when it declares a single one. */ +function rootType(schema: unknown): string | undefined { + if (typeof schema !== "object" || schema === null) return undefined; + const type = (schema as { type?: unknown }).type; + return typeof type === "string" ? type : undefined; +} async function testServer(command: string, args: string[]) { console.error(`Testing server: ${command} ${args.join(" ")}`); - const transport = new StdioClientTransport({ - command, - args, - }); + const transport = new StdioClientTransport({ command, args }); + // `auto` probes for 2026-07-28 and falls back to the 2025-11-25 handshake, so + // this one helper tests servers of either era. const client = new Client( - { - name: "mcp-test-client", - version: "1.0.0", - }, - { - capabilities: {}, - } + { name: "mcp-test-client", version: "1.0.0" }, + { capabilities: {}, versionNegotiation: { mode: "auto" } }, ); try { - // Connect to server await client.connect(transport); console.error("✓ Connected to server"); - // List tools const { tools } = await client.listTools(); console.error(`✓ Listed ${tools.length} tools`); - // Success + let checked = 0; + for (const tool of tools) { + if (!tool.outputSchema) continue; + + const toolArgs = argumentsFor(tool.name); + if (!toolArgs) { + console.error(` - ${tool.name}: no known arguments, skipping call`); + continue; + } + + // A throw here is the SDK rejecting the result against `outputSchema`. + const result = await client.callTool({ + name: tool.name, + arguments: toolArgs, + }); + + // Upstream (api.weather.gov) being unreachable surfaces as an error + // result, which is a legitimate answer and carries no structured data. + // Skip rather than fail the build on someone else's outage. + if (result.isError) { + console.error(` - ${tool.name}: returned an error result, skipping`); + continue; + } + + if (result.structuredContent === undefined) { + throw new Error( + `${tool.name} declares an output schema but returned no structuredContent`, + ); + } + + const expected = rootType(tool.outputSchema); + const isArray = Array.isArray(result.structuredContent); + + if (expected === "array" && !isArray) { + throw new Error( + `${tool.name} declares an array-rooted output schema but returned ` + + `${JSON.stringify(result.structuredContent).slice(0, 80)}`, + ); + } + if (expected === "object" && isArray) { + throw new Error( + `${tool.name} declares an object-rooted output schema but returned an array`, + ); + } + + console.error( + `✓ ${tool.name}: structuredContent is ${isArray ? "a top-level array" : "an object"}, matching its schema`, + ); + checked += 1; + } + + console.error(`✓ Verified structured output for ${checked} tools`); console.error("✓ Server test passed"); await client.close(); process.exit(0); diff --git a/tests/helpers/mock-mcp-server.ts b/tests/helpers/mock-mcp-server.ts index b8725158..0ab6cd73 100644 --- a/tests/helpers/mock-mcp-server.ts +++ b/tests/helpers/mock-mcp-server.ts @@ -1,40 +1,116 @@ #!/usr/bin/env node /** * Mock MCP Server for testing clients - * Verifies that clients call the tools/list method and returns an empty tool list + * + * Verifies that clients call `tools/list`, and advertises tools whose output + * schemas cover both shapes a structured result can take: an object root, and + * an array root (allowed as of protocol revision 2026-07-28). + * + * The array-rooted tool is deliberate. Clients that compile every declared + * `outputSchema` up front — as the Go and Rust quickstart clients do — will + * fail here if they assume an output schema is always `{"type": "object"}`. + * + * This uses the low-level `Server` rather than `McpServer` so the schemas are + * written out literally, which is the point of a mock: what goes on the wire is + * exactly what is in this file. */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import type { Tool } from "@modelcontextprotocol/server"; +import { Server } from "@modelcontextprotocol/server"; +import { serveStdio } from "@modelcontextprotocol/server/stdio"; -const server = new McpServer( +// Track whether tools/list was called +let toolsListCalled = false; + +const TOOLS: Tool[] = [ { - name: "mock-test-server", - version: "1.0.0", + name: "get_alerts", + description: "Mock alerts, returning a top-level array", + inputSchema: { + type: "object", + properties: { state: { type: "string" } }, + required: ["state"], + }, + // Array root: legal as of 2026-07-28, rejected by older revisions. + outputSchema: { + type: "array", + items: { + type: "object", + properties: { event: { type: "string" }, area: { type: "string" } }, + required: ["event", "area"], + }, + }, }, { - capabilities: { - tools: {}, + name: "get_forecast", + description: "Mock forecast, returning an object", + inputSchema: { + type: "object", + properties: { + latitude: { type: "number" }, + longitude: { type: "number" }, + }, + required: ["latitude", "longitude"], }, - } -); + outputSchema: { + type: "object", + properties: { + latitude: { type: "number" }, + longitude: { type: "number" }, + summary: { type: "string" }, + }, + required: ["latitude", "longitude", "summary"], + }, + }, +]; -// Track whether tools/list was called -let toolsListCalled = false; +function buildServer(): Server { + const server = new Server( + { name: "mock-test-server", version: "1.0.0" }, + { capabilities: { tools: {} } }, + ); -// Override the default tools/list handler to track calls -server.server.setRequestHandler(ListToolsRequestSchema, async () => { - toolsListCalled = true; - return { tools: [] }; -}); + server.setRequestHandler("tools/list", async () => { + toolsListCalled = true; + return { tools: TOOLS }; + }); + + server.setRequestHandler("tools/call", async (request) => { + const { name, arguments: args = {} } = request.params; + + if (name === "get_alerts") { + const state = String((args as { state?: unknown }).state ?? "??"); + const alerts = [{ event: "Mock Warning", area: state }]; + return { + content: [{ type: "text", text: `1 alert for ${state}` }], + structuredContent: alerts, + }; + } + + if (name === "get_forecast") { + const { latitude = 0, longitude = 0 } = args as { + latitude?: number; + longitude?: number; + }; + const forecast = { latitude, longitude, summary: "Mock conditions" }; + return { + content: [{ type: "text", text: forecast.summary }], + structuredContent: forecast, + }; + } -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Mock MCP Server running on stdio"); + return { + content: [{ type: "text", text: `Unknown tool: ${name}` }], + isError: true, + }; + }); + + return server; } +serveStdio(buildServer); +console.error("Mock MCP Server running on stdio"); + // Verify that tools/list was called when the connection closes process.stdin.on("end", () => { if (!toolsListCalled) { @@ -42,8 +118,3 @@ process.stdin.on("end", () => { process.exit(1); } }); - -main().catch((error) => { - console.error("Server error:", error); - process.exit(1); -}); diff --git a/tests/helpers/package-lock.json b/tests/helpers/package-lock.json index 69186917..a69f1f79 100644 --- a/tests/helpers/package-lock.json +++ b/tests/helpers/package-lock.json @@ -8,7 +8,9 @@ "name": "mcp-test-helpers", "version": "1.0.0", "dependencies": { - "@modelcontextprotocol/sdk": "^1.26.0" + "@modelcontextprotocol/client": "^2.0.0-beta.5", + "@modelcontextprotocol/server": "^2.0.0-beta.5", + "zod": "^4.4.3" }, "devDependencies": { "@types/node": "^22.13.4", @@ -18,240 +20,57 @@ "node": ">=16.0.0" } }, - "node_modules/@hono/node-server": { - "version": "1.19.13", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz", - "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.5.tgz", + "integrity": "sha512-YuuNm5f2TMoFQRje1UqVP8TJRjijCXMz4ckvoVpx1cUXuBEmykWQ2d8R536pek6UKcXT41T5nWc4qR1JFIbEmg==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", + "@modelcontextprotocol/core": "2.0.0-beta.5", "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" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@types/node": { - "version": "22.19.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", - "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "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/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "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": "https://github.com/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": ">=20" } }, - "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==", + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.5.tgz", + "integrity": "sha512-HKbY9XTbsDy1Y6r2I55TGE3JEapM0vg96e1MUmBIF9LGjos5gjhcIrTz1yvBPLg2aFKHjwhUAQfRdrCEnPxNew==", "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" + "zod": "^4.2.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=20" } }, - "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/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==", + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.5.tgz", + "integrity": "sha512-i1E5l75rQKsgY/AKAIspgMBH1vEL7dqiK7tHr0L+raYcb0SWOziqNGJXGIG6NY4AlXDWIKGJQGB7Nqfs3oUi5g==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "@modelcontextprotocol/core": "2.0.0-beta.5", + "zod": "^4.2.0" }, "engines": { - "node": ">= 0.4" + "node": ">=20" } }, - "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": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", - "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.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" + "undici-types": "~6.21.0" } }, "node_modules/cross-spawn": { @@ -268,106 +87,6 @@ "node": ">= 8" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "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/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/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.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "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", @@ -381,293 +100,14 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "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.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", - "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "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-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "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/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": "https://github.com/sponsors/ljharb" - } - }, - "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": "https://github.com/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/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": "https://github.com/sponsors/ljharb" - } - }, - "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": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.31", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz", - "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==", - "license": "MIT", - "engines": { - "node": ">=16.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/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "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/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.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "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", @@ -675,147 +115,14 @@ "license": "ISC" }, "node_modules/jose": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.4.tgz", + "integrity": "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } }, - "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/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": "https://github.com/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/ms": { - "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" - } - }, - "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": "https://github.com/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/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/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -825,159 +132,15 @@ "node": ">=8" } }, - "node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "license": "MIT", "engines": { "node": ">=16.20.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.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "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/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/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/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", @@ -999,127 +162,6 @@ "node": ">=8" } }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/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": "https://github.com/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": "https://github.com/sponsors/ljharb" - } - }, - "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/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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1141,24 +183,6 @@ "dev": true, "license": "MIT" }, - "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/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1174,29 +198,14 @@ "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/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25 || ^4" - } } } } diff --git a/tests/helpers/package.json b/tests/helpers/package.json index bc52b958..c797dd42 100644 --- a/tests/helpers/package.json +++ b/tests/helpers/package.json @@ -7,14 +7,16 @@ "build": "tsc", "clean": "rm -rf build" }, - "dependencies": { - "@modelcontextprotocol/sdk": "^1.26.0" - }, "devDependencies": { "@types/node": "^22.13.4", "typescript": "^5.7.3" }, "engines": { "node": ">=16.0.0" + }, + "dependencies": { + "@modelcontextprotocol/client": "^2.0.0-beta.5", + "@modelcontextprotocol/server": "^2.0.0-beta.5", + "zod": "^4.4.3" } } diff --git a/tests/smoke-test.sh b/tests/smoke-test.sh index e31908aa..fbce0341 100755 --- a/tests/smoke-test.sh +++ b/tests/smoke-test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash set -e # Source utilities @@ -38,53 +38,59 @@ run_test() { # Test: Python weather server test_weather_server_python() { - check_dependency uv + check_dependency uv || return 1 local server_dir="${PROJECT_ROOT}/weather-server-python" node "${TEST_CLIENT}" uv --directory "${server_dir}" run weather.py } # Test: TypeScript weather server test_weather_server_typescript() { - check_dependency node - check_dependency npm + check_dependency node || return 1 + check_dependency npm || return 1 local server_dir="${PROJECT_ROOT}/weather-server-typescript" - ensure_built "${server_dir}" + ensure_built "${server_dir}" || return 1 node "${TEST_CLIENT}" node "${server_dir}/build/index.js" } # Test: Rust weather server test_weather_server_rust() { - check_dependency cargo + check_dependency cargo || return 1 local server_dir="${PROJECT_ROOT}/weather-server-rust" - ensure_built "${server_dir}" + ensure_built "${server_dir}" || return 1 # Determine which binary to use - if [ -f "${server_dir}/target/release/weather" ]; then - local server_bin="${server_dir}/target/release/weather" - else - local server_bin="${server_dir}/target/debug/weather" - fi + local server_bin + server_bin=$(resolve_binary "${server_dir}/target/release/weather" \ + || resolve_binary "${server_dir}/target/debug/weather") || { + print_error "no weather binary found in ${server_dir}/target" + return 1 + } node "${TEST_CLIENT}" "${server_bin}" } # Test: Python MCP client test_mcp_client_python() { - check_dependency uv + check_dependency uv || return 1 local client_dir="${PROJECT_ROOT}/mcp-client-python" uv --directory "${client_dir}" run python "${client_dir}/client.py" "${MOCK_SERVER}" >/dev/null 2>&1 } # Test: TypeScript MCP client test_mcp_client_typescript() { - check_dependency node - check_dependency npm + check_dependency node || return 1 + check_dependency npm || return 1 local client_dir="${PROJECT_ROOT}/mcp-client-typescript" - ensure_built "${client_dir}" + ensure_built "${client_dir}" || return 1 node "${client_dir}/build/index.js" "${MOCK_SERVER}" >/dev/null 2>&1 } # Run all tests +# +# The Go examples and the Rust client are still uncovered. Each needs a change +# in its own directory before it can be tested here — the Go and Rust clients +# abort when no .env file is present, so they cannot be driven without +# credentials — so their coverage lands with those changes rather than here. print_header "Running smoke tests" run_test "weather-server-python" test_weather_server_python run_test "weather-server-typescript" test_weather_server_typescript diff --git a/tests/utils.sh b/tests/utils.sh index d3b2aa53..9e4a965b 100644 --- a/tests/utils.sh +++ b/tests/utils.sh @@ -30,12 +30,39 @@ command_exists() { command -v "$1" >/dev/null 2>&1 } -# Check dependency and exit if not found +# Check dependency and fail the calling test if not found. +# +# Returns rather than exits: a missing toolchain should fail its own test and +# let the rest of the suite run, not abort the whole script before the summary +# is printed. check_dependency() { local cmd=$1 if ! command_exists "${cmd}"; then print_error "${cmd} is not installed" - exit 1 + return 1 + fi +} + +# The executable suffix for this platform: ".exe" under MSYS/Git Bash, empty +# elsewhere. Windows will not run a binary without it, whatever the file is +# actually named. +exe_suffix() { + case "$(uname -s)" in + MINGW* | MSYS* | CYGWIN*) echo ".exe" ;; + *) echo "" ;; + esac +} + +# Echo the path to a built binary, accounting for the .exe suffix on Windows. +# Prints nothing and returns 1 when neither exists. +resolve_binary() { + local path=$1 + if [ -f "${path}$(exe_suffix)" ]; then + echo "${path}$(exe_suffix)" + elif [ -f "${path}" ]; then + echo "${path}" + else + return 1 fi } @@ -64,23 +91,46 @@ ensure_helpers_built() { fi } -# Ensure a project directory is built (TypeScript/Rust) +# Run a build step, showing its output only if it fails. +# +# Builds used to be run with `>/dev/null 2>&1` unconditionally, which hid +# compile errors: a failed build left no binary behind and the smoke test +# reported the downstream `spawn ... ENOENT` instead of the actual error. +run_build() { + local what=$1 + shift + local output + if ! output=$("$@" 2>&1); then + print_error "${what} failed" + echo "${output}" + return 1 + fi +} + +# Ensure a project directory is built (TypeScript/Rust/Go) ensure_built() { local dir=$1 cd "${dir}" || exit 1 # Install npm dependencies if needed if [ -f "package.json" ] && [ ! -d "node_modules" ]; then - npm install >/dev/null 2>&1 + run_build "npm install in ${dir}" npm install || return 1 fi # Build TypeScript if needed if [ -f "tsconfig.json" ] && [ ! -f "build/index.js" ]; then - npm run build >/dev/null 2>&1 + run_build "npm run build in ${dir}" npm run build || return 1 fi # Build Rust if needed - if [ -f "Cargo.toml" ] && [ ! -f "target/release/weather" ] && [ ! -f "target/debug/weather" ]; then - cargo build --release >/dev/null 2>&1 + if [ -f "Cargo.toml" ] \ + && ! resolve_binary "target/release/weather" >/dev/null \ + && ! resolve_binary "target/debug/weather" >/dev/null; then + run_build "cargo build in ${dir}" cargo build --release || return 1 + fi + + # Build Go if needed + if [ -f "go.mod" ] && ! resolve_binary "server" >/dev/null; then + run_build "go build in ${dir}" go build -o "server$(exe_suffix)" . || return 1 fi } From 1c8fb934696d5d63fde9b954c78b8c3c1a03c6f5 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 1 Aug 2026 11:07:47 -0700 Subject: [PATCH 2/3] Update to the stable 2.0 SDK packages and address review feedback @modelcontextprotocol/{client,server} 2.0.0 shipped on 2026-07-27, so the helpers move off 2.0.0-beta.5. Review fixes: - strip the UTF-8 BOM that preceded smoke-test.sh's shebang - ensure_built and ensure_helpers_built return rather than exit, so one project failing to build no longer aborts the suite before the summary - engines.node and the documented Node requirement go to 20, matching what the 2.0 SDK packages actually require - close the client on the failure path, so a failing test cannot leave the spawned server running - the README claimed Go and Rust client coverage that did not exist Adds the Go weather server to the smoke test, with the Go toolchain CI was missing. The Go and Rust clients stay uncovered: on main both abort without a .env, so credential-free startup has to land in their own directories. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 5 +++++ .gitignore | 8 ++++++++ tests/README.md | 8 ++++---- tests/helpers/mcp-test-client.ts | 3 +++ tests/helpers/package-lock.json | 28 ++++++++++++++-------------- tests/helpers/package.json | 6 +++--- tests/smoke-test.sh | 24 ++++++++++++++++++++---- tests/utils.sh | 10 +++++++--- 8 files changed, 64 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6e7ab6f..dd883ef6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,5 +36,10 @@ jobs: with: toolchain: stable + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25" + - name: Run smoke tests run: ./tests/smoke-test.sh diff --git a/.gitignore b/.gitignore index b013594d..8b2d761f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,14 @@ Gemfile.lock # Environment variables file .env +# Go-generated binaries +# `smoke-test.sh` builds the Go examples as `server`; `go build` with no -o +# names the binary after its directory. +*.exe +/weather-server-go/server +/weather-server-go/weather-server-go +/mcp-client-go/mcp-client-go + # Rust-generated files debug target diff --git a/tests/README.md b/tests/README.md index d917db66..5518b73e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,9 +7,9 @@ This directory contains smoke tests for the MCP quickstart examples. These tests The smoke tests verify: - **Servers**: Each weather server (Python, TypeScript, Rust, Go) can start, respond to MCP protocol requests, and honour the output schemas it advertises -- **Clients**: Each MCP client (Python, TypeScript, Go, Rust) can connect to a mock server and list tools +- **Clients**: The Python and TypeScript MCP clients can connect to a mock server and list tools -The Ruby examples are not covered: the `mcp` gem cannot negotiate protocol revision `2026-07-28`. +The Go and Rust clients are not covered here: on `main` both abort when no `.env` file is present, so they cannot be driven without credentials. Making them start credential-free is a change in their own directories, so their coverage lands with those changes rather than here. The Ruby examples are not covered either — the `mcp` gem cannot negotiate protocol revision `2026-07-28`. ## Structured output @@ -30,7 +30,7 @@ Tool calls reach the live NWS API. When it is unreachable the tools return an er ## Requirements -- **Node.js** 16+ +- **Node.js** 20+ (required by the 2.0 MCP SDK packages) - **npm** (for Node.js dependencies) - **Python** 3.10+ - **uv** (Python package manager) @@ -107,7 +107,7 @@ Install required dependencies: curl -LsSf https://astral.sh/uv/install.sh | sh # Node.js (via nvm) -nvm install 18 +nvm install 24 # Rust curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh diff --git a/tests/helpers/mcp-test-client.ts b/tests/helpers/mcp-test-client.ts index 2903881a..4f9a8b49 100644 --- a/tests/helpers/mcp-test-client.ts +++ b/tests/helpers/mcp-test-client.ts @@ -112,6 +112,9 @@ async function testServer(command: string, args: string[]) { process.exit(0); } catch (error) { console.error(`✗ Server test failed: ${error}`); + // Close on the way out too, or the spawned server outlives this process and + // a failing test hangs instead of reporting. + await client.close().catch(() => {}); process.exit(1); } } diff --git a/tests/helpers/package-lock.json b/tests/helpers/package-lock.json index a69f1f79..53ad2a45 100644 --- a/tests/helpers/package-lock.json +++ b/tests/helpers/package-lock.json @@ -8,8 +8,8 @@ "name": "mcp-test-helpers", "version": "1.0.0", "dependencies": { - "@modelcontextprotocol/client": "^2.0.0-beta.5", - "@modelcontextprotocol/server": "^2.0.0-beta.5", + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "zod": "^4.4.3" }, "devDependencies": { @@ -17,16 +17,16 @@ "typescript": "^5.7.3" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } }, "node_modules/@modelcontextprotocol/client": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.5.tgz", - "integrity": "sha512-YuuNm5f2TMoFQRje1UqVP8TJRjijCXMz4ckvoVpx1cUXuBEmykWQ2d8R536pek6UKcXT41T5nWc4qR1JFIbEmg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/core": "2.0.0-beta.5", + "@modelcontextprotocol/core": "2.0.0", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", @@ -39,9 +39,9 @@ } }, "node_modules/@modelcontextprotocol/core": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.5.tgz", - "integrity": "sha512-HKbY9XTbsDy1Y6r2I55TGE3JEapM0vg96e1MUmBIF9LGjos5gjhcIrTz1yvBPLg2aFKHjwhUAQfRdrCEnPxNew==", + "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" @@ -51,12 +51,12 @@ } }, "node_modules/@modelcontextprotocol/server": { - "version": "2.0.0-beta.5", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.5.tgz", - "integrity": "sha512-i1E5l75rQKsgY/AKAIspgMBH1vEL7dqiK7tHr0L+raYcb0SWOziqNGJXGIG6NY4AlXDWIKGJQGB7Nqfs3oUi5g==", + "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-beta.5", + "@modelcontextprotocol/core": "2.0.0", "zod": "^4.2.0" }, "engines": { diff --git a/tests/helpers/package.json b/tests/helpers/package.json index c797dd42..e1c5598b 100644 --- a/tests/helpers/package.json +++ b/tests/helpers/package.json @@ -12,11 +12,11 @@ "typescript": "^5.7.3" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" }, "dependencies": { - "@modelcontextprotocol/client": "^2.0.0-beta.5", - "@modelcontextprotocol/server": "^2.0.0-beta.5", + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "zod": "^4.4.3" } } diff --git a/tests/smoke-test.sh b/tests/smoke-test.sh index fbce0341..acb039b5 100755 --- a/tests/smoke-test.sh +++ b/tests/smoke-test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash set -e # Source utilities @@ -69,6 +69,21 @@ test_weather_server_rust() { node "${TEST_CLIENT}" "${server_bin}" } +# Test: Go weather server +test_weather_server_go() { + check_dependency go || return 1 + local server_dir="${PROJECT_ROOT}/weather-server-go" + ensure_built "${server_dir}" || return 1 + + local server_bin + server_bin=$(resolve_binary "${server_dir}/server") || { + print_error "no server binary found in ${server_dir}" + return 1 + } + + node "${TEST_CLIENT}" "${server_bin}" +} + # Test: Python MCP client test_mcp_client_python() { check_dependency uv || return 1 @@ -87,14 +102,15 @@ test_mcp_client_typescript() { # Run all tests # -# The Go examples and the Rust client are still uncovered. Each needs a change -# in its own directory before it can be tested here — the Go and Rust clients +# All four servers are covered. The Go and Rust clients are not: on main both # abort when no .env file is present, so they cannot be driven without -# credentials — so their coverage lands with those changes rather than here. +# credentials. Making them start credential-free is a change in their own +# directories, so their coverage lands with those changes rather than here. print_header "Running smoke tests" run_test "weather-server-python" test_weather_server_python run_test "weather-server-typescript" test_weather_server_typescript run_test "weather-server-rust" test_weather_server_rust +run_test "weather-server-go" test_weather_server_go run_test "mcp-client-python" test_mcp_client_python run_test "mcp-client-typescript" test_mcp_client_typescript diff --git a/tests/utils.sh b/tests/utils.sh index 9e4a965b..7423eae7 100644 --- a/tests/utils.sh +++ b/tests/utils.sh @@ -83,10 +83,10 @@ ensure_helpers_built() { if [ ! -f "${TEST_CLIENT}" ] || [ ! -f "${MOCK_SERVER}" ]; then print_error "Test helpers not built" print_header "Building test helpers..." - cd "${PROJECT_ROOT}/tests/helpers" || exit 1 + cd "${PROJECT_ROOT}/tests/helpers" || return 1 npm install >/dev/null 2>&1 npm run build >/dev/null 2>&1 - cd - >/dev/null || exit 1 + cd - >/dev/null || return 1 print_success "Test helpers built" fi } @@ -108,9 +108,13 @@ run_build() { } # Ensure a project directory is built (TypeScript/Rust/Go) +# +# Returns rather than exits, for the same reason check_dependency does: one +# project failing to build should fail its own test, not abort the suite +# before the summary is printed. ensure_built() { local dir=$1 - cd "${dir}" || exit 1 + cd "${dir}" || return 1 # Install npm dependencies if needed if [ -f "package.json" ] && [ ! -d "node_modules" ]; then From 8043f04dda7d9b1f1d086a17dbffcf114210f649 Mon Sep 17 00:00:00 2001 From: olaservo Date: Sat, 1 Aug 2026 21:30:19 -0700 Subject: [PATCH 3/3] Say "structured content", not "structured output" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spec never uses the phrase "structured output". It defines two separate things under Tool Result: "Structured Content" (the `structuredContent` field) and "Output Schema" (the `outputSchema` field). Collapsing them into "structured output" conflates the two. It is also actively confusing here. In LLM tooling "structured output" means constrained decoding — making the *model* emit conforming JSON. These clients call a model API, so a reader could reasonably take the phrase to mean the tool constrains the model's response, which is the opposite of what is going on: the tool describes the shape of its own result. Headings and prose now say "structured content". References to real identifiers are left alone: the Python SDK's own docs page is called Structured Output and lives at docs/servers/structured-output.md, and its decorator parameter is `structured_output`. Co-Authored-By: Claude Opus 5 --- tests/README.md | 2 +- tests/helpers/mcp-test-client.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index 5518b73e..1a769729 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,7 +11,7 @@ The smoke tests verify: The Go and Rust clients are not covered here: on `main` both abort when no `.env` file is present, so they cannot be driven without credentials. Making them start credential-free is a change in their own directories, so their coverage lands with those changes rather than here. The Ruby examples are not covered either — the `mcp` gem cannot negotiate protocol revision `2026-07-28`. -## Structured output +## Structured content Listing tools is not enough to catch a broken structured result, so each server test also **calls** every tool that declares an `outputSchema` and checks the answer: diff --git a/tests/helpers/mcp-test-client.ts b/tests/helpers/mcp-test-client.ts index 4f9a8b49..aeede7fa 100644 --- a/tests/helpers/mcp-test-client.ts +++ b/tests/helpers/mcp-test-client.ts @@ -106,7 +106,7 @@ async function testServer(command: string, args: string[]) { checked += 1; } - console.error(`✓ Verified structured output for ${checked} tools`); + console.error(`✓ Verified structured content for ${checked} tools`); console.error("✓ Server test passed"); await client.close(); process.exit(0);