Skip to main content
Glama
Sehrabdar

ToolBridge

by Sehrabdar
README.md
# ToolBridge — Secure MCP Server for Authenticated Tool Execution

> **Streamable HTTP is the production transport; stdio is used only for local development and rapid iteration.**

ToolBridge is a production-grade MCP (Model Context Protocol) server that exposes authenticated, authorized external tools to any MCP-compatible client. It enforces per-user authentication, per-user authorization, secure credential handling, tool-level permissions, audit logging, failure handling, and observability. MCP-compatible AI agents and clients connect to ToolBridge to access its tools — the agent/client itself is not part of this project.

---

## Architecture

```
MCP-compatible Client or AI Agent (external — not part of this project)
  ↓
MCP (Streamable HTTP — production | stdio — local dev)
  ↓
ToolBridge MCP Server
  ↓
Authentication
  ↓
Authorization / Policy
  ↓
Tool Executor
  ↓
External APIs (GitHub REST API)
```

The server combines two concerns, kept deliberately separate:

| Concern | Layer |
|---|---|
| MCP protocol, tool registration, tool routing | Official Python MCP SDK server |
| HTTP authentication, OAuth callback | Thin FastAPI auth layer |

---

## Phase Status

| Phase | Description | Status |
|---|---|---|
| **Phase 0** | Architecture & Requirements | ✅ COMPLETE |
| **Phase 1** | Project Foundation | ✅ COMPLETE |
| **Phase 2** | MCP Server Core | ✅ COMPLETE |
| **Phase 3** | GitHub Tool Integrations | ✅ COMPLETE |
| **Phase 4** | MCP Transport & Protocol | ✅ COMPLETE |
| Phase 5 | Authentication | ⏳ NEXT |
| Phase 6 | Authorization & Permissions | ⏳ |
| Phase 7 | Secure Credential Management | ⏳ |
| Phase 8 | Policy Engine | ⏳ |
| Phase 9 | Reliability & Error Handling | ⏳ |
| Phase 10 | Audit Logging & Observability | ⏳ |
| Phase 11 | Security Hardening | ⏳ |
| Phase 12 | MCP Server Evaluation | ⏳ |
| Phase 13 | Production Deployment | ⏳ |
| Phase 14 | Documentation & Demo | ⏳ |

---

## Technology Decisions

### Current Implementation (Phases 1–4)

| Technology | Purpose | Status |
|---|---|---|
| **Python 3.12** | Primary language | ✅ Implemented |
| **uv** | Dependency management & virtual environments | ✅ Implemented |
| **FastAPI** | Web framework (MCP HTTP transport, `/health` endpoint) | ✅ Implemented |
| **pydantic-settings** | Typed configuration from environment variables | ✅ Implemented |
| **structlog** | Structured application logging | ✅ Implemented |
| **SQLAlchemy (async)** | Async ORM / database connectivity | ✅ Implemented |
| **asyncpg** | Async PostgreSQL driver | ✅ Implemented |
| **Alembic** | Database schema migrations | ✅ Implemented |
| **PostgreSQL 16** | Primary data store (via Docker) | ✅ Implemented |
| **Docker / Docker Compose** | Containerisation & local dev environment | ✅ Implemented |
| **pytest** | Test framework | ✅ Implemented |
| **Ruff** | Linting & formatting | ✅ Implemented |
| **mypy** | Static type checking | ✅ Implemented |
| **GitHub Actions** | CI pipeline | ✅ Implemented |
| **Official Python MCP SDK** (`mcp` 2.x) | MCP server primitives, tool registration, in-process client testing | ✅ Implemented (Phase 2) |
| **httpx** | Async HTTP client for GitHub REST API calls | ✅ Implemented (Phase 3) |
| **respx** | httpx mock library for unit-testing GitHub client | ✅ Implemented (Phase 3) |
| **GitHub REST API** | External tool target (`search_repositories`, `list_issues`, `get_file_contents`) | ✅ Implemented (Phase 3) |

### Future Phases (not yet implemented)

