ADR 0236 — One runtime-tool declaration
Invariants: I1, I3, I7, I8, I14. Builds on ADR 0227, 0231 and 0238.
Context
A runtime tool (a pathless operation with identity, schemas and a handler) was declared by three families of types, one per import path:
RuntimeToolExecution*onstitchkit/cli;RuntimeMcpToolDefinition*onstitchkit/tools/mcp;RuntimeToolDefinition*onstitchkit/tools, carrying MCP and Agent presenters.
The split protected declaration-peer isolation (I7): the CLI binary and the MCP-only leaf must not
import ai or the other adapter’s SDK. The price was paid at the call site. Registration erased the
handler to handler(context: never), so runtimeTools: [{ input, handler: ({ input }) => … }]
failed with TS2339 on never, and a consumer had to pick a satisfies type by entrypoint before
registering. ToolSurfaceDefinition was an empty extension of ToolSurfaceProjection exported
beside it. The generic runner (tools/execute-result.ts), bundled by cli, tools and mcp,
also imported the connection error projection, an optional sub-export.
Decision
- One declaration, in a module with no SDK declaration.
RuntimeToolDefinitionWithOutputandRuntimeToolDefinitionWithoutOutput(withRuntimeToolDefinitionBase, the handler context and identity types) live intools/runtime-tool-declaration.tsand are exported under the same names fromstitchkit/cli,stitchkit/toolsandstitchkit/tools/mcp. A handler is typed from the schemas; a callback that needs a field the schema omits is refused at construction. - Presenters are the adapter’s extension. The fourth type argument of
RuntimeToolDefinitionWithOutputnames thepresentshape; the neutral default admits none.stitchkit/tools/mcpsuppliesRuntimeMcpToolPresenters,stitchkit/toolssuppliesRuntimeToolPresenters(MCP and Agent) and itsdefineRuntimeTooland tool factory fix that argument, so presenters keep their exact output type without a peer in the neutral module. Presenters are declared as methods: a presenter written for one tool’s output registers beside the others, and the canonical runner validates the output before calling it. - One registration type.
RuntimeToolDefinitionaccepts any definition and declares its handler as a method. A method parameter is compared bivariantly, so a definition built against its own schema registers unchanged, and an inline handler is contextually typed by the parsed object instead of erased. The mount that readspresent(Agent, MCP) narrows it with a type predicate: each presenter was typed when its tool was declared. - One surface container.
ToolSurfaceDefinitionis deleted;ToolSurfaceProjectionis the single{ services?, runtimeTools? }container, and the introspection helpers takeToolSurfaceProjection<RuntimeToolDefinition>. - The generic runner knows no connection class. An error class chooses its own projection to a
safe
AppErrorthrough theTOOL_ERROR_PROJECTIONprotocol (tools/internal); the runner calls it and keeps the thrown value as the raw cause. The connection errors implement it, andconnections/error-projection.tsis deleted.
Removed names: RuntimeToolExecution, RuntimeToolExecutionWithOutput, RuntimeMcpToolDefinition,
RuntimeMcpToolDefinitionWithOutput, ToolSurfaceDefinition. The new entrypoint names are
RuntimeToolDefinition, RuntimeToolDefinitionWithOutput and RuntimeToolDefinitionWithoutOutput
on cli and tools/mcp, and RuntimeToolDefinitionWithOutput gained its fourth type argument.
Reason and verification
One concept has one name and one way to declare it (I1, I8); the call site reads the same
whichever adapter registers the tool, and an inline handler type-checks (I3). Isolation (I7) is kept
by what the neutral module imports, not by splitting the declaration. stitchkit/cli and
stitchkit/tools are stable, so the break is a minor that cites this ADR with a migration in
docs/guide/upgrading.md (ADR 0198, 0238).
tests/runtime-tool-declaration.type-test.ts checks, through tsc, that an inline handler in each
registration list is typed, that construction stays strict, that a neutral definition carries no
presenter, and that a registered handler is not callable without the runner’s context. The packed
consumer lanes (cli-types, mcp-leaf-types, runtime-tool-types) keep the same assertions against
the installed artifact. tests/connections-error-projection.test.ts pins the projection and the
raw cause, and rg "connections/" packages/core/src/tools/execute-result.ts is empty.