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

# MCP starters

> Level 2 - Build MCP servers and integrate them with AI agents and MCP-compatible clients

MCP (Model Context Protocol) is an open standard that lets AI agents connect to external tools through a unified interface. Instead of writing custom integrations for every API, you build an **MCP server** that exposes your APIs as tools, and any MCP-compatible agent can discover and use them automatically.

This guide covers both sides of the MCP integration:

| Guide                     | What it covers                                                                                                  |
| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [MCP server](#mcp-server) | Build a pro-code MCP server with FastMCP, or expose APIs through Azure APIM using click-ops in the Azure Portal |
| [MCP client](#mcp-client) | Connect to an MCP server from different clients: FastMCP, Cursor, Claude Desktop, and Agno agents               |

***

# MCP server

There are two ways to create an MCP server:

<CardGroup cols={2}>
  <Card title="Pro-code with FastMCP" icon="code" href="#pro-code-mcp-server-with-fastmcp">
    Build a custom MCP server in Python using the FastMCP framework, starting from an OpenAPI spec or handwritten tool functions.
  </Card>

  <Card title="Click-ops with Azure APIM" icon="cloud" href="#expose-an-api-as-an-mcp-server-on-azure-apim">
    Expose existing APIs provisioned in Azure APIM as MCP servers directly from the Azure Portal without writing custom code.
  </Card>
</CardGroup>

***

## Pro-code MCP server with FastMCP

Build a production-ready MCP server using the [FastMCP](https://gofastmcp.com) Python framework. The `starter-mcp-server` repository provides a working reference implementation you can clone and extend.

<Card title="GitHub repository" icon="github" href="https://github.com/bb-ecos-agbs/starter-mcp-server">
  View source code, releases, and issues
</Card>

### Why FastMCP?

**FastMCP is the recommended way to build MCP servers in Python.** The official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) provides a low-level protocol implementation that requires manual handler registration, hand-crafted JSON Schema dictionaries, and transport boilerplate. FastMCP removes that complexity.

<CardGroup cols={2}>
  <Card title="Minimal boilerplate" icon="code">
    A working MCP server is 5 lines of code. Register any Python function as a tool with a single decorator.
  </Card>

  <Card title="Automatic schema generation" icon="wand-magic-sparkles">
    Type hints become JSON Schema, Python docstrings become tool descriptions, and you don't maintain schemas manually.
  </Card>

  <Card title="OpenAPI to MCP" icon="file-code">
    Auto-generate an MCP server from any OpenAPI specification, turning every REST endpoint into an MCP tool.
  </Card>

  <Card title="Multiple transports" icon="shuffle">
    STDIO, Streamable HTTP, and SSE with a single configuration change.
  </Card>
</CardGroup>

### Prerequisites

Install the following on your workstation:

* **Python 3.12+**: Managed via UV
* **UV Package Manager**: Modern Python package manager that replaces pip and poetry

### Quick start

Run the starter MCP server on your machine:

<Steps>
  <Step title="Clone and install UV">
    ```bash theme={"system"}
    git clone https://github.com/bb-ecos-agbs/starter-mcp-server.git
    cd starter-mcp-server

    # Install UV (macOS)
    brew install uv

    # Install UV (Linux/WSL)
    curl -LsSf https://astral.sh/uv/install.sh | sh
    ```
  </Step>

  <Step title="Set up environment">
    ```bash theme={"system"}
    # Create virtual environment
    uv venv --python 3.12
    source .venv/bin/activate  # macOS/Linux
    # Or .venv\Scripts\activate  # Windows

    # Copy environment template
    cp .env.example .env
    ```
  </Step>

  <Step title="Configure environment">
    Edit `.env` with your values:

    ```ini theme={"system"}
    # Base URL of the upstream API that this MCP server proxies
    API_BASE_URL=https://api.escuelajs.co

    # Server configuration
    MCP_TRANSPORT=streamable-http  # "stdio" | "streamable-http"
    MCP_SERVER_PORT=8557
    MCP_SERVER_HOST=0.0.0.0
    MCP_STATELESS_HTTP=false  # "false" = stateful sessions (required for elicitation), "true" = no session state between requests

    # Optional: VPN and web proxy settings (httpx reads these automatically)
    # HTTP_PROXY=http://webproxy.infra.backbase.cloud:8888
    # HTTPS_PROXY=http://webproxy.infra.backbase.cloud:8888
    # NO_PROXY=localhost,127.0.0.1
    ```
  </Step>

  <Step title="Install dependencies and run">
    ```bash theme={"system"}
    uv sync

    # Run the server
    uv run python -m src.main
    ```

    The MCP server runs at `http://localhost:8557/mcp`.
  </Step>
</Steps>

<Warning>
  **VPN and web proxy**: if your upstream API is behind a corporate network, uncomment, and configure the proxy settings in `.env`. For setup instructions, see the **[Onboarding guide](/agentic-ai/getting-started/onboarding)** on Confluence.
</Warning>

### Project structure

The `starter-mcp-server` repository contains the following directories and files:

```text theme={"system"}
starter-mcp-server/
├── .github/                         # CI/CD workflows
│   └── workflows/
│       ├── pull-request-check.yaml
│       ├── build-publish.yaml
│       ├── release-draft.yaml
│       ├── release.yaml
│       └── repository-provisioning.yaml
├── src/
│   ├── main.py                      # Entry point
│   └── starter_mcp_server.py        # MCP server (config, OpenAPI, tools, structured output, elicitation)
├── tests/
│   ├── test_config.py
│   ├── test_header_forwarding.py
│   ├── test_main.py
│   ├── test_server.py
│   └── test_tools.py
├── spec/
│   └── platzi-fake-store-api.yaml   # Sample OpenAPI spec
├── .env.example                     # Environment template
├── Dockerfile                       # Container definition
└── pyproject.toml                   # Dependencies
```

### Create an MCP server

The following sections show patterns from a minimal server through OpenAPI generation, custom tools, routing, and deployment.

#### Minimal example

This example registers one tool and starts the server:

```python theme={"system"}
from fastmcp import FastMCP

mcp = FastMCP(name="MyServer")

@mcp.tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

if __name__ == "__main__":
    mcp.run()
```

#### Create tools with `@mcp.tool`

Decorate any Python function to expose it as a tool. FastMCP auto-generates the name, description, and input schema from the function signature.

```python theme={"system"}
from typing import Annotated
from pydantic import Field
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError

mcp = FastMCP(name="CalculatorServer")

@mcp.tool(
    name="find_products",
    description="Search the product catalog with optional category filtering.",
    tags={"catalog", "search"},
    timeout=30.0,
    annotations={"readOnlyHint": True},
)
def search_products(query: str, category: str | None = None) -> list[dict]:
    return [{"id": 2, "name": "Widget"}]

@mcp.tool
def process_image(
    image_url: Annotated[str, "URL of the image to process"],
    width: Annotated[int, Field(description="Target width", ge=1, le=2000)] = 800,
) -> dict:
    """Process an image with optional resizing."""
    ...

@mcp.tool
def divide(a: float, b: float) -> float:
    """Divide a by b."""
    if b == 0:
        raise ToolError("Division by zero is not allowed.")
    return a / b
```

**Supported type annotations:**

| Type            | Example                            |
| --------------- | ---------------------------------- |
| Basic types     | `int`, `float`, `str`, `bool`      |
| Collections     | `list[str]`, `dict[str, int]`      |
| Optional        | `float \| None`, `Optional[float]` |
| Constrained     | `Literal["A", "B"]`, `Enum`        |
| Pydantic models | `UserData`                         |

`ToolError` messages are always sent to clients. When `mask_error_details=True`, FastMCP masks other exceptions from clients.

#### Return structured output

When a tool returns a dataclass or a Pydantic model, FastMCP automatically generates `structuredContent` in the MCP response, giving the calling agent a typed schema to work with instead of free-form text. The `starter-mcp-server` demonstrates both styles; the examples below are simplified, so see `starter_mcp_server.py` for the full models.

<Tabs>
  <Tab title="Dataclass">
    ```python theme={"system"}
    from dataclasses import dataclass
    from fastmcp import FastMCP

    mcp = FastMCP(name="CatalogServer")

    @dataclass
    class PriceConversion:
        original_price: float
        currency: str
        converted_price: float
        target_currency: str
        rate: float

    @mcp.tool
    def convert_price(price: float, target_currency: str) -> PriceConversion:
        """Convert a product price from USD to another currency."""
        rate = 0.92  # look up the real rate
        return PriceConversion(
            original_price=price,
            currency="USD",
            converted_price=round(price * rate, 2),
            target_currency=target_currency.upper(),
            rate=rate,
        )
    ```
  </Tab>

  <Tab title="Pydantic model">
    ```python theme={"system"}
    from pydantic import BaseModel, Field
    from fastmcp import FastMCP

    mcp = FastMCP(name="CatalogServer")

    class ProductSummary(BaseModel):
        id: int
        title: str
        price: float
        category: str
        in_stock: bool = True

    class CatalogPage(BaseModel):
        products: list[ProductSummary]
        total: int
        page: int = Field(description="Current page number (1-based)")
        page_size: int

    @mcp.tool
    async def get_catalog_page(page: int = 1, page_size: int = 5) -> CatalogPage:
        """Get a page of products with structured metadata."""
        products = [ProductSummary(id=1, title="Widget", price=9.99, category="Tools")]
        return CatalogPage(products=products, total=len(products), page=page, page_size=page_size)
    ```
  </Tab>
</Tabs>

#### Request user input with elicitation

Elicitation lets a tool pause mid-execution to ask the user for additional information or confirmation before continuing — useful for confirmation dialogs, collecting missing inputs, or interactive workflows. The `starter-mcp-server` uses this in its `order_product` tool: it fetches a product, asks the user to confirm via a typed schema, and branches on the response. The example below is simplified — see `starter_mcp_server.py` for the production handler.

```python theme={"system"}
from fastmcp import FastMCP, Context
from fastmcp.server.elicitation import (
    AcceptedElicitation,
    DeclinedElicitation,
    CancelledElicitation,
)
from pydantic import BaseModel, Field

mcp = FastMCP(name="OrdersServer")

class OrderDetails(BaseModel):
    confirm: bool = Field(description="Whether to proceed with the order")
    quantity: int = Field(default=1, ge=1, le=100)
    shipping_address: str = Field(default="")

@mcp.tool
async def order_product(ctx: Context, product_id: int) -> dict:
    """Place an order after asking the user to confirm."""
    result = await ctx.elicit(
        message="Please confirm your order.",
        response_type=OrderDetails,
    )

    # Handle every response branch
    if isinstance(result, CancelledElicitation):
        return {"status": "cancelled"}
    if isinstance(result, DeclinedElicitation):
        return {"status": "declined"}

    order = result.data  # OrderDetails, available on AcceptedElicitation
    if not order.confirm:
        return {"status": "not_confirmed"}
    return {"status": "success", "quantity": order.quantity}
```

<Warning>
  Elicitation requires **stateful sessions**, so it only works when the server runs with `MCP_STATELESS_HTTP=false` (or STDIO transport). With stateless HTTP there is no session to round-trip the user's response, and the elicitation call fails. This requirement is why the `starter-mcp-server` defaults `MCP_STATELESS_HTTP` to `false`.
</Warning>

#### Create from an OpenAPI spec

FastMCP can auto-generate an MCP server from any OpenAPI specification. Every endpoint becomes a tool that forwards requests to the underlying API. The `starter-mcp-server` uses this approach with the Platzi Fake Store API.

```python theme={"system"}
import httpx
import yaml
from fastmcp import FastMCP

with open("my-api-spec.yaml") as f:
    openapi_spec = yaml.safe_load(f)

client = httpx.AsyncClient(base_url="https://api.example.com")

mcp = FastMCP.from_openapi(
    openapi_spec=openapi_spec,
    client=client,
    name="My API Server",
)

if __name__ == "__main__":
    mcp.run()
```

#### Integrate a REST API as a tool

Instead of auto-generating from an OpenAPI spec, wrap any API call as a hand-written tool function:

```python theme={"system"}
import httpx
from fastmcp import FastMCP

mcp = FastMCP("API Tools Server")

@mcp.tool
async def get_payment_orders(status: str | None = None) -> dict:
    """Retrieve payment orders, optionally filtered by status."""
    async with httpx.AsyncClient() as client:
        params = {}
        if status:
            params["status"] = status
        response = await client.get(
            "https://api.example.com/client-api/v3/payment-orders",
            params=params,
            headers={"Authorization": "Bearer TOKEN"},
        )
        response.raise_for_status()
        return response.json()
```

#### Include and exclude tools

Use `RouteMap` to control which endpoints your MCP server exposes:

<Tabs>
  <Tab title="Exclude routes">
    ```python theme={"system"}
    from fastmcp import FastMCP
    from fastmcp.server.providers.openapi import RouteMap, MCPType

    mcp = FastMCP.from_openapi(
        openapi_spec=spec,
        client=client,
        route_maps=[
            RouteMap(pattern=r"^/admin/.*", mcp_type=MCPType.EXCLUDE),
            RouteMap(tags={"internal"}, mcp_type=MCPType.EXCLUDE),
            RouteMap(methods=["GET"], pattern=r"^/.*", mcp_type=MCPType.TOOL),
            RouteMap(mcp_type=MCPType.EXCLUDE),
        ],
    )
    ```

    FastMCP evaluates route maps in order. The first match wins.
  </Tab>

  <Tab title="Allowlist (include only)">
    ```python theme={"system"}
    mcp = FastMCP.from_openapi(
        openapi_spec=spec,
        client=client,
        route_maps=[
            RouteMap(methods=["GET"], pattern=r"^/client-api/v3/payment-orders$", mcp_type=MCPType.TOOL),
            RouteMap(methods=["POST"], pattern=r"^/client-api/v3/payment-orders$", mcp_type=MCPType.TOOL),
            RouteMap(mcp_type=MCPType.EXCLUDE),
        ],
    )
    ```
  </Tab>

  <Tab title="Tag-based visibility">
    ```python theme={"system"}
    from fastmcp import FastMCP

    mcp = FastMCP("MyServer")

    @mcp.tool(tags={"admin"})
    def delete_all_orders() -> str:
        return "Deleted"

    @mcp.tool(tags={"public"})
    def get_status() -> str:
        return "OK"

    # Disable admin tools; clients only see get_status
    mcp.disable(tags={"admin"})

    # Or use allowlist mode
    mcp.enable(tags={"public"}, only=True)

    # Re-enable when needed
    mcp.enable(tags={"admin"})
    ```
  </Tab>
</Tabs>

**MCPType values:**

| Value                       | Description                          |
| --------------------------- | ------------------------------------ |
| `MCPType.TOOL`              | Expose as an MCP Tool                |
| `MCPType.RESOURCE`          | Expose as an MCP Resource            |
| `MCPType.RESOURCE_TEMPLATE` | Expose as a Resource Template        |
| `MCPType.EXCLUDE`           | Exclude from the MCP server entirely |

#### Forward client headers

When your MCP server proxies an authenticated API, forward headers from the MCP client request to upstream API calls. The `starter-mcp-server` includes this pattern for forwarding `Authorization` headers:

```python theme={"system"}
from typing import Any
import httpx
from fastmcp.server.dependencies import get_http_request

class HeaderForwardingClient(httpx.AsyncClient):
    """Forwards auth headers from the MCP client request to the upstream API."""

    async def send(self, request: httpx.Request, **kwargs: Any) -> httpx.Response:
        try:
            incoming = get_http_request()
            auth = incoming.headers.get("Authorization", "")
            if auth:
                request.headers["Authorization"] = auth
        except Exception:
            pass
        request.headers["Content-Type"] = "application/json"
        return await super().send(request, **kwargs)

api_client = HeaderForwardingClient(base_url="https://api.example.com")

mcp = FastMCP.from_openapi(
    openapi_spec=spec,
    client=api_client,
    name="My Authenticated API Server",
)
```

<Warning>
  Header forwarding only works with HTTP transports such as `streamable-http`. It doesn't apply to STDIO transport because there is no HTTP request context.
</Warning>

#### Proxy bridge

The [Proxy Provider](https://gofastmcp.com/servers/providers/proxy) enables transport bridging, server aggregation, and gateway patterns:

<Tabs>
  <Tab title="Transport bridging">
    ```python theme={"system"}
    from fastmcp.server import create_proxy

    # Bridge HTTP server to local stdio
    http_proxy = create_proxy("http://example.com/mcp/sse", name="HTTP-to-stdio")

    if __name__ == "__main__":
        http_proxy.run()  # Defaults to stdio
    ```
  </Tab>

  <Tab title="Multi-server proxy">
    ```python theme={"system"}
    from fastmcp.server import create_proxy

    config = {
        "mcpServers": {
            "weather": {
                "url": "https://weather-api.example.com/mcp",
                "transport": "http"
            },
            "calendar": {
                "url": "https://calendar-api.example.com/mcp",
                "transport": "http"
            }
        }
    }

    # Creates unified proxy with prefixed components:
    # - weather_get_forecast
    # - calendar_add_event
    composite = create_proxy(config, name="Composite")
    ```
  </Tab>
</Tabs>

#### Run the server

Start the server with transport, host, port, and path options:

```python theme={"system"}
if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",  # "stdio" (default) | "streamable-http"
        host="0.0.0.0",
        port=8557,
        path="/mcp",
        stateless_http=False,  # keep stateful for elicitation; set True for stateless deployments
    )
```

**Transport comparison:**

| Feature           | STDIO      | Streamable HTTP |
| ----------------- | ---------- | --------------- |
| Network access    | No         | Yes             |
| Multiple clients  | No         | Yes             |
| Production ready  | Local only | Yes             |
| Header forwarding | N/A        | Yes             |

Add a health check alongside the MCP endpoint:

```python theme={"system"}
from starlette.requests import Request
from starlette.responses import JSONResponse

@mcp.custom_route("/health", methods=["GET"])
async def health(request: Request) -> JSONResponse:
    return JSONResponse({"status": "ok"})
```

### Development

Use these commands while you change the server or add tools:

#### Run tests

Execute the test suite with pytest:

```bash theme={"system"}
uv sync --extra dev
uv run pytest
```

#### Build Docker image

Build a local image tag for the service:

```bash theme={"system"}
docker build -t starter-mcp-server:local .
```

### CI/CD

The `.github/workflows` directory defines these standard workflows:

* **PR checks**: Linting, testing, and validation
* **Build and publish**: Docker image creation on merge
* **Release**: Automated versioning and release notes

<Info>
  See **[CI/CD workflows](/agentic-ai/ci-cd-workflows/overview)** for pipeline details.
</Info>

### Register in APIM via GitOps

After you deploy your FastMCP server, register it in APIM so the API gateway can route to it. This registration uses the [`agent-mcp-api`](https://github.com/bb-ecos-agbs/agent-mcp-api) template — a spec-only Maven project that produces an OpenAPI spec and Helm chart artifact for APIM deployment.

The deployment follows the GitOps pipeline: **GitHub PR → ArgoCD → Azure Service Operator (ASO) → APIM**.

<Steps>
  <Step title="Use the agent-mcp-api template">
    The [`agent-mcp-api`](https://github.com/bb-ecos-agbs/agent-mcp-api) project defines a complete MCP Streamable HTTP API spec for APIM (`POST /mcp`, `GET /mcp`, `DELETE /mcp`, `GET /health`, `GET /version`) with full JSON-RPC 2.0 envelope schemas and MCP session headers.
  </Step>

  <Step title="Configure applications-live registration">
    Register entries across **five files** in `applications-live` (`github.com/bb-ecos-<installation>/gc-<installation>-applications-live`) under `runtimes/<runtime>/apim/`:

    | File                         | Purpose                                                                                                                            |
    | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
    | `apiversionsets.values.yaml` | Register the version set                                                                                                           |
    | `backends.values.yaml`       | Register the backend (upstream MCP server URL)                                                                                     |
    | `products.values.yaml`       | Register the product with APIM policy (include `Accept: application/json, text/event-stream` for server-sent events (SSE) support) |
    | `subscriptions.values.yaml`  | Register the subscription                                                                                                          |
    | `apis/<your-mcp-api>.yaml`   | ArgoCD `Application` manifest referencing the `agent-mcp-api` Helm chart                                                           |
  </Step>

  <Step title="Set Helm values">
    The ArgoCD `Application` manifest sets these Helm parameters, and pulls shared values in through `valueFiles`:

    ```yaml theme={"system"}
    helm:
      releaseName: YOUR_RELEASE_NAME
      parameters:
        - name: path
          value: UNIQUE_API_PATH           # Required. Must be unique per APIM instance — override to avoid a path collision
        - name: name
          value: API_RESOURCE_NAME         # Required. APIM API resource name; defaults to versionSet + major version
        - name: versionSet
          value: VERSION_SET_NAME          # Optional. Inferred from the Helm chart name; must match apiversionsets.values.yaml when set
        - name: azureName
          value: AZURE_RESOURCE_NAME       # Optional. Azure ARM resource name; defaults to name
        - name: displayName
          value: DISPLAY_NAME              # Optional. Defaults to the OpenAPI spec title (info.title)
      valueFiles:
        - $apps-live/runtimes/<runtime>/apim/apis/common.apim.values.yaml
    ```

    `apimArmId` (the Azure Resource Manager ID of the target APIM instance) is required by the chart, but you don't set it per API — it's defined centrally in `common.apim.values.yaml` and pulled in through `valueFiles`, so every API in the installation inherits it.

    The `path` defaults to `info.x-api-domain` and `info.x-api-service` from the OpenAPI spec (joined by a slash), or the Maven `artifactId` if those fields are absent. Override it with the `apim-helm-plugin.path` Maven property at build time, or the `path` Helm value at deploy time — required when another API already uses the resolved path on the target APIM instance.
  </Step>

  <Step title="Deploy and validate">
    After ArgoCD picks up the merged PR, it deploys the API to APIM. Validate with a `tools/list` call through the APIM gateway:

    ```bash theme={"system"}
    curl -X POST https://api.<runtime>.<installation>.gcservices.io/<your-path>/<version>/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "api-key: YOUR_SUBSCRIPTION_KEY" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
    ```
  </Step>
</Steps>

<Info>
  For the complete reference including APIM policy configuration, product setup, and end-to-end validation details, see **[ADR-MCP - Track 1 - PoC Validation](https://backbase.atlassian.net/wiki/spaces/BAAP/pages/6582501539)** on Confluence.
</Info>

***

## Expose an API as an MCP server on Azure APIM

Azure APIM exposes your existing APIs as MCP servers without custom MCP server code. You select API operations in the Azure Portal, and APIM handles MCP protocol translation, tool discovery, and invocation.

This approach is useful when you already have APIs provisioned in APIM and want to make them available to AI agents without building a separate FastMCP server.

### Prerequisites

Before you can expose an API as an MCP server on APIM:

1. **Ensure network connectivity.** The platform must have network connectivity to the target APIs. If it doesn't, [raise a ticket with the Service Desk](https://servicedesk.atlassian.backbase.com/servicedesk/customer/portal/3071) to request access.
2. **Upload your API spec to APIM** and register your API in Azure APIM using `applications-live` (`github.com/bb-ecos-<installation>/gc-<installation>-applications-live` under `runtimes/<runtime>/apim`).

### High-level steps

Configure MCP in APIM through the portal:

1. Navigate to **APIs → MCP Servers (Preview)** in your APIM instance
2. Click **Create MCP Server → Expose an API as an MCP Server**
3. Select the API and operations to expose, starting with read-only endpoints
4. Configure APIM policies for security, including subscription key validation, rate limiting, and audit logging
5. Test with [MCP Inspector](https://github.com/modelcontextprotocol/inspector) or `curl`

<Warning>
  **Known limitation**: POST operations with `requestBody.content` sections in the OpenAPI spec cause `/tools/list` to hang indefinitely. Remove or simplify the `requestBody.content` section in your OpenAPI spec before you expose the API via MCP. See the full guide for details.
</Warning>

<Info>
  For complete click-ops instructions that cover APIM policies, security configuration, rate limiting, audit logging, external MCP server proxying, and known issues, see the **[APIM MCP: click-ops guide](https://backbase.atlassian.net/wiki/spaces/BAAP/pages/5928290430)** on Confluence.
</Info>

***

# MCP client

Connect to an MCP server from different types of clients, including programmatic Python clients, AI-native tools like Cursor and Claude Desktop, and AI agent frameworks like Agno.

<Card title="GitHub repository" icon="github" href="https://github.com/bb-ecos-agbs/starter-mcp-client">
  View source code, releases, and issues
</Card>

<Info>
  You need a running MCP server to connect to. See [MCP server](#mcp-server) to create one.
</Info>

## Transport types

Before connecting, determine which transport your MCP server uses:

| Transport           | Best for                                | How it works                              |
| ------------------- | --------------------------------------- | ----------------------------------------- |
| **Streamable HTTP** | Production remote servers (recommended) | Client connects to an HTTP endpoint       |
| **STDIO**           | Local development, CLI servers          | Client spawns the server as a sub-process |
| **SSE**             | Legacy remote servers                   | HTTP with server-sent events (deprecated) |

<Warning>
  SSE transport is deprecated by MCP. Always use **Streamable HTTP** for deployments. Use STDIO only for local development or CLI-based MCP servers such as `npx` and `uvx` packages.
</Warning>

## FastMCP client

[FastMCP](https://gofastmcp.com) provides a built-in `Client` class for connecting to any MCP server programmatically. The client infers the transport from what you pass to it.

<Tabs>
  <Tab title="Basic usage">
    ```python theme={"system"}
    import asyncio
    from fastmcp import Client

    async def main():
        async with Client("https://example.com/mcp") as client:
            tools = await client.list_tools()
            for tool in tools:
                print(f"  {tool.name}: {tool.description}")

            result = await client.call_tool("get_payment_orders", {"status": "ACCEPTED"})
            print(result)

    asyncio.run(main())
    ```
  </Tab>

  <Tab title="With auth headers">
    ```python theme={"system"}
    from fastmcp import Client
    from fastmcp.client.transports import StreamableHttpTransport

    transport = StreamableHttpTransport(
        url="https://api.example.com/mcp",
        headers={
            "Authorization": "Bearer your-token",
            "X-User-Context": "user-123",
        },
    )

    async with Client(transport) as client:
        tools = await client.list_tools()
        result = await client.call_tool("list_orders", {})
    ```
  </Tab>

  <Tab title="Multi-server client">
    ```python theme={"system"}
    from fastmcp import Client

    config = {
        "mcpServers": {
            "payments": {
                "url": "https://api.example.com:8557/mcp"
            },
            "accounts": {
                "url": "https://api.example.com:8558/mcp"
            },
            "local_tools": {
                "command": "python",
                "args": ["./local_server.py"]
            }
        }
    }

    async with Client(config) as client:
        orders = await client.call_tool("payments_get_payment_orders", {"status": "ACCEPTED"})
        accounts = await client.call_tool("accounts_list_accounts", {})
    ```
  </Tab>
</Tabs>

## Cursor IDE

Cursor has built-in MCP support (v0.40+). Create `.cursor/mcp.json` in your project root:

<Tabs>
  <Tab title="Streamable HTTP (remote)">
    ```json theme={"system"}
    {
      "mcpServers": {
        "my-mcp-server": {
          "type": "streamableHttp",
          "url": "https://api.example.com/mcp",
          "headers": {
            "Authorization": "Bearer your-token",
            "X-User-Context": "user-123"
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="STDIO (local)">
    ```json theme={"system"}
    {
      "mcpServers": {
        "my-mcp-server": {
          "command": "python",
          "args": ["path/to/my_mcp_server.py"]
        }
      }
    }
    ```
  </Tab>
</Tabs>

Restart Cursor after adding the configuration. MCP servers only load at startup.

## Claude Desktop

Claude Desktop reads its MCP configuration from `claude_desktop_config.json`:

| OS      | Path                                                              |
| ------- | ----------------------------------------------------------------- |
| macOS   | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json`                     |
| Linux   | `~/.config/Claude/claude_desktop_config.json`                     |

<Tabs>
  <Tab title="STDIO server">
    ```json theme={"system"}
    {
      "mcpServers": {
        "my-mcp-server": {
          "command": "python",
          "args": ["path/to/my_mcp_server.py"]
        }
      }
    }
    ```
  </Tab>

  <Tab title="Streamable HTTP (via mcp-remote)">
    Claude Desktop currently connects to remote MCP servers through a local proxy:

    ```json theme={"system"}
    {
      "mcpServers": {
        "my-mcp-server": {
          "command": "npx",
          "args": [
            "mcp-remote",
            "https://api.example.com/mcp"
          ]
        }
      }
    }
    ```
  </Tab>
</Tabs>

## Agno agent

[Agno](https://docs.agno.com) is a Python framework for building AI agents with built-in MCP support through its `MCPTools` class.

<Tabs>
  <Tab title="Streamable HTTP">
    ```python theme={"system"}
    from agno.agent import Agent
    from agno.models.openai import OpenAIChat
    from agno.tools.mcp import MCPTools, StreamableHTTPClientParams

    params = StreamableHTTPClientParams(
        url="http://api.example.com/mcp",
        headers={
            "Authorization": "Bearer your-token",
            "X-User-Context": "user-123",
        },
        timeout=30,
        terminate_on_close=True,
    )

    mcp_tools = MCPTools(
        transport="streamable-http",
        server_params=params,
        timeout_seconds=60,
    )
    await mcp_tools.connect()

    agent = Agent(
        name="My MCP Agent",
        model=OpenAIChat(id="gpt-4o-mini"),
        tools=[mcp_tools],
        instructions=["Use the available tools to answer user questions."],
        markdown=True,
    )

    await agent.aprint_response("Show me my payment orders")
    await mcp_tools.close()
    ```
  </Tab>

  <Tab title="STDIO">
    ```python theme={"system"}
    from agno.tools.mcp import MCPTools, StdioServerParameters

    server_params = StdioServerParameters(
        command="python",
        args=["my_mcp_server.py"],
        env={"API_KEY": "secret"},
    )

    mcp_tools = MCPTools(transport="stdio", server_params=server_params)
    await mcp_tools.connect()

    agent = Agent(model=my_model, tools=[mcp_tools])
    await agent.aprint_response("What is the license for this project?")
    await mcp_tools.close()
    ```
  </Tab>

  <Tab title="Multiple servers">
    ```python theme={"system"}
    from agno.tools.mcp import MultiMCPTools

    mcp_tools = MultiMCPTools(
        commands=[
            "npx -y @openbnb/mcp-server-airbnb --ignore-robots-txt",
            "npx -y @modelcontextprotocol/server-google-maps",
        ],
        env={**os.environ, "GOOGLE_MAPS_API_KEY": os.getenv("GOOGLE_MAPS_API_KEY")},
    )
    await mcp_tools.connect()

    agent = Agent(model=my_model, tools=[mcp_tools])
    ```
  </Tab>
</Tabs>

## Pass headers to MCP servers

How you pass authentication headers depends on your client type:

<Tabs>
  <Tab title="FastMCP client">
    ```python theme={"system"}
    from fastmcp import Client
    from fastmcp.client.transports import StreamableHttpTransport

    transport = StreamableHttpTransport(
        url="https://api.example.com/mcp",
        headers={"Authorization": "Bearer your-token"},
    )

    async with Client(transport) as client:
        result = await client.call_tool("get_orders", {})
    ```
  </Tab>

  <Tab title="Cursor IDE">
    ```json theme={"system"}
    {
      "mcpServers": {
        "my-server": {
          "type": "streamableHttp",
          "url": "https://api.example.com/mcp",
          "headers": {
            "Authorization": "Bearer your-token",
            "X-User-Context": "user-123",
            "api-key": "your-api-key"
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="Claude Desktop">
    For STDIO servers, pass secrets via environment variables:

    ```json theme={"system"}
    {
      "mcpServers": {
        "my-server": {
          "command": "python",
          "args": ["my_server.py"],
          "env": {
            "API_TOKEN": "your-token"
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="Agno">
    When you expose your agent through a FastAPI endpoint, extract headers from the incoming request:

    ```python theme={"system"}
    from agno.tools.mcp import MCPTools, StreamableHTTPClientParams

    async def create_mcp_tools(headers: dict[str, str]) -> MCPTools:
        params = StreamableHTTPClientParams(
            url="https://api.example.com/mcp",
            headers=headers,
        )
        tools = MCPTools(
            transport="streamable-http",
            server_params=params,
            timeout_seconds=60,
        )
        await tools.__aenter__()
        return tools
    ```
  </Tab>
</Tabs>

***

## Next steps

From here you can extend agents, teams, knowledge, and pipelines:

* **[Starter Agent](/agentic-ai/starter-kits/starter-agent)**: Start with basic agent patterns
* **[Multi-Agent](/agentic-ai/starter-kits/multi-agent)**: Build agent teams
* **[Knowledge Agent](/agentic-ai/starter-kits/knowledge-agent)**: Add RAG capabilities
* **[BB AI SDK](/agentic-ai/bb-ai-sdk/overview)**: AI Gateway and observability
* **[CI/CD workflows](/agentic-ai/ci-cd-workflows/overview)**: Pipeline details
