bpp-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. bpp_mcp-0.1.0/.gitignore +25 -0
  2. bpp_mcp-0.1.0/LICENSE +21 -0
  3. bpp_mcp-0.1.0/PKG-INFO +358 -0
  4. bpp_mcp-0.1.0/README.md +330 -0
  5. bpp_mcp-0.1.0/pyproject.toml +69 -0
  6. bpp_mcp-0.1.0/src/bpp_mcp/__init__.py +11 -0
  7. bpp_mcp-0.1.0/src/bpp_mcp/auth.py +110 -0
  8. bpp_mcp-0.1.0/src/bpp_mcp/catalog.py +163 -0
  9. bpp_mcp-0.1.0/src/bpp_mcp/client.py +304 -0
  10. bpp_mcp-0.1.0/src/bpp_mcp/config.py +84 -0
  11. bpp_mcp-0.1.0/src/bpp_mcp/data/autor_djangoql_schema.compact.txt +1163 -0
  12. bpp_mcp-0.1.0/src/bpp_mcp/data/autorzy_djangoql_schema.compact.txt +1163 -0
  13. bpp_mcp-0.1.0/src/bpp_mcp/data/rekord_djangoql_schema.compact.txt +1163 -0
  14. bpp_mcp-0.1.0/src/bpp_mcp/login_state.py +62 -0
  15. bpp_mcp-0.1.0/src/bpp_mcp/oauth_client.py +374 -0
  16. bpp_mcp-0.1.0/src/bpp_mcp/server.py +385 -0
  17. bpp_mcp-0.1.0/src/bpp_mcp/token_store.py +88 -0
  18. bpp_mcp-0.1.0/src/bpp_mcp/tools.py +557 -0
  19. bpp_mcp-0.1.0/tests/conftest.py +44 -0
  20. bpp_mcp-0.1.0/tests/test_auth.py +94 -0
  21. bpp_mcp-0.1.0/tests/test_cli.py +92 -0
  22. bpp_mcp-0.1.0/tests/test_client.py +146 -0
  23. bpp_mcp-0.1.0/tests/test_client_auth.py +59 -0
  24. bpp_mcp-0.1.0/tests/test_config.py +67 -0
  25. bpp_mcp-0.1.0/tests/test_djangoql_schema.py +103 -0
  26. bpp_mcp-0.1.0/tests/test_http_auth.py +112 -0
  27. bpp_mcp-0.1.0/tests/test_lista_publikacji.py +117 -0
  28. bpp_mcp-0.1.0/tests/test_login_state.py +89 -0
  29. bpp_mcp-0.1.0/tests/test_oauth_client.py +242 -0
  30. bpp_mcp-0.1.0/tests/test_oauth_login.py +247 -0
  31. bpp_mcp-0.1.0/tests/test_pobierz_rekord.py +224 -0
  32. bpp_mcp-0.1.0/tests/test_publikacje_autora.py +79 -0
  33. bpp_mcp-0.1.0/tests/test_publikacje_jednostki.py +57 -0
  34. bpp_mcp-0.1.0/tests/test_server.py +27 -0
  35. bpp_mcp-0.1.0/tests/test_slownik.py +55 -0
  36. bpp_mcp-0.1.0/tests/test_stdio_login.py +50 -0
  37. bpp_mcp-0.1.0/tests/test_szukaj_autora.py +60 -0
  38. bpp_mcp-0.1.0/tests/test_szukaj_publikacji.py +79 -0
  39. bpp_mcp-0.1.0/tests/test_token_store.py +90 -0
  40. bpp_mcp-0.1.0/tests/test_zapytanie.py +273 -0