| Technology | Purpose | Phase |
|---|---|---|
| **GitHub OAuth** | User identity via GitHub authorization | Phase 5 |
| **JWT** | Short-lived session tokens for MCP requests | Phase 5 |

---

## Phase 4 — MCP Transport & Protocol — ✅ COMPLETE

Integrated the MCP Streamable HTTP transport into the FastAPI application, enabling real MCP clients to connect over the network. The server now fully supports end-to-end tool execution over HTTP, distinct from the in-process testing implemented in Phase 2.

### What was delivered

| Item | Detail |
|---|---|
| HTTP Transport Mount | `_app.mount("/", mcp.streamable_http_app())` in `src/toolbridge/server/app.py` |
| Session Management | Added `lifespan` handler to properly manage `mcp.session_manager.run()` |
| E2E Transport Tests | `tests/mcp/test_http_transport.py` — spins up real Uvicorn server and tests with `mcp.Client` |
| Test suite updates | 4 new HTTP transport tests, covering tool discovery and error handling |

---

## Phase 3 — GitHub Tool Integrations — ✅ COMPLETE

Implemented the `GitHubClient` — a typed, async HTTP client wrapping the GitHub REST API — and wired all three MCP tools to it. Tools now make real API calls and return live data. Phase 3 raises the test count from 4 to 11 passing tests while maintaining 0 Ruff issues and 0 mypy issues.

### What was delivered

| Item | Detail |
|---|---|
| GitHub client | `src/toolbridge/github/client.py` — `GitHubClient` with typed `httpx.AsyncClient` |
| `search_repositories` | Issues `GET /search/repositories?q=<query>`, returns `list[GitHubRepository]` |
| `list_issues` | Issues `GET /repos/{repo}/issues?state=<state>`, filters out pull requests |
| `get_file_contents` | Issues `GET /repos/{repo}/contents/{path}`, Base64-decodes file content |
| Typed responses | `GitHubRepository`, `GitHubIssue`, `GitHubFileContent` TypedDicts |
| Error hierarchy | `GitHubClientError` → `GitHubNotFoundError`, `GitHubRateLimitError`, `GitHubAuthError` |
| MCP tool wiring | All three tools in `server.py` instantiate `GitHubClient` and delegate to it |
| Test suite | `tests/github/test_client.py` — 7 tests using `respx` HTTP mocks |
| Updated MCP tests | `tests/mcp/test_server.py` — 4 tests now use `respx` mocks, verify live structured output |
| Ruff | 0 issues |
| mypy | 0 issues |

#### Pull Request Filtering in `list_issues`

GitHub's REST API returns pull requests as part of the issues endpoint (a PR is technically a kind of issue in their data model). Each raw item includes a `"pull_request"` key only if it is actually a PR. `GitHubClient.list_issues` filters those out so the tool returns genuine issues only, matching what the tool name promises callers.

#### Base64 Decoding in `get_file_contents`

GitHub's Contents API returns file content as a Base64-encoded string. `GitHubClient.get_file_contents` decodes it to a UTF-8 string before returning, so callers receive human-readable text without needing to know about the encoding layer. If the target path is a directory (response is a JSON array rather than an object), the client raises `GitHubClientError` with a clear `"directory"` message.

#### Error Handling

All three client methods map GitHub HTTP status codes to typed exceptions before surfacing them:

| HTTP Status | Exception |
|---|---|
| 401, 403 | `GitHubAuthError` |
| 404 | `GitHubNotFoundError` |
| 429 | `GitHubRateLimitError` |
| Other 4xx/5xx | `httpx.HTTPStatusError` (via `raise_for_status`) |

---

## Phase 2 — MCP Server Core — ✅ COMPLETE

Implemented the core ToolBridge MCP server using the MCP 2.x `MCPServer` API
(`mcp.server.mcpserver.MCPServer`). The server exposes three discoverable MCP tools
with explicit Pydantic input/output schemas, backed by in-process tests that cover
both tool invocation and MCP tool discovery. Phase 2 maintains the quality bar
established in Phase 1: 4 passing tests, Ruff passing with 0 issues, and mypy
passing with 0 issues.

