Skip to main content
Glama
README.md
# Conduit

An MCP (Model Context Protocol) server that exposes real GitHub
repository and issue data to any MCP client — Claude Desktop, or any
other tool that speaks the protocol — through five standardized tools
instead of a one-off integration per client.

## Why MCP instead of a custom API wrapper

Without MCP, "let an LLM query GitHub" means writing a bespoke
integration for every client that wants it. MCP standardizes that: build
the server once, and Claude Desktop, a custom agent, or any other
MCP-speaking client can call it the same way. This server is the data
side of that — read-only access to public repos and issues, with a
caching layer in front of it.

## Tools exposed

| Tool | What it does |
|---|---|
| `list_repos(username, limit)` | A user's public repos, most recently updated first |
| `get_repo(owner, repo)` | Description, language, stars, open issue count |
| `list_issues(owner, repo, state, limit)` | Issues only — pull requests filtered out |
| `get_issue(owner, repo, number)` | Full issue detail, including body text |
| `get_readme(owner, repo)` | Raw README content |

Every tool response is slimmed down to the fields that matter
(`conduit/github_client.py`) — GitHub's raw API payloads run 60+ keys
deep, and handing an LLM all of it burns context for no reason.

## Caching, and why it's not optional here

GitHub's unauthenticated REST API allows 60 requests/hour per IP. An
LLM calling tools in a loop can burn through that in minutes. Every tool
response is cached in SQLite (`conduit/cache.py`) with a configurable
TTL (`CACHE_TTL_SECONDS`, default 300s) — a real engineering constraint
this project actually has to handle, not a hypothetical one.

## Two real bugs this caught while being built

1. **A run stuck silently on a closed event loop.** The first working
   version created its `httpx.AsyncClient` once at import time and
   reused it for every tool call. On Windows, that client's connection
   pool would occasionally try to close a pooled connection whose
   cleanup got scheduled against an event loop that had already moved
   on — `RuntimeError: Event loop is closed`, with no indication of why.
   Fixed two ways: the client is now built lazily on first real use
   instead of at import time, and connection keep-alive pooling is
   disabled (`httpx.Limits(max_keepalive_connections=0)`) so there's no
   pooled connection left around to race on.
2. **Tests failing on GitHub's rate limit looked like code failures.**
   Unauthenticated testing hit the 60/hour cap partway through
   development, and those failures were indistinguishable from real bugs
   in the output. `tests/conftest.py` now catches a 403 rate-limit
   response and skips with a clear reason instead of failing red for a
   constraint that has nothing to do with correctness.

## Using it with Claude Desktop

Add to your Claude Desktop MCP config (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "conduit": {
      "command": "python",
      "args": ["-m", "conduit"],
      "cwd": "/path/to/conduit",
      "env": { "GITHUB_TOKEN": "your-token-here" }
    }
  }
}
```

`GITHUB_TOKEN` is optional — public data works without one, just at the
lower rate limit.

## Running it

```
pip install -r requirements.txt
python -m conduit
```

Runs over stdio, which is what Claude Desktop (and most MCP clients)
expect — there's no web dashboard for this one, since an MCP server's
actual interface is the protocol itself, not a UI.

## Running the tests

```
pytest -q
```

12 tests. `test_cache.py` is fully offline. `test_github_client.py` and
`test_server.py` hit the real GitHub API on purpose — a mocked response
would never have caught either bug above — and skip cleanly if the rate
limit is already exhausted rather than failing.

## Known limitations

- Read-only by design. A version that could open issues or comment would
  be a legitimately different, higher-risk project — this one doesn't
  attempt it.
- No auth beyond an optional personal access token — fine for public
  repos, not built for private repo access control.
- The SQLite cache is unbounded — nothing evicts old entries beyond TTL
  expiry. Fine at the scale one developer's tool calls generate; not
  designed for high-volume multi-user deployment.

## License

MIT — see [LICENSE](LICENSE).