#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ MCP server pro Medevio API — FastMCP Spustit: python mcp_medevio.py Nástroje: get_agenda — agenda kalendáře (jeden den nebo rozsah) zapis_poznamku — vytvoř poznámku lékaře v kalendáři smaz_rezervaci — zruš jednorazovou rezervaci/poznámku get_pozadavky — seznam požadavků (aktivní nebo vyřízené) get_pozadavek — detail jednoho požadavku get_poznamky — interní klinické poznámky k požadavku uloz_poznamku — vytvoř nebo aktualizuj interní poznámku k požadavku hledej_pacienta — vyhledej pacienta podle jména / rodného čísla get_pacient — detail pacienta podle UUID TODO: - smaz_rezervaci neumí mazat opakované (recurring) poznámky — potřeba zachytit GraphQL mutaci z webu Medevio při smazání recurring reservation a přidat podporu (smazat celou sérii / jeden výskyt). """ import sys import json import traceback from pathlib import Path from datetime import datetime, date, time as dtime, timedelta from typing import Optional import requests from dateutil import parser as dtparser, tz try: from mcp.server.fastmcp import FastMCP except Exception: # Fallback, když balíček 'mcp' není nainstalován (např. python-runner na # toweru): modul lze importovat kvůli funkcím (zaloz_pozadavek_recept…), # jen MCP server běžet nemůže. @mcp.tool() se stane no-op průchodkou. class FastMCP: def __init__(self, *a, **k): pass def tool(self, *a, **k): def deco(f): return f return deco def run(self, *a, **k): raise RuntimeError("Balíček 'mcp' není nainstalován — MCP server nelze spustit.") # ── Všechny logy na stderr (stdout = JSON-RPC) ────────────────────────────── def log(msg: str): print(msg, file=sys.stderr, flush=True) # ── Konstanty ──────────────────────────────────────────────────────────────── GRAPHQL_URL = "https://api.medevio.cz/graphql" CLINIC_SLUG = "mudr-buzalkova" PRAGUE_TZ = tz.gettz("Europe/Prague") BASE_DIR = Path(__file__).resolve().parent TOKEN_PATH = BASE_DIR / "Medevio" / "token.txt" STORAGE_PATH = BASE_DIR / "Medevio" / "medevio_storage.json" # Playwright session cookies CALENDARS = { "vlado": "b6555c7e-4e95-4657-b441-87c2c9a7b2ca", "manzelka": "144c4e12-347c-49ca-9ec0-8ca965a4470d", } _BASE_HEADERS = { "content-type": "application/json", "origin": "https://my.medevio.cz", "referer": "https://my.medevio.cz/", } # ── Auth / token refresh ────────────────────────────────────────────────────── def _load_token() -> str: t = TOKEN_PATH.read_text(encoding="utf-8").strip() if t.startswith("Bearer "): t = t.split(" ", 1)[1] return t def _save_token(token: str) -> None: TOKEN_PATH.write_text(token, encoding="utf-8") log(f"✓ Token obnoven a uložen do {TOKEN_PATH}") def _load_storage_cookies() -> dict: """Načte session cookies z Playwright storage.json (gateway-access-token aj.).""" if not STORAGE_PATH.exists(): raise FileNotFoundError(f"Nenalezen soubor session: {STORAGE_PATH}") state = json.loads(STORAGE_PATH.read_text(encoding="utf-8")) return {c["name"]: c["value"] for c in state.get("cookies", [])} def _refresh_token_via_cookies() -> str: """ Obnoví Bearer token pomocí session cookies z medevio_storage.json. Volá AccessToken_AuthSelf s cookies → dostane nový JWT → uloží do token.txt. """ log("⟳ Obnova Bearer tokenu pomocí session cookies...") cookies = _load_storage_cookies() payload = { "operationName": "AccessToken_AuthSelf", "query": "query AccessToken_AuthSelf { authSelf { token } }", "variables": {}, } r = requests.post( GRAPHQL_URL, headers=_BASE_HEADERS, cookies=cookies, data=json.dumps(payload), timeout=20, ) r.raise_for_status() data = r.json() if "errors" in data: raise RuntimeError(f"AccessToken_AuthSelf selhalo: {data['errors']}") token = data["data"]["authSelf"]["token"] if not token: raise RuntimeError("AccessToken_AuthSelf vrátilo prázdný token.") _save_token(token) return token def _refresh_token_via_playwright() -> str: """ Záložní metoda: spustí headless Playwright, načte storage.json, přejde na Medevio a zachytí Bearer token z prvního GraphQL requestu. Použije se jen pokud i session cookies expirují. """ log("⟳ Záložní refresh přes Playwright (headless)...") from playwright.sync_api import sync_playwright import threading captured = [] done = threading.Event() def on_request(req): if "graphql" in req.url and not done.is_set(): auth = req.headers.get("authorization", "") if auth.startswith("Bearer "): token = auth.split(" ", 1)[1] captured.append(token) done.set() with sync_playwright() as pw: browser = pw.chromium.launch(headless=True) ctx = browser.new_context(storage_state=str(STORAGE_PATH)) page = ctx.new_page() page.on("request", on_request) page.goto( "https://my.medevio.cz/mudr-buzalkova/klinika/kalendar/agenda-dne/", wait_until="networkidle", timeout=30_000, ) # Počkáme max 10 s na zachycení tokenu done.wait(timeout=10) browser.close() if not captured: raise RuntimeError( "Playwright nenachytil Bearer token. " "Pravděpodobně expirovala i Playwright session — přihlaš se ručně na " "https://my.medevio.cz a zkopíruj nový Bearer token do Medevio/token.txt." ) _save_token(captured[0]) return captured[0] def _refresh_token() -> str: """Zkusí obnovit token: nejdřív přes cookies, při selhání přes Playwright.""" try: return _refresh_token_via_cookies() except Exception as e: log(f"Cookie refresh selhal ({e}), zkouším Playwright...") return _refresh_token_via_playwright() def _headers(token: Optional[str] = None) -> dict: t = token or _load_token() return {**_BASE_HEADERS, "authorization": f"Bearer {t}"} def _gql(operation: str, query: str, variables: dict) -> dict: """Zavolá GraphQL. Při 401/403 nebo auth chybě automaticky obnoví token a zkusí znovu.""" payload = {"operationName": operation, "query": query, "variables": variables} def _do(hdrs: dict) -> requests.Response: return requests.post(GRAPHQL_URL, headers=hdrs, data=json.dumps(payload), timeout=30) r = _do(_headers()) # Automatický refresh při vypršeném tokenu if r.status_code in (401, 403): log(f"⚠ HTTP {r.status_code} — obnova tokenu...") new_token = _refresh_token() r = _do(_headers(new_token)) r.raise_for_status() data = r.json() # GraphQL může vrátit auth chybu s HTTP 200 if "errors" in data: errs = data["errors"] is_auth = any( "auth" in str(e).lower() or "unauthenticated" in str(e).lower() or "unauthorized" in str(e).lower() for e in errs ) if is_auth: log("⚠ GraphQL auth chyba — obnova tokenu...") new_token = _refresh_token() r = _do(_headers(new_token)) r.raise_for_status() data = r.json() if "errors" in data: raise RuntimeError(f"GraphQL error [{operation}]: {data['errors']}") return data["data"] # ── Datetime helpers ────────────────────────────────────────────────────────── def _parse_day(val) -> date: if isinstance(val, date) and not isinstance(val, datetime): return val if isinstance(val, datetime): return val.date() s = str(val).strip().lower() today = date.today() if s in ("dnes", "today", ""): return today if s in ("zitra", "zítra", "tomorrow"): return today + timedelta(days=1) if s in ("vcera", "včera", "yesterday"): return today - timedelta(days=1) if s.startswith(("+", "-")) and s[1:].isdigit(): return today + timedelta(days=int(s)) return datetime.strptime(s, "%Y-%m-%d").date() def _day_to_utc_range(d: date) -> tuple[str, str]: start = datetime.combine(d, dtime.min).replace(tzinfo=PRAGUE_TZ) end = datetime.combine(d, dtime.max.replace(microsecond=0)).replace(tzinfo=PRAGUE_TZ) fmt = "%Y-%m-%dT%H:%M:%S.000Z" return start.astimezone(tz.UTC).strftime(fmt), end.astimezone(tz.UTC).strftime(fmt) def _to_utc_iso(dt: datetime) -> str: if dt.tzinfo is None: dt = dt.replace(tzinfo=PRAGUE_TZ) return dt.astimezone(tz.UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") def _resolve_calendar(cal: Optional[str]) -> str: if cal is None: return CALENDARS["vlado"] if cal in CALENDARS: return CALENDARS[cal] if len(cal) == 36 and cal.count("-") == 4: return cal raise ValueError(f"Neznámý kalendář: {cal!r}. Použij: {list(CALENDARS)} nebo UUID.") # ── MCP server ──────────────────────────────────────────────────────────────── mcp = FastMCP("medevio") # ───────────────────────────────────────────────────────────────────────────── # AGENDA # ───────────────────────────────────────────────────────────────────────────── _AGENDA_QUERY = """ query Agenda_ListAll( $calendarIds: [UUID!]!, $clinicSlug: String!, $locale: Locale!, $since: DateTime!, $until: DateTime! ) { reservations: listClinicReservations( clinicSlug: $clinicSlug, calendarIds: $calendarIds, since: $since, until: $until ) { id start end note done color canceledAt calendarId request { id displayTitle(locale: $locale) extendedPatient { id name surname dob phone identificationNumber insuranceCompanyObject { code shortName } } } } recurringReservations: listClinicRecurringReservations( clinicSlug: $clinicSlug, calendarIds: $calendarIds, since: $since, until: $until ) { recurringReservation { id calendarId color note rrule { frequency interval dtstart tzid byweekday } } instances { start end note color } } }""" _CAL_NAME = {v: k for k, v in CALENDARS.items()} def _parse_agenda_response(data: dict, cal_ids: list[str]) -> list[dict]: parsed = [] for res in data.get("reservations") or []: if res.get("canceledAt"): continue s = dtparser.isoparse(res["start"]).astimezone(PRAGUE_TZ) e = dtparser.isoparse(res["end"]).astimezone(PRAGUE_TZ) req = res.get("request") or {} pat = req.get("extendedPatient") or {} ins = pat.get("insuranceCompanyObject") or {} parsed.append({ "typ": "pacient" if req.get("id") else "poznamka", "kalendar": _CAL_NAME.get(res.get("calendarId"), res.get("calendarId")), "start": s.strftime("%Y-%m-%d %H:%M"), "end": e.strftime("%H:%M"), "titul": req.get("displayTitle") or "", "pacient": f"{pat.get('surname','')} {pat.get('name','')}".strip(), "dob": pat.get("dob") or "", "rc": pat.get("identificationNumber") or "", "telefon": pat.get("phone") or "", "pojistovna": ins.get("shortName") or "", "poznamka": (res.get("note") or "").strip(), "hotovo": bool(res.get("done")), "reservation_id": res["id"], "request_id": req.get("id") or "", "patient_id": pat.get("id") or "", "opakovana": False, }) for rr in data.get("recurringReservations") or []: rule = rr.get("recurringReservation") or {} cal_id = rule.get("calendarId") for inst in rr.get("instances") or []: s = dtparser.isoparse(inst["start"]).astimezone(PRAGUE_TZ) e = dtparser.isoparse(inst["end"]).astimezone(PRAGUE_TZ) parsed.append({ "typ": "poznamka", "kalendar": _CAL_NAME.get(cal_id, cal_id), "start": s.strftime("%Y-%m-%d %H:%M"), "end": e.strftime("%H:%M"), "titul": "", "pacient": "", "dob": "", "rc": "", "telefon": "", "pojistovna": "", "poznamka": (inst.get("note") or rule.get("note") or "").strip(), "hotovo": False, "reservation_id": rule.get("id"), "request_id": "", "patient_id": "", "opakovana": True, "rrule": rule.get("rrule"), }) parsed.sort(key=lambda x: x["start"]) return parsed @mcp.tool() def get_agenda( den: str = "dnes", do: str = "", kalendar: str = "", ) -> dict: """Vrátí agendu z Medevio kalendáře. Args: den: Datum začátku. Formáty: 'dnes', 'zitra', 'vcera', '+N', '-N', 'YYYY-MM-DD'. Default 'dnes'. do: Datum konce (volitelné). Stejné formáty jako 'den'. Prázdné = stejný den jako 'den'. kalendar: 'vlado', 'manzelka', nebo UUID. Prázdné = oba kalendáře. """ try: start_d = _parse_day(den) end_d = _parse_day(do) if do.strip() else start_d if kalendar.strip(): cal_ids = [_resolve_calendar(kalendar.strip())] else: cal_ids = list(CALENDARS.values()) fmt = "%Y-%m-%dT%H:%M:%S.000Z" since = datetime.combine(start_d, dtime.min).replace(tzinfo=PRAGUE_TZ).astimezone(tz.UTC).strftime(fmt) until = datetime.combine(end_d, dtime.max.replace(microsecond=0)).replace(tzinfo=PRAGUE_TZ).astimezone(tz.UTC).strftime(fmt) data = _gql("Agenda_ListAll", _AGENDA_QUERY, { "calendarIds": cal_ids, "clinicSlug": CLINIC_SLUG, "since": since, "until": until, "locale": "cs", }) zaznamy = _parse_agenda_response(data, cal_ids) return { "od": start_d.isoformat(), "do": end_d.isoformat(), "pocet": len(zaznamy), "zaznamy": zaznamy, } except Exception: log(f"get_agenda chyba: {traceback.format_exc()}") raise # ───────────────────────────────────────────────────────────────────────────── # ZÁPIS / MAZÁNÍ POZNÁMEK V KALENDÁŘI # ───────────────────────────────────────────────────────────────────────────── @mcp.tool() def zapis_poznamku( poznamka: str, den: str, cas: str, trvani_min: int = 5, kalendar: str = "vlado", color: str = "CHARCOAL", ) -> dict: """Vytvoří poznámku lékaře v Medevio kalendáři. Args: poznamka: Text poznámky. den: Datum: 'dnes', 'zitra', 'YYYY-MM-DD', '+N' apod. cas: Čas začátku ve formátu 'HH:MM' (lokální čas Praha). trvani_min: Délka v minutách (default 5). kalendar: 'vlado' (default), 'manzelka', nebo UUID. color: Barva bloku (default 'CHARCOAL'). Další možnosti: RED, GREEN, BLUE, ORANGE, PURPLE aj. """ mutation = """ mutation CreateReservation_MakeReservationByDoctor( $clinicSlug: String!, $color: ECRFIconColor, $note: String!, $timeSlotInput: TimeSlotInput! ) { reservation: makeReservationByDoctor( clinicSlug: $clinicSlug color: $color note: $note timeSlotInput: $timeSlotInput ) { id __typename } }""" try: d = _parse_day(den) t = datetime.strptime(cas.strip(), "%H:%M").time() cal_id = _resolve_calendar(kalendar) start_dt = datetime.combine(d, t).replace(tzinfo=PRAGUE_TZ) end_dt = start_dt + timedelta(minutes=int(trvani_min)) data = _gql("CreateReservation_MakeReservationByDoctor", mutation, { "clinicSlug": CLINIC_SLUG, "color": color, "note": poznamka, "timeSlotInput": { "calendarId": cal_id, "start": _to_utc_iso(start_dt), "end": _to_utc_iso(end_dt), }, }) res_id = data["reservation"]["id"] return { "ok": True, "reservation_id": res_id, "den": d.isoformat(), "cas": t.strftime("%H:%M"), "trvani_min": trvani_min, "poznamka": poznamka, "kalendar": _CAL_NAME.get(cal_id, cal_id), } except Exception: log(f"zapis_poznamku chyba: {traceback.format_exc()}") raise @mcp.tool() def smaz_rezervaci( reservation_id: str, clinic_slug: str = CLINIC_SLUG, ) -> dict: """Zruší (cancel) jednorazovou rezervaci nebo poznámku lékaře. Args: reservation_id: UUID rezervace (z get_agenda → reservation_id). clinic_slug: Slug kliniky (default mudr-buzalkova). """ mutation = """ mutation UpdateReservation_CancelReservationByDoctor( $clinicSlug: String!, $reservationId: UUID! ) { reservation: cancelReservationByDoctor( clinicSlug: $clinicSlug, reservationId: $reservationId ) { id __typename } }""" try: data = _gql("UpdateReservation_CancelReservationByDoctor", mutation, { "clinicSlug": clinic_slug, "reservationId": reservation_id, }) return {"ok": True, "zruseno_id": data["reservation"]["id"]} except Exception: log(f"smaz_rezervaci chyba: {traceback.format_exc()}") raise # ───────────────────────────────────────────────────────────────────────────── # POŽADAVKY # ───────────────────────────────────────────────────────────────────────────── _POZADAVKY_QUERY = """ query ClinicRequestList2( $clinicSlug: String!, $queueAssignment: QueueAssignmentFilter!, $state: PatientRequestState, $pageInfo: PageInfo!, $locale: Locale! ) { requestsResponse: listPatientRequestsForClinic2( clinicSlug: $clinicSlug, queueAssignment: $queueAssignment, state: $state, pageInfo: $pageInfo ) { count patientRequests { id displayTitle(locale: $locale) createdAt updatedAt doneAt removedAt extendedPatient { id name surname identificationNumber phone } lastMessage { createdAt } } } }""" @mcp.tool() def get_pozadavky( stav: str = "ACTIVE", pocet: int = 50, offset: int = 0, ) -> dict: """Vrátí seznam požadavků z Medevia. Args: stav: 'ACTIVE' (default) nebo 'DONE' (vyřízené). pocet: Počet záznamů na stránku (default 50, max 100). offset: Offset pro stránkování (default 0). """ try: data = _gql("ClinicRequestList2", _POZADAVKY_QUERY, { "clinicSlug": CLINIC_SLUG, "queueAssignment": "ANY", # 2026-06-12: Medevio prejmenovalo enum ALL -> ANY "state": stav.upper(), "pageInfo": {"first": min(pocet, 100), "offset": offset}, "locale": "cs", }) resp = data["requestsResponse"] zaznamy = [] for r in resp.get("patientRequests") or []: pat = r.get("extendedPatient") or {} lm = r.get("lastMessage") or {} zaznamy.append({ "id": r["id"], "titul": r.get("displayTitle") or "", "pacient": f"{pat.get('surname','')} {pat.get('name','')}".strip(), "rc": pat.get("identificationNumber") or "", "telefon": pat.get("phone") or "", "patient_id": pat.get("id") or "", "vytvoreno": r.get("createdAt") or "", "aktualizovano": r.get("updatedAt") or "", "dokonceno": r.get("doneAt") or "", "posledni_zprava": lm.get("createdAt") or "", }) return { "stav": stav.upper(), "celkem": resp.get("count", 0), "offset": offset, "pocet": len(zaznamy), "pozadavky": zaznamy, } except Exception: log(f"get_pozadavky chyba: {traceback.format_exc()}") raise # Pozn. 2026-06-12: Medevio změnilo schéma — argument je patientRequestId (UUID!), # evaluationResult/substate vyžadují subfields (vynechány), tags chce onlyImportant. _POZADAVEK_DETAIL_QUERY = """ query ClinicRequestDetail_GetPatientRequest2( $clinicSlug: String!, $requestId: UUID!, $locale: Locale! ) { request: getPatientRequest2(clinicSlug: $clinicSlug, patientRequestId: $requestId) { id doneAt doneBy { id name surname } removedAt createdAt createdBy { id name surname } displayTitle(locale: $locale) customTitle clinicMedicalRecord clinicMedicalRecordVisibleToPatient userNote queue { id name } hasMobileApp tags(onlyImportant: false) { id name } extendedPatient { id name surname dob identificationNumber phone email insuranceCompanyObject { code name shortName } note city street houseNumber } } }""" @mcp.tool() def get_pozadavek(request_id: str) -> dict: """Vrátí detail jednoho požadavku. Args: request_id: UUID požadavku. """ try: data = _gql("ClinicRequestDetail_GetPatientRequest2", _POZADAVEK_DETAIL_QUERY, { "clinicSlug": CLINIC_SLUG, "requestId": request_id, "locale": "cs", }) return data.get("request") or {} except Exception: log(f"get_pozadavek chyba: {traceback.format_exc()}") raise # ───────────────────────────────────────────────────────────────────────────── # INTERNÍ KLINICKÉ POZNÁMKY K POŽADAVKU # ───────────────────────────────────────────────────────────────────────────── _NOTES_GET_QUERY = """ query ClinicRequestNotes_Get($patientRequestId: String!) { notes: getClinicPatientRequestNotes(requestId: $patientRequestId) { id content createdAt updatedAt createdBy { id name surname } } }""" _NOTE_UPDATE_MUTATION = """ mutation ClinicRequestNotes_Update($noteInput: UpdateClinicPatientRequestNoteInput!) { updateClinicPatientRequestNote(noteInput: $noteInput) { id } }""" _NOTE_CREATE_MUTATION = """ mutation ClinicRequestNotes_Create($noteInput: CreateClinicPatientRequestNoteInput!) { createClinicPatientRequestNote(noteInput: $noteInput) { id } }""" @mcp.tool() def get_poznamky(request_id: str) -> dict: """Vrátí interní klinické poznámky k požadavku. Args: request_id: UUID požadavku. """ try: data = _gql("ClinicRequestNotes_Get", _NOTES_GET_QUERY, {"patientRequestId": request_id}) return {"request_id": request_id, "poznamky": data.get("notes") or []} except Exception: log(f"get_poznamky chyba: {traceback.format_exc()}") raise @mcp.tool() def uloz_poznamku( request_id: str, obsah: str, prepend: bool = False, ) -> dict: """Vytvoří nebo aktualizuje interní klinickou poznámku k požadavku. Pokud poznámka k požadavku neexistuje, vytvoří novou. Pokud existuje, aktualizuje ji (buď nahradí celý obsah, nebo přidá text na začátek). Args: request_id: UUID požadavku. obsah: Nový obsah poznámky (nebo text k přidání na začátek, pokud prepend=True). prepend: Pokud True, přidá `obsah` na začátek existující poznámky (default False = přepíše). """ try: notes_data = _gql("ClinicRequestNotes_Get", _NOTES_GET_QUERY, {"patientRequestId": request_id}) notes = notes_data.get("notes") or [] if notes: note = notes[0] note_id = note["id"] new_content = (obsah + "\n" + (note["content"] or "")) if prepend else obsah result = _gql("ClinicRequestNotes_Update", _NOTE_UPDATE_MUTATION, { "noteInput": {"id": note_id, "content": new_content} }) return {"ok": True, "akce": "update", "note_id": result["updateClinicPatientRequestNote"]["id"]} else: result = _gql("ClinicRequestNotes_Create", _NOTE_CREATE_MUTATION, { "noteInput": {"requestId": request_id, "content": obsah} }) return {"ok": True, "akce": "create", "note_id": result["createClinicPatientRequestNote"]["id"]} except Exception: log(f"uloz_poznamku chyba: {traceback.format_exc()}") raise # ───────────────────────────────────────────────────────────────────────────── # PACIENTI # ───────────────────────────────────────────────────────────────────────────── _SEARCH_QUERY = """ query Search($clinicSlug: String!, $locale: Locale!, $query: String!) { results: search(clinicSlug: $clinicSlug, locale: $locale, query: $query) { patients { id name surname identificationNumber dob insuranceCompanyObject { code shortName } status isInClinic } } }""" @mcp.tool() def hledej_pacienta(query: str) -> dict: """Vyhledá pacienta v Medeviu. Args: query: Jméno, příjmení, rodné číslo nebo jejich kombinace. """ try: data = _gql("Search", _SEARCH_QUERY, { "clinicSlug": CLINIC_SLUG, "locale": "cs", "query": query, }) pacienti = data.get("results", {}).get("patients") or [] return {"query": query, "pocet": len(pacienti), "pacienti": pacienti} except Exception: log(f"hledej_pacienta chyba: {traceback.format_exc()}") raise _PATIENT_DETAIL_QUERY = """ query GetPatientDetail($clinicSlug: String!, $patientId: String!) { patient: getPatientForClinic(clinicSlug: $clinicSlug, patientId: $patientId) { id name surname identificationNumber sex dob email phone status isInClinic hasMobileApp note city street houseNumber createdAt insuranceCompanyObject { code name shortName } user { id email phone } } }""" @mcp.tool() def get_pacient(patient_id: str) -> dict: """Vrátí detail pacienta. Args: patient_id: UUID pacienta (z hledej_pacienta nebo get_agenda). """ try: data = _gql("GetPatientDetail", _PATIENT_DETAIL_QUERY, { "clinicSlug": CLINIC_SLUG, "patientId": patient_id, }) return data.get("patient") or {} except Exception: log(f"get_pacient chyba: {traceback.format_exc()}") raise # ───────────────────────────────────────────────────────────────────────────── # ZALOŽENÍ POŽADAVKU "RECEPT NA LÉKY" # ───────────────────────────────────────────────────────────────────────────── # Flow ověřen 2026-06-12 (Chrome capture + živý test): # 1. fillECRFForm — vyplní formulář ERECEPT_SIMPLEST_BEZ_DAVKOVANI, # krok "erecept-gp-request", pole "nazev-leku" (volný text léků). # 2. createPatientRequestWithoutReservation — založí požadavek; objeví se # v aktivní frontě ordinace jako "Recept na léky". # Pozor: createPatientRequest (bez "WithoutReservation") požadavek také # vytvoří, ale NEZOBRAZÍ se v žádné frontě — nepoužívat. RECEPT_SID = "ERECEPT_SIMPLEST_BEZ_DAVKOVANI" RECEPT_STEP_ID = "erecept-gp-request" RECEPT_USER_ECRF_ID = "79488e86-e9e5-47e3-8b19-7e5229427f23" # šablona kliniky CLAUDE_TAG_ID = "c136aeca-0625-4c43-b81f-fc3949ec6ba6" # štítek "CLAUDE" OVERIT_TAG_ID = "9d3271b3-309d-4d20-93ee-285f3e56ba42" # štítek "OVĚŘIT PACIENTA" _FILL_MUTATION = """ mutation Step_FillECRFForm($input: FillECRFFormInput!) { patientEcrfFill: fillECRFForm(input: $input) { id } }""" _CREATE_REQUEST_MUTATION = """ mutation PatientRequestSubmission_CreatePatientRequestWithoutReservation( $clinicSlug: String!, $input: CreatePatientRequestWithoutReservationInput! ) { patientRequest: createPatientRequestWithoutReservation( clinicSlug: $clinicSlug, input: $input ) { id } }""" _ASSIGN_TAG_MUTATION = """ mutation TagRequestEditModal_AssignTagToRequest( $clinicSlug: String!, $requestId: UUID!, $tagId: UUID! ) { tagRequest: assignTagToPatientRequest( clinicSlug: $clinicSlug, patientRequestId: $requestId, tagId: $tagId ) { id } }""" def prirad_stitek(request_id: str, tag_id: str) -> None: """Přiřadí požadavku štítek (tag) podle jeho UUID.""" _gql("TagRequestEditModal_AssignTagToRequest", _ASSIGN_TAG_MUTATION, { "clinicSlug": CLINIC_SLUG, "requestId": request_id, "tagId": tag_id, }) @mcp.tool() def zaloz_pozadavek_recept(patient_id: str, leky: str, poznamka: str = "", stitek: bool = True, extra_stitky: list = None) -> dict: """Založí v Medeviu požadavek "Recept na léky" za pacienta. Požadavek se objeví v aktivní frontě ordinace stejně, jako by ho pacient založil sám v aplikaci — vyplní oba fieldy dotazníku: "Název léků" (leky) a "Poznámka" (poznamka). Volitelně přiřadí štítek CLAUDE pro odlišení automaticky založených požadavků. Args: patient_id: UUID pacienta (z hledej_pacienta / get_pacient). leky: Volný text názvů léků (pole dotazníku "Název léků:"). poznamka: Text do pole dotazníku "Poznámka" (jde přes userNote). stitek: True = přiřadí štítek CLAUDE (default). extra_stitky: Volitelný seznam UUID dalších štítků (např. OVĚŘIT PACIENTA). """ try: fill = _gql("Step_FillECRFForm", _FILL_MUTATION, { "input": { "fields": [{ "checkedEnumerations": [], "fieldName": "nazev-leku", "value": leky, }], "patientId": patient_id, "sid": RECEPT_SID, "stepId": RECEPT_STEP_ID, "byDoctor": False, } }) fill_id = fill["patientEcrfFill"]["id"] req = _gql( "PatientRequestSubmission_CreatePatientRequestWithoutReservation", _CREATE_REQUEST_MUTATION, { "clinicSlug": CLINIC_SLUG, "input": { "challengeId": None, "ecrfFillIds": [fill_id], "medicalRecordIds": [], "patientId": patient_id, "userNote": poznamka, "createdByDoctor": False, "userECRFId": RECEPT_USER_ECRF_ID, }, }, ) request_id = req["patientRequest"]["id"] # Štítek CLAUDE — označení automaticky založených požadavků. tag_ok = False if stitek: prirad_stitek(request_id, CLAUDE_TAG_ID) tag_ok = True # Další volitelné štítky (např. OVĚŘIT PACIENTA u nižší jistoty). for tid in (extra_stitky or []): prirad_stitek(request_id, tid) return { "ok": True, "request_id": request_id, "fill_id": fill_id, "patient_id": patient_id, "leky": leky, "stitek_claude": tag_ok, "extra_stitky": list(extra_stitky or []), } except Exception: log(f"zaloz_pozadavek_recept chyba: {traceback.format_exc()}") raise # ───────────────────────────────────────────────────────────────────────────── if __name__ == "__main__": log("MCP Medevio server spuštěn (FastMCP)") mcp.run()