Skip to main content
Glama
HamzaOuadid

mcp-issue-tracker

by HamzaOuadid

mcp-issue-tracker

実在するローカルのSQLiteベースのissueトラッカー上に構築されたMCPサーバーです。完全なCRUD(検索、取得、要約、作成、コメント、クローズ/再オープン)、実際にシードされたコーパス、そしてこのポートフォリオの他のMCPプロジェクト全体で使用されているものと同じ認証パススルー+デフォルト読み取り専用のセキュリティパターンを備えています。

20プロジェクトからなるポートフォリオのプロジェクト10として構築されました:「以前のプロジェクトと同じセキュリティ哲学を共有しつつ、異なるドメインに適用した、2つ目の独立したMCPサーバー実装」。

どのバリアントか、そしてその理由

仕様(10-second-mcp-server-docs-wiki-search-or-issue-tracker.md)では、docs/wiki検索かissueトラッカーのどちらかを選択できました。私はissueトラッカーを構築しました。

理由:docs/wikiサーバーは本質的に静的コンテンツに対する2つのツール(search、fetch)です。issueトラッカーには、実在するデータモデル(issues、コメント、ラベル、ステータス遷移)、実際の認可判断(誰が何を見られるか、誰が何を書けるか)、そしてセキュリティパターンの書き込みゲート側を実演する自然な場所が必要です。仕様の非目標は、参照実装と「同じ方法で明示的に正当化されゲートされる」場合に書き込み操作を明示的に許可しており、CRUDはまさにその正当化です。これは、パターンの読み取り側だけでなく、より具体的に役立つデモです。

Related MCP server: Lific

相互参照:mcp-starter-template と共有するパターン

このサーバーは、このポートフォリオのプロジェクト2である兄弟プロジェクト mcp-starter-template からセキュリティアーキテクチャを意図的に再利用しており、再設計はしていません:

パターン

mcp-starter-template

mcp-issue-tracker(このリポジトリ)

設定駆動のツール分類

server.yaml: tools.<name>.read_only

同じ構成、同じファイル名 — server.yaml

起動時のコード/設定の相互チェック

registry.py: 不一致時に ToolRegistrationError

ほぼそのまま移植 — registry.py

デフォルト読み取り専用

allowed_write_tools にない書き込みツールは拒否

同一 — さらに dry_run による第2のゲートを追加(下記参照)

認証パススルー

auth.py + identity.py: モックベアラートークンは実際の User に解決され、共有資格情報は使われない

同一設計、ドメインに適したユーザー(token-alice/token-bob/token-admin)

構造化エラー

errors.py: MCPError{code, message, retry_after?}

同一、issue検索用に +NOT_FOUND

監査証跡

audit.py: JSONL + SQLite audit_log、すべての呼び出しを記録

同一のデュアルシンク設計

レート制限

limiter.py: 固定ウィンドウのセッションごとの上限

同一、さらにそれを公開する get_rate_status ツールを追加(仕様の api_rate_state データモデルをクエリ可能にしたもの)

mcp-starter-template は、自身の相互参照セクションでこのリポジトリにリンクを戻しているため、このパターンは双方向から文書化されています。

アーキテクチャ

Claude Desktop / Claude Code (MCP client)
        │  JSON-RPC over stdio
        ▼
  server.py            FastMCP tool definitions (mcp SDK) — 8 tools
        │
        ▼
  service.py            Guarded dispatch: auth → rate-limit → write-gate → dry-run → audit
        │
        ├── auth.py + identity.py    Bearer-token → User (mock IdP, never a shared credential)
        ├── config.py                Loads/validates server.yaml
        ├── registry.py              Tool read/write classification, code/config cross-check
        ├── limiter.py                Per-caller fixed-window rate/spend budget
        ├── audit.py                  Every call → JSONL + SQLite audit_log
        │
        ▼
  db.py                  Real SQLite CRUD: issues / comments / labels / issue_labels
        │
        ▼
  seed_data.py            15 real, hand-authored issues for the sibling `ragbench` project

すべてのツール呼び出しは1つのパイプラインです:認証 → レート制限 →(書き込みの場合)許可リストチェック →(書き込みの場合)ドライランまたは実実行 → 監査ログ。いずれかの段階で拒否された場合、構造化された MCPError が発生し(クラッシュも黙って無視されることもありません)、それでも監査証跡に書き込まれます。

