Skip to main content

Serwer MCP dla API BPP (Bibliografia Publikacji Pracowników)

Project description

bpp-mcp

PyPI Python tests

Serwer MCP dla API BPP (Bibliografia Publikacji Pracowników). Wystawia read-only, anonimowe API BPP (/api/v1/) jako zestaw kuratorowanych, typowanych narzędzi dla Claude Desktop, Claude Code i innych klientów MCP.

Zamiast żmudnego chodzenia po hyperlinkach REST-owych (publikacja → autorzy → jednostka → …), serwer robi to za agenta: rozwija relacje, auto-follow-uje paginację i zwraca gotowe, zagnieżdżone obiekty.

Dlaczego MCP, a nie samo API?

API BPP jest hyperlinked — relacje to URL-e, nie zagnieżdżone dane. Pobranie jednego rekordu z autorami i źródłem to kilka–kilkanaście żądań. bpp-mcp ukrywa tę złożoność: pobierz_rekord zwraca jeden obiekt z rozwiniętymi autorami (nazwisko jak wydrukowane), źródłem i streszczeniami.

Konfiguracja

Serwer jest wielo-instancyjny — tę samą binarkę podłączasz do dowolnego wdrożenia BPP przez zmienne środowiskowe:

Zmienna Domyślnie Opis
BPP_BASE_URL wymagany bazowy URL instancji BPP (API i issuer OAuth)
BPP_BASIC_AUTH (brak) opcjonalny user:pass (tylko raporty slotów, stdio)
BPP_MCP_TRANSPORT stdio stdio (anon) lub http (OAuth per-user)
BPP_MCP_HTTP_HOST 127.0.0.1 bind serwera HTTP (tryb http)
BPP_MCP_HTTP_PORT 8000 port serwera HTTP (tryb http)
BPP_MCP_RESOURCE_URL http://<host>:<port>/mcp pole resource w protected-resource-metadata

Instalacja i uruchomienie

Pakiet jest na PyPI. Najprościej — bez instalowania czegokolwiek na stałe, przez uv:

BPP_BASE_URL=https://bpp.twoja-uczelnia.pl uvx bpp-mcp

uvx pobiera pakiet do własnego cache'a i uruchamia go w odizolowanym środowisku — nic nie ląduje w Twoim systemowym Pythonie.

Jeśli wolisz mieć komendę bpp-mcp na stałe w PATH:

uv tool install bpp-mcp        # albo: pip install bpp-mcp
BPP_BASE_URL=https://bpp.twoja-uczelnia.pl bpp-mcp

Aktualizacja: uv tool upgrade bpp-mcp (przy uvx wystarczy uvx bpp-mcp@latest).

Wersja rozwojowa prosto z gita (niewydany kod)
BPP_BASE_URL=https://bpp.twoja-uczelnia.pl \
  uvx --from git+https://github.com/iplweb/bpp-mcp bpp-mcp

Bierze czubek gałęzi main, więc dostajesz zmiany jeszcze przed wydaniem — ale też przed ich przetestowaniem w praktyce. Do normalnego użycia weź wersję z PyPI.

BPP_BASE_URL jest wymagany i nie ma wartości domyślnej — bez niego serwer nie wystartuje, tylko wypisze, czego brakuje. To celowe: każde wdrożenie BPP to inna uczelnia i inna bibliografia, więc zaszyty host oznaczałby, że użytkownik bez tej zmiennej dostaje cudze dane wyglądające na własne.

Serwer komunikuje się po stdio (standard MCP) — normalnie uruchamia go klient MCP, nie użytkownik ręcznie.

Tryb OAuth (HTTP, per-user)

Domyślnie bpp-mcp działa po stdio i anonimowo (dane publiczne). Aby działać z uprawnieniami zalogowanego użytkownika BPP (OAuth 2.1):

BPP_BASE_URL=https://bpp.twoja-uczelnia.pl uvx bpp-mcp --http --port 8000

Klient MCP (Claude) sam przeprowadza logowanie: wykrywa serwer autoryzacji BPP przez /.well-known/oauth-protected-resource, rejestruje się (DCR), otwiera przeglądarkę na logowanie BPP + ekran zgody (scope read), po czym wywołuje narzędzia z Bearer. bpp-mcp weryfikuje token przez GET /api/v1/whoami/ i forwarduje token bieżącego requestu do /api/v1/. Zapis jest zablokowany serwerowo (read-only).

