Two complementary changes to mitigate intermittent TLS handshake
timeouts and "notifications/cancelled: Bad Request" seen against the
single-worker uvicorn deployment.
1. bedesten rate-limiter back-pressure
- Add optional ``max_wait`` to ``_TokenBucket.acquire``: if the next
wait would exceed it, raise ``BedestenRateLimited`` immediately
instead of sleeping. After a server-side 429 the bucket pauses for
up to 30s; previously a queued request sat in ``asyncio.sleep``
for that whole window, holding the worker slot and pushing the
MCP client past its cancellation timeout.
- ``search_bedesten_unified`` / ``get_bedesten_document_markdown``
catch ``BedestenRateLimited`` and reuse the existing structured
429-style response, so callers get a fast, clean retry signal.
- Tunable via ``BEDESTEN_RATE_MAX_WAIT_S`` (default 8.0s).
2. Offload sync markitdown conversions to a thread
- Every ``markitdown.convert*`` call site is now wrapped in
``asyncio.to_thread(...)`` across 14 modules (bedesten, yargitay,
danistay, anayasa norm + bireysel, uyusmazlik, emsal, rekabet,
gib, kvkk, sayistay, bddk, sigorta_tahkim, kik_v2). PDF / large
HTML parsing was stalling the event loop for seconds, which on a
single-worker deployment delayed every other in-flight request
and queued new TLS handshakes until they timed out.
Verified locally:
- ``ast.parse`` + ``importlib.import_module`` on all 15 modified files
- ``mcp_server_main.create_app()`` constructs successfully
- New ``_TokenBucket.acquire(max_wait=...)`` smoke-tested across 6
paths: capacity-available, no-arg backward compat, max_wait raise,
max_wait wait+succeed, ``penalize_until`` + max_wait fast-raise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fetch tool's metadata lookup constructed BedestenSearchData without
the required itemTypeList field, causing a Pydantic validation error and
losing the chance to enrich the response with a proper title.
Probed the live API (2026-05-08): the per-IP limit is 10 requests in a
rolling 30s window, with HTTP 429 + Retry-After: 30 on the 11th call.
Add a token bucket inside BedestenApiClient (default capacity=1, refill
1 token / 3.5s — strict serialization, no burst) so we stay below the
threshold by default. When the server still returns 429 (e.g. the egress
IP is shared with other clients), pause the whole bucket for the
Retry-After window so queued in-flight requests wait gracefully instead
of hammering. Tunable via BEDESTEN_RATE_CAPACITY / BEDESTEN_RATE_REFILL_S.
Verified: 14 concurrent requests after a clean cooldown -> 13 OK,
1 stray 429 (bucket auto-paused 22.5s, then drained cleanly).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the Bedesten API rate-limits us, point the model at the local
yargi-cli tool (https://github.com/saidsurucu/yargi-cli) so the user
has a working alternative while waiting out the limit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bedesten API can intermittently return HTTP 429 Too Many Requests.
Previously the tool raised, leaving the LLM with an unhandled error.
Now search_bedesten_unified returns a dict with error="rate_limit_exceeded"
and get_bedesten_document_markdown returns a BedestenDocumentMarkdown
whose markdown_content describes the rate limit, so the model can
inform the user and retry. Non-429 errors still propagate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a prominent banner at the top of the README and inline notices
near the connection instructions stating the server has moved to
https://yargimcp.surucu.dev/mcp. The old https://yargimcp.fastmcp.app/mcp
endpoint is now a migration stub that returns only a notice tool.
Update Claude Desktop and Google Antigravity config URLs to the new host.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Dokploy FastMCP build pipeline runs `fastmcp inspect <module>:app`
and expects `app` to be a FastMCP instance, not a Starlette ASGI app.
Drop the `mcp.http_app()` wrapper and bind the FastMCP instance to
`app` directly so `fastmcp inspect` and `fastmcp run --transport http`
both work. Verified locally with `fastmcp inspect` and end-to-end MCP
initialize over `fastmcp run`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
migration_app.py is a minimal FastMCP server with a single
migration_notice tool. Intended for the deprecated endpoint so
existing MCP clients learn the server has moved to
https://yargimcp.surucu.dev/mcp and instruct the user to update
their client configuration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the mevzuat-mcp pattern: a thin app.py exposing
mcp.http_app() with a /health route, no FastAPI/CORS/OAuth wrapper.
Dockerfile builds on python:3.12-slim and runs uvicorn directly.
Removes Dockerfile/fly.toml entries from .dockerignore so the new
Dockerfile is included in the build context.
Existing asgi_app.py (api.yargimcp.com production) is untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Different embedding model families need different prompt prefixes —
Gemini wants "task: ... | query: ..." and "title: ... | text: ...",
e5 wants "query: ..." / "passage: ...", and using the wrong one
silently degrades retrieval quality. Add EMBEDDING_PROMPT_STYLE
(gemini/e5/raw) so the prefix matches the chosen model.
Defaults: gemini for OpenRouter (matches the existing default
google/gemini-embedding-001), e5 for the local provider (matches
the recommended multilingual-e5-large setup). Both override via
env var or constructor.
Update README and .env.example to recommend intfloat/multilingual-
e5-large served by HuggingFace Text Embeddings Inference (one
docker run) as the Turkish-optimized local setup, with a clear env
var reference table. Ollama and OpenRouter remain documented as
alternatives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a LocalEmbedder that targets any OpenAI-compatible embedding
endpoint (Ollama, llama.cpp, vLLM, LM Studio, ...). Zero new
Python dependencies — reuses the existing openai SDK with a
custom base_url. Defaults to Ollama at http://localhost:11434/v1
with nomic-embed-text @ 768 dims; override via env vars for other
servers/models (e.g. bge-m3 @ 1024 dims for better Turkish).
Refactors the shared encode/similarity logic into a private base
class so OpenRouterEmbedder and LocalEmbedder don't duplicate ~50
lines. OpenRouter keeps its ranking headers; local sends none.
Adds get_embedder() factory selecting the provider based on
EMBEDDING_PROVIDER (local) or OPENROUTER_API_KEY presence, and
is_semantic_search_available() that returns True for either path.
mcp_server_main now uses these so the semantic_search tool is
exposed when only a local server is configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
google/gemini-embedding-001 became paid on OpenRouter, leaving
users without credit unable to run the semantic_search tool. The
old code hardcoded the model and 3072 dimensions in three places.
Make OpenRouterEmbedder accept model/dimension via constructor
args or OPENROUTER_EMBEDDING_MODEL / OPENROUTER_EMBEDDING_DIMENSION
env vars, with the previous values as backward-compatible defaults.
Switch the VectorStore and the response payload in mcp_server_main
to read embedder.dimension instead of the hardcoded 3072 so a
configured non-Gemini model does not produce shape mismatches.
Bad dimension input (non-int or non-positive) now raises a clear
ValueError instead of a downstream shape error.
Documented the new env vars in .env.example.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verified 2026-05-03 against a real Chrome browser: POSTs to
/KararlarGenelKurul/DataTablesList consistently return HTTP 418
with the WAF block page "Bilgi Güvenliği Politikaları Gereği
Kısıtlanmıştır", regardless of headers, cookies, CSRF token, or
form payload. The block is server-side at sayistay.gov.tr and
cannot be worked around client-side. The Temyiz Kurulu and Daire
endpoints are unaffected (29k/22k records still return normally).
Detect the 418 + WAF marker in all three search methods and raise
a clear RuntimeError explaining it is an upstream restriction,
instead of the cryptic "Client error '418 I'm a teapot'" that
hides the real situation from MCP clients.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Token-optimization commit e34d81b tightened RekabetDecisionSummary
fields from Optional[str]/Optional[HttpUrl] to plain str with ""
defaults, but client.py kept passing None for unparsed cells and
HttpUrl(...) for URLs. Pydantic v2 rejected both, the broad
except Exception swallowed every row, and decisions came back []
while total_records_found stayed populated.
Default unparsed string fields to "" and pass URL strings directly
to the model. Verified against the live API for empty args,
PdfText filter, and KararTuru filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces two tools backed by the gib.gov.tr public JSON API
(reverse-engineered from the Next.js SPA chunks):
- search_gib_ozelge: keyword, ozelgeNo, kanunNo, date-range, paging
over 18k+ Revenue Administration tax rulings. Simple YYYY-MM-DD
dates are auto-expanded to ISO 8601 to satisfy the backend.
- get_gib_ozelge_document_markdown: fetch a single ruling by numeric
id and return 5000-char paginated Markdown with a metadata header
block (title, ozelgeNo, tarih, kanun, kaynak).
Also prunes stale auth/Fly.io-era entries from uv.lock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Insurance Arbitration Commission integration with Tavily search
and direct PDF download for 64 quarterly journal issues (2010-2025).
Tools:
- search_sigorta_tahkim_decisions: Search via Tavily API
- get_sigorta_tahkim_document_markdown: PDF download + paginated markdown
- search_within_sigorta_tahkim_issue: Keyword search within individual
decisions of a journal issue, with Turkish İ/I case folding support
Total tools: 25 (was 22)
Monkey-patch JSONRPCNotification to use extra="forbid" so that
requests with "id": null are no longer misclassified as notifications
(202 Accepted). They now correctly fail validation and return a
-32600 Invalid Request error.
Changed source_url from API endpoint (bedesten.adalet.gov.tr/document/{id})
to user-facing URL (mevzuat.adalet.gov.tr/ictihat/{id}) for direct browser access.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace EmbeddingGemma local model with OpenRouter API integration
- Use google/gemini-embedding-001 model via OpenRouter (3072 dimensions)
- Add conditional tool registration: auto-disable if OPENROUTER_API_KEY not set
- Add openai and numpy dependencies to pyproject.toml
- Update .env.example with OPENROUTER_API_KEY configuration
- Fix ruff lint issues in semantic_search module
Reverse engineered the AES-256-CBC encryption used by KİK's Angular web
application to generate document URL hashes from numeric IDs.
Key findings:
- Algorithm: AES-256-CBC with PKCS7 padding
- Key location: ekapv2.kik.gov.tr module 21554 (environment config)
- Output format: IV (16 bytes hex) + Ciphertext (16 bytes hex) = 64 chars
Changes:
- Added encrypt_document_id() static method to KikV2ApiClient
- Updated get_document_markdown() to auto-encrypt numeric gundemMaddesiId
- Added cryptography>=44.0.0 dependency for AES encryption
- Both primary and fallback URL paths now support encryption
This enables direct document retrieval from numeric search result IDs
without requiring the pre-encrypted hash from the web interface.
ImportError only catches import failures. Browser launch errors
(executable not found) are runtime exceptions. Changed to catch
all Exception types to properly fallback to curl.
- Add global _health_check_client variable for singleton pattern
- Define get_or_create_health_check_client() function for health checks
- Add cleanup for health check client in perform_cleanup()
Fixes Bedesten health check error: "name 'get_or_create_health_check_client' is not defined"