データモデル

  • issues(id, title, body, status, team, created_by, assignee, created_at, updated_at)

  • comments(id, issue_id, author, body, created_at)

  • labels(id, name) / issue_labels(issue_id, label_id) — 多対多

  • audit_log(timestamp, session_id, user_id, tool_name, read_or_write, dry_run, allowed, latency_ms, error_code, detail) — 仕様のデータモデルと完全に一致

  • schema_meta(key, value) — schema_version を固定(リスクの「ターゲットAPIバージョン」のエッジケースを参照)

ツール(8個 — 仕様は3〜5個を要求;書き込み操作は仕様の非目標に従って明示的に正当化)

ツール

読み取り/書き込み

コスト

説明

search_issues

読み取り

1

可視のissueを全文検索し、status/labelでフィルタ

get_issue

読み取り

1

本文、ラベル、すべてのコメントを含む完全な詳細

list_labels

読み取り

1

トラッカーが把握しているすべてのラベル

summarize_issue

読み取り

1

決定的な抽出型要約 — LLM呼び出しなし(下記参照)

get_rate_status

読み取り

0

このウィンドウ内での呼び出し元の残り呼び出し/コスト予算

create_issue

書き込み

5

呼び出し元のチームにスコープされたissueを作成

add_comment

書き込み

3

可視でオープンなissueにコメント

set_issue_status

書き込み

3

issueをオープン/クローズ

summarize_issue にLLMがない理由: この環境にはLLM APIキーが設定されておらず、このツールの役割は外部のLLMクライアント(Claude Desktopなど)に実データを渡すことです。つまり、それ自体がLLMを呼び出すことを想定していません。要約は純粋な文字列ロジックです:タイトル+ステータス+ラベル+切り詰めた本文スニペット+コメント数+最新のコメント。決定的で、テスト可能で、その実態を正直に示しています。

セキュリティモデル(具体的に)

  • 認証パススルー: すべてのツールは token 引数を受け取ります。これはモックのインメモリIDプロバイダーを介して実際の User(user_id、team、is_admin)に解決されます — mcp-starter-template と同じDEV-ONLYパターンで、同じように文書化されています(identity.py のdocstringは、本番デプロイではこれを実際の資格情報検証に置き換える必要があることを明示しています)。フォールバックIDはありません:トークンがない、または無効な場合は常に UNAUTHENTICATED になります。

  • チームスコープの可視性: team=NULL のissueは公開です。それ以外は、同じチームの呼び出し元または管理者のみが表示できます。token-alice(エンジニアリング)と token-bob(ドキュメント)は、同じ search_issues("") 呼び出しから異なる結果セットを参照します — これは単なる主張ではなく、テストで直接検証されています。

  • デフォルト読み取り専用、2段階のゲート: 書き込みツールは、その名前が allowed_write_tools にない限り WRITE_NOT_ALLOWED で拒否されます。それでも、グローバルな dry_run フラグ(デフォルトでオン)により、データベースに触れる代わりに合成の {"dry_run": true, "would_create": {...}} プレビューを返します。実際の変更が発生するには、両方のゲートを明示的に開く必要があります。

  • レート制限: 固定ウィンドウ、呼び出し元トークンごとの予算(calls_per_min と cost_per_session、ツールコストはレジストリから取得)。セッション途中でこれを使い切ると、そのウィンドウ内の後続のすべての呼び出しで retry_after 付きの RATE_LIMIT_EXCEEDED が返されます — プロセス自体は決してクラッシュせず、他の呼び出し元にも影響しません(仕様のエッジケースに従って明示的にテスト済み)。

  • 監査ログ: すべての呼び出し — 許可されたものも拒否されたものも、実実行もドライランも — audit_log(JSONL + SQLite)の1行になります。

インストール

git clone https://github.com/HamzaOuadid/mcp-issue-tracker.git
cd mcp-issue-tracker
pip install -e .

Python 3.10+ が必要です。依存関係:mcp(公式Python MCP SDK)、pydantic、PyYAML — いずれも上記のコマンドでインストールされます。

使い方

直接実行

mcp-issue-tracker

