# How to Connect an AI Agent to MCP

- Tool: MCP
- Difficulty: Intermediate
- Time required: 30-45 min
- Compatibility: Python 3.10+, claude-agent-sdk, a GitHub personal access token, a Postgres connection string, any OS
- Last updated: November 2026

## TL;DR

MCP (Model Context Protocol) lets an agent call tools from an external server instead of you writing a handler for every integration. This guide builds an on-call triage agent that connects the Claude Agent SDK to the hosted GitHub MCP server for recent issues and a read-only Postgres MCP server for incident data, with zero custom tool code. Budget 30-45 minutes, a GitHub token, and a Postgres connection string.

## What you'll build

You'll build an on-call triage agent for an engineering team. It connects to two MCP servers instead of two custom integrations: the hosted GitHub MCP server to pull recent issues from a repo, and DBHub, a small MCP server that wraps a Postgres database, configured read-only so the agent can query incident history but can never write to it. The agent's job is to look at open issues and recent incidents together and summarize what's likely connected.

MCP exists because every agent framework used to need its own GitHub client, its own database client, its own Slack client, each with slightly different auth and error handling. An MCP server packages one integration behind a standard protocol; any MCP-aware agent, regardless of framework, can use it the same way. You configure a connection (a command to run, or a URL to call) and a tool allowlist, and the SDK handles discovery, calling, and error surfacing.

Reach for MCP over writing your own tool handler (how-to-give-an-ai-agent-tools) when the integration already has a maintained MCP server, since GitHub, Postgres-via-DBHub, Slack, and dozens of others do, and you'd otherwise be re-implementing auth and pagination that's already solved. Write your own tool instead when the integration is internal and bespoke enough that no server exists for it.

## Before you start

- Python 3.10+ and a terminal
- An Anthropic API key (ANTHROPIC_API_KEY)
- pip install claude-agent-sdk
- A GitHub personal access token with read access to the target repo
- A Postgres connection string (DATABASE_URL) for the incidents table, or a test database

## Step-by-step guide

### 1. Install the SDK and set your tokens

Install claude-agent-sdk and export your Anthropic key alongside a GitHub token scoped to read-only access on the repo you'll triage. Keep both as environment variables; neither belongs in code or in the MCP server config file you'll write next.

```
pip install claude-agent-sdk
export ANTHROPIC_API_KEY="sk-ant-..."
export GITHUB_TOKEN="github_pat_..."
export DATABASE_URL="postgresql://user:password@localhost:5432/incidents"
```

**Expected result:** Three environment variables set; nothing connects yet.

### 2. Connect to the hosted GitHub MCP server over HTTP

Configure mcp_servers with an HTTP entry pointing at GitHub's hosted MCP endpoint, passing the token as a Bearer header. Scope allowed_tools to exactly the tool you need, list_issues, rather than a wildcard, since this agent only ever reads issues.

```
import os
from claude_agent_sdk import ClaudeAgentOptions

github_server = {
    "type": "http",
    "url": "https://api.githubcopilot.com/mcp/",
    "headers": {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
}

options = ClaudeAgentOptions(
    mcp_servers={"github": github_server},
    allowed_tools=["mcp__github__list_issues"],
)
```

**Expected result:** An options object with one MCP server configured; the connection itself happens on the first query().

### 3. Set up a read-only Postgres MCP server with DBHub

DBHub is a small MCP server that wraps a SQL database and exposes an execute_sql tool. Point it at your database through a config file, not a raw connection string in code, and set readonly = true so execute_sql rejects INSERT, UPDATE, DELETE, and DDL even if the agent generates one. DBHub resolves ${DATABASE_URL} from the environment when it starts, so the connection string never appears in the file.

```
# dbhub.toml
[[sources]]
id = "incidents_db"
dsn = "${DATABASE_URL}"

[[tools]]
name = "execute_sql"
source = "incidents_db"
readonly = true
```

**Expected result:** A dbhub.toml file next to your script, with no secrets written directly into it.

### 4. Add the Postgres server as a second stdio connection

