Java
TL;DR — Three artifacts on Maven Central under
ai.authplane.sdk. Pickauthplane-mcpfor the official MCP Java SDK on a Jakarta Servlet container,authplane-springfor Spring Boot, orauthplane-sdkfor framework-agnostic primitives. Requires Java 21+ and one JVM flag — see Required JVM property, which is easy to miss and fails at runtime, not at build time.
Install
All three are published to Maven Central.
<!-- Official MCP Java SDK adapter (pulls in the core) -->
<dependency>
<groupId>ai.authplane.sdk</groupId>
<artifactId>authplane-mcp</artifactId>
<version>0.2.0</version>
</dependency>
<!-- Spring Boot adapter (pulls in the core) -->
<dependency>
<groupId>ai.authplane.sdk</groupId>
<artifactId>authplane-spring</artifactId>
<version>0.2.0</version>
</dependency>
<!-- Core primitives only -->
<dependency>
<groupId>ai.authplane.sdk</groupId>
<artifactId>authplane-sdk</artifactId>
<version>0.2.0</version>
</dependency>
Requires Java 21+. authplane-spring additionally requires Spring Boot 4.x. The core’s only production dependency is com.nimbusds:nimbus-jose-jwt, pinned to 10.x — see Upgrading from 0.1.0 if you pin it yourself.
Required JVM property
This one is specific to Java, and it fails at runtime rather than at build time — so it is worth setting before your first request rather than after.
-Djdk.httpclient.allowRestrictedHeaders=host
The SDK’s outbound HTTP path uses java.net.http.HttpClient with explicit DNS pinning: a URL is resolved to an IP, the request connects to that IP, and the original hostname is sent in the Host header so virtual-host routing on the server side keeps working. Java 11+ blocks the Host header on the stdlib HttpClient by default, so the flag has to be set at JVM startup — in every JVM that loads the SDK, including test and CI.
The flag lifts a JDK-internal “prevent careless mistakes” guard, not a security boundary; every other Java HTTP client (Apache HttpClient, OkHttp, async-http-client) lets you set Host freely. No other AuthPlane SDK needs an equivalent.
authplane-mcp — Official MCP Java SDK adapter
For the official MCP Java SDK running on any Jakarta Servlet container.
Quickstart
import ai.authplane.sdk.mcp.AuthplaneMcpSetup;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema.Implementation;
import java.util.List;
AuthplaneMcpSetup setup = AuthplaneMcpSetup.builder()
.issuer("https://auth.company.com")
.resource("https://mcp.company.com/mcp")
.scopes(List.of("tools/query", "tools/write"))
.build()
.get();
// The host owns the transport provider; the adapter supplies auth.
HttpServletStreamableServerTransportProvider transport =
HttpServletStreamableServerTransportProvider.builder()
.mcpEndpoint(setup.mcpPath())
.securityValidator(setup.adapter())
.contextExtractor(setup.adapter())
.build();
McpServer.sync(transport)
.serverInfo(Implementation.builder("My Server", "1.0.0").build())
.tools(/* your tool specifications */)
.build();
setup.registerServlets(servletContext, transport);
build() returns a CompletableFuture<AuthplaneMcpSetup> that completes once RFC 8414 metadata discovery and the initial JWKS fetch have succeeded — so a server that reaches registerServlets(...) is already able to verify tokens.
What AuthplaneMcpSetup gives you
Two transport hooks, deliberately independent
AuthplaneMcpAdapter implements both ServerTransportSecurityValidator (headers only) and McpTransportContextExtractor<HttpServletRequest> (full servlet request). The two run independent verifications with no shared state — each verifies the Bearer token from scratch. That costs a second verification and buys freedom from any threading assumption about the MCP transport. One asymmetry to know: on a streaming SSE GET only the header-level hook runs, so DPoP binding and introspection-based revocation are not re-checked on that path — the SDK documents this on AuthplaneMcpAdapter.
Claims land in the McpTransportContext under AuthplaneMcpAdapter.CLAIMS_KEY.
Per-tool scope enforcement
new SyncToolSpecification(
Tool.builder("query", schema).description("Execute a query").build(),
(exchange, request) -> {
VerifiedClaims claims = AuthplaneMcpAdapter.getClaims(exchange.transportContext());
claims.requireScope("tools/query");
return new CallToolResult(List.of(new TextContent("result")), false, null, null);
}
);
requireScope(...) throws InsufficientScopeException, which the MCP server turns into a JSON-RPC error. hasScope(...) branches without throwing. Repeated requireScope calls are AND logic.
authplane-spring — Spring Boot adapter
Two integration paths. They are alternatives, not layers — pick one.
@SpringBootApplication
@Import(AuthplaneSecurityConfig.class)
public class MyMcpServer {
public static void main(String[] args) {
SpringApplication.run(MyMcpServer.class, args);
}
}
@Component
class MyTools {
@Tool(description = "Execute a query")
public String query(String sql) {
AuthplaneAuthentication.current().requireScope("tools/query");
return runQuery(sql);
}
}
authplane.issuer=https://auth.company.com
authplane.resource=https://mcp.company.com/mcp
authplane.scopes=tools/query,tools/write
Which path
Path A if you want the Spring Security ecosystem: @PreAuthorize, SecurityContext propagation, structured errors with a WWW-Authenticate header. Path B if you want minimal dependencies and no Spring Security on the classpath.
authplane-sdk — Core primitives
No framework assumptions. Use it directly when you are not on the official MCP SDK or Spring.
try (AuthplaneClient client =
AuthplaneClient.builder("https://auth.example.com").build().get()) {
AuthplaneResource verifier =
client.resource("https://api.example.com", List.of("read:data", "write:data"));
VerifiedClaims claims = verifier.verify(bearerToken).get().claims();
claims.requireScope("read:data");
String subject = claims.sub();
String clientId = claims.clientId();
}
Async model
AuthplaneClient.builder(...).build() returns a CompletableFuture<AuthplaneClient>; verify(...) returns a CompletableFuture<VerificationResult>. The .get() calls above are illustrative — compose these with your framework’s async model rather than blocking a request thread in production.
AuthplaneClient is AutoCloseable and owns the transport, caches and circuit breaker. Build one per process and share it; closing it releases those resources.
Resource identifiers
The resource identifier is published verbatim to unauthenticated callers — it appears in the resource_metadata parameter of the 401 WWW-Authenticate challenge and in the RFC 9728 document’s resource field. The SDK therefore validates it when you construct the resource, not at the first 401:
- No userinfo.
https://svc:pw@api.example.com/mcpis rejected — RFC 9110 §4.2.4. - No fragment. RFC 8707 §2 states the resource URI “MUST NOT include a fragment component”.
- A scheme is required.
//api.example.com/mcpis rejected; RFC 8707 §2 requires an absolute URI. - A query component is allowed and preserved.
https://api.example.com/mcp?tenant=aderiveshttps://api.example.com/.well-known/oauth-protected-resource/mcp?tenant=a, per RFC 9728 §3. It must match the RFC 3986 §3.4 grammar — unescaped brackets (?filter[a]=b) are the realistic failure, and percent-encoding them (?filter%5Ba%5D=b) keeps the identifier.
An invalid identifier fails at startup — including a Spring context that builds one — rather than surfacing as a broken discovery URL that clients silently fail to resolve.
Upgrading from 0.1.0
0.2.0 narrows what the SDK accepts, so a configuration that started on 0.1.0 can fail at startup instead — which is the point: each of these used to surface later, as a 500 or a broken discovery URL, at a place where the operator could no longer see the cause.
Resource identifiers are validated at construction. Userinfo, a fragment, a missing scheme and a query outside the RFC 3986 §3.4 grammar are all refused where you configure the identifier, not at the first 401. The realistic one is unescaped brackets — ?filter[a]=b, which java.net.URI, browsers and servlet containers all accept; percent-encode them (?filter%5Ba%5D=b) to keep the identifier. A Spring context that builds such a resource now fails to start.
A query component is preserved in the derived PRM URL. https://api.example.com/mcp?tenant=a now advertises …/.well-known/oauth-protected-resource/mcp?tenant=a in WWW-Authenticate: … resource_metadata=, where 0.1.0 advertised the query-less URL. Update any hard-coded expectation of the old value. Your existing PRM route keeps serving the document — routing is unchanged.
nimbus-jose-jwt moves to 10.x, closing a denial-of-service advisory on deeply nested JSON (GHSA-xwmg-2g98-w7v9) that affected 0.1.0. Nimbus types are part of this SDK’s public API — DPoPKeyMaterial.fromJwk takes a com.nimbusds.jose.jwk.JWK and publicJwk() returns one — so if you construct DPoP key material yourself, or pin nimbus in your own build, you need 10.x. Using only the SDK’s own entrypoints, Maven resolves it transitively and there is nothing to do.
Under the hood 0.2.0 also starts re-reading authorization server metadata on the verification path, so metadataRefreshSeconds takes effect on a resource server that only verifies tokens, and a rotated jwks_uri is followed. No configuration change is needed for that.
Related
- SDKs overview — pick between adapters
- Quickstart — end-to-end in 10 minutes
- Guides: Enable DPoP end-to-end — configuring inbound DPoP correctly
- Guides: Wire up the Token Vault — token exchange for upstream vending
- Concepts: Resource servers and PRM — what the document is for
- Concepts: DPoP — why proof-of-possession, when to enable
- Full user guides in the java-sdk repo: