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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
90 changes: 90 additions & 0 deletions docs/planning/2026-07-29-swagger-petstore-agent.md
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. |
113 changes: 113 additions & 0 deletions examples/petstore-client.ts
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";
Comment thread
sjungwon03 marked this conversation as resolved.

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) {
Comment thread
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 });
}
37 changes: 37 additions & 0 deletions examples/petstore-fixture.ts
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 }),
);
}
30 changes: 30 additions & 0 deletions examples/petstore-live.ts
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();
}
14 changes: 14 additions & 0 deletions examples/typechain-petstore-agent-fixture.ts
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();
}
22 changes: 22 additions & 0 deletions examples/typechain-petstore-agent-live.ts
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();
}
Loading