{"id":"903c7eeb-cb38-4b4f-817c-1475c168598b","slug":"naomytcheums-dotcom-conduit","kind":"mcp","name":"conduit","summary":"An MCP server exposing real GitHub repository and issue data to any MCP client (Claude Desktop, etc.) through standardized tools, with SQLite response caching. Two real bugs found and documented durin","owner":"naomytcheums-dotcom","repoUrl":"https://github.com/naomytcheums-dotcom/conduit","license":"MIT","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-08-11T23:23:39.000Z","addedAt":"2026-10-21T21:41:09.649Z","logoUrl":"https://avatars.githubusercontent.com/u/253335130?u=37a6f8a5f120f07e5724232e501277cbb709a2b6&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["developer-tools","databases","cloud-infra"],"tags":[],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-22T11:58:03.995Z","sample":false,"url":"https://conduits.dev/tools/naomytcheums-dotcom-conduit","language":"Python","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# Conduit\n\nAn MCP (Model Context Protocol) server that exposes real GitHub\nrepository and issue data to any MCP client — Claude Desktop, or any\nother tool that speaks the protocol — through five standardized tools\ninstead of a one-off integration per client.\n\n## Why MCP instead of a custom API wrapper\n\nWithout MCP, \"let an LLM query GitHub\" means writing a bespoke\nintegration for every client that wants it. MCP standardizes that: build\nthe server once, and Claude Desktop, a custom agent, or any other\nMCP-speaking client can call it the same way. This server is the data\nside of that — read-only access to public repos and issues, with a\ncaching layer in front of it.\n\n## Tools exposed\n\n| Tool | What it does |\n|---|---|\n| `list_repos(username, limit)` | A user's public repos, most recently updated first |\n| `get_repo(owner, repo)` | Description, language, stars, open issue count |\n| `list_issues(owner, repo, state, limit)` | Issues only — pull requests filtered out |\n| `get_issue(owner, repo, number)` | Full issue detail, including body text |\n| `get_readme(owner, repo)` | Raw README content |\n\nEvery tool response is slimmed down to the fields that matter\n(`conduit/github_client.py`) — GitHub's raw API payloads run 60+ keys\ndeep, and handing an LLM all of it burns context for no reason.\n\n## Caching, and why it's not optional here\n\nGitHub's unauthenticated REST API allows 60 requests/hour per IP. An\nLLM calling tools in a loop can burn through that in minutes. Every tool\nresponse is cached in SQLite (`conduit/cache.py`) with a configurable\nTTL (`CACHE_TTL_SECONDS`, default 300s) — a real engineering constraint\nthis project actually has to handle, not a hypothetical one.\n\n## Two real bugs this caught while being built\n\n1. **A run stuck silently on a closed event loop.** The first working\n   version created its `httpx.AsyncClient` once at import time and\n   reused it for every tool call. On Windows, that client's connection\n   pool would occasionally try to close a pooled connection whose\n   cleanup got scheduled against an event loop that had already moved\n   on — `RuntimeError: Event loop is closed`, with no indication of why.\n   Fixed two ways: the client is now built lazily on first real use\n   instead of at import time, and connection keep-alive pooling is\n   disabled (`httpx.Limits(max_keepalive_connections=0)`) so there's no\n   pooled connection left around to race on.\n2. **Tests failing on GitHub's rate limit looked like code failures.**\n   Unauthenticated testing hit the 60/hour cap partway through\n   development, and those failures were indistinguishable from real bugs\n   in the output. `tests/conftest.py` now catches a 403 rate-limit\n   response and skips with a clear reason instead of failing red for a\n   constraint that has nothing to do with correctness.\n\n## Using it with Claude Desktop\n\nAdd to your Claude Desktop MCP config (`claude_desktop_config.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"conduit\": {\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"conduit\"],\n      \"cwd\": \"/path/to/conduit\",\n      \"env\": { \"GITHUB_TOKEN\": \"your-token-here\" }\n    }\n  }\n}\n```\n\n`GITHUB_TOKEN` is optional — public data works without one, just at the\nlower rate limit.\n\n## Running it\n\n```\npip install -r requirements.txt\npython -m conduit\n```\n\nRuns over stdio, which is what Claude Desktop (and most MCP clients)\nexpect — there's no web dashboard for this one, since an MCP server's\nactual interface is the protocol itself, not a UI.\n\n## Running the tests\n\n```\npytest -q\n```\n\n12 tests. `test_cache.py` is fully offline. `test_github_client.py` and\n`test_server.py` hit the real GitHub API on purpose — a mocked response\nwould never have caught either bug above — and skip cleanly if the rate\nlimit is already exhausted rather than failing.\n\n## Known limitations\n\n- Read-only by design. A version that could open issues or comment would\n  be a legitimately different, higher-risk project — this one doesn't\n  attempt it.\n- No auth beyond an optional personal access token — fine for public\n  repos, not built for private repo access control.\n- The SQLite cache is unbounded — nothing evicts old entries beyond TTL\n  expiry. Fine at the scale one developer's tool calls generate; not\n  designed for high-volume multi-user deployment.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n"}