diff --git a/contrib/samples/mcpfilesystem/McpFilesystemAgent.java b/contrib/samples/mcpfilesystem/McpFilesystemAgent.java new file mode 100644 index 000000000..fd35792b8 --- /dev/null +++ b/contrib/samples/mcpfilesystem/McpFilesystemAgent.java @@ -0,0 +1,51 @@ +// Copyright 2025 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.example.mcpfilesystem; + +import com.google.adk.agents.LlmAgent; +import com.google.adk.tools.mcp.McpToolset; +import com.google.adk.tools.mcp.StdioServerParameters; +import com.google.common.collect.ImmutableList; + +/** Defines an agent that wires the MCP stdio filesystem server via {@link McpToolset}. */ +public final class McpFilesystemAgent { + /** Root agent instance exposed to runners and registries. */ + public static final LlmAgent ROOT_AGENT = + LlmAgent.builder() + .name("filesystem_agent") + .description("Assistant that performs file operations through the MCP filesystem server.") + .model("gemini-2.0-flash") + .instruction( + """ + You are a file system assistant. Use the provided tools to read, write, search, and manage + files and directories. Ask clarifying questions when unsure about file operations. + When the user requests that you append to a file, read the current contents, add the + appended text with a newline if needed, then overwrite the file with the combined + content. + """) + .tools(ImmutableList.of(createMcpToolset())) + .build(); + + private McpFilesystemAgent() {} + + private static McpToolset createMcpToolset() { + StdioServerParameters stdioParams = + StdioServerParameters.builder() + .command("npx") + .args( + ImmutableList.of("-y", "@modelcontextprotocol/server-filesystem", "/tmp/mcp-demo")) + .build(); + return new McpToolset(stdioParams.toServerParameters()); + } +} diff --git a/contrib/samples/mcpfilesystem/McpFilesystemRun.java b/contrib/samples/mcpfilesystem/McpFilesystemRun.java new file mode 100644 index 000000000..a4caa0d31 --- /dev/null +++ b/contrib/samples/mcpfilesystem/McpFilesystemRun.java @@ -0,0 +1,98 @@ +package com.example.mcpfilesystem; + +import com.google.adk.agents.RunConfig; +import com.google.adk.artifacts.InMemoryArtifactService; +import com.google.adk.events.Event; +import com.google.adk.memory.InMemoryMemoryService; +import com.google.adk.runner.Runner; +import com.google.adk.sessions.InMemorySessionService; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import io.reactivex.rxjava3.core.Flowable; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** Console runner that exercises {@link McpFilesystemAgent#ROOT_AGENT}. */ +public final class McpFilesystemRun { + private final String userId; + private final String sessionId; + private final Runner runner; + + private McpFilesystemRun() { + String appName = "mcp-filesystem-app"; + this.userId = "mcp-filesystem-user"; + this.sessionId = UUID.randomUUID().toString(); + + InMemorySessionService sessionService = new InMemorySessionService(); + this.runner = + new Runner( + McpFilesystemAgent.ROOT_AGENT, + appName, + new InMemoryArtifactService(), + sessionService, + new InMemoryMemoryService()); + + ConcurrentMap initialState = new ConcurrentHashMap<>(); + var unused = + sessionService.createSession(appName, userId, initialState, sessionId).blockingGet(); + } + + private void run(String prompt) { + System.out.println("You> " + prompt); + Content userMessage = + Content.builder() + .role("user") + .parts(ImmutableList.of(Part.builder().text(prompt).build())) + .build(); + RunConfig runConfig = RunConfig.builder().build(); + Flowable eventStream = + this.runner.runAsync(this.userId, this.sessionId, userMessage, runConfig); + List agentEvents = Lists.newArrayList(eventStream.blockingIterable()); + + StringBuilder sb = new StringBuilder(); + sb.append("Agent> "); + for (Event event : agentEvents) { + sb.append(event.stringifyContent().stripTrailing()); + } + System.out.println(sb); + } + + /** + * Entry point for the sample runner. + * + * @param args Optional command-line arguments. Pass {@code --run-extended} for additional + * prompts. + */ + public static void main(String[] args) { + McpFilesystemRun runner = new McpFilesystemRun(); + try { + runner.run("List the files available in /tmp/mcp-demo"); + if (args.length > 0 && Objects.equals(args[0], "--run-extended")) { + runner.run( + "Create or overwrite /tmp/mcp-demo/notes.txt with the text 'MCP demo note generated by" + + " the sample.'"); + runner.run("Read /tmp/mcp-demo/notes.txt to confirm the contents."); + runner.run("Search /tmp/mcp-demo for the phrase 'MCP demo note'."); + runner.run( + "Append the line 'Appended by the extended run.' to /tmp/mcp-demo/notes.txt and show" + + " the updated file."); + } + } finally { + McpFilesystemAgent.ROOT_AGENT + .toolsets() + .forEach( + toolset -> { + try { + toolset.close(); + } catch (Exception e) { + System.err.println("Failed to close toolset: " + e.getMessage()); + } + }); + } + } +} diff --git a/contrib/samples/mcpfilesystem/README.md b/contrib/samples/mcpfilesystem/README.md new file mode 100644 index 000000000..5853f355f --- /dev/null +++ b/contrib/samples/mcpfilesystem/README.md @@ -0,0 +1,71 @@ +# MCP Filesystem Agent Sample + +This sample mirrors the `tool_mcp_stdio_file_system_config/root_agent.yaml` configuration by wiring +an MCP filesystem toolset programmatically. The agent launches the filesystem stdio server via +`npx @modelcontextprotocol/server-filesystem` and interacts with it using the Google ADK runtime. + +## Project Layout + +``` +├── McpFilesystemAgent.java // Agent definition and MCP toolset wiring +├── McpFilesystemRun.java // Console runner entry point +├── pom.xml // Maven configuration and exec main class +└── README.md // This file +``` + +## Prerequisites + +- Java 17+ +- Maven 3.9+ +- Node.js 18+ with `npx` available (for `@modelcontextprotocol/server-filesystem`) + +`npx` downloads the MCP filesystem server on first run. Subsequent executions reuse the cached +package, so expect a longer startup time the first time you run the sample. + +## Build and Run + +Set the Gemini environment variables and launch the interactive session from this directory. One +command compiles and runs the sample: + +```bash +export GOOGLE_GENAI_USE_VERTEXAI=FALSE +export GOOGLE_API_KEY=your_api_key +mvn clean compile exec:java +``` + +The runner sends an initial prompt asking the agent to list files. To explore additional operations, +reuse the same environment and pass the `--run-extended` argument (you can keep `clean compile` if +you want compilation and execution in a single step): + +```bash +mvn clean compile exec:java -Dexec.args="--run-extended" +``` + +If you prefer to launch (and build) the sample while staying in the `google_adk` root, point Maven at +this module’s POM and again use a single command: + +```bash +export GOOGLE_GENAI_USE_VERTEXAI=FALSE +export GOOGLE_API_KEY=your_api_key +mvn -f contrib/samples/mcpfilesystem/pom.xml clean compile exec:java +``` + +To run the extended sequence from the repo root, reuse the same environment variables and pass +`--run-extended`: + +```bash +export GOOGLE_GENAI_USE_VERTEXAI=FALSE +export GOOGLE_API_KEY=your_api_key +mvn -f contrib/samples/mcpfilesystem/pom.xml clean compile exec:java -Dexec.args="--run-extended" +``` + +The extended flow drives the agent through: +- creating or overwriting `/tmp/mcp-demo/notes.txt` with default content +- reading the file back for confirmation +- searching the workspace for the seeded phrase +- appending a second line and displaying the updated file contents + +## Related Samples + +For the configuration-driven variant of this demo, see +`../configagent/tool_mcp_stdio_file_system_config/root_agent.yaml`. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml new file mode 100644 index 000000000..801ff5e14 --- /dev/null +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -0,0 +1,99 @@ + + + + 4.0.0 + + + com.google.adk + google-adk-parent + 0.3.1-SNAPSHOT + ../../.. + + + com.google.adk.samples + google-adk-sample-mcpfilesystem + Google ADK - Sample - MCP Filesystem + + Programmatic MCP filesystem sample mirroring the YAML-based configuration under + contrib/samples/configagent. + + jar + + + UTF-8 + 17 + 1.11.0 + com.example.mcpfilesystem.McpFilesystemRun + ${project.parent.version} + + + + + com.google.adk + google-adk + ${google-adk.version} + + + commons-logging + commons-logging + 1.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + true + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-source + generate-sources + + add-source + + + + . + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.2.0 + + ${exec.mainClass} + runtime + + + + + diff --git a/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java b/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java index ddd65be50..edbbc2c71 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java +++ b/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java @@ -77,19 +77,25 @@ public T getMcpSession() { @Override public Optional declaration() { - JsonSchema schema = this.mcpTool.inputSchema(); + JsonSchema inputSchema = this.mcpTool.inputSchema(); + Map outputSchema = this.mcpTool.outputSchema(); try { - return Optional.ofNullable(schema) + return Optional.ofNullable(inputSchema) .map( - value -> - FunctionDeclaration.builder() - .name(this.name()) - .description(this.description()) - .parametersJsonSchema(value) - .build()); - } catch (Exception e) { + value -> { + FunctionDeclaration.Builder builder = + FunctionDeclaration.builder() + .name(this.name()) + .description(this.description()) + .parametersJsonSchema(value); + Optional.ofNullable(outputSchema).ifPresent(builder::responseJsonSchema); + return builder.build(); + }); + } catch (RuntimeException e) { throw new McpToolDeclarationException( - String.format("MCP tool:%s failed to get declaration, schema:%s.", this.name(), schema), + String.format( + "MCP tool:%s failed to get declaration, inputSchema:%s. outputSchema:%s.", + this.name(), inputSchema, outputSchema), e); } }