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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

> **Published package — `@theorvane/type-mcp@0.3.2`:** provides standard decorators, a separate `@theorvane/type-mcp/legacy` entrypoint for CommonJS legacy decorators, definition validation, explicit instance resolution, MCP SDK compilation, stdio, `@theorvane/type-mcp/http` Streamable HTTP, and the tools-only `@theorvane/type-mcp/langchain` adapter.
>
> **Current `dev` source:** additionally includes SDK v2 protocol negotiation, modern component metadata, tool output schemas, explicit prompt arguments, resource URI templates, completion, and a narrow invocation context for cancellation and progress. The examples and capability map below target current source unless they explicitly say “published package.”
> **Current `dev` source:** additionally includes SDK v2 protocol negotiation, modern component metadata, tool output schemas, explicit prompt arguments, resource URI templates, completion, invocation context, protocol-backed testing, and image/audio helpers. The examples and capability map below target current source unless they explicitly say “published package.”
>
> **Integration boundary:** LangGraph `ToolNode` composition, graph topology, model choice, authorization, state, persistence, and deployment remain consumer responsibilities.

Expand Down Expand Up @@ -133,6 +133,8 @@ The methods above are ordinary application methods. In current source, use `crea
| `getMcpServerDefinition()` | Available | Reads a fresh frozen metadata copy; returns `undefined` for undecorated classes. |
| `createMcpServer()` | Available | Validates declarations and compiles the decorated server surface with an explicit resolver seam. |
| `McpInvocationContext` | Available | Optional final handler argument exposing request/session identity, cancellation, and progress reporting. |
| `McpImage` / `McpAudio` | Available | Browser-neutral byte helpers normalized to standard MCP media content. |
| `@theorvane/type-mcp/testing` | Available | Connects the official SDK client and a compiled server through the in-memory protocol transport. |
| `serveStdioServer()` / `startStdioServer()` | Available | SDK v2 factory-based 2025/2026 negotiation plus an instance-based 2025 compatibility helper. |
| `@theorvane/type-mcp/http` / `createMcpHandler()` | Available | Fetch/Streamable HTTP adapter with stateful 2025 sessions and the SDK v2 2026 per-request lifecycle; applications own route hosting, durable session policy, and authorization. |
| Definition validation and `TypeMcpDefinitionError` | Available | Validates declarations and reports safe definition errors. |
Expand All @@ -146,6 +148,7 @@ The methods above are ordinary application methods. In current source, use `crea
- [Choose a runtime boundary](docs/guides/runtime-selection.md) — select the released root, stdio, HTTP, or tools-only LangChain surface.
- [Dynamic prompts and resources](docs/guides/dynamic-declarations.md) — explicit prompt arguments, URI templates, and completion in current source.
- [Invocation context](docs/guides/invocation-context.md) — request identity, cancellation, progress, and streaming constraints.
- [Testing and media helpers](docs/guides/testing-media.md) — in-memory protocol sessions and image/audio byte results.
- [Configuration and compatibility](docs/guides/configuration.md) — Node, ESM/CommonJS, TypeScript decorators, schemas, and release boundaries.
- [Agent integration guide](docs/guides/agent-integration.md) — evidence-first coding-agent workflow and explicit runtime boundaries.
- [HTTP framework integration](docs/guides/http-and-nextjs.md) — published Streamable HTTP example and Fetch/Next.js route shape.
Expand Down
16 changes: 16 additions & 0 deletions docs/api/decorator-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ Decorated handlers may declare a final `McpInvocationContext` argument. Tools an

See the [invocation context guide](../guides/invocation-context.md) for usage and HTTP streaming constraints.

## Testing subpath

`createMcpTestSession(server, options?)` from `@theorvane/type-mcp/testing` connects an official SDK v2 `Client` and compiled `McpServer` with `InMemoryTransport`.

| Case | Behavior |
| --- | --- |
| Accept | A compiled server or promise plus optional client implementation identity. |
| Runtime | Returns a frozen session containing `client`, `server`, and idempotent `close()`. Setup failure closes both endpoints before rethrowing the original error. |
| Excluded | Mocks, snapshots, network transport assertions, subprocesses, and application fixtures. |

## Media helpers

`McpImage(bytes, { mimeType, annotations? })` and `McpAudio(bytes, { mimeType, annotations? })` defensively copy bytes and metadata. Direct helpers and helper/string lists normalize to standard MCP content blocks. MIME types must belong to the corresponding `image/*` or `audio/*` family. Filesystem and network loading remain consumer-owned.

See the [testing and media guide](../guides/testing-media.md).

## Server construction

