DEV Community

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_image in 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.