Launchpad MCP Server
by bnbhat
README.md
# Launchpad MCP Server (read-only)
A **read-only** [Model Context Protocol](https://modelcontextprotocol.io) server that
exposes [Launchpad](https://launchpad.net) bug-tracking data for a **single project** to
an AI agent (e.g. Hermes Agent / GitHub Copilot). The agent calls these tools to answer
engineering questions about tickets and always cites the canonical bug URL.
> **Read-only contract:** this server never writes to Launchpad. It does not call
> `lp_save()`, set attributes, post comments, or change status/assignee/milestone. It
> only performs Launchpad read operations.
## Tools
| Tool | Purpose |
| --- | --- |
| `search_bugs` | Search bugs by text, status, importance, tags (Any/All), assignee, milestone, created/modified date. |
| `get_bug` | Full detail for one bug, scoped to the project's bug task(s). |
| `get_bug_comments` | Ordered comment thread with comment-anchor URLs and a best-effort `is_resolution` flag. |
| `list_project_bugs` | Sync-friendly listing ordered by `-date_last_updated` for checkpointing. |
| `list_milestones` | Project milestones (active or all). |
| `list_tags` | Official tags plus tags observed on recent bugs. |
| `get_project_info` | Project orientation: name, summary, active milestones, series, bug tracker, URL. |
| `find_references` | Extract cross-references (other bugs, commit SHAs, merge proposals, branches) from a bug's description + comments as graph edges with provenance. |
Every result includes the canonical bug URL
`https://bugs.launchpad.net/<project>/+bug/<id>`, and comments include the anchor
`https://bugs.launchpad.net/<project>/+bug/<id>/comments/<n>`.
## Requirements
- Python 3.10+
- [`mcp`](https://pypi.org/project/mcp/) and [`launchpadlib`](https://pypi.org/project/launchpadlib/)
Note: `launchpadlib` depends on system libraries for OAuth/crypto. On Debian/Ubuntu you
may need `sudo apt-get install python3-launchpadlib` or build deps for `cryptography`.
## Setup
```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # then edit values
```
### Environment variables
| Variable | Default | Description |
| --- | --- | --- |
| `LP_PROJECT` | *(required)* | Project short name (e.g. `my-soc-project`) or distro source package (e.g. `ubuntu/+source/linux`). |
| `LP_SERVICE_ROOT` | `production` | `production`, `staging`, or `qastaging`. |
| `LP_AUTH` | `oauth` | `oauth` (browser authorize, needed for private projects) or `anonymous` (public projects only). |
| `LP_CREDENTIALS_DIR` | `~/.launchpadlib` | Where the OAuth token cache is stored. Never commit it. |
`production` maps to `https://api.launchpad.net/devel/`.
### OAuth first run
With `LP_AUTH=oauth`, run the dedicated login command **once** in an interactive
terminal:
```bash
set -a; source .env; set +a
python server.py --login
```
It prints a Launchpad authorization URL and waits:
```
Please open this authorization page:
(https://launchpad.net/+authorize-token?oauth_token=...)
in your browser. Use your browser to authorize this program to access Launchpad...
Press Enter after authorizing in your browser.
```
Open that URL in a browser (on any machine), click **Allow**, then return to the
terminal and press **Enter**. The token is cached as a plain file under
`LP_CREDENTIALS_DIR` (default `~/.launchpadlib`) and reused on later runs — no prompt
again. The URL and prompts are printed to **stderr** so they never interfere with the
MCP stdio protocol on stdout.
This server deliberately uses a file credential store instead of the system keyring, so
it works on **headless servers** (where `keyring`/D-Bus would otherwise block). The
credential file is git-ignored; never commit or print it.
For public projects you can skip authorization entirely with `LP_AUTH=anonymous`.
> Run `--login` **before** registering the server with an MCP client, so the token is
> already cached when the client launches it non-interactively. Note: Launchpad may
> briefly rate-limit the authorization endpoint if you request several tokens in quick
> succession — if the URL is slow to appear, wait a minute and retry.
## Verify the wiring (self-test)
Run a quick check against any **public** project without an MCP client:
```bash
LP_PROJECT=ubuntu LP_AUTH=anonymous python server.py --selftest
```
This calls `get_project_info` and `search_bugs(limit=3)` and prints the JSON results.
## Run as an MCP stdio server
```bash
LP_PROJECT=my-soc-project python server.py
```
The server speaks MCP over **stdio** and registers all eight tools with descriptions and
JSON schemas.
## Register with VS Code (using a virtualenv)
VS Code (with GitHub Copilot) discovers MCP servers from a `.vscode/mcp.json` file in the
workspace, or from your user-level `mcp.json`. Because this project runs inside a
virtualenv, the most important rule is:
> **Point `command` at the venv's Python interpreter (absolute path), not bare
> `python`.** That guarantees `mcp` and `launchpadlib` are importable when VS Code
> launches the server. Do **not** rely on `source .venv/bin/activate` — VS Code starts
> the process directly, without your shell's activation.
### 1. Authorize once before registering
VS Code launches the server non-interactively, so it cannot complete the OAuth browser
flow. Run the one-time login yourself first (see [OAuth first run](#oauth-first-run)):
```bash
source .venv/bin/activate
set -a; source .env; set +a
python server.py --login # authorize in browser, press Enter
```
This caches the token under `LP_CREDENTIALS_DIR`. (Public projects can skip this and use
`LP_AUTH=anonymous` instead.)
### 2. Create `.vscode/mcp.json`
Use **absolute paths** for both the venv interpreter and `server.py`, and set
`LP_CREDENTIALS_DIR` to an absolute path so the cached token is found regardless of the
working directory VS Code launches from:
```jsonc
{
"servers": {
"launchpad": {
"type": "stdio",
// Absolute path to the venv's Python interpreter.
"command": "/abs/path/to/launchpad-mcp/.venv/bin/python",
"args": ["/abs/path/to/launchpad-mcp/server.py"],
"env": {
"LP_PROJECT": "my-soc-project",
"LP_SERVICE_ROOT": "production",
"LP_AUTH": "oauth",
// Absolute path so the cached OAuth token is found at launch.
"LP_CREDENTIALS_DIR": "/abs/path/to/launchpad-mcp/.launchpadlib"
}
}
}
}
```
Find your venv's interpreter path with `echo "$PWD/.venv/bin/python"` (or
`which python` while the venv is activated).
### 3. Start the server
Open `.vscode/mcp.json` and click the **Start** CodeLens above the `"launchpad"` entry,
or run **Command Palette → "MCP: List Servers" → launchpad → Start**. Use **Show Output**
on the same menu to confirm it registered eight tools with no errors or auth prompt.
### 4. Use the tools in Copilot Chat
1. Open Copilot Chat and switch the mode dropdown to **Agent** (MCP tools are only
available in Agent mode).
2. Click the **tools** (🔧) icon in the chat input and make sure the `launchpad` tools
are enabled.
3. Ask natural-language questions, e.g. *"Find Critical carmel bugs that are In Progress"*
or *"Show full details and the comment thread for bug 2143797."* Every answer should
cite the canonical `https://bugs.launchpad.net/<project>/+bug/<id>` URL.
### Troubleshooting
- **Server fails to start / "command not found":** the `command` path is wrong. Verify the
venv interpreter exists: `ls -l /abs/path/to/launchpad-mcp/.venv/bin/python`.
- **`No module named 'mcp'` / `launchpadlib`:** `command` is pointing at a system Python,
not the venv. Re-check the absolute interpreter path.
- **It re-prompts for authorization or hangs:** the cached token wasn't found. Ensure
`LP_CREDENTIALS_DIR` is an absolute path matching where `--login` saved the token
(`<dir>/launchpad-mcp-credentials`).
- **Tools don't appear in chat:** you're not in **Agent** mode, or the tools are toggled
off in the 🔧 picker.
## Register with Hermes / other MCP clients
Any stdio MCP client uses the same three fields — `command`, `args`, `env`. Point
`command` at the venv interpreter and `args` at `server.py`, and pass the same `LP_*`
environment variables shown above. Adapt the surrounding JSON to your client's config
format.
## Domain notes
Launchpad separates a **bug** from its **bug tasks**. Status, importance, assignee, and
milestone live on the *task*, not the bug. When summarizing a bug for the configured
project, this server selects the task(s) whose target matches `LP_PROJECT` and reports
that task's fields; `get_bug` also returns all matching tasks under `tasks`.
## Project layout
```
launchpad-mcp/
server.py # MCP server: tool registration + dispatch (stdio)
launchpad_client.py # READ-ONLY launchpadlib wrapper (auth, project lookup, mapping)
formatting.py # URLs, comment anchors, reference extraction, is_resolution
requirements.txt # mcp, launchpadlib
.env.example # LP_PROJECT, LP_SERVICE_ROOT, auth, cache dir
README.md # this file
.gitignore
```
## Extending later (out of scope now)
There is a clear seam to add a local cache / vector index behind `LaunchpadClient`
without changing the tool interface. No write operations to Launchpad will ever be added.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues