Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ McpToolboxClient client = McpToolboxClient.builder()
// Cloud Run Production
McpToolboxClient client = McpToolboxClient.builder()
.baseUrl("https://my-toolbox-service.a.run.app/mcp")
// .apiKey("...") // Optional: Overrides automatic Google Auth
// .headers(Map.of("Authorization", "Bearer YOUR_TOKEN")) // Optional: Add custom headers, overrides automatic Google Auth
// .apiKey("...") // Optional: Deprecated but supported for backward compatibility
.build();
```

Expand Down Expand Up @@ -283,7 +284,7 @@ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/key.json"
| **Cloud Run** | Uses Service Account | **None.** (Automatic) |
| **CI/CD** | Uses Service Account Key | Set GOOGLE\_APPLICATION\_CREDENTIALS=/path/to/key.json |

*Note: If you provide an `.apiKey()` in the builder, it overrides the automatic ADC mechanism.*
*Note: If you provide an `.apiKey()` or `Authorization` in `.headers()` in the builder, it overrides the automatic ADC mechanism.*

### Authenticating the Tools

Expand Down
67 changes: 56 additions & 11 deletions src/main/java/com/google/cloud/mcp/HttpMcpToolboxClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ public class HttpMcpToolboxClient implements McpToolboxClient {
private static final String HTTP_WARNING =
"This connection is using HTTP. To prevent credential exposure, please ensure all"
+ " communication is sent over HTTPS.";

private final String baseUrl;
private final String apiKey;
private final Map<String, String> headers;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private boolean initialized = false;
Expand All @@ -58,8 +57,25 @@ public class HttpMcpToolboxClient implements McpToolboxClient {
* @param apiKey The API key for authentication (optional).
*/
public HttpMcpToolboxClient(String baseUrl, String apiKey) {
this(
baseUrl,
apiKey != null && !apiKey.isEmpty()
? Map.of("Authorization", apiKey.startsWith("Bearer ") ? apiKey : "Bearer " + apiKey)
: Collections.emptyMap());
}

/**
* Constructs a new HttpMcpToolboxClient with generic headers.
*
* @param baseUrl The base URL of the MCP Toolbox Server.
* @param headers The HTTP headers to include in requests.
*/
public HttpMcpToolboxClient(String baseUrl, Map<String, String> headers) {
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
this.apiKey = apiKey;
this.headers =
headers != null
? java.util.Collections.unmodifiableMap(new java.util.HashMap<>(headers))
: java.util.Collections.emptyMap();
this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
this.objectMapper = new ObjectMapper();
}
Expand All @@ -80,7 +96,11 @@ private synchronized CompletableFuture<Void> ensureInitialized(String authHeader
.uri(URI.create(baseUrl))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body));
if (authHeader != null) req.header("Authorization", authHeader);
this.headers.forEach(
(k, v) -> {
if (!"Authorization".equalsIgnoreCase(k)) req.setHeader(k, v);
});
if (authHeader != null) req.setHeader("Authorization", authHeader);

return httpClient
.sendAsync(req.build(), HttpResponse.BodyHandlers.ofString())
Expand All @@ -100,7 +120,11 @@ private synchronized CompletableFuture<Void> ensureInitialized(String authHeader
.header("Content-Type", "application/json")
.header("MCP-Protocol-Version", protocolVersion)
.POST(HttpRequest.BodyPublishers.ofString(notifBody));
if (authHeader != null) nReq.header("Authorization", authHeader);
this.headers.forEach(
(k, val) -> {
if (!"Authorization".equalsIgnoreCase(k)) nReq.setHeader(k, val);
});
if (authHeader != null) nReq.setHeader("Authorization", authHeader);

return httpClient
.sendAsync(nReq.build(), HttpResponse.BodyHandlers.ofString())
Expand Down Expand Up @@ -144,7 +168,11 @@ public CompletableFuture<Map<String, ToolDefinition>> loadToolset(String toolset
.header("Content-Type", "application/json")
.header("MCP-Protocol-Version", protocolVersion)
.POST(HttpRequest.BodyPublishers.ofString(body));
if (authHeader != null) req.header("Authorization", authHeader);
this.headers.forEach(
(k, val) -> {
if (!"Authorization".equalsIgnoreCase(k)) req.setHeader(k, val);
});
if (authHeader != null) req.setHeader("Authorization", authHeader);

return httpClient
.sendAsync(req.build(), HttpResponse.BodyHandlers.ofString())
Expand Down Expand Up @@ -246,8 +274,14 @@ public CompletableFuture<ToolResult> invokeTool(
// Determine priority Auth header before init so init requests can use it if
// needed
String finalAuthHeader = null;
if (extraHeaders.containsKey("Authorization")) {
finalAuthHeader = extraHeaders.get("Authorization");
String authKeyInExtra =
extraHeaders.keySet().stream()
.filter(k -> "Authorization".equalsIgnoreCase(k))
.findFirst()
.orElse(null);

if (authKeyInExtra != null) {
finalAuthHeader = extraHeaders.get(authKeyInExtra);
} else if (adcHeader != null) {
finalAuthHeader = adcHeader;
}
Expand All @@ -270,10 +304,19 @@ public CompletableFuture<ToolResult> invokeTool(
.header("MCP-Protocol-Version", protocolVersion)
.POST(HttpRequest.BodyPublishers.ofString(requestBody));

this.headers.forEach(
(k, val) -> {
if (!"Authorization".equalsIgnoreCase(k))
requestBuilder.setHeader(k, val);
});
extraHeaders.forEach(
(k, val) -> {
if (!"Authorization".equalsIgnoreCase(k))
requestBuilder.setHeader(k, val);
});
if (reqAuth != null) {
requestBuilder.setHeader("Authorization", reqAuth);
}
extraHeaders.forEach(requestBuilder::setHeader);

return httpClient
.sendAsync(
Expand All @@ -291,8 +334,10 @@ public CompletableFuture<ToolResult> invokeTool(
}

private String getAuthorizationHeader() {
if (this.apiKey != null && !this.apiKey.isEmpty()) {
return this.apiKey.startsWith("Bearer ") ? this.apiKey : "Bearer " + this.apiKey;
for (Map.Entry<String, String> entry : this.headers.entrySet()) {
if ("Authorization".equalsIgnoreCase(entry.getKey())) {
return entry.getValue();
}
}
try {
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/google/cloud/mcp/McpToolboxClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ interface Builder {
*/
Builder apiKey(String apiKey);

/**
* Sets additional HTTP headers to be included in all requests to the MCP Toolbox Server.
*
* @param headers The HTTP headers.
* @return The builder instance.
*/
Builder headers(Map<String, String> headers);

/**
* Builds and returns a new {@link McpToolboxClient} instance.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@

package com.google.cloud.mcp;

import java.util.HashMap;
import java.util.Map;

/** Implementation of the {@link McpToolboxClient.Builder} interface. */
public class McpToolboxClientBuilder implements McpToolboxClient.Builder {
private String baseUrl;
private String apiKey;
private Map<String, String> headers = new HashMap<>();

/** Constructs a new McpToolboxClientBuilder. */
public McpToolboxClientBuilder() {}
Expand All @@ -36,6 +40,14 @@ public McpToolboxClient.Builder apiKey(String apiKey) {
return this;
}

@Override
public McpToolboxClient.Builder headers(Map<String, String> headers) {
if (headers != null) {
this.headers.putAll(headers);
}
return this;
}

@Override
public McpToolboxClient build() {
if (baseUrl == null || baseUrl.isEmpty()) {
Expand All @@ -45,6 +57,15 @@ public McpToolboxClient build() {
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}
return new HttpMcpToolboxClient(baseUrl, apiKey);

Map<String, String> finalHeaders = new HashMap<>(this.headers);
if (apiKey != null && !apiKey.isEmpty()) {
String authValue = apiKey.startsWith("Bearer ") ? apiKey : "Bearer " + apiKey;
if (finalHeaders.keySet().stream().noneMatch(k -> "Authorization".equalsIgnoreCase(k))) {
finalHeaders.put("Authorization", authValue);
}
}

return new HttpMcpToolboxClient(baseUrl, finalHeaders);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.mcp;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Map;
import org.junit.jupiter.api.Test;

class McpToolboxClientBuilderTest {

@Test
void testHeadersAndApiKey() {
McpToolboxClient client =
McpToolboxClient.builder()
.baseUrl("http://localhost:8080")
.apiKey("my-api-key")
.headers(Map.of("X-Custom-Header", "value1", "Authorization", "Bearer custom-token"))
.build();

assertNotNull(client);
assertTrue(client instanceof HttpMcpToolboxClient);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@ public class ToolboxE2ESetup implements BeforeAllCallback, AfterAllCallback {

@Override
public void beforeAll(ExtensionContext context) throws Exception {
String projectId = getEnvVar(PROJECT_ID_ENV);
String projectId = System.getenv(PROJECT_ID_ENV);
if (projectId == null || projectId.trim().isEmpty()) {
logger.warning("Environment variable " + PROJECT_ID_ENV + " is not set. Skipping E2E tests.");
org.junit.jupiter.api.Assumptions.assumeTrue(
false, "Skipping E2E tests because " + PROJECT_ID_ENV + " is not set.");
return;
}

String toolboxVersion = getEnvVar(TOOLBOX_VERSION_ENV);
String manifestVersion = getEnvVar(TOOLBOX_MANIFEST_VERSION_ENV);

Expand Down
Loading