Bezpieczeństwo: trzymaj --host 127.0.0.1 (domyślnie). Bind na inny host wyłącza wbudowaną ochronę DNS-rebinding SDK i eksponuje serwer poza maszynę. Token jest forwardowany do API BPP bez wiązania audience (świadome odstępstwo od MCP-MUST: bpp-mcp i API BPP = ta sama domena zaufania; mitygacje: scope read, twardy read-only serwerowo, krótki TTL).

Logowanie w trybie stdio (per-user, bez hostowania)

Domyślny tryb stdio może działać z uprawnieniami zalogowanego użytkownika bez uruchamiania serwera HTTP. Zaloguj się raz:

BPP_BASE_URL=https://bpp.twoja-uczelnia.pl uvx bpp-mcp login

Otworzy się przeglądarka na logowanie BPP (hasło/LDAP/Microsoft/ORCID/Keycloak) i ekran zgody (scope read). Po zalogowaniu token trafia do lokalnego pliku ~/.config/bpp-mcp/<instancja>/tokens.json (uprawnienia 0600), a bpp-mcp uruchamiany przez Claude forwarduje go do /api/v1/ — bez dodatkowych kroków.

Praca zdalna / host bez GUI. Adres autoryzacji jest zawsze wypisywany też tekstem, więc można go otworzyć w przeglądarce na innej maszynie. Callback na 127.0.0.1 wtedy nie wróci (przeglądarka jest gdzie indziej) — po zalogowaniu skopiuj z paska adresu cały adres przekierowania (zaczyna się od http://127.0.0.1:) albo sam parametr code i wklej w terminalu, gdzie czeka bpp-mcp login. Obie drogi — loopback i wklejka — działają równolegle; liczy się ta, która dojdzie pierwsza.

Co odblokowuje:

  • bogatsze wyniki istniejących narzędzi (rekordy widoczne dla Twojego konta),
  • narzędzia zapytanie_rekord / zapytanie_autor / zapytanie_autorzy (wykonywanie DjangoQL) — wymagają zalogowania i uprawnień redaktora.

Wylogowanie (usuwa token tej instancji):

BPP_BASE_URL=https://bpp.twoja-uczelnia.pl uvx bpp-mcp logout

Gdy instancja nie wystawia /.well-known/. Logowanie zaczyna się od odczytu metadanych serwera autoryzacji (RFC 8414) spod /.well-known/oauth-authorization-server. Część wdrożeń blokuje na brzegu cały /.well-known/ (typowo regułą nginksa na pliki ukryte, location ~ /\.) i oddaje 403, mimo że serwer autoryzacji działa. bpp-mcp cofa się wtedy na konwencjonalne ścieżki django-oauth-toolkit (/o/authorize/, /o/token/, /o/register/) na tym samym hoście i loguje normalnie. Prawidłowo wystawione metadane zawsze mają pierwszeństwo. Właściwą naprawą po stronie serwera jest location ^~ /.well-known/ przed regułą na pliki ukryte — bez tego natywny przycisk „authorize" w trybie HTTP nadal nie zadziała (tam discovery robi sam klient Claude, nie bpp-mcp).

Token jest krótkotrwały (access ~30 min) i odświeżany po cichu (refresh ~7 dni, rotujący). Zmiana hasła lub dezaktywacja konta w BPP unieważnia go — wtedy bpp-mcp wraca do trybu anonimowego, a narzędzia zapytanie_* poproszą o ponowne bpp-mcp login. Host bierze z BPP_BASE_URL (wymagany).

Różnica względem trybu HTTP: natywny przycisk „authorize" w Claude (jak przy GitHub) należy do trybu HTTP (sekcja wyżej) — wymaga działającego serwera pod URL-em. Tryb stdio nie pokazuje tego przycisku; logowanie przeprowadza komenda bpp-mcp login. Oba forwardują token do tego samego API i wykluczają zapis (read-only serwerowo).

Podłączenie do Claude Desktop

Dodaj wpis w pliku konfiguracyjnym Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "bpp": {
      "command": "uvx",
      "args": ["bpp-mcp"],
      "env": {
        "BPP_BASE_URL": "https://bpp.twoja-uczelnia.pl"
      }
    }
  }
}

