mcp-issue-tracker
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 ссылается на этот репозиторий в собственном разделе перекрёстных ссылок, так что паттерн задокументирован с обеих сторон.
Архитектура
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; операции записи явно обоснованы согласно не-целям спецификации)
Инструмент | Чтение/Запись | Стоимость | Описание |
| чтение | 1 | Полнотекстовый поиск по видимым задачам, фильтр по |
| чтение | 1 | Полная информация: body, labels, все комментарии |
| чтение | 1 | Все метки, известные трекеру |
| чтение | 1 | Детерминированное экстрактивное резюме — без вызова LLM (см. ниже) |
| чтение | 0 | Оставшийся бюджет вызовов/стоимости вызывающего в этом окне |
| запись | 5 | Создать задачу в рамках команды вызывающего |
| запись | 3 | Прокомментировать видимую открытую задачу |
| запись | 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, задокументированный так же (в docstringidentity.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.
Переопределение переменных окружения
Переменная | Назначение | По умолчанию |
| Путь к базе данных SQLite |
|
| Путь к |
|
| Путь к JSONL-журналу аудита | отключён, если не задан |
| Путь к SQLite-журналу аудита | в памяти, если не задан |
| Переопределяет | из |
| Разделённые запятыми имена инструментов для allowlist | из |
Мок-пользователи
Токен | Пользователь | Команда | Админ |
| 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 и управляет им с помощью реального 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/ -v88 тестов, все проходят. Покрытие:
test_identity_auth.py— имитация разрешения IdP, отклонение отсутствующих/недействительных токенов через auth-passthrough, отсутствие запасной идентификацииtest_registry.py— только чтение по умолчанию, контроль доступа через белый список, несоответствие классификации кода/конфигурации вызывает быстрый сбой при запускеtest_limiter.py— бюджет фиксированного окна, изоляция по сессиям, сброс окна,retry_aftertest_audit.py— двухканальное логирование в JSONL + SQLite, отклонённые вызовы содержатerror_codetest_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-клиент официального SDKmcp(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) — не нужно поднимать сервер, что соответствует принятому в остальных проектах портфолио соглашению «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.
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