Agent skill
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, all sub-APIs (`ctx.log`, `ctx.state`, `ctx.elicit`, `ctx.sample`, `ctx.progress`), and when to use each.
Install this agent skill to your Project
npx add-skill https://github.com/cyanheads/mcp-ts-core/tree/main/skills/api-context
Metadata
Additional technical details for this skill
- type
- reference
- author
- cyanheads
- version
- 1.0
- audience
- external
SKILL.md
Overview
Every tool and resource handler receives a single Context (ctx) argument. It provides request identity, structured logging, tenant-scoped storage, optional protocol capabilities (elicitation, sampling), cancellation, and task progress — 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
import type { Context } from '@cyanheads/mcp-ts-core';
interface Context {
// Identity & tracing
readonly requestId: string; // Unique per request, auto-generated
readonly timestamp: string; // ISO 8601 request start time
readonly tenantId?: string; // From JWT 'tid' claim; 'default' in stdio mode
readonly traceId?: string; // OTEL trace ID (present when OTEL enabled)
readonly spanId?: string; // OTEL span ID (present when OTEL enabled)
readonly auth?: AuthContext; // Parsed auth claims (clientId, scopes, sub)
// Structured logging — auto-includes requestId, traceId, tenantId
readonly log: ContextLogger;
// Tenant-scoped key-value storage
readonly state: ContextState;
// Optional protocol capabilities (undefined when client doesn't support them)
readonly elicit?: (message: string, schema: z.ZodObject<z.ZodRawShape>) => Promise<ElicitResult>;
readonly sample?: (messages: SamplingMessage[], opts?: SamplingOpts) => Promise<CreateMessageResult>;
// Cancellation
readonly signal: AbortSignal;
// Task progress — present only when tool is defined with task: true
readonly progress?: ContextProgress;
// Raw URI — present only for resource handlers
readonly uri?: URL;
}
Identity fields
| Field | Always present | Source |
|---|---|---|
requestId |
Yes | Auto-generated UUID per request |
timestamp |
Yes | ISO 8601, request start |
tenantId |
In stdio (as 'default'); from JWT tid claim in HTTP |
JWT / stdio default |
traceId |
When OTEL enabled | OTEL trace context |
spanId |
When OTEL enabled | OTEL trace context |
auth |
When auth enabled | Parsed JWT claims |
ctx.log
Request-scoped structured logger. Every log line is automatically annotated with requestId, traceId, and tenantId — no manual spreading needed.
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
// 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
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
// 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)iftenantIdis missing (won't happen in stdio mode — defaults to'default'). - Keys are tenant-prefixed internally; handlers never need to namespace manually.
- Workers persistence: The
in-memoryprovider loses data on cold starts. Usecloudflare-kv,cloudflare-r2, orcloudflare-d1for durable storage in Workers.
ctx.elicit / ctx.sample
Both are optional — undefined when the connected client doesn't support the capability. Check for presence before calling. A simple truthiness check is enough; no type guards needed.
ctx.elicit — ask the user for structured input
Presents a form to the user via the MCP elicitation protocol. The user fills in a Zod-validated schema and returns an action (accept, decline, or cancel).
if (ctx.elicit) {
const result = await ctx.elicit(
'Which output format do you want?',
z.object({
format: z.enum(['json', 'csv', 'markdown']).describe('Output format'),
includeHeaders: z.boolean().default(true).describe('Include column headers'),
}),
);
if (result.action === 'accept') {
// result.content is Record<string, string | number | boolean | string[]> | undefined
await produceOutput(result.content?.format as string, result.content?.includeHeaders as boolean);
} else {
// 'decline' or 'cancel' — user opted out
throw new McpError(JsonRpcErrorCode.InvalidRequest, 'User declined input');
}
}
ElicitResult (from @modelcontextprotocol/sdk/types.js):
// Actual SDK type — a flat object, not a discriminated union
interface ElicitResult {
action: 'accept' | 'decline' | 'cancel';
// Present when action === 'accept'; values are primitives or string arrays
content?: Record<string, string | number | boolean | string[]>;
}
Note:
contentis not typed against the Zod schema you pass — it is aRecordof primitives. Validatecontentagainst your schema manually (e.g.MySchema.parse(result.content)) whenaction === 'accept'.
Convention: Only call ctx.elicit from tool handlers, not from services.
ctx.sample — request an LLM completion from the client
Requests a completion from the client's LLM via the MCP sampling protocol. Useful for AI-assisted tool behavior without managing a separate LLM provider.
if (ctx.sample) {
const result = await ctx.sample(
[
{ role: 'user', content: { type: 'text', text: `Summarize: ${data}` } },
],
{ maxTokens: 500 },
);
return { summary: result.content.text };
}
SamplingOpts:
interface SamplingOpts {
includeContext?: 'none' | 'thisServer' | 'allServers';
maxTokens?: number;
modelPreferences?: Record<string, unknown>;
stopSequences?: string[];
temperature?: number;
}
Convention: Only call ctx.sample from tool handlers, not from services.
ctx.signal
Standard AbortSignal. Present on every context. Set when the client cancels the request or when a task tool is cancelled.
// Check before expensive operations
if (ctx.signal.aborted) return earlyResult;
// Pass through to fetch / other async APIs
const response = await fetch(url, { signal: ctx.signal });
// Loop with cancellation check
for (const item of items) {
if (ctx.signal.aborted) break;
await processItem(item);
}
In task tools (task: true), the framework signals ctx.signal when the client sends a cancellation request.
ctx.progress
Present only when the tool definition includes task: true. Undefined for standard (non-task) tools and all resource handlers.
Methods
| Method | Purpose |
|---|---|
ctx.progress.setTotal(n) |
Set the total number of steps (enables percentage calculation on client) |
ctx.progress.increment(amount?) |
Advance progress by amount (default: 1) |
ctx.progress.update(message) |
Send a descriptive status message without advancing the counter |
Usage
const asyncCountdown = tool('async_countdown', {
description: 'Count down from a number with progress updates.',
task: true,
input: z.object({
count: z.number().int().positive().describe('Number to count down from'),
delayMs: z.number().default(1000).describe('Delay between counts in ms'),
}),
output: z.object({
finalCount: z.number().describe('Final count value'),
message: z.string().describe('Completion message'),
}),
async handler(input, ctx) {
await ctx.progress!.setTotal(input.count);
for (let i = input.count; i > 0; i--) {
if (ctx.signal.aborted) break;
await ctx.progress!.update(`Counting: ${i}`);
await new Promise(resolve => setTimeout(resolve, input.delayMs));
await ctx.progress!.increment();
}
return { finalCount: 0, message: 'Countdown complete' };
},
});
Note: Use the non-null assertion (ctx.progress!) when accessing inside a task: true handler — the type is ContextProgress | undefined even though it's guaranteed present at runtime. TypeScript cannot narrow based on the task flag.
ctx.uri
Present only for resource handlers. The raw URL object for the matched resource URI.
export const myResource = resource('myscheme://{itemId}/data', {
async handler(params, ctx) {
ctx.log.debug('Resource accessed', { uri: ctx.uri?.toString() });
// params.itemId is extracted from the URI pattern — prefer params over ctx.uri
return fetchItem(params.itemId);
},
});
Prefer params (the extracted URI template variables) over parsing ctx.uri manually. ctx.uri is available when the raw URL string is needed.
Quick reference
| Property | Type | Present when |
|---|---|---|
ctx.requestId |
string |
Always |
ctx.timestamp |
string |
Always |
ctx.tenantId |
string | undefined |
Always in stdio ('default'); HTTP with auth |
ctx.traceId |
string | undefined |
OTEL enabled |
ctx.spanId |
string | undefined |
OTEL enabled |
ctx.auth |
AuthContext | undefined |
Auth enabled |
ctx.log |
ContextLogger |
Always |
ctx.state |
ContextState |
Always (throws if tenantId missing) |
ctx.signal |
AbortSignal |
Always |
ctx.elicit |
function | undefined |
Client supports elicitation |
ctx.sample |
function | undefined |
Client supports sampling |
ctx.progress |
ContextProgress | undefined |
Tool defined with task: true |
ctx.uri |
URL | undefined |
Resource handlers only |
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
add-resource
Scaffold a new MCP resource definition. Use when the user asks to add a resource, expose data via URI, or create a readable endpoint.
field-test
Exercise tools, resources, and prompts with real-world inputs to verify behavior end-to-end. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface. Calls each definition with realistic and adversarial inputs and produces a report of issues, pain points, and recommendations.
release
Verify release readiness and publish. The git wrapup protocol handles version bumps, changelog, README, commits, and tagging during the coding session. This skill verifies nothing was missed, runs final checks, and presents the irreversible publish commands.
add-export
Add a new subpath export to the @cyanheads/mcp-ts-core package. Use when creating a new public API surface that consumers import from a dedicated subpath (e.g., @cyanheads/mcp-ts-core/newutil).
api-errors
McpError constructor, JsonRpcErrorCode reference, and error handling patterns for `@cyanheads/mcp-ts-core`. Use when looking up error codes, understanding where errors should be thrown vs. caught, or using ErrorHandler.tryCatch in services.
api-utils
API reference for all utilities exported from `@cyanheads/mcp-ts-core/utils`. Use when looking up utility method signatures, options, peer dependencies, or usage patterns.
Didn't find tool you were looking for?