Unlike the GitHub server, DBHub runs as a local process you launch via npx, so it's a stdio entry: a command and arguments, not a URL. Add it to the same mcp_servers dict and extend allowed_tools with its one tool.

```
options = ClaudeAgentOptions(
    mcp_servers={
        "github": github_server,
        "postgres": {
            "command": "npx",
            "args": ["-y", "@bytebase/dbhub", "--config", "dbhub.toml"],
        },
    },
    allowed_tools=["mcp__github__list_issues", "mcp__postgres__execute_sql"],
)
```

**Expected result:** Both servers configured on one options object; GitHub connects over HTTP, Postgres starts as a local subprocess on first use.

### 5. Verify both servers connected before trusting the output

Run query() and watch for the SystemMessage with subtype 'init'. It reports each server's status: connected, pending, failed, or needs-auth. Check for failures before you act on anything the agent says, since a failed MCP server means Claude silently fell back to no tool data for that source rather than raising an error.

```
from claude_agent_sdk import query, SystemMessage, ResultMessage

async def run_triage():
    async for message in query(
        prompt="List the 5 most recent open issues in acme/platform, and check the incidents "
               "database for anything from the last 24 hours that might be related.",
        options=options,
    ):
        if isinstance(message, SystemMessage) and message.subtype == "init":
            statuses = message.data.get("mcp_servers", [])
            failed = [s for s in statuses if s.get("status") in ("failed", "needs-auth")]
            if failed:
                print(f"WARNING: unavailable MCP servers: {failed}")
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)
```

**Expected result:** The init message shows both 'github' and 'postgres' with status 'connected', then a final summary that cross-references issues and incidents.

### 6. Harden it: name servers explicitly and cap tool timeouts

Two production details matter once this runs unattended. List each server's tools explicitly in allowed_tools, as this guide already does, rather than a wildcard, so a new tool added to GitHub's server later doesn't silently become available to your agent. And set MCP_TIMEOUT if DBHub or your network is slow to connect, since the default 30-second window can fail a cold start that would otherwise succeed.

```
import os
os.environ["MCP_TIMEOUT"] = "45000"  # milliseconds, for a slower DBHub cold start

# allowed_tools stays an explicit list, never "mcp__github__*"
assert all(not t.endswith("*") for t in options.allowed_tools)
```

**Expected result:** A slow-starting DBHub process no longer fails the connection at the default 30-second mark, and no wildcard tool grants exist.

## Complete code example

File: `oncall_triage_agent.py`

```python
"""On-call triage agent: connects the Claude Agent SDK to the hosted GitHub
MCP server and a read-only Postgres MCP server (DBHub), with zero custom tools.
Run: python oncall_triage_agent.py
(pip install claude-agent-sdk; set ANTHROPIC_API_KEY, GITHUB_TOKEN, DATABASE_URL)
Also requires a dbhub.toml next to this file:
  [[sources]]
  id = "incidents_db"
  dsn = "${DATABASE_URL}"
  [[tools]]
  name = "execute_sql"
  source = "incidents_db"
  readonly = true
"""
import asyncio
import os

from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage, ResultMessage

os.environ.setdefault("MCP_TIMEOUT", "45000")

GITHUB_SERVER = {
    "type": "http",
    "url": "https://api.githubcopilot.com/mcp/",
    "headers": {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"},
}

POSTGRES_SERVER = {
    "command": "npx",
    "args": ["-y", "@bytebase/dbhub", "--config", "dbhub.toml"],
}

options = ClaudeAgentOptions(
    mcp_servers={"github": GITHUB_SERVER, "postgres": POSTGRES_SERVER},
    allowed_tools=["mcp__github__list_issues", "mcp__postgres__execute_sql"],
)


async def run_triage(repo: str) -> None:
    async for message in query(
        prompt=(
            f"List the 5 most recent open issues in {repo}, and check the incidents "
            "database for anything from the last 24 hours that might be related. "
            "Summarize any likely connections."
        ),
        options=options,
    ):
        if isinstance(message, SystemMessage) and message.subtype == "init":
            statuses = message.data.get("mcp_servers", [])
            failed = [s for s in statuses if s.get("status") in ("failed", "needs-auth")]
            if failed:
                print(f"WARNING: unavailable MCP servers, results may be incomplete: {failed}")
        elif isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


if __name__ == "__main__":
    asyncio.run(run_triage("acme/platform"))
```

