Skip to main content
Glama
bnbhat

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.