Skip to main content
Glama
HamzaOuadid

mcp-issue-tracker

by HamzaOuadid

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가 바로 그 정당화입니다. 이는 패턴의 읽기 절반만이 아니라 더 구체적으로 유용한 데모입니다.

Related MCP server: Lific

상호 참조: mcp-starter-template과 공유 패턴

이 서버는 보안 아키텍처를 처음부터 다시 설계하지 않고, 자매 프로젝트인 mcp-starter-template(이 포트폴리오의 프로젝트 2)에서 의도적으로 재사용합니다.

패턴

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: 목업 bearer 토큰이 공유 자격 증명이 아닌 실제 User로 확인됨

동일한 설계, 도메인에 적합한 사용자(token-alice/token-bob/token-admin)

구조화된 오류

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

동일, 이슈 조회 시 +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

모든 도구 호출은 하나의 파이프라인입니다: 인증 → 속도 제한 → (쓰기인 경우) 허용 목록 검사 → (쓰기인 경우) 드라이런 또는 실제 실행 → 감사 로그. 어느 단계에서든 거부되면 구조화된 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

표시 가능한 이슈에 대한 전체 텍스트 검색, status/label로 필터링

get_issue

읽기

1

전체 세부 정보: 본문, 라벨, 모든 댓글

list_labels

읽기

1

트래커가 알고 있는 모든 라벨

summarize_issue

읽기

1

결정적 추출 요약 — LLM 호출 없음 (아래 참조)

get_rate_status

읽기

0

이번 윈도우에서 호출자의 남은 호출/비용 예산

create_issue

쓰기

5

호출자의 팀 범위로 이슈 생성

add_comment

쓰기

3

표시 가능하고 열린 이슈에 댓글 작성

set_issue_status

쓰기

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로 이를 대체할 것입니다.

환경 변수 재정의

변수

용도

기본값

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). 실제로 이슈/댓글/상태 변경을 생성하려면:

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 — mock IdP 해석, 누락/유효하지 않은 토큰에 대한 인증-패스스루 거부, 폴백 신원 없음

  • test_registry.py — 기본적으로 읽기 전용, 허용 목록(allowlist) 게이팅, 코드/설정 분류 불일치 시 시작 시 빠르게 실패

  • 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 — 네 가지 읽기 도구를 실제 시드된 코퍼스에 대해 엔드투엔드로 테스트, "같은 질의에 두 사용자가 다른 결과를 보는 경우" 포함

  • test_service_write.py — 기본적으로 쓰기 불가, 드라이런 미리보기 vs 실제 변경, 종료된 이슈 댓글 차단, 팀 간 쓰기 거부

  • 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)에 인라인으로 명시되어 있어 대체가 조용히 이루어지지 않습니다.

  • 실제 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 참조.

Maintenance

ActivitySlowing
ResponsivenessNo issues

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