Install any skill in seconds. Free to start, no credit card required.
Get Started Free →REST API server and MCP protocol integration
.claude/skills/xberg-io-api-server-mcp/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 79% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 25% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 18% | 0% |
Axum server for document extraction, plus the rmcp Model Context Protocol surface
Locations: crates/xberg/src/api/ (router.rs, handlers.rs, startup.rs, types.rs, error.rs, jobs.rs) and crates/xberg/src/mcp/. There is no api/server.rs.
Registered in api/router.rs, all on one Router:
| Route | Handler | | --- | --- | | POST /extract | extract_handler — multipart files, URL fields, or JSON; builds ExtractInput | | POST /extract-async | extract_async_handler — queues a job | | GET/DELETE /jobs/{job_id} | job_status_handler / cancel_job_handler | | POST /detect | detect_handler | | GET /formats | formats_handler | | GET /health | health_handler | | GET /info, GET /version | info_handler, version_handler | | GET /cache/stats | cache_stats_handler | | DELETE /cache/clear | cache_clear_handler | | GET /cache/manifest, POST /cache/warm | cache_manifest_handler, cache_warm_handler | | PUT /process, POST /v1/convert/file | openweb_external_handler, openweb_docling_handler (api/openweb.rs) | | GET /openapi.json | openapi_schema_handler (feature api) | | GET /metrics | metrics_handler (feature prometheus) |
There is no POST /extract-url (URL ingestion is a field on ExtractInput passed to /extract) and no POST /batch (batch is /extract-async + /jobs/{job_id}).
Middleware, in order: DefaultBodyLimit::max(limits.max_request_body_bytes) + RequestBodyLimitLayer (default 100 MB), CORS, request-id, compression, catch-panic, sensitive-header stripping, tracing. CORS is built explicitly as CorsLayer::new().allow_origin(Any).allow_methods(Any).allow_headers(Any) and warns loudly; restrict it with XBERG_CORS_ORIGINS.
crates/xberg/src/cache/ — GenericCache is a filesystem-backed store with LRU-style eviction, not an in-memory map. Keys are BLAKE3 content hashes (blake3_hash_bytes / blake3_hash_file, cache/utilities.rs). Eviction is bounded by max_age_days, max_cache_size_mb and min_free_space_mb — there is no entry-count limit.
ApiError is a struct, not an enum: { status: StatusCode, body: ErrorResponse } (api/error.rs). Status comes from the constructor, not a variant:
validation() → 400, unprocessable() → 422, internal() → 500, bad_gateway() → 502From<XbergError> picks one via error.api_status_category()(Validation / Unprocessable / Internal)
There is no 404, 413 or 503 path with a named variant. Do not match on ApiError.
crates/xberg/src/mcp/. Transport is a single nested rmcp streamable-HTTP service — Router::new().nest_service("/mcp", http_service) — not a set of /mcp/* REST paths. Tools, resources and prompts are JSON-RPC methods on it. Stdio transport serves the same router over stdin/stdout.
extract, extract_batch, detect_mime_type, list_formats, cache_stats, cache_clear, get_version, cache_manifest, cache_warm. The set is pinned by test_all_tools_are_registered in mcp/server.rs. There is no get_capabilities.
extract, extract_batch and cache_warm are task-eligible (TASK_ELIGIBLE_TOOLS, mcp/server.rs).
mcp/resources.rs: xberg://formats, xberg://models, xberg://languages/ocr, plus xberg://presets/embeddings behind #[cfg(feature = "embeddings")].
mcp/prompts.rs: extract_document, extract_with_ocr, semantic_search.
There is no .env.example. Server-side vars are read in core/server_config/env.rs:
XBERG_HOST, XBERG_PORT (defaults 127.0.0.1:8000)XBERG_MAX_REQUEST_BODY_BYTES, XBERG_MAX_MULTIPART_FIELD_BYTES (both default 100 MB)XBERG_CORS_ORIGINS (comma-separated)Extraction-side vars are documented on ExtractionConfig::apply_env_overrides (core/config/extraction/env.rs) — XBERG_OCR_BACKEND, XBERG_OCR_LANGUAGE, XBERG_CHUNKING_MAX_CHARS, XBERG_CACHE_ENABLED, XBERG_LLM_*, and others. Read that doc comment rather than guessing a name.
limits.max_request_body_bytes, never hardcode.ErrorResponse.XBERG_CORS_ORIGINS./extract-async — do not block a request thread on a multi-minute extraction.mcp/server.rs and extend test_all_tools_are_registered — the test is the contract.xberg://presets/embeddings is — a missing feature must not breakresources/list.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 10,914 | 11,099 | +2% | 1 | 1 | 0% | 2,212 | 3,208 | +45% | 0 | 0 | — |
case-02 | fail→pass | 15,409 | 14,149 | -8% | 1 | 1 | 0% | 2,640 | 4,717 | +79% | 0 | 0 | — |
case-03 | fail→fail | 10,152 | 8,855 | -13% | 1 | 1 | 0% | 2,002 | 2,938 | +47% | 0 | 0 | — |
case-04 | fail→pass | 16,592 | 10,822 | -35% | 1 | 1 | 0% | 2,958 | 3,688 | +25% | 0 | 0 | — |
case-10 | fail→pass | 9,964 | 4,714 | -53% | 1 | 1 | 0% | 1,596 | 2,504 | +57% | 0 | 0 | — |
case-05 | fail→pass | 14,944 | 11,805 | -21% | 1 | 1 | 0% | 2,971 | 3,507 | +18% | 0 | 0 | — |
case-06 | fail→pass | 36,283 | 7,264 | -80% | 1 | 1 | 0% | 2,907 | 2,899 | -0% | 0 | 0 | — |
case-07 | fail→pass | 9,716 | 9,672 | -0% | 1 | 1 | 0% | 1,834 | 3,658 | +99% | 0 | 0 | — |
case-08 | fail→fail | 10,839 | 7,633 | -30% | 1 | 1 | 0% | 2,248 | 3,097 | +38% | 0 | 0 | — |
case-09 | fail→fail | 13,204 | 7,201 | -45% | 1 | 1 | 0% | 2,342 | 3,034 | +30% | 0 | 0 | — |
case-11 | fail→pass | 12,848 | 8,218 | -36% | 1 | 1 | 0% | 2,325 | 3,111 | +34% | 0 | 0 | — |
case-12 | fail→pass | 13,060 | 12,161 | -7% | 1 | 1 | 0% | 2,640 | 3,906 | +48% | 0 | 0 | — |
case-13 | fail→pass | 28,669 | 2,630 | -91% | 1 | 1 | 0% | 4,088 | 2,052 | -50% | 0 | 0 | — |
case-14 | fail→pass | 22,965 | 16,522 | -28% | 1 | 1 | 0% | 3,717 | 5,260 | +42% | 0 | 0 | — |
case-15 | fail→pass | 17,668 | 1,752 | -90% | 1 | 1 | 0% | 2,560 | 1,948 | -24% | 0 | 0 | — |
case-16 | fail→pass | 12,664 | 4,740 | -63% | 1 | 1 | 0% | 1,697 | 2,684 | +58% | 0 | 0 | — |
case-17 | fail→fail | 11,459 | 3,108 | -73% | 1 | 1 | 0% | 2,034 | 2,215 | +9% | 0 | 0 | — |
case-18 | pass→pass | 9,492 | 4,876 | -49% | 1 | 1 | 0% | 1,385 | 2,496 | +80% | 0 | 0 | — |
case-19 | fail→pass | 15,890 | 3,436 | -78% | 1 | 1 | 0% | 2,431 | 2,089 | -14% | 0 | 0 | — |
case-20 | pass→pass | 19,065 | 20,228 | +6% | 1 | 1 | 0% | 3,686 | 4,878 | +32% | 0 | 0 | — |
case-21 | pass→fail | 12,674 | 10,340 | -18% | 1 | 1 | 0% | 1,859 | 3,152 | +70% | 0 | 0 | — |
case-22 | fail→pass | 20,282 | 10,403 | -49% | 1 | 1 | 0% | 2,979 | 3,533 | +19% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +64 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/24/2026 | +68% |
| gemini-3.6-flash | verified | 8/11/2026 | +68% |
Other measured skills in the registry, with their headline benchmark lift.