{"slug": "build-one-ai-tool-server-call-it-from-three-different-agents-mcp-explained", "title": "Build One AI Tool Server, Call It From Three Different Agents (MCP Explained)", "summary": "A developer built a single MCP server that exposes image-generation tools via the Model Context Protocol, allowing three different AI agents—a Google ADK agent, a Rust CLI client, and Claude Code—to call the same server over stdio. The server uses FastMCP to wrap Google's Gemini image model and supports stateful editing through the Interactions API.", "body_md": "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?\n\nThat'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**:\n\nNone of them share a single line of code. Let's see how.\n\nThink of MCP as **USB-C for AI tools**.\n\nBefore 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.\n\nMCP fixes this with a simple split:\n\nThe 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`\n\n.\n\n💡\n\nFun consequence:because stdoutisthe communication channel, an MCP server must never`print()`\n\nto it. Our server logs tostderrinstead. One stray print statement would garble the protocol!\n\nAn **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.\n\nHere's the repo layout:\n\n```\nnb2lite-agent-claude/\n├── MCP/            ← the star: an MCP server wrapping Gemini's image model\n│   └── server.py\n├── python/         ← consumer 1: a Google ADK agent\n├── rust/           ← consumer 2: a Rust CLI client\n└── .mcp.json       ← consumer 3: config that plugs the server into Claude Code\n```\n\nAnd here's how the pieces connect:\n\n```\n Claude Code  ──┐\n ADK agent    ──┼── MCP over stdio ──►  MCP/server.py  ──►  Gemini image API\n Rust CLI     ──┘                            │\n                                             ▼\n                                      images/ folder\n                                   (your generated PNGs)\n```\n\nThree arrows in, one server, one API out. The Gemini-specific code exists in exactly **one file**.\n\nThe server is built with **FastMCP**, which ships with the official `mcp`\n\nPython package. Writing a tool is as easy as decorating a function:\n\n``` python\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"NB2Lite Agent\")\n\n@mcp.tool()\ndef generate_image(\n    prompt: str, aspect_ratio: str = \"1:1\", thinking_level: str = \"low\"\n) -> str:\n    \"\"\"Generates a new image from a text prompt.\"\"\"\n    ...\n```\n\nThat'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\n\nThe server exposes four tools:\n\n| Tool | What it does |\n|---|---|\n`generate_image` |\nText prompt → brand-new image |\n`edit_image` |\n\"Change X in the image we just made\" |\n`edit_local_image` |\nEdit an image file from your disk |\n`get_help` |\nThe server describes its own config and tools |\n\nUnder the hood, they all call Google's `gemini-3.1-flash-lite-image`\n\nmodel — a fast image model — through something called the **Interactions API**.\n\nMost image APIs are goldfish: every request starts from zero. The Interactions API is different — it's **stateful**. Every generation returns an `interaction_id`\n\n, and you can pass that ID back to continue the session:\n\n```\ninteraction = ai_client.interactions.create(\n    model=MODEL_NAME,\n    previous_interaction_id=previous_interaction_id,  # 👈 \"continue from here\"\n    input=edit_prompt,\n    response_format={\"type\": \"image\"},\n    store=True,  # 👈 remember this interaction on Google's side\n)\n```\n\nIn practice, a conversation looks like this:\n\n`generate_image(...)`\n\n→ gets back `int_abc`\n\n\"`edit_image(previous_interaction_id=\"int_abc\", edit_prompt=\"add a neon RAMEN sign\")`\n\nNotice 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.\n\nA 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:\n\n```\n🟢 Image successfully saved!\n• Saved to: /home/you/images/gen_1780123456_a3b2c1d0.png\n• Interaction ID: int_abc\n```\n\nTwo beginner-friendly lessons hide in here:\n\n`🔴 Image generation failed: ...`\n\nstring 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`\n\n:\n\n```\n{\n  \"mcpServers\": {\n    \"nb2lite-agent\": {\n      \"type\": \"stdio\",\n      \"command\": \"python3\",\n      \"args\": [\"/path/to/MCP/server.py\"],\n      \"env\": { \"GEMINI_API_KEY\": \"${GEMINI_API_KEY}\" }\n    }\n  }\n}\n```\n\nClaude 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.\n\nThe [Agent Development Kit (ADK)](https://google.github.io/adk-docs/) is Google's framework for building your *own* agents. Its `MCPToolset`\n\ndoes all the MCP plumbing — spawn the server, do the handshake, convert every discovered tool into something the LLM can call:\n\n```\nroot_agent = LlmAgent(\n    name=\"nb2lite_adk_agent\",\n    model=\"gemini-2.5-flash\",\n    instruction=\"...remember the most recent Interaction ID and pass it \"\n                \"as previous_interaction_id for follow-up edits...\",\n    tools=[\n        MCPToolset(\n            connection_params=StdioConnectionParams(\n                server_params=StdioServerParameters(\n                    command=\"python3\",\n                    args=[str(MCP_SERVER)],\n                ),\n            ),\n        )\n    ],\n)\n```\n\nTwo things worth noticing:\n\n`generate_image`\n\nin this file. The toolset `instruction`\n\nexplicitly tells the LLM to track interaction IDs. The protocol carries the ID; the LLM's memory Run it with `adk run nb2lite_adk_agent`\n\nfor a chat in your terminal, or `adk web`\n\nfor a browser UI.\n\nTo 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\n\n``` js\nlet service = ()\n    .serve(TokioChildProcess::new(Command::new(\"python3\").configure(\n        |cmd| { cmd.arg(&server); },\n    ))?)\n    .await?;\n\nlet result = service\n    .call_tool(CallToolRequestParam {\n        name: \"generate_image\".into(),\n        arguments: json!({ \"prompt\": prompt, \"aspect_ratio\": \"16:9\" })\n            .as_object().cloned(),\n    })\n    .await?;\n```\n\nThere's no AI model in this binary at all — it's a plain program calling the tools directly:\n\n```\ncargo run -- tools                                          # list the tools\ncargo run -- generate \"a cyberpunk ramen kitchen\" 16:9 high # make an image\ncargo run -- edit int_abc123 \"add a neon RAMEN sign\"        # refine it\n```\n\nThat'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.\n\nWithout 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.\n\nWith 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.\n\n**Write the tool once. Let every agent call it.**\n\nThe 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`\n\nfor Claude Code):\n\n```\n{\n  \"mcpServers\": {\n    \"nb2lite-agent\": {\n      \"type\": \"stdio\",\n      \"command\": \"docker\",\n      \"args\": [\"run\", \"-i\", \"--rm\", \"-e\", \"GEMINI_API_KEY\",\n               \"-v\", \"/absolute/path/to/images:/images\",\n               \"xbill9/nb2lite-mcp:latest\"]\n    }\n  }\n}\n```\n\nSet `GEMINI_API_KEY`\n\nin your environment, ask your agent to generate an image, and check your mounted `images/`\n\nfolder. (Remember: `-i`\n\nbut never `-t`\n\n— a TTY corrupts the protocol stream!)\n\nQuestions about MCP, ADK, or the Rust side? Drop them in the comments! 👇", "url": "https://wpnews.pro/news/build-one-ai-tool-server-call-it-from-three-different-agents-mcp-explained", "canonical_source": "https://dev.to/xbill/build-one-ai-tool-server-call-it-from-three-different-agents-mcp-explained-22l2", "published_at": "2026-07-15 20:35:35+00:00", "updated_at": "2026-07-15 21:10:28.702663+00:00", "lang": "en", "topics": ["artificial-intelligence", "developer-tools", "ai-tools", "ai-infrastructure", "large-language-models"], "entities": ["FastMCP", "Gemini", "Google", "Claude Code", "Model Context Protocol", "Interactions API", "ADK agent"], "alternates": {"html": "https://wpnews.pro/news/build-one-ai-tool-server-call-it-from-three-different-agents-mcp-explained", "markdown": "https://wpnews.pro/news/build-one-ai-tool-server-call-it-from-three-different-agents-mcp-explained.md", "text": "https://wpnews.pro/news/build-one-ai-tool-server-call-it-from-three-different-agents-mcp-explained.txt", "jsonld": "https://wpnews.pro/news/build-one-ai-tool-server-call-it-from-three-different-agents-mcp-explained.jsonld"}}