Jeśli uvx nie jest w PATH Claude Desktop (typowe na macOS — aplikacja nie dziedziczy PATH z powłoki), podaj pełną ścieżkę, np. ~/.local/bin/uvx; pokaże ją which uvx.

Podłączenie do Claude Code

claude mcp add bpp \
  --env BPP_BASE_URL=https://bpp.twoja-uczelnia.pl \
  -- uvx bpp-mcp

Narzędzia

Narzędzie Rola
szukaj_publikacji(q, rok_od?, rok_do?, limit=25) rankowane wyszukiwanie pełnotekstowe publikacji
szukaj_autora(nazwisko) znajdź autorów po (bieżącym) nazwisku
publikacje_autora(id_lub_slug, rok_od?, rok_do?, limit=25) publikacje autora (ID lub slug)
publikacje_jednostki(id_lub_slug, rok_od?, rok_do?, limit=25) publikacje jednostki i pod-jednostek
pobierz_rekord(typ, id, pelne_dane_autorow=False) detal rekordu z rozwiniętymi relacjami
lista_publikacji(typ, rok_od?, rok_do?, charakter_formalny?, zmienione_po?, limit=25, offset=0) harvest/przyrost listy publikacji
slownik(rodzaj) mały słownik referencyjny (tłumaczenie ID↔nazwa)
zapytanie_rekord(q, limit=25, offset=0) wykonaj DjangoQL po publikacjach (bpp.Rekord) — autoryzowane
zapytanie_autor(q, limit=25, offset=0) wykonaj DjangoQL po autorach (bpp.Autor) — autoryzowane
zapytanie_autorzy(q, limit=25, offset=0) wykonaj DjangoQL po wpisach autorstwa (bpp.Autorzy) — autoryzowane
djangoql_schema(model="rekord") schemat DjangoQL-dla-LLM korzenia rekord/autor/autorzy (do budowy zapytań)

