API reference: Feature modules
The per-feature subpath modules: OpenAPI generation, the typed client, contract tests, MCP, docs UIs, streaming, multipart, WebSocket primitives, tracing, metrics stores, and the CLI internals. Each module also re-exports from the root barrel unless noted. See the API reference overview for the complete module map.
single source of truthApp route contractsmethod + path + request + responses
/openapiOpenAPI 3.1 documentgenerateOpenAPI(app, opts)
/client · /contractTyped client & contract testscreateClient(app), runContractTests(app)
/docs · /mcp · /a2aDocs UIs, MCP tools & A2A agentsscalarHtml(), createMcpHandler(), createA2aHandler()
@daloyjs/core/openapi
ts
generateOpenAPI(app: App, opts: OpenAPIOptions): Record<string, unknown>;
openapiToYAML(doc: Record<string, unknown>): string;
interface OpenAPIOptions {
info: OpenAPIInfo;
servers?: { url: string; description?: string }[];
securitySchemes?: SecuritySchemeMap;
webhooks?: Record<string, WebhookDefinition | WebhookDefinition[]>;
}
interface OpenAPIInfo {
title: string;
version: string;
description?: string;
termsOfService?: string;
contact?: { name?: string; email?: string; url?: string };
license?: { name: string; identifier?: string; url?: string };
summary?: string;
}
// OpenAPI 3.1 top-level webhooks. Mirrors RouteDefinition minus path + handler.
interface WebhookDefinition {
method: HttpMethod;
operationId?: string;
summary?: string;
description?: string;
tags?: string[];
deprecated?: boolean;
request?: RequestSchemas;
responses: ResponsesMap;
auth?: AuthSpec;
}Discriminated unions (OpenAPI)
ts
discriminator(opts: DiscriminatorObject): unknown; // { propertyName, mapping? }
discriminatedUnion(prop: string, branches: StandardSchemaV1[],
opts?: DiscriminatedUnionOptions): StandardSchemaV1;@daloyjs/core/client
ts
createClient<A extends App>(app: A, opts: ClientOptions): ClientFor<A>;
createInProcessClient<A extends App>(app: A, opts?: InProcessClientOptions): ClientFor<A>;
interface ClientOptions {
baseUrl: string;
fetch?: typeof fetch;
headers?: Record<string, string>;
}
interface InProcessClientOptions {
baseUrl?: string; // default: http://daloy.local
headers?: Record<string, string>;
}
// ClientFor<A> is keyed by operationId; each method takes
// { params?, query?, headers?, body? } and returns a discriminated union
// keyed by status: { status, body, headers }.
type ClientFor<A extends App> = { /* generated from A["routes"] */ };
type RoutesOf<A extends App> = A["routes"][number];
// Path params are filled by whole segment and percent-encoded. A missing,
// empty, "." or ".." param throws TypeError before any request is sent.
// Wildcard routes ("*name") accept "/"-separated values, each segment checked.@daloyjs/core/contract
ts
runContractTests(app: App, opts?: ContractTestOptions): Promise<ContractReport>;
interface ContractTestOptions {
requireOperationId?: boolean; // default: true
allowBodyOnSafeMethods?: boolean; // default: false
}
interface ContractReport { ok: boolean; checked: number; issues: ContractIssue[] }
interface ContractIssue { route: string; method: HttpMethod; code: string; message: string }@daloyjs/core/mcp
ts
const MCP_PROTOCOL_VERSION = "2026-07-28";
const MCP_MODERN_ERA_MIN_VERSION = "2026-07-28";
const MCP_PROTOCOL_VERSIONS: readonly string[]; // 2024-11-05 … 2026-07-28
const MCP_DEFAULT_MAX_BODY_BYTES = 262144;
const MCP_MAX_REQUEST_STATE_LENGTH = 8192;
const MCP_META_KEYS: {
protocolVersion: "io.modelcontextprotocol/protocolVersion";
clientInfo: "io.modelcontextprotocol/clientInfo";
clientCapabilities: "io.modelcontextprotocol/clientCapabilities";
logLevel: "io.modelcontextprotocol/logLevel";
serverInfo: "io.modelcontextprotocol/serverInfo";
};
const MCP_ERROR_CODES: {
headerMismatch: -32020;
missingRequiredClientCapability: -32021;
unsupportedProtocolVersion: -32022;
};
createMcpHandler(options: McpHandlerOptions): McpHandler;
mcpRoutes(path: PathString, handler: McpHandler, options?: McpRoutesOptions):
RouteDefinition<PathString, "GET" | "POST" | "OPTIONS">[];
validateMcpInput(schema: McpJsonSchema, value: unknown): string[];
isModernProtocolVersion(version: string): boolean;
class McpToolError extends Error {}
type McpHandler = (request: Request) => Promise<Response>;
type McpJsonValue = null | boolean | number | string | McpJsonValue[] | { [key: string]: McpJsonValue };
type McpJsonObject = { [key: string]: McpJsonValue };
type McpJsonSchema = McpJsonObject;
type McpJsonRpcId = string | number | null;
type McpProtocolEra = "modern" | "legacy";
interface McpRoutesOptions {
public?: boolean; // default false; opt out of the production auth boot guard
hooks?: Hooks; // since 1.7.0; applied to the POST transport only
methodAuth?: McpMethodAuthOptions; // since 1.8.0; not with public, or with auth in hooks
}
const MCP_DISCOVERY_METHODS: readonly [
"server/discover", "tools/list", "resources/list",
"resources/templates/list", "prompts/list",
];
type McpDiscoveryMethod = (typeof MCP_DISCOVERY_METHODS)[number];
interface McpMethodAuthOptions {
call: Hooks; // auth hook for everything else (incl. no Mcp-Method header)
discovery: Hooks | "public"; // auth hook for discovery methods, or no credential
discoveryMethods?: readonly McpDiscoveryMethod[]; // default: all of MCP_DISCOVERY_METHODS
}
interface McpCacheHints {
ttlMs?: number; // default 0 (revalidate every call)
scope?: "public" | "private"; // default "private"
}
interface McpHandlerOptions {
serverInfo: McpServerInfo;
instructions?: string;
tools?: readonly McpTool[];
resources?: readonly McpResourceDefinition[];
resourceTemplates?: readonly McpResourceTemplateDefinition[];
prompts?: readonly McpPromptDefinition[];
extensions?: Record<string, McpJsonObject>; // capabilities.extensions
cache?: McpCacheHints; // ttlMs / cacheScope on cacheable results
allowedOrigins?: readonly string[]; // bare origins, plus optional "null"
protocolVersions?: readonly string[]; // default MCP_PROTOCOL_VERSIONS
preferredProtocolVersion?: string; // default MCP_PROTOCOL_VERSION
maxBodyBytes?: number; // default MCP_DEFAULT_MAX_BODY_BYTES
headers?: Record<string, string>;
exposeInternalErrors?: boolean; // default: on only when NODE_ENV is "development" or "test"
maxResourceUriLength?: number; // default 8192; longer resources/read URIs get -32602
}
// exposeInternalErrors fails closed: when omitted, mcpRoutes() on an App whose
// env is "production" always redacts. An explicit value wins.
interface McpImplementation { name: string; version: string; title?: string }
interface McpInputRequest {
method: "elicitation/create" | "sampling/createMessage" | "roots/list";
params?: McpJsonObject;
}
type McpInputRequests = { [id: string]: McpInputRequest };
type McpInputResponses = { [id: string]: McpJsonValue };
interface McpInputRequiredResult {
resultType: "input_required";
inputRequests?: McpInputRequests;
requestState?: string;
}
interface McpServerInfo {
name: string;
version: string;
title?: string;
description?: string;
websiteUrl?: string;
icons?: McpIcon[];
}
interface McpTool<TArgs extends Record<string, unknown> = Record<string, unknown>> {
name: string;
description: string;
title?: string;
inputSchema: McpJsonSchema;
outputSchema?: McpJsonSchema;
annotations?: McpToolAnnotations;
icons?: McpIcon[];
handler: McpToolHandler<TArgs>;
}
type McpToolHandler<TArgs extends Record<string, unknown> = Record<string, unknown>> =
(args: TArgs, ctx: McpRequestContext) =>
| string | McpToolResult | McpInputRequiredResult
| Promise<string | McpToolResult | McpInputRequiredResult>;
interface McpToolResult {
content?: McpContent[];
structuredContent?: McpJsonObject;
isError?: boolean;
}
interface McpRequestContext {
request: Request;
protocolVersion: string;
era: McpProtocolEra;
id: McpJsonRpcId;
method: string;
clientCapabilities: McpJsonObject; // {} on legacy requests
clientInfo?: McpImplementation; // advisory only, never a security input
logLevel?: string;
inputResponses?: McpInputResponses; // present on a multi round-trip retry
requestState?: string; // attacker-controlled; verify before trusting
}
// One endpoint, both protocol eras. Modern (2026-07-28+) requests carry version,
// identity, and capabilities in params._meta and must mirror them into the
// MCP-Protocol-Version / Mcp-Method / Mcp-Name headers; a missing or mismatched
// header is rejected with 400 and -32020. Legacy (2025-11-25 and earlier) clients
// keep the initialize handshake unchanged.
// tools/call arguments are validated against inputSchema before the handler runs.
// Schema violations return JSON-RPC -32602. Unsupported keywords such as
// pattern, format, $ref, anyOf, oneOf, and allOf are advertised but not enforced.
// In production secureDefaults apps, mcpRoutes() must be covered by an auth hook
// unless mounted with mcpRoutes(path, handler, { public: true }).@daloyjs/core/a2a
ts
const A2A_PROTOCOL_VERSION = "1.0";
const A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json";
const A2A_DEFAULT_MAX_BODY_BYTES = 262144;
const A2A_ERROR_CODES: {
parseError: -32700; invalidRequest: -32600; methodNotFound: -32601;
invalidParams: -32602; internalError: -32603;
taskNotFound: -32001; taskNotCancelable: -32002;
pushNotificationNotSupported: -32003; unsupportedOperation: -32004;
contentTypeNotSupported: -32005; invalidAgentResponse: -32006;
extendedAgentCardNotConfigured: -32007; extensionSupportRequired: -32008;
versionNotSupported: -32009;
};
createA2aHandler(options: A2aHandlerOptions): A2aHandler;
a2aRoutes(path: PathString, handler: A2aHandler, options?: A2aRoutesOptions):
RouteDefinition<PathString, "GET" | "POST" | "OPTIONS">[];
memoryTaskStore(options?: { maxTasks?: number; ttlMs?: number }): A2aTaskStore;
a2aText(text: string, mediaType?: string): A2aPart;
a2aData(data: unknown, mediaType?: string): A2aPart;
class A2aError extends Error { readonly code: number } // -32602 or -32001..-32099
// Calling other agents (since 1.5.0)
createA2aClient(options: A2aClientOptions): A2aClient;
interface A2aClientOptions {
url: string; // agent base URL or full card URL; https: unless loopback
fetch?: typeof fetch; // default fetchGuard() (SSRF-guarded)
headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
allowedOrigins?: readonly string[]; // extra origins the card's endpoint may use (default: card origin)
allowInsecureHttp?: boolean; // default false
timeoutMs?: number; // default 10_000
maxResponseBytes?: number; // default 1 MiB
cardMaxAgeMs?: number; // default 300_000
extensions?: readonly string[]; // A2A-Extensions
propagateTrace?: boolean | ((headers: Headers, request?: Request) => void); // default off
}
interface A2aClient {
getAgentCard(opts?): Promise<A2aAgentCard>;
sendMessage(message: string | A2aPart[] | { parts, taskId?, contextId?, ... }, opts?): Promise<{ message } | { task }>;
getTask(id, opts?): Promise<A2aTask>;
cancelTask(id, opts?): Promise<A2aTask>;
listTasks(query?, opts?): Promise<A2aListTasksResult>;
}
class A2aClientError extends Error { code: number; data?: unknown; status?: number } // code 0 = client-side refusal
interface A2aHandler {
readonly agentCard: Readonly<A2aAgentCard>;
handleCard(request: Request): Promise<Response>;
handleRpc(request: Request, host?: { state: Record<string, unknown> }): Promise<Response>;
}
interface A2aHandlerOptions {
card: A2aAgentCardInput; // name, description, version, url, skills, securitySchemes, ...
onMessage(ctx: A2aMessageContext): A2aReply | Promise<A2aReply>;
taskStore?: A2aTaskStore; // enables GetTask / CancelTask / ListTasks / multi-turn
taskOwner?(host: { request: Request; state: Record<string, unknown> }): string | undefined | null;
allowedOrigins?: readonly string[];
maxBodyBytes?: number; // default 256 KiB
maxHistory?: number; // default 50
cardMaxAgeSeconds?: number; // default 300
exposeInternalErrors?: boolean; // fails closed; forced off on a production App
headers?: Record<string, string>;
}
type A2aReply =
| string
| { reply: string | readonly A2aPart[]; metadata?: Record<string, unknown> }
| { status: "completed" | "failed" | "rejected" | "input-required" | "auth-required";
message?: string | readonly A2aPart[]; artifacts?: readonly A2aArtifactInput[];
metadata?: Record<string, unknown> };
interface A2aRoutesOptions {
public?: boolean; // opt out of the production auth boot guard
hooks?: Hooks; // applied to the JSON-RPC POST only
cardPath?: PathString | false; // default A2A_AGENT_CARD_PATH
}
// Capabilities are derived: streaming, pushNotifications, and extendedAgentCard
// are false and their methods return -32004 / -32003. A task store requires
// taskOwner; an unresolved owner is refused with 401. In production
// secureDefaults apps, a2aRoutes() must be covered by an auth hook unless
// mounted with { public: true }, and the card url must be https: (non-loopback).@daloyjs/core/docs
ts
scalarHtml(opts: ScalarHtmlOptions): string;
swaggerUiHtml(opts: SwaggerUiHtmlOptions): string;
redocHtml(opts: RedocHtmlOptions): string;
docsContentSecurityPolicy(opts?: DocsContentSecurityPolicyOptions): string;
htmlResponse(html: string, opts?: HtmlResponseOptions): Response;
interface DocsOptions { specUrl: string; title?: string; assets?: DocsAssetOptions; scriptNonce?: string }
interface ScalarHtmlOptions extends DocsOptions { configuration?: ScalarReferenceConfiguration }
interface SwaggerUiHtmlOptions extends DocsOptions { configuration?: SwaggerUiConfiguration }
interface RedocHtmlOptions extends DocsOptions { configuration?: RedocConfiguration }
interface DocsAssetOptions {
// version-pinned URL + matching SRI hash per asset (see /docs/docs-asset-integrity)
scalarScriptUrl?: string; scalarScriptIntegrity?: string;
swaggerUiCssUrl?: string; swaggerUiCssIntegrity?: string;
swaggerUiBundleUrl?: string; swaggerUiBundleIntegrity?: string;
redocScriptUrl?: string; redocScriptIntegrity?: string;
asyncapiScriptUrl?: string; asyncapiScriptIntegrity?: string;
asyncapiStyleUrl?: string; asyncapiStyleIntegrity?: string;
crossOrigin?: "anonymous" | "use-credentials"; // default: "anonymous"
}
// RedocConfiguration is an index-signature bag of JSON-serializable Redoc
// standalone options (disableSearch, hideDownloadButtons, sortPropsAlphabetically,
// theme, ...), forwarded verbatim to Redoc.init(specUrl, configuration, element).
interface DocsContentSecurityPolicyOptions {
assetOrigins?: readonly string[];
connectOrigins?: readonly string[];
scriptNonce?: string;
allowInlineStyles?: boolean;
allowBlobWorkers?: boolean; // append worker-src 'self' blob: (Redoc needs it)
}
interface HtmlResponseOptions extends DocsContentSecurityPolicyOptions {
contentSecurityPolicy?: string;
}@daloyjs/core/streaming
ts
interface SSEMessage { data?: unknown; event?: string; id?: string; retry?: number; comment?: string }
sseStream (source, opts?: SSEStreamOptions): ReadableStream<Uint8Array>;
sseResponse(source, opts?: SSEResponseOptions): Response;
ndjsonStream (source, opts?: StreamOptions): ReadableStream<Uint8Array>;
ndjsonResponse(source, opts?: NDJSONResponseOptions): Response;
interface StreamOptions { signal?: AbortSignal }
interface SSEStreamOptions extends StreamOptions { keepAliveMs?: number }
interface SSEResponseOptions extends SSEStreamOptions { status?: number; headers?: HeadersInit }
interface NDJSONResponseOptions extends StreamOptions { status?: number; headers?: HeadersInit }
// data and comment split on CRLF, lone CR, and lone LF, so a stray "\r" cannot
// forge a field. CR/LF runs in event (and CR/LF/NUL in id) become a space.
// The Node adapter cancels a streamed body when the client disconnects.@daloyjs/core/multipart
ts
fileField(opts?: FileFieldOptions): FileFieldSchema<UploadedFile>;
multipartObject<S>(shape: S, opts?: MultipartObjectOptions): StandardSchemaV1;
isFileFieldSchema(value: unknown): boolean;
isMultipartObjectSchema(value: unknown): boolean;
type UploadedFile = Blob & { readonly name?: string };
interface FileFieldOptions {
maxBytes?: number;
accept?: string | readonly string[]; // MIME or extension allowlist
filename?: { maxLength?: number; pattern?: RegExp };
magicBytes?: FileMagicBytesOption; // refuses content-type spoofing
optional?: boolean;
format?: string;
}
interface MultipartObjectOptions { strict?: boolean } // refuses unknown fields by default@daloyjs/core/websocket
ts
defineWebSocket<P, S, TData>(handler: WebSocketHandler<P, S, TData>): WebSocketHandler<P, S, TData>;
interface WebSocketHandler<P, S = AppState, TData = unknown> {
beforeUpgrade?: (ctx: WebSocketContext<P, S>) => void | Response | Promise<void | Response>;
open?: (conn: WebSocketConnection<TData>, ctx) => void | Promise<void>;
message?: (conn, msg: MessageEvent, ctx) => void | Promise<void>;
close?: (conn, code: number, reason: string, ctx) => void | Promise<void>;
error?: (conn, err: Error, ctx) => void | Promise<void>;
// limits
maxPayloadLength?: number; // default: 1 MiB
backpressureLimit?: number; // default: 1 MiB
idleTimeoutSeconds?: number; // default: 120
allowedSubprotocols?: readonly string[];
origin?: string | readonly string[] | ((origin) => boolean);
}
wsRateLimit(opts: { windowMs; max; groupId?; keyGenerator?; store? }): WebSocketBeforeUpgrade;
// store / keyGenerator failures are rethrown and answered with a generic 500,
// never echoed to the client
normalizeWebSocketOptions(handler, ctx): NormalizedWebSocketOptions;
// Constants
WS_GUID; WS_READY_STATE; WS_OPCODE; WS_CLOSE_CODE; WS_MAX_CONTROL_PAYLOAD;
DEFAULT_WS_BACKPRESSURE_LIMIT; // 1 MiB
DEFAULT_WS_MAX_PAYLOAD_LENGTH; // 1 MiB
DEFAULT_WS_MAX_MESSAGE_FRAGMENTS; // 4096 data frames per message; more closes with 1002 (since 1.3.7)
DEFAULT_WS_IDLE_TIMEOUT_SECONDS; // 120
// Frame primitives (for custom adapters)
parseSubprotocols(header: string | null | undefined): string[];
validateSelectedSubprotocol(selected, allowed): boolean;
checkWebSocketOrigin(origin, allowed): boolean;
parseFrame(buf: Uint8Array, opts?): ParsedFrame | typeof FRAME_INCOMPLETE;
encodeFrame(opts): Uint8Array;
encodeClosePayload(code: number, reason?: string): Uint8Array;
decodeClosePayload(payload: Uint8Array): { code: number; reason: string };
encodeSendPayload(data: string | ArrayBufferLike | ArrayBufferView): Uint8Array;
computeAcceptKey(secWebSocketKey: string): string;
class WebSocketRegistry {}
class WebSocketProtocolError extends Error {}
class WebSocketPayloadTooLargeError extends WebSocketProtocolError {}
class FrameSink { /* event emitter over an async byte stream */ }@daloyjs/core/tracing
ts
otelTracing(opts: OtelTracingOptions): Hooks; // BYO @opentelemetry/api tracer
interface OtelTracingOptions {
tracer: TracingTracer;
serviceName?: string;
includeRequestHeaders?: readonly string[];
includeResponseHeaders?: readonly string[];
recordExceptions?: boolean;
redactQuery?: false | ((query: string) => string | undefined);
// default: url.query redacted via sanitizeUrlQueryForLog(); false records it raw (unsafe) (since 1.3.7)
}
const TRACING_SPAN_KIND_SERVER: number;
const TRACING_SPAN_STATUS_UNSET: number;
const TRACING_SPAN_STATUS_OK: number;
const TRACING_SPAN_STATUS_ERROR: number;@daloyjs/core/rate-limit-redis
ts
redisRateLimitStore(opts: RedisRateLimitStoreOptions): RateLimitStore;
redisAutoBanStore(opts: RedisAutoBanStoreOptions): AutoBanStore; // atomic strike() for autoBan() (since 1.3.7)
ioredisAdapter (client: IoredisLike): RedisCommands;
nodeRedisAdapter(client: NodeRedisLike): RedisCommands;
interface RedisRateLimitStoreOptions {
client: RedisCommands;
prefix?: string; // default: "daloy:rl:"
scriptCacheKey?: string;
}
interface RedisAutoBanStoreOptions {
client: RedisCommands;
prefix?: string; // default: "daloy:ab:"
}
interface RedisCommands {
evalsha?: (...) => Promise<unknown>;
eval?: (...) => Promise<unknown>;
// ... narrow subset; adapters provided for ioredis + node-redis.
}@daloyjs/core/banner
ts
formatStartupBanner(opts: StartupBannerOptions): string;
printStartupBanner (opts: StartupBannerOptions): void;@daloyjs/core/cli
ts
// Internals used by bin/daloy.mjs. Most users will not import this directly,
// but the surface is public-typed so wrappers can compose it.
type DevRuntime = "node" | "bun" | "deno";
detectRuntime(): DevRuntime;
buildDevCommand(runtime: DevRuntime, entry: string): { command: string; args: string[] };
parseArgs(argv: readonly string[]): { command: string; opts: CliOptions };
buildAiDump(app: App, opts: CliOptions): Record<string, unknown>;
assertSafeEntryPath(entry: string, context: string): void;
normalizeEntryArg(entry: string): string;Next up: runtime adapters.