@@ -0,0 +1,25 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ build/
7
+ dist/
8
+ .eggs/
9
+
10
+ # venv / uv
11
+ .venv/
12
+ venv/
13
+ .python-version
14
+
15
+ # testy / narzędzia
16
+ .pytest_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ htmlcov/
20
+ .mypy_cache/
21
+
22
+ # IDE / OS
23
+ .idea/
24
+ .vscode/
25
+ .DS_Store
bpp_mcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017-2026 IPLWeb / Michał Pasternak <michal.dtz@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
bpp_mcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,358 @@
1
+ Metadata-Version: 2.4
2
+ Name: bpp-mcp
3
+ Version: 0.1.0
4
+ Summary: Serwer MCP dla API BPP (Bibliografia Publikacji Pracowników)
5
+ Project-URL: Homepage, https://github.com/iplweb/bpp-mcp
6
+ Project-URL: Repository, https://github.com/iplweb/bpp-mcp
7
+ Author-email: IPLWeb / Michał Pasternak <michal.dtz@gmail.com>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: api,bibliografia,bpp,llm,mcp
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx>=0.27
21
+ Requires-Dist: mcp[cli]>=1.28.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: respx>=0.21; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # bpp-mcp
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/bpp-mcp.svg)](https://pypi.org/project/bpp-mcp/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/bpp-mcp.svg)](https://pypi.org/project/bpp-mcp/)
33
+ [![tests](https://github.com/iplweb/bpp-mcp/actions/workflows/tests.yml/badge.svg)](https://github.com/iplweb/bpp-mcp/actions/workflows/tests.yml)
34
+
35
+ Serwer [MCP](https://modelcontextprotocol.io) dla **API BPP** (Bibliografia
36
+ Publikacji Pracowników). Wystawia read-only, anonimowe API BPP (`/api/v1/`)
37
+ jako zestaw kuratorowanych, typowanych narzędzi dla Claude Desktop, Claude
38
+ Code i innych klientów MCP.
39
+
40
+ Zamiast żmudnego chodzenia po hyperlinkach REST-owych (publikacja → autorzy →
41
+ jednostka → …), serwer robi to za agenta: rozwija relacje, auto-follow-uje
42
+ paginację i zwraca gotowe, zagnieżdżone obiekty.
43
+
44
+ ## Dlaczego MCP, a nie samo API?
45
+
46
+ API BPP jest **hyperlinked** — relacje to URL-e, nie zagnieżdżone dane.
47
+ Pobranie jednego rekordu z autorami i źródłem to kilka–kilkanaście żądań.
48
+ `bpp-mcp` ukrywa tę złożoność: `pobierz_rekord` zwraca jeden obiekt z
49
+ rozwiniętymi autorami (nazwisko jak wydrukowane), źródłem i streszczeniami.
50
+
51
+ ## Konfiguracja
52
+
53
+ Serwer jest wielo-instancyjny — tę samą binarkę podłączasz do dowolnego
54
+ wdrożenia BPP przez zmienne środowiskowe:
55
+
56
+ | Zmienna | Domyślnie | Opis |
57
+ |---|---|---|
58
+ | `BPP_BASE_URL` | **wymagany** | bazowy URL instancji BPP (API i issuer OAuth) |
59
+ | `BPP_BASIC_AUTH` | *(brak)* | opcjonalny `user:pass` (tylko raporty slotów, stdio) |
60
+ | `BPP_MCP_TRANSPORT` | `stdio` | `stdio` (anon) lub `http` (OAuth per-user) |
61
+ | `BPP_MCP_HTTP_HOST` | `127.0.0.1` | bind serwera HTTP (tryb `http`) |
62
+ | `BPP_MCP_HTTP_PORT` | `8000` | port serwera HTTP (tryb `http`) |
63
+ | `BPP_MCP_RESOURCE_URL` | `http://<host>:<port>/mcp` | pole `resource` w protected-resource-metadata |
64
+
65
+ ## Instalacja i uruchomienie
66
+
67
+ Pakiet jest na [PyPI](https://pypi.org/project/bpp-mcp/). Najprościej — bez
68
+ instalowania czegokolwiek na stałe, przez [uv](https://docs.astral.sh/uv/):
69
+
70
+ ```bash
71
+ BPP_BASE_URL=https://bpp.twoja-uczelnia.pl uvx bpp-mcp
72
+ ```
73
+
74
+ `uvx` pobiera pakiet do własnego cache'a i uruchamia go w odizolowanym
75
+ środowisku — nic nie ląduje w Twoim systemowym Pythonie.
76
+
77
+ Jeśli wolisz mieć komendę `bpp-mcp` na stałe w `PATH`:
78
+
79
+ ```bash
80
+ uv tool install bpp-mcp # albo: pip install bpp-mcp
81
+ BPP_BASE_URL=https://bpp.twoja-uczelnia.pl bpp-mcp
82
+ ```
83
+
84
+ Aktualizacja: `uv tool upgrade bpp-mcp` (przy `uvx` wystarczy `uvx bpp-mcp@latest`).
85
+
86
+ <details>
87
+ <summary>Wersja rozwojowa prosto z gita (niewydany kod)</summary>
88
+
89
+ ```bash
90
+ BPP_BASE_URL=https://bpp.twoja-uczelnia.pl \
91
+ uvx --from git+https://github.com/iplweb/bpp-mcp bpp-mcp
92
+ ```
93
+
94
+ Bierze czubek gałęzi `main`, więc dostajesz zmiany jeszcze przed wydaniem —
95
+ ale też przed ich przetestowaniem w praktyce. Do normalnego użycia weź wersję
96
+ z PyPI.
97
+
98
+ </details>
99
+
100
+ `BPP_BASE_URL` jest **wymagany i nie ma wartości domyślnej** — bez niego serwer
101
+ nie wystartuje, tylko wypisze, czego brakuje. To celowe: każde wdrożenie BPP to
102
+ inna uczelnia i inna bibliografia, więc zaszyty host oznaczałby, że użytkownik
103
+ bez tej zmiennej dostaje cudze dane wyglądające na własne.
104
+
105
+ Serwer komunikuje się po stdio (standard MCP) — normalnie uruchamia go klient
106
+ MCP, nie użytkownik ręcznie.
107
+
108
+ ### Tryb OAuth (HTTP, per-user)
109
+
110
+ Domyślnie `bpp-mcp` działa po **stdio** i anonimowo (dane publiczne). Aby działać
111
+ **z uprawnieniami zalogowanego użytkownika BPP** (OAuth 2.1):
112
+
113
+ ```bash
114
+ BPP_BASE_URL=https://bpp.twoja-uczelnia.pl uvx bpp-mcp --http --port 8000
115
+ ```
116
+
117
+ Klient MCP (Claude) sam przeprowadza logowanie: wykrywa serwer autoryzacji BPP
118
+ przez `/.well-known/oauth-protected-resource`, rejestruje się (DCR), otwiera
119
+ przeglądarkę na logowanie BPP + ekran zgody (scope `read`), po czym wywołuje
120
+ narzędzia z `Bearer`. `bpp-mcp` weryfikuje token przez `GET /api/v1/whoami/` i
121
+ forwarduje token **bieżącego requestu** do `/api/v1/`. Zapis jest zablokowany
122
+ serwerowo (read-only).
123
+
124
+ **Bezpieczeństwo:** trzymaj `--host 127.0.0.1` (domyślnie). Bind na inny host
125
+ wyłącza wbudowaną ochronę DNS-rebinding SDK i eksponuje serwer poza maszynę.
126
+ Token jest forwardowany do API BPP bez wiązania `audience` (świadome odstępstwo
127
+ od MCP-MUST: `bpp-mcp` i API BPP = ta sama domena zaufania; mitygacje: scope
128
+ `read`, twardy read-only serwerowo, krótki TTL).
129
+
130
+ ### Logowanie w trybie stdio (per-user, bez hostowania)
131
+
132
+ Domyślny tryb stdio może działać **z uprawnieniami zalogowanego użytkownika**
133
+ bez uruchamiania serwera HTTP. Zaloguj się **raz**:
134
+
135
+ ```bash
136
+ BPP_BASE_URL=https://bpp.twoja-uczelnia.pl uvx bpp-mcp login
137
+ ```
138
+
139
+ Otworzy się przeglądarka na logowanie BPP (hasło/LDAP/Microsoft/ORCID/Keycloak)
140
+ i ekran zgody (scope `read`). Po zalogowaniu token trafia do lokalnego pliku
141
+ `~/.config/bpp-mcp/<instancja>/tokens.json` (uprawnienia `0600`), a `bpp-mcp`
142
+ uruchamiany przez Claude forwarduje go do `/api/v1/` — bez dodatkowych kroków.
143
+
144
+ **Praca zdalna / host bez GUI.** Adres autoryzacji jest zawsze wypisywany też
145
+ tekstem, więc można go otworzyć w przeglądarce na innej maszynie. Callback na
146
+ `127.0.0.1` wtedy nie wróci (przeglądarka jest gdzie indziej) — po zalogowaniu
147
+ skopiuj z paska adresu cały adres przekierowania (zaczyna się od
148
+ `http://127.0.0.1:`) albo sam parametr `code` i wklej w terminalu, gdzie czeka
149
+ `bpp-mcp login`. Obie drogi — loopback i wklejka — działają równolegle; liczy
150
+ się ta, która dojdzie pierwsza.
151
+
152
+ Co odblokowuje:
153
+
154
+ - **bogatsze wyniki** istniejących narzędzi (rekordy widoczne dla Twojego konta),
155
+ - narzędzia **`zapytanie_rekord` / `zapytanie_autor` / `zapytanie_autorzy`**
156
+ (wykonywanie DjangoQL) — wymagają zalogowania i uprawnień redaktora.
157
+
158
+ Wylogowanie (usuwa token tej instancji):
159
+
160
+ ```bash
161
+ BPP_BASE_URL=https://bpp.twoja-uczelnia.pl uvx bpp-mcp logout
162
+ ```
163
+
164
+ **Gdy instancja nie wystawia `/.well-known/`.** Logowanie zaczyna się od
165
+ odczytu metadanych serwera autoryzacji (RFC 8414) spod
166
+ `/.well-known/oauth-authorization-server`. Część wdrożeń blokuje na brzegu cały
167
+ `/.well-known/` (typowo regułą nginksa na pliki ukryte, `location ~ /\.`) i
168
+ oddaje `403`, mimo że serwer autoryzacji działa. `bpp-mcp` cofa się wtedy na
169
+ konwencjonalne ścieżki django-oauth-toolkit (`/o/authorize/`, `/o/token/`,
170
+ `/o/register/`) na tym samym hoście i loguje normalnie. Prawidłowo wystawione
171
+ metadane zawsze mają pierwszeństwo. Właściwą naprawą po stronie serwera jest
172
+ `location ^~ /.well-known/` przed regułą na pliki ukryte — bez tego natywny
173
+ przycisk „authorize" w trybie HTTP nadal nie zadziała (tam discovery robi sam
174
+ klient Claude, nie `bpp-mcp`).
175
+
176
+ Token jest krótkotrwały (access ~30 min) i odświeżany po cichu (refresh ~7 dni,
177
+ rotujący). Zmiana hasła lub dezaktywacja konta w BPP unieważnia go — wtedy
178
+ `bpp-mcp` wraca do trybu anonimowego, a narzędzia `zapytanie_*` poproszą o
179
+ ponowne `bpp-mcp login`. Host bierze z `BPP_BASE_URL` (wymagany).
180
+
181
+ **Różnica względem trybu HTTP:** natywny przycisk „authorize" w Claude (jak przy
182
+ GitHub) należy do trybu HTTP (sekcja wyżej) — wymaga działającego serwera pod
183
+ URL-em. Tryb stdio nie pokazuje tego przycisku; logowanie przeprowadza komenda
184
+ `bpp-mcp login`. Oba forwardują token do tego samego API i wykluczają zapis
185
+ (read-only serwerowo).
186
+
187
+ ## Podłączenie do Claude Desktop
188
+
189
+ Dodaj wpis w pliku konfiguracyjnym Claude Desktop
190
+ (`claude_desktop_config.json`):
191
+
192
+ ```json
193
+ {
194
+ "mcpServers": {
195
+ "bpp": {
196
+ "command": "uvx",
197
+ "args": ["bpp-mcp"],
198
+ "env": {
199
+ "BPP_BASE_URL": "https://bpp.twoja-uczelnia.pl"
200
+ }
201
+ }
202
+ }
203
+ }
204
+ ```
205
+
206
+ Jeśli `uvx` nie jest w `PATH` Claude Desktop (typowe na macOS — aplikacja nie
207
+ dziedziczy `PATH` z powłoki), podaj pełną ścieżkę, np. `~/.local/bin/uvx`;
208
+ pokaże ją `which uvx`.
209
+
210
+ ## Podłączenie do Claude Code
211
+
212
+ ```bash
213
+ claude mcp add bpp \
214
+ --env BPP_BASE_URL=https://bpp.twoja-uczelnia.pl \
215
+ -- uvx bpp-mcp
216
+ ```
217
+
218
+ ## Narzędzia
219
+
220
+ | Narzędzie | Rola |
221
+ |---|---|
222
+ | `szukaj_publikacji(q, rok_od?, rok_do?, limit=25)` | rankowane wyszukiwanie pełnotekstowe publikacji |
223
+ | `szukaj_autora(nazwisko)` | znajdź autorów po (bieżącym) nazwisku |
224
+ | `publikacje_autora(id_lub_slug, rok_od?, rok_do?, limit=25)` | publikacje autora (ID lub slug) |
225
+ | `publikacje_jednostki(id_lub_slug, rok_od?, rok_do?, limit=25)` | publikacje jednostki i pod-jednostek |
226
+ | `pobierz_rekord(typ, id, pelne_dane_autorow=False)` | detal rekordu z rozwiniętymi relacjami |
227
+ | `lista_publikacji(typ, rok_od?, rok_do?, charakter_formalny?, zmienione_po?, limit=25, offset=0)` | harvest/przyrost listy publikacji |
228
+ | `slownik(rodzaj)` | mały słownik referencyjny (tłumaczenie ID↔nazwa) |
229
+ | `zapytanie_rekord(q, limit=25, offset=0)` | **wykonaj** DjangoQL po publikacjach (`bpp.Rekord`) — autoryzowane |
230
+ | `zapytanie_autor(q, limit=25, offset=0)` | **wykonaj** DjangoQL po autorach (`bpp.Autor`) — autoryzowane |
231
+ | `zapytanie_autorzy(q, limit=25, offset=0)` | **wykonaj** DjangoQL po wpisach autorstwa (`bpp.Autorzy`) — autoryzowane |
232
+ | `djangoql_schema(model="rekord")` | schemat DjangoQL-dla-LLM korzenia `rekord`/`autor`/`autorzy` (do budowy zapytań) |
233
+
234
+ **Zapytania DjangoQL (`zapytanie_*`) są AUTORYZOWANE** — endpointy
235
+ `/api/v1/zapytanie/{rekord,autor,autorzy}/` wymagają `Bearer` (tryb OAuth/HTTP,
236
+ patrz wyżej) albo sesji, oraz uprawnień redaktora (superuser lub staff w grupie
237
+ „wprowadzanie danych"). Bez tego zwracają czytelny błąd: 401 (token), 403 (brak
238
+ uprawnień), 400 (zła składnia/pole, z pozycją do korekty; pola PII jak
239
+ `autor.email` są zablokowane), 503 (timeout — zawęź). Buduj zapytanie z
240
+ `djangoql_schema("rekord")`; w trybie stdio bez tokenu dostaniesz 401/403.
241
+
242
+ Dodatkowo serwer wystawia **prompt** MCP (nie narzędzie wykonujące):
243
+
244
+ | Prompt | Rola |
245
+ |---|---|
246
+ | `zloz_zapytanie_djangoql(opis)` | złóż zapytanie DjangoQL (z opisu po polsku) — wykonasz je `zapytanie_rekord` |
247
+
248
+ `typ` w `pobierz_rekord` / `lista_publikacji`: `wydawnictwo_ciagle`,
249
+ `wydawnictwo_zwarte`, `patent`, `praca_doktorska`, `praca_habilitacyjna`.
250
+
251
+ `rodzaj` w `slownik`: `charakter_formalny`, `typ_kbn`, `jezyk`,
252
+ `dyscyplina_naukowa`, `rodzaj_zrodla`, `poziom_wydawcy`, `funkcja_autora`,
253
+ `tytul`, `czas_udostepnienia_openaccess`. Dane wolumenowe
254
+ (konferencja/wydawca/nagroda) są odrzucane — to nie słowniki.
255
+
256
+ ### Uwagi
257
+
258
+ - **`szukaj_publikacji` i `szukaj_autora` wymagają instancji BPP z Fazą 0**
259
+ (rozszerzenie API o wyszukiwanie). Na starszej instancji `szukaj_publikacji`
260
+ zwróci czytelny błąd (404 → komunikat o wymaganej wersji).
261
+ - **`zapytanie_rekord/autor/autorzy` wymagają nowszej instancji BPP** (z
262
+ endpointami `/api/v1/zapytanie/*`) **oraz uwierzytelnienia** (Bearer/sesja +
263
+ uprawnienia redaktora) — patrz tabela wyżej. Pozostałe narzędzia
264
+ (`publikacje_*`, `pobierz_rekord`, `lista_publikacji`, `slownik`) są anonimowe
265
+ i działają na każdej wersji API.
266
+ - **`szukaj_autora` — wykrywanie możliwości:** django-filter po cichu ignoruje
267
+ nieznane parametry. Na starej instancji filtr `nazwisko` zostanie
268
+ zignorowany i endpoint zwróci *wszystkich* autorów bez błędu. Narzędzie
269
+ ustawia wtedy flagę `mozliwe_ze_niefiltrowane` (gdy trafień jest podejrzanie
270
+ dużo). Filtr obejmuje wyłącznie bieżące `nazwisko` (nie `poprzednie_nazwiska`).
271
+ - **`publikacje_autora` / `publikacje_jednostki`** mają twardy sufit 100
272
+ pozycji (endpoint `recent_*`). Przy dobiciu do limitu zwracana jest flaga
273
+ `obcieto: true` — pełny harvest per autor rób przez `lista_publikacji`
274
+ z chunkowaniem po latach. Endpoint `recent_*` NIE zwraca łącznej liczby
275
+ prac encji (jego `count` to tylko liczba pozycji po obcięciu), dlatego
276
+ narzędzie eksponuje wyłącznie `zwrocono` (liczba zwróconych) + `obcieto`,
277
+ bez mylącego `count`.
278
+ - **`szukaj_publikacji` / `szukaj_autora` / `lista_publikacji`** zwracają
279
+ `laczna_liczba` (serwerowy `count` — realna liczba trafień), `zwrocono`
280
+ (ile faktycznie przyszło) oraz flagę `niepelne`. `niepelne: true` oznacza,
281
+ że auto-follow paginacji przerwał bezpiecznik (sufit liczby stron / zapętlony
282
+ `next`) zanim objął wszystko — wynik może być niekompletny.
283
+
284
+ ## DjangoQL — schemat do budowy zapytań (`djangoql_schema`)
285
+
286
+ `djangoql_schema(model)` zwraca zbundlowany, bezpieczny schemat jednego z trzech
287
+ korzeni — `rekord` (`bpp.Rekord`), `autor` (`bpp.Autor`), `autorzy`
288
+ (`bpp.Autorzy`) — po jednym na endpoint `/api/v1/zapytanie/*`
289
+ dla języka [DjangoQL](https://github.com/ivelum/djangoql): reguły gramatyki
290
+ (operatory per typ, negacja, trawersowanie relacji, sufiksy `__year` / `__count`
291
+ itd.), pola z typami oraz sekcję `dictionaries` z dozwolonymi WARTOŚCIAMI
292
+ wyłącznie bezpiecznych słowników zamkniętych (charaktery, dyscypliny, języki,
293
+ licencje OA…). W schemacie NIE ma żadnych danych osób ani instytucji.
294
+
295
+ Dzięki temu LLM może zbudować PRECYZYJNE zapytanie, np.:
296
+
297
+ ```text
298
+ rok >= 2020 and jezyk.nazwa = "angielski" and impact_factor > 0
299
+ ```
300
+
301
+ - **Konstrukcja tu, wykonanie osobno.** To narzędzie tylko *buduje* zapytania.
302
+ Wykonasz je narzędziami `zapytanie_rekord` / `zapytanie_autor` /
303
+ `zapytanie_autorzy` (patrz tabela narzędzi) — wymagają zalogowania
304
+ (Bearer/sesja + uprawnienia redaktora); anonimowo zwracają 401/403.
305
+ - **Wersjonowanie.** Pierwsza linia schematu to `# BPP <wersja>` (np.
306
+ `# BPP 202607.1397`). Plik jest generowany per wersja BPP i powinien pasować
307
+ do odpytywanej instancji. Źródło: repo
308
+ [iplweb/bpp-schema-for-llm](https://github.com/iplweb/bpp-schema-for-llm)
309
+ (schemat przeskanowany — bez danych osobowych). Plik jest zbundlowany jako
310
+ zasób pakietu (`bpp_mcp/data/`) i wczytywany przez `importlib.resources`.
311
+
312
+ ### Prompt `zloz_zapytanie_djangoql(opis)` — złóż zapytanie do wklejenia
313
+
314
+ Serwer wystawia prompt MCP `zloz_zapytanie_djangoql(opis)`. To **nie** jest
315
+ narzędzie wykonujące — prompt zwraca instrukcję dla klienta LLM, jak z opisu po
316
+ polsku ułożyć jedno poprawne zapytanie DjangoQL. Instrukcja każe najpierw
317
+ wywołać `djangoql_schema("rekord")` (jedyne źródło pól, typów, relacji i wartości
318
+ `dictionaries`), podaje zwięzłe reguły (operator wg typu, trawersacja relacji
319
+ kropką, wartości słownikowe dosłownie, negacja tylko `!=`/`!~`/`not in`,
320
+ łączenie `and`/`or` + nawiasy), a na końcu każe zwrócić gotowe zapytanie w bloku
321
+ kodu. **Wykonasz** je narzędziem `zapytanie_rekord` (po zalogowaniu) albo wklejasz
322
+ w edytor „zapytanie" BPP — prompt, tak jak `djangoql_schema`, tylko *konstruuje*,
323
+ nie *wykonuje*.
324
+
325
+ ## Rozwój
326
+
327
+ ```bash
328
+ uv sync --extra dev
329
+ uv run ruff format .
330
+ uv run ruff check .
331
+ uv run pytest -q
332
+ ```
333
+
334
+ Testy są w pełni offline (mock httpx przez [respx](https://lundberg.github.io/respx/));
335
+ domyślne CI nie wykonuje żadnych żywych wywołań.
336
+
337
+ ### Wydanie na PyPI
338
+
339
+ Publikacja idzie przez [trusted publishing](https://docs.pypi.org/trusted-publishers/)
340
+ (OIDC) — w repozytorium nie ma i nie może być tokenu API PyPI. Wydanie wyzwala
341
+ push tagu:
342
+
343
+ ```bash
344
+ # 1. podbij `version` w pyproject.toml, zacommituj
345
+ # 2. otaguj i wypchnij
346
+ git tag vX.Y.Z
347
+ git push origin vX.Y.Z
348
+ ```
349
+
350
+ Workflow [`release.yml`](.github/workflows/release.yml) przepuszcza pełną
351
+ matrycę testów, **sprawdza, czy tag zgadza się z `project.version`** (rozjazd =
352
+ przerwane wydanie, bo numeru raz zajętego na PyPI nie da się odzyskać), buduje
353
+ sdist + wheel, weryfikuje je `twine check --strict` i obecność zbundlowanych
354
+ schematów DjangoQL, po czym publikuje z osobnego joba w środowisku `pypi`.
355
+
356
+ ## Licencja
357
+
358
+ MIT — IPLWeb / Michał Pasternak. Patrz [LICENSE](LICENSE).