-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add read-only Swagger Petstore agent examples #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
cd2a179
docs(planning): add Swagger Petstore agent plan
sjungwon03 53279ed
feat: add read-only Swagger Petstore client
sjungwon03 ead4b7e
feat: wrap read-only Petstore API in TypeMCP
sjungwon03 b4ac792
feat: add TypeChain Petstore agent workflow
sjungwon03 17af6ce
feat: add live Swagger Petstore demonstrations
sjungwon03 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Response>; | ||
|
|
||
| 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<readonly PetstorePet[]> { | ||
| 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) { | ||
|
sjungwon03 marked this conversation as resolved.
|
||
| throw new Error( | ||
| "Swagger Petstore returned no valid available-pet records", | ||
| ); | ||
| } | ||
| return pets; | ||
| } | ||
|
|
||
| public async getPet(petId: number): Promise<PetstorePet> { | ||
| 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<Readonly<Record<string, number>>> { | ||
| 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<unknown> { | ||
| 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<typeof rawPetSchema>): 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 }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }), | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { PetstoreServer } from "./typemcp-petstore-server.js"; | ||
|
|
||
| export async function run(): Promise<void> { | ||
| 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(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { createFixturePetstoreServer } from "./petstore-fixture.js"; | ||
| import { summarizeAvailablePets } from "./typechain-petstore-agent.js"; | ||
|
|
||
| export async function run(): Promise<void> { | ||
| const summary = await summarizeAvailablePets( | ||
| createFixturePetstoreServer(), | ||
| 2, | ||
| ); | ||
| console.log(JSON.stringify(summary, null, 2)); | ||
| } | ||
|
|
||
| if (import.meta.url === `file://${process.argv[1]}`) { | ||
| await run(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { summarizeAvailablePets } from "./typechain-petstore-agent.js"; | ||
| import { PetstoreServer } from "./typemcp-petstore-server.js"; | ||
|
|
||
| export async function run(): Promise<void> { | ||
| 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(); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.