ADR 0005: MCP server with the official C# SDK, stateless streamable HTTP and shared authorization
Status: Accepted (release 0.1.0)
Context
Section titled “Context”Bookend offers an MCP surface that mirrors the REST API (loans, findings, boarding preview, package submission, evidence, queue statistics) for integrations and assistants, authenticated with the same tokens and audited call by call. The options were the official ModelContextProtocol C# SDK, a hand-rolled JSON-RPC endpoint, or a separate MCP sidecar process.
Decision
Section titled “Decision”- Use the official SDK (
ModelContextProtocol.AspNetCore2.2.0, pinned) hosted inside the API process:AddMcpServer().WithHttpTransport(Stateless = true).WithTools<BookendMcpTools>()andapp.MapMcp("/mcp").RequireAuthorization(Policies.ReadOnly). - Stateless streamable HTTP: no server-side session table, nothing to replicate, and a bank’s reverse proxy needs no sticky routing. Each request carries its own bearer token, so the ASP.NET authentication and authorization pipeline applies unchanged.
- Tools are thin: they resolve the same application services the REST endpoints use and never touch persistence directly. A per-call wrapper (
McpToolAudit) checks the tool’s policy withIAuthorizationService, serializes the result, convertsBookendExceptions into tool errors, and appends anauth_eventsrow (mcp_call) with the arguments’ SHA-256 and the result size. - No separate sidecar: a second process would need its own token validation, dependency graph and audit path, for no isolation benefit inside the bank’s network.
Consequences
Section titled “Consequences”- One container, one authorization model, one audit trail. The contract tests drive
/mcpthrough the official client SDK with an API-key token, so wire compatibility is verified, not assumed. - Stateless mode means no server-initiated notifications or resumable streams; none of the tools need them.
- Tool arguments are not stored (they may contain PDF bytes); only their hash is, which is enough to correlate a call with what the caller logged.