Skip to content
AgentEnsemble AgentEnsemble
Get Started

MCP Bridge

The agentensemble-mcp module bridges the Model Context Protocol (MCP) ecosystem into AgentEnsemble. It adapts MCP server tools to the AgentTool interface so they can be used alongside Java-native tools in any agent’s tool list.


implementation("net.agentensemble:agentensemble-mcp:VERSION")

Use McpToolFactory.fromServer() to connect to any MCP-compatible server and obtain its tools as AgentTool instances. Wrap the transport in try-with-resources to ensure the subprocess is cleaned up:

import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import net.agentensemble.mcp.McpToolFactory;
try (StdioMcpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("npx", "--yes", "@modelcontextprotocol/server-filesystem", "/workspace"))
.build()) {
// Get all tools from the server
List<AgentTool> tools = McpToolFactory.fromServer(transport);
// Or filter to specific tools by name
List<AgentTool> filtered = McpToolFactory.fromServer(transport, "read_file", "write_file");
}

For managed lifecycle (recommended), prefer McpServerLifecycle via McpToolFactory.filesystem() or McpToolFactory.git() — see below.

Each tool’s parameter schema is passed through directly from the MCP server to the LLM — the LLM sees properly typed, named parameters just as with TypedAgentTool records.


McpToolFactory provides convenience methods for the well-known MCP reference servers:

import net.agentensemble.mcp.McpToolFactory;
import net.agentensemble.mcp.McpServerLifecycle;
// Filesystem server: read, write, search, list files
try (McpServerLifecycle fs = McpToolFactory.filesystem(Path.of("/workspace"))) {
fs.start();
List<AgentTool> fsTools = fs.tools();
var agent = Agent.builder()
.role("File Manager")
.tools(fsTools)
.llm(chatModel)
.build();
}
// Git server: status, diff, log, commit, branch
try (McpServerLifecycle git = McpToolFactory.git(Path.of("/repo"))) {
git.start();
List<AgentTool> gitTools = git.tools();
// ...
}

These methods require npx (Node.js) to be available on the system PATH. They spawn the MCP reference servers as subprocesses:

  • filesystem(Path) runs npx @modelcontextprotocol/server-filesystem <dir>
  • git(Path) runs npx @modelcontextprotocol/server-git --repository <dir>

McpServerLifecycle manages the MCP server subprocess lifecycle. It implements AutoCloseable for use with try-with-resources:

try (McpServerLifecycle server = McpToolFactory.filesystem(projectDir)) {
server.start(); // Spawn subprocess, initialize MCP protocol
List<AgentTool> tools = server.tools(); // List and cache tools
boolean alive = server.isAlive(); // Check health
// ... use tools ...
} // Automatically closes the server on exit

State machine:

+-- close() ----+
v |
CREATED --start()--> STARTED --close()--> CLOSED
| ^ |
| +------- start() ----+
+-- close() -> CLOSED (close before start is safe)
  • start() spawns the server subprocess and performs a health check; idempotent when already running, and revivable — calling it after close() spawns a fresh subprocess and rebuilds the connection.
  • tools() lists available tools (cached within a session). The cache is dropped inside close(), so a close() -> start() -> tools() sequence relists from the fresh connection.
  • close() shuts down the server; idempotent and safe before start() (an unstarted-then-closed lifecycle stays in CLOSED; calling start() afterwards spawns a fresh subprocess).
  • isAlive() / isRunning() return true when started and not yet closed.

Tool instances returned by tools() survive a close/restart cycle: they look up the active McpClient through a supplier on every call, so an Agent built once with fs.tools() keeps working after fs.close() followed by fs.start().

For loops and request handlers that build an ensemble per iteration, do not wrap the inner run() in try-with-resources around the lifecycle — closing per iteration would spawn a new npx subprocess every time. Instead, keep the lifecycle at the process scope, or bind it to the ensemble (next section).


Long-running ensembles can take ownership of an MCP server lifecycle so it is started the moment .managedResource(fs) runs in the builder (a synchronous side effect of the builder method, not deferred to .build() or start(int)) and closed during Ensemble.stop():

McpServerLifecycle fs = McpToolFactory.filesystem(projectDir);
Ensemble kitchen = Ensemble.builder()
.chatLanguageModel(model)
.webDashboard(WebDashboard.onPort(7329))
.managedResource(fs) // started immediately; closed on stop()
.scheduledTask(ScheduledTask.builder()
.name("hourly-report")
.task(Task.builder()
.description("Summarize today's filesystem changes")
.tools(fs.tools()) // tools() works because managedResource() started fs
.build())
.schedule(Schedule.every(Duration.ofHours(1)))
.build())
.build();
kitchen.start(7329); // fs is already running; scheduled tasks fire against it
// ... fs stays alive across every scheduled firing ...
kitchen.stop(); // fs is closed (ensemble owns the lifecycle)

Ownership rule (mirrors webDashboard()): a resource that is already running when registered is treated as caller-owned and is never closed by the ensemble. A not-yet-running resource is owned by the ensemble and is closed during stop().

McpServerLifecycle implements ManagedResource; any other start()/close()-style resource can be wired the same way.


MCP tools and Java tools produce standard AgentTool instances. They can be freely mixed in a single agent’s tool list:

// MCP tools for filesystem operations
try (McpServerLifecycle fs = McpToolFactory.filesystem(workDir)) {
fs.start();
// Combine MCP tools with Java tools by name
List<Object> allTools = new ArrayList<>(fs.tools());
allTools.add(new CalculatorTool());
allTools.add(WebSearchTool.ofTavily(apiKey));
var agent = Agent.builder()
.role("Developer")
.tools(allTools)
.llm(chatModel)
.build();
}

MCP tools integrate naturally with the agentensemble-coding module. Start the filesystem and git servers, then combine their tools into a coding agent:

import net.agentensemble.coding.CodingTask;
import net.agentensemble.mcp.McpToolFactory;
import net.agentensemble.mcp.McpServerLifecycle;
try (McpServerLifecycle fs = McpToolFactory.filesystem(projectDir);
McpServerLifecycle git = McpToolFactory.git(projectDir)) {
fs.start();
git.start();
List<Object> mcpTools = new ArrayList<>();
mcpTools.addAll(fs.tools());
mcpTools.addAll(git.tools());
Agent agent = Agent.builder()
.role("Senior Software Engineer")
.goal("Implement, debug, and refactor code with precision")
.tools(mcpTools)
.llm(model)
.maxIterations(75)
.build();
Task task = CodingTask.fix("Fix the failing authentication test")
.toBuilder().agent(agent).build();
EnsembleOutput output = Ensemble.run(model, task);
}

For the full walkthrough, see the MCP Coding Example and the Coding Agents Guide.


import net.agentensemble.Agent;
import net.agentensemble.Ensemble;
import net.agentensemble.Task;
import net.agentensemble.mcp.McpToolFactory;
import net.agentensemble.mcp.McpServerLifecycle;
// Start MCP filesystem server scoped to the project directory
try (McpServerLifecycle fs = McpToolFactory.filesystem(Path.of("/my/project"))) {
fs.start();
Agent coder = Agent.builder()
.role("Software Engineer")
.goal("Read code and answer questions about it")
.tools(fs.tools())
.llm(chatModel)
.maxIterations(20)
.build();
Task task = Task.builder()
.description("Find the main entry point of the application and explain what it does")
.build();
var result = Ensemble.builder()
.agent(coder)
.task(task)
.build()
.run();
System.out.println(result.output());
}