## Common mistakes

- **Using a wildcard like mcp__github__* when the agent only needs one specific tool** — undefined Fix: List the exact tool names in allowed_tools. A wildcard grants every current and future tool the server exposes, more than this agent needs.
- **Connecting a database MCP server without a readonly flag, assuming the agent won't write** — undefined Fix: A model can and will generate a write statement if the prompt implies one. Enforce readonly at the server config level, like DBHub's readonly = true, not through prompting alone.
- **Ignoring the init message's server statuses and trusting the final answer regardless** — undefined Fix: A failed or needs-auth server means that source contributed nothing, silently. Check status before treating the agent's summary as complete.
- **Putting the GitHub token or database connection string directly in the mcp_servers dict as a literal string** — undefined Fix: Read both from environment variables, as this guide does. A hardcoded credential in a config object is one accidental print statement or log line away from exposure.
- **Assuming permission_mode='acceptEdits' auto-approves MCP tool calls** — undefined Fix: It doesn't; that mode only covers file edits and filesystem Bash commands. Use allowedTools to grant MCP tool access explicitly.

## Best practices

- Scope allowed_tools to specific tool names, not server-wide wildcards, even for servers you trust.
- Enforce read-only access at the MCP server's own configuration, not just through agent instructions.
- Check the init SystemMessage's server statuses before acting on results that depend on more than one server.
- Pass credentials through environment variables into headers or env, never as literals in the server config.
- Prefer an existing, maintained MCP server (GitHub, DBHub, Slack) over writing a custom tool for an integration that already has one.
- Raise MCP_TIMEOUT for stdio servers with a slow cold start instead of retrying blindly on connection failures.

## Use cases

### undefined



### undefined



### undefined



## Frequently asked questions

### Do I need to write an MCP server myself to use MCP?

No, not for integrations that already have one. This guide connects to two existing servers, GitHub's hosted server and DBHub for Postgres, with zero server-side code. You'd only write a server if you're exposing your own internal tools to other MCP clients.

### What's the difference between an HTTP MCP server and a stdio one?

An HTTP server is hosted remotely and you connect with a URL and headers, like the GitHub server in this guide. A stdio server runs as a local subprocess you launch with a command, like DBHub here. Use HTTP for cloud-hosted integrations and stdio for anything that runs on the same machine as your agent.

### Is the current MCP spec still the 'SSE transport with an initialize handshake' version I've read about?

No. The current spec (2026-07-28) moved to stateless, self-contained requests over stdio or Streamable HTTP; servers no longer initiate requests back to the client. If you're reading older MCP material, check the spec date before trusting its transport details.

### How do I know which tools an MCP server exposes before I write allowed_tools?

Check the server's own documentation, or run a query once with mcp_servers configured and no allowedTools restriction, then read the tools array in the init SystemMessage, which lists every mcp__ tool the connected servers expose.

### Can one agent use MCP servers and custom SDK tools at the same time?

Yes. ClaudeAgentOptions.mcp_servers accepts external servers like the ones in this guide alongside in-process servers built with create_sdk_mcp_server, and allowed_tools covers both by their mcp__ prefix the same way.

### Can someone set up these MCP connections for me instead of doing it myself?

Yes. RapidDev connects agents to MCP servers for GitHub, databases, and internal APIs, including the read-only enforcement and status checks most tutorials skip. A free 30-minute consultation is the fastest way to scope your version.

---

Source: https://www.rapidevelopers.com/build-ai-agent/how-to-connect-an-ai-agent-to-mcp
© RapidDev — https://www.rapidevelopers.com/build-ai-agent/how-to-connect-an-ai-agent-to-mcp
