mcp-issue-tracker
mcp-issue-tracker
실제 로컬 SQLite 기반 이슈 트래커 위에 구축된 MCP 서버 — 전체 CRUD(검색, 조회, 요약, 생성, 댓글, 닫기/재열기), 실제 시드 데이터, 그리고 이 포트폴리오의 다른 MCP 작업에서 사용된 것과 동일한 인증 패스스루 + 기본 읽기 전용 보안 패턴을 제공합니다.
20개 프로젝트 포트폴리오의 프로젝트 10으로 구축: "이전 구현과 동일한 보안 철학을 공유하면서 다른 도메인에 적용한 두 번째 독자적 MCP 서버 구현."
어떤 변형을 선택했고, 왜 그런가
스펙(10-second-mcp-server-docs-wiki-search-or-issue-tracker.md)은 문서/위키 검색 또는 이슈 트래커 중 선택하도록 했습니다. 저는 이슈 트래커를 구축했습니다.
이유: 문서/위키 서버는 본질적으로 정적 콘텐츠에 대한 두 가지 도구(search, fetch)에 불과합니다. 이슈 트래커는 실제 데이터 모델(이슈, 댓글, 라벨, 상태 전환), 실제 권한 부여 결정(누가 무엇을 볼 수 있고, 누가 무엇을 쓸 수 있는지), 그리고 보안 패턴의 쓰기 게이트 절반을 시연할 자연스러운 장소가 필요합니다. 스펙의 비목표(non-goal)는 참조 구현과 "동일한 방식으로 명시적으로 정당화되고 게이트된" 쓰기 작업을 명시적으로 허용하며, CRUD가 바로 그 정당화입니다. 이는 패턴의 읽기 절반만이 아니라 더 구체적으로 유용한 데모입니다.
상호 참조: mcp-starter-template과 공유 패턴
이 서버는 보안 아키텍처를 처음부터 다시 설계하지 않고, 자매 프로젝트인 mcp-starter-template(이 포트폴리오의 프로젝트 2)에서 의도적으로 재사용합니다.
패턴 |
|
|
구성 기반 도구 분류 |
| 동일한 형식, 동일한 파일 이름 — |
시작 시 코드/구성 교차 검증 |
| 거의 그대로 포팅됨 — |
기본 읽기 전용 |
| 동일 — 여기에 |
인증 패스스루 |
| 동일한 설계, 도메인에 적합한 사용자( |
구조화된 오류 |
| 동일, 이슈 조회 시 |
감사 추적 |
| 동일한 이중 싱크 설계 |
속도 제한 |
| 동일, 여기에 |
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모든 도구 호출은 하나의 파이프라인입니다: 인증 → 속도 제한 → (쓰기인 경우) 허용 목록 검사 → (쓰기인 경우) 드라이런 또는 실제 실행 → 감사 로그. 어느 단계에서든 거부되면 구조화된 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 | 표시 가능한 이슈에 대한 전체 텍스트 검색, |
| 읽기 | 1 | 전체 세부 정보: 본문, 라벨, 모든 댓글 |
| 읽기 | 1 | 트래커가 알고 있는 모든 라벨 |
| 읽기 | 1 | 결정적 추출 요약 — LLM 호출 없음 (아래 참조) |
| 읽기 | 0 | 이번 윈도우에서 호출자의 남은 호출/비용 예산 |
| 쓰기 | 5 | 호출자의 팀 범위로 이슈 생성 |
| 쓰기 | 3 | 표시 가능하고 열린 이슈에 댓글 작성 |
| 쓰기 | 3 | 이슈 열기/닫기 |
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은 실제 배포에서 이를 실제 자격 증명 검증으로 대체해야 한다고 명시합니다). 대체 신원은 없습니다: 토큰이 없거나 유효하지 않으면 항상UNAUTHENTICATED입니다.팀 범위 가시성:
team=NULL인 이슈는 공개입니다. 그 외에는 같은 팀 호출자나 관리자에게만 표시됩니다.token-alice(engineering)와token-bob(docs)은 동일한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)에 한 행이 됩니다.
설치
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을 다시 시작하세요. "token-alice를 사용하여 이슈 트래커에서 ragbench 버그를 검색해줘" 같은 요청을 해보세요 — Claude가 search_issues를 대신 호출합니다. 모든 도구에는 token 인자가 필요하며(아래 목업 사용자 참조), 실제 배포에서는 mcp-starter-template의 문서화된 업그레이드 경로와 동일하게 실제 사용자별 OAuth로 이를 대체할 것입니다.
환경 변수 재정의
변수 | 용도 | 기본값 |
| SQLite DB 경로 |
|
|
| 저장소 루트의 |
| JSONL 감사 로그 경로 | 설정하지 않으면 비활성화 |
| SQLite 감사 로그 경로 | 설정하지 않으면 인메모리 |
|
|
|
| 허용 목록에 추가할 쉼표로 구분된 도구 이름 |
|
목업 사용자
토큰 | 사용자 | 팀 | 관리자 |
| Alice Nguyen | engineering | 아니요 |
| Bob Reyes | docs | 아니요 |
| Priya Shah | engineering | 예 (모든 팀을 볼 수 있음) |
실제 실행에서 쓰기 활성화
기본적으로 모든 쓰기 도구는 거부됩니다(WRITE_NOT_ALLOWED). 실제로 이슈/댓글/상태 변경을 생성하려면:
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— mock IdP 해석, 누락/유효하지 않은 토큰에 대한 인증-패스스루 거부, 폴백 신원 없음test_registry.py— 기본적으로 읽기 전용, 허용 목록(allowlist) 게이팅, 코드/설정 분류 불일치 시 시작 시 빠르게 실패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— 네 가지 읽기 도구를 실제 시드된 코퍼스에 대해 엔드투엔드로 테스트, "같은 질의에 두 사용자가 다른 결과를 보는 경우" 포함test_service_write.py— 기본적으로 쓰기 불가, 드라이런 미리보기 vs 실제 변경, 종료된 이슈 댓글 차단, 팀 간 쓰기 거부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)에 인라인으로 명시되어 있어 대체가 조용히 이루어지지 않습니다.실제 IdP가 아닌 모의(mock) 신원. 명시적으로 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도구 없음(라벨은 쓰기 시 생성(create-on-write)만 가능하며, 이는 스펙이 요구하지 않은 관리 표면을 과도하게 구축하지 않아도 패턴을 입증하기에 충분함).
포트폴리오 노트
이 프로젝트와 mcp-starter-template은 의도적으로 같은 요점을 두 번 전달하기 위해 존재합니다: MCP 보안 철학(인증-패스스루, 기본 읽기 전용, 감사, 속도 제한)은 일회성이 아닌 반복 가능한 패턴입니다. 동일한 모듈, 동일한 테스트 접근 방식, 동일한 실패 모드를 동일한 방식으로 처리합니다 — 한 저장소의 문서/설정 도메인과 이 저장소의 실제 이슈 트래커에 적용됩니다.
라이선스
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