### What was delivered

| Item | Detail |
|---|---|
| MCP server instance | `mcp = MCPServer("toolbridge")` in `src/toolbridge/mcp/server.py` |
| Tool: `search_repositories` | Input: `query: str` → Output: `SearchRepositoriesResult` |
| Tool: `list_issues` | Input: `repo: str`, `state: str` → Output: `ListIssuesResponse` |
| Tool: `get_file_contents` | Input: `repo: str`, `path: str` → Output: `FileContentsResponse` |
| Pydantic output schemas | `RepositoryResult`, `SearchRepositoriesResult`, `IssueResult`, `ListIssuesResponse`, `FileContentsResponse` |
| Test: tool invocation × 3 | `test_search_repositories_returns_structured_response`, `test_list_issues_returns_structured_response`, `test_get_file_contents_returns_structured_response` |
| Test: tool discovery | `test_tools_are_discoverable_with_schemas` — asserts all three tools are listed |
| Test suite | `tests/mcp/test_server.py` — 4 tests, 0 failures |
| Ruff | 0 issues |
| mypy | 0 issues |

#### MCP SDK Version Drift & Migration

During Phase 2 a real MCP SDK version-drift issue was encountered and deliberately
resolved.

**Initial implementation** used the MCP 1.x API:

```python
from mcp.server.fastmcp import FastMCP
```

**The problem**: when the virtual environment was recreated and dependencies were
resolved, `uv` resolved MCP `2.1.1` (the latest available version satisfying the
broad `mcp>=1.9.0` constraint in `pyproject.toml`). The `mcp.server.fastmcp` module
does not exist in MCP 2.x, causing:

```
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
```

**Deliberate resolution**: rather than silently adapting code until the error
disappeared, the project treated this as a real API compatibility issue and
deliberately migrated from the MCP 1.x `FastMCP` API to the MCP 2.x `MCPServer`
API:

```
MCP 1.x
FastMCP  (mcp.server.fastmcp)
    ↓
API / version drift discovered (mcp.server.fastmcp absent in MCP 2.x)
    ↓
MCP 2.x
MCPServer  (mcp.server.mcpserver)
```

The final import in the implementation is:

```python
from mcp.server.mcpserver import MCPServer
```

The `uv.lock` was updated as part of this resolution and now pins the resolved
version to MCP `2.1.1`. The `pyproject.toml` dependency constraint (`mcp>=1.9.0`)
remains broad enough to have admitted MCP 2.x; constraining it to `mcp>=2,<3`
would pin the project to MCP 2.x while preventing an uncontrolled future
major-version API change from silently breaking the server. That constraint update
is tracked as a follow-on hygiene item.

---

## MCP Tools

Three read-oriented tools are registered on the MCP server as of Phase 3:

```
search_repositories(query)              → SearchRepositoriesResult
list_issues(repo, state="open")         → ListIssuesResponse
get_file_contents(repo, path)           → FileContentsResponse
```

These tools are **discoverable** (verified by `test_tools_are_discoverable_with_schemas`),
return **schema-driven structured output** (verified by the three invocation tests),
and **make real GitHub REST API calls** via `GitHubClient` (Phase 3).

---

## Planned Database Schema

```sql
users (id, github_username, created_at)
user_tokens (id, user_id, provider, access_token, refresh_token NULL, expires_at NULL, scopes[])
tool_permissions (id, user_id, tool_name, allowed)
audit_log (id, user_id, tool_name, request_payload, response_status, latency_ms, created_at)
```

> **Note:** `refresh_token` and `expires_at` are intentionally nullable.
> The exact GitHub token lifecycle will be verified during Phase 5 before the schema is finalised.

---

## Getting Started (Phases 1–4)

### Prerequisites

