Skip to content

ADR-0010 — Annotation-driven handler discovery on Spring beans

  • Status: Accepted
  • Date: 2025-07-09

Context

An MCP server exposes four kinds of handlers — tools, prompts, resources, and resource templates. Mocapi needs a way for users to register handlers without reaching into the framework's internals: no manual registry, no factory boilerplate, no DSL the IDE can't resolve. The mechanism also has to play nicely with whatever bean wiring the host application already uses (Spring @Component, @Bean factories, @Service, hand-built configuration), and support multiple handlers per bean class so a single domain object can host several related operations.

Earlier iterations of mocapi required class-level marker annotations (@ToolService, @PromptService, @ResourceService) to narrow the bean-method scan. That was always a startup-time micro-optimization — and the measured cost of scanning every bean for handler annotations is sub-100ms even on Spring apps with thousands of beans. The marker added ceremony per handler class with no payoff: a developer adding a new tool had to remember both the method-level annotation and the class-level marker, and the annotation pair drifted in name (@ToolMethod on the method, @ToolService on the class) for no reason that survived scrutiny.

Reflective dispatch on annotated methods is provided by the methodical library (Callibrity OSS). Methodical 0.6+ delivers a stateless MethodInvokerFactory, an interceptor chain, and parameter resolvers — the exact primitives mocapi needs to invoke a Method reference with JSON-derived arguments and to layer cross-cutting behavior on top.

Decision

Handlers are discovered by scanning every Spring bean's methods for one of four mocapi method-level annotations. No class-level marker is required; the method annotation is the opt-in.

The four annotations carry a consistent @Mcp prefix:

Handler kind Annotation Package
Tool @McpTool com.callibrity.mocapi.api.tools
Prompt @McpPrompt com.callibrity.mocapi.api.prompts
Resource @McpResource com.callibrity.mocapi.api.resources
Resource template @McpResourceTemplate com.callibrity.mocapi.api.resources

A fifth annotation, @McpToolParams, opts an @McpTool parameter into record-based binding: the entire tools/call arguments object deserializes into a single typed record parameter, replacing per-name positional binding for tools that would otherwise carry many parameters.

Discovery runs once during the @PostConstruct of each provider autoconfiguration (tools provider, prompts provider, resources provider). A single pass walks every bean in the ApplicationContext, groups (bean, Method) pairs by which mocapi annotation they carry, and exposes the result as a cache the per-kind builders consume. No second pass, no duplicated reflection. Each registered handler is logged at INFO so startup output is the source of truth for what got wired (see architecture.md — Startup Logging).

Reflective dispatch goes through Methodical 0.6+. Each handler is built as a MethodInvoker with the appropriate ParameterResolver set (JSON-node resolver for tools, argument-map resolver for prompts and resource templates, scoped-value resolvers for McpSession / McpTransport / McpToolContext) and the appropriate interceptor chain (see ADR-0011).

Consequences

What this buys us. Adding a handler is one annotation on a method on a bean — nothing else. Existing Spring wiring patterns (@Component, @Bean, @Service, hand-built configuration) all work unchanged. Multiple handlers per bean class are natural. The discovery pass is centralized, which means the customizer SPI (ADR-0011) and the actuator inventory endpoint (ADR-0017) both consume the same single source of truth.

Costs. Every bean's methods are reflected at startup; for very large contexts that's a one-time cost in the tens of milliseconds, dwarfed by Spring's own bean instantiation. Methodical is a hard dependency of mocapi-server.

Non-goals. Mocapi does not support runtime registration of handlers discovered after @PostConstruct. Dynamic registration would require listChanged: true on the relevant capability and a notification path; both are explicitly out of scope (see ADR-0018).

Code anchors: mocapi-api/.../tools/McpTool.java (and sibling McpPrompt, McpResource, McpResourceTemplate, McpToolParams annotations); mocapi-autoconfigure/.../MocapiServerToolsAutoConfiguration.java.