feat(hub): MCP-сервер — инструменты поверх HTTP API
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Typ5sXWNMFirr2va2uL8o
This commit is contained in:
parent
acbc19c7eb
commit
cc0026bb1c
@ -3,7 +3,7 @@ name = "jira-hub"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Мост Jira для АРМ КА: зеркало задач, очередь команд, HTTP API, MCP"
|
description = "Мост Jira для АРМ КА: зеркало задач, очередь команд, HTTP API, MCP"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = ["mcp>=1.2"]
|
dependencies = ["mcp>=1.2,<2"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
jira-hub = "jira_hub.cli:main"
|
jira-hub = "jira_hub.cli:main"
|
||||||
|
|||||||
222
src/jira_hub/mcp_server.py
Normal file
222
src/jira_hub/mcp_server.py
Normal file
@ -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")
|
||||||
74
tests/test_mcp.py
Normal file
74
tests/test_mcp.py
Normal file
@ -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")
|
||||||
113
uv.lock
generated
113
uv.lock
generated
@ -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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@ -201,41 +210,40 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpcore2"
|
name = "httpcore"
|
||||||
version = "2.13.1"
|
version = "1.0.9"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
{ name = "h11" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
name = "httpx2"
|
name = "httpx"
|
||||||
version = "2.13.1"
|
version = "0.28.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
|
{ name = "anyio" },
|
||||||
{ name = "httpcore2", marker = "sys_platform != 'emscripten'" },
|
{ name = "certifi" },
|
||||||
{ name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" },
|
{ name = "httpcore" },
|
||||||
{ name = "idna" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
name = "httpx2-jsfetch"
|
name = "httpx-sse"
|
||||||
version = "1.0"
|
version = "0.4.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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 = [
|
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]]
|
[[package]]
|
||||||
@ -270,7 +278,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [{ name = "mcp", specifier = ">=1.2" }]
|
requires-dist = [{ name = "mcp", specifier = ">=1.2,<2" }]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [{ name = "pytest", specifier = ">=8" }]
|
dev = [{ name = "pytest", specifier = ">=8" }]
|
||||||
@ -304,15 +312,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcp"
|
name = "mcp"
|
||||||
version = "2.3.0"
|
version = "1.30.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
{ name = "httpx2" },
|
{ name = "httpx" },
|
||||||
|
{ name = "httpx-sse" },
|
||||||
{ name = "jsonschema" },
|
{ name = "jsonschema" },
|
||||||
{ name = "mcp-types" },
|
|
||||||
{ name = "opentelemetry-api" },
|
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
|
{ name = "pydantic-settings" },
|
||||||
{ name = "pyjwt", extra = ["crypto"] },
|
{ name = "pyjwt", extra = ["crypto"] },
|
||||||
{ name = "python-multipart" },
|
{ name = "python-multipart" },
|
||||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||||
@ -322,34 +330,9 @@ dependencies = [
|
|||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
{ 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 = [
|
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" },
|
{ 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]]
|
|
||||||
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" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.21.0"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.32"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.16.0"
|
version = "4.16.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user