From cc0026bb1c8e1cd5372b3fcf9682e8893484abc8 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 11 Oct 2026 22:32:23 +0400 Subject: [PATCH] =?UTF-8?q?feat(hub):=20MCP-=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=20=E2=80=94=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=8B=20=D0=BF=D0=BE=D0=B2=D0=B5=D1=80=D1=85?= =?UTF-8?q?=20HTTP=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o --- pyproject.toml | 2 +- src/jira_hub/mcp_server.py | 222 +++++++++++++++++++++++++++++++++++++ tests/test_mcp.py | 74 +++++++++++++ uv.lock | 113 +++++++++---------- 4 files changed, 352 insertions(+), 59 deletions(-) create mode 100644 src/jira_hub/mcp_server.py create mode 100644 tests/test_mcp.py diff --git a/pyproject.toml b/pyproject.toml index 1fdba5c..ac01160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "jira-hub" version = "0.1.0" description = "Мост Jira для АРМ КА: зеркало задач, очередь команд, HTTP API, MCP" requires-python = ">=3.13" -dependencies = ["mcp>=1.2"] +dependencies = ["mcp>=1.2,<2"] [project.scripts] jira-hub = "jira_hub.cli:main" diff --git a/src/jira_hub/mcp_server.py b/src/jira_hub/mcp_server.py new file mode 100644 index 0000000..2a0aa98 --- /dev/null +++ b/src/jira_hub/mcp_server.py @@ -0,0 +1,222 @@ +"""MCP-сервер хаба (stdio, FastMCP) — инструменты сессии Claude поверх HTTP API (specs/mcp/spec.md). +Автор команд из MCP — «claude». Если хаб выключен, инструмент возвращает подсказку, а не исключение.""" +import functools +import json +import time + +from mcp.server.fastmcp import FastMCP + +from .client import HubClient, HubError, HubUnavailable + +OFFLINE = "хаб не запущен: jira-hub serve" +POLL_SECONDS = 5 +AUTHOR = "claude" +mcp = FastMCP("jira-hub") + + +def _client() -> HubClient: + return HubClient.from_home() + + +def guard(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + try: + return fn(*args, **kwargs) + except HubUnavailable: + return OFFLINE + except HubError as e: + return f"ошибка хаба {e.status} {e.error}: {e.detail or ''}".rstrip(": ") + return wrapper + + +# --- форматирование -------------------------------------------------------------- + +def _cut(text, n=80): + text = (text or "").replace("\n", " ") + return text if len(text) <= n else text[: n - 1] + "…" + + +def _v(value): + return "—" if value in (None, "", []) else str(value) + + +def fmt_issues(issues) -> str: + if not issues: + return "задач нет" + lines = ["ключ | тема | статус | срок | исполнитель | спринт | статус по факту"] + for i in issues: + lines.append(" | ".join([i["key"], _cut(i.get("summary")), _v(i.get("status")), _v(i.get("duedate")), + _v(i.get("assignee")), _v(i.get("sprint")), _v(i.get("fact_status"))])) + return "\n".join(lines) + + +def fmt_issue(d) -> str: + head = [f"{d['key']} — {d.get('summary')}", + f"тип: {_v(d.get('issue_type'))}; статус: {_v(d.get('status'))}; приоритет: {_v(d.get('priority'))}; " + f"исполнитель: {_v(d.get('assignee'))}; срок: {_v(d.get('duedate'))}; план: {_v(d.get('planned_start'))}…{_v(d.get('planned_end'))}", + f"эпик: {_v(d.get('epic_key'))}; спринт: {_v(d.get('sprint'))}; метки: {', '.join(d.get('labels') or []) or '—'}; " + f"сосед: {'да' if d.get('neighbor') else 'нет'}; обновлено в Jira: {_v(d.get('jira_updated'))}; прочитано: {_v(d.get('fetched_at'))}"] + links = [f" → {l['target_key']} ({l['link_type']})" for l in d.get("links_out", [])] + \ + [f" ← {l['source_key']} ({l['link_type']})" for l in d.get("links_in", [])] + note = d.get("note") or {} + notes = [f" {k}: {note[k]}" for k in ("fact_status", "work_kind", "done_text", "rest_text", "owner_note") if note.get(k)] + cmds = [f" #{c['id']} {c['type']} {c['status']} {c.get('finished_at') or ''} {c.get('error') or ''}".rstrip() for c in d.get("commands", [])] + return "\n".join(head + ["связи:"] + (links or [" —"]) + ["заметки:"] + (notes or [" —"]) + ["команды:"] + (cmds or [" —"]) + + ["описание:", d.get("description") or "—"]) + + +def fmt_batch(b) -> str: + est = round((b.get("remaining_seconds") or 0) / 60) + lines = [f"пакет #{b['id']} «{b['title']}» ({b['author']}, {b['created_at']}): {b['status']}; осталось ≈ {est} мин; " + + ", ".join(f"{k}: {v}" for k, v in (b.get("counts") or {}).items())] + for c in b.get("commands", []): + lines.append(f" {c['seq']}. #{c['id']} {c['type']} {json.dumps(c['params'], ensure_ascii=False)} → {c['status']}" + + (f" ({c['error']})" if c.get("error") else "") + (f" {json.dumps(c['result'], ensure_ascii=False)}" if c.get("result") else "")) + return "\n".join(lines) + + +def fmt_agent(a) -> str: + state = "онлайн" if a.get("online") else "офлайн" + cur = f"выполняет #{a['current_command_id']}" if a.get("current_command_id") else "очередь свободна" + out = [f"агент {state} (версия {_v(a.get('agent_version'))}, настройки v{_v(a.get('settings_version'))}, последний контакт {_v(a.get('last_seen_at'))}); {cur}; " + f"429/503 за сутки: {a.get('rate_limited_24h', 0)}"] + if a.get("warning"): + out.append("внимание: " + a["warning"]) + for s in a.get("stuck", []): + out.append(f"нет ответа от агента: #{s['id']} {s['type']} с {s['claimed_at']}") + return "\n".join(out) + + +# --- инструменты ----------------------------------------------------------------- + +@mcp.tool() +@guard +def issues(sprint: str = "", assignee: str = "", work_kind: str = "", fact_status: str = "", status: str = "", + label: str = "", epic: str = "", q: str = "", neighbor: str = "", limit: int = 50) -> str: + """Задачи зеркала по фильтрам (спринт, исполнитель, вид работы, статус по факту, статус Jira, метка, эпик, + подстрока темы/ключа, neighbor=1 — только соседние). Компактная таблица.""" + data = _client().get("/issues", sprint=sprint, assignee=assignee, work_kind=work_kind, fact_status=fact_status, + status=status, label=label, epic=epic, q=q, neighbor=neighbor, limit=limit) + return fmt_issues(data["issues"]) + + +@mcp.tool() +@guard +def issue(key: str) -> str: + """Задача целиком: поля, связи в обе стороны, служебные заметки, последние команды, описание.""" + return fmt_issue(_client().get(f"/issues/{key}")) + + +@mcp.tool() +@guard +def notes_set(key: str, fact_status: str | None = None, work_kind: str | None = None, done_text: str | None = None, + rest_text: str | None = None, owner_note: str | None = None) -> str: + """Правка служебных заметок задачи (только переданные поля).""" + fields = {k: v for k, v in dict(fact_status=fact_status, work_kind=work_kind, done_text=done_text, + rest_text=rest_text, owner_note=owner_note).items() if v is not None} + fields["updated_by"] = AUTHOR + note = _client().put(f"/issues/{key}/note", fields) + return "заметки " + key + ": " + "; ".join(f"{k}: {note.get(k)}" for k in ("fact_status", "work_kind", "done_text", "rest_text", "owner_note") if note.get(k)) + + +@mcp.tool() +@guard +def batch_create(title: str, commands: list[dict]) -> str: + """Поставить пакет команд. Каждая команда — {"type": ensure_issue|ensure_fields|ensure_comment|ensure_status| + ensure_link|ensure_sprint|refresh, "params": {...}} по контракту specs/commands. Хвостовой refresh хаб добавит сам.""" + b = _client().post("/batches", {"title": title, "author": AUTHOR, "commands": commands}) + return f"пакет #{b['id']} поставлен: {b['count']} команд, оценка ≈ {round(b['estimate_seconds'] / 60)} мин" + + +@mcp.tool() +@guard +def batch_status(id: int) -> str: + """Состояние пакета и его команд.""" + return fmt_batch(_client().get(f"/batches/{id}")) + + +@mcp.tool() +@guard +def batch_wait(id: int, timeout_minutes: int = 10) -> str: + """Ждать завершения пакета (до 60 мин), вернуть итог по командам.""" + deadline = time.monotonic() + min(max(int(timeout_minutes), 1), 60) * 60 + while True: + b = _client().get(f"/batches/{id}") + if b["status"] in ("done", "cancelled") or time.monotonic() >= deadline: + return fmt_batch(b) + ("" if b["status"] in ("done", "cancelled") else "\n(таймаут ожидания, пакет ещё выполняется)") + time.sleep(POLL_SECONDS) + + +@mcp.tool() +@guard +def batches(limit: int = 10) -> str: + """Последние пакеты со статусом.""" + items = _client().get("/batches", limit=limit)["batches"] + return "\n".join(fmt_batch(b) for b in items) or "пакетов нет" + + +@mcp.tool() +@guard +def command_retry(id: int) -> str: + """Повторить команду — новый пакет с теми же параметрами.""" + b = _client().post(f"/commands/{id}/retry", {"author": AUTHOR}) + return f"пакет #{b['id']} «Повтор команды {id}»: {b['count']} команд" + + +@mcp.tool() +@guard +def batch_cancel(id: int) -> str: + """Отменить пакет (только команды в очереди; взятую агентом — command_cancel с force через API).""" + return fmt_batch(_client().post(f"/batches/{id}/cancel", {})) + + +@mcp.tool() +@guard +def refresh(keys: list[str] | None = None, full: bool = False, wait: bool = True) -> str: + """Обновить зеркало из Jira: по ключам или полностью (базовая JQL). wait — дождаться результата.""" + body = {"full": True} if full or not keys else {"keys": keys} + body["author"] = AUTHOR + b = _client().post("/refresh", body) + return batch_wait(b["id"], 30) if wait else f"пакет #{b['id']} «Обновление» поставлен" + + +@mcp.tool() +@guard +def agent_status() -> str: + """Агент: онлайн/офлайн, версия, текущая команда, 429 за сутки, команды без ответа.""" + return fmt_agent(_client().get("/agent")) + + +@mcp.tool() +@guard +def sync_log(limit: int = 10) -> str: + """Журнал обновлений зеркала.""" + runs = _client().get("/sync-runs", limit=limit)["runs"] + return "\n".join(f"#{r['id']} {r['kind']} {_cut(r.get('query'), 60)}: {r['started_at']} → {_v(r.get('finished_at'))}, " + f"задач {r['issues_count']}, связей {r['links_count']}, вызовов {r['jira_calls']}, 429: {r['rate_limited']}" + for r in runs) or "обновлений не было" + + +@mcp.tool() +@guard +def settings_get(key: str = "") -> str: + """Настройки хаба (все или одна по ключу).""" + data = _client().get("/settings") + return json.dumps(data.get(key) if key else data, ensure_ascii=False, indent=1) + + +@mcp.tool() +@guard +def settings_set(key: str, value: str) -> str: + """Записать настройку: value — JSON. Поднимает settings_version.""" + try: + parsed = json.loads(value) + except ValueError as e: + return f"значение не JSON: {e}" + out = _client().put("/settings", {key: parsed}) + return f"settings_version = {out['settings_version']}" + + +def run() -> None: + mcp.run(transport="stdio") diff --git a/tests/test_mcp.py b/tests/test_mcp.py new file mode 100644 index 0000000..9f8c494 --- /dev/null +++ b/tests/test_mcp.py @@ -0,0 +1,74 @@ +import json +import threading + +import pytest + +from jira_hub import api, mcp_server, settings +from jira_hub.client import HubClient + + +@pytest.fixture +def live(tmp_path, monkeypatch): + h = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups") + h.claim_wait = 0 + conn = h.conn() + tok = settings.tokens(conn) + conn.close() + srv = api.make_server(h, "127.0.0.1", 0) + threading.Thread(target=srv.serve_forever, daemon=True).start() + base = f"http://127.0.0.1:{srv.server_port}" + monkeypatch.setattr(mcp_server, "_client", lambda: HubClient(base, tok["client"])) + monkeypatch.setattr(mcp_server, "POLL_SECONDS", 0.05) + yield {"agent": HubClient(base, tok["agent"])} + srv.shutdown() + + +def test_offline_message(monkeypatch): + monkeypatch.setattr(mcp_server, "_client", lambda: HubClient("http://127.0.0.1:9", "t", timeout=1)) + assert mcp_server.issues() == mcp_server.OFFLINE + assert mcp_server.agent_status() == mcp_server.OFFLINE + + +def test_tools_roundtrip(live): + live["agent"].post("/agent/state", {"issues": [{"key": "A-1", "summary": "Тема раз", "status": "Готово", "duedate": "2026-10-23", + "assignee": "ivanov", "sprint": "Спринт 2", "labels": ["AWPCA"]}], "links": []}) + out = mcp_server.issues(sprint="Спринт 2") + assert "A-1" in out and "Тема раз" in out and "ivanov" in out + assert "В работе" in mcp_server.notes_set("A-1", fact_status="В работе") + detail = mcp_server.issue("A-1") + assert "fact_status: В работе" in detail and "Готово" in detail + created = mcp_server.batch_create("срок", [{"type": "ensure_fields", "params": {"key": "A-1", "duedate": "2026-10-24"}}]) + batch_id = int(created.split("#")[1].split()[0]) + assert "2 команд" in created and "мин" in created + bad = mcp_server.batch_create("x", [{"type": "ensure_fields", "params": {"key": "A-1"}}]) + assert bad.startswith("ошибка хаба 422") + cmd = live["agent"].post("/agent/claim", {})["command"] + live["agent"].post(f"/agent/commands/{cmd['id']}/result", {"status": "failed", "error": "HTTP 400 поле"}) + tail = live["agent"].post("/agent/claim", {})["command"] + live["agent"].post(f"/agent/commands/{tail['id']}/result", {"status": "done"}) + waited = mcp_server.batch_wait(batch_id, timeout_minutes=1) + assert "done" in waited and "failed" in waited and "HTTP 400" in waited + assert f"#{batch_id}" in mcp_server.batches(limit=5) + assert "Повтор команды" in mcp_server.command_retry(cmd["id"]) + assert "cancelled" in mcp_server.batch_cancel(batch_id + 1) + r = mcp_server.refresh(full=True, wait=False) + assert "Обновление" in r or "#" in r + assert "онлайн" in mcp_server.agent_status() + assert "A-1" in mcp_server.sync_log(limit=3) or "keys" in mcp_server.sync_log(limit=3) + assert "120" in mcp_server.settings_get("pacing") + assert "settings_version" in mcp_server.settings_set("pacing", json.dumps({"refresh": 1})) + assert mcp_server.settings_set("tokens", '"x"').startswith("ошибка хаба 422") + + +def test_formatters(): + assert mcp_server.fmt_issues([]) == "задач нет" + text = mcp_server.fmt_issues([{"key": "A-1", "summary": "x" * 100, "status": "Готово", "duedate": None, "assignee": None, + "sprint": None, "fact_status": None}]) + assert "x" * 79 + "…" in text and "x" * 80 not in text and "—" in text + assert "очередь" in mcp_server.fmt_agent({"online": False, "agent_version": None, "current_command_id": None, + "rate_limited_24h": 0, "stuck": [], "warning": None, "last_seen_at": None, + "settings_version": None}) + + +def test_settings_set_bad_json(live): + assert mcp_server.settings_set("pacing", "{не json").startswith("значение не JSON") diff --git a/uv.lock b/uv.lock index 6c144bb..561895d 100644 --- a/uv.lock +++ b/uv.lock @@ -37,6 +37,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + [[package]] name = "cffi" version = "2.1.1" @@ -201,41 +210,40 @@ wheels = [ ] [[package]] -name = "httpcore2" -version = "2.13.1" +name = "httpcore" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "certifi" }, { name = "h11" }, - { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] -name = "httpx2" -version = "2.13.1" +name = "httpx" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "sys_platform != 'emscripten'" }, - { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, - { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" }, + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, { name = "idna" }, - { name = "truststore", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] -name = "httpx2-jsfetch" -version = "1.0" +name = "httpx-sse" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] [[package]] @@ -270,7 +278,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "mcp", specifier = ">=1.2" }] +requires-dist = [{ name = "mcp", specifier = ">=1.2,<2" }] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=8" }] @@ -304,15 +312,15 @@ wheels = [ [[package]] name = "mcp" -version = "2.3.0" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "httpx2" }, + { name = "httpx" }, + { name = "httpx-sse" }, { name = "jsonschema" }, - { name = "mcp-types" }, - { name = "opentelemetry-api" }, { name = "pydantic" }, + { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, @@ -322,34 +330,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/8d/e0d339616f4810e9051d4aba6887afab289ab1f81875fe908b606cdfd0e3/mcp-2.3.0.tar.gz", hash = "sha256:8b147a50441cf059dc88c684e0aeed3687f0aa0f39c6cde7b90330effd2b34d8", size = 4171955, upload-time = "2026-10-02T22:06:56.091Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/93/0142dc84a666daf8ad51a34268f34c12fd6fda4f3810c4be2504eecc8212/mcp-1.30.0.tar.gz", hash = "sha256:445414625fce5c295faa505bb11bacece661ab6f4028d57c935db57820b7a3e4", size = 680511, upload-time = "2026-09-07T14:34:15.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/d7/4a2878f128df729db3ca04e71bfce26405cc2e0c9ebe19c592feeb4c61b9/mcp-2.3.0-py3-none-any.whl", hash = "sha256:dd0c44c089d16453e8ae31a3877a0054d7a2314caaa81f5e0541b9b1734b2377", size = 368664, upload-time = "2026-10-02T22:06:52.119Z" }, -] - -[[package]] -name = "mcp-types" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/2d/7c251e34207f6c51000312fc8839111ac45cfe02023f90b44e7f1051dd8e/mcp_types-2.3.0.tar.gz", hash = "sha256:d1e46549edb35ee19a94940fcee6d1addd7e589ab7ea92dda83f5d84781fc362", size = 65898, upload-time = "2026-10-02T22:06:57.863Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/48/32583066b155bb5d4507e7887dc88ee23118cc4de8e2a12870c49719e2a8/mcp_types-2.3.0-py3-none-any.whl", hash = "sha256:968efdbdaedfab06adae40d378a34395f1090c5921d4be3c9cde283aaf76d91d", size = 69108, upload-time = "2026-10-02T22:06:54.099Z" }, -] - -[[package]] -name = "opentelemetry-api" -version = "1.45.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f4/e58bc33317c92a0203664daaf00bf6f41166cc0149e5d6870a03f7cd004a/mcp-1.30.0-py3-none-any.whl", hash = "sha256:666edb5009503e1047c9d60346a756f94b261f05cc2625f23d41c728ffc484d0", size = 234581, upload-time = "2026-09-07T14:34:14.266Z" }, ] [[package]] @@ -481,6 +464,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/15/213d6fe84816e8a5ed7b7539ecebb3f14c20739c2469f4e372f0f15bb042/pydantic_core-2.50.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f3abcabb04503023e1053add24472e78878de0bfd5c8a93686be01f4032c363c", size = 2175368, upload-time = "2026-10-08T14:30:08.989Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, +] + [[package]] name = "pygments" version = "2.21.0" @@ -520,6 +517,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, +] + [[package]] name = "python-multipart" version = "0.0.32" @@ -664,15 +670,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" }, ] -[[package]] -name = "truststore" -version = "0.10.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, -] - [[package]] name = "typing-extensions" version = "4.16.0"