Schemas, Auth, Context, and Progress
Schemas, Auth, Context, and Progress at the Askr server boundary, with validated input and explicit failure states.
Example
Create one MCP server, register typed primitives on it, and let the selected transport supply auth, cancellation, sessions, and progress.
import { schema } from '@askrjs/schema';
import { createMcpServer } from '@askrjs/server/mcp';
export const mcp = createMcpServer({
name: 'project-tools',
version: '1.0.0',
}).tool(
'lookup-project',
{ input: schema.object({ id: schema.uuid() }) },
async (context, { id }) => ({
content: [{ type: 'text', text: await lookup(id, context.signal) }],
})
);Input schemas
Tool input, tool output, and prompt arguments are all @askrjs/schema schemas, and each is exposed to clients through generated JSON Schema (inputSchema, outputSchema) in the corresponding list response. Validation runs via safeParse before your handler executes; a failed parse short-circuits to a -32602 Invalid tool arguments (or Invalid prompt arguments) error carrying the schema's issues, so handlers never see malformed input. Skip input or arguments entirely and the server substitutes an empty object schema, which is useful for tools and prompts that take no parameters but still want to appear correctly in a client's UI.
Authorization
Every primitive accepts an optional auth: AuthRequirement in its options — the same requirement functions from @askrjs/auth used on HTTP routes, like requireScope('mcp:write') or requirePermission('reports:read'). Before a tool, resource, template, or prompt is listed or invoked, the server calls that requirement against the current AuthContext and checks .allowed; entries that fail are filtered out of list responses entirely and return Tool not found / Resource not found / Prompt not found if called directly, rather than a distinct "forbidden" error. That means unauthorized primitives are invisible, not merely blocked — a client with insufficient scope never learns the tool exists.
Handler context
Every handler receives an McpContext<Dependencies> with dependencies (whatever you passed into the transport's dependencies option), auth, the negotiated client info and clientCapabilities from initialize, protocolRevision, transport ("http" or "stdio"), an optional sessionId, and an AbortSignal that fires on cancellation or disconnect. It's the same context shape across transports, so a tool handler doesn't need to branch on whether it's being called over HTTP or stdio. context.progress(...) and context.log(...) are also part of this object — see progress notifications below.
Progress notifications
Call context.progress(progress, total?, message?) from inside a handler to emit a notifications/progress message, but only if the original request carried a _meta.progressToken — without one, the call is a no-op. context.log(level, data, logger?) sends notifications/message with one of the standard MCP log levels (debug through emergency). Both rely on environment.send, which is only wired up for transports that support server-initiated messages: the HTTP adapter provides it once a client has an open SSE channel for the session, and stdio provides it for every call since it can always write another line to stdout.