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

# Claude API

> Connect the Caylex Navigator to Anthropic's Messages API with a customer-managed MCP client or Anthropic's hosted connector.

The Caylex Navigator is a remote [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that exposes a compact set of meta-tools to Claude. You can connect it to Anthropic's Messages API in three ways:

<CardGroup cols={3}>
  <Card title="FastMCP client" icon="star" href="#fastmcp-client-recommended">
    **Recommended.** Keep your existing Messages API loop and control exactly when MCP tools are refreshed.
  </Card>

  <Card title="Anthropic tool runner" icon="arrows-rotate" href="#anthropic-tool-runner">
    Let Anthropic's Python SDK run the tool-use loop while your application owns the MCP connection.
  </Card>

  <Card title="Hosted MCP connector" icon="cloud" href="#anthropic-hosted-mcp-connector">
    The shortest integration, but Anthropic may cache MCP tool definitions across requests and chats.
  </Card>
</CardGroup>

## Which approach should I use?

* Use the **FastMCP client** if you already have a custom agent harness or need fresh tool definitions after users authenticate. This is the recommended production approach.
* Use the **Anthropic tool runner** if you want a customer-managed MCP connection but do not already have a tool-use loop.
* Use the **hosted MCP connector** for the smallest proof of concept, when delayed tool-definition refreshes are acceptable.

<Note>
  `anthropic.beta.messages.tool_runner` is part of the regular `anthropic` Python SDK. It is **not** the [Claude Agent SDK](/integration/claude-sdk). The tool runner is a client-side convenience wrapper around repeated Messages API calls; the Agent SDK is a separate, higher-level agent harness with built-in tools, hooks, permissions, and subagents.
</Note>

## Common authentication

All three approaches below use a bearer token that packs the Caylex Navigator API key, end-user email, and chat session ID into one string:

* Token = `base64url(JSON{"api_key": "...", "user_email": "...", "session_id": "..."})` (padding optional)
* `api_key` and `user_email` are required.
* `session_id` is optional but strongly encouraged. It groups a chat's tool calls into one session in Caylex history and analytics.
* Send the result as `Authorization: Bearer <token>`.

```python theme={null}
import base64
import json


def build_caylex_bearer_token(
    api_key: str,
    user_email: str,
    session_id: str,
) -> str:
    payload = {
        "api_key": api_key,
        "user_email": user_email,
        "session_id": session_id,
    }
    data = json.dumps(payload).encode("utf-8")
    return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
```

<Warning>
  The packed token is not signed or encrypted. Your Navigator API key is the real credential and is validated on every request. Build the token server-side and treat it like an API key.
</Warning>

### Group tool calls by chat session

Generate one UUID when a chat begins and reuse it for every request in that chat:

```python theme={null}
import uuid

chat_session_id = str(uuid.uuid4())
```

The value must be a valid UUID. Start a new UUID for each new chat.

## Prerequisites

* Python 3.10+
* A Caylex Navigator Instance with an API key ([create one here](/platform/navigators))
* Users authenticated with your project's servers ([set up auth links](/auth/auth-links))
* These environment variables:

```bash theme={null}
export CAYLEX_API_KEY="ck_abc123.your-secret-key"
export CAYLEX_USER_EMAIL="user@example.com"
export ANTHROPIC_API_KEY="your-anthropic-key"
```

### System prompt and conversation messages

Anthropic accepts the system prompt through the top-level `system` parameter. Do not add a message with `role: "system"` — the `messages` array contains only `user` and `assistant` turns.

```python theme={null}
SYSTEM_PROMPT = """
You are a helpful assistant. Use the available Caylex tools when needed.
"""

user_message = "..."  # Supplied by your application
conversation_messages = [
    {"role": "user", "content": user_message},
]

response = await client.messages.create(
    model="claude-opus-4-8",
    max_tokens=2048,
    system=SYSTEM_PROMPT,
    messages=conversation_messages,
    tools=tools,
)
```

For subsequent turns, preserve the existing `conversation_messages` and append the new user message. The examples below show one user turn plus any tool-use iterations it triggers.

## FastMCP client (recommended)

This approach uses FastMCP's lightweight client-only package to call `tools/list` and `tools/call` directly. You pass the freshly discovered tools to the regular Messages API and keep control of your existing agent loop.

<Card title="Using a coding agent? Grab the Agent Skill" icon="wand-magic-sparkles" href="/integration/claude-api-skill">
  A ready-made SKILL.md for Claude Code, Cursor, and similar coding agents that implements this integration in your existing Messages API harness.
</Card>

Install the dependencies:

```bash theme={null}
pip install anthropic "fastmcp-slim[client]"
```

<Tip>
  `fastmcp-slim[client]` uses the normal `from fastmcp import Client` import while avoiding FastMCP's server framework and other server-side dependencies.
</Tip>

```python main.py theme={null}
import asyncio
import json
import os
import uuid

from anthropic import AsyncAnthropic
from fastmcp import Client as FastMCPClient
from fastmcp.client.transports import StreamableHttpTransport
from mcp.types import TextContent

SYSTEM_PROMPT = """
You are a helpful assistant. Use the available Caylex tools when needed.
"""


def build_caylex_bearer_token(
    api_key: str,
    user_email: str,
    session_id: str,
) -> str:
    import base64

    payload = {
        "api_key": api_key,
        "user_email": user_email,
        "session_id": session_id,
    }
    data = json.dumps(payload).encode("utf-8")
    return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")


def mcp_result_to_text(result) -> str:
    """Convert an MCP tool result into text for an Anthropic tool_result."""
    if result.structured_content is not None:
        return json.dumps(result.structured_content, default=str)

    parts: list[str] = []
    for block in result.content:
        if isinstance(block, TextContent):
            parts.append(block.text)
        else:
            parts.append(json.dumps(block.model_dump(mode="json"), default=str))
    return "\n".join(parts)


async def main() -> None:
    # Create this once per chat and reuse it for every user turn.
    chat_session_id = str(uuid.uuid4())
    token = build_caylex_bearer_token(
        api_key=os.environ["CAYLEX_API_KEY"],
        user_email=os.environ["CAYLEX_USER_EMAIL"],
        session_id=chat_session_id,
    )

    transport = StreamableHttpTransport(
        "https://navigator.caylex.ai/mcp",
        headers={"Authorization": f"Bearer {token}"},
    )
    mcp = FastMCPClient(transport)
    claude = AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

    user_message = input("User: ")
    conversation_messages = [
        {
            "role": "user",
            "content": user_message,
        }
    ]

    async with mcp:
        # Fetch a current tools/list response directly from Caylex before this
        # user turn. In a multi-turn chat, repeat this before each user turn.
        mcp_tools = await mcp.list_tools()
        claude_tools = [
            {
                "name": tool.name,
                "description": tool.description or "",
                "input_schema": tool.inputSchema,
            }
            for tool in mcp_tools
        ]

        while True:
            response = await claude.messages.create(
                model="claude-opus-4-8",
                max_tokens=2048,
                system=SYSTEM_PROMPT,
                messages=conversation_messages,
                tools=claude_tools,
            )
            conversation_messages.append(
                {"role": "assistant", "content": response.content}
            )

            tool_uses = [
                block for block in response.content if block.type == "tool_use"
            ]
            if not tool_uses:
                for block in response.content:
                    if block.type == "text":
                        print(block.text)
                break

            tool_results = []
            for tool_use in tool_uses:
                result = await mcp.call_tool(
                    tool_use.name,
                    dict(tool_use.input),
                    raise_on_error=False,
                )
                tool_results.append(
                    {
                        "type": "tool_result",
                        "tool_use_id": tool_use.id,
                        "content": mcp_result_to_text(result),
                        "is_error": bool(result.is_error),
                    }
                )

            conversation_messages.append(
                {"role": "user", "content": tool_results}
            )


if __name__ == "__main__":
    asyncio.run(main())
```

### Refresh after authentication

The tool definitions passed to one Claude tool loop remain fixed for that loop. If `get_authentication_status_and_link` returns a link and the user authenticates:

1. Finish the current agent turn after presenting the link.
2. Before the user's next turn, call `mcp.list_tools()` again.
3. Rebuild `claude_tools` from the returned list.
4. Continue with the same `chat_session_id` and conversation history.

This gives Claude the latest `suggest_tools` description and authenticated-server list without relying on a third-party MCP catalog cache.

## Anthropic tool runner

This option also owns the MCP connection, but uses Anthropic's beta tool runner to execute the tool-use loop automatically. It is convenient if you do not already have a custom loop.

Install:

```bash theme={null}
pip install "anthropic[mcp]" httpx
```

```python main.py theme={null}
import asyncio
import os
import uuid

import httpx
from anthropic import AsyncAnthropic
from anthropic.lib.tools.mcp import async_mcp_tool
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client

SYSTEM_PROMPT = """
You are a helpful assistant. Use the available Caylex tools when needed.
"""


# Reuse build_caylex_bearer_token from Common authentication above.


async def main() -> None:
    chat_session_id = str(uuid.uuid4())
    token = build_caylex_bearer_token(
        api_key=os.environ["CAYLEX_API_KEY"],
        user_email=os.environ["CAYLEX_USER_EMAIL"],
        session_id=chat_session_id,
    )
    claude = AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
    user_message = input("User: ")
    conversation_messages = [
        {"role": "user", "content": user_message},
    ]

    async with httpx.AsyncClient(
        headers={"Authorization": f"Bearer {token}"},
        timeout=httpx.Timeout(30, read=300),
        follow_redirects=True,
    ) as http_client:
        async with streamable_http_client(
            "https://navigator.caylex.ai/mcp",
            http_client=http_client,
        ) as streams:
            # Compatible with the stable MCP SDK's 3-tuple and the v2 2-tuple.
            read_stream, write_stream = streams[0], streams[1]

            async with ClientSession(read_stream, write_stream) as mcp:
                await mcp.initialize()
                tools_result = await mcp.list_tools()

                runner = claude.beta.messages.tool_runner(
                    model="claude-opus-4-8",
                    max_tokens=2048,
                    max_iterations=20,
                    system=SYSTEM_PROMPT,
                    messages=conversation_messages,
                    tools=[
                        async_mcp_tool(tool, mcp)
                        for tool in tools_result.tools
                    ],
                )

                final_message = None
                async for message in runner:
                    final_message = message

                if final_message is None:
                    raise RuntimeError("Claude returned no message")

                for block in final_message.content:
                    if block.type == "text":
                        print(block.text)


if __name__ == "__main__":
    asyncio.run(main())
```

`async_mcp_tool` copies each MCP tool's name, description, and input schema into an Anthropic tool, then forwards Claude's calls to `ClientSession.call_tool()`. The tool runner appends results and repeats Messages API calls until Claude returns a final answer.

<Note>
  This still uses Anthropic's regular Python SDK and Messages API. It does not install or invoke the Claude Agent SDK. Because the tool runner owns the inner tool loop, applications with custom retries, streaming, approvals, or persistence may prefer the FastMCP approach above.
</Note>

## Anthropic hosted MCP connector

Anthropic's hosted [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) performs MCP discovery and execution inside Anthropic's infrastructure. It requires the least code because your application does not run an MCP client.

Install:

```bash theme={null}
pip install anthropic
```

```python main.py theme={null}
import os
import uuid

import anthropic

SYSTEM_PROMPT = """
You are a helpful assistant. Use the available Caylex tools when needed.
"""


# Reuse build_caylex_bearer_token from Common authentication above.


chat_session_id = str(uuid.uuid4())
token = build_caylex_bearer_token(
    api_key=os.environ["CAYLEX_API_KEY"],
    user_email=os.environ["CAYLEX_USER_EMAIL"],
    session_id=chat_session_id,
)

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
user_message = input("User: ")
conversation_messages = [
    {"role": "user", "content": user_message},
]

response = client.beta.messages.create(
    model="claude-opus-4-8",
    max_tokens=2048,
    system=SYSTEM_PROMPT,
    messages=conversation_messages,
    mcp_servers=[
        {
            "type": "url",
            "url": "https://navigator.caylex.ai/mcp",
            "name": "caylex",
            "authorization_token": token,
        }
    ],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "caylex"}],
    betas=["mcp-client-2025-11-20"],
)

for block in response.content:
    if block.type == "text":
        print(block.text)
    elif block.type == "mcp_tool_use":
        print(f"called {block.name} with {block.input}")
    elif block.type == "mcp_tool_result":
        print(f"result (is_error={block.is_error}): {block.content}")
```

<Warning>
  Anthropic's hosted connector may cache the model-facing MCP tool catalog independently of your chat messages. A new chat does not always force a fresh `tools/list` request, and there is currently no supported API option to force a refresh.

  This matters because Caylex tool descriptions include live, user-specific information such as the authenticated-server list in `suggest_tools`. After a user authenticates, Claude may temporarily see an older server list or an older catalog that omits `get_authentication_status_and_link`.

  Use the FastMCP or tool-runner approach when fresh tool definitions are required.
</Warning>

<Note>
  The hosted connector requires `client.beta.messages.create`, an `mcp_toolset`, and the `mcp-client-2025-11-20` beta header. Its tool events use `mcp_tool_use` / `mcp_tool_result`; customer-managed integrations use ordinary `tool_use` / `tool_result` blocks.
</Note>

## Troubleshooting

<AccordionGroup>
  <Accordion title="The server list or authentication tool is stale">
    If you use Anthropic's hosted connector, the model-facing MCP catalog may be cached across requests or chats. There is currently no supported force-refresh parameter. Switch to a customer-managed MCP client, or wait for Anthropic's catalog cache to refresh.

    With FastMCP, call `mcp.list_tools()` before each user turn and rebuild the Anthropic tool definitions. With the tool runner, start the next turn with a fresh `list_tools()` result.
  </Accordion>

  <Accordion title="Invalid request parameters at initialize">
    Authentication likely failed. Confirm that the token decodes to the correct `api_key`, `user_email`, and UUID `session_id`, and that the API key belongs to the same environment as the Navigator URL.
  </Accordion>

  <Accordion title="Tool calls to a server fail with an auth error">
    The `user_email` packed into the token must exactly match the email the user authenticated with through an [Auth Link](/auth/auth-links).
  </Accordion>

  <Accordion title="Tool calls from one chat appear as separate sessions">
    Generate one UUID when the chat begins and include it as `session_id` in every bearer token for that chat. Do not generate a new UUID for each MCP request.
  </Accordion>

  <Accordion title="An extra argument I sent was ignored">
    The Navigator ignores unknown top-level arguments instead of failing the whole call. It reports them in response `_meta` under `caylex/ignored_arguments`, including the field names and reason.
  </Accordion>
</AccordionGroup>

## Further reading

* [Claude API Agent Skill](/integration/claude-api-skill) — a SKILL.md for coding agents that implements the FastMCP approach
* [FastMCP client-only package](https://gofastmcp.com/clients/client-only-package)
* [FastMCP Client](https://gofastmcp.com/clients/client)
* [FastMCP client transports](https://gofastmcp.com/clients/transports)
* [Anthropic tool runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner)
* [Anthropic hosted MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector)
* [Claude Agent SDK](/integration/claude-sdk)
* [Connecting Your Agent](/integration/connecting)
* [Server Authentication](/auth/server-authentication)