これにより、stdio(標準のMCPトランスポート)上でサーバーが起動します。ターミナルから対話的に実行するためのものではなく、MCPクライアントから起動するためのものです。手動で試す場合は、代わりに同梱のデモスクリプトを使用してください(下記参照)。

Claude Desktop への登録

claude_desktop_config.json に追加します(Windows: %APPDATA%\Claude\claude_desktop_config.json、macOS: ~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "issue-tracker": {
      "command": "mcp-issue-tracker",
      "args": [],
      "env": {
        "MCP_ISSUE_TRACKER_DB": "C:/Users/you/.mcp-issue-tracker/issue_tracker.db",
        "MCP_ISSUE_TRACKER_CONFIG": "C:/path/to/mcp-issue-tracker/server.yaml"
      }
    }
  }
}

(mcp-issue-tracker がPATHにない場合は、代わりに command をインタープリターに指定します:"command": "python", "args": ["-m", "mcp_issue_tracker.server"] で、"cwd" をリポジトリルートに設定するか、venvの mcp-issue-tracker.exe のフルパスを使用します。)

Claude Desktopを再起動します。*「issueトラッカーでragbenchのバグを検索して、token-aliceを使って」*のような依頼をしてみてください。Claudeが search_issues を呼び出してくれます。すべてのツールには token 引数が必要です(下記の モックユーザー を参照)。本番デプロイでは、mcp-starter-template の文書化されたアップグレードパスと同様に、これを実際のユーザーごとのOAuthに置き換えることになります。

環境変数の上書き

変数

目的

デフォルト

MCP_ISSUE_TRACKER_DB

SQLite DBパス

~/.mcp-issue-tracker/issue_tracker.db

MCP_ISSUE_TRACKER_CONFIG

server.yaml へのパス

リポジトリルートの server.yaml

MCP_ISSUE_TRACKER_AUDIT_JSONL

JSONL監査ログのパス

未設定の場合は無効

MCP_ISSUE_TRACKER_AUDIT_DB

SQLite監査ログのパス

未設定の場合はインメモリ

MCP_ISSUE_TRACKER_DRY_RUN

dry_run を上書き(true/false)

server.yaml の値(true)

MCP_ISSUE_TRACKER_ALLOWED_WRITES

許可リストに追加するツール名のカンマ区切りリスト

server.yaml の値(空)

モックユーザー

トークン

ユーザー

チーム

管理者

token-alice

Alice Nguyen

engineering

いいえ

token-bob

Bob Reyes

docs

いいえ

token-admin

Priya Shah

engineering

はい(すべてのチームを閲覧可)

実際の実行で書き込みを有効にする

デフォルトでは、すべての書き込みツールは拒否されます(WRITE_NOT_ALLOWED)。実際にissue/コメント/ステータス変更を作成するには:

export MCP_ISSUE_TRACKER_ALLOWED_WRITES="create_issue,add_comment,set_issue_status"
export MCP_ISSUE_TRACKER_DRY_RUN=false
mcp-issue-tracker

(PowerShell: $env:MCP_ISSUE_TRACKER_ALLOWED_WRITES = "create_issue,add_comment,set_issue_status"、$env:MCP_ISSUE_TRACKER_DRY_RUN = "false"。)

デモ実行(実際の出力)

scripts/demo.py によって生成されます。このスクリプトは python -m mcp_issue_tracker.server で実際のサーバーを起動し、stdio経由で実際の mcp SDKクライアント(mcp.client.stdio + ClientSession)を使って操作します — これはプロトコルが実際に返す内容であり、手入力ではありません:

$ list_tools()
  - search_issues: Search issues visible to the caller (team-scoped + public issues).
  - get_issue: Fetch one issue's full detail: body, labels, and every comment.
  - list_labels: List every label known to the tracker.
  - summarize_issue: Deterministic extractive summary of one issue (no LLM call).
  - get_rate_status: Report the caller's remaining call/cost budget for the current rate-limit window.
  - create_issue: Create a new issue, scoped to the caller's team. Write, allowlist-gated, dry-run by default.
  - add_comment: Add a comment to an existing, visible, open issue. Write, allowlist-gated, dry-run by default.
  - set_issue_status: Open or close an issue. Write, allowlist-gated, dry-run by default.

