Google ADK and Gemini
How do I give a Google ADK LlmAgent running Gemini nima's MCP tools over Streamable HTTP with a tool filter, and what is the REST fallback?
By the end of this page you have a Python agent built with Google's Agent
Development Kit (ADK), running a Gemini model. It connects to
https://mcp.getnima.io/mcp with a personal API key, loads only the nima read
tools you name, and calls ask_knowledge_graph and search_code before it
answers. You can run it in the ADK dev UI with adk web or headless with a
Runner. If you would rather not speak MCP, the last sections do the same with
two plain HTTP calls.
The nima MCP server is on for every workspace. search_code and
blast_radius are declared under the Code Intelligence plan feature: a
workspace whose plan lacks it can get 403 INSUFFICIENT_PERMISSIONS with
details.feature set to code_intelligence. Over MCP that 403 comes back as
an error tool result. Handle it in the agent.
Prerequisites for the ADK agent
| What | Value |
|---|---|
| nima key | A personal API key (nima_…) from Settings, then API keys. The form ticks knowledge:read and signals:read by default, which covers every tool in the first agent below. Tick signals:write only for file a signal from the agent. |
| Gemini key | A Gemini API key, as GOOGLE_API_KEY in the agent's .env file. |
| Python | 3.10 or later. |
| Code search | search_code and blast_radius are declared under the Code Intelligence plan feature. See the callout above. |
Install ADK and the MCP client library.
pip install google-adk mcpScaffold an agent folder. adk create writes agent.py, .env and
__init__.py.
adk create nima_agentPut the Gemini key in nima_agent/.env and the nima key in your shell.
# nima_agent/.env
GOOGLE_API_KEY="YOUR_API_KEY"export NIMA_API_KEY="nima_…"Connect an ADK agent to nima's MCP server
To connect an ADK agent to nima, use McpToolset with
StreamableHTTPConnectionParams, both imported from
google.adk.tools.mcp_tool, and put the bearer key in the headers argument.
Replace nima_agent/agent.py with this file.
# nima_agent/agent.py
import os
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
NIMA_API_KEY = os.environ.get("NIMA_API_KEY")
if not NIMA_API_KEY:
raise RuntimeError("Set NIMA_API_KEY to a personal nima API key (nima_...).")
# Tools this agent may use. A default key (knowledge:read + signals:read) can
# call all of them. nima lists every consent-scope tool to every key, so this
# filter, not the key's scopes, decides what the model sees.
READ_TOOLS = [
"ask_knowledge_graph",
"search_code",
"blast_radius",
"search_signals",
"get_signal",
"list_beacons",
"get_beacon",
]
nima_tools = McpToolset(
connection_params=StreamableHTTPConnectionParams(
url="https://mcp.getnima.io/mcp",
headers={"Authorization": f"Bearer {NIMA_API_KEY}"},
),
tool_filter=READ_TOOLS,
)
root_agent = LlmAgent(
model="gemini-flash-latest",
name="nima_context_agent",
description="Answers engineering questions with context from nima.",
instruction=(
"Before you answer a question about the product or its code, call "
"ask_knowledge_graph with the feature name to learn why it exists and who "
"asked for it, then call search_code to learn where it lives. search_code "
"reports the commit each repository was indexed at as lastCommitSha: say "
"so when it may be behind. Quote what the tools return. If they return "
"nothing, say nothing was found; never conclude that nothing exists."
),
tools=[nima_tools],
)Run it from the folder that contains nima_agent/:
adk webADK settings that matter for the nima connection
These are the McpToolset and StreamableHTTPConnectionParams details this
page relies on, as ADK's MCP documentation and source describe them.
| Item | What ADK says |
|---|---|
StreamableHTTPConnectionParams | The class ADK's MCP tools page uses for a Streamable HTTP server. Fields: url, headers, timeout, sse_read_timeout, terminate_on_close, httpx_client_factory. |
StreamableHTTPServerParams | An alias of StreamableHTTPConnectionParams in the ADK source. Same class. |
MCPToolset | Deprecated. Use McpToolset. |
timeout | Defaults to 5.0 seconds. It bounds only connecting and initializing the session, not a tool call. |
sse_read_timeout | Defaults to 300 seconds. ADK uses it as the read timeout while a tool call runs. nima cuts a tool call off at 60 seconds, so the default needs no change. |
tool_filter | A list of tool names to include, or a predicate function. |
header_provider | An optional McpToolset argument: a callable that takes a ReadonlyContext and returns headers. Use it when the key differs per user or per run. |
adk web | ADK documents it as not meant for production deployments. |
What nima does when the key lacks a tool's scope
With an API key, nima lists every tool behind its eight consent scopes in
tools/list, including tools whose scope the key does not hold. tool_filter
therefore loads create_signal even for a key without signals:write, and the
call fails when the model makes it.
A consent-scope tool the key cannot use comes back as an HTTP 403 with
insufficient_scope on the MCP call, not as a quiet omission. Create a key
with the scope the tool needs. Tools that need operations:write or
integrations:write do not appear at all unless the key holds that scope, and
those keys come only from the Slack /nima mcp-key command. Leave them out of
tool_filter.
Run the ADK agent headless with a Runner
Outside adk web, drive the same agent with a Runner, as ADK's MCP page
shows, and close the toolset when you are done. Save this next to the
nima_agent/ folder, not inside it.
# run.py
import asyncio
import os
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from nima_agent.agent import nima_tools, root_agent
APP_NAME = "nima_context_app"
USER_ID = "dev"
async def main() -> None:
if not os.environ.get("GOOGLE_API_KEY"):
raise RuntimeError("Set GOOGLE_API_KEY to a Gemini API key.")
session_service = InMemorySessionService()
session = await session_service.create_session(
state={}, app_name=APP_NAME, user_id=USER_ID
)
runner = Runner(
app_name=APP_NAME,
agent=root_agent,
artifact_service=InMemoryArtifactService(),
session_service=session_service,
)
question = "Why was the upload size limit added, and where is it enforced?"
content = types.Content(role="user", parts=[types.Part(text=question)])
try:
async for event in runner.run_async(
session_id=session.id, user_id=session.user_id, new_message=content
):
print(f"Event received: {event}")
finally:
await nima_tools.close()
if __name__ == "__main__":
asyncio.run(main())export GOOGLE_API_KEY="YOUR_API_KEY"
python run.pyWhat the agent gets back from ask_knowledge_graph and search_code
The agent makes two calls. The first goes to the knowledge graph:
{
"name": "ask_knowledge_graph",
"args": { "q": "upload size limit", "limit": 3 }
}The tool answers in markdown, trimmed here. Each entity line is indented two spaces in this sample only. The real answer starts it at the left margin.
Knowledge graph results for: "upload size limit"
### Upload size limit (feature) [implemented] · id: 7c21e5d4-9a0b-4f3e-8d61-2b5f0e9c4a17
References: 9 | Similarity: 0.742
Summary: Per-plan ceiling on a single uploaded file, checked before storage.
Facts:
- reason: large uploads stalled the import worker (source: SIGNAL:9f3c1b2e-4d5a-4c8b-9e10-7a2f6c4d1b33 [customer], learned 2026-05-14)
- owner: Platform (source: SPEC:5b7e2d90-1a4c-4e6f-8d3b-0c9a7f2e1b64, learned 2026-05-20)
Relations:
- ← Northwind (customer) requested this — "asked for a clear error instead of a timeout"The [implemented] label is the entity's modality: evidence that it is in the
code. proposal means asked for or specified and never confirmed built,
problem means a report of something wrong, and fixed means something
reported as broken now has fix evidence. Read the labels before you quote a
fact as shipped. The full answer format is on Knowledge graph
API.
The second call goes to code search:
{
"name": "search_code",
"args": { "q": "where is the upload size limit enforced", "limit": 5 }
}The tool answers in JSON, trimmed here:
{
"query": "where is the upload size limit enforced",
"repositories": [
{
"id": "3f8b0a41-6c2d-4f19-9ab7-15d0e2c7a884",
"repoFullName": "acme/orbit",
"indexStatus": "ready",
"lastCommitSha": "b41c9de2f7a1c05b8e3d6410a9f2c7d3e5b81a60",
"lastSyncedAt": "2026-09-21T09:14:02.331Z",
"indexWarnings": []
}
],
"droppedRepositories": [],
"results": [
{
"repositoryId": "3f8b0a41-6c2d-4f19-9ab7-15d0e2c7a884",
"repoFullName": "acme/orbit",
"filePath": "src/upload/upload.guard.ts",
"score": 0.4128,
"summary": "Rejects an upload whose size is over the workspace ceiling before the file reaches storage.",
"matchedChannels": ["keyword", "symbol", "vector"],
"match": "semantic",
"symbols": [
{
"name": "UploadSizeGuard",
"kind": "class",
"signature": null,
"lineStart": 18,
"lineEnd": 64
}
]
}
]
}Before you trust the structure, check that lastCommitSha matches the commit
you are working on. The index follows each repository's default branch only.
search_code never returns source code: read the file from your checkout at
the path and lines the hit gives. To see what a change to that file reaches,
have the agent call blast_radius (already in READ_TOOLS) with paths. See Knowledge
tools.
File a signal from the ADK agent
create_signal files one signal into the workspace. It needs a key with
signals:write. Add the tool to the filter and tell the agent when to use it.
Everything else in agent.py stays the same.
nima_tools = McpToolset(
connection_params=StreamableHTTPConnectionParams(
url="https://mcp.getnima.io/mcp",
headers={"Authorization": f"Bearer {NIMA_API_KEY}"},
),
tool_filter=READ_TOOLS + ["create_signal"],
)
root_agent = LlmAgent(
model="gemini-flash-latest",
name="nima_context_agent",
description="Answers engineering questions with context from nima.",
instruction=(
"Before you answer, call ask_knowledge_graph and search_code. "
"When you find a bug or a gap that nobody has reported, call search_signals "
"first. If nothing matches, file it once with create_signal: content in plain "
"words, sourceLabel 'adk agent', and the affected productAreas."
),
tools=[nima_tools],
)The call the agent makes:
{
"name": "create_signal",
"args": {
"content": "Uploads over the plan ceiling fail in upload.guard.ts with a generic 500 instead of a size error.",
"sourceLabel": "adk agent",
"productAreas": ["upload"]
}
}The argument limits are in Pipeline tools.
The new signal comes back with status: "pending". nima classifies it after the
write. There is no idempotency key, so a retried call files a second signal.
Use nima over REST instead of MCP in an ADK agent
Every tool is one HTTP route with the same bearer. ADK accepts plain Python
functions as tools, so two functions that use requests can replace the
toolset. You do not need an MCP client.
pip install google-adk requests# nima_agent/agent.py
import os
import requests
from google.adk.agents import LlmAgent
API = "https://api.getnima.io"
NIMA_API_KEY = os.environ.get("NIMA_API_KEY")
if not NIMA_API_KEY:
raise RuntimeError("Set NIMA_API_KEY to a personal nima API key (nima_...).")
HEADERS = {"Authorization": f"Bearer {NIMA_API_KEY}"}
def ask_nima(question: str) -> str:
"""Asks nima's knowledge graph why something exists and who asked for it. Returns markdown."""
response = requests.get(
f"{API}/v1/knowledge-graph/ask",
headers=HEADERS,
params={"q": question, "limit": 5},
timeout=60,
)
response.raise_for_status()
return response.text
def search_nima_code(query: str) -> dict:
"""Finds the files, symbols and line ranges in the indexed repositories that match the query."""
response = requests.get(
f"{API}/v1/code-intel/search",
headers=HEADERS,
params={"q": query, "limit": 5},
timeout=60,
)
response.raise_for_status()
return response.json()
root_agent = LlmAgent(
model="gemini-flash-latest",
name="nima_rest_agent",
description="Answers engineering questions with context from nima over REST.",
instruction=(
"Before you answer, call ask_nima for why and who, then search_nima_code "
"for where. Quote what they return. If they return nothing, say so."
),
tools=[ask_nima, search_nima_code],
)
if __name__ == "__main__":
print(ask_nima("upload size limit"))python nima_agent/agent.py checks the key and the route without calling
Gemini. adk web and the run.py above work as they are. In run.py, remove
the close() call and the nima_tools import, because there is no toolset to
close.
To file a signal over REST, POST /v1/signals with a JSON body of the same
fields as create_signal. See Signals.
Google ADK and nima troubleshooting
| Symptom | Cause |
|---|---|
A tool the key cannot use fails with 403 insufficient_scope | nima lists every consent-scope tool to every key, so tool_filter loaded it. Create a key that holds the tool's scope. |
| A tool that starts, steers or administers work is missing | Tools behind operations:write or integrations:write appear only for a key that holds that scope, and those keys come only from Slack /nima mcp-key. |
| The toolset fails to connect | timeout bounds only connecting and initializing the session, and defaults to 5 seconds. Raise it on a slow network. |
adk web does not find the agent | Run it from the parent folder, and define root_agent at module level, synchronously. ADK's MCP page requires a synchronous definition for deployment. |
ask_knowledge_graph "succeeds" with no answer | No match still returns 200. The body starts with No product entities found for:. |
search_code returns empty results | Only ready indexes are searched, and droppedRepositories names the ones whose search failed or timed out. Empty results do not prove the code is absent. |
search_code answers 403 | The plan lacks Code Intelligence, or the key lacks knowledge:read. |
| Two identical signals | create_signal has no idempotency key. Do not retry it blindly. |
429 under load | Each MCP tool call spends two rate-limit buckets, so expect about 100 tool calls per minute per client IP. Keys behind a shared address share a bucket. See Rate limits. |