From a74b30a0e8aab7173adcca2a8b38fb5064811d31 Mon Sep 17 00:00:00 2001 From: Jungwon Sohn Date: Wed, 29 Jul 2026 11:15:28 +0900 Subject: [PATCH] release: promote Swagger Petstore examples (#20) * ci: require verified dev-to-main promotions * fix: pin examples MCP SDK to audited graph (#3) Co-authored-by: sjungwon03 <> * fix(deps): adopt TypeMCP 0.2.2 remediation (#7) * chore: reconcile main release history into dev (#11) * release: publish verified TypeChain and TypeMCP examples (#1) Co-authored-by: sjungwon03 <> * release: promote audited MCP SDK graph to production (#5) * ci: require verified dev-to-main promotions * fix: pin examples MCP SDK to audited graph (#3) Co-authored-by: sjungwon03 <> --------- Co-authored-by: sjungwon03 <> * fix: retain published TypeMCP dependency contract * fix: remove obsolete reconciliation override * feat: add read-only Swagger Petstore agent examples (#16) * docs(planning): add Swagger Petstore agent plan * feat: add read-only Swagger Petstore client * feat: wrap read-only Petstore API in TypeMCP * feat: add TypeChain Petstore agent workflow * feat: add live Swagger Petstore demonstrations --------- Co-authored-by: sjungwon03 <> --- README.md | 30 ++++- .../2026-07-29-swagger-petstore-agent.md | 90 ++++++++++++++ examples/petstore-client.ts | 113 ++++++++++++++++++ examples/petstore-fixture.ts | 37 ++++++ examples/petstore-live.ts | 30 +++++ examples/typechain-petstore-agent-fixture.ts | 14 +++ examples/typechain-petstore-agent-live.ts | 22 ++++ examples/typechain-petstore-agent.ts | 67 +++++++++++ examples/typemcp-petstore-fixture.ts | 33 +++++ examples/typemcp-petstore-server.ts | 87 ++++++++++++++ package.json | 6 +- test/petstore-client.test.ts | 112 +++++++++++++++++ test/typechain-petstore-agent.test.ts | 16 +++ test/typemcp-petstore-server.test.ts | 16 +++ 14 files changed, 668 insertions(+), 5 deletions(-) create mode 100644 docs/planning/2026-07-29-swagger-petstore-agent.md create mode 100644 examples/petstore-client.ts create mode 100644 examples/petstore-fixture.ts create mode 100644 examples/petstore-live.ts create mode 100644 examples/typechain-petstore-agent-fixture.ts create mode 100644 examples/typechain-petstore-agent-live.ts create mode 100644 examples/typechain-petstore-agent.ts create mode 100644 examples/typemcp-petstore-fixture.ts create mode 100644 examples/typemcp-petstore-server.ts create mode 100644 test/petstore-client.test.ts create mode 100644 test/typechain-petstore-agent.test.ts create mode 100644 test/typemcp-petstore-server.test.ts diff --git a/README.md b/README.md index 9bc6131..be90134 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,32 @@ npm run check | Example | Run | Demonstrates | | --- | --- | --- | -| [TypeChain tool definition](examples/typechain-tool-definition.ts) | `npm run example:typechain` | `@Tool()` metadata and direct receiver-bound invocation. | -| [TypeChain policy guard](examples/typechain-policy-guard.ts) | `npm run example:policy` | `@Policy()` declaration plus an application-owned approval/audit decision. | -| [TypeMCP server definition](examples/typemcp-server-definition.ts) | `npm run example:typemcp` | `@McpServer()`, `@McpTool()`, metadata inspection, and explicit compilation. | -| [TypeMCP → TypeChain bridge](examples/typemcp-langchain-bridge.ts) | `npm run example:bridge` | Adapt a TypeMCP server into in-process LangChain-compatible tools through TypeChain. | +| [Swagger Petstore TypeMCP wrapper](examples/typemcp-petstore-server.ts) | `npm run example:petstore:live` | Wrap public Swagger Petstore **read-only** operations in `@McpServer()` / `@McpTool()` declarations. | +| [Swagger Petstore TypeChain workflow](examples/typechain-petstore-agent.ts) | `npm run example:petstore:agent` | Adapt the Petstore TypeMCP tool into an in-process TypeChain/LangChain-compatible tool and deterministically summarize live available pets. | +| [TypeChain tool definition](examples/typechain-tool-definition.ts) | `npm run example:typechain` | Foundational `@Tool()` metadata and direct receiver-bound invocation. | +| [TypeChain policy guard](examples/typechain-policy-guard.ts) | `npm run example:policy` | Foundational `@Policy()` declaration plus an application-owned approval/audit decision. | +| [TypeMCP server definition](examples/typemcp-server-definition.ts) | `npm run example:typemcp` | Minimal decorator and explicit compilation reference. | +| [TypeMCP → TypeChain bridge](examples/typemcp-langchain-bridge.ts) | `npm run example:bridge` | Minimal in-process adapter reference. | + +## Swagger Petstore scenario + +The primary scenario uses the public [Swagger Petstore v2](https://petstore.swagger.io/) demo API at the fixed base URL `https://petstore.swagger.io/v2`. + +```bash +# Real public GET calls: available pets, one pet detail, and inventory. +npm run example:petstore:live + +# Real TypeChain → adapted TypeMCP tool call followed by a deterministic summary. +npm run example:petstore:agent + +# Deterministic fixtures used by unit tests; no network request. +npm run example:petstore:typemcp:fixture +npm run example:petstore:agent:fixture +``` + +The TypeMCP server exposes exactly three tools: `search_available_pets`, `get_pet`, and `get_petstore_inventory`. The client fixes the public HTTPS base URL, sends only `GET` requests, applies a timeout, validates JSON response shapes, and contains no API-key, credential, create, update, or delete operation. + +The public Petstore service is demo infrastructure: records, status counts, and availability can change or be unavailable. Consequently, CI and unit tests use injected fixtures, while the two `:live` commands are intentional manual smoke demonstrations. The TypeChain workflow deterministically selects and calls the adapted `search_available_pets` tool; it is **not** an LLM-driven agent and it does not create an MCP client/session or host an MCP transport. ## Boundaries that the examples intentionally preserve diff --git a/docs/planning/2026-07-29-swagger-petstore-agent.md b/docs/planning/2026-07-29-swagger-petstore-agent.md new file mode 100644 index 0000000..b95d17c --- /dev/null +++ b/docs/planning/2026-07-29-swagger-petstore-agent.md @@ -0,0 +1,90 @@ +# Swagger Petstore Agent Examples Implementation Plan + +> **For Hermes:** Implement this issue-scoped plan with test-first slices and preserve the `dev` → release-only `main` workflow. + +**Goal:** Replace the local catalog examples with read-only Swagger Petstore examples that wrap live Petstore GET operations in TypeMCP and show a TypeChain-adapted, deterministic agent-style summary. + +**Architecture:** A small injected-fetch `PetstoreClient` owns the fixed public v2 base URL, timeout, GET-only requests, and JSON shape validation. `PetstoreServer` receives that client through TypeMCP’s explicit resolver and exposes `search_available_pets`, `get_pet`, and `get_petstore_inventory`; TypeChain adapts only the search tool into a deterministic summary flow. Unit tests inject fixture fetch responses, while a separate `example:petstore:live` command uses Node’s real `fetch` against the public demo API. + +**Tech Stack:** TypeScript strict mode, Node 20+ fetch/AbortSignal, Zod, Vitest, `@theorvane/type-mcp`, TypeChain’s TypeMCP bridge. + +--- + +## Scope and boundaries + +- Fixed endpoint: `https://petstore.swagger.io/v2`. +- Allowed operations: `GET /pet/findByStatus?status=available`, `GET /pet/{petId}`, and `GET /store/inventory`. +- The live executable can only read public demo data; it has no write endpoint, API key support, user-supplied base URL, MCP stdio/HTTP hosting, or model-provider integration. +- Deterministic tests must not access the network. +- Live demo output must declare that public demo contents change between invocations. + +## Task 1: Add the Petstore client contract and fixture tests + +**Files:** +- Create: `examples/petstore-client.ts` +- Create: `test/petstore-client.test.ts` + +1. Write failing fixture-fetch tests for available-pet query encoding, `getPet`, inventory normalization, non-OK response error, malformed JSON payload rejection, and timeout/abort behavior. +2. Run `npm test -- --run test/petstore-client.test.ts`; expect missing-module failure. +3. Implement `PetstoreClient` with injected `fetch`, a `readonly` fixed base URL, GET-only request helper, finite positive timeout validation, `AbortSignal.timeout`, response status guard, and Zod schemas. +4. Run the focused test until green; commit the slice. + +## Task 2: Add TypeMCP’s Petstore wrapper + +**Files:** +- Create: `examples/typemcp-petstore-server.ts` +- Create: `test/typemcp-petstore-server.test.ts` + +1. Write failing tests proving declaration metadata contains exactly `search_available_pets`, `get_pet`, and `get_petstore_inventory`, and that injected fixture client calls return normalized Petstore data. +2. Run focused test; expect missing module/decorator implementation failure. +3. Implement `@McpServer({ name: "swagger-petstore", version: "1.0.0" })` and three `@McpTool` methods with explicit Zod inputs. Inject `PetstoreClient` through the constructor and use an explicit resolver for compilation. +4. Verify focused tests and commit the slice. + +## Task 3: Add the TypeChain agent-style bridge + +**Files:** +- Create: `examples/typechain-petstore-agent.ts` +- Create: `test/typechain-petstore-agent.test.ts` + +1. Write a failing deterministic test that creates a server with a fixture client, bridges its TypeMCP search tool with `createTypeMcpLangChainTools`, invokes it, and returns a concise summary containing current available-pet details. +2. Run the focused test and confirm it fails because the agent module does not exist. +3. Implement `summarizeAvailablePets` with an explicit server resolver, tool lookup by its TypeMCP declaration name, input validation delegated to the adapted tool, and a plain deterministic summary—not LLM inference. +4. Run focused and full tests, then commit. + +## Task 4: Provide live read-only executable commands + +**Files:** +- Create: `examples/petstore-live.ts` +- Modify: `package.json` +- Modify: `test/run-example.ts` only if the current generic runner needs a deterministic fixture mode. + +1. Write a failing test for a separate deterministic fixture-mode runner where necessary; do not put live network calls in normal unit tests. +2. Add `example:petstore:live` to call the real client and print a compact JSON payload containing `source`, `readOnly`, selected available pets, one resolved pet detail when an ID exists, and normalized inventory counts. +3. Add `example:petstore:agent` to execute the live TypeChain/TypeMCP path and emit its summary. +4. Run both live commands manually after tests pass. A transient upstream error must be surfaced honestly rather than simulated as success. +5. Commit this slice. + +## Task 5: Update developer documentation and validate release quality + +**Files:** +- Modify: `README.md` +- Modify: `.github/workflows/verify.yml` only if it needs fixture-only commands added; never add public-network calls to CI. + +1. Document the Petstore TypeMCP wrapper, TypeChain bridge, live commands, fixed endpoint, public-demo volatility, read-only policy, and no-model-provider boundary. +2. Preserve existing local catalog examples only if still pedagogically distinct; otherwise remove stale commands/tests/docs in the same change. +3. Run: `npm run format`, `npm run lint`, `npm run build`, `npm test`, `npm run audit:prod`, `npm run check`, `git diff --check`. +4. Run both explicit live commands and capture actual output only in verification notes/PR, never as hardcoded test data. +5. Commit, push, open a `dev` PR, obtain exact-head review/CI, then use the established reviewed `dev → main` promotion path. + +## Acceptance cases + +| Case | Expected behavior | +| --- | --- | +| Available pet lookup | Client encodes `status=available`; TypeMCP tool returns validated Petstore records. | +| Pet detail | Tool takes a positive numeric ID and returns validated normalized detail. | +| Inventory | Tool returns a validated string-to-nonnegative-number map. | +| Untrusted API response | Invalid JSON shape and non-2xx status fail with descriptive error. | +| Timeout | Slow fetch is aborted; no hidden retry/write action is attempted. | +| Agent flow | TypeChain calls the TypeMCP-adapted search tool and returns a deterministic human-readable live-data summary. | +| Offline CI | All unit tests use fixture fetch; CI performs no Petstore request. | +| Safety | No write method, credential field, configurable base URL, hosted MCP transport, or model-provider behavior is introduced. | diff --git a/examples/petstore-client.ts b/examples/petstore-client.ts new file mode 100644 index 0000000..42c166b --- /dev/null +++ b/examples/petstore-client.ts @@ -0,0 +1,113 @@ +import { z } from "zod"; + +const PETSTORE_BASE_URL = "https://petstore.swagger.io/v2"; + +const rawPetSchema = z.object({ + id: z.number().int().nonnegative(), + name: z.string(), + photoUrls: z.array(z.string()).optional(), + status: z.string().optional(), + category: z + .object({ + name: z.string(), + }) + .optional(), +}); + +const inventorySchema = z.record(z.string(), z.number().finite().nonnegative()); + +export type FetchLike = (input: string, init: RequestInit) => Promise; + +export type PetstorePet = Readonly<{ + id: number; + name: string; + status: string | undefined; + category?: string; +}>; + +export type PetstoreClientOptions = Readonly<{ + fetch: FetchLike; + timeoutMs: number; +}>; + +export class PetstoreClient { + readonly #fetch: FetchLike; + readonly #timeoutMs: number; + + public constructor(options: PetstoreClientOptions) { + if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) { + throw new Error("timeoutMs must be a positive finite number"); + } + this.#fetch = options.fetch; + this.#timeoutMs = options.timeoutMs; + } + + public async findAvailablePets(): Promise { + const payload = await this.#getJson("/pet/findByStatus?status=available"); + const arrayPayload = z.array(z.unknown()).safeParse(payload); + if (!arrayPayload.success) { + throw new Error( + "Swagger Petstore returned an invalid available-pet payload", + ); + } + const pets = arrayPayload.data + .map((candidate) => rawPetSchema.safeParse(candidate)) + .flatMap((candidate) => + candidate.success ? [normalizePet(candidate.data)] : [], + ); + if (pets.length === 0) { + throw new Error( + "Swagger Petstore returned no valid available-pet records", + ); + } + return pets; + } + + public async getPet(petId: number): Promise { + if (!Number.isInteger(petId) || petId <= 0) { + throw new Error("petId must be a positive integer"); + } + const payload = await this.#getJson(`/pet/${petId}`); + const parsed = rawPetSchema.safeParse(payload); + if (!parsed.success) { + throw new Error("Swagger Petstore returned an invalid pet payload"); + } + return normalizePet(parsed.data); + } + + public async getInventory(): Promise>> { + const payload = await this.#getJson("/store/inventory"); + const parsed = inventorySchema.safeParse(payload); + if (!parsed.success) { + throw new Error("Swagger Petstore returned an invalid inventory payload"); + } + return parsed.data; + } + + async #getJson(path: string): Promise { + const response = await this.#fetch(`${PETSTORE_BASE_URL}${path}`, { + method: "GET", + headers: { accept: "application/json" }, + signal: AbortSignal.timeout(this.#timeoutMs), + }); + if (!response.ok) { + throw new Error( + `Swagger Petstore GET ${path.split("?")[0]} failed with HTTP ${response.status}`, + ); + } + return response.json(); + } +} + +function normalizePet(pet: z.infer): PetstorePet { + return { + id: pet.id, + name: pet.name, + status: pet.status, + ...(pet.category === undefined ? {} : { category: pet.category.name }), + }; +} + +export function createLivePetstoreClient(timeoutMs = 10_000): PetstoreClient { + return new PetstoreClient({ fetch: globalThis.fetch, timeoutMs }); +} diff --git a/examples/petstore-fixture.ts b/examples/petstore-fixture.ts new file mode 100644 index 0000000..0364a16 --- /dev/null +++ b/examples/petstore-fixture.ts @@ -0,0 +1,37 @@ +import { type FetchLike, PetstoreClient } from "./petstore-client.js"; +import { PetstoreServer } from "./typemcp-petstore-server.js"; + +function jsonResponse(body: unknown): Response { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +export function createFixturePetstoreServer(): PetstoreServer { + let calls = 0; + const fetch: FetchLike = async () => { + calls += 1; + switch (calls) { + case 1: + return jsonResponse([ + { id: 1, name: "Milo", photoUrls: [], status: "available" }, + { id: 2, name: "Nori", photoUrls: [], status: "available" }, + ]); + case 2: + return jsonResponse({ + id: 2, + name: "Nori", + photoUrls: [], + status: "pending", + }); + case 3: + return jsonResponse({ available: 2, sold: 1 }); + default: + throw new Error(`Unexpected fixture request ${calls}`); + } + }; + return PetstoreServer.withClient( + new PetstoreClient({ fetch, timeoutMs: 1_000 }), + ); +} diff --git a/examples/petstore-live.ts b/examples/petstore-live.ts new file mode 100644 index 0000000..88d582c --- /dev/null +++ b/examples/petstore-live.ts @@ -0,0 +1,30 @@ +import { PetstoreServer } from "./typemcp-petstore-server.js"; + +export async function run(): Promise { + const server = new PetstoreServer(); + const available = await server.searchAvailablePets({ limit: 3 }); + const firstPet = available[0]; + const pet = firstPet + ? await server.getPet({ petId: firstPet.id }) + : undefined; + const inventory = await server.getPetstoreInventory({}); + + console.log( + JSON.stringify( + { + source: "https://petstore.swagger.io/v2", + readOnly: true, + available, + pet, + inventory, + note: "Swagger Petstore is a public demo API; live data can change between runs.", + }, + null, + 2, + ), + ); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await run(); +} diff --git a/examples/typechain-petstore-agent-fixture.ts b/examples/typechain-petstore-agent-fixture.ts new file mode 100644 index 0000000..97a7430 --- /dev/null +++ b/examples/typechain-petstore-agent-fixture.ts @@ -0,0 +1,14 @@ +import { createFixturePetstoreServer } from "./petstore-fixture.js"; +import { summarizeAvailablePets } from "./typechain-petstore-agent.js"; + +export async function run(): Promise { + const summary = await summarizeAvailablePets( + createFixturePetstoreServer(), + 2, + ); + console.log(JSON.stringify(summary, null, 2)); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await run(); +} diff --git a/examples/typechain-petstore-agent-live.ts b/examples/typechain-petstore-agent-live.ts new file mode 100644 index 0000000..4138fdf --- /dev/null +++ b/examples/typechain-petstore-agent-live.ts @@ -0,0 +1,22 @@ +import { summarizeAvailablePets } from "./typechain-petstore-agent.js"; +import { PetstoreServer } from "./typemcp-petstore-server.js"; + +export async function run(): Promise { + const result = await summarizeAvailablePets(new PetstoreServer(), 3); + console.log( + JSON.stringify( + { + source: "https://petstore.swagger.io/v2", + readOnly: true, + ...result, + note: "This deterministic workflow invokes a TypeMCP-derived TypeChain tool; it does not configure an LLM or MCP transport.", + }, + null, + 2, + ), + ); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await run(); +} diff --git a/examples/typechain-petstore-agent.ts b/examples/typechain-petstore-agent.ts new file mode 100644 index 0000000..100e868 --- /dev/null +++ b/examples/typechain-petstore-agent.ts @@ -0,0 +1,67 @@ +import { createTypeMcpLangChainTools } from "@theorvane/type-chain/typemcp"; +import { z } from "zod"; + +import type { PetstorePet } from "./petstore-client.js"; +import { PetstoreServer } from "./typemcp-petstore-server.js"; + +const adaptedPetSchema = z.object({ + id: z.number().int().nonnegative(), + name: z.string(), + status: z.string().optional(), + category: z.string().optional(), +}); + +export type PetstoreAgentSummary = Readonly<{ + tool: "search_available_pets"; + pets: readonly PetstorePet[]; + summary: string; +}>; + +/** + * A deterministic agent-style workflow: it selects and invokes a TypeMCP tool + * after TypeChain adapts it to LangChain's tool interface. No model provider + * or MCP transport is created here. + */ +export async function summarizeAvailablePets( + server: PetstoreServer, + limit = 3, +): Promise { + const tools = await createTypeMcpLangChainTools(PetstoreServer, { + resolver: { resolve: () => server }, + }); + const search = tools.find((tool) => tool.name === "search_available_pets"); + if (!search) { + throw new Error( + "TypeChain did not adapt the search_available_pets MCP tool", + ); + } + + const rawResult = await search.invoke({ limit }); + if (typeof rawResult !== "string") { + throw new Error( + "TypeChain returned an unexpected non-text Petstore tool result", + ); + } + const parsed = z + .array(adaptedPetSchema) + .safeParse(JSON.parse(rawResult) as unknown); + if (!parsed.success) { + throw new Error("TypeChain returned an invalid Petstore tool result"); + } + const pets: readonly PetstorePet[] = parsed.data.map((pet) => ({ + id: pet.id, + name: pet.name, + status: pet.status, + ...(pet.category === undefined ? {} : { category: pet.category }), + })); + const names = pets.map((pet) => `${pet.name} (#${pet.id})`).join(", "); + + return { + tool: "search_available_pets", + pets, + summary: + pets.length === 0 + ? "No available pets were returned." + : `Available pets: ${names}.`, + }; +} diff --git a/examples/typemcp-petstore-fixture.ts b/examples/typemcp-petstore-fixture.ts new file mode 100644 index 0000000..6600a50 --- /dev/null +++ b/examples/typemcp-petstore-fixture.ts @@ -0,0 +1,33 @@ +import { createFixturePetstoreServer } from "./petstore-fixture.js"; +import { + compilePetstoreServer, + describePetstoreServer, +} from "./typemcp-petstore-server.js"; + +export async function run(): Promise { + const server = createFixturePetstoreServer(); + const definition = describePetstoreServer(); + const available = await server.searchAvailablePets({ limit: 1 }); + const pet = await server.getPet({ petId: 2 }); + const inventory = await server.getPetstoreInventory({}); + const compiled = await compilePetstoreServer(); + + console.log( + JSON.stringify( + { + name: definition.name, + tools: definition.tools.map((tool) => tool.name), + available, + pet, + inventory, + compiled: Boolean(compiled), + }, + null, + 2, + ), + ); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await run(); +} diff --git a/examples/typemcp-petstore-server.ts b/examples/typemcp-petstore-server.ts new file mode 100644 index 0000000..687d3da --- /dev/null +++ b/examples/typemcp-petstore-server.ts @@ -0,0 +1,87 @@ +import { + createMcpServer, + getMcpServerDefinition, + McpServer, + McpTool, +} from "@theorvane/type-mcp"; +import { z } from "zod"; + +import { + createLivePetstoreClient, + type PetstoreClient, + type PetstorePet, +} from "./petstore-client.js"; + +const listInput = z.object({ limit: z.number().int().min(1).max(10) }); +const petInput = z.object({ petId: z.number().int().positive() }); +const emptyInput = z.object({}); + +const clients = new WeakMap(); + +function clientFor(server: PetstoreServer): PetstoreClient { + const client = clients.get(server); + if (client) { + return client; + } + const liveClient = createLivePetstoreClient(); + clients.set(server, liveClient); + return liveClient; +} + +@McpServer({ name: "swagger-petstore", version: "1.0.0" }) +export class PetstoreServer { + public static withClient(client: PetstoreClient): PetstoreServer { + const server = new PetstoreServer(); + clients.set(server, client); + return server; + } + + @McpTool({ + name: "search_available_pets", + description: + "Read currently available pets from the public Swagger Petstore demo API.", + input: listInput, + }) + public searchAvailablePets( + input: z.infer, + ): Promise { + return clientFor(this) + .findAvailablePets() + .then((pets) => pets.slice(0, input.limit)); + } + + @McpTool({ + name: "get_pet", + description: + "Read one pet by its positive numeric ID from the public Swagger Petstore demo API.", + input: petInput, + }) + public getPet(input: z.infer): Promise { + return clientFor(this).getPet(input.petId); + } + + @McpTool({ + name: "get_petstore_inventory", + description: "Read the public Swagger Petstore inventory status counts.", + input: emptyInput, + }) + public getPetstoreInventory( + _input: z.infer, + ): Promise>> { + return clientFor(this).getInventory(); + } +} + +export function describePetstoreServer() { + const definition = getMcpServerDefinition(PetstoreServer); + if (!definition) { + throw new Error( + "Expected a decorated Swagger Petstore MCP server definition.", + ); + } + return definition; +} + +export async function compilePetstoreServer() { + return createMcpServer(PetstoreServer); +} diff --git a/package.json b/package.json index afe6f62..6bf2cf5 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,11 @@ "format": "biome format --write .", "format:check": "biome format .", "lint": "biome check .", - "test": "vitest run" + "test": "vitest run", + "example:petstore:typemcp:fixture": "tsx examples/typemcp-petstore-fixture.ts", + "example:petstore:agent:fixture": "tsx examples/typechain-petstore-agent-fixture.ts", + "example:petstore:live": "tsx examples/petstore-live.ts", + "example:petstore:agent": "tsx examples/typechain-petstore-agent-live.ts" }, "dependencies": { "@langchain/core": "^1.2.3", diff --git a/test/petstore-client.test.ts b/test/petstore-client.test.ts new file mode 100644 index 0000000..51a5a2e --- /dev/null +++ b/test/petstore-client.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it, vi } from "vitest"; + +import { type FetchLike, PetstoreClient } from "../examples/petstore-client.js"; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); +} + +describe("PetstoreClient", () => { + it("requests available pets from the fixed Swagger Petstore endpoint", async () => { + const fetch = vi.fn().mockResolvedValue( + jsonResponse([ + { + id: 7, + name: "Milo", + photoUrls: [], + status: "available", + }, + ]), + ); + const client = new PetstoreClient({ fetch, timeoutMs: 1_000 }); + + await expect(client.findAvailablePets()).resolves.toEqual([ + { id: 7, name: "Milo", status: "available" }, + ]); + expect(fetch).toHaveBeenCalledWith( + "https://petstore.swagger.io/v2/pet/findByStatus?status=available", + expect.objectContaining({ method: "GET" }), + ); + }); + + it("normalizes one pet and inventory records", async () => { + const fetch = vi + .fn() + .mockResolvedValueOnce( + jsonResponse({ + id: 12, + category: { id: 1, name: "Dogs" }, + name: "Piper", + photoUrls: ["https://example.test/piper.jpg"], + status: "pending", + }), + ) + .mockResolvedValueOnce(jsonResponse({ available: 4, sold: 2 })); + const client = new PetstoreClient({ fetch, timeoutMs: 1_000 }); + + await expect(client.getPet(12)).resolves.toEqual({ + id: 12, + name: "Piper", + status: "pending", + category: "Dogs", + }); + await expect(client.getInventory()).resolves.toEqual({ + available: 4, + sold: 2, + }); + }); + + it("skips malformed individual public-demo records while retaining valid Petstore data", async () => { + const fetch = vi.fn().mockResolvedValue( + jsonResponse([ + { id: "not-a-number", name: "broken", photoUrls: [] }, + { id: 7, name: "Milo", photoUrls: [], status: "available" }, + ]), + ); + + await expect( + new PetstoreClient({ fetch, timeoutMs: 1_000 }).findAvailablePets(), + ).resolves.toEqual([{ id: 7, name: "Milo", status: "available" }]); + }); + + it("rejects malformed API data and non-success responses", async () => { + const malformedFetch = vi + .fn() + .mockResolvedValue(jsonResponse({ id: 1 })); + const unavailableFetch = vi + .fn() + .mockResolvedValue(jsonResponse({ message: "gone" }, 404)); + + await expect( + new PetstoreClient({ + fetch: malformedFetch, + timeoutMs: 1_000, + }).findAvailablePets(), + ).rejects.toThrow( + "Swagger Petstore returned an invalid available-pet payload", + ); + await expect( + new PetstoreClient({ + fetch: unavailableFetch, + timeoutMs: 1_000, + }).getInventory(), + ).rejects.toThrow( + "Swagger Petstore GET /store/inventory failed with HTTP 404", + ); + }); + + it("rejects non-positive pet IDs and timeout values before requests", async () => { + const fetch = vi.fn(); + + expect(() => new PetstoreClient({ fetch, timeoutMs: 0 })).toThrow( + "timeoutMs must be a positive finite number", + ); + await expect( + new PetstoreClient({ fetch, timeoutMs: 1_000 }).getPet(0), + ).rejects.toThrow("petId must be a positive integer"); + expect(fetch).not.toHaveBeenCalled(); + }); +}); diff --git a/test/typechain-petstore-agent.test.ts b/test/typechain-petstore-agent.test.ts new file mode 100644 index 0000000..d5d6406 --- /dev/null +++ b/test/typechain-petstore-agent.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; + +import { runExample } from "./run-example.js"; + +describe("TypeChain Swagger Petstore agent-style flow", () => { + it("invokes the TypeMCP-adapted read-only tool and summarizes its fixture data", () => { + expect(runExample("example:petstore:agent:fixture")).toEqual({ + tool: "search_available_pets", + pets: [ + { id: 1, name: "Milo", status: "available" }, + { id: 2, name: "Nori", status: "available" }, + ], + summary: "Available pets: Milo (#1), Nori (#2).", + }); + }); +}); diff --git a/test/typemcp-petstore-server.test.ts b/test/typemcp-petstore-server.test.ts new file mode 100644 index 0000000..8f7d1ee --- /dev/null +++ b/test/typemcp-petstore-server.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; + +import { runExample } from "./run-example.js"; + +describe("Swagger Petstore TypeMCP wrapper", () => { + it("declares only read-only Petstore tools and returns fixture-backed data", () => { + expect(runExample("example:petstore:typemcp:fixture")).toEqual({ + name: "swagger-petstore", + tools: ["search_available_pets", "get_pet", "get_petstore_inventory"], + available: [{ id: 1, name: "Milo", status: "available" }], + pet: { id: 2, name: "Nori", status: "pending" }, + inventory: { available: 2, sold: 1 }, + compiled: true, + }); + }); +});