Zapytania DjangoQL (zapytanie_*) są AUTORYZOWANE — endpointy /api/v1/zapytanie/{rekord,autor,autorzy}/ wymagają Bearer (tryb OAuth/HTTP, patrz wyżej) albo sesji, oraz uprawnień redaktora (superuser lub staff w grupie „wprowadzanie danych"). Bez tego zwracają czytelny błąd: 401 (token), 403 (brak uprawnień), 400 (zła składnia/pole, z pozycją do korekty; pola PII jak autor.email są zablokowane), 503 (timeout — zawęź). Buduj zapytanie z djangoql_schema("rekord"); w trybie stdio bez tokenu dostaniesz 401/403.

Dodatkowo serwer wystawia prompt MCP (nie narzędzie wykonujące):

Prompt Rola
zloz_zapytanie_djangoql(opis) złóż zapytanie DjangoQL (z opisu po polsku) — wykonasz je zapytanie_rekord

typ w pobierz_rekord / lista_publikacji: wydawnictwo_ciagle, wydawnictwo_zwarte, patent, praca_doktorska, praca_habilitacyjna.

rodzaj w slownik: charakter_formalny, typ_kbn, jezyk, dyscyplina_naukowa, rodzaj_zrodla, poziom_wydawcy, funkcja_autora, tytul, czas_udostepnienia_openaccess. Dane wolumenowe (konferencja/wydawca/nagroda) są odrzucane — to nie słowniki.

Uwagi

  • szukaj_publikacji i szukaj_autora wymagają instancji BPP z Fazą 0 (rozszerzenie API o wyszukiwanie). Na starszej instancji szukaj_publikacji zwróci czytelny błąd (404 → komunikat o wymaganej wersji).
  • zapytanie_rekord/autor/autorzy wymagają nowszej instancji BPP (z endpointami /api/v1/zapytanie/*) oraz uwierzytelnienia (Bearer/sesja + uprawnienia redaktora) — patrz tabela wyżej. Pozostałe narzędzia (publikacje_*, pobierz_rekord, lista_publikacji, slownik) są anonimowe i działają na każdej wersji API.
  • szukaj_autora — wykrywanie możliwości: django-filter po cichu ignoruje nieznane parametry. Na starej instancji filtr nazwisko zostanie zignorowany i endpoint zwróci wszystkich autorów bez błędu. Narzędzie ustawia wtedy flagę mozliwe_ze_niefiltrowane (gdy trafień jest podejrzanie dużo). Filtr obejmuje wyłącznie bieżące nazwisko (nie poprzednie_nazwiska).
  • publikacje_autora / publikacje_jednostki mają twardy sufit 100 pozycji (endpoint recent_*). Przy dobiciu do limitu zwracana jest flaga obcieto: true — pełny harvest per autor rób przez lista_publikacji z chunkowaniem po latach. Endpoint recent_* NIE zwraca łącznej liczby prac encji (jego count to tylko liczba pozycji po obcięciu), dlatego narzędzie eksponuje wyłącznie zwrocono (liczba zwróconych) + obcieto, bez mylącego count.
  • szukaj_publikacji / szukaj_autora / lista_publikacji zwracają laczna_liczba (serwerowy count — realna liczba trafień), zwrocono (ile faktycznie przyszło) oraz flagę niepelne. niepelne: true oznacza, że auto-follow paginacji przerwał bezpiecznik (sufit liczby stron / zapętlony next) zanim objął wszystko — wynik może być niekompletny.

DjangoQL — schemat do budowy zapytań (djangoql_schema)

djangoql_schema(model, sekcje=None) zwraca porcję zbundlowanego, bezpiecznego schematu jednego z trzech korzeni — rekord (bpp.Rekord), autor (bpp.Autor), autorzy (bpp.Autorzy) — po jednym na endpoint /api/v1/zapytanie/* dla języka DjangoQL.

Bez parametru sekcje dostajesz RDZEŃ:

  • reguły gramatyki (operatory per typ, negacja, trawersowanie relacji, sufiksy __year / __count itd.),
  • pola modelu-korzenia z typami i — dla relacji — polem dopasowania,
  • całą sekcję dictionaries z dozwolonymi WARTOŚCIAMI wyłącznie bezpiecznych słowników zamkniętych (charaktery, dyscypliny, języki, licencje OA…), bez obcinania.

W schemacie NIE ma żadnych danych osób ani instytucji.

Dzięki temu LLM może zbudować PRECYZYJNE zapytanie, np.:

rok >= 2020 and jezyk.nazwa = "angielski" and impact_factor > 0
  • Sekcje modeli relacyjnych dobiera się na żądanie. Pól bpp.zrodlo, bpp.jednostka, pbn_api.publication itd. w rdzeniu nie ma — nazwy tych sekcji widać w blokach relacji modelu-korzenia (zapis zrodlo -> bpp.zrodlo) oraz w polu zwrotu sekcje_dostepne (obecnym zawsze, w obu trybach, bez korzenia i bez dictionaries). Wywołanie djangoql_schema("rekord", sekcje=["bpp.zrodlo", "bpp.jednostka"]) zwraca wyłącznie wskazane bloki (bez preambuły i bez słowników), sklejone w kolejności z pliku — kolejność argumentów nie ma znaczenia, duplikaty są pomijane. Nieznana nazwa kończy się błędem z podpowiedziami (difflib), a podanie korzenia albo dictionaries — informacją, że są już w rdzeniu. Typowy przepływ: jedno wywołanie po rdzeń i (opcjonalnie) jedno po komplet potrzebnych sekcji.
  • Dlaczego porcjowanie. Cały snapshot korzenia rekord to 74 kB tekstu (77 001 znaków po opakowaniu w JSON) i przebijał sufit wielkości pojedynczego wyniku narzędzia MCP (domyślnie 25 000 tokenów, zmienna MAX_MCP_OUTPUT_TOKENS) — klient odkładał wynik do pliku tymczasowego zamiast oddać go modelowi. Rdzeń to 20–25% snapshotu (rekord ~18 kB, autor ~17 kB, autorzy ~15 kB), a pojedyncza dobrana sekcja 0,3–8,4 kB. Nie ma parametru „zwróć wszystko" — kto potrzebuje całości, ma plik na dysku w pakiecie (bpp_mcp/data/).
  • Konstrukcja tu, wykonanie osobno. To narzędzie tylko buduje zapytania. Wykonasz je narzędziami zapytanie_rekord / zapytanie_autor / zapytanie_autorzy (patrz tabela narzędzi) — wymagają zalogowania (Bearer/sesja + uprawnienia redaktora); anonimowo zwracają 401/403.
  • Wersjonowanie. Pierwsza linia schematu to # BPP <wersja> (np. # BPP 202607.1397). Plik jest generowany per wersja BPP i powinien pasować do odpytywanej instancji. Źródło: repo iplweb/bpp-schema-for-llm (schemat przeskanowany — bez danych osobowych). Plik jest zbundlowany jako zasób pakietu (bpp_mcp/data/) i wczytywany przez importlib.resources.

Prompt zloz_zapytanie_djangoql(opis) — złóż zapytanie do wklejenia

Serwer wystawia prompt MCP zloz_zapytanie_djangoql(opis). To nie jest narzędzie wykonujące — prompt zwraca instrukcję dla klienta LLM, jak z opisu po polsku ułożyć jedno poprawne zapytanie DjangoQL. Instrukcja każe najpierw wywołać djangoql_schema("rekord") (jedyne źródło pól, typów, relacji i wartości dictionaries) i — dla relacji spoza rdzenia — dobrać sekcje parametrem sekcje, podaje zwięzłe reguły (operator wg typu, trawersacja relacji kropką, wartości słownikowe dosłownie, negacja tylko !=/!~/not in, łączenie and/or + nawiasy), a na końcu każe zwrócić gotowe zapytanie w bloku kodu. Wykonasz je narzędziem zapytanie_rekord (po zalogowaniu) albo wklejasz w edytor „zapytanie" BPP — prompt, tak jak djangoql_schema, tylko konstruuje, nie wykonuje.

Rozwój

uv sync --extra dev
uv run ruff format .
uv run ruff check .
uv run pytest -q

Testy są w pełni offline (mock httpx przez respx); domyślne CI nie wykonuje żadnych żywych wywołań.

Wydanie na PyPI

Publikacja idzie przez trusted publishing (OIDC) — w repozytorium nie ma i nie może być tokenu API PyPI. Wydanie wyzwala push tagu:

# 1. podbij `version` w pyproject.toml, zacommituj
# 2. otaguj i wypchnij
git tag vX.Y.Z
git push origin vX.Y.Z

Workflow release.yml przepuszcza pełną matrycę testów, sprawdza, czy tag zgadza się z project.version (rozjazd = przerwane wydanie, bo numeru raz zajętego na PyPI nie da się odzyskać), buduje sdist + wheel, weryfikuje je twine check --strict i obecność zbundlowanych schematów DjangoQL, po czym publikuje z osobnego joba w środowisku pypi.

Licencja

MIT — IPLWeb / Michał Pasternak. Patrz LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bpp_mcp-0.1.1.tar.gz (104.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bpp_mcp-0.1.1-py3-none-any.whl (92.2 kB view details)

Uploaded Python 3

File details

Details for the file bpp_mcp-0.1.1.tar.gz.

File metadata

  • Download URL: bpp_mcp-0.1.1.tar.gz
  • Upload date:
  • Size: 104.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for bpp_mcp-0.1.1.tar.gz
Algorithm Hash digest
SHA256 f90f8ecf22838b276251ef21781bbb7af827bc3bd3df660eef3871c371dba4d7
MD5 433990cf512be945ad4dfbd66365f6e3
BLAKE2b-256 5f231410eee11b8cc4dc341de2afc87c9cb376a0396fb0b842836ff64871064e

See more details on using hashes here.

Provenance

The following attestation bundles were made for bpp_mcp-0.1.1.tar.gz:

Publisher: release.yml on iplweb/bpp-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bpp_mcp-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: bpp_mcp-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 92.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for bpp_mcp-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 83ad144601b15319ac0064998f103334861b68be756b7c84fc9c80bded2ee00f
MD5 783cf53f9570b6c432f962425f9381e1
BLAKE2b-256 1a9065584e28ecbced260e441364cef70c6aa1ecbb7bb4bc043d71a9e897e91f

See more details on using hashes here.

Provenance

The following attestation bundles were made for bpp_mcp-0.1.1-py3-none-any.whl:

Publisher: release.yml on iplweb/bpp-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page