mcp-issue-tracker
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はまさにその正当化です。これは、パターンの読み取り側だけでなく、より具体的に役立つデモです。
相互参照:mcp-starter-template と共有するパターン
このサーバーは、このポートフォリオのプロジェクト2である兄弟プロジェクト mcp-starter-template からセキュリティアーキテクチャを意図的に再利用しており、再設計はしていません:
パターン |
|
|
設定駆動のツール分類 |
| 同じ構成、同じファイル名 — |
起動時のコード/設定の相互チェック |
| ほぼそのまま移植 — |
デフォルト読み取り専用 |
| 同一 — さらに |
認証パススルー |
| 同一設計、ドメインに適したユーザー( |
構造化エラー |
| 同一、issue検索用に |
監査証跡 |
| 同一のデュアルシンク設計 |
レート制限 |
| 同一、さらにそれを公開する |
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個を要求;書き込み操作は仕様の非目標に従って明示的に正当化)
ツール | 読み取り/書き込み | コスト | 説明 |
| 読み取り | 1 | 可視のissueを全文検索し、 |
| 読み取り | 1 | 本文、ラベル、すべてのコメントを含む完全な詳細 |
| 読み取り | 1 | トラッカーが把握しているすべてのラベル |
| 読み取り | 1 | 決定的な抽出型要約 — LLM呼び出しなし(下記参照) |
| 読み取り | 0 | このウィンドウ内での呼び出し元の残り呼び出し/コスト予算 |
| 書き込み | 5 | 呼び出し元のチームにスコープされたissueを作成 |
| 書き込み | 3 | 可視でオープンなissueにコメント |
| 書き込み | 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に置き換えることになります。
環境変数の上書き
変数 | 目的 | デフォルト |
| SQLite DBパス |
|
|
| リポジトリルートの |
| JSONL監査ログのパス | 未設定の場合は無効 |
| SQLite監査ログのパス | 未設定の場合はインメモリ |
|
|
|
| 許可リストに追加するツール名のカンマ区切りリスト |
|
モックユーザー
トークン | ユーザー | チーム | 管理者 |
| Alice Nguyen | engineering | いいえ |
| Bob Reyes | docs | いいえ |
| 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/ -v88テスト、すべて成功。 カバレッジ:
test_identity_auth.py— モックIdP解決、欠落/無効なトークンの認証パススルー拒否、フォールバックIDなしtest_registry.py— デフォルト読み取り専用、許可リストによるゲーティング、コード/設定の分類不一致は起動時に即座に失敗test_limiter.py— 固定ウィンドウのバジェット、セッションごとの分離、ウィンドウリセット、retry_aftertest_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をサブプロセスとして起動し、実際のmcpSDKの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.0SQLite(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 を参照。
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Run AI customer support from your terminal: conversations, knowledge base, and chat widget.
Shortcut project management. Create, update, search stories and manage workflows.
Securely search and manage workspace context files for AI agents and teams.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/HamzaOuadid/mcp-issue-tracker'
If you have feedback or need assistance with the MCP directory API, please join our Discord server