```ts
Expand Down
19 changes: 19 additions & 0 deletions docs/architecture/adr/0005-testing-media.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ADR 0005: Add protocol-backed testing and byte media helpers

- **Status:** Accepted
- **Date:** 2026-08-27
- **Issue:** [#182](https://github.com/Theorvane/type-mcp/issues/182)

## Context

FastMCP makes in-memory protocol testing and image/audio returns convenient. TypeMCP consumers currently repeat official SDK client wiring and manually construct base64 content blocks. Pulling filesystem access into the root package would weaken its framework-neutral, Fetch-compatible boundary.

## Decision

TypeMCP adds an isolated testing subpath that connects the official SDK v2 Client and McpServer through InMemoryTransport. It returns a session with explicit close ownership.

The root exports immutable McpImage and McpAudio byte helpers. They require an explicit image/* or audio/* MIME type, defensively copy bytes and annotations, and produce standard SDK content blocks. Tool normalization converts direct helpers and mixed helper/string lists.

## Consequences

The testing entrypoint adds the split SDK client as a production dependency because packed consumers resolve it at runtime. Media helpers remain browser-neutral and do not read files. Filesystem loading, mocks, snapshots, network transports, and file-resource helpers remain consumer responsibilities.
54 changes: 54 additions & 0 deletions docs/guides/testing-media.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Testing and media helpers

**Availability:** current `dev` source; not included in published `0.3.2`.

## In-memory protocol tests

Use `@theorvane/type-mcp/testing` to run a compiled server through the official SDK v2 protocol without opening a socket or subprocess.

```ts
import { createMcpServer } from "@theorvane/type-mcp";
import { createMcpTestSession } from "@theorvane/type-mcp/testing";
import { CatalogServer } from "./catalog-server.js";

const session = await createMcpTestSession(
createMcpServer(CatalogServer),
);

try {
const tools = await session.client.listTools();
const result = await session.client.callTool({
name: "findProduct",
arguments: { sku: "SKU-1" },
});
} finally {
await session.close();
}
```

The helper connects the official `Client` and compiled SDK `McpServer` through `InMemoryTransport`. `close()` is idempotent and closes both sides. If connection setup fails, both sides are cleaned up before the original error is rethrown. Pass `{ client: { name, version } }` to override the test client identity.

This is a protocol-backed integration helper, not a mock. Transport-specific HTTP/stdio behavior still needs transport tests.

## Image and audio results

`McpImage` and `McpAudio` accept bytes and an explicit MIME type. They defensively copy input bytes and optional annotations.

```ts
import { McpAudio, McpImage } from "@theorvane/type-mcp";

return new McpImage(pngBytes, {
mimeType: "image/png",
annotations: { audience: ["user"] },
});

