Metadata-Version: 2.3
Name: mcp-bugzilla
Version: 0.19.0
Summary: MCP server for Bugzilla
Requires-Dist: fastmcp==3.4.6
Requires-Dist: httpx-retries>=0.5.0
Requires-Dist: pytest ; extra == 'dev'
Requires-Dist: pytest-asyncio ; extra == 'dev'
Requires-Dist: respx ; extra == 'dev'
Requires-Python: >=3.13
Provides-Extra: dev
Description-Content-Type: text/markdown

# Bugzilla Model Context Protocol (MCP) Server

[![checks](https://github.com/openSUSE/mcp-bugzilla/actions/workflows/checks.yml/badge.svg)](https://github.com/openSUSE/mcp-bugzilla/actions/workflows/checks.yml)
[![release](https://github.com/openSUSE/mcp-bugzilla/actions/workflows/release.yml/badge.svg)](https://github.com/openSUSE/mcp-bugzilla/actions/workflows/release.yml)

A robust MCP server that provides seamless interaction with Bugzilla instances through the Model Context Protocol. This server exposes a comprehensive set of tools and prompts, enabling AI models and other MCP clients to efficiently query bug information, manage comments, and leverage Bugzilla's powerful quicksearch capabilities.

## Table of Contents

- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Troubleshooting](#troubleshooting)
- [License](#license)

## Features

### Tools

The server provides the following tools for interacting with Bugzilla:

#### Bug Information

- **`bug_info(bug_ids: set[int], include_fields: Optional[str] = None, exclude_fields: Optional[str] = None)`**: Retrieves comprehensive details for specified Bugzilla bug IDs. By default every field is returned; use Bugzilla's native field selection to trim large or bulk fetches.
  - **Parameters**:
    - `bug_ids`: A set of bug IDs to fetch details for
    - `include_fields`: Comma-separated field names to return (Bugzilla's native `Bug.get` parameter; supports field groups and `_default`/`_all`/`_extra`). For the leanest response request only scalar fields, e.g. `"id,status,resolution,summary"`. Requesting a user field (`assigned_to`, `creator`, `cc`, `qa_contact`) also returns its verbose `*_detail` object. Defaults to all fields
    - `exclude_fields`: Comma-separated field names to drop. To remove a user-object expansion, exclude both the base field and its `*_detail` together, e.g. `"cc,cc_detail"` — excluding the `*_detail` alone has no effect, as Bugzilla re-attaches it while the base field is present
  - **Returns**: A dictionary containing the array `bugs` which lists the requested information about the bugs (status, assignee, summary, description, extensions, etc.)
  - **Note on Bugzilla API parity**: both `include_fields` and `exclude_fields` are native Bugzilla `Bug.get` parameters, forwarded to the API unchanged; the underlying request is standard Bugzilla
  - **Example**: `bug_info({12345, 67890}, include_fields="id,status,resolution,summary")` fetches two bugs with just the essential scalar fields

- **`bug_history(id: int, new_since: Optional[datetime] = None, changed_fields: Optional[str] = None, exclude_authors: Optional[str] = None, limit: Optional[int] = None)`**: Fetches the change history of a given bug ID, with SQL-like controls to keep only the change events that matter for triage.
  - **Parameters**:
    - `id`: The bug ID to fetch history for
    - `new_since`: Optional datetime object to only return history newer than this time
    - `changed_fields`: Comma-separated Bugzilla field names; keep only changes to these fields, dropping events left with no matching change (e.g. `"status,resolution,assigned_to"` to see only lifecycle changes and hide the cc / flagtypes.name / summary churn that dominates most histories)
    - `exclude_authors`: Comma-separated substrings; drop events whose author matches any (e.g. `"upstream-release-monitoring"` to hide release-monitoring bot edits)
    - `limit`: Return only the most recent N events (chronological order preserved)
  - **Returns**: A list of history-event dictionaries, each containing `when`, `who`, and a `changes` list (each change has `field_name`, `added`, and `removed`)
  - **Note on Bugzilla API parity**: `new_since` mirrors Bugzilla's own API parameter. `changed_fields`, `exclude_authors`, and `limit` have no server-side Bugzilla equivalent — this server applies them client-side, as post-processing over the standard `Bug.history` response; the underlying Bugzilla request is unmodified
  - **Example**: `bug_history(2504555, changed_fields="status,resolution")` returns just the event where the bug was closed as RAWHIDE, filtering out the surrounding cc and needinfo-flag churn

- **`bug_comments(id: int, include_private_comments: bool = False, new_since: Optional[datetime] = None, include_fields: Optional[str] = "count,id,creator,creation_time,text,attachment_id", exclude_creators: Optional[str] = None, limit: Optional[int] = None)`**: Fetches comments associated with a given bug ID, with SQL-like controls to keep only what is needed and avoid flooding the context on large threads.
  - **Parameters**:
    - `id`: The bug ID to fetch comments for
    - `include_private_comments`: Whether to include private comments (default: `False`)
    - `new_since`: Optional datetime object to only return comments newer than this time
    - `include_fields`: Comma-separated field names to keep per comment (default: `count,id,creator,creation_time,text,attachment_id`). Pass `None` to return every field
    - `exclude_creators`: Comma-separated substrings; drop comments whose creator matches any (e.g. `"upstream-release-monitoring"` to hide release-monitoring/bot noise)
    - `limit`: Return only the most recent N comments (chronological order preserved)
  - **Returns**: A list of comment dictionaries. By default each contains `count`, `id`, `creator`, `creation_time`, `text`, and `attachment_id`; pass `include_fields=None` for the full objects (which also include `creator_id`, `bug_id`, `tags`, `time`, and `is_private`)
  - **Note on Bugzilla API parity**: `new_since` and `include_fields` mirror Bugzilla's own API parameters of the same names (`include_fields` is applied client-side here). `exclude_creators` and `limit` have no server-side Bugzilla equivalent — this server applies them client-side, as post-processing over the standard `Bug.comments` response; the underlying Bugzilla request is unmodified.
  - **Example**: `bug_comments(12345, exclude_creators="upstream-release-monitoring", limit=5)` returns the 5 most recent comments that aren't from the release-monitoring bot


- **`add_comment(bug_id: int, comment: str, is_private: bool = False)`**: Adds a new comment to a specified bug.
  - **Parameters**:
    - `bug_id`: The bug ID to add a comment to
    - `comment`: The comment text to add
    - `is_private`: Whether the comment should be private (default: `False`)
  - **Returns**: A dictionary containing the ID of the newly created comment
  - **Example**: `add_comment(12345, "Fixed in version 2.0", is_private=False)`

#### Attachments

- **`list_attachments(bug_id: int, exclude_obsolete: bool = False, patches_only: bool = False, limit: Optional[int] = None)`**: Lists a bug's attachments as metadata only (the base64 file contents are excluded to keep responses small), with filters to cut the noise on bugs that accumulate many attachments.
  - **Parameters**:
    - `bug_id`: The bug whose attachments to list
    - `exclude_obsolete`: Drop attachments flagged obsolete (`is_obsolete`). Note: some bots (e.g. release-monitoring) never set this flag, so it only helps where obsolete is actually used
    - `patches_only`: Keep only attachments flagged as patches (`is_patch`)
    - `limit`: Return only the most recent N attachments (chronological order preserved)
  - **Returns**: A list of attachment metadata objects (`id`, `file_name`, `summary`, `content_type`, `size`, `is_private`, `is_obsolete`, `is_patch`, `creation_time`, ...). Use an `id` with `download_attachment` to fetch the file.
  - **Note on Bugzilla API parity**: `exclude_obsolete`, `patches_only`, and `limit` have no server-side Bugzilla equivalent — this server applies them client-side, as post-processing over the standard attachment metadata; the underlying Bugzilla request is unmodified
  - **Example**: `list_attachments(2381747, patches_only=True, limit=3)` returns the 3 most recent patch attachments

- **`download_attachment(attachment_id: int, output_dir: Optional[str] = None, delivery: "auto" | "inline" | "save" = "auto", include_private: bool = False)`**: Downloads a single attachment by id. The `delivery` argument lets the caller choose how the content is returned.
  - **Parameters**:
    - `attachment_id`: The attachment id to download (discover via `list_attachments`)
    - `output_dir`: Optional directory to save the file in when it is written to disk. Defaults to the server's configured download directory (`--download-dir` / `BUGZILLA_DOWNLOAD_DIR`).
    - `delivery`:
      - `auto` (default): textual attachments (logs, patches, plain/xml/json, ...) up to 256 KiB are returned inline as decoded `content`; binary attachments, or larger text, are written to disk.
      - `inline`: always return the content in the response — decoded `content` for text, or base64 `data_base64` for binary (and for text whose bytes are not valid UTF-8). Refused above 1 MiB.
      - `save`: always write the file to disk and return its `path`.
    - `include_private`: Whether to download a private attachment (default: `False`); a private attachment is refused unless this is set to `True`.
  - **Returns**: Text inline: `{"mode": "text", "content": <decoded text>, ...metadata}`. Binary inline: `{"mode": "base64", "data_base64": <base64>, ...metadata}`. On disk: `{"mode": "saved", "path": <absolute path>, ...metadata}`. The saved file is named `<attachment_id>-<sanitized file_name>`.
  - **Example**: `download_attachment(685495)` · `download_attachment(685495, delivery="save")`

#### Write Operations

**Note**: Write operations require appropriate Bugzilla permissions. These tools enable bug management and workflow automation.

* **`update_bug_status(bug_id: int, status: str, resolution: str = None, comment: str = "")`**: Updates the status of a bug with optional comment.

  + **Parameters**:
    - `bug_id`: The bug ID to update
    - `status`: New status (e.g., `NEW`, `ASSIGNED`, `MODIFIED`, `ON_QA`, `VERIFIED`, `CLOSED`)
    - `resolution`: Required when status is `CLOSED` (e.g., `FIXED`, `WONTFIX`, `NOTABUG`, `DUPLICATE`)
    - `comment`: Optional comment explaining the status change
  + **Returns**: A dictionary containing the updated bug fields
  + **Example**: `update_bug_status(12345, "CLOSED", resolution="FIXED", comment="Fixed in version 2.0")`

* **`assign_bug(bug_id: int, assignee: str, comment: str = "")`**: Assigns a bug to a user.

  + **Parameters**:
    - `bug_id`: The bug ID to assign
    - `assignee`: Email address of the assignee
    - `comment`: Optional comment explaining the assignment
  + **Returns**: A dictionary containing the updated bug fields
  + **Example**: `assign_bug(12345, "developer@example.com", comment="You're the expert on this component")`

* **`update_bug_fields(bug_id: int, priority: str = None, severity: str = None, resolution: str = None, comment: str = "")`**: Updates various bug fields.

  + **Parameters**:
    - `bug_id`: The bug ID to update
    - `priority`: Priority level (e.g., `urgent`, `high`, `medium`, `low`, `unspecified`)
    - `severity`: Severity level (e.g., `urgent`, `high`, `medium`, `low`, `unspecified`)
    - `resolution`: Resolution (only for closed bugs)
    - `comment`: Optional comment explaining the changes
  + **Returns**: A dictionary containing the updated bug fields
  + **Example**: `update_bug_fields(12345, priority="high", severity="urgent", comment="Escalating due to customer impact")`

* **`add_cc_to_bug(bug_id: int, cc_email: str)`**: Adds an email address to the CC list of a bug.

  + **Parameters**:
    - `bug_id`: The bug ID
    - `cc_email`: Email address to add to CC list
  + **Returns**: A dictionary containing the updated bug fields
  + **Example**: `add_cc_to_bug(12345, "manager@example.com")`

* **`mark_as_duplicate(bug_id: int, duplicate_of: int, comment: str = "")`**: Marks a bug as a duplicate of another bug and closes it.

  + **Parameters**:
    - `bug_id`: The bug ID to mark as duplicate
    - `duplicate_of`: The bug ID this is a duplicate of
    - `comment`: Optional comment (auto-generated if not provided)
  + **Returns**: A dictionary containing the updated bug fields including status `CLOSED`, resolution `DUPLICATE`, and `dupe_of` reference
  + **Example**: `mark_as_duplicate(12345, 789012, comment="Same root cause as the original report")`

* **`create_bug(product, component, summary, version, description, op_sys="All", platform="All", priority=None, severity=None, cc=None, custom_fields=None)`**: Files a new bug.

  + **Parameters**:
    - `product`, `component`, `summary`, `version`, `description`: Required core fields
    - `op_sys`, `platform`: Default to `All`
    - `priority`, `severity`: Optional, instance-specific values
    - `cc`: Optional list of email addresses to CC
    - `custom_fields`: Optional dict of extra/custom fields (e.g. `{"cf_fixed_in": "1.2.3"}`)
  + **Returns**: A dictionary with the new bug `id`. If the Bugzilla instance mandates additional fields, its error message is surfaced so you can retry with them.
  + **Example**: `create_bug("MyProduct", "general", "App crashes on launch", "unspecified", "Steps to reproduce: ...")`

* **`add_attachment(bug_id, file_name, summary, data, content_type="text/plain", is_patch=False, is_private=False, comment="")`**: Attaches a file to a bug.

  + **Parameters**:
    - `bug_id`: The bug to attach to
    - `file_name`: File name shown in Bugzilla
    - `summary`: Short description of the attachment
    - `data`: The attachment content, **base64-encoded**
    - `content_type`: MIME type (ignored when `is_patch=True`)
    - `is_patch`: Mark the attachment as a patch
    - `is_private`: Restrict to the insider group
    - `comment`: Optional comment added alongside the attachment
  + **Returns**: A dictionary with the created attachment `ids`
  + **Example**: `add_attachment(12345, "crash.log", "Crash log", "aGVsbG8=", content_type="text/plain")`

#### Bug Search

- **`bugs_quicksearch(query: str, include_fields: str = "...", limit: int = 50, offset: int = 0)`**: Executes a search for bugs using Bugzilla's powerful [quicksearch syntax](https://bugzilla.readthedocs.io/en/latest/using/finding.html#quicksearch).
  - **Parameters**:
    - `query`: A quicksearch query string (e.g., `"product:Firefox"`)
    - `include_fields`: Comma-separated list of fields to return (default: `"id,product,component,assigned_to,status,resolution,summary,last_change_time"`)
    - `limit`: Maximum number of results to return (default: `50`)
    - `offset`: Number of results to skip for pagination (default: `0`)
  - **Returns**: A list of dictionaries, each containing essential bug fields
  - **Example**: `bugs_quicksearch("product:Firefox status:NEW", limit=10)`

- **`quicksearch_syntax_resource()`**: Returns documentation on Bugzilla's quicksearch syntax.
  - **Returns**: A string containing HTML documentation.

- **`summarize_bug_prompt(id: int)`**: Returns a detailed summary prompt for all comments of a given bug ID.
  - **Returns**: A well-structured summary of the bug's comments including usernames (bold italic) and dates (bold).

#### Utility Tools

- **`bug_url(bug_id: int)`**: Constructs and returns the direct URL to a specific bug on the Bugzilla server.
  - **Returns**: A string representing the bug's URL (e.g., `"https://bugzilla.example.com/show_bug.cgi?id=12345"`)

- **`bugzilla_server_info()`**: Returns comprehensive Bugzilla server information.
  - **Returns**: A dictionary containing `url`, `version`, `extensions`, `timezone`, `time`, and `parameters`.

- **`mcp_server_info_resource()`**: Returns the configuration arguments being used by the current server instance (version, host, port, etc.).
  - **Returns**: A dictionary containing server configuration.

- **`get_current_headers_resource()`**: Returns the HTTP headers from the current request.
  - **Returns**: A dictionary of HTTP headers.


## Requirements

- Python 3.13
- A Bugzilla instance with REST API access
- A Bugzilla user account with an API key *(optional: public instances support anonymous access)*
- Network access to the Bugzilla server

## Installation

### PyPI

The easiest way to install and run `mcp-bugzilla` is using [uv](https://github.com/astral-sh/uv):

#### Option 1: Using `uvx` (Recommended - No Installation Required)

Run the server directly without installing it:

```bash
uvx mcp-bugzilla --bugzilla-server https://bugzilla.example.com
```

This will automatically download and run the latest version.

#### Option 2: Install with `uv pip`

If you prefer to have `mcp-bugzilla` installed in your system:

```bash
# Install the package
uv pip install mcp-bugzilla

# Run the server
mcp-bugzilla --bugzilla-server https://bugzilla.example.com
```

#### Option 3: Install in a Virtual Environment

Create an isolated environment for `mcp-bugzilla`:

```bash
# Create a virtual environment
uv venv mcp-bugzilla-env
source mcp-bugzilla-env/bin/activate  # On Windows: mcp-bugzilla-env\Scripts\activate

# Install the package
uv pip install mcp-bugzilla

# Run the server
mcp-bugzilla --bugzilla-server https://bugzilla.example.com
```

**Note**: If you don't have `uv` installed, install it first from https://github.com/astral-sh/uv#installation

### From Source

1. **Clone the repository:**
   ```bash
   git clone <repository-url>
   cd mcp-bugzilla
   ```

2. **Install dependencies:**
   ```bash
   uv sync
   ```

3. **Run the server:**
   ```bash
   uv run mcp-bugzilla --bugzilla-server https://bugzilla.example.com --host 127.0.0.1 --port 8000
   ```

   This will start the HTTP server at `http://127.0.0.1:8000/mcp/`.

### Docker

Pull the pre-built image from the openSUSE registry:

```bash
docker pull registry.opensuse.org/opensuse/mcp-bugzilla:latest
```

Run the container:

```bash
docker run -p 8000:8000 \
  registry.opensuse.org/opensuse/mcp-bugzilla:latest \
  --bugzilla-server https://bugzilla.example.com \
  --host 0.0.0.0 \
  --port 8000
```

You can also pass configuration via environment variables:

```bash
docker run -p 8000:8000 \
  -e BUGZILLA_SERVER=https://bugzilla.example.com \
  -e MCP_HOST=0.0.0.0 \
  -e MCP_PORT=8000 \
  registry.opensuse.org/opensuse/mcp-bugzilla:latest
```

Podman works identically — just substitute `docker` with `podman`.

## Configuration

### Command-Line Arguments

The `mcp-bugzilla` command supports the following options:

| Argument | Environment Variable | Default | Description |
|----------|---------------------|---------|-------------|
| `--bugzilla-server <URL>` | `BUGZILLA_SERVER` | *Required* | Base URL of the Bugzilla server (e.g., `https://bugzilla.example.com`) |
| `--transport {http,stdio}` | `MCP_TRANSPORT` | `http` | Transport for the MCP server. `stdio` is for direct subprocess launches by an MCP client; `http` exposes a network endpoint |
| `--host <ADDRESS>` | `MCP_HOST` | `127.0.0.1` | Host address for the MCP server to listen on (http transport only) |
| `--port <PORT>` | `MCP_PORT` | `8000` | Port for the MCP server to listen on (http transport only) |
| `--mcp-auth-header <HEADER>` | `MCP_AUTH_HEADER` | *none* | HTTP header name that clients use to send the Bugzilla API key to this MCP server (http transport only). When not specified, inbound header authentication is disabled and client headers are ignored. |
| `--bugzilla-api-key <KEY>` | `BUGZILLA_API_KEY` | *none* | Static Bugzilla API key. Optional: if omitted and not provided per-request via `--mcp-auth-header` (http), access is **anonymous**. For `--transport stdio` this is the only source of the key |
| `--bugzilla-auth-mode {query,bearer}` | `BUGZILLA_AUTH_MODE` | `query` | How to authenticate with Bugzilla: `query` sends `?api_key=<KEY>` (default, works with most instances); `bearer` sends `Authorization: Bearer <KEY>` header (required for Red Hat Bugzilla and similar) |
| `--read-only` | `MCP_READ_ONLY` | `False` | Disables all tools which can modify a bug. Works well in conjunction with `MCP_BUGZILLA_DISABLED_METHODS` |
| `--download-dir <DIR>` | `BUGZILLA_DOWNLOAD_DIR` | `<tmpdir>/mcp-bugzilla` | Directory where `download_attachment` writes binary/oversized attachments. The default directory is created on first use and restricted to the owner (`0o700`); an explicit `output_dir` keeps its own permissions |

**Note**: `--host` and `--port` are rejected with an error when used together with `--transport stdio`.

**Note**: Command-line arguments take precedence over environment variables.

#### Deprecated Arguments

The following arguments are **deprecated** and will be removed in a future release. They continue to work
and emit a warning in the logs when used. Migrate to the replacements shown:

| Deprecated | Replacement | Notes |
|------------|-------------|-------|
| `--api-key` / `BUGZILLA_API_KEY` (old semantics) | `--bugzilla-api-key` / `BUGZILLA_API_KEY` | Same env var; the new arg makes the intent explicit. Previously required for stdio; now optional (anonymous if absent) |
| `--api-key-header` / `MCP_API_KEY_HEADER` | `--mcp-auth-header` / `MCP_AUTH_HEADER` | Clarifies that this controls the *inbound* client→MCP header |
| `--use-auth-header` | `--bugzilla-auth-mode bearer` | Replaces a boolean flag with an explicit mode choice |

### Environment Variables

You can configure the server using environment variables instead of command-line arguments:

```bash
export BUGZILLA_SERVER=https://bugzilla.example.com
export MCP_HOST=127.0.0.1
export MCP_PORT=8000
export MCP_AUTH_HEADER=ApiKey
export BUGZILLA_API_KEY=your_api_key   # optional; omit for anonymous access
export BUGZILLA_AUTH_MODE=query        # or "bearer" for Red Hat Bugzilla
export LOG_LEVEL=INFO  # Optional: DEBUG, INFO, WARNING, ERROR, CRITICAL
export MCP_READ_ONLY=true  # Optional: set to true to disable write operations
# Selective Disabling Tools (Optional)
# Works fine in conjunction with --read-only flag
export MCP_BUGZILLA_DISABLED_METHODS='bug_info,bug_comments'

mcp-bugzilla
```

### Methods Disabling

The server allows you to selectively disable specific tools or prompts using an environment variable. This is useful for restricting functionality based on security or resource requirements.

**Method**: `MCP_BUGZILLA_DISABLED_METHODS=tool1,tool2`


## Usage

### Starting the Server

Start the server with your Bugzilla instance URL:

```bash
mcp-bugzilla --bugzilla-server https://bugzilla.opensuse.org
```

The server will start listening on `http://127.0.0.1:8000/mcp/` by default.

#### Stdio transport

For MCP clients that launch the server as a subprocess and speak over stdin/stdout (e.g. Claude Desktop-style configs), use `--transport stdio`. To authenticate with Bugzilla, provide the API key via `BUGZILLA_API_KEY` or `--bugzilla-api-key`. If the key is omitted, access is **anonymous**:

```bash
# Authenticated stdio
BUGZILLA_API_KEY=your_api_key \
  mcp-bugzilla --bugzilla-server https://bugzilla.opensuse.org --transport stdio

# Anonymous stdio (public Bugzilla instances)
mcp-bugzilla --bugzilla-server https://bugzilla.opensuse.org --transport stdio --read-only
```

`--host` and `--port` are not valid in stdio mode and will cause the server to exit with an error.

### Endpoint

The MCP server exposes an HTTP endpoint at:

```
http://<host>:<port>/mcp/
```

For example, with default settings:
```
http://127.0.0.1:8000/mcp/
```

### Authentication

Authentication is **optional**. If no API key is provided, the server accesses Bugzilla anonymously, which works for public read-only operations on Bugzilla instances that allow anonymous access.

#### Sending the API key from the MCP client (http transport)

Include the Bugzilla API key in the HTTP request header sent to this MCP server. To enable inbound header authentication, you must specify the header name using `--mcp-auth-header <HEADER>` (e.g., `--mcp-auth-header ApiKey`) or configure it via the `MCP_AUTH_HEADER` environment variable (e.g., `MCP_AUTH_HEADER=ApiKey`). If not configured, any inbound client headers are ignored:

```http
POST /mcp/ HTTP/1.1
Host: 127.0.0.1:8000
ApiKey: YOUR_API_KEY_HERE
Content-Type: application/json
```

If the client omits this header (or if inbound header authentication is disabled), the server falls back to the `--bugzilla-api-key` / `BUGZILLA_API_KEY` static key, or uses anonymous access if that is also unset.

**Example with curl**:
```bash
curl -X POST http://127.0.0.1:8000/mcp/ \
  -H "ApiKey: YOUR_API_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "server_url_resource"}, "id": 1}'
```

### MCP → Bugzilla Authentication Modes

The `--bugzilla-auth-mode` argument controls how this server authenticates with Bugzilla:

#### Mode `query` (Default)

Sends the API key as the `api_key` query parameter in every Bugzilla REST request:
```bash
mcp-bugzilla --bugzilla-server https://bugzilla.example.com
# Equivalent: --bugzilla-auth-mode query
```

This works with standard Bugzilla instances (Mozilla Bugzilla, openSUSE Bugzilla, etc.).

#### Mode `bearer` (for enterprise instances)

Some Bugzilla instances (such as Red Hat Bugzilla) require the API key to be sent via the `Authorization: Bearer` header. Use `--bugzilla-auth-mode bearer` for these instances:
```bash
mcp-bugzilla --bugzilla-server https://bugzilla.redhat.com --bugzilla-auth-mode bearer
```

**When to use `bearer` mode:**
- Red Hat Bugzilla (bugzilla.redhat.com)
- Other enterprise Bugzilla instances that reject `api_key` query parameters
- Bugzilla instances with strict authentication requirements

**Note**: `--mcp-auth-header` controls which header the *MCP clients* use to pass the key *to this server*; `--bugzilla-auth-mode` controls how *this server* forwards it *to Bugzilla*. These are independent settings.

### MCP Client Integration

The server follows the [Model Context Protocol (MCP) specification](https://modelcontextprotocol.io/). It can be integrated with any MCP-compatible client.

**Example MCP client configuration** (format may vary by client):

```json
{
  "mcpServers": {
    "bugzilla": {
      "url": "http://127.0.0.1:8000/mcp/",
      "headers": {
        "ApiKey": "YOUR_API_KEY_HERE"
      }
    }
  }
}
```

## API Reference

### Tool Response Format

All tools return JSON responses following the MCP protocol. Successful responses include the tool's return value, while errors include detailed error messages.

### Error Handling

The server provides detailed error messages for common issues:

- **Authentication**: API key is optional; if absent, access is anonymous. If Bugzilla returns 401, your key may be invalid or the instance requires authentication.
- **Invalid Bug ID**: Returns `ToolError` with details if a bug ID doesn't exist
- **API Errors**: Returns `ToolError` with the HTTP status code and error message from Bugzilla
- **Network Errors**: Returns `ToolError` for connection or timeout issues

### Logging

The server includes structured logging with color-coded output:

- **LLM Requests/Responses**: Cyan - Shows tool calls from MCP clients
- **Bugzilla API Requests/Responses**: Green - Shows HTTP requests to Bugzilla
- **Errors**: Red - Shows error messages

Set the log level using the `LOG_LEVEL` environment variable:
- `DEBUG`: Detailed debugging information
- `INFO`: General informational messages (default)
- `WARNING`: Warning messages
- `ERROR`: Error messages only
- `CRITICAL`: Critical errors only

## Examples

### Example 1: Get Bug Information

```python
# MCP client call
result = client.call_tool("bug_info", {"bug_ids": [12345]})
print(result)
# Returns complete bug details including status, assignee, summary, etc.
```

### Example 2: Search for Bugs

```python
# Search for new bugs in Firefox product
result = client.call_tool(
    "bugs_quicksearch", {"query": "product:Firefox status:NEW", "limit": 10}
)
# Returns list of bugs with essential fields
```

### Example 3: Add a Comment

```python
# Add a public comment to a bug
result = client.call_tool(
    "add_comment",
    {
        "bug_id": 12345,
        "comment": "This issue has been resolved in version 2.0",
        "is_private": False,
    },
)
# Returns: {"id": 67890} - the new comment ID
```

### Example 4: Get Bug History and Comments

```python
# Get the history of changes for a bug
history = client.call_tool(
    "bug_history",
    {"id": 12345, "new_since": datetime.fromisoformat("2026-01-01T00:00:00")},
)

# Get all public comments
public_comments = client.call_tool(
    "bug_comments", {"id": 12345, "include_private_comments": False}
)

# Get all comments including private ones
all_comments = client.call_tool(
    "bug_comments", {"id": 12345, "include_private_comments": True}
)
```

### Example 5: Quicksearch Syntax Examples

```python
# Search by product and status
bugs_quicksearch("product:Firefox status:NEW")

# Search by assignee
bugs_quicksearch("assigned_to:user@example.com")

# Search by component
bugs_quicksearch("component:Core status:RESOLVED")

# Search with multiple criteria
bugs_quicksearch("product:Firefox component:JavaScript status:NEW priority:P1")

# Use pagination
page1 = bugs_quicksearch("status:NEW", limit=50, offset=0)
page2 = bugs_quicksearch("status:NEW", limit=50, offset=50)
```

### Example 6: Using with Red Hat Bugzilla

Red Hat Bugzilla requires `Authorization: Bearer` authentication instead of the default `api_key` query parameter. Use `--bugzilla-auth-mode bearer`:
```bash
mcp-bugzilla \
  --bugzilla-server https://bugzilla.redhat.com \
  --bugzilla-auth-mode bearer \
  --host 127.0.0.1 \
  --port 8000

# Client sends the key in the ApiKey header as usual
curl -X POST http://127.0.0.1:8000/mcp/ \
  -H "ApiKey: YOUR_API_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "server_url"}, "id": 1}'
```

### Example 7: Update Bug Status
```python
# Close a bug as fixed
result = client.call_tool(
    "update_bug_status",
    {
        "bug_id": 12345,
        "status": "CLOSED",
        "resolution": "FIXED",
        "comment": "Fixed in commit abc123",
    },
)
```

### Example 8: Assign a Bug
```python
# Assign bug to a developer
result = client.call_tool(
    "assign_bug",
    {
        "bug_id": 12345,
        "assignee": "developer@example.com",
        "comment": "Please review this regression",
    },
)
```

### Example 9: Mark as Duplicate
```python
# Mark bug as duplicate and close it
result = client.call_tool(
    "mark_as_duplicate",
    {
        "bug_id": 12345,
        "duplicate_of": 67890,
        "comment": "This is a duplicate of the original report",
    },
)
# Bug is automatically closed with resolution DUPLICATE
```

## Troubleshooting

### Server Won't Start

**Issue**: Server fails to start with "bugzilla-server argument required"

**Solution**: Ensure you provide the `--bugzilla-server` argument or set the `BUGZILLA_SERVER` environment variable:
```bash
mcp-bugzilla --bugzilla-server https://bugzilla.example.com
```

### Authentication Errors

**Issue**: Receiving 401 Unauthorized from Bugzilla

**Solution**: 
1. Ensure you have enabled inbound header authentication (e.g., via `--mcp-auth-header ApiKey` or `MCP_AUTH_HEADER=ApiKey`) and that you're sending the API key in the correct HTTP header.
2. Verify the API key is valid and not expired
3. Check that `--bugzilla-auth-mode` matches what the Bugzilla instance expects (`query` for standard instances, `bearer` for Red Hat Bugzilla)

### API Errors

**Issue**: Receiving `ToolError` with HTTP status codes

**Common causes**:
- **401 Unauthorized**: Invalid or expired API key
- **404 Not Found**: Bug ID doesn't exist or you don't have permission to view it
- **403 Forbidden**: Insufficient permissions for the requested operation
- **500 Internal Server Error**: Bugzilla server error

**Solution**: Check the error message for details and verify your API key permissions.

### Connection Issues

**Issue**: Cannot connect to Bugzilla server

**Solution**:
1. Verify the Bugzilla server URL is correct and accessible
2. Check network connectivity
3. Ensure the Bugzilla instance has REST API enabled
4. Verify firewall rules allow outbound connections

### Logging Issues

**Issue**: Not seeing enough (or too much) log output

**Solution**: Adjust the `LOG_LEVEL` environment variable:
```bash
export LOG_LEVEL=DEBUG  # For more verbose output
export LOG_LEVEL=ERROR  # For minimal output
```

## License

This project is licensed under the Apache 2.0 License. See the [LICENSE](https://www.apache.org/licenses/LICENSE-2.0.txt) file for details.

**Author**: Sai Karthik <kskarthik@disroot.org>

**Contributors**: https://github.com/openSUSE/mcp-bugzilla/graphs/contributors
