> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nasiko.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect your agent to the MCP gateway

> Wire any agent, in any language or framework, to discover and call every tool a user has connected — with no hardcoded tool names.

Wire any agent — any language, any framework — to the Nasiko MCP gateway so it can discover and call every tool a user has connected, with zero hardcoded tool names.

If your agent runs on Nasiko, it doesn't need an MCP SDK, a special client library, or any per-connector code. It needs exactly two things at deploy time, and one small tool-calling loop. This page covers that, in whatever framework you're already using — LangChain, LangGraph, CrewAI, Google ADK, the raw OpenAI SDK, Anthropic's SDK, or a hand-rolled agent in Node, Rust, or Go.

<Note>
  For the router to work, each agent is given two environment variables at deploy time: `OPENAI_BASE_URL` (the gateway's address, not `api.openai.com`) and `OPENAI_API_KEY` — not a real provider key, but a signed ticket that says "I am agent X, acting on behalf of user Y." Your agent framework already reads both of these to construct its LLM client, so most of the wiring is already done for you — this page is about the **tool-calling** half, not the LLM half.
</Note>

## How it works

Every tool call your agent makes goes through one HTTP endpoint — `MCP_GATEWAY_URL` — using plain JSON-RPC 2.0. There is no MCP client SDK dependency: you `POST` a JSON body and read a JSON body back. The two methods you need are `tools/list` (what's available right now, for this user, for this agent) and `tools/call` (invoke one).

Authentication is a single header, `x-nasiko-agent-token` — a short-lived, per-request credential minted by the platform and forwarded to your agent on every inbound call. You read it once, and forward the exact same value on every outbound call you make to the gateway during that request.

<Warning>
  This token is **not** your agent's static API key. It's minted fresh per inbound request and expires in minutes. Never cache it across requests, never log its full value, and never forward it anywhere except the gateway.
</Warning>

## Quickstart

<Steps>
  <Step title="Read the gateway URL">
    Injected automatically into your agent's environment at deploy time — already includes the full path. If it's empty, your agent either isn't deployed with the gateway enabled, or was deployed before it was — skip tool-calling gracefully rather than erroring out.

    ```python theme={null}
    MCP_GATEWAY_URL = os.environ.get("MCP_GATEWAY_URL", "")  # already includes /api/mcp
    ```
  </Step>

  <Step title="Read the inbound delegation token">
    Every request that reaches your agent carries `x-nasiko-agent-token` as a header. Pull it out using whatever your framework exposes for inbound headers.

    <CodeGroup>
      ```python a2a-sdk theme={null}
      def get_token(context: RequestContext) -> str | None:
          call_context = context.call_context
          if not call_context:
              return None
          return call_context.state.get("headers", {}).get("x-nasiko-agent-token")
      ```

      ```python FastAPI / Starlette theme={null}
      from fastapi import Request

      async def handler(request: Request):
          token = request.headers.get("x-nasiko-agent-token")
      ```

      ```python Flask theme={null}
      from flask import request

      token = request.headers.get("x-nasiko-agent-token")
      ```

      ```javascript Express theme={null}
      app.post("/", (req, res) => {
        const token = req.headers["x-nasiko-agent-token"];
      });
      ```

      ```go Go theme={null}
      token := r.Header.Get("X-Nasiko-Agent-Token")
      ```
    </CodeGroup>

    If your framework doesn't expose inbound headers anywhere in the handler context, wrap it in middleware that copies them somewhere your handler can reach:

    ```python theme={null}
    class TokenMiddleware:
        def __init__(self, app): self.app = app
        async def __call__(self, scope, receive, send):
            if scope["type"] == "http":
                headers = dict(scope["headers"])
                scope["state"]["mcp_token"] = headers.get(b"x-nasiko-agent-token", b"").decode() or None
            await self.app(scope, receive, send)
    ```

    No token present? Skip tool access for this request — don't crash.
  </Step>

  <Step title="Call the gateway">
    One helper function covers both methods you'll ever need.

    ```python theme={null}
    async def mcp_call(method: str, params: dict | None, token: str) -> dict:
        body = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
        if params is not None:
            body["params"] = params
        async with httpx.AsyncClient(timeout=60.0) as client:
            resp = await client.post(MCP_GATEWAY_URL, headers={"x-nasiko-agent-token": token}, json=body)
            return resp.json()
    ```

    Never hardcode tool names — they're per-user and change whenever a connector is added or removed. Always call `tools/list` fresh. See [Protocol reference](#protocol-reference) for exact request and response shapes.
  </Step>

  <Step title="Merge into your LLM's tool list">
    Add two function-calling tools **alongside** whatever tools your agent already has — don't replace them.

    ```python theme={null}
    tool_defs = existing_tools + [
        {"type": "function", "function": {"name": "mcp_list_tools", "description": "List available MCP tools.", "parameters": {"type": "object", "properties": {}}}},
        {"type": "function", "function": {"name": "mcp_call_tool", "description": "Call an MCP tool by exact name from mcp_list_tools.", "parameters": {"type": "object", "properties": {
            "tool_name": {"type": "string"}, "arguments": {"type": "object"}
        }, "required": ["tool_name"]}}},
    ]
    ```

    Dispatch by name in whatever tool-execution code you already have — one branch, nothing else changes:

    ```python theme={null}
    if call.function.name == "mcp_list_tools":
        result = await mcp_call("tools/list", None, token)
    elif call.function.name == "mcp_call_tool":
        args = json.loads(call.function.arguments)
        result = await mcp_call("tools/call", {"name": args["tool_name"], "arguments": args.get("arguments", {})}, token)
    else:
        result = run_my_existing_tool(call)  # unchanged
    ```

    Name collision with a tool you already have? Rename either side — there's no fixed contract, `mcp_list_tools` and `mcp_call_tool` are just this page's convention.

    LLMs tend to refuse a tool instead of trying it when they're not 100% sure it's relevant. Push back in your system prompt:

    > "Always call mcp\_list\_tools first. If any tool plausibly relates to the request, call it with best-effort arguments — don't ask for clarification, don't skip it because you're unsure. Only say nothing's available after actually trying."
  </Step>

  <Step title="Bound the loop and handle timeouts">
    If you're not using a framework with a built-in agent loop, cap the manual one and make sure a mid-loop cutoff doesn't silently drop results.

    ```python theme={null}
    for _ in range(10):                       # cap the loop
        resp = await llm.chat.completions.create(model=model, messages=messages, tools=tool_defs)
        choice = resp.choices[0].message
        messages.append(choice.model_dump(exclude_none=True))
        if not choice.tool_calls:
            return choice.content
        for call in choice.tool_calls:
            result = dispatch(call)           # your logic from the previous step
            messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

    # hit cap mid-tool-use: don't drop results, force a final answer
    messages.append({"role": "user", "content": "Stop calling tools. Summarize what you found."})
    return (await llm.chat.completions.create(model=model, messages=messages)).choices[0].message.content
    ```

    ```python theme={null}
    try:
        result = await mcp_call(...)
    except httpx.TimeoutException:
        return "That tool call took too long and was cancelled."
    ```

    Use 30–60s or longer timeouts for gateway calls, not a short default — real connectors (Notion, Slack, and the like) can be genuinely slow. A caught-but-unmessaged timeout often logs as an empty string and looks to the user like a silent crash.

    If you're using LangChain, LangGraph, CrewAI, or Google ADK, skip this step entirely — see [Framework integrations](#framework-integrations) below, your framework already runs this loop for you.
  </Step>
</Steps>

## Protocol reference

Plain JSON-RPC 2.0 over a single endpoint (`MCP_GATEWAY_URL`), always `POST`, always HTTP 200 — a failed tool call is a JSON-RPC `error` object in the body, not an HTTP error status.

### Methods

| Method       | Params                  | Result                                                                    | Notes                                                                                                                                                                            |
| ------------ | ----------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `initialize` | —                       | Handshake info                                                            | Optional. Only needed if wrapping the gateway in a real MCP client SDK — skip it if calling the gateway directly as shown above.                                                 |
| `ping`       | —                       | `{}`                                                                      | Optional liveness check.                                                                                                                                                         |
| `tools/list` | —                       | `{"tools": [{"name", "description", "inputSchema"}]}`                     | Per-user, per-agent. Changes any time the user connects or disconnects a service, or your agent's permissions change. Always fetch fresh — never cache across turns or sessions. |
| `tools/call` | `{"name", "arguments"}` | `{"content": [...], "isError": bool}` or `{"error": {"code", "message"}}` | `name` must be the exact namespaced string from `tools/list` (`connectorId__toolName`) — treat it as opaque, never construct it yourself.                                        |

### Error codes

| Code      | Meaning                                     | What your agent should do                                                                          |
| --------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `-32000`  | Blocked by the user's own permission rules  | Tell the user it's blocked. Don't retry, don't silently substitute a different tool.               |
| `-32001`  | Needs human approval (an "ask"-stance tool) | Tell the user approval is pending. Don't retry-loop.                                               |
| `-32602`  | Missing or invalid delegation token         | A bug in your own header forwarding — not user-facing.                                             |
| `-32601`  | Unknown method                              | Check your `method` string.                                                                        |
| Any other | A real failure from the underlying service  | Report it honestly. Never fake success, never silently swap in a fallback the user didn't ask for. |

## Framework integrations

Pick your framework. In every case, the pattern is the same: wrap the two gateway calls as tools your framework already knows how to call, and add them to whatever tool list you already have.

### LangChain

```python theme={null}
from langchain_core.tools import tool

@tool
async def mcp_list_tools() -> dict:
    """List available MCP tools."""
    return await mcp_call("tools/list", None, token)

@tool
async def mcp_call_tool(tool_name: str, arguments: dict) -> dict:
    """Call an MCP tool by exact name from mcp_list_tools."""
    return await mcp_call("tools/call", {"name": tool_name, "arguments": arguments}, token)

agent_executor = AgentExecutor(
    agent=agent,
    tools=existing_tools + [mcp_list_tools, mcp_call_tool],
    max_iterations=10,                 # = the loop cap
    early_stopping_method="generate",  # = force a final answer instead of dropping results
)
```

### LangGraph

```python theme={null}
from langgraph.prebuilt import create_react_agent

graph = create_react_agent(model, tools=existing_tools + [mcp_list_tools, mcp_call_tool])
result = await graph.ainvoke({"messages": [...]}, config={"recursion_limit": 10})  # = the loop cap
```

`mcp_list_tools` and `mcp_call_tool` are the same `@tool`-decorated functions from the LangChain section above — LangGraph's prebuilt ReAct agent accepts the same tool objects.

### CrewAI

Same pattern as LangChain: wrap the two functions as `@tool`-decorated CrewAI tools, add them to the agent's `tools` list, and set `max_iter` on the agent to bound the loop.

### Google ADK

ADK derives a tool's schema from a plain Python function's type hints and docstring — no decorator needed, just pass the functions directly:

```python theme={null}
from google.adk.agents import Agent

async def mcp_list_tools() -> dict:
    """List available MCP tools."""
    return await mcp_call("tools/list", None, token)

async def mcp_call_tool(tool_name: str, arguments: dict) -> dict:
    """Call an MCP tool by exact name from mcp_list_tools.

    Args:
        tool_name: Exact namespaced tool name from mcp_list_tools.
        arguments: Arguments object for the tool call.
    """
    return await mcp_call("tools/call", {"name": tool_name, "arguments": arguments}, token)

root_agent = Agent(
    model="gemini-2.0-flash",
    tools=existing_tools + [mcp_list_tools, mcp_call_tool],
)
```

### OpenAI Agents SDK and raw SDK

See [Quickstart](#quickstart) above — the manual loop shown there *is* the raw-SDK pattern. If you're on the OpenAI Agents SDK specifically, wrap the two functions with `@function_tool` and add them to your `Agent`'s `tools` list the same way; the SDK's own `Runner` loop handles the rest.

### Anthropic Claude (native tool use)

Claude's native tool-use format isn't OpenAI-compatible: the schema field is `input_schema` (not `parameters`), and tool results go back as a `tool_result` content block, not a `role: tool` message.

```python theme={null}
tool_defs = existing_tools + [
    {"name": "mcp_list_tools", "description": "List available MCP tools.", "input_schema": {"type": "object", "properties": {}}},
    {"name": "mcp_call_tool", "description": "Call an MCP tool by exact name.", "input_schema": {"type": "object", "properties": {
        "tool_name": {"type": "string"}, "arguments": {"type": "object"}
    }, "required": ["tool_name"]}},
]

for block in response.content:
    if block.type == "tool_use":
        result = dispatch_by_name(block.name, block.input)   # same dispatch logic as the Quickstart
        messages.append({"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}
        ]})
```

### Node.js and TypeScript

No SDK dependency needed — a raw `fetch` call works with any Node framework or agent library:

```typescript theme={null}
async function mcpCall(method: string, params: object | null, token: string) {
  const body = { jsonrpc: "2.0", id: crypto.randomUUID(), method, ...(params ? { params } : {}) };
  const resp = await fetch(process.env.MCP_GATEWAY_URL!, {
    method: "POST",
    headers: { "x-nasiko-agent-token": token, "Content-Type": "application/json" },
    signal: AbortSignal.timeout(60_000),
    body: JSON.stringify(body),
  });
  return resp.json();
}
```

### Rust

```rust theme={null}
async fn mcp_call(client: &reqwest::Client, gateway_url: &str, method: &str, params: Option<Value>, token: &str) -> Value {
    let mut body = json!({ "jsonrpc": "2.0", "id": Uuid::new_v4().to_string(), "method": method });
    if let Some(p) = params {
        body["params"] = p;
    }
    client
        .post(gateway_url)
        .header("x-nasiko-agent-token", token)
        .timeout(std::time::Duration::from_secs(60))
        .json(&body)
        .send()
        .await
        .and_then(|r| r.json::<Value>())  // handle the Result properly in real code
        .unwrap_or_else(|e| json!({ "error": { "message": e.to_string() } }))
}
```

## Local testing

Test with `curl` before wiring up any agent code — this isolates "is my token and URL correct" from "is my agent code correct."

```bash theme={null}
curl -s -X POST "$MCP_GATEWAY_URL" \
  -H "x-nasiko-agent-token: $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"1","method":"tools/list"}' | jq
```

A non-empty `result.tools` array means your token and URL are both correct — any remaining problem is in your agent code, not the gateway. A `-32602` means your token is invalid, expired, or missing; grab a fresh one from a real inbound request rather than reusing an old value.

## Security

* Never log the full token value — log `token present: true/false` only.
* Never forward the token to a third-party service, or echo it back to the end user in a response.
* Never cache or persist the token — it's minted fresh per inbound request and expires in minutes by design.
* The token is the *only* credential this endpoint accepts. There's no separate API key to rotate or manage.

## Troubleshooting

| Symptom                                                    | Likely cause                                                                                                                             |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `tools/list` returns an empty array                        | Not a bug — the user has no connectors configured yet, or none granted to this specific agent. Check the connector settings.             |
| Every gateway call returns `-32602`                        | Token isn't being forwarded correctly, or you're reading the wrong header. Double-check you're reading `x-nasiko-agent-token` exactly.   |
| Agent works, then randomly "loses" tool access mid-session | You're caching the token across requests — re-read it from the inbound request every time.                                               |
| Agent ignores an obviously relevant tool                   | System prompt isn't pushing the model hard enough to try tools — see the prompt guidance in Quickstart step 4.                           |
| Blank or empty error message on a slow request             | Timeout too short, or the exception isn't caught explicitly — see Quickstart step 5.                                                     |
| Agent answers using stale or wrong tool names              | Tool list is being cached — always call `tools/list` fresh, every turn.                                                                  |
| `MCP_GATEWAY_URL` is empty in production                   | Agent was deployed before the gateway was enabled for it — redeploy, a restart alone won't pick up newly injected environment variables. |

## Checklist

Before calling your integration done, verify each of these against a real deployment:

* `MCP_GATEWAY_URL` missing — agent still answers plain questions, no crash
* Token missing — same, graceful skip
* `tools/list` returns tools when the user has a connector configured
* `tools/call` result reaches the LLM and is used in the final answer
* No connector for the topic — an honest "nothing available," never a made-up answer
* A bad tool call reports the real error, with no silent fallback to something else
* A slow tool produces a clean timeout message, not a blank crash
* Pre-existing tools still work unchanged, and the LLM can call one of yours and one gateway tool in the same turn

## Related

* [Connect an external MCP server](/mcp-hub/external-mcp-server) — register a managed toolkit or your own already-running MCP server as a connector your agents can use
* [Per-agent tool permissions](/onboarding/acl/user-agent-mcp) — control which connectors and tools an agent may call
* [A2A agents and frameworks](/adlc/a2a-agents) — how agents themselves are invoked and discovered on the platform
* [Agent development lifecycle](/adlc/overview) — end-to-end: scaffold, build, deploy, and version an agent
