Skip to content
Draft
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
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ src/
│ ├── proxy.ts # proxy, proxyRequest, fetchWithEvent
│ ├── ws.ts # defineWebSocketHandler, defineWebSocket
│ ├── json-rpc.ts # defineJsonRpcHandler, defineJsonRpcWebSocketHandler
│ ├── mcp.ts # defineMcpHandler, defineMcpTool, defineMcpResource, defineMcpPrompt
│ ├── event-stream.ts # createEventStream (SSE)
│ ├── static.ts # serveStatic
│ ├── cache.ts # handleCacheHeaders
Expand All @@ -89,6 +90,7 @@ src/
│ └── internal/ # Internal helpers (not exported)
│ ├── auth.ts, body.ts, cors.ts, encoding.ts, ...
│ ├── iron-crypto.ts # Session sealing crypto
│ ├── mcp.ts # MCP internal handler logic (handleMcpRequest, resolveMcpOptions)
│ ├── standard-schema.ts # Standard schema validation
│ └── validate.ts
├── _entries/ # Platform-specific entry points
Expand Down Expand Up @@ -226,6 +228,30 @@ h3/tracing → Tracing plugin
| `srvx` | Server abstraction (multi-runtime) |
| `crossws` | WebSocket abstraction (optional peer dep) |

## MCP (Model Context Protocol)

h3 implements MCP as a built-in utility — no SDK dependency. Wire format is JSON-RPC 2.0 over HTTP. Protocol version: `"2025-06-18"` (also accepts `"2025-03-26"`).

### Architecture

- **Public API** (`src/utils/mcp.ts`): Types + `defineMcpHandler`, `defineMcpTool`, `defineMcpResource`, `defineMcpPrompt`
- **Internal handler** (`src/utils/internal/mcp.ts`): `handleMcpRequest` processes HTTP → JSON-RPC, `resolveMcpOptions` handles lazy resolution
- Built on top of `src/utils/json-rpc.ts` (`processJsonRpcBody`, `createMethodMap`)

### Key patterns

- **`MaybeLazy<T>`**: `tools`, `resources`, `prompts` accept `T | (() => T | Promise<T>)`. Lazy values are resolved once and cached via `_resolveLazy()`. For static handler options, caching persists across requests. For dynamic `(event) => options`, each request gets fresh resolution.
- **`McpResolvedOptions`**: Internal type with pre-bound lazy resolvers. Created by `resolveMcpOptions()` and passed to `handleMcpRequest()`.
- `defineMcpHandler` supports both static options and `(event: H3Event) => McpHandlerOptions` for per-request config.

### MCP methods implemented

`initialize`, `ping`, `notifications/initialized`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`

### Tests

`test/mcp.test.ts` — unit tests for define helpers + integration tests via `describeMatrix` (web + node).

## Best Practices for Contributing

- Prefer web standard APIs over runtime-specific ones
Expand Down
139 changes: 139 additions & 0 deletions docs/2.utils/6.mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,143 @@ icon: material-symbols:swap-calls

> H3 MCP related utils.

<!-- automd:jsdocs src="../../src/utils/mcp.ts" -->

### `defineMcpHandler(options)`

Define an H3 event handler that implements the Model Context Protocol (MCP) over HTTP using JSON-RPC 2.0 as the wire format.

Supports MCP methods: `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`, and `notifications/initialized`.

**Example:**

```ts
app.all(
"/mcp",
defineMcpHandler({
name: "my-server",
version: "1.0.0",
tools: [echoTool],
resources: [readmeResource],
prompts: [greetPrompt],
}),
);
```

**Example:**

```ts
// Dynamic options based on request context
app.all(
"/mcp",
defineMcpHandler((event) => ({
name: "my-server",
version: "1.0.0",
tools: getToolsForUser(event),
})),
);
```

### `defineMcpPrompt(definition)`

Define an MCP prompt with optional arguments and a handler that returns messages.

**Example:**

```ts
// Prompt with arguments
const greetPrompt = defineMcpPrompt({
name: "greet",
description: "Generate a greeting",
args: [{ name: "name", required: true }],
handler: async (args, event) => ({
messages: [
{ role: "user", content: { type: "text", text: `Hello ${args.name}!` } },
],
}),
});
```

**Example:**

```ts
// Prompt without arguments
const helpPrompt = defineMcpPrompt({
name: "help",
description: "Show help information",
handler: async (event) => ({
messages: [
{ role: "user", content: { type: "text", text: "How can I help?" } },
],
}),
});
```

### `defineMcpResource(definition)`

Define an MCP resource with a static URI and a handler that returns its contents.

**Example:**

```ts
const readmeResource = defineMcpResource({
name: "readme",
uri: "file:///readme",
description: "Project README",
mimeType: "text/markdown",
handler: async (uri, event) => ({
contents: [{ uri: uri.toString(), text: "# My Project" }],
}),
});
```

### `defineMcpTool(definition)`

Define an MCP tool with a name, optional JSON Schema input, and a handler function.

**Example:**

```ts
// Tool with input parameters
const echoTool = defineMcpTool({
name: "echo",
description: "Echo back a message",
inputSchema: {
type: "object",
properties: { message: { type: "string" } },
required: ["message"],
},
handler: async (args, event) => ({
content: [{ type: "text", text: args.message as string }],
}),
});
```

**Example:**

```ts
// Tool without input parameters
const pingTool = defineMcpTool({
name: "ping",
description: "Returns pong",
handler: async (event) => ({
content: [{ type: "text", text: "pong" }],
}),
});
```

<!-- /automd -->

<!-- automd:jsdocs src="../../src/utils/json-rpc.ts" -->

### `createJsonRpcError(id, code, message, data?)`

Creates a JSON-RPC error response object.

### `createMethodMap(methods)`

Build a null-prototype lookup map to prevent prototype pollution. This ensures that method names like "__proto__", "constructor", "toString", "hasOwnProperty", etc. cannot resolve to inherited Object.prototype properties.

### `defineJsonRpcHandler()`

Creates an H3 event handler that implements the JSON-RPC 2.0 specification.
Expand Down Expand Up @@ -76,4 +211,8 @@ app.get(
);
```

