Skip to content

Commit 11d01df

Browse files
committed
HttpClientStreamHttpTransport: add authorization error handler
1 parent ce2c747 commit 11d01df

File tree

4 files changed

+301
-15
lines changed

4 files changed

+301
-15
lines changed

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.modelcontextprotocol.client.McpAsyncClient;
2424
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
2525
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
26+
import io.modelcontextprotocol.client.transport.customizer.McpHttpClientAuthorizationErrorHandler;
2627
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
2728
import io.modelcontextprotocol.common.McpTransportContext;
2829
import io.modelcontextprotocol.json.McpJsonDefaults;
@@ -115,6 +116,8 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
115116

116117
private final boolean openConnectionOnStartup;
117118

119+
private final McpHttpClientAuthorizationErrorHandler authorizationErrorHandler;
120+
118121
private final boolean resumableStreams;
119122

120123
private final McpAsyncHttpClientRequestCustomizer httpRequestCustomizer;
@@ -132,14 +135,15 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
132135
private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient httpClient,
133136
HttpRequest.Builder requestBuilder, String baseUri, String endpoint, boolean resumableStreams,
134137
boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer,
135-
List<String> supportedProtocolVersions) {
138+
McpHttpClientAuthorizationErrorHandler authorizationErrorHandler, List<String> supportedProtocolVersions) {
136139
this.jsonMapper = jsonMapper;
137140
this.httpClient = httpClient;
138141
this.requestBuilder = requestBuilder;
139142
this.baseUri = URI.create(baseUri);
140143
this.endpoint = endpoint;
141144
this.resumableStreams = resumableStreams;
142145
this.openConnectionOnStartup = openConnectionOnStartup;
146+
this.authorizationErrorHandler = authorizationErrorHandler;
143147
this.activeSession.set(createTransportSession());
144148
this.httpRequestCustomizer = httpRequestCustomizer;
145149
this.supportedProtocolVersions = Collections.unmodifiableList(supportedProtocolVersions);
@@ -478,6 +482,17 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
478482
})).onErrorMap(CompletionException.class, t -> t.getCause()).onErrorComplete().subscribe();
479483

