first commit
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# mcp-client
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`MCPClient` is the consumer-side surface of the Model Context Protocol. `connect(transport)` performs the MCP initialize handshake, negotiates the protocol version, populates the cached lists of remote tools / resources / prompts / roots, and then exposes every standard method (`callTool`, `readResource`, `subscribeResource`, `getPrompt`, `complete`, `setLogLevel`, `ping`) plus sampling + roots + completion provider slots the server can call back into.
|
||||
|
||||
## Features
|
||||
|
||||
- Initialize handshake: protocol-version negotiation, capabilities exchange, `notifications/initialized` emission, automatic refresh of every cached list.
|
||||
- `callTool` with optional `onProgress` callback — auto-generates a progress token, attaches it to the outgoing request, and forwards `notifications/progress` events to the callback.
|
||||
- `AbortSignal` cancellation — propagates as `notifications/cancelled` over the wire.
|
||||
- `subscribeResource(uri)` — registers interest; the client auto-refreshes the resource and re-emits a `notification:emitted` event on `notifications/resources/updated`.
|
||||
- Sampling / roots / completion providers — call-backs the server invokes; the client side of the bidirectional MCP surface.
|
||||
- Cached lists with auto-refresh on `notifications/{tools,resources,prompts}/list_changed`.
|
||||
- Public `events: EventBus<MCPEventMap>` for lifecycle observability.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { InMemoryTransport, MCPClient, MCPServer } from "@suaveplan/mcp";
|
||||
|
||||
const server = new MCPServer({ info: { name: "demo-server", version: "0.1.0" } });
|
||||
const client = new MCPClient({ info: { name: "demo-client", version: "0.1.0" } });
|
||||
|
||||
const [a, b] = InMemoryTransport.pair();
|
||||
await server.start(a);
|
||||
await client.connect(b);
|
||||
|
||||
console.log(client.tools());
|
||||
const result = await client.callTool("echo", { msg: "hi" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MCPClient } from "@suaveplan/mcp";
|
||||
|
||||
const client = new MCPClient({
|
||||
info: { name: "ide", version: "0.1.0" },
|
||||
capabilities: { sampling: {}, roots: { listChanged: true } },
|
||||
defaultTimeoutMs: 5_000,
|
||||
});
|
||||
|
||||
client.setSamplingProvider(async (params, ctx) => {
|
||||
const completion = await myLLM.complete(params, { signal: ctx.signal });
|
||||
return { role: "assistant", content: { type: "text", text: completion }, model: "demo" };
|
||||
});
|
||||
|
||||
const ac = new AbortController();
|
||||
setTimeout(() => ac.abort(), 1_000);
|
||||
|
||||
const result = await client.callTool(
|
||||
"slow",
|
||||
{ ms: 5_000 },
|
||||
{ signal: ac.signal, onProgress: (p, total) => log(`progress ${p}/${total ?? "?"}`) },
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new MCPClient(options: MCPClientOptions)` — `info`, optional `capabilities`, `acceptedProtocolVersions`, `events`, `clock`, `idGenerator`, `defaultTimeoutMs`.
|
||||
- Lifecycle: `connect(transport)`, `disconnect(reason?)`, `initialized`.
|
||||
- Refresh: `refreshTools`, `refreshResources`, `refreshResourceTemplates`, `refreshPrompts`, `refreshRoots`, `refreshAll`.
|
||||
- Methods: `ping`, `callTool`, `readResource`, `subscribeResource`, `unsubscribeResource`, `getPrompt`, `complete`, `setLogLevel`.
|
||||
- Providers: `setSamplingProvider`, `setRootsProvider`, `setCompletionProvider`.
|
||||
- Snapshots: `tools`, `resources`, `resourceTemplates`, `prompts`, `roots`, `snapshot`.
|
||||
- Hooks: `onLogMessage`, `registerRoot`.
|
||||
- `events: EventBus<MCPEventMap>`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`connect` does not return until the initialize handshake completes and the four list refreshes finish; if any step throws the client disconnects the transport cleanly before propagating the error. Progress tokens are auto-generated only when the caller supplies an `onProgress` callback — otherwise no token is sent (per MCP spec: clients must not send progress tokens they don't intend to consume). The client never re-enters `callTool` from inside an `onProgress` callback; the progress correlation table is keyed by token and lookups are O(n) across pending requests, which is fine for the spec's expected concurrency (a few in-flight requests, not thousands). `disconnect` aborts in-flight requests by closing the dispatcher first, so the rejection cascade fires before the `connection:closed` event is emitted.
|
||||
56
docs/devtools/tooling/mcp/src/error-codes/error-codes.md
Normal file
56
docs/devtools/tooling/mcp/src/error-codes/error-codes.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/mcp` raises three typed error classes — `MCPError`, `MCPTransportError`, and `MCPProtocolError` — for every failure mode the package exposes. Codes are registered via `defineCodes` from `@suaveplan/error`, giving consumers a stable fully-qualified string (e.g. `"MCP.UNAUTHORIZED"`) they can pattern-match on without `instanceof` chains. Discriminating on `error.code` keeps consumers, retries, dashboards, and i18n catalogues in lockstep with the package no matter how the class hierarchy evolves.
|
||||
|
||||
## Features
|
||||
|
||||
- Stable code registry: 15 codes spanning protocol, transport, lifecycle, registration, and handler failures.
|
||||
- Three classes (`MCPError`, `MCPTransportError`, `MCPProtocolError`) — extends `SuaveplanError<MCPCode>` so callers get the cause-chain, redaction, OTel adapter, and i18n resolver hook for free.
|
||||
- Type-safe `code` discriminator — no `instanceof` required.
|
||||
- Public constructors delegate to `super` per `@suaveplan/error` policy.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { MCP_CODES, MCPError } from "@suaveplan/mcp/error-codes";
|
||||
|
||||
throw new MCPError(MCP_CODES.NOT_FOUND, 'Tool "echo" not found');
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MCP_CODES, MCPError, MCPTransportError } from "@suaveplan/mcp";
|
||||
|
||||
try {
|
||||
await client.callTool("slow", { ms: 10_000 }, { timeoutMs: 100 });
|
||||
} catch (error) {
|
||||
if (error instanceof MCPTransportError) {
|
||||
metrics.record("mcp.transport_error");
|
||||
return;
|
||||
}
|
||||
if (error instanceof MCPError && error.code === MCP_CODES.TIMEOUT) {
|
||||
logger.warn("call timed out", { code: error.code });
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `MCP_CODES` — frozen object whose values are the fully-qualified code strings (`"MCP.<LEAF>"`).
|
||||
- `MCPCode` — string-literal union of every value of `MCP_CODES`.
|
||||
- `MCPError extends SuaveplanError<MCPCode>` — constructor: `(code, message, options?)`.
|
||||
- `MCPTransportError extends MCPError` — same constructor; thrown when the underlying transport rejects a `send` (`TRANSPORT_CLOSED`) or surfaces an upstream failure (`TRANSPORT_ERROR`).
|
||||
- `MCPProtocolError extends MCPError` — thrown for `PROTOCOL_INVALID` and `PROTOCOL_VERSION_MISMATCH`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is declared via `defineCodes("MCP", [...] as const)` which asserts the array is a literal tuple and produces a `Record<Key, "MCP.Key">` mapping. Adding a code anywhere except the tuple breaks both the type and the runtime registry — the type-system guarantees the two stay in sync. Every registered code is reachable from a real call site exercised by a test (see `error-codes.test.ts`). The constructor delegates to `SuaveplanError`'s protected constructor; subclasses keep `name` distinct so logs and traces can still distinguish transport from protocol failures even though they share the typed-code surface.
|
||||
59
docs/devtools/tooling/mcp/src/events/events.md
Normal file
59
docs/devtools/tooling/mcp/src/events/events.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# events
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`events.ts` declares `MCPEventMap` — the typed payload map shared by `MCPServer` and `MCPClient` via the `EventBus<MCPEventMap>` surface. Subscribers can observe connection lifecycle (`opened` / `closed`), request lifecycle (`received` / `completed`), and notification fan-out without reaching into private dispatcher state. `createMCPEventBus()` returns a ready-to-use `EventBus` instance carrying this map — the recommended way for callers to share a bus across multiple servers or clients in the same process.
|
||||
|
||||
## Features
|
||||
|
||||
- `connection:opened` — fired when a dispatcher attaches to a transport. Payload: `{ side, dispatcherId }`.
|
||||
- `connection:closed` — fired when a dispatcher closes. Payload: `{ side, dispatcherId, reason? }`.
|
||||
- `request:received` — fired when an incoming request arrives. Payload: `{ side, method, id }`.
|
||||
- `request:completed` — fired when a request handler finishes (success, error, or cancellation). Payload: `{ side, method, id, durationMs, outcome }`.
|
||||
- `notification:emitted` — fired when a notification is sent. Payload: `{ side, method, params }`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createMCPEventBus } from "@suaveplan/mcp/events";
|
||||
import { MCPServer } from "@suaveplan/mcp";
|
||||
|
||||
const events = createMCPEventBus();
|
||||
const server = new MCPServer({ info: { name: "demo", version: "0.1.0" }, events });
|
||||
|
||||
events.on("request:completed", (e) => {
|
||||
console.log(`${e.method} ${String(e.id)} ${e.outcome} ${String(e.durationMs)}ms`);
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createMCPEventBus, type MCPEventMap } from "@suaveplan/mcp";
|
||||
import { MCPClient, MCPServer, InMemoryTransport } from "@suaveplan/mcp";
|
||||
|
||||
const bus = createMCPEventBus();
|
||||
const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, events: bus });
|
||||
const client = new MCPClient({ info: { name: "c", version: "0.1.0" }, events: bus });
|
||||
|
||||
const records: Array<MCPEventMap["request:completed"]> = [];
|
||||
bus.on("request:completed", (e) => records.push(e));
|
||||
|
||||
const [a, b] = InMemoryTransport.pair();
|
||||
await server.start(a);
|
||||
await client.connect(b);
|
||||
// records now carries the initialize + tools/list + resources/list + prompts/list + roots/list events.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `MCPEventMap` — `Record<string, unknown>` with explicit per-key payload shapes.
|
||||
- `createMCPEventBus()` — returns `EventBus<MCPEventMap>` using the canonical defaults from `@suaveplan/events`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Both `MCPServer` and `MCPClient` accept an `events` option in their constructor — callers may share one bus across a fleet to drive monitors and dashboards. When not provided, each side constructs its own bus via `createMCPEventBus()`. The `side` field carries `"client"` or `"server"` so downstream subscribers can split metrics per direction without parsing the dispatcher id. `request:completed` always fires exactly once per request — on success, on error, or on cancellation — making it the safe place to bind per-request latency dashboards.
|
||||
@@ -0,0 +1,68 @@
|
||||
# dispatcher
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`JsonRpcDispatcher` is the symmetric JSON-RPC 2.0 engine sitting under `MCPServer` and `MCPClient`. It owns id allocation, the pending-request map, request/response correlation, notification fan-out, abort propagation (in both directions), per-request timeout enforcement, and Zod-validated handler dispatch. Both the server and client wire one dispatcher to a `Transport`; the only difference between the two sides is which methods each one registers — the dispatcher itself is direction-agnostic.
|
||||
|
||||
## Features
|
||||
|
||||
- `request(method, params, options?)` — async, returns `Promise<unknown>`; correlates with the matching response or rejects on abort/timeout/transport-close.
|
||||
- `notify(method, params?)` — fire-and-forget notifications.
|
||||
- `registerHandler(method, fn)` / `unregisterHandler(method)` — server-side method handlers.
|
||||
- `onNotification(method, fn)` — subscribe to incoming notifications.
|
||||
- `paramSchemas` map — per-method Zod schemas validated before the handler runs; rejections respond with JSON-RPC `-32602`.
|
||||
- Cancellation: `AbortSignal` on a caller sends `notifications/cancelled` and rejects with `MCPError(REQUEST_CANCELLED)`. Incoming `notifications/cancelled` aborts the matching in-flight handler.
|
||||
- Progress: handlers receive `ctx.sendProgress(token, progress, total?)` which emits `notifications/progress`; callers' `onProgress` hook is invoked when correlated by token.
|
||||
- OTel + telemetry: every request/notification increments the package meter via `recordRequest` / `recordNotification`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { JsonRpcDispatcher, InMemoryTransport } from "@suaveplan/mcp";
|
||||
|
||||
const [a, b] = InMemoryTransport.pair();
|
||||
const left = new JsonRpcDispatcher({ side: "server", transport: a, dispatcherId: "L" });
|
||||
const right = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "R" });
|
||||
|
||||
left.registerHandler("ping", async () => ({ ok: true }));
|
||||
const result = await right.request("ping", undefined);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { JsonRpcDispatcher, MCPError, MCP_CODES } from "@suaveplan/mcp";
|
||||
import z from "zod";
|
||||
|
||||
const schema = z.object({ msg: z.string() });
|
||||
const d = new JsonRpcDispatcher({
|
||||
side: "server",
|
||||
transport,
|
||||
dispatcherId: "demo",
|
||||
paramSchemas: { "echo": schema },
|
||||
});
|
||||
|
||||
d.registerHandler("echo", async (params, ctx) => {
|
||||
const ac = new AbortController();
|
||||
ctx.signal.addEventListener("abort", () => ac.abort());
|
||||
ctx.sendProgress("p", 1, 3);
|
||||
if (ctx.signal.aborted) throw new MCPError(MCP_CODES.REQUEST_CANCELLED, "cancelled");
|
||||
return { echoed: (params as { msg: string }).msg };
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new JsonRpcDispatcher(options: DispatcherOptions)` — constructor takes the transport, dispatcher id, optional clock + id-generator, optional listeners + param-schema map.
|
||||
- `request(method, params, options?: { signal?, timeoutMs?, onProgress? })` → `Promise<unknown>`.
|
||||
- `notify(method, params?)` → `Promise<void>`.
|
||||
- `setProgressToken(method, token)` — used by `MCPClient` to attach an auto-generated token to the most-recent pending request for forward-progress correlation.
|
||||
- `close(reason?)` — rejects every pending request with `MCPTransportError(TRANSPORT_CLOSED)`, aborts every in-flight handler, and closes the transport.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The pending map keys requests by `stringifyId(id)` so numeric and string ids never collide. `request()` schedules the abort+timeout cleanup inside the executor so cancellation paths can synchronously remove the pending entry before resolution. Notifications never carry an id; the type guards in `handleMessage` discriminate request/response/notification by the `id` + `method` + `result|error` shape. The `paramSchemas` registry is keyed by the JSON-RPC method name so adding a new validated method is a one-line registration on the server side — the dispatcher itself stays unaware of MCP-specific semantics.
|
||||
62
docs/devtools/tooling/mcp/src/schemas/schemas.md
Normal file
62
docs/devtools/tooling/mcp/src/schemas/schemas.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# schemas
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`schemas.ts` is the single source of truth for every wire shape in `@suaveplan/mcp`. Every public TypeScript type for an MCP wire payload — JSON-RPC envelopes, initialize, tool/resource/prompt records, sampling, completion, roots, logging, progress, cancellation — is derived via `z.infer<>` from a schema declared here. The schema IS the contract, so the runtime parser and the static TS type can never drift apart: changing one is changing the other.
|
||||
|
||||
## Features
|
||||
|
||||
- JSON-RPC 2.0 envelope schemas: `JsonRpcRequestSchema`, `JsonRpcResponseSchema`, `JsonRpcNotificationSchema`, and the `JsonRpcMessageSchema` discriminated union.
|
||||
- Initialize handshake: `InitializeParamsSchema`, `InitializeResultSchema`, `ClientCapabilitiesSchema`, `ServerCapabilitiesSchema`, `ImplementationSchema`.
|
||||
- Tools: `ToolSchema`, `ToolsListResultSchema`, `ToolCallParamsSchema`, `ToolCallResultSchema`.
|
||||
- Resources: `ResourceSchema`, `ResourceTemplateSchema`, `ResourceContentSchema`, plus `*Read` / `*Subscribe` params.
|
||||
- Prompts: `PromptSchema`, `PromptArgumentSchema`, `PromptMessageSchema`, `PromptGetParamsSchema`, `PromptGetResultSchema`.
|
||||
- Sampling + completion + roots + logging schemas.
|
||||
- Notification params: `ProgressNotificationParamsSchema`, `CancelledNotificationParamsSchema`, `LoggingMessageParamsSchema`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { JsonRpcRequestSchema } from "@suaveplan/mcp/schemas";
|
||||
|
||||
const request = JsonRpcRequestSchema.parse({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/list",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
type ToolCallParams,
|
||||
ToolCallParamsSchema,
|
||||
} from "@suaveplan/mcp";
|
||||
|
||||
function safeToolCall(raw: unknown): ToolCallParams | null {
|
||||
const parsed = ToolCallParamsSchema.safeParse(raw);
|
||||
if (!parsed.success) {
|
||||
for (const issue of parsed.error.issues) {
|
||||
metrics.record("mcp.invalid_params", { path: issue.path.join(".") });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- Every schema is exported alongside its inferred TypeScript type (`<Name>Schema` + `<Name>`).
|
||||
- Schemas validate strictly — extra fields are preserved but the inferred type only includes declared shapes.
|
||||
- `JsonRpcMessageSchema` is a `z.union` over request/response/notification; downstream code uses `safeParse` to discriminate.
|
||||
- `ContentItemSchema` is a `z.discriminatedUnion("type", ...)` over text, image, and inline-resource variants.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `import z from "zod"` form is the Bun-safe default-import idiom mandated by the suaveplan guidelines; the named `{ z }` form fails under Bun's CJS interop for the Zod 4 build. `_meta.progressToken` is `string | number | undefined` because the MCP spec allows both shapes and a missing token means progress is not requested. Server- and client-side dispatchers register a subset of these schemas as the params-validation map for the JSON-RPC dispatcher: invalid params produce a JSON-RPC `-32602` response mapped to `MCPError(INVALID_PARAMS)` carrying the failing field path.
|
||||
@@ -0,0 +1,87 @@
|
||||
# mcp-server
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`MCPServer` is the host-side surface of the Model Context Protocol. Owners register tools, resources, prompts, and roots; the server then exposes them to a remote `MCPClient` (or any spec-compliant peer) over the JSON-RPC dispatcher and a pluggable transport. Every public method documented here either mutates the registration tables, configures a hook (authorize, sampling, completion, log level), or drives the dispatcher (start / stop / direct invocation).
|
||||
|
||||
## Features
|
||||
|
||||
- Register / unregister tools, resources, resource templates, prompts, and roots — every collision raises `MCPError(DUPLICATE_REGISTRATION)`.
|
||||
- Lifecycle: `start(transport)` attaches a JSON-RPC dispatcher and registers handlers for every MCP method; `stop(reason?)` rejects pending requests and aborts in-flight handlers.
|
||||
- Authorize hook: `setAuthorizer(fn)` gates every method; rejection yields `-32001` mapped to `MCPError(UNAUTHORIZED)`.
|
||||
- Sampling + completion providers — surfaces are pluggable so consumers can wire LLM backends.
|
||||
- Progress + abort: tool handlers receive `progress(value, total?)` and an `AbortSignal`; clients see `notifications/progress` and `notifications/cancelled` over the wire.
|
||||
- Resource subscriptions: `markResourceUpdated(uri)` fans out `notifications/resources/updated` to subscribed peers.
|
||||
- Direct invocation API (`invokeTool`, `readResource`, `getPromptMessages`) — useful in tests and in-process callers.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { InMemoryTransport, MCPServer } from "@suaveplan/mcp";
|
||||
|
||||
const server = new MCPServer({ info: { name: "demo", version: "0.1.0" } });
|
||||
|
||||
server.registerTool({
|
||||
name: "echo",
|
||||
description: "Echo back the input",
|
||||
inputSchema: { type: "object", properties: { msg: { type: "string" } } },
|
||||
async handler({ msg }) {
|
||||
return { content: [{ type: "text", text: String(msg) }] };
|
||||
},
|
||||
});
|
||||
|
||||
const [a, b] = InMemoryTransport.pair();
|
||||
await server.start(a);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { MCPServer } from "@suaveplan/mcp";
|
||||
import z from "zod";
|
||||
|
||||
const server = new MCPServer({ info: { name: "admin", version: "0.1.0" } });
|
||||
|
||||
server.setAuthorizer(async (req) => req.method !== "tools/call" || hasPermission(req.params));
|
||||
|
||||
server.registerTool({
|
||||
name: "deploy",
|
||||
description: "Deploy a release",
|
||||
inputSchema: { type: "object", properties: { tag: { type: "string" } }, required: ["tag"] },
|
||||
paramsSchema: z.object({ tag: z.string().min(1) }),
|
||||
async handler(args, ctx) {
|
||||
for (let p = 0; p < 100 && !ctx.signal.aborted; p += 10) {
|
||||
await sleep(50);
|
||||
ctx.progress(p, 100);
|
||||
}
|
||||
return { content: [{ type: "text", text: `Deployed ${(args as { tag: string }).tag}` }] };
|
||||
},
|
||||
});
|
||||
|
||||
server.registerResource({
|
||||
uri: "config://current",
|
||||
name: "Active configuration",
|
||||
mimeType: "application/json",
|
||||
async handler() {
|
||||
return { uri: "config://current", mimeType: "application/json", text: JSON.stringify(currentConfig) };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new MCPServer(options: MCPServerOptions)`.
|
||||
- Registration: `registerTool`, `unregisterTool`, `registerResource`, `unregisterResource`, `registerResourceTemplate`, `unregisterResourceTemplate`, `registerPrompt`, `unregisterPrompt`, `registerRoot`, `unregisterRoot`.
|
||||
- Lists: `listTools`, `listResources`, `listResourceTemplates`, `listPrompts`, `listRoots`.
|
||||
- Hooks: `setAuthorizer`, `setSamplingProvider`, `setCompletionProvider`, `setLogLevel`.
|
||||
- Direct API: `invokeTool`, `readResource`, `getPromptMessages`, `createSamplingMessage`, `listClientRoots`, `emitLog`.
|
||||
- Lifecycle: `start(transport)`, `stop(reason?)`, `running`, `initialized`, `getInfo()`.
|
||||
- `events: EventBus<MCPEventMap>` — subscribe to connection / request / notification lifecycle.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`start` builds a `paramSchemas` map keyed by method name so the dispatcher validates incoming params before invoking the handler — invalid params respond with JSON-RPC `-32602` mapped to `MCPError(INVALID_PARAMS)`. List-changed notifications fan out from registration mutators only when a dispatcher is attached; the rejection is swallowed because the transport may have closed between the registration and the notify. `invokeTool` wraps any thrown error into a `MCPToolResult` with `isError: true` so JSON-RPC level errors are reserved for protocol failures, not tool-handler failures (matching the MCP spec convention). `createSamplingMessage` and `listClientRoots` use the dispatcher to call out to the connected client — these methods exist on the server side because MCP's sampling and roots flows are client-side capabilities that the server invokes.
|
||||
58
docs/devtools/tooling/mcp/src/telemetry/telemetry.md
Normal file
58
docs/devtools/tooling/mcp/src/telemetry/telemetry.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# telemetry
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`telemetry` registers the OpenTelemetry meter `@suaveplan/mcp` and exposes three instruments (`mcp_request_total`, `mcp_notification_total`, `mcp_request_duration_ms`) that the dispatcher writes to on every request, response, and notification. Instruments resolve lazily through `metrics.getMeter`, so the module is safe to import in production builds that have not opted into OTel — when no provider is registered the API returns a no-op meter, the instruments stay alive, and consumers pay no cost.
|
||||
|
||||
## Features
|
||||
|
||||
- `requestCounter` — total JSON-RPC requests, tagged with `direction` (`client` / `server`), `method`, and `status` (`success` / `error` / `cancelled`).
|
||||
- `notificationCounter` — total notifications crossing the dispatcher, tagged with `direction` and `method`.
|
||||
- `requestDurationHistogram` — duration in ms with `unit: "ms"`, tagged with `direction` and `method`.
|
||||
- Two helper functions (`recordRequest`, `recordNotification`) keep call sites tiny.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { recordRequest } from "@suaveplan/mcp/telemetry";
|
||||
|
||||
recordRequest("client", "tools/call", "success", 12.3);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
notificationCounter,
|
||||
recordRequest,
|
||||
requestDurationHistogram,
|
||||
} from "@suaveplan/mcp";
|
||||
|
||||
// Tag the histogram from inside a wrapper that adds your own dimension.
|
||||
function withTenant(tenant: string) {
|
||||
return {
|
||||
record(method: string, ms: number) {
|
||||
requestDurationHistogram.record(ms, { direction: "client", method, tenant });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Manually emit a notification metric from a custom transport bridge.
|
||||
notificationCounter.add(1, { direction: "server", method: "notifications/initialized" });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `requestCounter: Counter` — created from `meter.createCounter("mcp_request_total", { description })`.
|
||||
- `notificationCounter: Counter` — `"mcp_notification_total"`.
|
||||
- `requestDurationHistogram: Histogram` — `"mcp_request_duration_ms"` with `unit: "ms"`.
|
||||
- `recordRequest(direction, method, outcome, durationMs)` — increments the counter and records the histogram in one call.
|
||||
- `recordNotification(direction, method)` — increments the notification counter.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`metrics.getMeter` returns a `Meter` even when no provider has been globally registered; instrument writes then become no-ops. Tests install a real `MeterProvider` in `tests/setup/polyfills.ts` BEFORE any source module imports, so the lazy binding inside this module captures the test exporter the first time it runs. Status tags use the dispatcher's `MCPRequestOutcome` type union (`success` / `error` / `cancelled`) so downstream dashboards can pivot on the same value the EventBus carries on `request:completed` events.
|
||||
61
docs/devtools/tooling/mcp/src/transport/http-sse/http-sse.md
Normal file
61
docs/devtools/tooling/mcp/src/transport/http-sse/http-sse.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# http-sse
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`HttpSseTransport` rides MCP over a duplex pair: outbound requests via HTTP POST to `endpoint` and inbound server-pushed messages via Server-Sent Events on the same endpoint. The transport is universal — it imports neither `fetch` nor `EventSource` directly; both are passed in as `fetchImpl` and `eventSourceImpl`. Production callers in browsers / Node / Bun / Deno bind the platform globals; tests bind fakes.
|
||||
|
||||
## Features
|
||||
|
||||
- Injectable `fetchImpl` and `eventSourceImpl` — no platform-global dependence.
|
||||
- POSTs JSON-stringified `JsonRpcMessage` bodies with `content-type: application/json`.
|
||||
- Parses each incoming SSE event payload through `JsonRpcMessageSchema` — malformed events are dropped silently.
|
||||
- Maps non-2xx HTTP responses to `MCPTransportError(TRANSPORT_ERROR)` with the status code in the message.
|
||||
- Inherits the closed-guard / listener registry / idempotent close from `BaseTransport`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createHttpSseTransport } from "@suaveplan/mcp/transport/http-sse";
|
||||
|
||||
const transport = createHttpSseTransport({
|
||||
endpoint: "https://mcp.example.com/v1",
|
||||
fetchImpl: globalThis.fetch,
|
||||
eventSourceImpl: globalThis.EventSource,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createHttpSseTransport, type FetchLike } from "@suaveplan/mcp";
|
||||
|
||||
// Inject a bearer token via a wrapping fetch.
|
||||
function authedFetch(token: string): FetchLike {
|
||||
return async (url, init) => {
|
||||
return await globalThis.fetch(url, {
|
||||
...init,
|
||||
headers: { ...init.headers, authorization: `Bearer ${token}` },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const transport = createHttpSseTransport({
|
||||
endpoint: "https://mcp.example.com/v1",
|
||||
fetchImpl: authedFetch(process.env.MCP_TOKEN ?? ""),
|
||||
eventSourceImpl: globalThis.EventSource,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface HttpSseTransportOptions` — `{ endpoint, fetchImpl, eventSourceImpl }`.
|
||||
- `class HttpSseTransport extends BaseTransport` — constructor opens an `EventSource` against `endpoint` and subscribes to `"message"` events.
|
||||
- `createHttpSseTransport(options)` — convenience factory.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The transport opens its EventSource synchronously in the constructor; the spec semantics of EventSource say the connection is established asynchronously, so callers can `send()` before the first event arrives. `sendInternal` checks `response.ok` and surfaces non-2xx as `MCPTransportError(TRANSPORT_ERROR)` carrying the status code — the dispatcher then rejects the corresponding pending request. The `EventSourceLike` interface in `types/types.ts` accepts only what the transport actually uses (`addEventListener("message", h)` + `close()`), so callers can supply any class with that surface — a polyfill, a mock, or a polling stub.
|
||||
@@ -0,0 +1,55 @@
|
||||
# in-memory
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`InMemoryTransport.pair()` creates two `Transport` instances wired head-to-head. A `send` on one is immediately dispatched on the other after a microtask boundary, giving callers a fully working transport without I/O. The package's tests use it to wire `MCPServer` + `MCPClient` in the same process, and production callers embedding both sides reach for it for the same reason.
|
||||
|
||||
## Features
|
||||
|
||||
- Static `InMemoryTransport.pair()` factory returns `[a, b]`.
|
||||
- Microtask-deferred delivery: callers see `send()` as truly async (consistent with stdio and HTTP transports).
|
||||
- Idempotent close: closing one half closes its peer as well.
|
||||
- Inherits the `closed` guard from `BaseTransport`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { InMemoryTransport } from "@suaveplan/mcp/transport/in-memory";
|
||||
|
||||
const [a, b] = InMemoryTransport.pair();
|
||||
a.onMessage((m) => console.log("from b:", m));
|
||||
await b.send({ jsonrpc: "2.0", id: 1, method: "ping" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
InMemoryTransport,
|
||||
MCPClient,
|
||||
MCPServer,
|
||||
} from "@suaveplan/mcp";
|
||||
|
||||
const [serverSide, clientSide] = InMemoryTransport.pair();
|
||||
const server = new MCPServer({ info: { name: "demo", version: "0.1.0" } });
|
||||
const client = new MCPClient({ info: { name: "agent", version: "0.1.0" } });
|
||||
|
||||
await server.start(serverSide);
|
||||
await client.connect(clientSide);
|
||||
const tools = client.tools(); // Populated by the initialize-handshake refresh.
|
||||
await client.disconnect();
|
||||
await server.stop();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class InMemoryTransport extends BaseTransport` — only public API is the static factory and the inherited `Transport` methods.
|
||||
- `static InMemoryTransport.pair(): [InMemoryTransport, InMemoryTransport]` — returns a wired pair.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`sendInternal` `await`s `Promise.resolve()` before invoking `peer.dispatch(message)` so callers observe send semantics consistent with network transports: a `send` returning before the peer's listener has run. If the peer has been closed, `sendInternal` returns silently (dispatcher's `request()` then times out or remains pending until its abort signal fires) — this mirrors a half-closed pipe rather than a hard failure. `closeInternal` closes the peer too, but guards against re-entrant cascades by clearing its own `peer` reference before the recursive `close()`.
|
||||
66
docs/devtools/tooling/mcp/src/transport/stdio/stdio.md
Normal file
66
docs/devtools/tooling/mcp/src/transport/stdio/stdio.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# stdio
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`StdioTransport` frames JSON-RPC messages over a newline-delimited JSON byte stream — the MCP spec default for sub-process child servers. The transport is universal: it accepts an injectable `readable: AsyncIterable<Uint8Array>` and a `writable: UniversalWritable`, so callers wire `Readable.toWeb(process.stdin)`, `Bun.stdin.stream()`, browser `ReadableStream`, or an in-memory queue without the package depending on any Node-only module.
|
||||
|
||||
## Features
|
||||
|
||||
- Newline-delimited JSON framing — one message per `\n`-terminated line.
|
||||
- Injectable streams: no `node:fs` / `node:process` / `node:stream` imports.
|
||||
- Streaming parser tolerates partial chunks via a buffer + `TextDecoder({ stream: true })`.
|
||||
- Malformed lines are skipped silently; framing recovers on the next newline.
|
||||
- Inherits `closed` guard, listener registry, and idempotent close from `BaseTransport`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createStdioTransport } from "@suaveplan/mcp/transport/stdio";
|
||||
import type { JsonRpcMessage } from "@suaveplan/mcp";
|
||||
|
||||
const sentLines: string[] = [];
|
||||
const inbox = new ReadableStream<Uint8Array>({ start() { /* … */ } });
|
||||
|
||||
const transport = createStdioTransport({
|
||||
readable: inbox,
|
||||
writable: {
|
||||
async write(chunk) { sentLines.push(new TextDecoder().decode(chunk)); },
|
||||
async close() {},
|
||||
},
|
||||
});
|
||||
|
||||
transport.onMessage((m: JsonRpcMessage) => console.log("rx", m));
|
||||
await transport.send({ jsonrpc: "2.0", id: 1, method: "ping" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createStdioTransport } from "@suaveplan/mcp";
|
||||
|
||||
// Bridge to a child process spawned by your test harness.
|
||||
async function bridgeProcess(child: { stdout: AsyncIterable<Uint8Array>; stdin: WritableStream }) {
|
||||
const writer = child.stdin.getWriter();
|
||||
return createStdioTransport({
|
||||
readable: child.stdout,
|
||||
writable: {
|
||||
async write(chunk) { await writer.write(chunk); },
|
||||
async close() { await writer.close(); },
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface StdioTransportOptions` — `{ readable: AsyncIterable<Uint8Array>; writable: UniversalWritable }`.
|
||||
- `class StdioTransport extends BaseTransport` — constructor takes `StdioTransportOptions`; starts reading asynchronously.
|
||||
- `createStdioTransport(options): StdioTransport` — convenience factory mirroring the documented public API.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The reader loop maintains an internal text buffer; each chunk is decoded with `TextDecoder({ stream: true })` so multi-byte sequences split across reads still decode cleanly. After each newline the line is `trim`-ed and `JSON.parse`-d; failures or schema rejections are dropped because line framing makes the next message independent. Send framing writes `JSON.stringify(message) + "\n"` — `JSON.stringify` always escapes control characters in strings, so a literal newline in payload text becomes `\\n` and never breaks the framing. `closeInternal` flips a `stopped` flag the reader checks on each iteration and then closes the writable side; calling close while a long read is pending lets the reader exit on its next `for await` step.
|
||||
@@ -0,0 +1,68 @@
|
||||
# transport
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The `Transport` interface is the contract that decouples `JsonRpcDispatcher` from any specific I/O backend. Concrete transports — `InMemoryTransport`, `StdioTransport`, `HttpSseTransport` — implement `send` / `onMessage` / `close` and rely on `BaseTransport` for closed-flag bookkeeping, listener registration, and the guarded send path that raises `MCPTransportError(TRANSPORT_CLOSED)` when callers send after close.
|
||||
|
||||
## Features
|
||||
|
||||
- `Transport` interface: `send(message)`, `onMessage(handler)`, `close()`, `closed` getter.
|
||||
- `BaseTransport` abstract class: implements the closed flag, listener set, and `send` guard; subclasses only implement `sendInternal` and call `dispatch` when they receive an inbound message.
|
||||
- Idempotent `close()` — calling twice is a no-op.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { BaseTransport } from "@suaveplan/mcp/transport";
|
||||
|
||||
class LoopbackTransport extends BaseTransport {
|
||||
private peer?: LoopbackTransport;
|
||||
static pair() {
|
||||
const a = new LoopbackTransport();
|
||||
const b = new LoopbackTransport();
|
||||
a.peer = b; b.peer = a;
|
||||
return [a, b] as const;
|
||||
}
|
||||
protected async sendInternal(message: unknown) {
|
||||
this.peer?.["dispatch"](message as never);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { BaseTransport, type Transport } from "@suaveplan/mcp";
|
||||
import type { JsonRpcMessage } from "@suaveplan/mcp/schemas";
|
||||
|
||||
class LoggingTransport extends BaseTransport {
|
||||
constructor(private readonly inner: Transport) {
|
||||
super();
|
||||
inner.onMessage((m) => this["dispatch"](m as JsonRpcMessage));
|
||||
}
|
||||
protected async sendInternal(message: JsonRpcMessage) {
|
||||
console.log("→", message);
|
||||
await this.inner.send(message);
|
||||
}
|
||||
protected async closeInternal() {
|
||||
await this.inner.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface Transport` — see source for the four members.
|
||||
- `abstract class BaseTransport implements Transport`.
|
||||
- `BaseTransport.send(message)` — guarded: raises `MCPTransportError(MCP_CODES.TRANSPORT_CLOSED)` if `closed === true`.
|
||||
- `BaseTransport.onMessage(handler)` — returns an unsubscribe function.
|
||||
- `BaseTransport.close()` — idempotent; calls `closeInternal()` and clears the listener set.
|
||||
- Subclasses override `sendInternal(message)` and optionally `closeInternal()`; they call the protected `dispatch(message)` when an inbound frame arrives.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`dispatch` is `protected` rather than public so external callers can never spoof incoming messages — only the subclass that parses the wire can publish a message to the listener set. The closed flag is set before `closeInternal` runs so any reentrant `send` raised by cleanup logic still fails cleanly. The dispatcher passes itself directly to `Transport.onMessage`; the returned unsubscribe is stored and invoked on `dispatcher.close()` so an idle transport stops fanning messages back into a dead dispatcher.
|
||||
70
docs/devtools/tooling/mcp/src/types/types.md
Normal file
70
docs/devtools/tooling/mcp/src/types/types.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`types/types.ts` collects the structural / scalar types `@suaveplan/mcp` uses across modules that are NOT part of the wire format. Wire shapes live in `schemas/schemas.ts` and are derived from Zod schemas; this module is the home for transport-layer plumbing types — `UniversalWritable`, `EventSourceLike`, `EventSourceCtor`, `FetchLike` — plus scalar aliases that recur in many signatures (`JsonRpcId`, `ProgressToken`, `Cursor`, `MCPDirection`, `MCPRequestOutcome`, `MCPLoggingLevel`).
|
||||
|
||||
## Features
|
||||
|
||||
- `JsonRpcId`, `ProgressToken`, `Cursor` — scalar aliases keeping public signatures self-documenting.
|
||||
- `MCPLoggingLevel` — string-literal union mirroring the wire `LoggingLevelSchema` (`debug`/`info`/`notice`/`warning`/`error`/`critical`/`alert`/`emergency`).
|
||||
- `MCPDirection` (`"client" | "server"`) — used by telemetry tags and event payloads.
|
||||
- `MCPRequestOutcome` (`"success" | "error" | "cancelled"`) — used by telemetry tags and the `request:completed` event.
|
||||
- `UniversalWritable` — minimal `{ write(chunk): Promise<void>; close(): Promise<void> }` surface accepted by `StdioTransport`.
|
||||
- `EventSourceLike` + `EventSourceCtor` — minimal contracts that `HttpSseTransport` accepts.
|
||||
- `FetchLike` — narrowed `fetch` signature accepted by `HttpSseTransport`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { MCPDirection, MCPRequestOutcome } from "@suaveplan/mcp";
|
||||
|
||||
function recordOutcome(direction: MCPDirection, outcome: MCPRequestOutcome): void {
|
||||
metrics.counter("mcp.requests", { direction, outcome });
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { FetchLike, UniversalWritable } from "@suaveplan/mcp";
|
||||
|
||||
// Wrap node:fs streams behind the UniversalWritable shape so we never import them
|
||||
// at the package boundary.
|
||||
async function fileSink(path: string): Promise<UniversalWritable> {
|
||||
const { createWriteStream } = await import("node:fs");
|
||||
const handle = createWriteStream(path);
|
||||
return {
|
||||
async write(chunk: Uint8Array): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
handle.write(chunk, (err) => (err ? reject(err) : resolve())),
|
||||
);
|
||||
},
|
||||
async close() {
|
||||
await new Promise<void>((resolve) => handle.end(resolve));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const stubFetch: FetchLike = async () => ({ ok: true, status: 200, async text() { return "{}"; } });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `JsonRpcId = string | number`.
|
||||
- `ProgressToken = string | number`.
|
||||
- `Cursor = string`.
|
||||
- `MCPLoggingLevel` — see above.
|
||||
- `MCPDirection`, `MCPRequestOutcome` — see above.
|
||||
- `UniversalWritable` — `{ write(chunk: Uint8Array): Promise<void>; close(): Promise<void> }`.
|
||||
- `EventSourceLike` — `{ addEventListener("message", h): void; close(): void }`.
|
||||
- `EventSourceCtor` — `new (url: string) => EventSourceLike`.
|
||||
- `FetchLike` — narrowed fetch signature returning `{ ok, status, text() }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Keeping these structural types out of the Zod-schema module is intentional: schemas describe **wire** types, this file describes **runtime plumbing**. Universal purity is enforced by NOT importing platform-globals here — `FetchLike` is structurally narrower than the DOM `fetch` because the package only POSTs JSON, never multipart, never streamed bodies, so the narrower contract preserves drop-in compatibility with the platform global while keeping the type self-documenting.
|
||||
Reference in New Issue
Block a user