### `processJsonRpcBody(body, methodMap, context)`

Validates and processes a parsed JSON-RPC body (single or batch).

<!-- /automd -->
101 changes: 101 additions & 0 deletions examples/mcp.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { z } from "zod";
import { H3, serve, defineMcpHandler, defineMcpTool, defineMcpResource, defineMcpPrompt } from "h3";

export const app = new H3();

// --- Define MCP tools ---

const echoTool = defineMcpTool({
name: "echo",
description: "Echo back a message",
inputSchema: { message: z.string().describe("The message to echo") },
handler: async ({ message }) => ({
content: [{ type: "text", text: message }],
}),
});

const calculatorTool = defineMcpTool({
name: "calculator",
description: "Perform basic math operations",
inputSchema: {
operation: z.enum(["add", "subtract", "multiply", "divide"]),
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
},
handler: async ({ operation, a, b }) => {
let result;
switch (operation) {
case "add":
result = a + b;
break;
case "subtract":
result = a - b;
break;
case "multiply":
result = a * b;
break;
case "divide":
result = b !== 0 ? a / b : "Error: Division by zero";
break;
}
return {
content: [{ type: "text", text: JSON.stringify({ operation, a, b, result }, null, 2) }],
};
},
});

// --- Define MCP resources ---

const aboutResource = defineMcpResource({
name: "about",
uri: "file:///about",
description: "Information about this MCP server",
handler: async (uri) => ({
contents: [
{
uri: uri.toString(),
text: "This is an example MCP server built with h3.",
},
],
}),
});

// --- Define MCP prompts ---

const summarizePrompt = defineMcpPrompt({
name: "summarize",
description: "Generate a prompt to summarize text",
argsSchema: {
text: z.string().describe("The text to summarize"),
},
handler: async ({ text }) => ({
messages: [
{
role: "user",
content: {
type: "text",
text: `Please summarize the following text:\n\n${text}`,
},
},
],
}),
});

// --- Create the MCP handler ---

app.all(
"/mcp",
defineMcpHandler({
name: "h3-mcp-example",
version: "1.0.0",
tools: [echoTool, calculatorTool],
resources: [aboutResource],
prompts: [summarizePrompt],
}),
);

// --- Landing page ---

app.get("/", () => "MCP server running at /mcp");

serve(app);
33 changes: 33 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,39 @@ export type {
JsonRpcWebSocketMethod,
} from "./utils/json-rpc.ts";

// MCP

export {
type McpTextContent,
type McpImageContent,
type McpAudioContent,
type McpResourceLink,
type McpEmbeddedResource,
type McpContentBlock,
type McpAnnotations,
type McpTextResourceContents,
type McpBlobResourceContents,
type McpResourceContents,
type McpCallToolResult,
type McpReadResourceResult,
type McpPromptMessage,
type McpGetPromptResult,
type McpToolAnnotations,
type McpToolCallback,
type McpToolDefinition,
type McpResourceCallback,
type McpResourceDefinition,
type McpPromptCallback,
type McpPromptArgument,
type McpPromptDefinition,
type MaybeLazy,
type McpHandlerOptions,
defineMcpHandler,
defineMcpTool,
defineMcpResource,
defineMcpPrompt,
} from "./utils/mcp.ts";

// ---- Deprecated ----

export * from "./_deprecated.ts";
Loading
Loading