ADR 0017 — Typed tool-path context via createToolkit
- Status: Accepted — extends ADR 0003
- Date: 2026-05-29
Context
ADR 0003 keeps the transport-level context loose (RuntimeContext) and
types it only at the handler boundary, where createImplement<AppContext>()
gives ctx.user its real type on every surface.
The handler is typed; the wiring into a tool transport was not. The context
a transport injects, and a ToolExtend.resolve result, were
Record<string, unknown>:
mountMcp({ context }); // context?: Record<string, unknown>
buildMcpServer({ context: (a) => … }); // (auth) => Record<string, unknown>
So TypeScript could not catch a missing or wrong-typed user in the injected
context. There is no runtime hole — wire it right and it works — but the
fullstack-type-safety pitch leaks on the tool side. Adding the CLI transport
(ADR 0016) would have made it a fourth untyped wiring site.
Decision
Add a typed factory that mirrors createImplement, leave the loose path
intact. createToolkit<AppContext>() fixes the context shape once and returns
context-pinned mountMcp / mountAgent / buildMcpServer / createMcpHandler
/ createStdioMcpServer / createCli — each with its injected context (and
ToolExtend.resolve) checked against AppContext:
const tools = createToolkit<{ user: User }>();
tools.createCli({ context: (auth) => ({ user: auth }) }); // type-checked
It is pure typing sugar: each method forwards verbatim to the underlying
function. The underlying functions stay Record<string, unknown>-typed for
callers who do not opt in — ADR 0003’s loose-by-default rule is preserved. The
only structural change is that ToolExtend becomes generic
(ToolExtend<TContext>, default Record<string, unknown>), so the typed
boundary can flow a context shape into resolve.
Why a factory, not generic configs
The alternative — making every config (McpMountConfig, McpServerBuildConfig,
AgentMountConfig, McpHandlerConfig, StdioMcpServerConfig) generic over
<TContext> — is maximally safe but threads a generic through the entire mount
stack. The factory achieves the same safety at the boundary the app actually
declares its context at, without that invasive threading, and reads exactly like
the createImplement<TCtx>() the framework already uses for handlers.
Consequences
- The tool surface is born as type-safe as the handler — the CLI transport ships typed instead of adding a fourth untyped wiring site.
createImplement<AppContext>()(handler) andcreateToolkit<AppContext>()(tool transports) are the two places an app fixes its context type — one pattern, two boundaries.- No migration: existing
mountMcp({ context })calls keep compiling against the loose form.