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.clientCapabilities`, `ctx.enrich`, `ctx.content`), and when to use each.

소스 정보

저장소
cyanheads/pubmed-mcp-server
최근 소스 활동
2026년 10월 9일 17:24
감지된 SKILL.md 언어
영어
스타
162
포크
24

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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.clientCapabilities`, `ctx.enrich`, `ctx.content`), and when to use each.
metadata
{"author":"cyanheads","version":"2.12","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; // Per request; returned on its errors as data.requestId 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, options?) => never — suspends and asks the caller readonly inputs: ContextInputs; // the request's responses, limited to what the client declared // What the client declared: the SDK-parsed `initialize` value (2025 era) or the // request's envelope as sent (2026-07-28); undefined when no view exists // (see § ctx.clientCapabilities) readonly clientCapabilities: ClientCapabilities | undefined; // 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; // Contract resolver — always present (returns {} when no contract is attached or the // reason is unknown), strictly typed on HandlerContext<R> against declared reasons. // The framework already sends a declared hint with any failure carrying the reason. 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 | A generated `XXXXX-XXXXX` token, one per call — never the client's JSON-RPC id, which the call's log records carry as `jsonRpcId` instead. Every log record of the call carries it, and the framework returns it on the call's error envelope as `data.requestId`. A 2025-era `ctx.requestInput` round trip re-enters the handler under a new token; the shared `jsonRpcId` ties the entries together | | `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`, `clientCapabilities`, `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 — except that an `extra` key named like a canonical field the context sets never replaces it. ### 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, endpoint: 'search' }); // 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, endpoint: 'search' })); // 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). One level check gates both sinks: `MCP_LOG_LEVEL` (or a runtime `logger.setLevel()`) is a floor for the client stream as well as the process log, compared on the RFC 5424 order, so a `notice` floor drops `info` from both. The SDK then filters by the client's own level — `logging/setLevel`, or the `io.modelcontextprotocol/logLevel` a 2026-07-28 request carries — which can only narrow the floor, never widen it. The wire payload is `{ message, ...data }` with every sensitive field masked as `[REDACTED]` at any depth: the same field list and matcher the logs are redacted with, extensible through `sanitization.setSensitiveFields`. A key is masked when some run of its adjacent words, joined, equals a sensitive name, case and separators ignored — words split at every character other than a letter or digit, at a lowercase letter followed by a capital, at the end of a run of capitals, and around each run of digits — so `API_KEY`, `x-api-key`, `accessToken`, `apiKey2`, and `tokenCount` are masked while `max_tokens`, `MAX_TOKENS`, and `tokenizer` are not. An `Error` in `data`, at any depth, goes on the wire as `{ type, message }` only — no stack, cause, or other own property such as a request URL; the process log writes it in full (`api-telemetry` Logs). The mirror keeps the process log's bounds: objects through 15 levels below the data root, one 16 levels down as `'[MaxDepth]'`, repeated content (an object or `toJSON()` result reached again, or a string of 1,024+ characters written again) charged about its written size against 1,000,000 characters and cut with `'[Truncated]'`, at most 400,000 reads a walk, one per object, field, and array element (so data a getter or Proxy builds on every read is cut too), at most 16 MiB (16,777,216 characters) of strings, field names, and primitives a walk writes, repeated or not, then `'[Truncated]'` and nothing more, a reference back to an enclosing object as `'[Circular]'`, and a value whose read throws (a getter, a Proxy trap, a `toJSON`) as `'[Unreadable]'`, with `data` that cannot be read at all (a revoked Proxy) written as `data: '[Unreadable]'` on both sinks — so no value in `data` can stall the handler or fail it. The caller's `data` object is never modified. `ctx.log.error` adds `error: <message>`. `message` and `error` are reserved wire keys, written after `data`: a `message` in `data` never replaces the log line on the wire, and on `ctx.log.error` with an `Error` the `error` key is always that error's message as a string — a Symbol as `'Symbol(…)'`, a number as its digits, and `'[Unreadable]'` when reading it throws or it is an object — and the notification is still sent. The process log line still carries the caller's own fields, except one reusing a canonical name the context already sets (`requestId`, `traceId`, `spanId`, `tenantId`, …) — there the context's value wins, so the line stays correlated to its request — and one named after a field the logger writes on the line itself (`level`, `time`, `msg`, `env`, `version`, `pid`, `hostname`, and `err` on `ctx.log.error` with an `Error`), which is written as `data_<name>` so the line keeps its own `level` and `version`. 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 JSON-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>
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기