$ search_issues(token="token-alice", query="ragbench eval")
  {
    "count": 3,
    "results": [
      {
        "id": 7,
        "title": "gate.py exits 0 even when --baseline file is missing",
        "status": "open",
        "team": "engineering",
        "labels": ["bug", "ci"]
      },
      {
        "id": 2,
        "title": "Support --k as a single int, not just a comma list",
        "status": "open",
        "team": null,
        "labels": ["cli", "enhancement"]
      },
      {
        "id": 1,
        "title": "eval crashes on queries.jsonl with a duplicate query_id",
        "status": "open",
        "team": "engineering",
        "labels": ["bug", "eval"]
      }
    ]
  }

$ search_issues(token="token-bob", label="docs")   # bob is on the docs team
  {
    "count": 3,
    "results": [
      { "id": 15, "title": "CLI help text for `ragbench eval --rerank` doesn't mention offline fallback", "team": "docs" },
      { "id": 8,  "title": "Add a copy-paste example for `report --format html` to the README", "team": "docs" },
      { "id": 4,  "title": "README missing a pointer to the pgvector migration path", "team": "docs" }
    ]
  }

$ get_issue(token="token-admin", issue_id=1)
  {
    "id": 1,
    "title": "eval crashes on queries.jsonl with a duplicate query_id",
    "status": "open",
    "team": "engineering",
    "labels": ["bug", "eval"],
    "comments": [
      { "id": 1, "author": "root-admin",
        "body": "Confirmed on a 40-query file with one accidental duplicate id. Repro attached in the linked gist." }
    ]
  }

