Skip to main content

api-context

Canonical reference for the unified `Context` object passed to every tool and resource handler in `@cyanheads/mcp-ts-core`. Covers the full interface, its `RequestContext` base, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.requestInput`, `ctx.inputs`, `ctx.enrich`, `ctx.content`), and when to use each.

Zur Installation springen

Quellinformationen

Repository
cyanheads/mcp-ts-core
Letzte Quellaktivität
13. September 2026 um 21:13
Erkannte Sprache von SKILL.md
Englisch
Sterne
151
Forks
29

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
api-context
description
Canonical reference for the unified `Context` object passed to every tool and resource handler in `@cyanheads/mcp-ts-core`. Covers the full interface, its `RequestContext` base, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.requestInput`, `ctx.inputs`, `ctx.enrich`, `ctx.content`), and when to use each.
metadata
{"author":"cyanheads","version":"2.4","audience":"external","type":"reference"}
## Overview Every tool and resource handler receives a single `Context` (`ctx`) argument. It provides request identity, structured logging, tenant-scoped storage, multi-round-trip input collection, and cancellation — all auto-correlated to the current request. The framework auto-instruments every handler call (OTel span, duration, payload metrics). Use `ctx.log` for domain-specific logging and `ctx.state` for storage inside handlers. Use the global `logger` and `StorageService` directly only in lifecycle/background code (`setup()`, services). --- ## `Context` interface ```ts import type { Context } from '@cyanheads/mcp-ts-core'; interface Context extends RequestContext { // Identity & tracing (inherited from RequestContext — see § RequestContext) readonly requestId: string; // Unique per request, auto-generated readonly timestamp: string; // ISO 8601 request start time readonly tenantId?: string; // JWT 'tid' claim; 'default' for stdio and HTTP+MCP_AUTH_MODE=none readonly sessionId?: string; // Mcp-Session-Id (HTTP stateful/auto); undefined elsewhere unless opted in readonly traceId?: string; // Trace containing this handler execution readonly spanId?: string; // The handler's own execution span readonly auth?: AuthContext; // Parsed auth claims (clientId, scopes, sub) readonly operation?: string; // Label for the operation this context belongs to readonly extra?: Readonly<Record<string, unknown>>; // Correlation bag — the one open field // Structured logging — auto-includes requestId, traceId, tenantId. // Dual-sink: Pino on the server, plus notifications/message to the client. readonly log: ContextLogger; // Tenant-scoped key-value storage readonly state: ContextState; // Multi-round-trip input — always present, both eras (see § ctx.requestInput) readonly requestInput: RequestInputFn; // (spec) => never — suspends and asks the caller readonly inputs: ContextInputs; // reader over a retried request's responses // List-changed / resource-updated notifications — wired in every handler ctx; // delivery is request-scoped (see § list-changed notifications) readonly notifyResourceListChanged?: () => void; readonly notifyResourceUpdated?: (uri: string) => void; readonly notifyPromptListChanged?: () => void; readonly notifyToolListChanged?: () => void; // Cancellation readonly signal: AbortSignal; // Raw URI — present only for resource handlers readonly uri?: URL; // Agent-facing success-path enrichment — accumulates notices, query echo, totals // onto the request; reaches structuredContent + content[]. Always present (no-op // when no `enrichment` block), strictly typed on HandlerContext<R, E> against the // declared fields. Kind-tagged helpers: enrich.notice / .total / .echo. readonly enrich: Enrich; // Non-text content blocks (image/audio bytes) for the calling model — prepended // to content[] after format() runs, never placed in structuredContent. Always // present (no-op when never called). Helpers: content.image / .audio; content(block) // pushes a raw ContentBlock. readonly content: ContentCollect; // Opt-in contract resolver — always present (returns {} when no contract is attached // or the reason is unknown), strictly typed on HandlerContext<R> against declared reasons. recoveryFor(reason: string): { recovery: { hint: string } } | {}; } ``` > **`ctx.fail` is on `HandlerContext<R>`, not `Context`.** When a definition declares `errors: [...]`, the handler receives `HandlerContext<R> = Context & { fail: TypedFail<R>; recoveryFor: TypedRecoveryFor<R> }` — both the typed `fail` and the strictly-typed `recoveryFor` live on the intersection. The bare `Context.recoveryFor` is the loose, always-present resolver. See [`ctx.fail`](#ctxfail) and [`ctx.recoveryFor`](#ctxrecoveryfor) below. ### Identity fields | Field | Always present | Source | |:------|:--------------|:-------| | `requestId` | Yes | Auto-generated UUID per request | | `timestamp` | Yes | ISO 8601, request start | | `tenantId` | Stdio and HTTP+`MCP_AUTH_MODE=none` (as `'default'`); JWT `tid` claim in HTTP+`jwt`/`oauth` | JWT / single-tenant default | | `sessionId` | HTTP `stateful` / `auto` mode; undefined for stdio and stateless HTTP unless opted in | `Mcp-Session-Id` header (or server-minted) — see [§ `ctx.sessionId`](#ctxsessionid) | | `traceId` | When OTEL enabled | Trace containing this handler execution | | `spanId` | When OTEL enabled | The active `tool_execution:*` / `resource_read:*` span | | `auth` | When auth enabled | Parsed JWT claims | --- ## `RequestContext` — the one canonical request shape `Context extends RequestContext`. There is a single request-shape type; the handler-facing `Context` adds handler-only surfaces (`log`, `state`, `signal`, `requestInput`, `inputs`, `enrich`, `content`, `uri`) on top of it and redeclares none of the identity fields. A handler's `ctx` is therefore assignable anywhere a `RequestContext` is — services, storage, the framework logger — with no slice helper and no cast. ```ts import { requestContextService, withExtra } from '@cyanheads/mcp-ts-core/utils'; import type { RequestContext } from '@cyanheads/mcp-ts-core/utils'; // A service typed against RequestContext accepts a handler ctx directly. async function fetchUser(id: string, ctx: RequestContext) { /* … */ } await fetchUser('123', ctx); // ctx is a Context — no conversion ``` ### Closed by design `RequestContext` has **no index signature**. Its fields are exactly: `auth`, `extra`, `operation`, `requestId`, `sessionId`, `spanId`, `tenantId`, `timestamp`, `traceId`. A misspelled canonical field (`tenatId`) is a compile error instead of a silently-ignored key. Operation-specific correlation data goes in **`extra`** — the one deliberate open bag (`Readonly<Record<string, unknown>>`). The logger flattens `extra` into the emitted line, so log output looks the same as a top-level spread. ### Adding correlation data Three supported ways, most common first: ```ts // 1. Per-log-call metadata — the common case. Nothing lands on the context. ctx.log.info('Retrying upstream call', { attempt, url }); // 2. A copy of this context carrying extra fields. `withExtra` MERGES into any // bag the parent already had; a hand-written `{ ...ctx, extra: {…} }` replaces it. logger.warning('Retrying upstream call', withExtra(ctx, { attempt, url })); // 3. A derived context for a sub-operation. `additionalContext` lands on `extra`, // merged over whatever the parent already carried. const childCtx = requestContextService.createRequestContext({ parentContext: ctx, operation: 'processItem', additionalContext: { itemId: item.id }, // → childCtx.extra.itemId }); // Reading an ad-hoc key back off a context: const itemId = childCtx.extra?.itemId; ``` `createRequestContext(params)` takes a closed parameter object — `additionalContext`, `operation`, `parentContext`, `tenantId` — and nothing else; a key it doesn't declare is a compile error rather than an arbitrary passthrough. Never re-open the shape to get past a type error: no index signature, no widening a parameter back to `Record<string, unknown>`, no `as` cast. A `{ ...ctx, someKey }` object literal that fails to compile is the signal to move `someKey` into `extra`, not to loosen the type. `ErrorContext` (the `ErrorHandler` call's `context`) is `Partial<RequestContext>` and is closed the same way — put ad-hoc keys under `extra` via `withExtra`, or pass them in the `ErrorHandler` call's own `context` field. `RequestContextLike` is a deprecated alias for `RequestContext`, kept for one minor. Replace every use with `RequestContext`, and collapse any `RequestContextLike | RequestContext` parameter union to plain `RequestContext`. --- ## `ctx.log` Request-scoped structured logger. Every log line is automatically annotated with `requestId`, `traceId`, and `tenantId` — no manual spreading needed. **Dual-sink.** Each call writes to Pino *and* mirrors onto the MCP wire as a `notifications/message` (the framework advertises the `logging` capability, and the SDK filters by the level the client set via `logging/setLevel`). The wire payload is `{ message, ...data }`; `ctx.log.error` adds `error: <message>`. Delivery is fire-and-forget — a client that never upgraded to SSE, set a higher level, or already disconnected drops the notification, and a failed send never fails the handler. Treat `ctx.log` as client-visible: it is no longer a server-only sink, so don't log anything there you wouldn't put in a tool result. ### Methods | Method | Level | |:-------|:------| | `ctx.log.debug(msg, data?)` | Verbose debugging | | `ctx.log.info(msg, data?)` | Normal operational events | | `ctx.log.notice(msg, data?)` | Significant but non-error events | | `ctx.log.warning(msg, data?)` | Recoverable issues, unexpected states | | `ctx.log.error(msg, error?, data?)` | Errors (second arg is the Error object) | ### Usage ```ts // Basic ctx.log.info('Processing query', { query: input.query }); // With error object (second arg) ctx.log.error('Failed to fetch upstream', error, { url, statusCode }); // Debug detail ctx.log.debug('Cache miss', { key, ttl }); ``` ### `ctx.log` vs global `logger` | Use | Where | |:----|:------| | `ctx.log` | Inside tool/resource handlers — auto-correlated to the request | | `core.logger` / `logger` | In `setup()`, service constructors, background tasks — no request context available | The global `logger` is imported from `@cyanheads/mcp-ts-core/utils`. In handlers, prefer `ctx.log`. --- ## `ctx.state` Tenant-scoped key-value storage. Delegates to `StorageService` with automatic `tenantId` scoping — data written under tenant A is invisible to tenant B. ### Interface ```ts interface ContextState { get<T = unknown>(key: string): Promise<T | null>; get<T>(key: string, schema: ZodType<T>): Promise<T | null>; // runtime-validated set(key: string, value: unknown, opts?: { ttl?: number }): Promise<void>; delete(key: string): Promise<void>; deleteMany(keys: string[]): Promise<number>; getMany<T = unknown>(keys: string[]): Promise<Map<string, T>>; setMany(entries: Map<string, unknown>, opts?: { ttl?: number }): Promise<void>; list(prefix?: string, opts?: { cursor?: string; limit?: number }): Promise<{ items: Array<{ key: string; value: unknown }>; cursor?: string; // opaque base64url; omitted on last page }>; } ``` ### Usage ```ts // Store — accepts any serializable value, no manual JSON.stringify needed await ctx.state.set('item/123', { name: 'Widget', count: 42 }); await ctx.state.set('session/xyz', token, { ttl: 3600 }); // TTL in seconds // Retrieve — generic type assertion or Zod-validated const item = await ctx.state.get<Item>('item/123'); // T | null (type assertion) const safe = await ctx.state.get('item/123', ItemSchema); // T | null (runtime validated) // Delete await ctx.state.delete('item/123'); // Batch operations const values = await ctx.state.getMany<Item>(['item/1', 'item/2']); // Map<string, T> await ctx.state.setMany(new Map([['a', 1], ['b', 2]])); const deleted = await ctx.state.deleteMany(['item/1', 'item/2']); // number // List with prefix + pagination const page = await ctx.state.list('item/', { cursor, limit: 20 }); for (const { key, value } of page.items) { /* ... */ } if (page.cursor) { /* more pages available */ } ``` ### Behavior notes - Throws `McpError(InvalidRequest)` if `tenantId` is missing. Won't happen in stdio (any auth mode) or HTTP+`MCP_AUTH_MODE=none` — both default to `'default'`. Can happen in HTTP+`MCP_AUTH_MODE=jwt`/`oauth` when the token lacks a `tid` claim (intentional fail-closed: distinct authenticated callers must not silently share state). - Keys are tenant-prefixed internally; handlers never need to namespace manually. - **Key charset:** `^[a-zA-Z0-9_.\-/]+$`, 1024 chars max, no `..`. Slashes are the namespace separator — a colon (`item:123`) throws `McpError(ValidationError)` on every call. The rule covers `list` prefixes and every key in a batch operation. `createMockContext().state` enforces it identically, so an illegal key fails in the test rather than in a deployment. - **Workers persistence:** The `in-memory` provider loses data on cold starts. Use `cloudflare-kv`, `cloudflare-r2`, or `cloudflare-d1` for durable storage in Workers. --- ## `ctx.sessionId` Optional HTTP session identifier. Surfaced when the request carries a durable session — handlers use it as a *discovery / scoping key* on top of tenant-keyed `ctx.state`, not as an authorization principal. ### When it's defined | Transport / mode | `ctx.sessionId` | |:-----------------|:----------------| | stdio (any auth) | `undefined` | | HTTP, `MCP_SESSION_MODE=stateless` | `undefined` (default) — see [opt-in](#stateless-mode-opt-in) | | HTTP, `stateful` / `auto`, `MCP_AUTH_MODE=none` | session token; possession = access (no identity binding) |
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen