Skip to content

Commit e20696a

Browse files
authored
feat!: transition Java SDK to MCP transport (#38)
* feat!: transition Java SDK to native MCP JSON-RPC transport This PR updates the SDK to achieve transport parity with the Python, JS, and Go SDKs by seamlessly adopting the MCP standard and removing legacy REST mappings. * Fix 405 error and debug test assertions * Fix file formatting * Refactor ToolResult text extraction and update tests * Fix parsing of unauthorized error into ToolResult * Fix string assertion in testRunToolWrongAuth * Fix string assertion case sensitivity in auth test * chore: format test file * docs: update endpoint URLs to include /mcp path * doc: Simplify urls in README * fix(java): parse toolbox/authInvoke from _meta in list tools * refactor: Remove `ToolResult.text()` helper method, update text content extraction to stream-based collection. * chore: Revert /mcp auto-append and update docs to explicitly require /mcp * docs: add release-please version annotations * chore: revert example directory changes * build: upgrade libraries-bom to 26.48.0 and google-auth-library to 1.23.0 * doc: Remove the optional step of deploying to a specific target * doc: Revert to simplified README examples with comments * doc: Improve README doc formatting and rendering and typos
1 parent fb65e26 commit e20696a

12 files changed

Lines changed: 396 additions & 149 deletions

File tree

DEVELOPER.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ Commits](https://www.conventionalcommits.org/) for structuring commit messages.
3030
```
3131
This will generate the package JAR in your local `.m2` repository, making it available for other local Maven projects.
3232

33-
1. (Optional) Run deploy to a local directory for verification:
34-
```bash
35-
mvn deploy -DaltDeploymentRepository=local::default::file://$(pwd)/target/staging-deploy
36-
```
37-
3833
## Release Process
3934

4035
This repository uses [Release Please](https://github.com/googleapis/release-please) to automate the release process.

README.md

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Add the dependency to your `pom.xml`:
9696
<dependency>
9797
<groupId>com.google.cloud.mcp</groupId>
9898
<artifactId>mcp-toolbox-sdk-java</artifactId>
99-
<version>0.1.1</version>
99+
<version>0.1.1</version> <!-- x-release-please-version -->
100100
<scope>compile</scope>
101101
</dependency>
102102
```
@@ -106,21 +106,25 @@ Add the dependency to your `pom.xml`:
106106
```
107107
dependencies {
108108
// Source: https://mvnrepository.com/artifact/com.google.cloud.mcp/mcp-toolbox-sdk-java
109-
implementation("com.google.cloud.mcp:mcp-toolbox-sdk-java:0.1.1")
109+
implementation("com.google.cloud.mcp:mcp-toolbox-sdk-java:0.1.1") // x-release-please-version
110110
}
111111
```
112112

113113
## Usage
114114

115115
### Load the Client
116116

117-
The McpToolboxClient is your entry point. It is thread-safe and designed to be instantiated once and reused.
117+
The `McpToolboxClient` is your entry point. It is thread-safe and designed to be instantiated once and reused.
118118

119-
```
120-
import com.google.cloud.mcp.McpToolboxClient;
119+
```java
120+
// Local Development
121+
McpToolboxClient client = McpToolboxClient.builder()
122+
.baseUrl("http://localhost:5000/mcp")
123+
.build();
121124

125+
// Cloud Run Production
122126
McpToolboxClient client = McpToolboxClient.builder()
123-
.baseUrl("[https://my-toolbox-service.a.run.app](https://my-toolbox-service.a.run.app)")
127+
.baseUrl("https://my-toolbox-service.a.run.app/mcp")
124128
// .apiKey("...") // Optional: Overrides automatic Google Auth
125129
.build();
126130
```
@@ -162,7 +166,7 @@ client.loadTool("get-toy-price").thenAccept(toolDef -> {
162166

163167
### Invoke a Tool
164168

165-
Invoking a tool sends a request to the MCP Server to execute the logic (SQL, API call, etc.). Arguments are passed as a Map<String, Object>.
169+
Invoking a tool sends a request to the MCP Server to execute the logic (SQL, API call, etc.). Arguments are passed as a `Map<String, Object>`.
166170

167171
```java
168172
import java.util.Map;
@@ -173,27 +177,30 @@ Map<String, Object> args = Map.of(
173177
);
174178

175179
client.invokeTool("get-toy-price", args).thenAccept(result -> {
180+
// Pick the first item from the response.
176181
System.out.println("Result: " + result.content().get(0).text());
182+
});
177183
```
178184

179185
## Quickstart
180186

181187
Here is the minimal code needed to connect to a toolbox and invoke a tool.
182188

183-
```
189+
```java
184190
import com.google.cloud.mcp.McpToolboxClient;
185191
import java.util.Map;
186192

187193
public class App {
188194
public static void main(String[] args) {
189195
// 1. Create the Client
190196
McpToolboxClient client = McpToolboxClient.builder()
191-
.baseUrl("[https://my-toolbox-service.a.run.app](https://my-toolbox-service.a.run.app)")
197+
.baseUrl("https://my-toolbox-service.a.run.app/mcp")
192198
.build();
193199

194200
// 2. Invoke a Tool
195201
client.invokeTool("get-toy-price", Map.of("description", "plush dinosaur"))
196202
.thenAccept(result -> {
203+
// Pick the first item from the response.
197204
System.out.println("Tool Output: " + result.content().get(0).text());
198205
})
199206
.exceptionally(ex -> {
@@ -205,14 +212,14 @@ public class App {
205212
}
206213
```
207214

208-
For a detailed example, check the ExampleUsage.java file in teh example folder of this repo.
215+
For a detailed example, check the ExampleUsage.java file in the example folder of this repo.
209216

210217
> [!NOTE]
211218
>
212-
> The SDK is Async-First, using Java's CompletableFuture to bridge both patterns naturally.
213-
> - Asynchronous: Chain methods using .thenCompose(), .thenAccept(), and .exceptionally() > for non-blocking execution.
214-
> - If you prefer synchronous execution, simply call .join() on the result to block until > completion.
215-
```
219+
> The SDK is Async-First, using Java's `CompletableFuture` to bridge both patterns naturally.
220+
> - Asynchronous: Chain methods using `.thenCompose()`, `.thenAccept()`, and `.exceptionally()` for non-blocking execution.
221+
> - If you prefer synchronous execution, simply call `.join()` on the result to block until completion.
222+
```java
216223
// Async (Non-blocking)
217224
client.invokeTool("tool-name", args).thenAccept(result -> ...);
218225
// Sync (Blocking)
@@ -221,7 +228,7 @@ ToolResult result = client.invokeTool("tool-name", args).join();
221228

222229
## Authentication
223230

224-
## Client to Server Authentication
231+
### Client to Server Authentication
225232

226233
This section describes how to authenticate the `ToolboxClient` itself when connecting to a Toolbox server instance that requires authentication. This is crucial for securing your Toolbox server endpoint, especially when deployed on platforms like Cloud Run, GKE, or any environment where unauthenticated access is restricted.
227234

@@ -241,7 +248,7 @@ The Java SDK handles the generation of **Authorization headers** (Bearer tokens)
241248

242249
You need to set up [ADC](https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment).
243250

244-
### Authenticating with Google Cloud Servers (Cloud Run
251+
### Authenticating with Google Cloud Servers (Cloud Run)
245252

246253
For Toolbox servers hosted on Google Cloud (e.g., Cloud Run), the SDK provides seamless OIDC authentication.
247254

@@ -276,15 +283,14 @@ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/key.json"
276283
| **Cloud Run** | Uses Service Account | **None.** (Automatic) |
277284
| **CI/CD** | Uses Service Account Key | Set GOOGLE\_APPLICATION\_CREDENTIALS=/path/to/key.json |
278285

279-
*Note: If you provide an .apiKey() in the builder, it overrides the automatic ADC mechanism.*
286+
*Note: If you provide an `.apiKey()` in the builder, it overrides the automatic ADC mechanism.*
280287

281-
## Authenticating the Tools
288+
### Authenticating the Tools
282289

283290
Tools can be configured within the Toolbox service to require authentication, ensuring only authorized users or applications can invoke them, especially when accessing sensitive data.
284291

285-
#### Warning
286-
287-
Always use HTTPS to connect your application with the Toolbox service, especially in production environments or whenever the communication involves sensitive data (including scenarios where tools require authentication tokens). Using plain HTTP lacks encryption and exposes your application and data to significant security risks, such as eavesdropping and tampering.
292+
> [!WARNING]
293+
> Always use HTTPS to connect your application with the Toolbox service, especially in production environments or whenever the communication involves sensitive data (including scenarios where tools require authentication tokens). Using plain HTTP lacks encryption and exposes your application and data to significant security risks, such as eavesdropping and tampering.
288294
289295

290296
### When is Authentication Needed?
@@ -309,7 +315,7 @@ You must provide the SDK with an `AuthTokenGetter` (a function that returns a `C
309315

310316
**Important:** The **Service Name** (or Auth Source) used when adding the getter (e.g., `"salesforce_auth"`) must exactly match the name of the corresponding auth source defined in the tool's configuration.
311317

312-
```
318+
```java
313319
import com.google.cloud.mcp.AuthTokenGetter;
314320

315321
// Define your token retrieval logic
@@ -347,7 +353,7 @@ public class AuthExample {
347353

348354
// 2. Initialize the client
349355
McpToolboxClient client = McpToolboxClient.builder()
350-
.baseUrl("[http://127.0.0.1:5000](http://127.0.0.1:5000)")
356+
.baseUrl("http://127.0.0.1:5000/mcp")
351357
.build();
352358

353359
// 3. Load tool, attach auth, and execute
@@ -359,6 +365,7 @@ public class AuthExample {
359365
return tool.execute(Map.of("input", "some input"));
360366
})
361367
.thenAccept(result -> {
368+
// Pick the first item from the response.
362369
System.out.println(result.content().get(0).text());
363370
})
364371
.join();
@@ -376,19 +383,17 @@ The SDK allows you to pre-set, or "bind", values for specific tool parameters be
376383
* Enforcing consistency: Ensuring specific values for certain parameters.
377384
* Pre-filling known data: Providing defaults or context.
378385

379-
##### Important
380-
381-
The parameter names used for binding (e.g., `"api_key"`) must exactly match the parameter names defined in the tool's configuration within the Toolbox service.
382-
383-
##### Note
386+
> [!IMPORTANT]
387+
> The parameter names used for binding (e.g., `"api_key"`) must exactly match the parameter names defined in the tool's configuration within the Toolbox service.
384388
385-
You do not need to modify the tool's configuration in the Toolbox service to bind parameter values using the SDK.
389+
> [!NOTE]
390+
> You do not need to modify the tool's configuration in the Toolbox service to bind parameter values using the SDK.
386391
387392
### Option A: Static Binding
388393

389394
Bind a fixed value to a tool object.
390395

391-
```
396+
```java
392397
client.loadTool("get-toy-price").thenCompose(tool -> {
393398
// Bind 'currency' to 'USD' permanently for this tool instance
394399
tool.bindParam("currency", "USD");
@@ -402,7 +407,7 @@ client.loadTool("get-toy-price").thenCompose(tool -> {
402407

403408
Instead of a static value, you can bind a parameter to a synchronous or asynchronous function (`Supplier`). This function will be called **each time** the tool is invoked to dynamically determine the parameter's value at runtime.
404409

405-
```
410+
```java
406411
client.loadTool("check-order-status").thenCompose(tool -> {
407412
// Bind 'user_id' to a function that fetches the current user from context
408413
tool.bindParam("user_id", () -> SecurityContext.getCurrentUser().getId());
@@ -412,15 +417,15 @@ client.loadTool("check-order-status").thenCompose(tool -> {
412417
});
413418
```
414419

415-
##### Important
416-
417-
You don't need to modify tool configurations to bind parameter values.
420+
> [!IMPORTANT]
421+
>
422+
> You don't need to modify tool configurations to bind parameter values.
418423
419424
## Error Handling
420425

421-
The SDK uses Java's CompletableFuture API. Errors (Network issues, 4xx/5xx responses) are propagated as exceptions wrapped in CompletionException.
426+
The SDK uses Java's `CompletableFuture` API. Errors (Network issues, 4xx/5xx responses) are propagated as exceptions wrapped in `CompletionException`.
422427

423-
```
428+
```java
424429

425430
client.invokeTool("invalid-tool", Map.of())
426431
.handle((result, ex) -> {
@@ -435,8 +440,8 @@ client.invokeTool("invalid-tool", Map.of())
435440

436441
## Contributing
437442

438-
We welcome contributions\! Please see [CONTRIBUTING.md](https://www.google.com/search?q=CONTRIBUTING.md) for details on how to submit pull requests, report bugs, or request features.
443+
We welcome contributions\! Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for details on how to submit pull requests, report bugs, or request features.
439444

440445
## License
441446

442-
This project is licensed under the Apache 2.0 License \- see the [LICENSE](https://www.google.com/search?q=LICENSE) file for details.
447+
This project is licensed under the Apache 2.0 License \- see the [LICENSE](https://github.com/googleapis/mcp-toolbox-sdk-java/blob/main/LICENSE) file for details.

demo-applications/cymbal-transit/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,6 @@ or
120120
To directly deploy your agent to Cloud Run and test there:
121121

122122
``` bash
123-
gcloud run deploy cymbal-transit --source . --set-env-vars GCP_PROJECT_ID=<<YOUR_PROJECT_ID>>,GCP_REGION=us-central1,GEMINI_MODEL_NAME=gemini-2.5-flash,MCP_TOOLBOX_URL=<<YOUR_MCP_TOOLBOX_URL>> --allow-unauthenticated
123+
gcloud run deploy cymbal-transit --source . --set-env-vars GCP_PROJECT_ID=<<YOUR_PROJECT_ID>>,GCP_REGION=us-central1,GEMINI_MODEL_NAME=gemini-2.5-flash,MCP_TOOLBOX_URL=<<YOUR_MCP_TOOLBOX_URL_ENDING_IN_/mcp>> --allow-unauthenticated
124124
```
125-
Replace the placeholder variables enclosed within <<>>.
125+
Replace the placeholder variables enclosed within <<>>. Ensure that your `MCP_TOOLBOX_URL` explicitly ends with `/mcp` (e.g., `https://my-toolbox-service.a.run.app/mcp`).

demo-applications/cymbal-transit/src/main/java/cloudcode/cymbal/web/CymbalTransitController.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ public void init() {
189189
public CompletableFuture<String> findAllSchedules() {
190190
return mcpClient.invokeTool("find-bus-schedules", Collections.emptyMap()).thenApply(result -> {
191191
if (result.isError() || result.content() == null || result.content().isEmpty()) return "No schedules found.";
192-
return result.content().get(0).text();
192+
return getTextContent(result);
193193
});
194194
}
195195

@@ -201,7 +201,7 @@ public CompletableFuture<String> querySchedules(String origin, String destinatio
201201
return mcpClient.invokeTool("query-schedules", params).thenApply(result -> {
202202
if (result.isError() || result.content() == null || result.content().isEmpty()) return "No specific schedules found.";
203203
System.out.println(result);
204-
return result.content().get(0).text();
204+
return getTextContent(result);
205205
});
206206
}
207207

@@ -214,17 +214,26 @@ public CompletableFuture<String> bookTicket(String tripId, String passengerName)
214214
})
215215
.thenApply(result -> {
216216
if (result.isError() || result.content() == null || result.content().isEmpty()) return "Transaction failed.";
217-
return result.content().get(0).text();
217+
return getTextContent(result);
218218
});
219219
}
220220

221221
public CompletableFuture<String> searchPolicies(String searchQuery) {
222222
return mcpClient.invokeTool("search-policies", Map.of("search_query", searchQuery))
223223
.thenApply(result -> {
224224
if (result.isError() || result.content() == null || result.content().isEmpty()) return "No policy information found.";
225-
return result.content().get(0).text();
225+
return getTextContent(result);
226226
});
227227
}
228+
229+
private String getTextContent(com.google.cloud.mcp.ToolResult result) {
230+
if (result.content() == null)
231+
return "";
232+
return result.content().stream()
233+
.filter(c -> "text".equals(c.type()) && c.text() != null)
234+
.map(c -> c.text())
235+
.collect(java.util.stream.Collectors.joining("\n"));
236+
}
228237
}
229238

230239
/**

example/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ These sample Java files allow you to test the features supported in the Java ver
44

55
1. First set up database (AlloyDB in this case) and ingest data for the tools referenced in this example. In order to demonstrate the use of tools across applications, we have considered a RETAIL STORE and TOY STORE application data together. In order to install cluster, instance and setup data, for this sample use case, follow the first 4 steps of the first codelab (hybrid search) and the 5th step of the second codelab (toystore app) below:
66

7-
[https://codelabs.developers.google.com/hybrid-search-on-cloudrun](url)
7+
https://codelabs.developers.google.com/hybrid-search-on-cloudrun
88
and
9-
[https://codelabs.developers.google.com/toy-store-app](url)
9+
https://codelabs.developers.google.com/toy-store-app
1010

1111
2. To start with tools, go ahead and create the MCP Toolbox Server for the sameple use case we are looking at.
1212

@@ -45,7 +45,7 @@ In any case remember to change the `YOUR_TOOLBOX_SERVICE_ENDPOINT` placeholder i
4545
<dependency>
4646
<groupId>com.google.cloud.mcp</groupId>
4747
<artifactId>mcp-toolbox-sdk-java</artifactId>
48-
<version>0.1.1</version>
48+
<version>0.1.1</version> <!-- x-release-please-version -->
4949
</dependency>
5050
```
5151

example/src/main/java/cloudcode/helloworld/ExampleUsage.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public static void main(String[] args) {
101101
return client.invokeTool("get-retail-facet-filters", Map.of());
102102
})
103103
.thenCompose(result -> {
104-
System.out.println(" -> Result: " + (result.content() != null ? "Received Data" : "Empty"));
104+
System.out.println(" -> Result: " + (result.content() != null ? "Received Data" : "Empty"));
105105

106106
// STEP 3: INVOKE TOOL WITH AUTHENTICATED PARAMETERS
107107
System.out.println("\n[5] Testing Authenticated Tool: 'get-toy-price'...");
@@ -121,7 +121,8 @@ public static void main(String[] args) {
121121
return tool.execute(Map.of("description", "barbie"))
122122
.thenCompose(result1 -> {
123123
if (result1.content() != null && !result1.content().isEmpty()) {
124-
System.out.println(" -> Result (Unbound): " + result1.content().get(0).text());
124+
System.out
125+
.println(" -> Result (Unbound): " + result1.content().get(0).text());
125126
}
126127

127128
// NOW bind the parameter
@@ -136,9 +137,9 @@ public static void main(String[] args) {
136137
.thenAccept(result -> {
137138
System.out.println("\n[6] Final Result (Bound):");
138139
if (result.isError()) {
139-
System.err.println("Tool execution failed: " + result.content().get(0).text());
140+
System.err.println("Tool execution failed: " + result.content().get(0).text());
140141
} else if (result.content() != null && !result.content().isEmpty()) {
141-
String output = result.content().get(0).text();
142+
String output = result.content().get(0).text();
142143
System.out.println(" " + output.substring(0, Math.min(output.length(), 200)) + "...");
143144
} else {
144145
System.out.println(" Empty Response");

pom.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@
6767
<dependency>
6868
<groupId>com.google.cloud</groupId>
6969
<artifactId>libraries-bom</artifactId>
70-
<version>26.32.0</version>
70+
<version>26.48.0</version>
7171
<type>pom</type>
7272
<scope>import</scope>
7373
</dependency>
7474
<dependency>
7575
<groupId>com.google.errorprone</groupId>
7676
<artifactId>error_prone_annotations</artifactId>
77-
<version>2.21.1</version>
77+
<version>2.30.0</version>
7878
</dependency>
7979
</dependencies>
8080
</dependencyManagement>
@@ -91,6 +91,7 @@
9191
<dependency>
9292
<groupId>com.google.auth</groupId>
9393
<artifactId>google-auth-library-oauth2-http</artifactId>
94+
<version>${google.auth.version}</version>
9495
</dependency>
9596

9697
<!-- Testing -->

0 commit comments

Comments
 (0)