From 225ffe86d7af8e6ee9d655e892ba35621531ba6f Mon Sep 17 00:00:00 2001 From: sjungwon03 <> Date: Wed, 29 Jul 2026 00:31:28 +0900 Subject: [PATCH 1/8] ci: require verified dev-to-main promotions --- .github/workflows/release-promotion.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/release-promotion.yml diff --git a/.github/workflows/release-promotion.yml b/.github/workflows/release-promotion.yml new file mode 100644 index 0000000..2f2b40f --- /dev/null +++ b/.github/workflows/release-promotion.yml @@ -0,0 +1,23 @@ +name: Release promotion + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + release-promotion: + name: release-promotion + runs-on: ubuntu-latest + steps: + - name: Require dev as the promotion source + run: test "${{ github.head_ref }}" = "dev" + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run check From 3155e9cb56d845aa54bb6819eac9cf28548be993 Mon Sep 17 00:00:00 2001 From: Jungwon Sohn Date: Wed, 29 Jul 2026 02:38:52 +0900 Subject: [PATCH 2/8] fix: pin examples MCP SDK to audited graph (#3) Co-authored-by: sjungwon03 <> --- .github/workflows/verify.yml | 1 + README.md | 7 ++++++ package-lock.json | 16 ++++++------- package.json | 8 ++++++- test/dependency-security.test.ts | 39 ++++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 test/dependency-security.test.ts diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3f9e520..ea34350 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -25,3 +25,4 @@ jobs: - run: npm run example:policy - run: npm run example:typemcp - run: npm run example:bridge + - run: npm run audit:prod diff --git a/README.md b/README.md index 02c8cdf..567c56a 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,19 @@ These examples show declaration and adapter boundaries, not a full hosted applic - The bridge is in-process only. It adapts a decorated TypeMCP class to LangChain tools; it does not create an MCP client or network connection. - No model provider is configured. Add a model only in your application after deciding its credentials, authorization, and runtime policy. +## Dependency-security boundary + +The published `@theorvane/type-mcp` package currently exact-pins an MCP SDK release whose transitive `@hono/node-server` graph is affected by [GHSA-frvp-7c67-39w9](https://github.com/advisories/GHSA-frvp-7c67-39w9). This **examples application** uses an npm `overrides` entry to resolve its lockfile to `@modelcontextprotocol/sdk@1.30.0` and `@hono/node-server@2.0.12`; `npm run audit:prod` verifies the resulting production graph. + +This override is intentionally scoped to this repository and is **not a remediation for downstream TypeMCP consumers**. The package-level remediation remains tracked in [Theorvane/type-mcp#93](https://github.com/Theorvane/type-mcp/issues/93). + ## Development ```bash npm run lint npm run build npm test +npm run audit:prod npm run check ``` diff --git a/package-lock.json b/package-lock.json index 4e67c10..3525ab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -674,12 +674,12 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.17", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", - "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" @@ -815,12 +815,12 @@ "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", diff --git a/package.json b/package.json index 2780dc9..3241cc3 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "node": ">=20" }, "scripts": { + "audit:prod": "npm audit --omit=dev --audit-level=low", "build": "tsc --noEmit", - "check": "npm run format:check && npm run build && npm test", + "check": "npm run format:check && npm run build && npm test && npm run audit:prod", "example:typechain": "tsx examples/typechain-tool-definition.ts", "example:policy": "tsx examples/typechain-policy-guard.ts", "example:typemcp": "tsx examples/typemcp-server-definition.ts", @@ -32,5 +33,10 @@ "tsx": "^4.21.0", "typescript": "^5.9.3", "vitest": "^4.1.0" + }, + "overrides": { + "@theorvane/type-mcp": { + "@modelcontextprotocol/sdk": "1.30.0" + } } } diff --git a/test/dependency-security.test.ts b/test/dependency-security.test.ts new file mode 100644 index 0000000..076de06 --- /dev/null +++ b/test/dependency-security.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repositoryRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); + +type Lockfile = { + readonly packages: Record; +}; + +describe("production dependency security", () => { + it("pins the examples application to the audited MCP SDK graph", () => { + const packageJson = JSON.parse( + readFileSync(resolve(repositoryRoot, "package.json"), "utf8"), + ) as { + readonly overrides?: { + readonly "@theorvane/type-mcp"?: { + readonly "@modelcontextprotocol/sdk"?: string; + }; + }; + }; + const lockfile = JSON.parse( + readFileSync(resolve(repositoryRoot, "package-lock.json"), "utf8"), + ) as Lockfile; + + expect( + packageJson.overrides?.["@theorvane/type-mcp"]?.[ + "@modelcontextprotocol/sdk" + ], + ).toBe("1.30.0"); + expect( + lockfile.packages["node_modules/@modelcontextprotocol/sdk"]?.version, + ).toBe("1.30.0"); + expect(lockfile.packages["node_modules/@hono/node-server"]?.version).toBe( + "2.0.12", + ); + }); +}); From db7088f3b4e820f2a1083255c779efdf65875ad6 Mon Sep 17 00:00:00 2001 From: Jungwon Sohn Date: Wed, 29 Jul 2026 09:07:42 +0900 Subject: [PATCH 3/8] fix(deps): adopt TypeMCP 0.2.2 remediation (#7) --- README.md | 6 ++---- package-lock.json | 11 ++++++----- package.json | 7 +------ test/dependency-security.test.ts | 14 ++++---------- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 567c56a..9bc6131 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,7 @@ These examples show declaration and adapter boundaries, not a full hosted applic ## Dependency-security boundary -The published `@theorvane/type-mcp` package currently exact-pins an MCP SDK release whose transitive `@hono/node-server` graph is affected by [GHSA-frvp-7c67-39w9](https://github.com/advisories/GHSA-frvp-7c67-39w9). This **examples application** uses an npm `overrides` entry to resolve its lockfile to `@modelcontextprotocol/sdk@1.30.0` and `@hono/node-server@2.0.12`; `npm run audit:prod` verifies the resulting production graph. - -This override is intentionally scoped to this repository and is **not a remediation for downstream TypeMCP consumers**. The package-level remediation remains tracked in [Theorvane/type-mcp#93](https://github.com/Theorvane/type-mcp/issues/93). +These examples use the published `@theorvane/type-mcp@^0.2.2` remediation. Its consumer-enforceable dependency contract resolves `@modelcontextprotocol/sdk@1.30.0` and `@hono/node-server@2.0.12`; `npm run audit:prod` verifies the installed production graph with no local npm override. ## Development @@ -49,7 +47,7 @@ npm run check ## Packages - [`@theorvane/type-chain`](https://www.npmjs.com/package/@theorvane/type-chain) `0.1.1` -- [`@theorvane/type-mcp`](https://www.npmjs.com/package/@theorvane/type-mcp) `0.2.0` +- [`@theorvane/type-mcp`](https://www.npmjs.com/package/@theorvane/type-mcp) `0.2.2` or later within the `0.2.x` range ## License diff --git a/package-lock.json b/package-lock.json index 3525ab6..fe0b1eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@langchain/core": "^1.2.3", "@theorvane/type-chain": "^0.1.1", - "@theorvane/type-mcp": "^0.2.0", + "@theorvane/type-mcp": "^0.2.2", "langchain": "^1.5.4", "zod": "^4.4.3" }, @@ -1216,12 +1216,13 @@ } }, "node_modules/@theorvane/type-mcp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@theorvane/type-mcp/-/type-mcp-0.2.1.tgz", - "integrity": "sha512-PafubwBhNu9b4nhsT0YX4gZRyiRQtqYmm2hKBPmd9xXG7NX87lOiAEXnkwfWsUwjf0bS88kHdNt3NRhY+/kPvw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@theorvane/type-mcp/-/type-mcp-0.2.2.tgz", + "integrity": "sha512-Oj7ZNnJsC0SWdXrCiUNMfd3HYLtBkm7rNyL5xSsvVkUvowoTT+f+5ojl6Lq0DMLeKuMMRwFIMimwdzT35MWI2w==", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "1.26.0", + "@hono/node-server": "2.0.12", + "@modelcontextprotocol/sdk": "1.30.0", "zod": "^4.4.3" }, "engines": { diff --git a/package.json b/package.json index 3241cc3..afe6f62 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "@langchain/core": "^1.2.3", "@theorvane/type-chain": "^0.1.1", - "@theorvane/type-mcp": "^0.2.0", + "@theorvane/type-mcp": "^0.2.2", "langchain": "^1.5.4", "zod": "^4.4.3" }, @@ -33,10 +33,5 @@ "tsx": "^4.21.0", "typescript": "^5.9.3", "vitest": "^4.1.0" - }, - "overrides": { - "@theorvane/type-mcp": { - "@modelcontextprotocol/sdk": "1.30.0" - } } } diff --git a/test/dependency-security.test.ts b/test/dependency-security.test.ts index 076de06..c557313 100644 --- a/test/dependency-security.test.ts +++ b/test/dependency-security.test.ts @@ -10,25 +10,19 @@ type Lockfile = { }; describe("production dependency security", () => { - it("pins the examples application to the audited MCP SDK graph", () => { + it("uses the published TypeMCP remediation without a local override", () => { const packageJson = JSON.parse( readFileSync(resolve(repositoryRoot, "package.json"), "utf8"), ) as { - readonly overrides?: { - readonly "@theorvane/type-mcp"?: { - readonly "@modelcontextprotocol/sdk"?: string; - }; + readonly dependencies?: { + readonly "@theorvane/type-mcp"?: string; }; }; const lockfile = JSON.parse( readFileSync(resolve(repositoryRoot, "package-lock.json"), "utf8"), ) as Lockfile; - expect( - packageJson.overrides?.["@theorvane/type-mcp"]?.[ - "@modelcontextprotocol/sdk" - ], - ).toBe("1.30.0"); + expect(packageJson.dependencies?.["@theorvane/type-mcp"]).toBe("^0.2.2"); expect( lockfile.packages["node_modules/@modelcontextprotocol/sdk"]?.version, ).toBe("1.30.0"); From eff78edf5d100e1ce7fa99ac6a63d3cd8ed75853 Mon Sep 17 00:00:00 2001 From: Jungwon Sohn Date: Wed, 29 Jul 2026 09:22:06 +0900 Subject: [PATCH 4/8] 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 From 654bffa64179dcf58f1143fa3812323dd3417518 Mon Sep 17 00:00:00 2001 From: sjungwon03 Date: Wed, 29 Jul 2026 09:29:30 +0900 Subject: [PATCH 5/8] fix: remove obsolete reconciliation override --- package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/package.json b/package.json index ec88e32..afe6f62 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,5 @@ "tsx": "^4.21.0", "typescript": "^5.9.3", "vitest": "^4.1.0" - }, - "overrides": { - "@theorvane/type-mcp": { - "@modelcontextprotocol/sdk": "1.30.0" - } } } From 69f9ee84376fba11c42f7e16755c71804fb1e564 Mon Sep 17 00:00:00 2001 From: Jungwon Sohn Date: Wed, 29 Jul 2026 11:04:00 +0900 Subject: [PATCH 6/8] 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 --- 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, + }); + }); +}); From 76d4171ab111ab69c77924ab90b4cb829f83701a Mon Sep 17 00:00:00 2001 From: Jungwon Sohn Date: Wed, 29 Jul 2026 11:42:07 +0900 Subject: [PATCH 7/8] feat: add real TypeChain Petstore agent factory (#22) * docs(planning): define real Petstore agent delivery * feat: add real TypeChain Petstore agent factory * docs: explain application-owned Petstore agent runtime --- README.md | 26 +++++++- ...026-07-29-real-typechain-petstore-agent.md | 63 +++++++++++++++++++ .../typechain-petstore-real-agent-fixture.ts | 51 +++++++++++++++ examples/typechain-petstore-real-agent.ts | 30 +++++++++ package.json | 3 +- test/typechain-petstore-real-agent.test.ts | 16 +++++ 6 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 docs/planning/2026-07-29-real-typechain-petstore-agent.md create mode 100644 examples/typechain-petstore-real-agent-fixture.ts create mode 100644 examples/typechain-petstore-real-agent.ts create mode 100644 test/typechain-petstore-real-agent.test.ts diff --git a/README.md b/README.md index be90134..2ac148e 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ npm run check | --- | --- | --- | | [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. | +| [Swagger Petstore real TypeChain agent](examples/typechain-petstore-real-agent.ts) | `npm run example:petstore:agent:real:fixture` | Build a real LangChain agent loop with TypeChain `createTypeMcpAgent()` and an application-supplied model. | | [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. | @@ -34,6 +35,9 @@ npm run example:petstore:live # Real TypeChain → adapted TypeMCP tool call followed by a deterministic summary. npm run example:petstore:agent +# Real LangChain agent-loop proof; no network or provider credential. +npm run example:petstore:agent:real:fixture + # Deterministic fixtures used by unit tests; no network request. npm run example:petstore:typemcp:fixture npm run example:petstore:agent:fixture @@ -41,7 +45,27 @@ 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. +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. `summarizeAvailablePets()` is a deterministic adapter workflow, while `createPetstoreAgent()` is the separate real LangChain agent factory. The latter delegates tool selection and agent-loop execution to LangChain through TypeChain `createTypeMcpAgent()`. + +### Application-owned model runtime + +The real agent factory accepts an already configured LangChain-compatible chat model. The application—not this repository—chooses a provider/model and owns its package installation, credentials, authorization, retry policy, and observability: + +```ts +import { createPetstoreAgent } from "./examples/typechain-petstore-real-agent.js"; + +// Construct and authenticate a LangChain-compatible model in your application. +declare const applicationModel: Parameters< + typeof createPetstoreAgent +>[0]["model"]; + +const agent = await createPetstoreAgent({ model: applicationModel }); +const response = await agent.invoke({ + messages: [{ role: "user", content: "Which pets are available?" }], +}); +``` + +`npm run example:petstore:agent:real:fixture` proves this is an actual agent loop without a live model: LangChain's `FakeToolCallingModel` selects `search_available_pets`, and the agent executes the TypeMCP-derived tool against fixture data. This repository intentionally supplies no provider SDK, API key, environment-variable model configuration, or automated live LLM command. ## Boundaries that the examples intentionally preserve diff --git a/docs/planning/2026-07-29-real-typechain-petstore-agent.md b/docs/planning/2026-07-29-real-typechain-petstore-agent.md new file mode 100644 index 0000000..3080906 --- /dev/null +++ b/docs/planning/2026-07-29-real-typechain-petstore-agent.md @@ -0,0 +1,63 @@ +# Real TypeChain Petstore Agent Implementation Plan + +> **For Hermes:** Implement this issue-scoped plan with test-first slices and preserve the `dev` → release-only `main` workflow. + +**Goal:** Add a real LangChain agent factory that uses TypeChain `createTypeMcpAgent()` to operate the existing read-only Swagger Petstore TypeMCP tools, while keeping provider credentials and runtime selection application-owned. + +**Architecture:** Create a narrowly scoped `createPetstoreAgent()` factory. It accepts a caller-provided LangChain chat model and an optional resolver-backed `PetstoreServer`, then delegates agent construction to TypeChain's `createTypeMcpAgent()`. A test uses LangChain's `FakeToolCallingModel` plus the existing deterministic fixture server to prove an actual agent loop selects and invokes `search_available_pets`; no network or model provider runs in CI. A separate manual composition example accepts a model from the caller rather than initializing a provider or reading a secret. + +**Tech stack:** TypeScript, Vitest, LangChain `FakeToolCallingModel`, `@theorvane/type-chain/typemcp`, TypeMCP decorators, Zod. + +--- + +### Task 1: Define and prove the real agent factory contract + +**Files:** +- Create: `test/typechain-petstore-real-agent.test.ts` +- Create: `examples/typechain-petstore-real-agent.ts` + +1. Write a failing integration test importing `createPetstoreAgent()`. +2. Use `FakeToolCallingModel` to emit one `search_available_pets` call with `{ limit: 2 }`, followed by an empty tool-call response. +3. Invoke the constructed agent with a user message and assert it includes a LangChain tool message for that call containing the fixture pet result. +4. Run the focused test and observe failure because the module/export does not exist. +5. Implement only the factory: accept `{ model, server? }`, default to a `PetstoreServer`, supply its explicit resolver to `createTypeMcpAgent()`, and return the LangChain agent. +6. Re-run the focused test, then lint and typecheck. + +### Task 2: Add executable fixture agent-loop evidence + +**Files:** +- Create: `examples/typechain-petstore-real-agent-fixture.ts` +- Modify: `package.json` +- Modify: `test/typechain-petstore-real-agent.test.ts` + +1. Write a failing assertion for a runnable fixture command that reports the agent's selected tool and its tool result. +2. Implement the fixture runner with `FakeToolCallingModel` and the existing `createFixturePetstoreServer()`. +3. Add `example:petstore:agent:real:fixture`; do not add a provider package or live credential-dependent command. +4. Run the focused test and fixture command successfully. + +### Task 3: Document real-agent composition without adding provider ownership + +**Files:** +- Modify: `README.md` +- Modify: `docs/planning/2026-07-29-real-typechain-petstore-agent.md` + +1. Document the distinction between the deterministic adapter workflow and the real `createTypeMcpAgent()` factory. +2. Show a short application-owned pseudo-composition snippet with a `model` supplied by the caller. Do not use an API key, `process.env`, or provider-specific package. +3. State that TypeChain constructs the in-process LangChain agent but does not select/configure a provider, host an MCP transport, create an MCP client/session, or authorize tool invocations. +4. Run formatting, full checks, audit, and whitespace checks. + +### Task 4: Deliver through governed PRs + +1. Commit the plan and implementation in focused commits. +2. Create a `dev` PR closing #21. +3. Obtain exact-head independent review, green `verify`, and resolved threads before squash merge. +4. Reconcile `main` ancestry into `dev` only if required by current remote history, via a two-parent merge with separate review. +5. Create a reviewed `dev` → `main` promotion; require exact-head `verify`, `release-promotion`, independent approval, and resolved threads before squash merge. +6. Clone current `main` cleanly and run `npm ci`, `npm run check`, and the real-agent fixture command. + +## Explicit non-goals + +- No provider SDK, API key, model identifier, or environment-variable model configuration. +- No automated live LLM invocation in CI. +- No MCP HTTP/stdio server, client/session, or cross-process transport. +- No Petstore write endpoint or credential path. diff --git a/examples/typechain-petstore-real-agent-fixture.ts b/examples/typechain-petstore-real-agent-fixture.ts new file mode 100644 index 0000000..db2b333 --- /dev/null +++ b/examples/typechain-petstore-real-agent-fixture.ts @@ -0,0 +1,51 @@ +import { ToolMessage } from "@langchain/core/messages"; +import { FakeToolCallingModel } from "langchain"; + +import { createFixturePetstoreServer } from "./petstore-fixture.js"; +import { createPetstoreAgent } from "./typechain-petstore-real-agent.js"; + +export async function run(): Promise { + const agent = await createPetstoreAgent({ + model: new FakeToolCallingModel({ + toolCalls: [ + [ + { + id: "available-pets-call", + name: "search_available_pets", + args: { limit: 2 }, + }, + ], + [], + ], + }), + server: createFixturePetstoreServer(), + }); + const result = await agent.invoke({ + messages: [ + { + role: "user", + content: "Which pets are currently available?", + }, + ], + }); + const toolMessage = result.messages.find(ToolMessage.isInstance); + if (!toolMessage) { + throw new Error("Expected the LangChain agent to execute a Petstore tool."); + } + + console.log( + JSON.stringify( + { + tool: "search_available_pets", + toolCallId: toolMessage.tool_call_id, + result: JSON.parse(toolMessage.content as string) as unknown, + }, + null, + 2, + ), + ); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await run(); +} diff --git a/examples/typechain-petstore-real-agent.ts b/examples/typechain-petstore-real-agent.ts new file mode 100644 index 0000000..30d2ede --- /dev/null +++ b/examples/typechain-petstore-real-agent.ts @@ -0,0 +1,30 @@ +import { createTypeMcpAgent } from "@theorvane/type-chain/typemcp"; + +import { PetstoreServer } from "./typemcp-petstore-server.js"; + +type PetstoreAgentModel = Parameters< + typeof createTypeMcpAgent +>[0]["model"]; + +export type CreatePetstoreAgentOptions = Readonly<{ + /** The application selects, configures, and authenticates this LangChain model. */ + model: PetstoreAgentModel; + /** Optional explicit Petstore resolver target; defaults to the live read-only server. */ + server?: PetstoreServer; +}>; + +/** + * Builds a real LangChain agent from the in-process TypeMCP Petstore tools. + * It does not choose a model provider, read credentials, or start an MCP transport. + */ +export async function createPetstoreAgent({ + model, + server, +}: CreatePetstoreAgentOptions) { + const resolvedServer = server ?? new PetstoreServer(); + return createTypeMcpAgent({ + model, + server: PetstoreServer, + resolver: { resolve: () => resolvedServer }, + }); +} diff --git a/package.json b/package.json index 6bf2cf5..1df3527 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "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" + "example:petstore:agent": "tsx examples/typechain-petstore-agent-live.ts", + "example:petstore:agent:real:fixture": "tsx examples/typechain-petstore-real-agent-fixture.ts" }, "dependencies": { "@langchain/core": "^1.2.3", diff --git a/test/typechain-petstore-real-agent.test.ts b/test/typechain-petstore-real-agent.test.ts new file mode 100644 index 0000000..32df43c --- /dev/null +++ b/test/typechain-petstore-real-agent.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; + +import { runExample } from "./run-example.js"; + +describe("real TypeChain Swagger Petstore agent", () => { + it("runs a LangChain agent loop that selects and executes the TypeMCP search tool", () => { + expect(runExample("example:petstore:agent:real:fixture")).toEqual({ + tool: "search_available_pets", + toolCallId: "available-pets-call", + result: [ + { id: 1, name: "Milo", status: "available" }, + { id: 2, name: "Nori", status: "available" }, + ], + }); + }); +}); From 4e00e86013e78a44d8dd4ce8ee1a0ef608968388 Mon Sep 17 00:00:00 2001 From: Jungwon Sohn Date: Wed, 29 Jul 2026 15:00:01 +0900 Subject: [PATCH 8/8] refactor: build Petstore TypeMCP HTTP agent example (#28) * docs: design real Petstore MCP agent example * docs(planning): detail Petstore MCP agent rewrite * feat: add loopback Petstore MCP runtime and client * feat: add TypeChain Petstore MCP agent * refactor: center examples on Petstore MCP agent * ci: verify Petstore MCP fixtures --- .github/workflows/verify.yml | 6 +- README.md | 119 ++-- ...-petstore-real-mcp-agent-implementation.md | 529 ++++++++++++++++++ ...026-07-29-real-typechain-petstore-agent.md | 2 + .../2026-07-29-swagger-petstore-agent.md | 6 +- ...26-07-29-petstore-real-mcp-agent-design.md | 158 ++++++ examples/petstore-mcp-client.ts | 79 +++ examples/petstore-mcp-fixture.ts | 42 ++ examples/petstore-mcp-handler.ts | 20 + examples/petstore-mcp-runtime.ts | 108 ++++ examples/typechain-petstore-agent-fixture.ts | 14 - examples/typechain-petstore-agent-live.ts | 22 - examples/typechain-petstore-agent.ts | 67 --- .../typechain-petstore-mcp-agent-fixture.ts | 67 +++ examples/typechain-petstore-mcp-agent.ts | 61 ++ .../typechain-petstore-real-agent-fixture.ts | 51 -- examples/typechain-petstore-real-agent.ts | 30 - examples/typechain-policy-guard.ts | 61 -- examples/typechain-tool-definition.ts | 41 -- examples/typemcp-langchain-bridge.ts | 20 - examples/typemcp-petstore-fixture.ts | 33 -- examples/typemcp-server-definition.ts | 51 -- package.json | 12 +- test/petstore-mcp-client.test.ts | 19 + test/petstore-mcp-runtime.test.ts | 12 + test/typechain-petstore-agent.test.ts | 16 - ...s => typechain-petstore-mcp-agent.test.ts} | 11 +- test/typechain-policy-guard.test.ts | 12 - test/typechain-tool-definition.test.ts | 12 - test/typemcp-langchain-bridge.test.ts | 12 - test/typemcp-petstore-server.test.ts | 16 - test/typemcp-server-definition.test.ts | 13 - test/verify-workflow.test.ts | 21 + 33 files changed, 1208 insertions(+), 535 deletions(-) create mode 100644 docs/planning/2026-07-29-petstore-real-mcp-agent-implementation.md create mode 100644 docs/superpowers/specs/2026-07-29-petstore-real-mcp-agent-design.md create mode 100644 examples/petstore-mcp-client.ts create mode 100644 examples/petstore-mcp-fixture.ts create mode 100644 examples/petstore-mcp-handler.ts create mode 100644 examples/petstore-mcp-runtime.ts delete mode 100644 examples/typechain-petstore-agent-fixture.ts delete mode 100644 examples/typechain-petstore-agent-live.ts delete mode 100644 examples/typechain-petstore-agent.ts create mode 100644 examples/typechain-petstore-mcp-agent-fixture.ts create mode 100644 examples/typechain-petstore-mcp-agent.ts delete mode 100644 examples/typechain-petstore-real-agent-fixture.ts delete mode 100644 examples/typechain-petstore-real-agent.ts delete mode 100644 examples/typechain-policy-guard.ts delete mode 100644 examples/typechain-tool-definition.ts delete mode 100644 examples/typemcp-langchain-bridge.ts delete mode 100644 examples/typemcp-petstore-fixture.ts delete mode 100644 examples/typemcp-server-definition.ts create mode 100644 test/petstore-mcp-client.test.ts create mode 100644 test/petstore-mcp-runtime.test.ts delete mode 100644 test/typechain-petstore-agent.test.ts rename test/{typechain-petstore-real-agent.test.ts => typechain-petstore-mcp-agent.test.ts} (50%) delete mode 100644 test/typechain-policy-guard.test.ts delete mode 100644 test/typechain-tool-definition.test.ts delete mode 100644 test/typemcp-langchain-bridge.test.ts delete mode 100644 test/typemcp-petstore-server.test.ts delete mode 100644 test/typemcp-server-definition.test.ts create mode 100644 test/verify-workflow.test.ts diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index ea34350..b2b144e 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -21,8 +21,6 @@ jobs: - run: npm run lint - run: npm run build - run: npm test - - run: npm run example:typechain - - run: npm run example:policy - - run: npm run example:typemcp - - run: npm run example:bridge + - run: npm run example:petstore:mcp:fixture + - run: npm run example:petstore:mcp-agent:fixture - run: npm run audit:prod diff --git a/README.md b/README.md index 2ac148e..3ba719e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,21 @@ -# Theorvane Examples +# Theorvane Petstore MCP Example -Runnable, minimal examples for [TypeChain](https://github.com/Theorvane/type-chain) and [TypeMCP](https://github.com/Theorvane/type-mcp). Every example uses the published packages, explicit Zod schemas, standard TypeScript decorators, and application-owned runtime boundaries. +A single runnable example showing a truthful end-to-end path from the public [Swagger Petstore v2](https://petstore.swagger.io/) demo API to a TypeChain `@Agent()` that uses an **actual local MCP HTTP session**. + +## What this proves + +```text +Swagger Petstore v2 GET API + → injected PetstoreClient + → TypeMCP @McpServer() / @McpTool() + → TypeMCP Streamable HTTP handler + → local 127.0.0.1 ephemeral-port MCP runtime + → official MCP SDK client session + → TypeChain @Agent() + @Tool() MCP-client façade + → TypeChain buildAgent() / LangChain agent loop +``` + +The fixture run proves the MCP SDK client performs protocol initialization, `tools/list`, and `tools/call` over loopback Streamable HTTP. It also proves a LangChain `FakeToolCallingModel` selects `search_available_pets`, and the TypeChain `@Agent()` façade sends that call through the MCP client rather than calling the REST client or TypeMCP server directly. ## Prerequisites @@ -8,77 +23,89 @@ Runnable, minimal examples for [TypeChain](https://github.com/Theorvane/type-cha - npm 10 or newer ```bash -npm install +npm ci npm run check ``` -## Examples +## Run the deterministic MCP agent example -| Example | Run | Demonstrates | -| --- | --- | --- | -| [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. | -| [Swagger Petstore real TypeChain agent](examples/typechain-petstore-real-agent.ts) | `npm run example:petstore:agent:real:fixture` | Build a real LangChain agent loop with TypeChain `createTypeMcpAgent()` and an application-supplied model. | -| [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. | +```bash +npm run example:petstore:mcp-agent:fixture +``` -## Swagger Petstore scenario +It starts an in-process Node HTTP server bound only to `127.0.0.1` on an ephemeral port, connects the official MCP SDK `StreamableHTTPClientTransport`, discovers the tools, runs the agent loop, and shuts down the MCP client session and HTTP server in `finally` blocks. + +Expected result contains fixture data: + +```json +{ + "discoveredTools": [ + "search_available_pets", + "get_pet", + "get_petstore_inventory" + ], + "tool": "search_available_pets", + "result": [ + { "id": 1, "name": "Milo", "status": "available" }, + { "id": 2, "name": "Nori", "status": "available" } + ] +} +``` -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`. +For the narrower protocol-only smoke run (loopback URL, 404 route boundary, `tools/list`, and `tools/call`), use: + +```bash +npm run example:petstore:mcp:fixture +``` + +## Optional public REST smoke run ```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 +This calls the mutable public Swagger Petstore demo API. Its data and uptime are outside this repository's control, so it is not used by tests or CI. The command is a GET-only REST smoke run; it does **not** demonstrate MCP client connectivity or a model-backed agent. -# Real LangChain agent-loop proof; no network or provider credential. -npm run example:petstore:agent:real:fixture +## Read-only Petstore contract -# Deterministic fixtures used by unit tests; no network request. -npm run example:petstore:typemcp:fixture -npm run example:petstore:agent:fixture -``` +`PetstoreClient` has a fixed base URL, `https://petstore.swagger.io/v2`, and only sends `GET` requests. The TypeMCP server exposes exactly three tools: -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. +| MCP tool | REST operation | +| --- | --- | +| `search_available_pets` | `GET /pet/findByStatus?status=available` | +| `get_pet` | `GET /pet/{petId}` | +| `get_petstore_inventory` | `GET /store/inventory` | -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. `summarizeAvailablePets()` is a deterministic adapter workflow, while `createPetstoreAgent()` is the separate real LangChain agent factory. The latter delegates tool selection and agent-loop execution to LangChain through TypeChain `createTypeMcpAgent()`. +The example has no create, update, delete, API-key, credential, environment-configured API host, or public listener path. -### Application-owned model runtime +## Application-owned model runtime -The real agent factory accepts an already configured LangChain-compatible chat model. The application—not this repository—chooses a provider/model and owns its package installation, credentials, authorization, retry policy, and observability: +The fixture uses `FakeToolCallingModel`, not a live provider. An application that wants a real model must install/configure that model itself, then pass it together with a connected MCP client to the agent builder: ```ts -import { createPetstoreAgent } from "./examples/typechain-petstore-real-agent.js"; +import { buildPetstoreMcpAgent } from "./examples/typechain-petstore-mcp-agent.js"; -// Construct and authenticate a LangChain-compatible model in your application. +// The consuming application chooses, authenticates, and authorizes this model. declare const applicationModel: Parameters< - typeof createPetstoreAgent + typeof buildPetstoreMcpAgent >[0]["model"]; -const agent = await createPetstoreAgent({ model: applicationModel }); -const response = await agent.invoke({ - messages: [{ role: "user", content: "Which pets are available?" }], +// The application owns the lifecycle of this client connection. +declare const connectedMcpClient: Parameters< + typeof buildPetstoreMcpAgent +>[0]["client"]; + +const agent = buildPetstoreMcpAgent({ + model: applicationModel, + client: connectedMcpClient, }); ``` -`npm run example:petstore:agent:real:fixture` proves this is an actual agent loop without a live model: LangChain's `FakeToolCallingModel` selects `search_available_pets`, and the agent executes the TypeMCP-derived tool against fixture data. This repository intentionally supplies no provider SDK, API key, environment-variable model configuration, or automated live LLM command. - -## Boundaries that the examples intentionally preserve - -These examples show declaration and adapter boundaries, not a full hosted application: - -- The application owns API credentials, authorization, approval, retries, timeouts, audit persistence, and redaction. -- TypeMCP transport selection and MCP session lifecycle are application decisions. The examples do not start a stdio or HTTP server. -- The bridge is in-process only. It adapts a decorated TypeMCP class to LangChain tools; it does not create an MCP client or network connection. -- No model provider is configured. Add a model only in your application after deciding its credentials, authorization, and runtime policy. +This repository deliberately does not select a provider/model, install provider SDKs, read credentials or `process.env`, run a live LLM command, or implement authorization/retry/audit/redaction policy. -## Dependency-security boundary +## Example-only runtime boundary -These examples use the published `@theorvane/type-mcp@^0.2.2` remediation. Its consumer-enforceable dependency contract resolves `@modelcontextprotocol/sdk@1.30.0` and `@hono/node-server@2.0.12`; `npm run audit:prod` verifies the installed production graph with no local npm override. +The loopback runtime is a deterministic integration example, not a production deployment recipe. It intentionally does **not** provide public hosting, authentication, authorization, durable sessions, a reverse proxy, observability, deployment configuration, or persistent MCP lifecycle management. A production application must own those decisions. ## Development diff --git a/docs/planning/2026-07-29-petstore-real-mcp-agent-implementation.md b/docs/planning/2026-07-29-petstore-real-mcp-agent-implementation.md new file mode 100644 index 0000000..f0014e8 --- /dev/null +++ b/docs/planning/2026-07-29-petstore-real-mcp-agent-implementation.md @@ -0,0 +1,529 @@ +# Petstore Real MCP Agent Implementation Plan + +> **For Hermes:** Implement this plan task-by-task with test-first slices. Preserve the issue-first `dev` → release-only `main` workflow. + +**Goal:** Replace the mixed example catalog with one read-only Swagger Petstore flow in which a TypeMCP Streamable HTTP server is consumed by an MCP SDK client and a TypeChain `@Agent()` / `buildAgent()` runtime. + +**Architecture:** `PetstoreClient` remains the sole REST boundary and uses fixed-host GET requests. `PetstoreServer` exposes its three tools with TypeMCP, and a Node loopback runtime hosts TypeMCP's official Fetch Streamable HTTP handler at an ephemeral `127.0.0.1` `/mcp` URL. An MCP SDK `Client` plus `StreamableHTTPClientTransport` owns protocol initialization/discovery/tool calls. `PetstoreMcpAgentTools` is a TypeChain `@Agent()` class whose explicit-schema `@Tool()` methods delegate only to that MCP client; `buildAgent()` constructs the LangChain loop from the caller-provided model. + +**Tech Stack:** TypeScript (NodeNext), Zod, `@theorvane/type-mcp@^0.2.2`, `@theorvane/type-chain@^0.1.1`, `@modelcontextprotocol/sdk@1.30.0` transitive dependency, Node `http`, LangChain `FakeToolCallingModel`, Vitest, Biome. + +--- + +## Ground rules + +- Work from `origin/dev` on an English issue-scoped branch, never directly on `dev` or `main`. +- Never add a Petstore write method, credential/API key, host override, provider SDK, `process.env` lookup, public listener, live LLM command, or external network dependency to tests. +- Bind the local MCP runtime to literal host `127.0.0.1` and port `0`; test cleanup must close client transport/session before the HTTP runtime. +- Do not claim TypeChain implements an MCP client. The example owns the SDK client integration, while TypeChain owns annotated façade-tool adaptation and agent construction. +- Retain only Petstore-centered examples after the cleanup task. + +## Task 1: Define the MCP client and loopback runtime contracts + +**Objective:** Add focused tests that specify a real Streamable HTTP MCP lifecycle before introducing runtime code. + +**Files:** +- Create: `test/petstore-mcp-runtime.test.ts` +- Create: `test/petstore-mcp-client.test.ts` +- Create: `examples/petstore-mcp-runtime.ts` +- Create: `examples/petstore-mcp-client.ts` +- Modify: `examples/petstore-fixture.ts` + +**Step 1: Write a failing loopback runtime test** + +Use `createFixturePetstoreServer()` and import a non-existent `startPetstoreMcpRuntime()`. Assert that it yields an `http://127.0.0.1:/mcp` URL and that a request to any non-`/mcp` path returns 404. Use `try/finally` to await `runtime.close()`. + +```ts +const runtime = await startPetstoreMcpRuntime({ + serverFactory: createFixturePetstoreServer, +}); +try { + expect(runtime.url.hostname).toBe("127.0.0.1"); + expect(runtime.url.pathname).toBe("/mcp"); + expect((await fetch(new URL("/not-mcp", runtime.url))).status).toBe(404); +} finally { + await runtime.close(); +} +``` + +**Step 2: Run the focused test and confirm RED** + +Run: + +```bash +npm test -- --run test/petstore-mcp-runtime.test.ts +``` + +Expected: TypeScript/module failure because `examples/petstore-mcp-runtime.ts` does not exist. + +**Step 3: Write a failing official-client test** + +Import non-existent `connectPetstoreMcpClient()`. Start the fixture runtime, connect the SDK-backed client, and assert that `listTools()` returns exactly the three expected names. Then call `search_available_pets` with `{ limit: 2 }` and assert the MCP result represents fixture pets Milo and Nori. Close the client then runtime in nested `finally` blocks. + +```ts +const client = await connectPetstoreMcpClient(runtime.url); +try { + expect((await client.listTools()).map((tool) => tool.name)).toEqual([ + "search_available_pets", + "get_pet", + "get_petstore_inventory", + ]); + expect(await client.callTool("search_available_pets", { limit: 2 })) + .toEqual([ + { id: 1, name: "Milo", status: "available" }, + { id: 2, name: "Nori", status: "available" }, + ]); +} finally { + await client.close(); +} +``` + +**Step 4: Run the focused test and confirm RED** + +Run: + +```bash +npm test -- --run test/petstore-mcp-client.test.ts +``` + +Expected: module failure for the absent client wrapper. + +**Step 5: Commit the test-only slice** + +```bash +git add test/petstore-mcp-runtime.test.ts test/petstore-mcp-client.test.ts +git commit -m "test: specify Petstore MCP HTTP lifecycle" +``` + +## Task 2: Compile Petstore TypeMCP tools into a session-safe HTTP handler + +**Objective:** Implement a factory that compiles a fresh TypeMCP server per Streamable HTTP session without direct client-side access to Petstore classes. + +**Files:** +- Modify: `examples/typemcp-petstore-server.ts` +- Create: `examples/petstore-mcp-handler.ts` +- Test: `test/petstore-mcp-client.test.ts` + +**Step 1: Implement `createPetstoreMcpHandler()`** + +Export a handler factory accepting a `serverFactory: () => PetstoreServer`. It must use only published TypeMCP APIs: + +```ts +import { createMcpServer } from "@theorvane/type-mcp"; +import { createMcpHandler } from "@theorvane/type-mcp/http"; + +export function createPetstoreMcpHandler(options: { + readonly serverFactory: () => PetstoreServer; +}) { + return createMcpHandler(() => { + const server = options.serverFactory(); + return createMcpServer(PetstoreServer, { + resolver: { resolve: () => server }, + }); + }); +} +``` + +Do not create an MCP server globally: each Streamable HTTP session needs its own compiled server and deterministic fixture call sequence. + +**Step 2: Run the client test and confirm it still fails** + +Run: + +```bash +npm test -- --run test/petstore-mcp-client.test.ts +``` + +Expected: failure remains because the runtime/client wrappers are absent. + +**Step 3: Add a focused compile regression assertion** + +In `test/typemcp-petstore-server.test.ts`, retain the three-tool declaration assertion but change it from subprocess-only verification to direct definition metadata where possible. Verify exact names and no tool name other than the three read-only operations. + +**Step 4: Run TypeMCP tests** + +Run: + +```bash +npm test -- --run test/typemcp-petstore-server.test.ts +``` + +Expected: PASS. + +**Step 5: Commit the handler slice** + +```bash +git add examples/petstore-mcp-handler.ts examples/typemcp-petstore-server.ts test/typemcp-petstore-server.test.ts +git commit -m "feat: compile Petstore TypeMCP HTTP handler" +``` + +## Task 3: Implement the loopback Streamable HTTP runtime + +**Objective:** Host the Fetch handler on an ephemeral local-only Node HTTP server with deterministic shutdown. + +**Files:** +- Create: `examples/petstore-mcp-runtime.ts` +- Test: `test/petstore-mcp-runtime.test.ts` + +**Step 1: Implement the Node request adapter** + +Use `node:http` and convert each incoming request into a Web `Request`, preserving request method, headers, and body only when allowed. Convert the `Response` returned from the Fetch handler back to `ServerResponse`, including headers/body/status. Keep the adapter local to this file; do not add an HTTP framework. + +**Step 2: Implement the runtime factory** + +```ts +export type PetstoreMcpRuntime = Readonly<{ + url: URL; + close(): Promise; +}>; + +export async function startPetstoreMcpRuntime(options: { + readonly serverFactory: () => PetstoreServer; +}): Promise +``` + +- Build `createPetstoreMcpHandler({ serverFactory })` once for the local runtime. +- Route only `/mcp`; answer 404 for every other pathname. +- Call `nodeServer.listen({ host: "127.0.0.1", port: 0 })` and read the assigned port from `nodeServer.address()`. +- Return URL `http://127.0.0.1:/mcp`. +- Make `close()` idempotent and resolve after `nodeServer.close()`. + +**Step 3: Run the runtime test and confirm GREEN** + +Run: + +```bash +npm test -- --run test/petstore-mcp-runtime.test.ts +``` + +Expected: PASS; no process remains listening after the test. + +**Step 4: Run formatting/type checking** + +```bash +npm run format +npm run lint +npm run build +``` + +Expected: all PASS. + +**Step 5: Commit the runtime slice** + +```bash +git add examples/petstore-mcp-runtime.ts test/petstore-mcp-runtime.test.ts +git commit -m "feat: host Petstore MCP on loopback HTTP" +``` + +## Task 4: Implement the official MCP SDK client wrapper + +**Objective:** Make protocol initialization, discovery, calls, and close explicit at the application boundary. + +**Files:** +- Create: `examples/petstore-mcp-client.ts` +- Test: `test/petstore-mcp-client.test.ts` + +**Step 1: Implement connection using official SDK classes** + +Import direct installed SDK paths: + +```ts +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +``` + +Create `new Client({ name: "petstore-example-client", version: "1.0.0" })`, create `new StreamableHTTPClientTransport(url)`, and call `client.connect(transport)`. `connect()` performs MCP initialization automatically. If it fails, close the transport before rethrowing. + +**Step 2: Implement typed wrapper operations** + +Expose: + +```ts +export interface PetstoreMcpClient { + listTools(): Promise; + callTool(name: PetstoreMcpToolName, args: Record): Promise; + close(): Promise; +} +``` + +- `listTools()` calls `client.listTools()` and returns tool descriptors. +- `callTool()` calls `client.callTool({ name, arguments: args })`. +- Parse only JSON-compatible text/structured result content needed by Petstore tools. Reject `isError` results, missing content, non-text/malformed JSON, and unexpected response shape with clear errors. +- `close()` calls `transport.terminateSession()` when a session exists, tolerates a server method-not-allowed response, and always calls `transport.close()` once. Do not invent reconnection/OAuth support. + +**Step 3: Run the client test and confirm GREEN** + +```bash +npm test -- --run test/petstore-mcp-client.test.ts +``` + +Expected: PASS and verifies real initialization, `tools/list`, and `tools/call` against the loopback TypeMCP handler. + +**Step 4: Commit the client boundary** + +```bash +git add examples/petstore-mcp-client.ts test/petstore-mcp-client.test.ts +git commit -m "feat: add Petstore MCP SDK client" +``` + +## Task 5: Build the TypeChain `@Agent()` MCP façade and agent E2E proof + +**Objective:** Ensure the annotated TypeChain agent calls the MCP client—not in-process TypeMCP or REST APIs. + +**Files:** +- Create: `examples/typechain-petstore-mcp-agent.ts` +- Create: `examples/typechain-petstore-mcp-agent-fixture.ts` +- Create: `test/typechain-petstore-mcp-agent.test.ts` +- Modify: `package.json` + +**Step 1: Write the failing agent E2E test** + +Use the fixture runner through `runExample("example:petstore:mcp-agent:fixture")`. Assert output includes: + +```ts +{ + discoveredTools: [ + "search_available_pets", + "get_pet", + "get_petstore_inventory", + ], + tool: "search_available_pets", + toolCallId: "available-pets-call", + result: [ + { id: 1, name: "Milo", status: "available" }, + { id: 2, name: "Nori", status: "available" }, + ], +} +``` + +**Step 2: Run the test and confirm RED** + +```bash +npm test -- --run test/typechain-petstore-mcp-agent.test.ts +``` + +Expected: npm script does not exist. + +**Step 3: Implement the annotated façade** + +Import `Agent`, `buildAgent`, and `Tool` from the published TypeChain subpaths. Define `PetstoreMcpAgentTools` with a connected `PetstoreMcpClient` constructor dependency. + +```ts +@Agent({ + systemPrompt: "Use the Petstore MCP tools for factual Petstore questions.", +}) +export class PetstoreMcpAgentTools { + public constructor(private readonly client: PetstoreMcpClient) {} + + @Tool({ + name: "search_available_pets", + description: "Read currently available pets through the connected MCP server.", + input: z.object({ limit: z.number().int().min(1).max(10) }), + }) + public searchAvailablePets(input: { limit: number }) { + return this.client.callTool("search_available_pets", input); + } +} +``` + +Add analogous explicit-schema façades for `get_pet` and `get_petstore_inventory`. None may import `PetstoreClient`, `PetstoreServer`, TypeMCP decorators, `createMcpServer`, or TypeMCP LangChain adapters. + +Export: + +```ts +export function buildPetstoreMcpAgent({ model, client }: { + readonly model: Parameters[1]["model"]; + readonly client: PetstoreMcpClient; +}) { + return buildAgent(new PetstoreMcpAgentTools(client), { model }); +} +``` + +**Step 4: Implement the fixture runner** + +- Start runtime with `createFixturePetstoreServer`. +- Connect `PetstoreMcpClient`. +- Get/serialize discovered tool names before invoking the agent. +- Construct `FakeToolCallingModel` with exactly one `search_available_pets({ limit: 2 })` call followed by an empty tool-call response. +- Invoke `buildPetstoreMcpAgent({ model, client })`. +- Find `ToolMessage`, parse content, print only the asserted JSON shape. +- Use nested `try/finally` to close client and runtime no matter what happens. + +**Step 5: Add the runner script and confirm GREEN** + +Add: + +```json +"example:petstore:mcp-agent:fixture": "tsx examples/typechain-petstore-mcp-agent-fixture.ts" +``` + +Run: + +```bash +npm run example:petstore:mcp-agent:fixture +npm test -- --run test/typechain-petstore-mcp-agent.test.ts +``` + +Expected: fixture JSON exactly contains `Milo`/`Nori`; test PASS. + +**Step 6: Verify the façade boundary statically** + +Add an assertion or a source-contract test showing `typechain-petstore-mcp-agent.ts` imports TypeChain agent/tool and local MCP client only, and does not import the REST client or TypeMCP runtime/adapter modules. + +**Step 7: Commit agent proof** + +```bash +git add examples/typechain-petstore-mcp-agent.ts examples/typechain-petstore-mcp-agent-fixture.ts test/typechain-petstore-mcp-agent.test.ts package.json +git commit -m "feat: add TypeChain MCP Petstore agent" +``` + +## Task 6: Remove superseded examples and rewrite documentation + +**Objective:** Leave one coherent Petstore curriculum and truthful runtime boundaries. + +**Files:** +- Delete: `examples/typechain-tool-definition.ts` +- Delete: `examples/typechain-policy-guard.ts` +- Delete: `examples/typemcp-server-definition.ts` +- Delete: `examples/typemcp-langchain-bridge.ts` +- Delete: `examples/typechain-petstore-agent.ts` +- Delete: `examples/typechain-petstore-agent-fixture.ts` +- Delete: `examples/typechain-petstore-agent-live.ts` +- Delete: `examples/typechain-petstore-real-agent.ts` +- Delete: `examples/typechain-petstore-real-agent-fixture.ts` +- Delete: `examples/typemcp-petstore-fixture.ts` +- Delete associated obsolete test files: + - `test/typechain-tool-definition.test.ts` + - `test/typechain-policy-guard.test.ts` + - `test/typemcp-server-definition.test.ts` + - `test/typemcp-langchain-bridge.test.ts` + - `test/typechain-petstore-agent.test.ts` + - `test/typechain-petstore-real-agent.test.ts` +- Modify: `package.json` +- Modify: `README.md` +- Modify: `test/dependency-security.test.ts` +- Modify: `docs/planning/2026-07-29-swagger-petstore-agent.md` +- Modify: `docs/planning/2026-07-29-real-typechain-petstore-agent.md` +- Modify: `docs/superpowers/specs/2026-07-29-petstore-real-mcp-agent-design.md` + +**Step 1: Remove obsolete scripts and files** + +Keep only these runnable scripts in addition to development commands: + +```json +"example:petstore:live": "tsx examples/petstore-live.ts", +"example:petstore:mcp-agent:fixture": "tsx examples/typechain-petstore-mcp-agent-fixture.ts" +``` + +Update `petstore-live.ts` if needed so it remains a concise optional GET-only REST smoke example. It must not be described as a real MCP client/agent demonstration. + +**Step 2: Rewrite README from scratch around one flow** + +Include: + +1. Architecture diagram: public Petstore GET → TypeMCP server → loopback HTTP MCP → SDK client → TypeChain `@Agent()` façade → LangChain agent. +2. One fixture command and the exact proof it provides (`initialize`, `tools/list`, `tools/call`, agent call). +3. One optional live REST command with public-demo variability warning. +4. Exact three tool names and GET-only fixed-host boundary. +5. Caller-owned model/provider/credentials rule. +6. Explicit statement that the loopback runtime has no production authorization, public hosting, durable sessions, deployment configuration, or live LLM configuration. + +Do not call this an external/remote production MCP service; it is a local protocol E2E example. + +**Step 3: Update historical planning docs** + +Mark old in-process plans as superseded by the new spec and link to it. Do not erase historical facts or leave them appearing active. + +**Step 4: Run full cleanup validation** + +```bash +npm run format +npm run lint +npm run build +npm test +npm run audit:prod +npm run check +npm run example:petstore:mcp-agent:fixture +npm run example:petstore:live +git diff --check +``` + +Expected: format/lint/build/tests/audit all PASS; fixture command is deterministic; live REST command is manually smoke-tested and may fail only with a clearly reported public-service error. + +**Step 5: Clean-install verification** + +```bash +rm -rf /tmp/theorvane-examples-petstore-mcp-clean +cp -a . /tmp/theorvane-examples-petstore-mcp-clean +cd /tmp/theorvane-examples-petstore-mcp-clean +rm -rf node_modules +npm ci +npm run check +npm run example:petstore:mcp-agent:fixture +``` + +Expected: clean install passes with no uncommitted generated output. + +**Step 6: Commit cleanup/documentation** + +```bash +git add -A +git commit -m "refactor: center examples on Petstore MCP agent" +``` + +## Task 7: PR, independent review, and release promotion + +**Objective:** Deliver the scoped rewrite through the protected branch workflow. + +**Files:** +- GitHub issue: `Theorvane/examples#27` +- Feature branch: `feat/27-petstore-real-mcp-agent` created from current `origin/dev` +- PR: feature branch → `dev` +- Release issue/PR: current reviewed `dev` → `main` + +**Step 1: Rebase/recreate implementation branch from current dev** + +After design-doc approval, create `feat/27-petstore-real-mcp-agent` from the current `origin/dev`. Cherry-pick the spec/plan commits if the docs branch is separate; preserve the issue link. + +**Step 2: Create the feature PR** + +PR body must include: + +- closes #27; +- fixed GET-only Petstore scope; +- test proof for MCP initialize/list/call and `@Agent()` façade path; +- deletion list for superseded in-process/catalog examples; +- no credential/provider/production hosting claims; +- full local and clean-install verification commands/results. + +**Step 3: Verify exact PR head before review** + +```bash +gh pr view --repo Theorvane/examples --json headRefOid,mergeable,mergeStateStatus,reviewDecision,statusCheckRollup +``` + +Require exact-head `verify`, no unresolved review threads, and independent review from `sjungwon03-ai`/the code-reviewer profile. Reviewers inspect GitHub source/diff/Actions/rules only; they do not run local commands. + +**Step 4: Merge only after exact-head approval/checks** + +Merge feature PR into `dev` according to the dev ruleset. Confirm the issue closes and capture the resulting `dev` head. + +**Step 5: Preserve main ancestry when needed** + +Before `dev → main`, require `git merge-base --is-ancestor origin/main origin/dev`. If false because a prior production promotion was squash-merged, create a separate two-parent reconciliation issue/PR that adds no product diff, get an exact-head review/check, and merge it into `dev` before release promotion. + +**Step 6: Create release PR and repeat independent verification** + +Create a fresh release issue and PR from current `dev` to `main`. Require exact source head, successful `verify` and `release-promotion`, independent fresh approval, resolved threads, and the repository-required merge method. Do not treat feature-PR approval as release approval. + +**Step 7: Production verification and closure** + +After merge, clone `main` cleanly and run: + +```bash +npm ci +npm run check +npm run example:petstore:mcp-agent:fixture +``` + +Verify post-merge GitHub Actions success at canonical `main`, close the release issue, and ensure no open implementation/release PR remains. diff --git a/docs/planning/2026-07-29-real-typechain-petstore-agent.md b/docs/planning/2026-07-29-real-typechain-petstore-agent.md index 3080906..7e4ccc5 100644 --- a/docs/planning/2026-07-29-real-typechain-petstore-agent.md +++ b/docs/planning/2026-07-29-real-typechain-petstore-agent.md @@ -1,5 +1,7 @@ # Real TypeChain Petstore Agent Implementation Plan +> **Status: superseded.** This plan documented the prior in-process `createTypeMcpAgent()` demonstration. The active [real MCP HTTP agent design](../superpowers/specs/2026-07-29-petstore-real-mcp-agent-design.md) instead verifies a loopback Streamable HTTP MCP server, official MCP SDK client, and TypeChain `@Agent()` / `buildAgent()` façade. + > **For Hermes:** Implement this issue-scoped plan with test-first slices and preserve the `dev` → release-only `main` workflow. **Goal:** Add a real LangChain agent factory that uses TypeChain `createTypeMcpAgent()` to operate the existing read-only Swagger Petstore TypeMCP tools, while keeping provider credentials and runtime selection application-owned. diff --git a/docs/planning/2026-07-29-swagger-petstore-agent.md b/docs/planning/2026-07-29-swagger-petstore-agent.md index b95d17c..689c01b 100644 --- a/docs/planning/2026-07-29-swagger-petstore-agent.md +++ b/docs/planning/2026-07-29-swagger-petstore-agent.md @@ -1,6 +1,8 @@ -# Swagger Petstore Agent Examples Implementation Plan +# Swagger Petstore Agent Implementation Plan + +> **Status: superseded.** The initial in-process TypeMCP adapter workflow described below was replaced by the [real MCP HTTP agent design](../superpowers/specs/2026-07-29-petstore-real-mcp-agent-design.md) and its [implementation plan](2026-07-29-petstore-real-mcp-agent-implementation.md). The active example uses loopback Streamable HTTP, an official MCP SDK client, and TypeChain `@Agent()` / `buildAgent()` façade tools. + -> **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. diff --git a/docs/superpowers/specs/2026-07-29-petstore-real-mcp-agent-design.md b/docs/superpowers/specs/2026-07-29-petstore-real-mcp-agent-design.md new file mode 100644 index 0000000..6cd1bf0 --- /dev/null +++ b/docs/superpowers/specs/2026-07-29-petstore-real-mcp-agent-design.md @@ -0,0 +1,158 @@ +# Petstore Real MCP Agent Design + +**Issue:** [#27](https://github.com/Theorvane/examples/issues/27) +**Status:** Implementing +**Decision:** Replace the mixed catalog and in-process-only Petstore examples with one deterministic, end-to-end Swagger Petstore scenario. TypeMCP exposes the Petstore tools through local Streamable HTTP; an MCP SDK client consumes that service; a TypeChain `@Agent()` class invokes the MCP client through `@Tool()` façades. + +## Goal + +Make the repository demonstrate a truthful, runnable path from a public read-only REST API to a real MCP client session and then to a TypeChain `@Agent()`-built LangChain agent. + +The end-to-end test must prove this sequence: + +```text +FakeToolCallingModel + → LangChain agent built by TypeChain buildAgent() + → @Agent() PetstoreMcpAgentTools + → @Tool() MCP client façade + → MCP SDK Streamable HTTP client session + → loopback HTTP server (127.0.0.1, ephemeral port) + → TypeMCP createMcpHandler() / createMcpServer() + → @McpServer() / @McpTool() PetstoreServer + → injected fixture PetstoreClient +``` + +A separate live command may use the public Swagger Petstore v2 endpoint, but deterministic tests and CI never depend on it. + +## Scope + +### Included + +- One Petstore-only example structure. +- `PetstoreClient` fixed to `https://petstore.swagger.io/v2`, with only validated `GET` operations: + - `findAvailablePets()` + - `getPet(petId)` + - `getInventory()` +- A TypeMCP `@McpServer()` / `@McpTool()` wrapper exposing exactly: + - `search_available_pets` + - `get_pet` + - `get_petstore_inventory` +- A Fetch-compatible Streamable HTTP handler built with `createMcpHandler()` and `createMcpServer()`. +- A local runtime helper that binds a Node HTTP server only to `127.0.0.1` on port `0`, exposes an `/mcp` URL, and has an explicit asynchronous `close()` lifecycle. +- An MCP SDK client helper that connects to the loopback endpoint and exposes tool-list and tool-call operations. +- A TypeChain class decorated with `@Agent()` and `@Tool()`. Its tool methods only delegate to the MCP SDK client; they never call `PetstoreClient`, `PetstoreServer`, or TypeMCP adapters directly. +- Agent construction with `buildAgent()` and a caller-provided LangChain-compatible model. +- A fixture E2E runner/test that starts the loopback runtime, connects the MCP client, validates `tools/list`, then proves a `FakeToolCallingModel`-driven agent calls `search_available_pets` through MCP and returns fixture pets. +- README and focused planning/spec documentation covering the protocol boundary and lifecycle. + +### Removed or replaced + +- Generic catalog examples unrelated to Petstore: + - TypeChain tool-definition example + - TypeChain policy-guard example + - TypeMCP server-definition example + - TypeMCP-to-TypeChain in-process bridge example +- The current in-process TypeMCP adapter workflow and factory. Their tests, scripts, and README references are removed because they do not prove an MCP client/session boundary. + +### Excluded + +- Public write methods (create, update, delete), credentials, API keys, custom API hosts, or environment-configured Petstore behavior. +- External deployment, public listening interfaces, authentication, authorization, durable MCP session storage, retries, audit persistence, or observability infrastructure. +- stdio transport, an HTTP framework dependency, provider SDKs, live model credentials, and automated live LLM calls. +- A claim that TypeChain itself implements an MCP client. The MCP SDK client is an application integration dependency used by the example; TypeChain operates the annotated façade tools. + +## Architecture + +### Petstore API boundary + +`PetstoreClient` remains the only component that performs public REST I/O. It owns fixed URL construction, GET method selection, timeout, HTTP failure handling, and Zod response normalization. It accepts injected `fetch` for fixtures. + +### TypeMCP server boundary + +`PetstoreServer` carries TypeMCP metadata and invokes its injected `PetstoreClient`. A handler factory compiles a fresh decorated server for each MCP session via: + +```ts +createMcpHandler(() => + createMcpServer(PetstoreServer, { + resolver: { resolve: () => petstoreServer }, + }), +); +``` + +The resolver must return a session-safe server instance. Tests use fixture-backed instances and live commands use a live client. The handler owns SDK protocol/session routing; the application runtime owns route hosting and shutdown. + +### Loopback runtime boundary + +`startPetstoreMcpRuntime()` creates a Node HTTP server whose request handler routes only `/mcp` to the Fetch handler. It listens on `127.0.0.1` and port `0`, returning: + +```ts +type PetstoreMcpRuntime = { + readonly url: URL; + close(): Promise; +}; +``` + +The helper must reject other paths and must close the HTTP server on test cleanup. It does not expose a public deployment recipe. + +### MCP client boundary + +An SDK client wrapper receives the runtime URL, creates an official Streamable HTTP client transport, connects, and exposes typed `listTools()` and `callTool()` operations. It must provide `close()` so tests always end client sessions before stopping the runtime. + +The wrapper must preserve JSON-compatible tool arguments/results and throw useful errors for protocol failures or malformed MCP tool content. It must not reach into TypeMCP metadata or call Petstore classes directly. + +### TypeChain `@Agent()` boundary + +`PetstoreMcpAgentTools` receives the connected MCP client wrapper. It is decorated with: + +- `@Agent({ systemPrompt })` from `@theorvane/type-chain/agent` +- explicit-schema `@Tool()` methods from the TypeChain package + +Each decorated method makes exactly one corresponding MCP `tools/call` request. `buildPetstoreMcpAgent({ model, client })` creates the runtime using: + +```ts +buildAgent(new PetstoreMcpAgentTools(client), { model }); +``` + +The model remains application-supplied. No provider, model identifier, credential, or environment lookup appears in the repository. + +## Error handling and lifecycle + +- Invalid REST response, HTTP error, or timeout propagates from `PetstoreClient` through the MCP tool result in a safe, actionable form. +- Failed MCP connection closes the partially allocated client transport. +- The runtime closes even if initialization, discovery, or agent invocation fails; tests use `try/finally`. +- The loopback helper refuses unexpected paths rather than accidentally serving a general HTTP surface. +- Tests use fixture fetch data and an ephemeral port, avoiding public API availability and port collisions. + +## Test plan + +1. Retain focused fixture tests for REST URL/method/validation behavior. +2. Test TypeMCP declaration names and read-only input schemas. +3. Test the loopback runtime using an official MCP client: + - connection/initialization succeeds; + - `tools/list` contains exactly the three declared tools; + - `tools/call` invokes fixture data through the protocol. +4. Test the agent E2E path with `FakeToolCallingModel`: + - model requests `search_available_pets`; + - `@Tool()` façade invokes the connected MCP client; + - the returned `ToolMessage` contains the fixture `Milo` and `Nori` records. +5. Test cleanup by closing the client and runtime; no persistent server process remains. +6. Run full `npm run check`, all fixture commands, `npm run audit:prod`, and `git diff --check`. + +## Documentation contract + +README will contain one architecture diagram, one fixture command, one optional live REST smoke command, and concise boundaries: + +- fixture E2E validates a real local Streamable HTTP MCP session; +- the `@Agent()` façade uses the MCP client, not direct Petstore or TypeMCP calls; +- the live Petstore REST API is public mutable demo infrastructure; +- provider/model configuration and credentials are application-owned; +- loopback hosting and ephemeral sessions are example-only, not a production security/deployment policy. + +## Acceptance criteria + +- The old generic and in-process-only examples, tests, scripts, and README rows are gone. +- The repository has exactly one documented Petstore flow. +- Test evidence proves MCP `initialize`, `tools/list`, and `tools/call` across actual loopback HTTP. +- Test evidence proves TypeChain `@Agent()` / `buildAgent()` tool selection reaches the MCP client façade. +- No code path adds Petstore write requests, credentials, provider packages, live LLM calls, or external server exposure. +- The repository check suite, production dependency audit, and example runner pass from a clean install. diff --git a/examples/petstore-mcp-client.ts b/examples/petstore-mcp-client.ts new file mode 100644 index 0000000..7658ae7 --- /dev/null +++ b/examples/petstore-mcp-client.ts @@ -0,0 +1,79 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js"; + +export const petstoreMcpToolNames = [ + "search_available_pets", + "get_pet", + "get_petstore_inventory", +] as const; + +export type PetstoreMcpToolName = (typeof petstoreMcpToolNames)[number]; + +export type PetstoreMcpClient = Readonly<{ + listTools(): Promise; + callTool( + name: PetstoreMcpToolName, + args: Record, + ): Promise; + close(): Promise; +}>; + +/** Connects an official MCP SDK client to a loopback Streamable HTTP endpoint. */ +export async function connectPetstoreMcpClient( + url: URL, +): Promise { + const client = new Client({ + name: "petstore-example-client", + version: "1.0.0", + }); + const transport = new StreamableHTTPClientTransport(url); + try { + await client.connect(transport as Parameters[0]); + } catch (error) { + await transport.close(); + throw error; + } + let closed = false; + + return { + listTools: async () => + (await client.listTools()).tools.map(({ name }) => ({ name })), + callTool: async (name, args) => { + const result = await client.callTool( + { name, arguments: args }, + CallToolResultSchema, + ); + const parsed = CallToolResultSchema.safeParse(result); + if (!parsed.success) { + throw new Error(`MCP tool ${name} returned an invalid protocol result`); + } + if (parsed.data.isError === true) { + throw new Error(`MCP tool ${name} returned an error`); + } + const content = parsed.data.content[0]; + if (content === undefined || content.type !== "text") { + throw new Error(`MCP tool ${name} returned no text result`); + } + try { + return JSON.parse(content.text) as unknown; + } catch { + throw new Error(`MCP tool ${name} returned invalid JSON text`); + } + }, + close: async () => { + if (closed) { + return; + } + closed = true; + if (transport.sessionId !== undefined) { + try { + await transport.terminateSession(); + } catch { + // The endpoint may decline DELETE; local transport shutdown still follows. + } + } + await transport.close(); + }, + }; +} diff --git a/examples/petstore-mcp-fixture.ts b/examples/petstore-mcp-fixture.ts new file mode 100644 index 0000000..b939878 --- /dev/null +++ b/examples/petstore-mcp-fixture.ts @@ -0,0 +1,42 @@ +import { createFixturePetstoreServer } from "./petstore-fixture.js"; +import { connectPetstoreMcpClient } from "./petstore-mcp-client.js"; +import { startPetstoreMcpRuntime } from "./petstore-mcp-runtime.js"; + +export async function run(): Promise { + const runtime = await startPetstoreMcpRuntime({ + serverFactory: createFixturePetstoreServer, + }); + try { + const unexpectedPathStatus = (await fetch(new URL("/not-mcp", runtime.url))) + .status; + const client = await connectPetstoreMcpClient(runtime.url); + try { + const discoveredTools = (await client.listTools()).map( + (tool) => tool.name, + ); + const available = await client.callTool("search_available_pets", { + limit: 2, + }); + console.log( + JSON.stringify( + { + url: runtime.url.toString(), + unexpectedPathStatus, + discoveredTools, + available, + }, + null, + 2, + ), + ); + } finally { + await client.close(); + } + } finally { + await runtime.close(); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await run(); +} diff --git a/examples/petstore-mcp-handler.ts b/examples/petstore-mcp-handler.ts new file mode 100644 index 0000000..739e76d --- /dev/null +++ b/examples/petstore-mcp-handler.ts @@ -0,0 +1,20 @@ +import { createMcpServer } from "@theorvane/type-mcp"; +import { createMcpHandler } from "@theorvane/type-mcp/http"; + +import { PetstoreServer } from "./typemcp-petstore-server.js"; + +export type PetstoreMcpHandlerOptions = Readonly<{ + serverFactory: () => PetstoreServer; +}>; + +/** Creates a session-safe Streamable HTTP handler for the read-only Petstore tools. */ +export function createPetstoreMcpHandler({ + serverFactory, +}: PetstoreMcpHandlerOptions) { + return createMcpHandler(async () => { + const server = serverFactory(); + return createMcpServer(PetstoreServer, { + resolve: () => server, + }); + }); +} diff --git a/examples/petstore-mcp-runtime.ts b/examples/petstore-mcp-runtime.ts new file mode 100644 index 0000000..c300892 --- /dev/null +++ b/examples/petstore-mcp-runtime.ts @@ -0,0 +1,108 @@ +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from "node:http"; +import type { AddressInfo } from "node:net"; + +import { createPetstoreMcpHandler } from "./petstore-mcp-handler.js"; +import type { PetstoreServer } from "./typemcp-petstore-server.js"; + +export type PetstoreMcpRuntime = Readonly<{ + url: URL; + close(): Promise; +}>; + +export type StartPetstoreMcpRuntimeOptions = Readonly<{ + serverFactory: () => PetstoreServer; +}>; + +/** Hosts the Petstore MCP handler only on an ephemeral local loopback address. */ +export async function startPetstoreMcpRuntime({ + serverFactory, +}: StartPetstoreMcpRuntimeOptions): Promise { + const handler = createPetstoreMcpHandler({ serverFactory }); + const nodeServer = createServer(async (request, response) => { + try { + const url = new URL(request.url ?? "/", "http://127.0.0.1"); + if (url.pathname !== "/mcp") { + response.writeHead(404).end(); + return; + } + await writeFetchResponse( + response, + await handler(await toFetchRequest(request)), + ); + } catch (error) { + response.writeHead(500, { "content-type": "application/json" }).end( + JSON.stringify({ + error: error instanceof Error ? error.message : "Unknown error", + }), + ); + } + }); + + await new Promise((resolve, reject) => { + nodeServer.once("error", reject); + nodeServer.listen({ host: "127.0.0.1", port: 0 }, () => { + nodeServer.off("error", reject); + resolve(); + }); + }); + const address = nodeServer.address(); + if (address === null || typeof address === "string") { + await closeServer(nodeServer); + throw new Error("Expected the Petstore MCP runtime to bind a TCP address"); + } + const port = (address as AddressInfo).port; + let closed = false; + + return { + url: new URL(`http://127.0.0.1:${port}/mcp`), + close: async () => { + if (closed) { + return; + } + closed = true; + await closeServer(nodeServer); + }, + }; +} + +async function toFetchRequest(request: IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + const headers = new Headers(); + for (const [name, value] of Object.entries(request.headers)) { + if (value !== undefined) { + headers.set(name, Array.isArray(value) ? value.join(", ") : value); + } + } + const method = request.method ?? "GET"; + const body = + chunks.length === 0 || method === "GET" || method === "HEAD" + ? undefined + : Buffer.concat(chunks); + return new Request(`http://127.0.0.1${request.url ?? "/"}`, { + method, + headers, + ...(body === undefined ? {} : { body }), + }); +} + +async function writeFetchResponse( + response: ServerResponse, + fetchResponse: Response, +): Promise { + const headers = Object.fromEntries(fetchResponse.headers.entries()); + response.writeHead(fetchResponse.status, headers); + response.end(Buffer.from(await fetchResponse.arrayBuffer())); +} + +function closeServer(server: ReturnType): Promise { + return new Promise((resolve, reject) => { + server.close((error) => (error === undefined ? resolve() : reject(error))); + }); +} diff --git a/examples/typechain-petstore-agent-fixture.ts b/examples/typechain-petstore-agent-fixture.ts deleted file mode 100644 index 97a7430..0000000 --- a/examples/typechain-petstore-agent-fixture.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 4138fdf..0000000 --- a/examples/typechain-petstore-agent-live.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 100e868..0000000 --- a/examples/typechain-petstore-agent.ts +++ /dev/null @@ -1,67 +0,0 @@ -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/typechain-petstore-mcp-agent-fixture.ts b/examples/typechain-petstore-mcp-agent-fixture.ts new file mode 100644 index 0000000..99baddc --- /dev/null +++ b/examples/typechain-petstore-mcp-agent-fixture.ts @@ -0,0 +1,67 @@ +import { ToolMessage } from "@langchain/core/messages"; +import { FakeToolCallingModel } from "langchain"; +import { createFixturePetstoreServer } from "./petstore-fixture.js"; +import { connectPetstoreMcpClient } from "./petstore-mcp-client.js"; +import { startPetstoreMcpRuntime } from "./petstore-mcp-runtime.js"; +import { buildPetstoreMcpAgent } from "./typechain-petstore-mcp-agent.js"; + +export async function run(): Promise { + const runtime = await startPetstoreMcpRuntime({ + serverFactory: createFixturePetstoreServer, + }); + try { + const client = await connectPetstoreMcpClient(runtime.url); + try { + const discoveredTools = (await client.listTools()).map( + (tool) => tool.name, + ); + const agent = buildPetstoreMcpAgent({ + model: new FakeToolCallingModel({ + toolCalls: [ + [ + { + id: "available-pets-call", + name: "search_available_pets", + args: { limit: 2 }, + }, + ], + [], + ], + }), + client, + }); + const result = await agent.invoke({ + messages: [ + { + role: "user", + content: "Which pets are currently available?", + }, + ], + }); + const toolMessage = result.messages.find(ToolMessage.isInstance); + if (!toolMessage || typeof toolMessage.content !== "string") { + throw new Error("Expected the TypeChain agent to execute an MCP tool."); + } + console.log( + JSON.stringify( + { + discoveredTools, + tool: "search_available_pets", + toolCallId: toolMessage.tool_call_id, + result: JSON.parse(toolMessage.content) as unknown, + }, + null, + 2, + ), + ); + } finally { + await client.close(); + } + } finally { + await runtime.close(); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await run(); +} diff --git a/examples/typechain-petstore-mcp-agent.ts b/examples/typechain-petstore-mcp-agent.ts new file mode 100644 index 0000000..17e1b2f --- /dev/null +++ b/examples/typechain-petstore-mcp-agent.ts @@ -0,0 +1,61 @@ +import { Tool } from "@theorvane/type-chain"; +import { Agent, buildAgent } from "@theorvane/type-chain/agent"; +import { z } from "zod"; + +import type { PetstoreMcpClient } from "./petstore-mcp-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({}); + +@Agent({ + systemPrompt: "Use the Petstore MCP tools for factual Petstore questions.", +}) +export class PetstoreMcpAgentTools { + public constructor(private readonly client: PetstoreMcpClient) {} + + @Tool({ + name: "search_available_pets", + description: + "Read currently available pets through the connected MCP server.", + schema: listInput, + }) + public searchAvailablePets( + input: z.infer, + ): Promise { + return this.client.callTool("search_available_pets", input); + } + + @Tool({ + name: "get_pet", + description: "Read a pet by numeric ID through the connected MCP server.", + schema: petInput, + }) + public getPet(input: z.infer): Promise { + return this.client.callTool("get_pet", input); + } + + @Tool({ + name: "get_petstore_inventory", + description: "Read Petstore inventory through the connected MCP server.", + schema: emptyInput, + }) + public getPetstoreInventory( + input: z.infer, + ): Promise { + return this.client.callTool("get_petstore_inventory", input); + } +} + +export type BuildPetstoreMcpAgentOptions = Readonly<{ + model: Parameters[1]["model"]; + client: PetstoreMcpClient; +}>; + +/** Builds a TypeChain @Agent whose tools delegate only to an MCP SDK client. */ +export function buildPetstoreMcpAgent({ + model, + client, +}: BuildPetstoreMcpAgentOptions) { + return buildAgent(new PetstoreMcpAgentTools(client), { model }); +} diff --git a/examples/typechain-petstore-real-agent-fixture.ts b/examples/typechain-petstore-real-agent-fixture.ts deleted file mode 100644 index db2b333..0000000 --- a/examples/typechain-petstore-real-agent-fixture.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ToolMessage } from "@langchain/core/messages"; -import { FakeToolCallingModel } from "langchain"; - -import { createFixturePetstoreServer } from "./petstore-fixture.js"; -import { createPetstoreAgent } from "./typechain-petstore-real-agent.js"; - -export async function run(): Promise { - const agent = await createPetstoreAgent({ - model: new FakeToolCallingModel({ - toolCalls: [ - [ - { - id: "available-pets-call", - name: "search_available_pets", - args: { limit: 2 }, - }, - ], - [], - ], - }), - server: createFixturePetstoreServer(), - }); - const result = await agent.invoke({ - messages: [ - { - role: "user", - content: "Which pets are currently available?", - }, - ], - }); - const toolMessage = result.messages.find(ToolMessage.isInstance); - if (!toolMessage) { - throw new Error("Expected the LangChain agent to execute a Petstore tool."); - } - - console.log( - JSON.stringify( - { - tool: "search_available_pets", - toolCallId: toolMessage.tool_call_id, - result: JSON.parse(toolMessage.content as string) as unknown, - }, - null, - 2, - ), - ); -} - -if (import.meta.url === `file://${process.argv[1]}`) { - await run(); -} diff --git a/examples/typechain-petstore-real-agent.ts b/examples/typechain-petstore-real-agent.ts deleted file mode 100644 index 30d2ede..0000000 --- a/examples/typechain-petstore-real-agent.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createTypeMcpAgent } from "@theorvane/type-chain/typemcp"; - -import { PetstoreServer } from "./typemcp-petstore-server.js"; - -type PetstoreAgentModel = Parameters< - typeof createTypeMcpAgent ->[0]["model"]; - -export type CreatePetstoreAgentOptions = Readonly<{ - /** The application selects, configures, and authenticates this LangChain model. */ - model: PetstoreAgentModel; - /** Optional explicit Petstore resolver target; defaults to the live read-only server. */ - server?: PetstoreServer; -}>; - -/** - * Builds a real LangChain agent from the in-process TypeMCP Petstore tools. - * It does not choose a model provider, read credentials, or start an MCP transport. - */ -export async function createPetstoreAgent({ - model, - server, -}: CreatePetstoreAgentOptions) { - const resolvedServer = server ?? new PetstoreServer(); - return createTypeMcpAgent({ - model, - server: PetstoreServer, - resolver: { resolve: () => resolvedServer }, - }); -} diff --git a/examples/typechain-policy-guard.ts b/examples/typechain-policy-guard.ts deleted file mode 100644 index cf2d51e..0000000 --- a/examples/typechain-policy-guard.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Policy, Tool, withToolPolicyGuard } from "@theorvane/type-chain"; -import { z } from "zod"; - -export class ArchiveTools { - @Policy({ approval: "required", audit: "required" }) - @Tool({ - name: "archive_project", - description: - "Archive a project after an application-owned approval decision.", - schema: z.object({ projectId: z.string().min(1), approved: z.boolean() }), - }) - public archiveProject(input: { - readonly projectId: string; - readonly approved: boolean; - }) { - return { archived: true, projectId: input.projectId }; - } -} - -export async function run(): Promise { - const auditEvents: string[] = []; - const [archive] = withToolPolicyGuard( - new ArchiveTools(), - ({ input, policy }) => { - const request = input as { - readonly projectId: string; - readonly approved: boolean; - }; - auditEvents.push( - `approval=${policy.approval};project=${request.projectId}`, - ); - if (!request.approved) { - throw new Error( - "The application did not approve this archive request.", - ); - } - }, - ); - - if (!archive) { - throw new Error("Expected one guarded archive tool."); - } - - console.log( - JSON.stringify( - { - auditEvents, - result: await archive.invoke({ - projectId: "project-42", - approved: true, - }), - }, - null, - 2, - ), - ); -} - -if (import.meta.url === `file://${process.argv[1]}`) { - await run(); -} diff --git a/examples/typechain-tool-definition.ts b/examples/typechain-tool-definition.ts deleted file mode 100644 index 4b57896..0000000 --- a/examples/typechain-tool-definition.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getToolDefinitions, Tool } from "@theorvane/type-chain"; -import { z } from "zod"; - -const catalog = new Map([ - ["sku-1", { id: "sku-1", name: "Notebook", inStock: true }], - ["sku-2", { id: "sku-2", name: "Mechanical keyboard", inStock: false }], -]); - -export class CatalogTools { - @Tool({ - name: "find_product", - description: "Find a catalog product by its identifier.", - schema: z.object({ id: z.string().min(1) }), - }) - public findProduct(input: { readonly id: string }) { - return catalog.get(input.id) ?? null; - } -} - -export function getCatalogTool() { - const [tool] = getToolDefinitions(new CatalogTools()); - if (!tool) { - throw new Error("Expected one decorated catalog tool."); - } - return tool; -} - -export function run(): void { - const tool = getCatalogTool(); - console.log( - JSON.stringify( - { name: tool.name, result: tool.invoke({ id: "sku-1" }) }, - null, - 2, - ), - ); -} - -if (import.meta.url === `file://${process.argv[1]}`) { - run(); -} diff --git a/examples/typemcp-langchain-bridge.ts b/examples/typemcp-langchain-bridge.ts deleted file mode 100644 index fe41b51..0000000 --- a/examples/typemcp-langchain-bridge.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createTypeMcpLangChainTools } from "@theorvane/type-chain/typemcp"; - -import { CatalogServer } from "./typemcp-server-definition.js"; - -export async function run(): Promise { - const tools = await createTypeMcpLangChainTools(CatalogServer, { - resolver: { resolve: () => new CatalogServer() }, - }); - const [findProduct] = tools; - if (!findProduct) { - throw new Error("Expected the TypeMCP catalog tool to be adapted."); - } - - const result = await findProduct.invoke({ id: "sku-1" }); - console.log(JSON.stringify({ name: findProduct.name, result }, null, 2)); -} - -if (import.meta.url === `file://${process.argv[1]}`) { - await run(); -} diff --git a/examples/typemcp-petstore-fixture.ts b/examples/typemcp-petstore-fixture.ts deleted file mode 100644 index 6600a50..0000000 --- a/examples/typemcp-petstore-fixture.ts +++ /dev/null @@ -1,33 +0,0 @@ -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-server-definition.ts b/examples/typemcp-server-definition.ts deleted file mode 100644 index 2c6bbc5..0000000 --- a/examples/typemcp-server-definition.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - createMcpServer, - getMcpServerDefinition, - McpServer, - McpTool, -} from "@theorvane/type-mcp"; -import { z } from "zod"; - -@McpServer({ name: "catalog", version: "1.0.0" }) -export class CatalogServer { - @McpTool({ - name: "find_product", - description: "Find a product in the local catalog.", - input: z.object({ id: z.string().min(1) }), - }) - public findProduct(input: { readonly id: string }) { - return { id: input.id, name: "Notebook", inStock: true }; - } -} - -export function describeCatalogServer() { - const definition = getMcpServerDefinition(CatalogServer); - if (!definition) { - throw new Error("Expected a decorated MCP server definition."); - } - return definition; -} - -export async function compileCatalogServer() { - return createMcpServer(CatalogServer); -} - -export async function run(): Promise { - const definition = describeCatalogServer(); - const server = await compileCatalogServer(); - console.log( - JSON.stringify( - { - name: definition.name, - tools: definition.tools.map((tool) => tool.name), - compiled: Boolean(server), - }, - null, - 2, - ), - ); -} - -if (import.meta.url === `file://${process.argv[1]}`) { - await run(); -} diff --git a/package.json b/package.json index 1df3527..2650088 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@theorvane/examples", "version": "0.1.0", "private": true, - "description": "Runnable TypeChain and TypeMCP examples with explicit runtime boundaries.", + "description": "Runnable read-only Petstore TypeMCP HTTP and TypeChain agent example.", "type": "module", "engines": { "node": ">=20" @@ -11,19 +11,13 @@ "audit:prod": "npm audit --omit=dev --audit-level=low", "build": "tsc --noEmit", "check": "npm run format:check && npm run build && npm test && npm run audit:prod", - "example:typechain": "tsx examples/typechain-tool-definition.ts", - "example:policy": "tsx examples/typechain-policy-guard.ts", - "example:typemcp": "tsx examples/typemcp-server-definition.ts", - "example:bridge": "tsx examples/typemcp-langchain-bridge.ts", "format": "biome format --write .", "format:check": "biome format .", "lint": "biome check .", "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", - "example:petstore:agent:real:fixture": "tsx examples/typechain-petstore-real-agent-fixture.ts" + "example:petstore:mcp:fixture": "tsx examples/petstore-mcp-fixture.ts", + "example:petstore:mcp-agent:fixture": "tsx examples/typechain-petstore-mcp-agent-fixture.ts" }, "dependencies": { "@langchain/core": "^1.2.3", diff --git a/test/petstore-mcp-client.test.ts b/test/petstore-mcp-client.test.ts new file mode 100644 index 0000000..bb76cb2 --- /dev/null +++ b/test/petstore-mcp-client.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { runExample } from "./run-example.js"; + +describe("Petstore MCP SDK client", () => { + it("initializes, lists tools, and calls fixture data over loopback HTTP", () => { + expect(runExample("example:petstore:mcp:fixture")).toMatchObject({ + discoveredTools: [ + "search_available_pets", + "get_pet", + "get_petstore_inventory", + ], + available: [ + { id: 1, name: "Milo", status: "available" }, + { id: 2, name: "Nori", status: "available" }, + ], + }); + }); +}); diff --git a/test/petstore-mcp-runtime.test.ts b/test/petstore-mcp-runtime.test.ts new file mode 100644 index 0000000..4b5caee --- /dev/null +++ b/test/petstore-mcp-runtime.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; + +import { runExample } from "./run-example.js"; + +describe("Petstore Streamable HTTP runtime", () => { + it("binds only an ephemeral loopback MCP route", () => { + expect(runExample("example:petstore:mcp:fixture")).toMatchObject({ + url: expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+\/mcp$/), + unexpectedPathStatus: 404, + }); + }); +}); diff --git a/test/typechain-petstore-agent.test.ts b/test/typechain-petstore-agent.test.ts deleted file mode 100644 index d5d6406..0000000 --- a/test/typechain-petstore-agent.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -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/typechain-petstore-real-agent.test.ts b/test/typechain-petstore-mcp-agent.test.ts similarity index 50% rename from test/typechain-petstore-real-agent.test.ts rename to test/typechain-petstore-mcp-agent.test.ts index 32df43c..8177dbe 100644 --- a/test/typechain-petstore-real-agent.test.ts +++ b/test/typechain-petstore-mcp-agent.test.ts @@ -2,9 +2,14 @@ import { describe, expect, it } from "vitest"; import { runExample } from "./run-example.js"; -describe("real TypeChain Swagger Petstore agent", () => { - it("runs a LangChain agent loop that selects and executes the TypeMCP search tool", () => { - expect(runExample("example:petstore:agent:real:fixture")).toEqual({ +describe("TypeChain Petstore MCP agent", () => { + it("uses an annotated facade to select and execute a Petstore MCP tool", () => { + expect(runExample("example:petstore:mcp-agent:fixture")).toEqual({ + discoveredTools: [ + "search_available_pets", + "get_pet", + "get_petstore_inventory", + ], tool: "search_available_pets", toolCallId: "available-pets-call", result: [ diff --git a/test/typechain-policy-guard.test.ts b/test/typechain-policy-guard.test.ts deleted file mode 100644 index ed2aa1b..0000000 --- a/test/typechain-policy-guard.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { runExample } from "./run-example.js"; - -describe("TypeChain policy guard", () => { - it("demonstrates an application-owned approval and audit decision", () => { - expect(runExample("example:policy")).toEqual({ - auditEvents: ["approval=required;project=project-42"], - result: { archived: true, projectId: "project-42" }, - }); - }); -}); diff --git a/test/typechain-tool-definition.test.ts b/test/typechain-tool-definition.test.ts deleted file mode 100644 index 192505f..0000000 --- a/test/typechain-tool-definition.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { runExample } from "./run-example.js"; - -describe("TypeChain tool definition", () => { - it("records metadata and preserves a receiver-bound invocation", () => { - expect(runExample("example:typechain")).toEqual({ - name: "find_product", - result: { id: "sku-1", name: "Notebook", inStock: true }, - }); - }); -}); diff --git a/test/typemcp-langchain-bridge.test.ts b/test/typemcp-langchain-bridge.test.ts deleted file mode 100644 index 90e964a..0000000 --- a/test/typemcp-langchain-bridge.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { runExample } from "./run-example.js"; - -describe("TypeMCP to TypeChain bridge", () => { - it("adapts a TypeMCP declaration to an in-process LangChain tool", () => { - expect(runExample("example:bridge")).toEqual({ - name: "find_product", - result: '{"id":"sku-1","name":"Notebook","inStock":true}', - }); - }); -}); diff --git a/test/typemcp-petstore-server.test.ts b/test/typemcp-petstore-server.test.ts deleted file mode 100644 index 8f7d1ee..0000000 --- a/test/typemcp-petstore-server.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -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, - }); - }); -}); diff --git a/test/typemcp-server-definition.test.ts b/test/typemcp-server-definition.test.ts deleted file mode 100644 index fb61292..0000000 --- a/test/typemcp-server-definition.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { runExample } from "./run-example.js"; - -describe("TypeMCP server definition", () => { - it("records a tool declaration and compiles a server", () => { - expect(runExample("example:typemcp")).toEqual({ - name: "catalog", - tools: ["find_product"], - compiled: true, - }); - }); -}); diff --git a/test/verify-workflow.test.ts b/test/verify-workflow.test.ts new file mode 100644 index 0000000..5227476 --- /dev/null +++ b/test/verify-workflow.test.ts @@ -0,0 +1,21 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repositoryRoot = resolve(fileURLToPath(new URL("..", import.meta.url))); + +describe("example verification workflow", () => { + it("runs the deterministic Petstore MCP fixtures instead of removed catalog scripts", () => { + const workflow = readFileSync( + resolve(repositoryRoot, ".github/workflows/verify.yml"), + "utf8", + ); + + expect(workflow).toContain("npm run example:petstore:mcp:fixture"); + expect(workflow).toContain("npm run example:petstore:mcp-agent:fixture"); + expect(workflow).not.toMatch( + /example:(?:typechain|policy|typemcp|bridge|petstore:agent:real:fixture)/, + ); + }); +});