Releases: callibrity/mocapi
0.17.0
Changed
-
Tool input/output schemas now mark fields as required by default. When a tool is declared with
@McpToolParamson a record (or any Jackson-deserializable type), every component is now listed in the generated input schema'srequiredarray unless it is explicitly opted out. The same applies to record-shaped output schemas. The previous behavior emitted norequiredarray at all for these shapes, which was misleading: tool authors writingrecord HelloRequest(String firstName, String lastName)clearly meant both fields to be required, but the schema told clients they were optional. The new policy mirrors the per-parameter path that has always been required-by-default.Opt-outs (any one of these makes a field optional):
- declare the type as
Optional<T>, - annotate with
@jakarta.annotation.Nullable, or - annotate with
@io.swagger.v3.oas.annotations.media.Schema(requiredMode = NOT_REQUIRED).
Migration. If you have a record component that was implicitly optional under the old behavior, switch its type to
Optional<T>(the most idiomatic option), or add@Nullable/@Schema(requiredMode = NOT_REQUIRED). Clients that were sending requests missing such fields will otherwise be rejected at schema validation time once you upgrade. - declare the type as
0.16.0
Changed
DELETE /mcpnow returns200 OKinstead of204 No Content. The MCP Streamable HTTP spec only constrains the405"I refuse" case for client-initiated session termination; any 2xx is conformant. The official TypeScript SDK returns200, and at least one widely used client (the MCP Inspector's proxy) mishandles204because its underlyingfetchimplementation rejects the null-body status when the proxy attaches a non-null body. Switching to200matches the reference implementation and unblocks those clients without changing semantics — the body is still empty andterminate()still runs.
0.15.0
Added
- Startup banner. A new
MocapiStartupBannerlogs an ASCII-art
mocapiblock plus a one-shot summary of the running configuration
when the application becomes ready — registered tool/prompt/resource
counts, the active transport (with endpoint path), the session-store
backend, OAuth2 state, and any observability modules. Discovery is
bean-presence driven via runtime class lookup so the banner has no
hard classpath dependency on optional starters; each line gracefully
collapses to a sensible default when the backing module is absent.
Gated bymocapi.banner.enabled(defaulttrue); set tofalseto
suppress.
Changed
- Strict tool return-type contract (breaking).
@McpToolmethod
return types are now checked at handler-build time and classified
once into one of four permitted shapes; the old runtime type switch
inMcpToolsService.toCallToolResultis gone, and every handler
carries a pre-selectedResultMapper. Permitted shapes (applied to
the effective type after recursively peeling any number of
CompletionStagelayers): (1)void/Void→ emptyCallToolResult;
(2)CallToolResult→ passthrough, author owns the result; (3)
CharSequence(includingString) → single text content block, no
structured content; (4) a record/POJO whose Jackson-derived schema is
type: "object"with declared properties → advertised as
outputSchema, serialized intostructuredContent. Everything else
(List<T>,Map<K,V>, primitives, arrays,Optional<T>,
JsonNode/ObjectNode, raw/wildcardCompletionStage, empty
records, …) fails to register with a message naming the offending
type and suggesting a fix. Previously,
non-object returns silently advertised a misleadingoutputSchema
and/or shippedstructuredContent = nullin violation of the MCP
spec; the new contract makes those mistakes impossible.
Added
CompletionStage<T>/CompletableFuture<T>tool return types.
Mocapi awaits the future on an innermost
CompletionStageAwaitingInterceptorand applies the same mapping
rules to the awaited value. The interceptor loops, so any depth of
nesting (CompletionStage<CompletionStage<X>>etc.) is unwrapped
inside-out by a single instance.CompletionExceptionis unwrapped
so domain exceptions surface with their original type. Raw and
wildcardCompletionStagedeclarations are rejected at registration
(no concrete inner type to derive a schema from).CharSequencereturn type shortcut. Tools that want to return
plain text can declareString(or anyCharSequence) and mocapi
produces a text-onlyCallToolResult— no need to hand-build a
CallToolResultor invent a single-field record.- Public
ResultMapperAPI.ResultMapperis a sealed interface
with four impls:VoidResultMapper,PassthroughResultMapper,
TextContentResultMapper,StructuredResultMapper. Users building
CallToolHandlers directly (outside the@McpToolauto-detection
path) can select a mapper explicitly. McpToolExceptionfor structured tool errors. A new public
exception type inmocapi-apilets tool authors signal execution
failures that carry a structured payload and/or extra content blocks,
not just a text message. Throwing it (or a subclass) produces a
CallToolResultwithisError: true, the exception message as the
first text block,getAdditionalContent()blocks appended after it,
andgetStructuredContent()serialized intostructuredContent(the
payload must serialize to a JSON object per MCP spec). Handled
identically when thrown synchronously or from a failed
CompletionStage. Authors are encouraged to subclass for reusable,
domain-specific error shapes; mocapi catches the parent type. No
"error code" field — per the MCP spec, tool-execution errors have no
protocol-level code slot; put any code insidestructuredContent.
0.14.0
Added
- Opt-in output schema validation for tool calls. A new
mocapi.tools.validate-outputproperty (defaultfalse) installs anOutputSchemaValidatingInterceptornext to the existing input validator on every tool handler. When on, each tool's return value is validated against its derived output schema and a mismatch fails loudly with JSON-RPC-32603, surfacing schema/reality drift at test time instead of as an opaque gateway 500 in production. The interceptor switches on the result shape:CallToolResultvalidatesstructuredContent(skipped when null),JsonNodevalidates directly,nullis skipped, anything else is serialized viavalueToTreeand validated. Flip it on in@SpringBootTestruns to catch drift before clients do.
Changed
mocapi-servertests aligned todraft_2020_12. Production has defaultedmocapi.tools.schema-versiontodraft_2020_12for a while, but threemocapi-servertests were still instantiatingDefaultMethodSchemaGeneratorwithSchemaVersion.DRAFT_7. Flipping them to match the production default is a correctness fix, not cosmetic — and it also cutmocapi-server's test-suite wall time from ~15.7s to ~5.7s.
Documentation
- Removed two obsolete example ITs (
PostgresqlStarterAutoConfigurationIT,RedisStarterAutoConfigurationIT) that spun up Testcontainers only to verify Substrate's own Postgres/Redis auto-configurations. They didn't exercise a single mocapi type and referred to mocapi-specific "starters" that no longer exist. Saves ~22s of Docker per CI run.
0.13.0
Highlights
/actuator/mcpnow shows handler internals. Each tool / prompt / resource / resource-template entry carries ahandlerblock with kind, declaring class, method name, and the outer-to-inner toString sequence of every interceptor wrapping the reflective call. Reading the list top-to-bottom reconstructs the stratum chain — useful for "why is Jakarta validation firing before authorization?"–class questions. Actuator counts and lists are now operator-view (every registered handler, no guard filtering); filtering still applies to the MCP protocol'stools/list.- Interceptor strata API. Customizers no longer negotiate
@Orderaround a genericinterceptor(...)bucket. Each*HandlerConfiggains five typed add methods —correlationInterceptor,observationInterceptor,auditInterceptor,validationInterceptor,invocationInterceptor— and the handler builder assembles CORRELATION → OBSERVATION → AUDIT → AUTHORIZATION (guards) → VALIDATION (JSON schema + semantic) → INVOCATION in a stable outer-to-inner order. Fixes the "Jakarta Bean Validation runs before the authorization gate" smell from earlier versions. - mocapi-oauth2 restructured. Filter chain split into separate metadata and MCP beans; RFC 9728 metadata document now populated by five independently-overridable facet customizers;
McpTokenStrategySPI abstracts JWT vs. opaque-token validation. Package split: token strategies in.oauth2.token, metadata customizers in.oauth2.metadata. McpToolContextcleanup. Logging concerns moved toMcpLogger.ctx.log(...)andctx.isEnabled(...)are gone; usectx.logger(name).info("...")andctx.logger(name).isDebugEnabled().McpLoggeritself compressed to two abstract methods with default per-level shortcuts.mocapi-bomre-exportsmethodical-bom. Consumers get the Methodical version mocapi was built against transitively, preventingNoSuchMethodErrorfrom stalemethodical-corepins.
Breaking changes
McpToolContext lost log(...) and isEnabled(...)
Migrate:
// Before
ctx.log(LoggingLevel.INFO, "weather", "Processing " + city);
if (ctx.isEnabled(LoggingLevel.DEBUG)) ctx.log(LoggingLevel.DEBUG, "weather", expensiveFormat());
// After
ctx.logger("weather").info("Processing {}", city);
var log = ctx.logger("weather");
if (log.isDebugEnabled()) log.debug(expensiveFormat());handlerName() and logger(String) are now abstract — custom McpToolContext test doubles must implement them. The runtime DefaultMcpToolContext wires both correctly.
*HandlerConfig.interceptor(...) removed
Customizer code that added a generic interceptor must pick a stratum:
// Before
config.interceptor(new MyTimingInterceptor());
// After — pick the stratum that matches intent
config.observationInterceptor(new MyTimingInterceptor());Mapping:
| Old call | New call |
|---|---|
| MDC / request-id stamping | correlationInterceptor |
| spans / metrics | observationInterceptor |
| audit / outcome recording | auditInterceptor |
| schema / bean validation | validationInterceptor |
| retry / timeout wrappers | invocationInterceptor |
Guards are still added via config.guard(Guard) — they're the AUTHORIZATION stratum.
Service accessor semantics
McpToolsService.allToolDescriptors() / McpPromptsService.allDescriptors() / McpResourcesService.allResourceDescriptors() / allResourceTemplateDescriptors() now return every registered descriptor, not the per-caller-visible subset. Per-caller guard filtering still applies at the MCP list endpoints (listTools, listPrompts, listResources, listResourceTemplates). The visibilityFilter() hook on PaginatedService is gone; subclasses pass a Predicate<T> to the new paginate(Predicate, params, resultCtor) overload.
mocapi-oauth2 reshape
- Single combined
SecurityFilterChainsplit intomcpMetadataFilterChain(permit-all, RFC 9728 path) andmcpFilterChain(authenticated, MCP endpoint). OAuth2ProtectedResourceMetadataCustomizer→McpMetadataCustomizer(moved tocom.callibrity.mocapi.oauth2.metadata).McpSecurityFilterChainCustomizer→McpFilterChainCustomizer; newMcpMetadataFilterChainCustomizerSPI for the metadata chain.McpSecurityFilterChainBuilderdeleted; use the static factoriesMcpFilterChains.createMcpMetadataFilterChain(...)/createMcpFilterChain(...).- Token strategy abstracted: register a
@Primary McpTokenStrategybean to swap JWT/opaque wholesale; the built-ins auto-select via@ConditionalOnBeanonJwtDecoder/OpaqueTokenIntrospector. McpMetadataCustomizers.of(...)helper removed — five baseline facet beans (ResourceMetadataCustomizer,AuthorizationServersMetadataCustomizer,ScopesSupportedMetadataCustomizer,ResourceNameMetadataCustomizer,ClaimsMetadataCustomizer) replace it.- Packages split: tokens in
.oauth2.token, metadata customizers in.oauth2.metadata.
Requirements
Methodical 0.8.0 → 0.9.2. mocapi-bom re-exports methodical-bom so importers get the right version transitively.
0.12.1
[0.12.1] - 2026-04-20
Fixed
mocapi-oteladded tomocapi-bom. The module was added to the reactor
in 0.12.0 but missed in the BOM's<dependencyManagement>block, so users
importing the BOM had to specify<version>explicitly onmocapi-otel.
Now BOM-managed like every other published module. Pure BOM fix — no code
changes from 0.12.0.
0.12.0
[0.12.0] - 2026-04-20
Added
-
Two-layer OpenTelemetry trace hierarchy with MCP / GenAI / JSON-RPC
semantic conventions. Traces now carry two nested spans per handler
invocation:- Outer
jsonrpc.server— emitted by ripcurl's newripcurl-o11y
module via a per-@JsonRpcMethodinterceptor. Carries OpenTelemetry
RPC / JSON-RPC semconv attrs:rpc.system.name=jsonrpc,
jsonrpc.protocol.version=2.0,jsonrpc.request.id(from ripcurl's new
JsonRpcDispatcher.CURRENT_REQUESTScopedValue),rpc.response.status_code
("OK"on success, numeric JSON-RPC error code on failure),error.type.
mocapi-o11y enriches each one via a newMcpObservationFilterthat adds
mcp.method.name,mcp.session.id,mcp.protocol.versionfrom the bound
McpSession/ request envelope. - Inner
mcp.handler.execution— emitted by mocapi-o11y's new
McpHandlerObservationInterceptorfor every tool / prompt / resource /
resource-template invocation. Carriesmcp.handler.kindand the
kind-specific GenAI / MCP attrs:gen_ai.operation.name=execute_tool+
gen_ai.tool.namefor tools;gen_ai.prompt.namefor prompts;
mcp.resource.urifor resources. Only fires for methods that route
through a mocapi handler — dispatch-only methods (tools/list,
initialize, notifications) emit only the outer span.
Attributes align with the OTel MCP semconv
(https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/) and JSON-RPC
semconv (https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/).
Known gaps: span kind is the defaultINTERNALon both spans rather than
SERVER(the ambient HTTP span already carriesSERVERvia Spring MVC);
tool_errorforCallToolResult.isError=trueis not yet emitted. - Outer
-
mocapi-otelmodule. Source-less dependency bundle pullingmocapi-o11y
plusspring-boot-starter-opentelemetry(Spring Boot 4's OTel SDK +
Micrometer Observation → OTel tracing bridge + autoconfig). Add this module
plus the exporter for your target backend —opentelemetry-exporter-otlp
for Jaeger / Tempo / Grafana,spring-cloud-azure-starter-monitorfor
Azure App Insights, Datadog, etc. — and traces flow end-to-end. No default
properties are set: choices likemanagement.otlp.metrics.export.enabled
are backend-specific and left to the consumer. Same source-less pattern as
mocapi-jakarta-validation. See docs/otel.md. -
HandlerKindenum (com.callibrity.mocapi.server.handler.HandlerKind).
Replaces the scattered string constants"tool"/"prompt"/"resource"
/"resource_template"used across mocapi-audit, mocapi-logging, and
mocapi-o11y as MDC values, metric labels, and span tag values. Lives in
mocapi-server(notmocapi-api) since it's an internal classification
consumed by cross-cutting feature modules, not something users writing
@McpTool/@McpPrompt/@McpResource/@McpResourceTemplatereference
in their code. Each enum value has atag()method returning the stable
string form.
Changed
- Feature modules are now self-sufficient. Adding a
mocapi-*feature
module to a Spring Boot app is sufficient to activate the feature — no more
"also addspring-boot-starter-X" footnotes. Specifically:mocapi-o11ynow transitively pullsspring-boot-micrometer-observation,
so anObservationRegistrybean is always present when mocapi-o11y is on
the classpath (previously the autoconfig silently no-op'd unless the user
separately added Actuator or a tracing starter).mocapi-actuatornow depends onspring-boot-starter-actuator(the
starter) instead of justspring-boot-actuator(the library), so the
/actuator/mcpendpoint actually serves over HTTP without additional
wiring.mocapi-oauth2drops its unusedspring-boot-starter-validation
dependency (@ValidatedonMocapiOAuth2Propertieshad no constraint
annotations — dead weight).
MocapiJakartaValidationAutoConfigurationno longer declares a shared
JakartaValidationInterceptor@Bean. The interceptor is stateless; each
per-handler customizer now takesValidatordirectly and constructs a
localJakartaValidationInterceptorinline. One less bean in the context,
nothing functionally changes.
Breaking changes
- mocapi-o11y / mocapi-audit / mocapi-logging interceptor constructors take
HandlerKindinstead ofString kind. Users constructing
McpHandlerObservationInterceptor,McpMdcInterceptor, or
AuditLoggingInterceptordirectly (rare — normally attached via the
per-kind customizer beans in the matching autoconfig) must pass a
HandlerKindenum value (HandlerKind.TOOL, etc.) instead of the raw
string. Emitted tag / MDC / audit-field string values are unchanged. McpObservationInterceptorremoved. Replaced by the split
JsonRpcObservationInterceptorin ripcurl-o11y (JSON-RPC layer) plus
McpHandlerObservationInterceptorandMcpObservationFilterin
mocapi-o11y (MCP layer). Direct consumers (rare) migrate to the new
types.- Old
HandlerKindsholder class removed frommocapi-api. Replaced by
theHandlerKindenum inmocapi-server. No known user-facing callers
sinceHandlerKindswas an internal helper.
Requirements
- ripcurl
2.11.0. Adds:JsonRpcDispatcher.CURRENT_REQUESTScopedValuebound by the dispatcher
during each call / notification — makes the full envelope (method, id,
version, params) readable from per-method interceptors and downstream
code.- New
ripcurl-o11ymodule withJsonRpcObservationInterceptor+
RipCurlObservationAutoConfiguration. Emitsjsonrpc.server
observations carrying OTel JSON-RPC semconv attrs. Activates when
Micrometer'sObservationRegistryis present on the classpath.
mocapi-o11y's autoconfig now composes on top of ripcurl-o11y's observation:
the outer JSON-RPC span comes from ripcurl, mocapi adds MCP-specific
enrichment and the inner handler-layer span.
Fixed
- GraalVM native-image AOT failure caused by static-final SLF4J
loggers. Spring Boot AOT pulls@AutoConfiguration-annotated
classes (and, transitively, classes their bean-factory methods
return) into GraalVM's build-time class-initialization pass. The
firstLoggerFactory.getLogger(...)call inside any such class's
<clinit>runs before Logback'sSLF4JServiceProviderhas been
registered with Substrate's service-loader system, so SLF4J
falls back toNOP_FallbackServiceProviderand caches that
decision forever. The NOP instance then gets baked into the image
heap, and GraalVM rejects it because its class is
initialize-at-run-time. Converted every static-final Logger in
main sources — both explicitprivate static final Loggerfields
and the ones Lombok generated via@Slf4j— to non-static
instance fields (or removed entirely, for the SSE writer chain
where the log lines were low-value trace noise on a hot path).
Constructor execution happens at runtime after Logback is fully
wired, so the NOP race no longer fires. Affects:
MocapiServer{Tools,Prompts,Resources}AutoConfiguration,
MocapiAudit/Logging/O11y/JakartaValidation/SpringSecurityGuards
AutoConfiguration,DefaultMcpServer,
McpSessionService,McpPromptsService,McpResourcesService,
McpToolsService,McpLifecycleService,StdioServer,
StreamableHttpTransport,AuditLoggingInterceptor, and the
three streamable-HTTP message writers (Direct,Sse,Closed
— the writers lose their loggers entirely).
0.11.0
[0.11.0] - 2026-04-20
Breaking changes
-
Bean-level
MethodInterceptorautowiring removed from handler
autoconfigs. The three handler autoconfigs
(MocapiServerToolsAutoConfiguration,
MocapiServerPromptsAutoConfiguration,
MocapiServerResourcesAutoConfiguration) no longer pick up bare
MethodInterceptor<? super X>beans from the context and apply
them to every handler of that kind. Interceptors now attach
exclusively via the*HandlerCustomizerSPI. Migration: wrap
each formerMethodInterceptorbean in a customizer, e.g.
@Bean CallToolHandlerCustomizer c() { return cfg -> cfg.interceptor(new MyInterceptor()); }.
The same applies forGetPromptHandlerCustomizer,
ReadResourceHandlerCustomizer, and
ReadResourceTemplateHandlerCustomizer. -
Artifact layout consolidated around
mocapi-autoconfigure.
All feature-level-spring-boot-startermodules have been
collapsed into a singlemocapi-autoconfiguremodule containing
every autoconfig (server, transports, OAuth2, logging,
observability, Jakarta Validation, and the/actuator/mcp
endpoint). Each autoconfig is@ConditionalOnClass-gated on a
class from its feature code module, so users light up a feature
by adding the feature module to their classpath — no
mocapi-specific starter name to remember. Old-to-new artifact
mapping:Old artifact New artifact mocapi-oauth2-spring-boot-startermocapi-oauth2mocapi-jakarta-validation-spring-boot-startermocapi-jakarta-validationmocapi-logging-spring-boot-startermocapi-loggingmocapi-o11y-spring-boot-startermocapi-o11ymocapi-actuator-spring-boot-startermocapi-actuator(pair withspring-boot-starter-actuatorto activateMocapiActuatorAutoConfigurationinmocapi-autoconfigure)The two transport starters
(mocapi-streamable-http-spring-boot-starter,
mocapi-stdio-spring-boot-starter) remain user-facing and now
pull inmocapi-autoconfigure+ their transport module.
mocapi-jakarta-validationis a new code module bundling the
same runtime deps the old starter packaged (no source of its own).
This is pre-1.0 — no compat shims. Migration is a one-line
artifactId rename in each consumer pom.
Changed
mocapi-actuatorextracted frommocapi-autoconfigure. The
actuator endpoint code (McpActuatorEndpoint,McpActuatorSnapshot,
McpActuatorSnapshots) now lives in its ownmocapi-actuatormodule
— matching the pattern used by every other feature (mocapi-oauth2,
mocapi-logging,mocapi-o11y,mocapi-jakarta-validation):
feature code in a dedicated module, feature autoconfig in
mocapi-autoconfigure. Activation is unchanged in spirit but now
requires bothmocapi-actuatorandspring-boot-starter-actuator
on the classpath. Side benefit: consumers who want only the
snapshot-building machinery for a custom admin endpoint can depend
onmocapi-actuatorwithout pulling in the autoconfig.
Added
-
mocapi-auditmodule — structured audit logging for every
handler invocation. Drop inmocapi-auditand every MCP tool /
prompt / resource / resource-template call emits one INFO event
on themocapi.auditSLF4J logger carrying caller identity,
session id, handler kind/name, outcome
(success/forbidden/invalid_params/error), duration, and
(opt-in viamocapi.audit.hash-arguments=true) a SHA-256 hash
of the arguments. Caller identity is extracted by a pluggable
AuditCallerIdentityProvider; the default reads Spring Security's
SecurityContextHolderwhen present and returnsanonymous
otherwise. No bundled encoder — users route themocapi.audit
logger to their SIEM / file sink with standard Logback config.
Audit attaches at@Order(200);mocapi-logging's MDC
customizers now pin to@Order(100)andmocapi-o11y's
observation customizers to@Order(300)so the in-flight
interceptor chain runs MDC → audit → o11y → user → guard →
validation → method. See Audit. -
Custom
ParameterResolvers via the customizer SPI. Each
*HandlerConfig(tool, prompt, resource, resource-template)
gains aresolver(ParameterResolver<? super X>)mutator that
slots alongside the existinginterceptor(...)andguard(...)
entry points. Customizer beans (e.g.CallToolHandlerCustomizer)
can now register bespoke resolvers — for patterns like
@CurrentTenant String tenantpulled from the session — without
forking the autoconfig. User resolvers are placed ahead of the
structural catch-all (Jackson for tools,StringMapArgResolver
for prompts and resource templates), so a specificsupports()
check always wins over the generic fallback. Builder signatures
changed:CallToolHandlers.build(...)takes anObjectMapper
(the structural resolvers are constructed internally);
GetPromptHandlers.build(...)and
ReadResourceTemplateHandlers.build(...)take a
ConversionService; none of the builders accept a resolver list
any longer. See
docs/tools-guide.md ("Custom Parameter
Resolvers") and docs/prompts-guide.md. -
New module
mocapi-spring-security-guards. First realGuard
implementation mocapi ships. Two method-level annotations —
@RequiresScope(String[])(all scopes required, AND semantics) and
@RequiresRole(String[])(any role grants access, OR semantics) —
placed on user@McpTool/@McpPrompt/@McpResource/
@McpResourceTemplatemethods cause the module's autoconfig
(MocapiSpringSecurityGuardsAutoConfiguration, in
mocapi-autoconfigure) to attach aScopeGuard/RoleGuardto
that handler via the customizer SPI. Both guards read
SecurityContextHolder.getContext().getAuthentication()at check
time; denial hides the handler from list operations and returns
JSON-RPC-32003with a descriptive reason
(unauthenticated/missing scope(s): .../insufficient role).
Role values may be bare (ADMIN) or prefixed (ROLE_ADMIN). The
autoconfig is doubly@ConditionalOnClass-gated: on the feature
module'sScopeGuardclass and on Spring Security's
Authenticationclass, so it stays dormant when either is absent.
See docs/guards.md and
docs/authorization.md ("Per-handler
authorization" section). -
Guard SPI (
com.callibrity.mocapi.server.guards). Plugins attach
per-handlerGuards via the existing*HandlerCustomizerbeans; a
guard'scheck()returnsAlloworDeny(reason). Evaluation is AND
with short-circuit on firstDeny. A denied guard both hides the
handler from list operations (tools/list,prompts/list,
resources/list,resources/templates/list) and rejects invocation
with JSON-RPC code-32003(JsonRpcErrorCodes.FORBIDDEN) and message
"Forbidden: <reason>". The SPI carries no framework coupling — guard
implementations reach into their own auth model (Spring Security,
McpSession.CURRENT, a plainScopedValue, …). See
docs/guards.md; the reference Spring Security
implementation ships separately asmocapi-spring-security-guards. -
New module
mocapi-actuator-spring-boot-starter: a Spring Boot
Actuator endpoint at/actuator/mcpthat returns a read-only
inventory of every MCP tool / prompt / resource / resource-template
registered on the node. The response is a JSON document with a
serverblock (name, version,protocolVersion), acounts
block, and per-kind descriptor lists. Tool entries carry
inputSchemaDigest/outputSchemaDigest— SHA-256 hex prefixed
sha256:— rather than full schema bodies (the full schemas are
still available via MCPtools/list). Activation follows standard
actuator rules: the autoconfig is guarded by
@ConditionalOnAvailableEndpoint, so users opt in via
management.endpoints.web.exposure.include=mcp.BuildProperties
is optional; if absent theserver.versionfield is simply
omitted. No session state, no metrics fold-in — session
introspection is out of scope for a per-node snapshot, and metrics
already have/actuator/metrics+/actuator/prometheus. -
New module pair
mocapi-o11y+mocapi-o11y-spring-boot-starter:
single-interceptor observability for MCP handler invocations via
the Micrometer Observation API. OneMcpObservationInterceptor
wraps every@McpTool/@McpPrompt/@McpResource/
@McpResourceTemplatecall in anObservation, so the same code
produces both metrics (when aMeterObservationHandleris
registered) and distributed-tracing spans (when a
TracingObservationHandleris registered) — whichever observation
handlers the user has on their classpath participate automatically.Observation names:
mcp.tool,mcp.prompt,mcp.resource,
mcp.resource_template. Low-cardinality tags:
mcp.handler.kind(tool/prompt/resource/
resource_template) andmcp.handler.name(tool / prompt name, or
resource URI / URI template). The standardoutcometag is added
automatically byDefaultMeterObservationHandleron exception.The starter autoconfig activates only when an
ObservationRegistry
bean is present. Spring Boot 3+ auto-creates one whenever
spring-boot-starter-actuatoror any Micrometer Observation
autoconfiguration is on the classpath, so adding this starter
alongside e.g.micrometer-registry-prometheusor
micrometer-tracing-bridge-oteljust works. Stdio-only apps with
no metrics / tracing stack on the classpath get no registry and no
customizers wire — pure no-op.Wiring uses the per-handler customizer SPI (spec 180): four
*HandlerCustomizerbeans, one per handler kind, each closing over
the descriptor's name / uri / uriTemplate at startup so the hot
path does zero reflection. -
Per-handler customizer SPI in `mocapi-...
0.10.0
Added
- Auto-derivation of
mocapi.oauth2.resourcefrom the Spring Boot
spring.security.oauth2.resourceserver.jwt.audiencesproperty.
Whenaudienceshas exactly one entry andmocapi.oauth2.resource
is unset, mocapi publishes that single audience as the RFC 9728
protected-resource metadataresourcefield. Common case for
Auth0 / Okta / Keycloak / Entra setups with one logical API.
Removes the duplicate-string-in-two-properties ceremony that the
0.9.0 configuration required.
Changed
- Startup-time invariant:
mocapi.oauth2.resource(whether
explicitly set or auto-derived) must be a member of the configured
spring.security.oauth2.resourceserver.jwt.audienceslist. A
mismatch fails at application start with a descriptive error
naming both the rejected resource and the accepted audiences.
Rationale: clients that follow the protected-resource metadata
document request tokens bound to the advertisedresource
identifier; if that identifier isn't in the server's accepted
audiences, every token the client obtains would be rejected
during validation. Catching that at startup is cheaper than a
silently-broken deployment where every MCP request returns 401.
Notes
@NotBlankis no longer enforced onmocapi.oauth2.resource—
the property becomes optional with the auto-derivation handling
the common case. Applications that were already setting the
property with a matching audience keep working unchanged.- Documentation (
docs/authorization.md) updated with the
minimum-configuration example (two Spring Boot properties, no
mocapi.oauth2.*needed), the recommendedjwk-set-uripattern
to skip Spring's OIDC discovery HTTP call at startup, and an
explicit note about the resource-must-be-in-audiences invariant.
0.9.0
Added
- New
mocapi-jakarta-validation-spring-boot-starterpom-only starter
that turns on Jakarta Bean Validation across mocapi's reflective-
dispatch surface. Bundlesspring-boot-starter-validation,
methodical-jakarta-validation, andripcurl-jakarta-validation.
Once on the classpath,@NotBlank/@Size/@Pattern/etc. on user
@ToolMethod/@PromptMethod/@ResourceTemplateMethod
parameters surface as MCP-spec-idiomatic errors per handler type:
tools/callviolations produceCallToolResult.isError=true(the
spec's "Input validation errors" path for LLM self-correction),
whileprompts/getandresources/readviolations produce JSON-RPC
-32602 Invalid paramswith per-violation{field, message}detail
in the response'sdatafield. Mocapi's internal protocol handlers
deliberately keep their hand-rolled checks so mocapi's own contract
is always enforced regardless of consumer classpath — this starter
is user-code-only. Seedocs/validation.mdfor setup and
examples/jakarta-validationfor a runnable app. - New
mocapi-oauth2module andmocapi-oauth2-spring-boot-starter
that wire OAuth2 resource-server protection for the Streamable HTTP
transport per MCP 2025-11-25 authorization. Adds bearer-token
validation on the MCP endpoint, a401 WWW-Authenticatechallenge
withresource_metadata="..."(RFC 8707), and an RFC 9728
protected-resource metadata document served at
/.well-known/oauth-protected-resource. Token validation (decoder,
issuer, audience) uses the standard Spring Boot
spring.security.oauth2.resourceserver.jwt.*properties; mocapi
fills in the metadata document frommocapi.oauth2.*with
fall-back to the configuredissuer-uri. Two extension hooks —
OAuth2ProtectedResourceMetadataCustomizerand
MocapiOAuth2SecurityFilterChainCustomizer— let apps append
claims or layer authorization rules without redeclaring the chain.
End-to-end integration test bundles Spring Authorization Server
in-process to verify the fullclient_credentials→ bearer →/mcp
flow. Seedocs/authorization.mdfor setup. - Opaque (non-JWT) access token support in
mocapi-oauth2. When
spring.security.oauth2.resourceserver.opaquetoken.*is configured
instead ofjwt.*, the chain switches to RFC 7662 introspection.
Mocapi wraps Spring'sOpaqueTokenIntrospectorwith an
audience-enforcing decorator so the MCP-mandatedaudcheck still
runs (Spring's opaque path does not include an audience validator
by default). JWT and opaque modes are mutually exclusive; JWT wins
if both happen to be configured.