Skip to content

Commit a998703

Browse files
authored
feat: add pre & post processing hooks for tools (#58)
1 parent 92a7a02 commit a998703

12 files changed

Lines changed: 799 additions & 322 deletions

src/main/java/com/google/cloud/mcp/McpToolboxClient.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,22 @@ interface Builder {
143143
*/
144144
Builder credentialsProvider(CredentialsProvider credentialsProvider);
145145

146+
/**
147+
* Adds a global pre-processor that will be applied to all tools loaded by this client.
148+
*
149+
* @param preProcessor The pre-processor to add.
150+
* @return The builder instance.
151+
*/
152+
Builder preProcessor(ToolPreProcessor preProcessor);
153+
154+
/**
155+
* Adds a global post-processor that will be applied to all tools loaded by this client.
156+
*
157+
* @param postProcessor The post-processor to add.
158+
* @return The builder instance.
159+
*/
160+
Builder postProcessor(ToolPostProcessor postProcessor);
161+
146162
/**
147163
* Builds and returns a new {@link McpToolboxClient} instance.
148164
*

src/main/java/com/google/cloud/mcp/McpToolboxClientBuilder.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package com.google.cloud.mcp;
1818

19+
import java.util.ArrayList;
1920
import java.util.HashMap;
21+
import java.util.List;
2022
import java.util.Map;
2123
import java.util.concurrent.CompletableFuture;
2224

@@ -26,6 +28,8 @@ public final class McpToolboxClientBuilder implements McpToolboxClient.Builder {
2628
private String apiKey;
2729
private Map<String, String> headers = new HashMap<>();
2830
private CredentialsProvider credentialsProvider;
31+
private final List<ToolPreProcessor> preProcessors = new ArrayList<>();
32+
private final List<ToolPostProcessor> postProcessors = new ArrayList<>();
2933

3034
/** Constructs a new McpToolboxClientBuilder. */
3135
public McpToolboxClientBuilder() {}
@@ -56,6 +60,22 @@ public McpToolboxClient.Builder credentialsProvider(CredentialsProvider credenti
5660
return this;
5761
}
5862

63+
@Override
64+
public McpToolboxClient.Builder preProcessor(ToolPreProcessor preProcessor) {
65+
if (preProcessor != null) {
66+
this.preProcessors.add(preProcessor);
67+
}
68+
return this;
69+
}
70+
71+
@Override
72+
public McpToolboxClient.Builder postProcessor(ToolPostProcessor postProcessor) {
73+
if (postProcessor != null) {
74+
this.postProcessors.add(postProcessor);
75+
}
76+
return this;
77+
}
78+
5979
@Override
6080
public McpToolboxClient build() {
6181
if (baseUrl == null || baseUrl.isEmpty()) {
@@ -83,6 +103,7 @@ public McpToolboxClient build() {
83103
}
84104

85105
Transport transport = new HttpMcpTransport(baseUrl, this.headers, resolvedProvider);
86-
return new McpToolboxClientImpl(transport, this.headers, resolvedProvider);
106+
return new McpToolboxClientImpl(
107+
transport, this.headers, resolvedProvider, preProcessors, postProcessors);
87108
}
88109
}

src/main/java/com/google/cloud/mcp/McpToolboxClientImpl.java

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Collections;
2222
import java.util.HashMap;
2323
import java.util.HashSet;
24+
import java.util.List;
2425
import java.util.Map;
2526
import java.util.Set;
2627
import java.util.concurrent.CompletableFuture;
@@ -49,6 +50,9 @@ public final class McpToolboxClientImpl implements McpToolboxClient {
4950
/** Jackson ObjectMapper for JSON parsing. */
5051
private final ObjectMapper objectMapper;
5152

53+
private final List<ToolPreProcessor> preProcessors;
54+
private final List<ToolPostProcessor> postProcessors;
55+
5256
/**
5357
* Constructs a new McpToolboxClientImpl.
5458
*
@@ -61,22 +65,14 @@ public McpToolboxClientImpl(final Transport clientTransport) {
6165
/**
6266
* Constructs a new McpToolboxClientImpl.
6367
*
64-
* @param clientTransport The underlying MCP transport layer.
65-
* @param clientHeaders Fallback headers for deprecated constructor compatibility.
66-
* @param provider Fallback provider for deprecated constructor compatibility.
68+
* @param transport The underlying MCP transport layer.
69+
* @param headers Fallback headers for deprecated constructor compatibility.
70+
* @param credentialsProvider Fallback provider for deprecated constructor compatibility.
6771
*/
6872
@Deprecated
6973
public McpToolboxClientImpl(
70-
final Transport clientTransport,
71-
final Map<String, String> clientHeaders,
72-
final CredentialsProvider provider) {
73-
this.transport = clientTransport;
74-
this.headers =
75-
clientHeaders != null
76-
? java.util.Collections.unmodifiableMap(new java.util.HashMap<>(clientHeaders))
77-
: java.util.Collections.emptyMap();
78-
this.credentialsProvider = provider;
79-
this.objectMapper = new ObjectMapper();
74+
Transport transport, Map<String, String> headers, CredentialsProvider credentialsProvider) {
75+
this(transport, headers, credentialsProvider, null, null);
8076
}
8177

8278
/**
@@ -152,6 +148,32 @@ private static CredentialsProvider apiKeyToProvider(final String apiKey) {
152148
return () -> CompletableFuture.completedFuture(bearerKey);
153149
}
154150

151+
/**
152+
* Primary constructor for McpToolboxClientImpl.
153+
*
154+
* @param transport The underlying MCP transport layer.
155+
* @param headers Default HTTP headers.
156+
* @param credentialsProvider Provider for credentials.
157+
* @param preProcessors List of pre-processors.
158+
* @param postProcessors List of post-processors.
159+
*/
160+
public McpToolboxClientImpl(
161+
Transport transport,
162+
Map<String, String> headers,
163+
CredentialsProvider credentialsProvider,
164+
List<ToolPreProcessor> preProcessors,
165+
List<ToolPostProcessor> postProcessors) {
166+
this.transport = transport;
167+
this.headers =
168+
headers != null
169+
? java.util.Collections.unmodifiableMap(new java.util.HashMap<>(headers))
170+
: java.util.Collections.emptyMap();
171+
this.credentialsProvider = credentialsProvider;
172+
this.preProcessors = preProcessors != null ? List.copyOf(preProcessors) : List.of();
173+
this.postProcessors = postProcessors != null ? List.copyOf(postProcessors) : List.of();
174+
this.objectMapper = new ObjectMapper();
175+
}
176+
155177
private CompletableFuture<Map<String, String>> getMergedMetadata(
156178
final Map<String, String> extraMetadata) {
157179
if (this.transport instanceof HttpMcpTransport) {
@@ -250,6 +272,12 @@ public CompletableFuture<Map<String, Tool>> loadToolset(
250272
if (authBinds != null && authBinds.containsKey(toolName)) {
251273
authBinds.get(toolName).forEach(tool::addAuthTokenGetter);
252274
}
275+
for (ToolPreProcessor preProcessor : this.preProcessors) {
276+
tool.addPreProcessor(preProcessor);
277+
}
278+
for (ToolPostProcessor postProcessor : this.postProcessors) {
279+
tool.addPostProcessor(postProcessor);
280+
}
253281
tools.put(toolName, tool);
254282
}
255283
return tools;
@@ -279,6 +307,12 @@ public CompletableFuture<Tool> loadTool(
279307
if (authTokenGetters != null) {
280308
authTokenGetters.forEach(tool::addAuthTokenGetter);
281309
}
310+
for (ToolPreProcessor preProcessor : this.preProcessors) {
311+
tool.addPreProcessor(preProcessor);
312+
}
313+
for (ToolPostProcessor postProcessor : this.postProcessors) {
314+
tool.addPostProcessor(postProcessor);
315+
}
282316
return tool;
283317
});
284318
}

src/main/java/com/google/cloud/mcp/ResolvedAuth.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
public final class ResolvedAuth {
2323
private final Map<String, String> tokens;
2424

25+
/**
26+
* Constructs a new ResolvedAuth.
27+
*
28+
* @param tokens The map of resolved auth tokens.
29+
*/
2530
public ResolvedAuth(Map<String, String> tokens) {
2631
Map<String, String> copy = new java.util.HashMap<>();
2732
if (tokens != null) {

src/main/java/com/google/cloud/mcp/Tool.java

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public class Tool {
3535

3636
private final Map<String, Object> boundParameters = new HashMap<>();
3737
private final Map<String, AuthTokenGetter> authGetters = new HashMap<>();
38+
private final List<ToolPreProcessor> preProcessors = new ArrayList<>();
39+
private final List<ToolPostProcessor> postProcessors = new ArrayList<>();
3840

3941
/**
4042
* Constructs a new Tool.
@@ -103,6 +105,28 @@ public Tool addAuthTokenGetter(String serviceName, AuthTokenGetter getter) {
103105
return this;
104106
}
105107

108+
/**
109+
* Adds a pre-processor to the tool.
110+
*
111+
* @param processor The pre-processor to add.
112+
* @return The tool instance.
113+
*/
114+
public Tool addPreProcessor(ToolPreProcessor processor) {
115+
this.preProcessors.add(processor);
116+
return this;
117+
}
118+
119+
/**
120+
* Adds a post-processor to the tool.
121+
*
122+
* @param processor The post-processor to add.
123+
* @return The tool instance.
124+
*/
125+
public Tool addPostProcessor(ToolPostProcessor processor) {
126+
this.postProcessors.add(processor);
127+
return this;
128+
}
129+
106130
/**
107131
* Executes the tool with the provided arguments, applying any bound parameters and resolving
108132
* authentication tokens.
@@ -111,34 +135,53 @@ public Tool addAuthTokenGetter(String serviceName, AuthTokenGetter getter) {
111135
* @return A CompletableFuture containing the result of the tool execution.
112136
*/
113137
public CompletableFuture<ToolResult> execute(Map<String, Object> args) {
114-
Map<String, Object> finalArgs = new HashMap<>(args);
115-
Map<String, String> extraHeaders = new HashMap<>();
116-
117-
// 1. Apply Bound Parameters
118-
for (Map.Entry<String, Object> entry : boundParameters.entrySet()) {
119-
Object val = entry.getValue();
120-
if (val instanceof Supplier) {
121-
finalArgs.put(entry.getKey(), ((Supplier<?>) val).get());
122-
} else {
123-
finalArgs.put(entry.getKey(), val);
124-
}
138+
CompletableFuture<Map<String, Object>> argsFuture =
139+
CompletableFuture.completedFuture(new HashMap<>(args));
140+
141+
for (ToolPreProcessor preProcessor : preProcessors) {
142+
argsFuture = argsFuture.thenCompose(currentArgs -> preProcessor.process(name, currentArgs));
125143
}
126144

127-
// 2. Resolve Auth & Execute
128-
return AuthResolver.resolve(authGetters)
129-
.thenCompose(
130-
resolvedAuth -> {
131-
try {
132-
// Apply credential parameter bindings and extra headers
133-
resolvedAuth.applyTo(finalArgs, extraHeaders, definition);
134-
135-
// 3. Validation & Cleanup
136-
validateAndSanitizeArgs(finalArgs);
137-
return client.invokeTool(name, finalArgs, extraHeaders);
138-
} catch (Exception e) {
139-
return CompletableFuture.failedFuture(e);
145+
CompletableFuture<ToolResult> resultFuture =
146+
argsFuture.thenCompose(
147+
processedArgs -> {
148+
Map<String, Object> finalArgs =
149+
java.util.Collections.synchronizedMap(new HashMap<>(processedArgs));
150+
Map<String, String> extraHeaders =
151+
java.util.Collections.synchronizedMap(new HashMap<>());
152+
153+
// 1. Apply Bound Parameters
154+
for (Map.Entry<String, Object> entry : boundParameters.entrySet()) {
155+
Object val = entry.getValue();
156+
if (val instanceof Supplier) {
157+
finalArgs.put(entry.getKey(), ((Supplier<?>) val).get());
158+
} else {
159+
finalArgs.put(entry.getKey(), val);
160+
}
140161
}
162+
163+
// 2. Resolve Auth & Execute
164+
return AuthResolver.resolve(authGetters)
165+
.thenCompose(
166+
resolvedAuth -> {
167+
try {
168+
// Apply credential parameter bindings and extra headers
169+
resolvedAuth.applyTo(finalArgs, extraHeaders, definition);
170+
171+
// Validation & Cleanup
172+
validateAndSanitizeArgs(finalArgs);
173+
return client.invokeTool(name, finalArgs, extraHeaders);
174+
} catch (Exception e) {
175+
return CompletableFuture.failedFuture(e);
176+
}
177+
});
141178
});
179+
180+
for (ToolPostProcessor postProcessor : postProcessors) {
181+
resultFuture = resultFuture.thenCompose(res -> postProcessor.process(name, res));
182+
}
183+
184+
return resultFuture;
142185
}
143186

144187
/** Validates arguments against the tool definition and removes null values. */

src/main/java/com/google/cloud/mcp/ToolDefinition.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
*
2626
* @param description A description of what the tool does.
2727
* @param parameters A list of parameters the tool accepts.
28+
* @param authRequired List of auth services required by the tool.
29+
* @param readOnlyHint Hint indicating whether the tool is read-only.
30+
* @param destructiveHint Hint indicating whether the tool is destructive.
2831
*/
2932
@JsonIgnoreProperties(ignoreUnknown = true)
3033
public record ToolDefinition(
@@ -34,7 +37,13 @@ public record ToolDefinition(
3437
Boolean readOnlyHint,
3538
Boolean destructiveHint) {
3639

37-
/** Backward-compatible constructor. */
40+
/**
41+
* Backward-compatible constructor.
42+
*
43+
* @param description A description of what the tool does.
44+
* @param parameters A list of parameters the tool accepts.
45+
* @param authRequired List of auth services required.
46+
*/
3847
public ToolDefinition(String description, List<Parameter> parameters, List<String> authRequired) {
3948
this(description, parameters, authRequired, null, null);
4049
}
@@ -58,7 +67,15 @@ public record Parameter(
5867
List<String> authSources, // Maps services to parameters
5968
@JsonProperty("default") Object defaultValue) {
6069

61-
/** Backward-compatible constructor. */
70+
/**
71+
* Backward-compatible constructor.
72+
*
73+
* @param name The name of the parameter.
74+
* @param type The type of the parameter.
75+
* @param required Whether the parameter is required.
76+
* @param description A description of the parameter.
77+
* @param authSources Authentication sources list.
78+
*/
6279
public Parameter(
6380
String name, String type, boolean required, String description, List<String> authSources) {
6481
this(name, type, required, description, authSources, null);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.mcp;
18+
19+
import java.util.concurrent.CompletableFuture;
20+
21+
/** A functional interface for post-processing tool results after invocation. */
22+
@FunctionalInterface
23+
public interface ToolPostProcessor {
24+
25+
/**
26+
* Processes the result of a tool after it has been invoked.
27+
*
28+
* @param toolName The name of the tool that was invoked.
29+
* @param result The original tool result.
30+
* @return A CompletableFuture containing the processed tool result.
31+
*/
32+
CompletableFuture<ToolResult> process(String toolName, ToolResult result);
33+
}

0 commit comments

Comments
 (0)