Serwer MCP dla API BPP (Bibliografia Publikacji Pracowników)
Project description
bpp-mcp
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_publikacjiiszukaj_autorawymagają instancji BPP z Fazą 0 (rozszerzenie API o wyszukiwanie). Na starszej instancjiszukaj_publikacjizwróci czytelny błąd (404 → komunikat o wymaganej wersji).zapytanie_rekord/autor/autorzywymagają 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 filtrnazwiskozostanie 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żącenazwisko(niepoprzednie_nazwiska).publikacje_autora/publikacje_jednostkimają twardy sufit 100 pozycji (endpointrecent_*). Przy dobiciu do limitu zwracana jest flagaobcieto: true— pełny harvest per autor rób przezlista_publikacjiz chunkowaniem po latach. Endpointrecent_*NIE zwraca łącznej liczby prac encji (jegocountto tylko liczba pozycji po obcięciu), dlatego narzędzie eksponuje wyłączniezwrocono(liczba zwróconych) +obcieto, bez mylącegocount.szukaj_publikacji/szukaj_autora/lista_publikacjizwracająlaczna_liczba(serwerowycount— realna liczba trafień),zwrocono(ile faktycznie przyszło) oraz flagęniepelne.niepelne: trueoznacza, że auto-follow paginacji przerwał bezpiecznik (sufit liczby stron / zapętlonynext) 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/__countitd.), - pola modelu-korzenia z typami i — dla relacji — polem dopasowania,
- całą sekcję
dictionariesz 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.publicationitd. w rdzeniu nie ma — nazwy tych sekcji widać w blokach relacji modelu-korzenia (zapiszrodlo -> bpp.zrodlo) oraz w polu zwrotusekcje_dostepne(obecnym zawsze, w obu trybach, bez korzenia i bezdictionaries). Wywołaniedjangoql_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 albodictionaries— 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
rekordto 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, zmiennaMAX_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 przezimportlib.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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f90f8ecf22838b276251ef21781bbb7af827bc3bd3df660eef3871c371dba4d7
|
|
| MD5 |
433990cf512be945ad4dfbd66365f6e3
|
|
| BLAKE2b-256 |
5f231410eee11b8cc4dc341de2afc87c9cb376a0396fb0b842836ff64871064e
|
Provenance
The following attestation bundles were made for bpp_mcp-0.1.1.tar.gz:
Publisher:
release.yml on iplweb/bpp-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bpp_mcp-0.1.1.tar.gz -
Subject digest:
f90f8ecf22838b276251ef21781bbb7af827bc3bd3df660eef3871c371dba4d7 - Sigstore transparency entry: 2226156033
- Sigstore integration time:
-
Permalink:
iplweb/bpp-mcp@3ead6eb7dcee6d0c5366b0f81ca2f730522d2435 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/iplweb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3ead6eb7dcee6d0c5366b0f81ca2f730522d2435 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83ad144601b15319ac0064998f103334861b68be756b7c84fc9c80bded2ee00f
|
|
| MD5 |
783cf53f9570b6c432f962425f9381e1
|
|
| BLAKE2b-256 |
1a9065584e28ecbced260e441364cef70c6aa1ecbb7bb4bc043d71a9e897e91f
|
Provenance
The following attestation bundles were made for bpp_mcp-0.1.1-py3-none-any.whl:
Publisher:
release.yml on iplweb/bpp-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bpp_mcp-0.1.1-py3-none-any.whl -
Subject digest:
83ad144601b15319ac0064998f103334861b68be756b7c84fc9c80bded2ee00f - Sigstore transparency entry: 2226156421
- Sigstore integration time:
-
Permalink:
iplweb/bpp-mcp@3ead6eb7dcee6d0c5366b0f81ca2f730522d2435 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/iplweb
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3ead6eb7dcee6d0c5366b0f81ca2f730522d2435 -
Trigger Event:
push
-
Statement type: