Skip to main content
Glama
HamzaOuadid

mcp-issue-tracker

by HamzaOuadid

mcp-issue-tracker

MCP-сервер поверх настоящего локального трекера задач на SQLite — полный CRUD (поиск, получение, суммаризация, создание, комментарии, закрытие/переоткрытие), реальный наполненный корпус данных и тот же паттерн безопасности auth-passthrough + read-only-by-default, что используется в других MCP-проектах этого портфолио.

Создан как Проект 10 в портфолио из 20 проектов: «вторая, отдельная реализация MCP-сервера, разделяющая ту же философию безопасности, что и предыдущая, но применённая к другой предметной области».

Какой вариант и почему

Спецификация (10-second-mcp-server-docs-wiki-search-or-issue-tracker.md) предлагала выбор: поиск по документации/вики или трекер задач. Я сделал трекер задач.

Обоснование: сервер документации/вики — это по сути два инструмента (search, fetch) поверх статического контента. Трекеру задач нужны настоящая модель данных (issues, comments, labels, переходы статусов), настоящие решения об авторизации (кто что видит, кто что может писать) и естественное место для демонстрации второй половины паттерна безопасности — ограничения записи. Не-цель спецификации явно разрешает операции записи, «если они явно обоснованы и ограничены так же», как в эталонной реализации, и 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 (см. ниже)

Сквозная аутентификация

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

Каждый вызов инструмента — это один конвейер: аутентификация → ограничение частоты → (если запись) проверка allowlist → (если запись) dry-run или реальное выполнение → журнал аудита. Отказ на любом этапе порождает структурированную ошибку 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

Полная информация: body, labels, все комментарии

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 самостоятельно. Резюме — это чистая строковая логика: title + status + labels + усечённый фрагмент body + количество комментариев + самый последний комментарий. Детерминированно, тестируемо и честно о том, что это такое.

Модель безопасности, конкретно

  • Сквозная аутентификация (auth passthrough): каждый инструмент принимает аргумент token. Он преобразуется в реального пользователя User (user_id, team, is_admin) через мок-провайдер идентификации в памяти — тот же паттерн DEV-ONLY, что и в mcp-starter-template, задокументированный так же (в docstring identity.py явно сказано, что в реальном развёртывании его нужно заменить на настоящую проверку учётных данных). Запасной идентичности нет: отсутствующий/недействительный токен → всегда UNAUTHENTICATED.

  • Видимость в рамках команды: задача с team=NULL является публичной; в противном случае она видна только вызывающим из той же команды или администратору. token-alice (engineering) и token-bob (docs) видят разные наборы результатов от одного и того же вызова search_issues("") — это проверяется непосредственно в тестах, а не просто декларируется.

  • Только чтение по умолчанию, два уровня защиты: инструменту записи отказывается с ошибкой WRITE_NOT_ALLOWED, если его имени нет в allowed_write_tools. Даже в этом случае глобальный флаг dry_run (включён по умолчанию) заставляет инструмент вернуть синтетическое превью {"dry_run": true, "would_create": {...}} вместо обращения к базе данных. Оба барьера должны быть явно открыты, чтобы произошла реальная мутация.

  • Ограничение частоты запросов: фиксированное окно, бюджет на каждый токен вызывающего (calls_per_min и cost_per_session, стоимость инструмента из реестра). Его исчерпание в середине сессии возвращает RATE_LIMIT_EXCEEDED с retry_after на каждый последующий вызов в этом окне — сам процесс никогда не падает, и другие вызывающие не затронуты (явно протестировано, согласно крайнему случаю из спецификации).

  • Журнал аудита: каждый вызов — разрешённый или отклонённый, реальный или dry-run — становится одной строкой в 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", установленным в корень репозитория, или используйте полный путь к mcp-issue-tracker.exe из виртуального окружения.)

Перезапустите Claude Desktop. Спросите его, например: «Поищи в трекере задач ошибки ragbench, используя token-alice» — Claude вызовет search_issues за вас. Каждому инструменту нужен аргумент token (см. Мок-пользователи ниже); в реальном развёртывании это было бы заменено на настоящий OAuth для каждого пользователя, как и документированный путь обновления в mcp-starter-template.

Переопределение переменных окружения

Переменная

Назначение

По умолчанию