return [
new McpImage(firstPng, { mimeType: "image/png" }),
"Audio preview",
new McpAudio(wavBytes, { mimeType: "audio/wav" }),
];
```

Direct helpers and lists containing only helpers and strings become standard MCP image, audio, and text content blocks. `McpImage` rejects non-`image/*` MIME types; `McpAudio` rejects non-`audio/*` types.

The root package intentionally does not read file paths. Consumers that load media from disk, object storage, or the network own that I/O and pass the resulting `Uint8Array`.
45 changes: 45 additions & 0 deletions docs/planning/2026-08-27_issue-182-testing-media.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Task brief — testing and media helpers

**Owner:** Codex

**Date:** 2026-08-27

**Status:** complete

**Related plan:** GitHub issue `#182`

**Stacked base:** PR `#181` / `feat/180-invocation-context` until merged to `dev`

## Objective

Consumers can test compiled servers through the real in-memory MCP protocol and return image/audio bytes without hand-building base64 content.

## Scope

**In:** testing subpath, connected client/server session, deterministic cleanup, byte-only image/audio helpers, MIME validation, direct/list normalization, exports, packed-consumer and docs verification.

**Out:** filesystem loading, file resources, mocks, snapshots, network transports, OAuth, providers, and MCP Apps.

## Acceptance criteria

- [x] A testing subpath connects a compiled server and official SDK client in memory.
- [x] Cleanup closes both client and server and failed connections are cleaned up.
- [x] Image/audio helpers validate MIME families and defensively copy inputs.
- [x] Direct and mixed-list helper returns become standard content blocks.
- [x] Existing result normalization remains compatible.
- [x] Full package verification passes.

## Red → green evidence

| Stage | Command | Result / expected reason |
| --- | --- | --- |
| Red: media | `npx vitest run test/media-helpers.test.ts` | Failed as expected because `McpImage` and `McpAudio` were not constructors. |
| Red: testing | `npx vitest run test/testing-helper.test.ts` | Failed as expected because `src/testing.js` did not exist. |
| Green | focused media/testing/result/package suites | Passed: 7 tests cover content conversion, validation, connection, idempotent and failed cleanup, compatibility, and the export map. |
| Regression | `npm test` | Passed from a clean `npm ci`: 33 files and 91 tests; lint, typecheck, build, package/publish, packed ESM/CJS consumers, production audit, and diff checks passed. |

## Review handoff

- Spec review: pending
- Quality review: pending
- Final checks: all required local checks passed
2 changes: 2 additions & 0 deletions docs/product/mvp-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
| Prompts | Published baseline: named zero-argument handlers; current `dev`: explicit validated arguments and completion |
| Compilation | Decorator metadata compiled to the stable split `@modelcontextprotocol/server` v2 `McpServer` |
| Invocation context | Current `dev`: request/session identity, SDK cancellation signal, and progress reporting |
| Testing | Current `dev`: official SDK v2 in-memory client/server session with explicit cleanup |
| Media | Current `dev`: byte-only image/audio helpers with explicit MIME validation |
| Instance construction | Direct constructor default plus async-capable `InstanceResolver` interface |
| Local transport | stdio helper |
| Web transport | Fetch-standard Streamable HTTP handler |
Expand Down
13 changes: 1 addition & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
"default": "./dist/langchain.cjs"
}
},
"./testing": {
"import": {
"types": "./dist/testing.d.ts",
"default": "./dist/testing.js"
},
"require": {
"types": "./dist/testing.d.cts",
"default": "./dist/testing.cjs"
}
},
"./legacy": {
"import": {
"types": "./dist/legacy.d.ts",
Expand Down Expand Up @@ -86,6 +96,7 @@
},
"dependencies": {
"@hono/node-server": "2.1.1",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"zod": "^4.4.3"
},
Expand All @@ -99,7 +110,6 @@
"@biomejs/biome": "^2.5.4",
"@langchain/core": "1.2.9",
"@langchain/langgraph": "1.4.9",
"@modelcontextprotocol/client": "2.0.0",
"@types/node": "^26.2.0",
"rimraf": "^6.0.1",
"tsup": "^8.5.1",
Expand Down
10 changes: 8 additions & 2 deletions scripts/verify-compatibility-consumer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ try {
include: ["server.ts"],
},
`import { z } from "zod";
import { createMcpServer, getMcpServerDefinition, McpServer, McpTool } from "@theorvane/type-mcp";
import { createMcpServer, getMcpServerDefinition, McpImage, McpServer, McpTool } from "@theorvane/type-mcp";
import { createMcpHandler } from "@theorvane/type-mcp/http";
import { createLangChainTools } from "@theorvane/type-mcp/langchain";
import { createMcpTestSession } from "@theorvane/type-mcp/testing";
import { McpServer as LegacyMcpServer, McpTool as LegacyMcpTool } from "@theorvane/type-mcp/legacy";

@McpServer({ name: "standard-catalog", version: "1.0.0" })
Expand All @@ -92,6 +93,8 @@ if (getMcpServerDefinition(StandardCatalog)?.tools[0]?.name !== "find_product")
if (typeof createMcpServer !== "function") throw new Error("ESM root export missing.");
if (typeof createMcpHandler !== "function") throw new Error("ESM http export missing.");
if (typeof createLangChainTools !== "function") throw new Error("ESM langchain export missing.");
if (typeof createMcpTestSession !== "function") throw new Error("ESM testing export missing.");
if (new McpImage(new Uint8Array([1]), { mimeType: "image/png" }).toContent().data !== "AQ==") throw new Error("ESM media export failed.");
if (typeof LegacyMcpServer !== "function" || typeof LegacyMcpTool !== "function") throw new Error("ESM legacy export missing.");
`,
);
Expand All @@ -113,9 +116,10 @@ if (typeof LegacyMcpServer !== "function" || typeof LegacyMcpTool !== "function"
include: ["server.ts"],
},
`import { z } from "zod";
import { createMcpServer } from "@theorvane/type-mcp";
import { createMcpServer, McpImage } from "@theorvane/type-mcp";
import { createMcpHandler } from "@theorvane/type-mcp/http";
import { createLangChainTools } from "@theorvane/type-mcp/langchain";
import { createMcpTestSession } from "@theorvane/type-mcp/testing";
import { getMcpServerDefinition, McpServer, McpTool } from "@theorvane/type-mcp/legacy";

@McpServer({ name: "legacy-catalog", version: "1.0.0" })
Expand All @@ -128,6 +132,8 @@ if (getMcpServerDefinition(LegacyCatalog)?.tools[0]?.name !== "find_product") th
if (typeof createMcpServer !== "function") throw new Error("CJS root export missing.");
if (typeof createMcpHandler !== "function") throw new Error("CJS http export missing.");
if (typeof createLangChainTools !== "function") throw new Error("CJS langchain export missing.");
if (typeof createMcpTestSession !== "function") throw new Error("CJS testing export missing.");
if (new McpImage(new Uint8Array([1]), { mimeType: "image/png" }).toContent().data !== "AQ==") throw new Error("CJS media export failed.");
`,
);

Expand Down
4 changes: 4 additions & 0 deletions scripts/verify-package-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ const exportsToVerify = [
key: "./langchain",
symbols: [{ name: "createLangChainTools", runtimeType: "function" }],
},
{
key: "./testing",
symbols: [{ name: "createMcpTestSession", runtimeType: "function" }],
},
{
key: "./legacy",
symbols: [
Expand Down
Loading
Loading