Skip to content

Releases: numikel/law-scrapper-mcp

v4.3.1

Choose a tag to compare

@github-actions github-actions released this 06 Sep 21:20
3e286b7

Changelog - v4.3.1

All notable changes to the law-scrapper-mcp project for version v4.3.1 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[4.3.1] - 2026-09-06

Two error-message findings deferred from the v4.3.0 review (issues #60 and #61). Both
change what a caller reads on failure, neither changes a contract: the accepted range of
LAW_MCP_ERROR_MESSAGE_MAX_CHARS is unchanged, no error category was added or renamed, and
no tool schema moved.

Fixed

  • A truncated content_too_large message no longer cuts the source PDF URL in half. Under a
    low LAW_MCP_ERROR_MESSAGE_MAX_CHARS the cut used to land inside the URL — the one token the
    caller can act on — while the appended guidance still said to fetch the content from "the
    given address". The cut now lands in the descriptive prefix and the trailing URL is kept whole
    whenever it fits within the cap; when even the URL plus the truncation announcement would
    exceed the cap, the plain cut still applies, so the cap always holds. That fallback is pinned
    by a regression test as a deliberate trade-off (#60).
  • A transport failure toward api.sejm.gov.pl (connection refused, read timeout, protocol
    error) no longer copies the raw httpx text — host, URL, TLS or errno detail — into the tool
    error or the ERROR log. The message names the failure class and the endpoint
    (Błąd połączenia z API Sejmu (ConnectError) podczas żądania GET acts/DU/2024/1); the
    httpx exception is kept as the chained cause and logged at DEBUG only, the same ERROR/DEBUG
    split the validation and upstream categories already had. With this, every unavailable
    message is project-authored at its raise site (#61).
  • law_scrapper_mcp.__version__ is read from the installed distribution instead of a hardcoded
    string that sat outside the release script's synced locations and had stayed at 3.0.0 since
    v3.0.0. A checkout that was never installed reports 0.0.0+unknown.

v4.3.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 19:58
a72e3c9

Changelog - v4.3.0

All notable changes to the law-scrapper-mcp project for version v4.3.0 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[4.3.0] - 2026-09-06

Failed content loading now looks like a failure: a transient upstream problem during
get_act_details(load_content=True) used to surface as a successful response with no readable
text, and now fails the call itself. A new regression test also pins the guarantee, introduced
when this project migrated to the official MCP SDK, that any domain exception surfaces as a
protocol error (isError=true) rather than a success body with an embedded error field. Every
error message built from caller-sourced or exception text is now length-bounded.

BREAKING — content loading failures are protocol errors

get_act_details(eli=..., load_content=True) used to return a successful response with
is_loaded=false whenever loading failed — an open circuit breaker, a timeout, an HTTP 5xx
from api.sejm.gov.pl, or a conversion error. The only record of the failure was a server log
line, and the response even hinted at retrying the call that had just failed.

Such calls now fail with isError=true. Clients that treated is_loaded=false as "this act has
no text" must distinguish two cases:

  • content_status="unavailable" in a successful response — the act permanently has no
    readable text; retrying will not change that.
  • isError=true — the upstream is unreachable; retrying later may succeed.

This mirrors the change v3.1.1 made for oversized acts, which shipped as a PATCH; it is a
signalling fix, since the contract always meant "success = content loaded".

Added

  • content_status field on get_act_details output, with values not_requested, loaded and
    unavailable. The field is part of the tool's outputSchema and its structuredContent.
    loaded reflects the final in-memory state rather than this call's load_content flag, so an
    act loaded by an earlier call reports loaded even on a metadata-only call that never asked to
    load anything.
  • LAW_MCP_ERROR_MESSAGE_MAX_CHARS (default 500, range 80-10000) caps the length of error
    messages built from an exception's own text, which can quote caller input of unbounded length.
    Truncation is announced in the message rather than silent.
  • Every tool error message now ends with a fixed one-sentence remediation hint, so a caller can
    tell "retry shortly" from "this identifier does not exist" without parsing category names.
    Seven categories carry a hint: not_found, validation, precondition, content_too_large,
    unavailable, upstream, internal. content_too_large (a refusal because the act is too
    large to convert) is split out of precondition rather than sharing its "do a step first"
    wording — there is no prior step for an oversized act, and the one actionable remedy (fetch the
    source file) is already the last sentence of the body.

Changed

  • ActService._load_content no longer swallows exceptions. Transient failures propagate; only a
    permanent absence of readable text is handled internally, as ContentNotAvailableError — no
    HTML or PDF URL at all, a 404 on the text.html or text.pdf fetch, or an empty extraction
    from either format.
  • An act with neither an HTML nor a PDF text URL no longer triggers a request that its metadata
    already proves pointless.
  • A permanent absence is remembered for as long as the act's metadata stays cached
    (LAW_MCP_CACHE_DETAILS_TTL, default one hour): a repeated load_content=true call on such an
    act answers content_status="unavailable" from memory instead of re-fetching text.html /
    text.pdf. The removed placeholder document used to play that role by accident; without a
    replacement every retry would have cost api.sejm.gov.pl one or two requests for an answer that
    cannot change before the metadata it was derived from expires.
  • Hints for an act with no readable content no longer point at read_act_content, search_in_act
    or another load_content=true call; one hint names the source PDF URL when the act has one,
    alongside the unrelated analyze_act_relationships hint every get_act_details response
    already carries. An act with no PDF either gets the same explanatory hint without a URL to
    offer, rather than one pointing at a file the server itself declined to fetch.
  • A message built from an exception's own text now ends in a sentence terminator before the
    remediation sentence is appended, unless it already ends in one or ends in a URL — so the two
    sentences don't run together, and a ContentTooLargeError message can still end on a bare,
    unpunctuated PDF URL without a stray period being glued onto it.
  • The set of error categories treated as caller-sourced — and therefore subject to the length
    cap — is now validation, not_found, precondition, content_too_large and unavailable.
    unavailable is included because ApiUnavailableError's text can embed a raw httpx
    exception, which is not authored by this project even though the category itself is not new.

Removed

  • The placeholder documents *No readable content available for {eli}…* and
    *Content extraction failed…*. They were stored in the document store as if they were the act,
    made is_loaded=true untrue, and were matched by search_in_act.

Fixed

  • A failure to load content is no longer indistinguishable from an act without text: a transient
    upstream failure now propagates as isError=true instead of silently reporting
    is_loaded=false.
  • validation, not_found, precondition, content_too_large and unavailable error messages
    are now length-bounded, so text quoting unbounded caller input or upstream detail cannot fill
    the caller's whole context window.

v4.2.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 18:15
bc69f04

Changelog - v4.2.0

All notable changes to the law-scrapper-mcp project for version v4.2.0 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[4.2.0] - 2026-09-02

Sweep of the deferred review findings from clusters 2–9 (GitHub issues #14, #16, #18#21, #27,
#31, #32, #34, #38#41, #45#47, #49, #50, #52, #54, #55). No MCP tool changes its name,
parameter names or response shape; several fields and validations now behave as documented.

Added

  • LAW_MCP_API_MAX_SERVER_PAUSE (default 60, at most 600): the cap on how long a
    server-sent Retry-After may hold all outbound traffic, previously a fixed constant. The
    clamp now lives in the rate limiter itself, so every caller of pause_for() inherits it (#46,
    #47).
  • Document downloads are budgeted while streaming: the client aborts a HTML or PDF body as
    soon as it passes LAW_MCP_DOC_STORE_MAX_SIZE_BYTES (or earlier, from Content-Length),
    raising the same Polish ContentTooLargeError the conversion step already used. The whole
    body is no longer materialised before the size check (#19).
  • search_in_act builds only the requested page of match positions (DocumentStore.scan_page)
    instead of every match in the document; memory is bounded by limit, total_count stays
    exact (#16).
  • CI builds the Docker image and smoke-tests both transports (/health over streamable-http, an
    initialize round-trip over STDIO), so the image cannot silently stop building again.
  • CI runs the MCP conformance suite for real again, on @modelcontextprotocol/conformance
    0.2.0-alpha.11 — the first line that accepts the SDK's protocol era 2026-07-28 — with the
    baseline in conformance-baseline.yml (#14).

Changed

  • search_legal_acts validates limit and offset like every other listing tool: a malformed
    or non-positive limit, or a malformed or negative offset, is a tool error with a Polish
    message instead of silently yielding page one. limit still has no upper clamp; its
    description now says why (#18, #19).
  • track_legal_changes sends limit and offset upstream and reads totalCount, so a date
    range wider than one API page is stored as a page-scoped set with a truthful
    corpus_count instead of being labelled complete after silent upstream truncation (#54).
  • DocumentStore.load refuses a document over the size limit instead of truncating it (the
    branch was unreachable from production since 4.0.0, and its character slice against a byte
    budget was wrong for Polish text) (#32, #21).
  • LAW_MCP_LOG_LEVEL accepts exactly DEBUG, INFO, WARNING, ERROR, CRITICAL
    (case-insensitive); WARN-style aliases that used to work on STDIO and crash on
    streamable-http are rejected at startup on both. LAW_MCP_SHUTDOWN_GRACE is an integer
    (#31).
  • LAW_MCP_AUTH_REQUIRED_SCOPES with LAW_MCP_AUTH_MODE=bearer is rejected at startup: a
    shared secret has no scope semantics, so the setting could never restrict anything. Remove
    the variable or switch to oauth (#38).
  • LAW_MCP_API_RATE_PER_SECOND is bounded to 0.1100 and rejects non-finite values;
    LAW_MCP_API_RATE_BURST to 11000; LAW_MCP_API_MAX_ATTEMPTS to 120 (#47, #27).
  • /health is exempt from the per-client rate limiter only for loopback peers; a probe from
    another host is metered like any other request (#39).
  • JWKS discovery no longer runs while holding the verifier's lock, so concurrent requests
    during a slow identity-provider response are not serialised behind it (#39).
  • build_http_app() derives the auth settings and token verifier from the live settings
    object, the same place it already reads host and rate-limit settings (#41).
  • uvicorn is a declared dependency rather than a transitive one (#31).
  • Removed two internal methods without production callers: DocumentStore.search() and
    MetadataService.get_metadata(); ContentTooLargeError is exported from the client
    facade (#20, #31).

Fixed

  • A bracketed IPv6 allowlist entry with a trailing slash (http://[::1]:8080/) was classified as
    remote after the [::1].evil.com tightening, so a value 4.1.0 accepted refused startup under
    auth_mode=none; the host parser now drops any path suffix before classifying, for every form.
  • A cache entry for acts/search is shared by search_legal_acts, browse_acts and
    track_legal_changes whenever their parameters coincide, and the first writer's TTL used to
    decide freshness for all three. Each read is now bounded by its own caller's TTL, so a call
    documented as 300 s is never served a 600 s entry.
  • The Docker image did not build: the builder stage ran uv sync --no-editable, which builds
    the project wheel, without README.md in the build context, and hatchling refuses to build
    a package whose declared readme is missing. Broken since the readme field was added on
    2026-08-10; surfaced by the Glama indexer. A test now pins that the builder stage copies
    every file the package metadata names.
  • effective_date in search_legal_acts, browse_acts and track_legal_changes was always
    null: the code read a dateEffect key that neither list endpoint returns. It now reads
    entryIntoForce, the field the API does return, so the effective_date filter and sort in
    filter_results operate on data (#52).
  • A negative offset passed to search_legal_acts reached api.sejm.gov.pl verbatim (#18).
  • httpx's request log line carried the full search URL — keywords and title included — at
    INFO, the path the F13 audit finding never covered. The httpx and httpcore loggers are now
    held at WARNING or above (#34).
  • [::1].evil.com in LAW_MCP_ALLOWED_HOSTS was classified as loopback because the bracket
    parser ignored everything after ]; trailing content other than a port or :* now disables
    the loopback match (#38).
  • backoff() raised OverflowError for a large attempt number and returned sub-base delays
    for attempts below 1; the exponent is now bounded and computed in floating point (#27).
  • LAW_MCP_API_RATE_PER_SECOND=inf was accepted and silently disabled pacing (#47).
  • The pacing deadline and the rate limiter measured time on different clocks when a limiter
    was injected, which made the deadline bound fail open; both now share the limiter's clock
    (#45).
  • StaticTokenVerifier accepts no empty secret, so an empty Authorization value can never
    match one (#38).
  • A section with end_pos == 0 was treated as open-ended by section_for_position;
    overlapping sections are now rejected when a document is loaded (#20).

v4.1.0

Choose a tag to compare

@github-actions github-actions released this 02 Sep 06:59

Changelog - v4.1.0

All notable changes to the law-scrapper-mcp project for version v4.1.0 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[4.1.0] - 2026-09-02

Result-set scope as first-class contract — a single new field disambiguates complete result sets from windows into larger match sets, closing two categories of false inference over stored results.

Added

  • Result-set scope: A new set_scope field on every tool response indicates whether the result set is complete (the full set of matches for the query) or page (a window into a larger match set). This directly addresses misinterpretations where a tool returned twenty results from a corpus of thousands but the response implied it was the full set. The scope is stored alongside results in ResultStore and propagates to filter_results, so filtering and paginating both carry the scope forward. Clients that do not use ResultStore can read the scope from result_set_info.set_scope.
  • Correct total_count in search_legal_acts: Previously, search_legal_acts reported the current page size as total_count (e.g., 20 when querying a corpus of 1984 matches). The API response carries both count (page size) and totalCount (corpus size) — now the tool reads totalCount, so pagination hints and scope are computed against the actual match count. This reverses a long-standing misreport in the pagination field: was_truncated now correctly reflects whether there are more matches beyond the current page.

Changed

  • Pagination hints are now scope-aware: A hint to paginate is only generated when set_scope="page". When set_scope="complete", results cannot be paginated further (the full set fits), so hints encourage filtering instead. This eliminates the prior pattern of suggesting pagination to a tool that was always returning the complete set.

v4.0.2

Choose a tag to compare

@github-actions github-actions released this 01 Sep 17:21

Changelog - v4.0.2

All notable changes to the law-scrapper-mcp project for version v4.0.2 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[4.0.2] - 2026-09-01

Outbound politeness towards api.sejm.gov.pl — egress rate limiting and concurrent-request budgeting to respect the public API. No MCP tool changes signature or response shape.

Added

  • Egress rate limiting — A token bucket now bounds how fast requests leave for the Sejm API, on top of the existing concurrency bound. Configurable with LAW_MCP_API_RATE_PER_SECOND (default 5.0) and LAW_MCP_API_RATE_BURST (default 10). A Retry-After sent by the API now pauses the whole client rather than each failing request separately, so honouring it no longer ends in a synchronised retry burst. The pause is capped at 60 seconds regardless of the header's requested duration, so a large or misconfigured Retry-After cannot silently wedge every in-flight tool call. Pacing is also bounded by LAW_MCP_API_RETRY_BUDGET: a call that cannot be paced within its own time budget fails immediately with a message naming the pause, instead of waiting out a window it was never going to survive and returning a bare client-side timeout.
  • Retry-After is now read in both forms RFC 9110 allows. Only the delta-seconds form was understood before; a date-form header — which the WAF in front of the API may send even though the API itself does not — was discarded, which silently left the client-wide pause switched off. Repeated headers are resolved to the longest wait.
  • Separate concurrency budget for content downloadsLAW_MCP_API_MAX_CONCURRENT_CONTENT (default 2) governs act HTML and PDF downloads, so a run of document fetches can no longer occupy every slot and stall concurrent searches.

Changed

  • LAW_MCP_API_MAX_CONCURRENT default lowered from 10 to 8, and its meaning narrowed to light JSON requests. Together with the new LAW_MCP_API_MAX_CONCURRENT_CONTENT of 2 the peak concurrency the API sees is unchanged at ten. Deployments that relied on the default now get eight light slots plus two heavy ones; set both variables explicitly to restore any other split.
  • browse_acts fetches one page instead of a whole year. It now queries acts/search with limit and offset rather than acts/{publisher}/{year}, which ignores both and returns the full year every time — 1 093 224 B and 1984 records for DU/2024, of which a default page kept twenty. Results, ordering and response fields are unchanged. A browse_acts call with a non-numeric year now returns a clean tool error instead of silently querying year=0. limit is now clamped to the same maximum of 100 items every other list tool applies, because it reaches the API and decides the page width where the year endpoint used to ignore it; a negative limit or offset returns a clean tool error instead of being silently dropped.
  • search_legal_acts and browse_acts now query the same acts/search endpoint, which makes a pre-existing asymmetry newly visible: search_legal_acts still reports total_count as the size of the current page, while browse_acts correctly reports the size of the whole year.

v4.0.1

Choose a tag to compare

@github-actions github-actions released this 31 Aug 19:25

Changelog - v4.0.1

All notable changes to the law-scrapper-mcp project for version v4.0.1 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[4.0.1] - 2026-08-31

Cleanup release — hardening and test coverage from Klaster 7 review findings, no behavior change for existing correct configurations.

Fixed

  • trusted_proxies CIDR validation moved to startup — Invalid LAW_MCP_TRUSTED_PROXIES entries now fail fast at configuration time instead of on the first proxied request.
  • Binary auth token files no longer crash with UnicodeDecodeErrorLAW_MCP_AUTH_TOKEN_FILE pointing at a non-text file now produces a clean, Polish-language configuration error.
  • OAuth JWKS discovery rejects non-https:// URIs — A discovered JWKS URI that isn't https:// is refused instead of being fetched, closing a downgrade path.
  • LAW_MCP_AUTH_ISSUER/LAW_MCP_AUTH_JWKS_URI now must be https:// — The discovery-time check above only covered a JWKS URI discovered from the issuer; a directly configured auth_jwks_uri skipped discovery entirely, and the discovery request itself could still go out over plain HTTP. Both are now rejected at startup.
  • JWKS/IdP communication failures now log at WARNING instead of INFO — Includes a fix for an except-clause ordering bug that had silently suppressed the intended WARNING level for these failures.

Changed

  • Test coverage strengthened — Added a 31-byte token boundary test, a rate-limit-zero rejection test, a compare_digest timing-safety spy, and fixed caplog scoping so auth-related log assertions can't leak between tests. Also restores the wildcard-bind regression guard (test_config_contains_no_wildcard_bind) dropped when config.py's security-boundary logic moved out, now scanning all three modules.
  • httpx2 declared as an explicit dev dependency — It was previously an undeclared transitive dependency; its test client is now properly closed after use.
  • bearer_app test fixture no longer mutates global module state — Removed a use of importlib.reload that could leak state between tests.
  • Security-boundary validation extracted from config.py — Moved into a dedicated config_validation.py module, alongside a genuinely dependency-free config_primitives.py for shared leaf helpers. No behavior change; internal organization only.

v4.0.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 18:21

Changelog - v4.0.0

All notable changes to the law-scrapper-mcp project for version v4.0.0 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[v4.0.0] - 2026-08-31

BREAKING CHANGES

  • Default HTTP bind moved from 0.0.0.0 to 127.0.0.1 — A deployment relying on the implicit wildcard bind becomes unreachable until LAW_MCP_HOST is set explicitly.
  • Binding beyond loopback now requires authentication — Setting LAW_MCP_HOST to a non-loopback address requires LAW_MCP_AUTH_MODE set to bearer or oauth. A container configured with 0.0.0.0 and no token refuses to start instead of exposing an unauthenticated MCP endpoint.

Added

  • Static bearer token authenticationLAW_MCP_AUTH_MODE=bearer validates incoming requests against LAW_MCP_AUTH_TOKEN or LAW_MCP_AUTH_TOKEN_FILE; correctly handles bracketed IPv6 hosts in the self-named issuer URL and never echoes the token value in configuration validation errors
  • OAuth resource server verificationLAW_MCP_AUTH_MODE=oauth verifies bearer tokens against an OAuth issuer via LAW_MCP_AUTH_ISSUER, LAW_MCP_AUTH_AUDIENCE, and LAW_MCP_AUTH_RESOURCE_SERVER_URL, tolerating malformed JWKS/discovery documents without crashing
  • Per-client HTTP rate limiting — Bounds request throughput per client on the Streamable HTTP transport. Enabled by default: 60 requests per 60s window, burst of 10; tune via LAW_MCP_RATE_LIMIT_ENABLED, LAW_MCP_RATE_LIMIT_REQUESTS, LAW_MCP_RATE_LIMIT_WINDOW, LAW_MCP_RATE_LIMIT_BURST, and LAW_MCP_TRUSTED_PROXIES for client identification behind a proxy
  • Network and auth settings surfaced in configurationLAW_MCP_HOST and the auth settings above are validated at startup with Polish-language errors for misconfiguration; LAW_MCP_ALLOWED_HOSTS and LAW_MCP_ALLOWED_ORIGINS (recognizing bare IPv6 loopback) are enforced per request by the MCP SDK's transport-security layer, protecting against DNS-rebinding and cross-origin access
  • Authenticated deployment documentation — Deployment docs cover bearer and OAuth setup for non-loopback binds

v3.1.2

Choose a tag to compare

@github-actions github-actions released this 25 Aug 22:12

Changelog - v3.1.2

All notable changes to the law-scrapper-mcp project for version v3.1.2 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[3.1.2] - 2026-08-25

Changed

  • Log output contract — Every log record now carries a request_id field: lifespan for records emitted outside a tool call, an eight-character hexadecimal id for records emitted inside one. JSON logs keep Polish diacritics literal instead of escaping them to \uXXXX, and the JSON timestamp field now ends in an explicit +00:00 instead of being a naive UTC value. Log aggregation pipelines parsing this output absorb one field-shape change, not three.

Fixed

  • Query text no longer reaches INFO through the result store or the tool error pathResultStore.store() logs the result set id and counts at INFO and moves the query summary to DEBUG; the tool error decorator logs validation failures at ERROR without the caller-supplied message, which moves to DEBUG. In the legal domain a query can be sensitive even when every act it matches is public. Tool responses are unchanged.

v3.1.1

Choose a tag to compare

@github-actions github-actions released this 24 Aug 05:03

Changelog - v3.1.1

All notable changes to the law-scrapper-mcp project for version v3.1.1 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[v3.1.1] - 2026-08-24

Added

  • Configurable graceful shutdown windowLAW_MCP_SHUTDOWN_GRACE (default 15s) reaches uvicorn as timeout_graceful_shutdown. The HTTP branch builds its own uvicorn.Server on the SDK's public streamable_http_app(), enabling clean teardown in containerized deployments with stop_grace_period: 30s in Docker Compose.
  • Upstream state in /health — The health endpoint response carries an upstream object with circuit_state and failure_count once the lifespan has started (circuit_state is unknown and failure_count is omitted before startup or after teardown). The endpoint consistently returns HTTP 200 while the process is alive.

Changed

  • Document conversion moved off the event loophtml_to_markdown, pdf_to_text, and index_sections run through asyncio.to_thread, keeping /health and concurrent requests responsive during large act processing.
  • Pre- and post-conversion size limits — Content exceeding LAW_MCP_DOC_STORE_MAX_SIZE_BYTES is refused before conversion with an error citing the source PDF URL, and re-verified post-conversion to guard against uncompressed text expansion. Refusing oversized documents prevents silent mid-clause truncation.
  • get_act_details(load_content=True) error handling — Calls attempting to load oversized acts return is_error=true with the refusal details and source URL instead of reporting successful loading of truncated text. Metadata for oversized acts remains accessible via get_act_details without load_content.

v3.1.0

Choose a tag to compare

@github-actions github-actions released this 22 Aug 10:05

Changelog - v3.1.0

All notable changes to the law-scrapper-mcp project for version v3.1.0 will be documented in this file.

The format is based on Keep a Changelog 1.1.0,
and this project adheres to Semantic Versioning 2.0.0.

[v3.1.0] - 2026-08-22

Added

  • Listing and result set paginationlist_loaded_documents and list_result_sets accept limit/offset and return page_info.
  • Search and browse pagination metadatasearch_legal_acts and browse_acts return page_info; browse_acts accepts offset.
  • Failed metadata category trackingMetadataOutput.failed_categories provides an explicit list of metadata categories that could not be fetched.
  • Context character ceiling visibilitySearchInActOutput.context_chars_requested / context_chars_applied, plus an informational hint when context was trimmed.
  • API retry loop configurationLAW_MCP_API_MAX_ATTEMPTS (default 3) and LAW_MCP_API_RETRY_BUDGET (default 45.0 s) settings for the API client retry loop.

Changed

  • CI Action Runtime — CI and release workflows bump pinned GitHub Actions to the Node 24 runtime (actions/checkout 7.0.1, astral-sh/setup-uv 10.0.1, actions/upload-artifact 7.0.1, actions/download-artifact 8.0.1, softprops/action-gh-release 3.0.2).
  • API Client Layering & ResilienceSejmApiClient splits request handling into three layers (_send / _execute_with_resilience / _request); exception translation occurs outside the retry loop.
  • Circuit Breaker Admission Contract — The circuit breaker gains an explicit try_acquire / release_success / release_failure / release_probe contract; the probe counter increments on admission rather than completion.
  • Specific Exception Classification — HTTP 500 and 504 raise ApiUnavailableError instead of generic SejmApiError, aligning with the circuit breaker classification while remaining backward-compatible via inheritance.
  • Search In Act Pagination & Binary Searchsearch_in_act builds context and resolves sections only for hits on the requested page; section lookup uses binary search.
  • Concurrent Metadata Retrievalget_system_metadata(category="all") fetches categories concurrently within client semaphore limits instead of sequentially.
  • Search In Act Input Schema Documentation — The context_chars description in search_in_act inputSchema clearly states the 2000-character ceiling and trimming behavior.

Fixed

  • Metadata count integrity — A failure when fetching an individual metadata category no longer silently understates total_count.
  • Search count discrepancy handlingSearchOutput.total_count is raised to the actual number of returned records when the Sejm API reports a count lower than its response payload.
  • 5xx error retries — Fixed exception translation so HTTP 5xx errors properly reach the retry policy instead of being intercepted prematurely.
  • Transport error handling — Transport errors other than timeouts (ConnectError, ReadError, RemoteProtocolError) are translated to domain exceptions rather than leaking raw httpx exceptions to the services layer.
  • Circuit breaker failure counting — A failed operation increments the breaker failure count by 1 rather than by the number of retry attempts; retry loops obey the configured time budget and abort if the circuit opens.
  • Half-open state transition — Confirmed failures in a retry sequence are booked even when subsequent attempts are rejected by the circuit breaker, preventing stuck half-open states.
  • Local protocol error exclusionhttpx.LocalProtocolError (malformed client requests) is excluded from retry loops and circuit breaker failure tallies.
  • Retry setting validationLAW_MCP_API_MAX_ATTEMPTS and LAW_MCP_API_RETRY_BUDGET are validated (ge=1 / gt=0) to prevent silent zero-iteration loops.
  • Retry-After header validation — Rejects non-finite, NaN, and negative Retry-After values to prevent scheduling anomalies.
  • Accurate User-Agent reporting — The User-Agent header reports the actual server version and contact address instead of a hardcoded string.

Removed

  • tenacity dependency — Replaced with an explicit, resilient retry loop in client/sejm_client.py.
  • LAW_MCP_API_MAX_RETRIES setting — Removed deprecated setting in favor of LAW_MCP_API_MAX_ATTEMPTS.