Build One AI Tool Server, Call It From Three Different Agents (MCP Explained)
AI's context would waste thousands of tokens for nothing - the AI can't do much with raw pixels, but it can absolutely tell you a file path.
- Friendly errors. Every tool catches exceptions and returns a readable ๐ด Image generation failed: ... string instead of crashing. The AI reads the error and can fix its own mistake (wrong aspect ratio? it'll retry with a valid one).
Consumer 1: Claude Code (zero code!)
Plugging the server into Claude Code takes only a config file, .mcp.json:
{
"mcpServers": {
"nb2lite-agent": {
"type": "stdio",
"command": "python3",
"args": ["/path/to/MCP/server.py"],
"env": {
"GEMINI_API_KEY": "${GEMINI_API_KEY}"
}
}
}
}
Claude Code launches the server, discovers the four tools, and from then on you can just type "generate a 16:9 image of a mountain sunrise" in your coding session.
Consumer 2: a Google ADK agent
The Agent Development Kit (ADK) is Google's framework for building your own agents. Its MCPToolset does all the MCP plumbing - spawn the server, do the handshake, convert every discovered tool into something the LLM can call:
root_agent = LlmAgent(
name="nb2lite_adk_agent",
model="gemini-2.5-flash",
instruction="...remember the most recent Interaction ID and pass it "
"as previous_interaction_id for follow-up edits...",
tools=[
MCPToolset(
connection_params=StdioConnectionParams(
server_params=StdioServerParameters(
command="python3",
args=[str(MCP_SERVER)],
),
),
)
],
)
Two things worth noticing:
- We never define
generate_imagein this file. The toolset imports the tools over the protocol at startup. - The instruction explicitly tells the LLM to track interaction IDs. The protocol carries the ID; the LLM's memory keeps it.
Run it with adk run nb2lite_adk_agent for a chat in your terminal, or adk web for a browser UI.
Consumer 3: a Rust CLI
To prove the "any language" claim, the repo includes a Rust client using rmcp, the official Rust MCP SDK. It spawns the same Python server as a child process:
let service = ()
.serve(TokioChildProcess::new(
Command::new("python3")
.configure(|cmd| {
cmd.arg(&server);
}),
)?)
.await?;
let result = service
.call_tool(CallToolRequestParam {
name: "generate_image".into(),
arguments: json!({
"prompt": prompt,
"aspect_ratio": "16:9"
})
.as_object()
.cloned(),
})
.await?;
There's no AI model in this binary at all - it's a plain program calling the tools directly:
cargo run -- tools # list the tools
cargo run -- generate "a cyberpunk ramen kitchen" 16:9 high # make an image
cargo run -- edit int_abc123 "add a neon RAMEN sign" # refine it
That's a nice mental model to end on: an MCP tool call is just a function call over a pipe. An LLM can make it, and so can your shell script.
The takeaway
Without MCP, supporting these three consumers means three integrations: a Claude-specific setup, an ADK wrapper, and a Rust port of the Gemini client. Three places to update every time the API changes.
With MCP, the capability lives in one file, and each consumer is ~30 lines of config or boilerplate. Adding a fourth consumer tomorrow - LangChain, an editor plugin, whatever - costs about the same. Write the tool once. Let every agent call it.
Try it yourself
The server is published as a ready-to-run Docker image - you don't need the repo at all. Point any MCP client at it (this is a .mcp.json for Claude Code):
{
"mcpServers": {
"nb2lite-agent": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GEMINI_API_KEY",
"-v",
"/absolute/path/to/images:/images",
"xbill9/nb2lite-mcp:latest"
]
}
}
}
Set GEMINI_API_KEY in your environment, ask your agent to generate an image, and check your mounted images/ folder. (Remember: -i but never -t - a TTY corrupts the protocol stream!)
Questions about MCP, ADK, or the Rust side? Drop them in the comments! ๐
Comments
No comments yet. Start the discussion.