- Python 3.12+
- [uv](https://docs.astral.sh/uv/)
- Docker & Docker Compose
- A GitHub personal access token (for tool execution)

### Setup

```bash
# Clone and enter the project
git clone <repo-url>
cd toolbridge

# Copy environment configuration
cp .env.example .env
# Edit .env — set GITHUB_TOKEN to your GitHub personal access token

# Install dependencies
uv sync

# Start PostgreSQL
docker compose up -d

# Run the application
uv run uvicorn toolbridge.main:app --reload

# Verify health
curl http://localhost:8000/health
# {"status":"ok","db":"ok"}
```

### Running Tests

```bash
# Unit tests (no external services required)
uv run pytest tests/unit/

# GitHub client tests (uses respx HTTP mocks — no real API calls)
uv run pytest tests/github/

# MCP server tests (uses respx HTTP mocks — no real API calls)
uv run pytest tests/mcp/

# Integration tests (requires PostgreSQL from docker compose up -d)
uv run pytest tests/integration/ -m integration

# All tests
uv run pytest
```

### Code Quality

```bash
# Linting
uv run ruff check .

# Formatting check
uv run ruff format --check .

# Type checking
uv run mypy .
```

### Database Migrations

```bash
# Run pending migrations
uv run alembic upgrade head

# Create a new migration
uv run alembic revision --autogenerate -m "description"
```

---

## Project Structure

```
toolbridge/
│
├── src/
│   └── toolbridge/
│       ├── config/       # Typed settings (pydantic-settings)
│       ├── logging/      # Structured logging (structlog)
│       ├── db/           # Async SQLAlchemy engine + health check
│       ├── server/       # FastAPI application + MCP HTTP transport (Phase 4)
│       ├── github/       # GitHub REST API client + typed responses (Phase 3)
│       └── mcp/          # MCP server instance + tool definitions (Phase 2–3)
│
├── tests/
│   ├── unit/             # Tests with no external service dependencies
│   ├── integration/      # Tests requiring PostgreSQL
│   ├── mcp/              # MCP server and HTTP transport tests (Phase 2–4)
│   └── github/           # GitHub client unit tests with respx mocks (Phase 3)
│
├── migrations/           # Alembic migration scripts
├── docs/                 # Architecture documentation
├── scripts/              # Development utility scripts
├── .github/workflows/    # GitHub Actions CI
│
├── pyproject.toml        # Project metadata, dependencies, tool config
├── Dockerfile            # Multi-stage production image
├── docker-compose.yml    # Local development environment (PostgreSQL)
├── .env.example          # Environment variable reference (no secrets)
└── README.md
```

---

## Observability

Logs are emitted in JSON format in production and human-readable format in development.
Every log record includes: `timestamp`, `level`, `logger`, `message`.

Future phases will add: `request_id`, `user_id`, `tool_name`, `latency_ms`, `status`.

---

## Security Notes

- Secrets are **never** committed. The `.env` file is in `.gitignore`.
- See `.env.example` for all required environment variables.
- OAuth credentials, JWT secrets, and API keys are out of scope until Phase 5/7.

---

## Engineering Decisions & Implementation Notes

### MCP SDK Version Drift (Phase 2)

During Phase 2, MCP SDK version drift exposed an API incompatibility between the
original MCP 1.x `FastMCP` implementation and the MCP 2.x package resolved by `uv`
(`mcp==2.1.1`). The issue was resolved deliberately by migrating the server from
`FastMCP` to `MCPServer` and updating the code to the MCP 2.x API at
`mcp.server.mcpserver`.

This keeps the implementation aligned with the resolved MCP major version. Constraining
the `pyproject.toml` dependency to `mcp>=2,<3` is tracked as a follow-on step to
prevent an uncontrolled future major-version upgrade from introducing another
breaking API change.

### GitHub Client Design (Phase 3)

`GitHubClient` is a thin, typed wrapper around `httpx.AsyncClient`. Each method maps
to a single GitHub REST API endpoint and converts HTTP error codes into a typed
exception hierarchy before they propagate to the MCP tool layer. This keeps error
handling logic out of the MCP tool handlers themselves and makes the client testable
in isolation with `respx` HTTP mocks — no real network calls are required by any
unit test.