docs
Cookbooks

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

WhatValue
nima keyA 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 keyA Gemini API key, as GOOGLE_API_KEY in the agent's .env file.
Python3.10 or later.
Code searchsearch_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 mcp

Scaffold an agent folder. adk create writes agent.py, .env and __init__.py.

adk create nima_agent

Put 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 web

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

ItemWhat ADK says
StreamableHTTPConnectionParamsThe 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.
StreamableHTTPServerParamsAn alias of StreamableHTTPConnectionParams in the ADK source. Same class.
MCPToolsetDeprecated. Use McpToolset.
timeoutDefaults to 5.0 seconds. It bounds only connecting and initializing the session, not a tool call.
sse_read_timeoutDefaults 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_filterA list of tool names to include, or a predicate function.
header_providerAn optional McpToolset argument: a callable that takes a ReadonlyContext and returns headers. Use it when the key differs per user or per run.
adk webADK 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.py

What 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

SymptomCause
A tool the key cannot use fails with 403 insufficient_scopenima 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 missingTools 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 connecttimeout bounds only connecting and initializing the session, and defaults to 5 seconds. Raise it on a slow network.
adk web does not find the agentRun 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 answerNo match still returns 200. The body starts with No product entities found for:.
search_code returns empty resultsOnly 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 403The plan lacks Code Intelligence, or the key lacks knowledge:read.
Two identical signalscreate_signal has no idempotency key. Do not retry it blindly.
429 under loadEach 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.

On this page