Google's Agent Development Kit (ADK) is a code-first framework: you write a Python module, declare an Agent, give it tools, and run it. That is great when one developer owns one agent. It gets harder when a team needs dozens of agents, changes their instructions every week, and wants to know who ran what, on which model, for how many tokens.
apowerb is an open source (Apache-2.0) platform we built for that second case. Agents are stored as database records, created from a web UI or a REST API, and apowerb builds the ADK objects for you. This post walks through creating an ADK agent with an MCP server attached, using nothing but HTTP calls, and shows what came back on a real run.
From a record to an ADK agent
Each agent is a row: a name, an instruction, a model string, a list of tools, optional sub-agents and MCP servers. A function called to_agent() turns that row into an ADK object:
-
llmagents become anLlmAgent;sequential,parallelandloopagents become aSequentialAgent,ParallelAgentorLoopAgent, with their sub-agents built recursively. - The model string is wrapped in ADK's
LiteLlmmodel, so any LiteLLM provider works. Gemini models take thegemini/prefix. - Each entry in
mcp_serversbecomes an ADKMcpToolset.
When an agent changes, apowerb regenerates its ADK module and the ADK FastAPI app mounted inside apowerb picks it up. Runs go through the standard ADK runner, so sessions, events and traces are plain ADK.
1. Get a token
import requests
BASE_URL = "http://localhost:8000"
EMAIL = "you@example.com"
response = requests.post(
f"{BASE_URL}/api/auth/token",
data={"username": EMAIL, "password": "YOUR_PASSWORD"},
)
response.raise_for_status()
HEADERS = {"Authorization": f"Bearer {response.json()['access_token']}"}
2. Create an agent with an MCP server
For the test I used DeepWiki's public MCP server, which needs no account and answers questions about GitHub repositories.
agent = requests.post(
f"{BASE_URL}/api/agents",
headers=HEADERS,
json={
"agent_name": "docs_assistant",
"agent_description": "Answers questions with the tools of an MCP server",
"agent_instruction": "You are a helpful assistant. Use the MCP tools when they fit the request.",
"agent_model": "gemini/gemini-flash-latest",
"agent_model_params": {"model_api_key": "YOUR_GEMINI_API_KEY"},
"agent_type": "llm",
"agent_tools": [],
"mcp_servers": [
{"name": "my_mcp_server", "transport": "http", "url": "https://mcp.deepwiki.com/mcp"}
],
},
)
agent.raise_for_status()
Remote servers use the http transport by default. The stdio transport launches a local command, so it is off unless the operator sets MCP_STDIO_ENABLED=1, and only administrators can use it.
3. Run it
result = requests.post(
f"{BASE_URL}/api/adk/run",
headers=HEADERS,
json={
"agent_name": "docs_assistant",
"user_id": EMAIL,
"session_id": "session_001",
"new_message": {
"role": "user",
"parts": [{"text": "Use your tools: what is the GitHub repository google/adk-python about?"}],
},
},
)
events = result.json()
The response is the list of ADK events for the turn. On the real run, the events contained a functionCall to the MCP tool ask_wiki_question, followed by the model's answer:
The google/adk-python repository contains the Agent Development Kit (ADK), an open-source, code-first Python framework designed for building, evaluating, and deploying sophisticated AI agents.
Asked "Which tools do you have access to?", the same agent listed the three DeepWiki tools (ask_wiki_question, read_wiki_structure, read_wiki_contents) next to apowerb's own built-in tools.
The same body posted to /api/adk/run_sse streams the events as server-sent events. The full conversation and its execution trace are then available at /api/adk/sessions/{agent}/{user}/{session} and .../trace.
What the platform adds around ADK
ADK stays in charge of the agent loop. apowerb adds what a team needs around it:
- a web UI and REST API to create and edit agents without redeploying code;
- built-in tools for RAG, Text-to-SQL, BI charts, Google Workspace and Microsoft 365;
- scheduled runs and runs triggered by email;
- per-user token quotas and a per-session audit trail;
- self-hosting with Docker Compose or a Helm chart.
Try it
- Code: https://github.com/apowerb/apowerb
- Docs and quickstart: https://docs.apowerb.com/quickstart
pip install apowerb
Feedback and issues are welcome on GitHub.
Top comments (5)
Great walkthrough, David! Creating an agent with an MCP toolset and running it with nothing but HTTP calls is exactly the demo people need 🤩.
One improvement that would make it even more useful for real deployments: an example with an authenticated MCP server. DeepWiki is public, but most teams will plug in internal servers that need a token. The mcp_servers config already accepts headers, so a short snippet with an Authorization header, and a word on where that token is stored, would answer the first question readers will have. Same for the model key: the shared default key in .env keeps it out of the request body.
Two ideas for a follow-up: what the agent sees when the MCP server is slow or down (plus a raise_for_status() on the /run call), and the toolbox-db type, which connects agents to databases through MCP Toolbox. That one deserves its own post.
Thanks Anis, good points.
Authenticated MCP server. Yes,
headersis passed as-is to the MCP client (HTTP and SSE transports):Read
MCP_TOKENfrom your environment or secret manager on the client side, never hard-code it. On where apowerb keeps it: it is stored with the agent's MCP config, and I'd rather cover storage and rotation properly in a follow-up than hand-wave it here.Model key. Right: with
"agent_model": "thaink2/default", the agent uses the server'sDEFAULT_LLM_MODEL/DEFAULT_LLM_API_KEY, so there is no key in the request body and none stored on the agent.raise_for_status()on/run: fair, I'll add it. A slow or unavailable MCP server and thetoolbox-dbtype are noted for the next post.Before storage and rotation there is the question of whose token it is. In to_agent, load_mcp_servers passes the headers from the agent's mcp_servers config to McpToolset as stored, when the agent is built, so every run of that agent reaches the internal server with the same Authorization header, whoever started the run. The run carries a user_id and the MCP server never sees it. For a server that answers differently per user, everyone who can run the agent gets the token owner's access, and the server's audit log records the token rather than the person.
The same code already solves this for its own integrations. Both run routes set the invoker so that user-personal tools such as Outlook, Gmail and Drive resolve against the person running the agent rather than its owner, and the comment a few lines below the MCP call in to_agent says Google integration tokens are resolved lazily against the request-bound invoker instead of being pre-injected. Resolving the MCP Authorization header the same way, from the invoking user's credential at call time, would keep an internal server's permission checks meaningful and make rotation a per-user event rather than one secret shared on the agent.
Looking forward to the follow-up on token storage and rotation. That’s the first thing a security team will ask about, and showing how to rotate an MCP token across every agent that uses it would make a great post.
You gate stdio behind MCP_STDIO_ENABLED and admin-only, which handles the local-command risk. The remaining one with the
httptransport is that any user who can POST /api/agents now chooses a URL your server will connect to from inside your network. Pointing it at a cloud metadata address or an internal admin service turns the platform into a request forwarder, and the MCP client will happily send an initialize and tools/list to whatever answers. Is there an allowlist of hosts or a block on private and link-local ranges (checked after DNS resolution, since a hostname can resolve to 127.0.0.1)?The second thing worth recording per run is the tool list the model actually saw. Remote servers can change their descriptions between runs, and a trace that stores only the functionCall can't show which description led to that call. A hash of tools/list per run would let the audit trail answer that.
For a server that needs no account and no key you can also try
npx -y canli-mcp(I built it, MIT, runs locally, github.com/arhancanli/canlicapital): it exposes three front tools over 306, which keeps the tool list short enough that the model isn't choosing among hundreds. It is stdio, so it would need your admin flag.