$ summarize_issue(token="token-admin", issue_id=1)
  #1 "eval crashes on queries.jsonl with a duplicate query_id" (open) [bug, eval]: Running `ragbench eval
  ./index --queries queries.jsonl` raises an unhandled KeyError deep in metrics.py when two lines in the
  query file share the same query_id... | 1 comment(s); most recent from root-admin: "Confirmed on a
  40-query file with one accidental duplicate id. Repro attached in the linked gist."

$ list_labels(token="token-alice")
  ["bug", "ci", "cli", "docs", "dx", "enhancement", "eval", "good-first-issue",
   "hybrid", "ingest", "ops", "performance", "question", "rerank", "windows"]

$ get_rate_status(token="token-alice")
  { "calls_remaining": 27, "cost_remaining": 98, "reset_at_seconds": 59.938 }

$ create_issue(...)   # default config: write tools are NOT allowlisted
  ERROR: [WRITE_NOT_ALLOWED] Write tool 'create_issue' is not enabled. Add it to
  allowed_write_tools in server.yaml (or MCP_ISSUE_TRACKER_ALLOWED_WRITES) to allow it.

$ get_issue(token="token-bob", issue_id=1)   # issue 1 is engineering-scoped, bob is docs
  ERROR: [NOT_FOUND] Issue 1 was not found or is not visible to you.

$ search_issues(token="not-a-real-token")   # missing/invalid token
  ERROR: [UNAUTHENTICATED] Missing or invalid identity token; call rejected.

自分で再現するには:

python scripts/demo.py

テスト

pip install -e ".[dev]"
pytest tests/ -v

88テスト、すべて成功。 カバレッジ:

  • test_identity_auth.py — モックIdP解決、欠落/無効なトークンの認証パススルー拒否、フォールバックIDなし

  • test_registry.py — デフォルト読み取り専用、許可リストによるゲーティング、コード/設定の分類不一致は起動時に即座に失敗

  • test_limiter.py — 固定ウィンドウのバジェット、セッションごとの分離、ウィンドウリセット、retry_after

  • test_audit.py — JSONL + SQLiteのデュアルシンクログ、拒否された呼び出しは error_code を保持

  • test_db.py — 実際のSQLite CRUD、チームスコープの可視性、SQLインジェクション形式の入力でもクラッシュや漏洩が発生しない

  • test_tools_issues.py — 決定論的な要約、引数検証

  • test_service_read.py — 実際にシードされたコーパスに対する4つの読み取りツールのエンドツーエンド(「同じクエリでも2人のユーザーは異なる結果を見る」を含む)

  • test_service_write.py — デフォルトでは書き込み不可、ドライランのプレビューと実際の変更、クローズ済みIssueへのコメントブロック、クロスチームの書き込み拒否

  • test_edge_cases.py — セッション中のレート制限枯渇はグレースフルに劣化(クラッシュしない)、スキーマバージョンの固定、SQLインジェクション安全性、設定欠落時のフォールバック

  • test_server_integration.py — 実際のMCPプロトコルに対するエンドツーエンド: python -m mcp_issue_tracker.server をサブプロセスとして起動し、実際の mcp SDKのstdioクライアント(ClientSession)で駆動して、list_tools() と call_tool() が本物のJSON-RPC上で動作することを確認する(手作りの代用品ではない)

88 passed, 1 warning in ~15-27s

環境

  • Python 3.10+

  • mcp>=1.2.0(公式Python MCP SDK — pip install mcp)、pydantic>=2.0、PyYAML>=6.0

  • SQLite(Pythonにバンドル)— 起動するサーバーなし、このポートフォリオの他の部分の「Postgres/Dockerの代わりにSQLite」という慣例に一致

  • LLM APIキーは使用も必要もない — summarize_issue は純粋な文字列ロジック(Architecture参照)

リスク / 未解決の質問

  • 仕様の「ライブ公開API」という枠組みからの逸脱。 仕様のセクション5/10/11は、ライブのサードパーティAPI(例: 実際のGitHub Issues API)を、そのAPI自身のクォータに対する実際のレート制限でラップすることを説明しています。このビルドは代わりに、このポートフォリオイニシアチブの明示的な環境ノート(LLMキーなし、タスクが許せばライブのサードパーティ依存よりローカルデータを優先)に従い、真のCRUDを備えた実際のローカルSQLiteバックエンドのトラッカーを使用します。結果:仕様のデータモデルにおける api_rate_state は、サードパーティAPIのクォータではなく、このサーバー自身の 呼び出し元ごとのバジェットとして実装されています(get_rate_status で公開);「対象とするAPIバージョンの文書化」というエッジケースは、代わりに固定されたローカルの schema_version として実装されています。どちらもコード内(limiter.py、db.py)にインラインで注記されており、置き換えが暗黙的になりません。

  • モックIDであり、実際のIdPではない。 明示的にDEV-ONLYであり、identity.py のdocstringに文書化されています — mcp-starter-template と同じ姿勢です。実際のデプロイでは、AuthMiddleware の前にOAuth/JWT/mTLSが必要です。

  • シングルライターSQLite。 デモ/ポートフォリオサーバーには十分;同時マルチライターのデプロイには実際のデータベースが必要です(ragbench のREADMEが自身のSQLite使用について明示的に示しているのと同じトレードオフ)。

  • 仕様のマイルストーン(セクション8)に対するスコープ削減: 監査ログを照会するための独立したCLIはありません(AuditLogger.query() または sqlite3 issue_tracker.db で直接照会します);タグ付きリリース(git tag)はありません — 公開後にリポジトリ所有者に委ねられます;ラベル管理には専用の delete_label/rename_label ツールはありません(ラベルは書き込み時作成のみで、仕様が求めていない管理画面を過剰に構築せずにパターンを実証するには十分です)。

ポートフォリオノート

このプロジェクトと mcp-starter-template は、意図的に同じポイントを2回伝えるために存在しています。MCPのセキュリティ哲学(認証パススルー、デフォルト読み取り専用、監査、レート制限)は、繰り返し可能なパターンであり、一度きりのものではありません。同じモジュール、同じテストアプローチ、同じ障害モードを同じ方法で処理 — 一方のリポジトリではドキュメント/設定ドメインに、もう一方では実際のIssueトラッカーに適用されています。

ライセンス

MIT — LICENSE を参照。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables agents to read and drive a local-first Kanban board for issue tracking, allowing them to list, create, update, and resolve issues from Claude Code sessions.
    11 npm
    1
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI clients to search and retrieve customer and support-ticket data from a SQLite database, and to create support tickets only when an explicit approval flag is supplied, with all actions validated and audit-logged.
    -
  • A
    license
    B
    quality
    C
    maintenance
    Enables local engineering workflow management by consolidating tickets, QA evidence, time tracking, root cause investigation, knowledge, and reporting into a single SQLite database, allowing generation of complete ticket packages for handoffs, dailies, or career evidence.
    23
    9 npm
    MIT