# Build One AI Tool Server, Call It From Three Different Agents (MCP Explained)

> Source: <https://dev.to/xbill/build-one-ai-tool-server-call-it-from-three-different-agents-mcp-explained-22l2>
> Published: 2026-07-15 20:35:35+00:00

Have you ever wanted to give an AI assistant a new ability — like generating images — and have that ability work in *any* AI tool you use, not just one?

That's exactly what this project does, and the magic ingredient is the **Model Context Protocol (MCP)**. In this article we'll walk through a real, working repo where **one small Python server** gives image-generation superpowers to **three completely different programs**:

None of them share a single line of code. Let's see how.

Think of MCP as **USB-C for AI tools**.

Before USB-C, every device needed its own special cable. Before MCP, every AI app needed its own special plugin format — a ChatGPT plugin didn't work in Claude, a Claude tool didn't work in your Python agent, and so on.

MCP fixes this with a simple split:

The two sides talk JSON messages. The simplest way they connect is called **stdio**: the client just launches the server as a child process and they chat over standard input/output — the same pipes you use when you run `echo hi | grep h`

.

💡

Fun consequence:because stdoutisthe communication channel, an MCP server must never`print()`

to it. Our server logs tostderrinstead. One stray print statement would garble the protocol!

An **agent** is an AI model in a loop with tools: the model reads your request, decides a tool would help, calls it, reads the result, and keeps going until the job is done. The AI is the brain; MCP tools are the hands.

Here's the repo layout:

```
nb2lite-agent-claude/
├── MCP/            ← the star: an MCP server wrapping Gemini's image model
│   └── server.py
├── python/         ← consumer 1: a Google ADK agent
├── rust/           ← consumer 2: a Rust CLI client
└── .mcp.json       ← consumer 3: config that plugs the server into Claude Code
```

And here's how the pieces connect:

```
 Claude Code  ──┐
 ADK agent    ──┼── MCP over stdio ──►  MCP/server.py  ──►  Gemini image API
 Rust CLI     ──┘                            │
                                             ▼
                                      images/ folder
                                   (your generated PNGs)
```

Three arrows in, one server, one API out. The Gemini-specific code exists in exactly **one file**.

The server is built with **FastMCP**, which ships with the official `mcp`

Python package. Writing a tool is as easy as decorating a function:

``` python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("NB2Lite Agent")

@mcp.tool()
def generate_image(
    prompt: str, aspect_ratio: str = "1:1", thinking_level: str = "low"
) -> str:
    """Generates a new image from a text prompt."""
    ...
```

That's it. FastMCP reads the function signature and docstring and automatically tells every connected AI: *"there's a tool called generate_image, here's what it does, here are its parameters."* Your code

The server exposes four tools:

| Tool | What it does |
|---|---|
`generate_image` |
Text prompt → brand-new image |
`edit_image` |
"Change X in the image we just made" |
`edit_local_image` |
Edit an image file from your disk |
`get_help` |
The server describes its own config and tools |

Under the hood, they all call Google's `gemini-3.1-flash-lite-image`

model — a fast image model — through something called the **Interactions API**.

Most image APIs are goldfish: every request starts from zero. The Interactions API is different — it's **stateful**. Every generation returns an `interaction_id`

, and you can pass that ID back to continue the session:

```
interaction = ai_client.interactions.create(
    model=MODEL_NAME,
    previous_interaction_id=previous_interaction_id,  # 👈 "continue from here"
    input=edit_prompt,
    response_format={"type": "image"},
    store=True,  # 👈 remember this interaction on Google's side
)
```

In practice, a conversation looks like this:

`generate_image(...)`

→ gets back `int_abc`

"`edit_image(previous_interaction_id="int_abc", edit_prompt="add a neon RAMEN sign")`

Notice who remembers what: Google's servers store the image session, and the **agent's conversation memory** holds the ID. The MCP server itself stays stateless — you can restart it anytime.

A tool *could* send the image bytes back to the AI. This server deliberately doesn't — it saves the file to disk and returns a short message:

```
🟢 Image successfully saved!
• Saved to: /home/you/images/gen_1780123456_a3b2c1d0.png
• Interaction ID: int_abc
```

Two beginner-friendly lessons hide in here:

`🔴 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).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.

The [Agent Development Kit (ADK)](https://google.github.io/adk-docs/) 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:

`generate_image`

in this file. The toolset `instruction`

explicitly tells the LLM to track interaction IDs. The protocol carries the ID; the LLM's memory Run it with `adk run nb2lite_adk_agent`

for a chat in your terminal, or `adk web`

for a browser UI.

To prove the "any language" claim, the repo includes a Rust client using [ rmcp](https://crates.io/crates/rmcp), the official Rust MCP SDK. It spawns the

``` js
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.

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.**

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! 👇