MCP_ISSUE_TRACKER_DB

Путь к базе данных SQLite

~/.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

Разделённые запятыми имена инструментов для allowlist

из 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 и управляет им с помощью реального mcp SDK-клиента через stdio (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, отклонение отсутствующих/недействительных токенов через auth-passthrough, отсутствие запасной идентификации

  • test_registry.py — только чтение по умолчанию, контроль доступа через белый список, несоответствие классификации кода/конфигурации вызывает быстрый сбой при запуске

  • test_limiter.py — бюджет фиксированного окна, изоляция по сессиям, сброс окна, retry_after

  • test_audit.py — двухканальное логирование в JSONL + SQLite, отклонённые вызовы содержат error_code

  • test_db.py — реальные CRUD-операции в SQLite, видимость в пределах команды, ввод в форме SQL-инъекции не приводит к сбою или утечке

  • test_tools_issues.py — детерминированная суммаризация, проверка аргументов

  • test_service_read.py — четыре инструмента чтения, протестированные end-to-end на реальном наполненном корпусе, включая «два пользователя видят разные результаты одного и того же запроса»

  • test_service_write.py — запись не разрешена по умолчанию, предпросмотр dry-run против реального изменения, блокировка комментариев в закрытых задачах, запрет записи между командами

  • test_edge_cases.py — исчерпание rate-limit в середине сессии корректно деградирует (без сбоя), фиксация версии схемы, защита от SQL-инъекций, запасное поведение при отсутствии конфигурации

  • test_server_integration.py — сквозное тестирование против реального протокола MCP: запускает python -m mcp_issue_tracker.server как подпроцесс и управляет им через stdio-клиент официального SDK mcp (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) — не нужно поднимать сервер, что соответствует принятому в остальных проектах портфолио соглашению «SQLite вместо Postgres/Docker»

  • Никакие LLM API-ключи не используются и не требуются — summarize_issue это чистая строковая логика (см. «Архитектура»)

Риски / Открытые вопросы

  • Отклонение от формулировки «живого публичного API» в спецификации. Разделы 5/10/11 спецификации описывают обёртку над живым сторонним API (например, реальным GitHub Issues API) с реальными rate-лимитами, привязанными к собственной квоте этого API. Вместо этого данная реализация использует реальный локальный трекер на SQLite с полноценными CRUD-операциями, согласно явному примечанию об окружении этой инициативы портфолио (без LLM-ключей, предпочитать локальные данные живым сторонним зависимостям, где задача это позволяет). Следствия: api_rate_state из модели данных спецификации реализован как собственный бюджет на вызывающего (доступен через get_rate_status), а не как квота стороннего API; краевой случай «документировать целевую версию API» реализован через фиксированную локальную schema_version. Оба момента отмечены прямо в коде (limiter.py, db.py), так что замена не является скрытой.

  • Мок-идентичность, а не настоящий IdP. Явно DEV-ONLY, описано в docstring identity.py — та же позиция, что и у mcp-starter-template. Для реального развёртывания требуется OAuth/JWT/mTLS перед AuthMiddleware.

  • SQLite с одним писателем. Подходит для демо/портфолио-сервера; конкурентное развёртывание с несколькими писателями потребовало бы настоящую базу данных (тот же компромисс, который README проекта ragbench явно отмечает для собственного использования SQLite).

  • Сокращение объёма по сравнению с вехами спецификации (раздел 8): нет отдельного CLI для запроса журнала аудита (запрашивайте его напрямую через AuditLogger.query() или sqlite3 issue_tracker.db); нет tagged-релиза (git tag) — это остаётся владельцу репозитория после публикации; управление метками не имеет отдельного инструмента delete_label/rename_label (метки создаются только при записи, чего достаточно для демонстрации паттерна без избыточного создания административной поверхности, которую спецификация не запрашивала).

Примечание о портфолио

И этот проект, и mcp-starter-template существуют, чтобы намеренно дважды донести одну и ту же мысль: философия безопасности MCP (auth-passthrough, только чтение по умолчанию, аудит, ограничение частоты) — это повторяемый паттерн, а не разовая мера. Те же модули, тот же подход к тестированию, те же сбои обрабатываются одинаково — применены к домену документации/конфигурации в одном репозитории и к реальному трекеру задач в этом.

Лицензия

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