Skip to content

@invokta/mcp

@invokta/mcp maps every engine capability to one MCP tool and keeps the official MCP SDK behind the adapter boundary. Every tool call executes through engine.invoke.

The compatibility baseline is MCP 2025-11-25; the approved TypeScript SDK is pinned to 1.30.0 in the package manifest.

Terminal window
yarn add @invokta/mcp

The package is native ESM and requires Node.js 22.20.0 or later. SDK and transport types are not part of its public API.

The package root exports:

  • serveMcpStdio and ServeMcpStdioOptions;
  • toMcpToolName;
  • validateMcpToolCatalog and McpToolNameCollisionError;
  • serveMcpHttp and ServeMcpHttpOptions;
  • McpHttpAuthOptions;
  • McpHttpAuthenticationRequest and McpHttpHeaderView;
  • McpHttpProtectedResourceMetadata; and
  • McpHttpServerAddress and McpHttpServerHandle.
Capability MCP tool
Engine map key Portable name alias
title title
description description
Input JSON Schema inputSchema
Output JSON Schema outputSchema
readOnly readOnlyHint
destructive destructiveHint
idempotent idempotentHint
openWorld openWorldHint

Engine name and version become the MCP server identity. Omitted tool arguments default to {} before input validation.

Tool names always match ^[a-zA-Z0-9_-]{1,64}$. Each unsupported character in the capability ID becomes _. A name longer than 64 characters uses its first 51 portable characters, _, and the first 12 lowercase hexadecimal characters of the original ID’s SHA-256 digest. An empty ID becomes _; an already compatible ID remains unchanged. For example, support.classify-ticket is published as support_classify-ticket.

tools/call resolves the published alias back to the original capability ID before calling engine.invoke. If two capability IDs derive the same alias, the protocol server fails closed rather than publishing an ambiguous catalog. Hosts that need to display or submit the alias outside tools/list can derive the same value with toMcpToolName(capabilityId).

Build tooling can call validateMcpToolCatalog(engine) to construct this exact catalog without starting stdio or HTTP. A collision throws McpToolNameCollisionError, a TypeError with code MCP_TOOL_NAME_COLLISION, the derived toolName, and the two capabilityIds.

A successful call returns the validated result as structuredContent plus a JSON text fallback. Invocation failures return isError: true with one safe JSON text error and no structuredContent. Unsafe or unknown errors fall back to:

{"code":"EXECUTION_FAILED","message":"Capability execution failed."}

An unknown tool is a protocol InvalidParams error rather than an invocation CAPABILITY_NOT_FOUND. Valid request IDs such as numeric 0 and the empty string retain their wire identity, and cancellation reaches only its matching request.

interface ServeMcpStdioOptions {
readonly principal?: Principal | null;
readonly maxReadBufferBytes?: number;
}
function serveMcpStdio<Capabilities extends CapabilityMap>(
engine: Engine<Capabilities>,
options?: ServeMcpStdioOptions,
): Promise<void>;
await serveMcpStdio(engine, {
principal: { id: "local:mcp-host" },
maxReadBufferBytes: 10 * 1024 * 1024,
});

The principal defaults to null. The read buffer defaults to 10,485,760 bytes and must be a positive safe integer. A message that keeps the buffer at the limit is accepted; crossing it closes the transport, aborts active requests, removes stream listeners, and rejects the promise.

serveMcpStdio remains pending for the protocol connection lifetime. Clean stdin end or close shuts down the server and aborts active requests. A broken stdout pipe is treated as a normal client disconnect; another output failure is reported. Standard output is reserved for protocol messages, so diagnostics belong on standard error.