480484
})).flatMap(responseEvent -> {
485+
int statusCode = responseEvent.responseInfo().statusCode();
486+
if (statusCode == 401 || statusCode == 403) {
487+
return Mono.deferContextual(ctx -> {
488+
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
489+
return Mono.from(this.authorizationErrorHandler.handle(responseEvent.responseInfo(),
490+
transportContext, Mono.defer(() -> this.sendMessage(sentMessage))));
491+
})
492+
.then(Mono.error(new McpHttpClientTransportException("Authorization error when sending message",
493+
responseEvent.responseInfo())));
494+
}
495+
481496
if (transportSession.markInitialized(
482497
responseEvent.responseInfo().headers().firstValue("mcp-session-id").orElseGet(() -> null))) {
483498
// Once we have a session, we try to open an async stream for
@@ -488,8 +503,6 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
488503

489504
String sessionRepresentation = sessionIdOrPlaceholder(transportSession);
490505

491-
int statusCode = responseEvent.responseInfo().statusCode();
492-
493506
if (statusCode >= 200 && statusCode < 300) {
494507

495508
String contentType = responseEvent.responseInfo()
@@ -664,6 +677,8 @@ public static class Builder {
664677
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
665678
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
666679

680+
private McpHttpClientAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientAuthorizationErrorHandler.NOOP;
681+
667682
/**
668683
* Creates a new builder with the specified base URI.
669684
* @param baseUri the base URI of the MCP server
@@ -801,6 +816,16 @@ public Builder asyncHttpRequestCustomizer(McpAsyncHttpClientRequestCustomizer as
801816
return this;
802817
}
803818

819+
/**
820+
* Sets the handler
821+
* @param authorizationErrorHandler
822+
* @return
823+
*/
824+
public Builder authorizationErrorHandler(McpHttpClientAuthorizationErrorHandler authorizationErrorHandler) {
825+
this.authorizationErrorHandler = authorizationErrorHandler;
826+
return this;
827+
}
828+
804829
/**
805830
* Sets the connection timeout for the HTTP client.
806831
* @param connectTimeout the connection timeout duration
@@ -845,7 +870,7 @@ public HttpClientStreamableHttpTransport build() {
845870
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
846871
return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
847872
httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
848-
httpRequestCustomizer, supportedProtocolVersions);
873+
httpRequestCustomizer, authorizationErrorHandler, supportedProtocolVersions);
849874
}
850875

851876
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport;
6+
7+
import java.net.http.HttpResponse;
8+
9+
import io.modelcontextprotocol.spec.McpTransportException;
10+
11+
/**
12+
* Authorization-related exception for {@link java.net.http.HttpClient}-based client
13+
* transport.
14+
*
15+
* @see <a href=
16+
* "https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization">MCP
17+
* Specification: Authorization</a>
18+
* @author Daniel Garnier-Moiroux
19+
*/
20+
public class McpHttpClientTransportException extends McpTransportException {
21+
22+
private final HttpResponse.ResponseInfo responseInfo;
23+
24+
public McpHttpClientTransportException(String message, HttpResponse.ResponseInfo responseInfo) {
25+
super(message);
26+
this.responseInfo = responseInfo;
27+
}
28+
29+
public HttpResponse.ResponseInfo getResponseInfo() {
30+
return responseInfo;
31+
}
32+
33+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.client.transport.customizer;
6+
7+
import java.net.http.HttpResponse;
8+
9+
import io.modelcontextprotocol.common.McpTransportContext;
10+
import org.reactivestreams.Publisher;
11+
import reactor.core.publisher.Mono;
12+
import reactor.core.scheduler.Schedulers;
13+
14+
/**
15+
* Handle security-related errors in HTTP-client based transports. This class handles MCP
16+
* server responses with status code 401 and 403.
17+
*
18+
* @see <a href=
19+
* "https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization">MCP
20+
* Specification: Authorization</a>
21+
* @author Daniel Garnier-Moiroux
22+
*/
23+
public interface McpHttpClientAuthorizationErrorHandler {
24+
25+
/**
26+
* Handle HTTP error, and signal whether the HTTP request should be retried or not.
27+
* @param responseInfo the HTTP response information
28+
* @param context the MCP client transport context
29+
* @return {@link Publisher} emitting true if the original request should be replayed,
30+
* false otherwise.
31+
*/
32+
Publisher<Boolean> handle(HttpResponse.ResponseInfo responseInfo, McpTransportContext context);
33+
34+
McpHttpClientAuthorizationErrorHandler NOOP = new Noop();
35+
36+
/**
37+
* Handle HTTP error, and optionally retry the HTTP request.
38+
* @param responseInfo the HTTP response information
39+
* @param context the MCP client transport context
40+
* @param retryHandler the handler to use to retry the HTTP request.
41+
* @return a {@link Publisher} to signal either an error or a retry
42+
*/
43+
default Publisher<Void> handle(HttpResponse.ResponseInfo responseInfo, McpTransportContext context,
44+
Publisher<Void> retryHandler) {
45+
return Mono.from(this.handle(responseInfo, context))
46+
.flatMap(shouldRetry -> shouldRetry != null && shouldRetry ? Mono.from(retryHandler) : Mono.empty());
47+
}
48+
49+
/**
50+
* Create a {@link McpHttpClientAuthorizationErrorHandler} from a synchronous handler.
51+
* Will be subscribed on {@link Schedulers#boundedElastic()}. The handler may be
52+
* blocking.
53+
* @param handler the synchronous handler
54+
* @return an async handler
55+
*/
56+
static McpHttpClientAuthorizationErrorHandler fromSync(Sync handler) {
57+
return (info, context) -> {
58+
try {
59+
var shouldRetry = handler.handle(info, context);
60+
return Mono.just(shouldRetry).subscribeOn(Schedulers.boundedElastic());
61+
}
62+
catch (Exception e) {
63+
return Mono.error(e);
64+
}
65+
};
66+
}
67+
68+
/**
69+
* Synchronous authorization error handler.
70+
*/
71+
interface Sync {
72+
73+
/**
74+
* Handle HTTP error, and signal whether the HTTP request should be retried or
75+
* not.
76+
* @param responseInfo the HTTP response information
77+
* @param context the MCP client transport context
78+
* @return true if the original request should be replayed, false otherwise.
79+
*/
80+
boolean handle(HttpResponse.ResponseInfo responseInfo, McpTransportContext context);
81+
82+
}
83+
84+
class Noop implements McpHttpClientAuthorizationErrorHandler {
85+
86+
@Override
87+
public Publisher<Boolean> handle(HttpResponse.ResponseInfo responseInfo, McpTransportContext context) {
88+
return Mono.just(false);
89+
}
90+
91+
}
92+
93+
}

0 commit comments

Comments
 (0)