Class SecuredExampleMcpServer
- All Implemented Interfaces:
jakarta.servlet.Servlet,jakarta.servlet.ServletConfig,Serializable,BasicUniversalConfig,DefaultConfig,DefaultHtmlConfig,ReadinessStateAware
ExampleMcpServer: identical notes-service surface (tools, prompt,
resources, resource template, subscriptions), but every POST / JSON-RPC call now requires a valid
bearer token.
This class changes nothing about what the server exposes — it inherits
ExampleMcpServer.createMcpConfig() unmodified, so the same publishNote/deleteNote
tools, summarize prompt, and note:///... resources are still there. The only override is
createMcpOptions(), which layers an McpResourceServerConfig on top of the parent's
capabilities. That single override is the entire security story:
setEnabled(true)— turns on the RS (resource-server) gate at all. Off by default, so an ordinaryExampleMcpServeris completely unaffected by this class existing.setResource(...)— this server's own canonical URL (RFC 9728resource/ RFC 8707 default audience). A bearer token'saudclaim must include this exact URL or it is rejected — the confused-deputy defense: a token minted for some OTHER resource cannot be replayed here even if it is otherwise perfectly valid and signed by a trusted issuer.setTokenValidator(...)— aJwtTokenValidatorthat checks the token's signature (againstOfflineAuthorizationServer.jwkSource(), offline — no JWKS HTTP fetch),iss(must equalOfflineAuthorizationServer.issuerUri()), andaud/exp/nbf(validated against the resource URL above and wall-clock time).addAuthorizationServer(...)— advertised in the RFC 9728 Protected Resource Metadata (PRM) document a client fetches from the well-known.well-known/oauth-protected-resourcepath this framework serves automatically once RS auth is enabled. This is how a compliant client discovers WHICH authorization server to go get a token from, without that URL being hardcoded into the client at all.addRequiredScope(...)— the coarse, endpoint-wide baseline scope (READ_SCOPE) every request must carry (on top of a merely valid, correctly-audienced token) before any JSON-RPC method dispatches. A token missing it gets a403with aWWW-Authenticatechallenge naming the missing scope, not a silent failure.addOperationScope(...)— SEP-2350 per-operation step-up (H3):publishNoteanddeleteNoteadditionally requireWRITE_SCOPEon top of the baseline. A token carrying onlyREAD_SCOPEcan discover the server and read resources, but a mutating tool call gets its own scoped403 insufficient_scopenamingWRITE_SCOPEspecifically — the baseline scope alone is never enough to invoke either write tool.
The net effect on the wire: an unauthenticated (or wrongly-audienced, or expired, or insufficiently-scoped)
POST / now gets 401/403 plus a WWW-Authenticate: Bearer ... challenge
instead of ever reaching ExampleMcpServer's tool/prompt/resource handlers; a request bearing a valid
bearer token dispatches exactly as it always did.
The one wrinkle this class works around:
The resource URL is this server's OWN address, which is a problem when
SecuredExampleServer boots on an OS-assigned ephemeral port (as the end-to-end test does): naively,
that address is not knowable until AFTER Jetty has bound its socket. It is tempting to assume
createMcpOptions() (like createMcpConfig()) is called lazily on the first incoming MCP
request, late enough to bind the resource URL once the port is known — but it is NOT: the REST
framework eagerly walks every @Bean-annotated method declared on the resource (including this
inherited getMcpOptions()) while building the servlet's RestContext, i.e. during Jetty's
OWN servlet initialization, strictly before Server.start() returns. A constructor-supplied resource
URL is therefore required; see SecuredExampleServer.start(int) for how it opens the Jetty connector
(and thus learns the real ephemeral port) before this servlet is even constructed.
-
Nested Class Summary
Nested classes/interfaces inherited from class org.apache.juneau.rest.server.servlet.RestServlet
RestServlet.Builder<R extends RestServlet,SELF extends RestServlet.Builder<R, SELF>>, RestServlet.DefaultBuilder<R extends RestServlet> -
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final StringThe baseline OAuth scope every request to this server must carry, on top of a valid, correctly-audienced token.static final StringThe additional step-up scopepublishNote/deleteNoterequire on top ofREAD_SCOPE(H3, SEP-2350 per-operation scoping), viaMcpResourceServerConfig.addOperationScope(...).Fields inherited from class jakarta.servlet.http.HttpServlet
LEGACY_DO_HEAD -
Constructor Summary
ConstructorsConstructorDescriptionSecuredExampleMcpServer(OfflineAuthorizationServer authServer, URI resource) Constructor. -
Method Summary
Modifier and TypeMethodDescriptionprotected McpOptionsAdvertises capabilities and instructions.Returns this server's own canonical resource URL, as supplied to the constructor.Methods inherited from class org.apache.juneau.examples.mcp.ExampleMcpServer
createMcpConfigMethods inherited from class org.apache.juneau.rest.server.mcp.v20260728.McpRestServlet
getMcpOptions, getSubscriptionBroker, mcpProtectedResourceMetadataPathInserted, mcpProtectedResourceMetadataRoot, mcpResourceServerAuth, mcpTraceContextExtractor, revisionMethods inherited from class org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet
acceptReadinessState, getMcpConfig, handleMcpMethods inherited from class org.apache.juneau.rest.server.servlet.RestServlet
builder, createAuthenticator, destroy, doLog, getContext, getPath, getPaths, getRequest, getResponse, getRestBuilder, init, log, log, service, setContextMethods inherited from class jakarta.servlet.http.HttpServlet
doDelete, doGet, doHead, doOptions, doPatch, doPost, doPut, doTrace, getLastModified, isSensitiveHeader, serviceMethods inherited from class jakarta.servlet.GenericServlet
getInitParameter, getInitParameterNames, getServletConfig, getServletContext, getServletInfo, getServletName, init, log, log
-
Field Details
-
READ_SCOPE
The baseline OAuth scope every request to this server must carry, on top of a valid, correctly-audienced token. Advertised in the PRM document'sscopes_supportedand required viaMcpResourceServerConfig.addRequiredScope(...).- See Also:
-
WRITE_SCOPE
The additional step-up scopepublishNote/deleteNoterequire on top ofREAD_SCOPE(H3, SEP-2350 per-operation scoping), viaMcpResourceServerConfig.addOperationScope(...).- See Also:
-
-
Constructor Details
-
SecuredExampleMcpServer
Constructor.- Parameters:
authServer- The offline authorization server this instance validates bearer tokens against (itspublic keyandissuer). Must not benull .resource- This server's own canonical root URL (e.g.http://localhost:10001/), known and fixed before construction — see the class javadoc's "one wrinkle" section for why this cannot be supplied later. Must not benull .
-
-
Method Details
-
getResource
Returns this server's own canonical resource URL, as supplied to the constructor.- Returns:
- The resource URL. Never
null .
-
createMcpOptions
Description copied from class:ExampleMcpServerAdvertises capabilities and instructions. Explicit here so theserver/discoverhandshake truthfully reports everything this server supports — notablyresources.subscribe, which pairs with thesubscriptions/listendemo.On revision
2026-07-28,server/discover'sinstructionsfield is sourced from thisMcpOptions, notMcpServerConfig.setInstructions(java.lang.String), which only feeds the legacy v1initializehandshake.- Overrides:
createMcpOptionsin classExampleMcpServer- Returns:
- A non-
null options instance.
-