interface ServeMcpHttpOptions {
readonly host?: string;
readonly port?: number;
readonly allowedHosts?: ReadonlyArray<string>;
readonly allowedOrigins?: ReadonlyArray<string>;
readonly maxRequestBodyBytes?: number;
readonly auth: McpHttpAuthOptions;
}
interface McpHttpServerHandle {
address(): { readonly host: string; readonly port: number };
close(): Promise<void>;
}
const server = await serveMcpHttp(engine, {
host: "127.0.0.1",
port: 3000,
allowedHosts: ["engine.example"],
allowedOrigins: ["https://console.example"],
maxRequestBodyBytes: 1024 * 1024,
auth: {
mode: "required",
authenticate,
},
});

Defaults are 127.0.0.1, port 3000, and a 1,048,576-byte body limit. Use port 0 in tests to let the operating system select an available port, then read it with server.address(). The address is the listener address, not a public URL.

server.close() is idempotent, shares concurrent close calls, aborts active request signals, and closes their connections.

Each accepted POST creates a fresh MCP server and transport. The adapter has no session ID, resumption, event store, server-to-client SSE, cross-request cancellation, resources, prompts, sampling, elicitation, or tasks.

The sole protocol endpoint is the exact canonical POST mount path, /mcp by default. The optional path option mounts it under a prefix such as /e/orders/mcp so several engines can share one origin, each with its own resource identifier; a mount path uses unreserved ASCII segments, at most 256 bytes, and ends in /mcp. When Protected Resource Metadata is configured, its generated well-known GET route is the only non-protocol route, served at the RFC 9728 path-suffix location for the mounted path. Dot segments, percent-encoded aliases, queries, fragments, absolute-form targets, and batches never reach protocol dispatch.

Checks occur before capability execution:

  1. exactly one raw Host header and an allowed host;
  2. an optional but valid and allowed Origin;
  3. canonical route, supported method, and declared body size;
  4. exactly one unambiguous Authorization value for protected requests;
  5. authentication and principal snapshotting;
  6. exact JSON content type, required accepted response media, streamed body bound, strict UTF-8, and one JSON-RPC message; and
  7. MCP dispatch to engine.invoke.

A non-loopback bind requires a non-empty allowedHosts list. Forwarded-host headers are ignored. An absent Origin is allowed for non-browser clients; any present Origin must match a configured normalized HTTP(S) origin exactly, including scheme and any non-default port.

Accept must include both application/json and text/event-stream with positive quality. One raw Content-Type header must identify application/json. The body-size boundary is inclusive, and decoding is strict incremental UTF-8.

Status Boundary meaning
200 MCP response, including capability FORBIDDEN as a tool error
202 Accepted notification with no protocol result body
400 Invalid target/header ambiguity, JSON parsing, or top-level batch
401 Missing/invalid credentials or malformed returned Principal
403 Rejected Host or Origin
404 Unknown route
405 Unsupported method
406 Required response media types not accepted
413 Declared or streamed body crossed the configured limit
415 Missing, duplicated, or unsupported content type
500 Authentication infrastructure or internal boundary failure
type McpHttpAuthOptions =
| {
readonly mode: "required";
readonly authenticate: (
request: McpHttpAuthenticationRequest,
) => Principal | null | Promise<Principal | null>;
readonly resourceMetadata?:
McpHttpProtectedResourceMetadata;
}
| {
readonly mode:
"dangerously-disabled-for-development";
};

The authentication request contains normalized path, method, a read-only header view, and a cancellation signal. Returning a principal authenticates the request; returning null produces 401; throwing produces a sanitized 500. The adapter validates and deep-snapshots the principal before engine.invoke.

Domain authorization stays in the capability access rule. Read the HTTP authentication guide for verifier mapping, Protected Resource Metadata, proxy boundaries, and secret handling.

auth: {
mode: "dangerously-disabled-for-development",
}

This mode supplies principal = null. Keep it on an isolated loopback process; authenticated capabilities remain closed. It is not a production authentication strategy.

The adapter provides no TLS termination, proxy-trust policy, rate limit, concurrency limit, authentication retry, or verifier timeout. The host and deployment own those controls. Verifiers and capability dependencies must observe their request signals and apply their own finite outbound deadlines.

See execution channels for composition roots and scope and limits for the complete framework boundary.