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.

Ir a la instalación

Datos de origen

Repositorio
cyanheads/mcp-ts-core
Última actividad en el origen
13 de septiembre de 2026 a las 21:13
Idioma detectado de SKILL.md
inglés
Estrellas
151
Forks
29

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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) |
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub