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

상호 참조: 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_mincost_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 참조.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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.

View all MCP Connectors

Latest Blog Posts

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