@xberg-io/opencode-xberg 1.3.7 → 1.3.8

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. package/.opencode/ai-rulez-bundle.json +51 -0
  2. package/.opencode/ai-rulez-content.js +167 -0
  3. package/.opencode/commands/xberg-extract.md +14 -0
  4. package/.opencode/commands/xberg-ocr.md +14 -0
  5. package/.opencode/plugins/xberg.js +75 -62
  6. package/.opencode/skills/batch-extraction/SKILL.md +168 -0
  7. package/.opencode/skills/chunking/SKILL.md +163 -0
  8. package/.opencode/skills/extracting-keywords/SKILL.md +171 -0
  9. package/.opencode/skills/extracting-tables/SKILL.md +156 -0
  10. package/.opencode/skills/extracting-with-ocr/SKILL.md +130 -0
  11. package/.opencode/skills/picking-a-format/SKILL.md +104 -0
  12. package/.opencode/skills/xberg/SKILL.md +427 -0
  13. package/.opencode/skills/xberg/references/advanced-features.md +628 -0
  14. package/.opencode/skills/xberg/references/cli-reference.md +462 -0
  15. package/.opencode/skills/xberg/references/configuration.md +587 -0
  16. package/.opencode/skills/xberg/references/nodejs-api.md +485 -0
  17. package/.opencode/skills/xberg/references/other-bindings.md +345 -0
  18. package/.opencode/skills/xberg/references/python-api.md +657 -0
  19. package/.opencode/skills/xberg/references/rust-api.md +412 -0
  20. package/.opencode/skills/xberg/references/supported-formats.md +252 -0
  21. package/.pi/prompts/xberg-extract.md +14 -0
  22. package/.pi/prompts/xberg-ocr.md +14 -0
  23. package/.pi/skills/batch-extraction/SKILL.md +168 -0
  24. package/.pi/skills/chunking/SKILL.md +163 -0
  25. package/.pi/skills/extracting-keywords/SKILL.md +171 -0
  26. package/.pi/skills/extracting-tables/SKILL.md +156 -0
  27. package/.pi/skills/extracting-with-ocr/SKILL.md +130 -0
  28. package/.pi/skills/picking-a-format/SKILL.md +104 -0
  29. package/.pi/skills/xberg/SKILL.md +427 -0
  30. package/.pi/skills/xberg/references/advanced-features.md +628 -0
  31. package/.pi/skills/xberg/references/cli-reference.md +462 -0
  32. package/.pi/skills/xberg/references/configuration.md +587 -0
  33. package/.pi/skills/xberg/references/nodejs-api.md +485 -0
  34. package/.pi/skills/xberg/references/other-bindings.md +345 -0
  35. package/.pi/skills/xberg/references/python-api.md +657 -0
  36. package/.pi/skills/xberg/references/rust-api.md +412 -0
  37. package/.pi/skills/xberg/references/supported-formats.md +252 -0
  38. package/README.md +40 -0
  39. package/package.json +69 -16
  40. package/scripts/mcp-launch.sh +188 -0
@@ -0,0 +1,628 @@
1
+ <!--
2
+ AI-RULEZ :: GENERATED FILE — DO NOT EDIT
3
+ Content-Hash: blake3:353a7a05cbe84a2af1d053c8173d2154f418972a2bbfc2846dbb2c17a8a46197
4
+ Source-Hash: blake3:3fca38eb564d0ed9c9194d070fd8dec96de727edc92e82954cb4b9af52cddbaf
5
+ Schema-Version: v1
6
+ -->
7
+
8
+ # Advanced Features Reference
9
+
10
+ Xberg provides advanced features for customization, semantic processing, and integration with external systems. All library examples use the 1.0 unified API: `extract(input, config)` returns an `ExtractionResult` envelope, and per-document data lives in `result.results[i]`.
11
+
12
+ ## Plugin System
13
+
14
+ The plugin system extends Xberg's extraction pipeline with custom post-processors, validators, and OCR backends. Plugin callbacks receive a single `ExtractedDocument` (not the envelope), with `.content`, `.mime_type`/`.mimeType`, and `.metadata`.
15
+
16
+ ### Custom Post-Processors
17
+
18
+ Post-processors enrich extraction results after document parsing. They run non-destructively — if a post-processor fails, the extraction still succeeds (errors are logged).
19
+
20
+ === "Python"
21
+
22
+ ```python
23
+ import asyncio
24
+ from xberg import register_post_processor, ExtractInput, extract, ExtractionConfig, ExtractedDocument
25
+
26
+ class MetadataEnricher:
27
+ def name(self) -> str:
28
+ return "metadata_enricher"
29
+
30
+ def version(self) -> str:
31
+ return "1.1.0"
32
+
33
+ def process(self, result: ExtractedDocument, config: ExtractionConfig) -> ExtractedDocument:
34
+ result.metadata.additional["processed_by"] = "metadata_enricher"
35
+ result.metadata.additional["char_count"] = str(len(result.content))
36
+ return result
37
+
38
+ def processing_stage(self) -> str:
39
+ return "middle" # "early", "middle", or "late"
40
+
41
+ register_post_processor(MetadataEnricher())
42
+
43
+ async def main() -> None:
44
+ result = await extract(ExtractInput(uri="document.pdf"), ExtractionConfig())
45
+ print(len(result.results[0].content))
46
+
47
+ asyncio.run(main())
48
+ ```
49
+
50
+ === "TypeScript"
51
+
52
+ ```typescript
53
+ import { registerPostProcessor, extract } from "@xberg-io/xberg";
54
+
55
+ const enricher = {
56
+ name() {
57
+ return "metadata_enricher";
58
+ },
59
+ async process(result) {
60
+ result.metadata.processed_by = "metadata_enricher";
61
+ result.metadata.char_count = result.content.length;
62
+ return result;
63
+ },
64
+ processingStage() {
65
+ return "middle"; // "early" | "middle" | "late"
66
+ },
67
+ };
68
+
69
+ registerPostProcessor(enricher);
70
+
71
+ const output = await extract({ kind: "uri", uri: "document.pdf" });
72
+ console.log(output.results[0].content.length);
73
+ ```
74
+
75
+ ### Custom Validators
76
+
77
+ Validators perform quality checks. Unlike post-processors, a validator that raises causes the whole extraction to fail.
78
+
79
+ === "Python"
80
+
81
+ ```python
82
+ from xberg import register_validator, ExtractedDocument, ExtractionConfig, ValidationError
83
+
84
+ class MinimumContentValidator:
85
+ def name(self) -> str:
86
+ return "min_content_validator"
87
+
88
+ def version(self) -> str:
89
+ return "1.1.0"
90
+
91
+ def validate(self, result: ExtractedDocument, config: ExtractionConfig) -> None:
92
+ if len(result.content) < 100:
93
+ raise ValidationError("Extracted content too short (< 100 chars)")
94
+
95
+ def priority(self) -> int:
96
+ return 100 # higher runs first
97
+
98
+ def should_validate(self, result: ExtractedDocument, config: ExtractionConfig) -> bool:
99
+ return "pdf" in result.mime_type.lower()
100
+
101
+ register_validator(MinimumContentValidator())
102
+ ```
103
+
104
+ === "TypeScript"
105
+
106
+ ```typescript
107
+ import { registerValidator } from "@xberg-io/xberg";
108
+
109
+ const validator = {
110
+ name() {
111
+ return "min_content_validator";
112
+ },
113
+ validate(result) {
114
+ if (result.content.length < 100) {
115
+ throw new Error("Extracted content too short (< 100 chars)");
116
+ }
117
+ },
118
+ priority() {
119
+ return 100;
120
+ },
121
+ shouldValidate(result) {
122
+ return result.mimeType.toLowerCase().includes("pdf");
123
+ },
124
+ };
125
+
126
+ registerValidator(validator);
127
+ ```
128
+
129
+ ### Custom OCR Backends
130
+
131
+ Register a custom OCR engine to integrate proprietary or specialized OCR.
132
+
133
+ === "Python"
134
+
135
+ ```python
136
+ from xberg import ExtractedDocument, OcrBackendType, OcrConfig, register_ocr_backend
137
+
138
+ class CustomOcrBackend:
139
+ def name(self) -> str:
140
+ return "custom_ocr"
141
+
142
+ def supports_language(self, language: str) -> bool:
143
+ return language in {"eng", "deu", "fra", "spa"}
144
+
145
+ def backend_type(self) -> OcrBackendType:
146
+ return OcrBackendType.CUSTOM
147
+
148
+ # The second argument is the resolved OcrConfig, not a language string.
149
+ def process_image(self, image_bytes: bytes, config: OcrConfig) -> ExtractedDocument:
150
+ language = config.language[0] if config.language else "eng"
151
+ # text = my_ocr_engine.recognize(image_bytes, language)
152
+ return ExtractedDocument(content="Extracted text from image", mime_type="text/plain")
153
+
154
+ register_ocr_backend(CustomOcrBackend())
155
+
156
+ config = OcrConfig(backend="custom_ocr", language=["eng"])
157
+ ```
158
+
159
+ === "TypeScript"
160
+
161
+ ```typescript
162
+ import { registerOcrBackend, extract } from "@xberg-io/xberg";
163
+
164
+ const supportedLangs = ["eng", "deu", "fra", "spa"];
165
+
166
+ const backend = {
167
+ name() {
168
+ return "custom_ocr";
169
+ },
170
+ supportedLanguages() {
171
+ return supportedLangs;
172
+ },
173
+ supportsLanguage(lang) {
174
+ return supportedLangs.includes(lang);
175
+ },
176
+ backendType() {
177
+ return "Custom";
178
+ },
179
+ // The second argument is the resolved OcrConfig, not a language string.
180
+ async processImage(imageBytes, config) {
181
+ const language = config?.language?.[0] ?? "eng";
182
+ // const text = await myOcrEngine.recognize(imageBytes, language);
183
+ return {
184
+ content: "Extracted text from image",
185
+ mime_type: "text/plain",
186
+ tables: [],
187
+ };
188
+ },
189
+ };
190
+
191
+ registerOcrBackend(backend);
192
+
193
+ const config = { ocr: { backend: "custom_ocr", language: ["eng"] } };
194
+ const output = await extract({ kind: "uri", uri: "scanned.pdf" }, config);
195
+ ```
196
+
197
+ ## Per-Input Configuration in Batch Operations
198
+
199
+ Override extraction settings for individual inputs by setting `config` on each `ExtractInput` to a `FileExtractionConfig`. Inputs without an override use the batch-level `ExtractionConfig`. This suits mixed-format batches where different documents need different OCR, output, or processing.
200
+
201
+ === "Python"
202
+
203
+ ```python
204
+ import asyncio
205
+ from xberg import ExtractInput, extract_batch, ExtractionConfig, FileExtractionConfig, OcrConfig
206
+
207
+ async def main() -> None:
208
+ report = ExtractInput(uri="report.pdf")
209
+ scan = ExtractInput(
210
+ kind="uri",
211
+ uri="scan.tiff",
212
+ config=FileExtractionConfig(
213
+ force_ocr=True,
214
+ ocr=OcrConfig(backend="tesseract", language=["deu"]),
215
+ ),
216
+ )
217
+
218
+ output = await extract_batch([report, scan], ExtractionConfig(output_format="markdown"))
219
+ for doc in output.results:
220
+ print(len(doc.content))
221
+
222
+ asyncio.run(main())
223
+ ```
224
+
225
+ === "TypeScript"
226
+
227
+ ```typescript
228
+ import { extractBatch } from "@xberg-io/xberg";
229
+
230
+ const output = await extractBatch(
231
+ [
232
+ { kind: "uri", uri: "report.pdf" },
233
+ {
234
+ kind: "uri",
235
+ uri: "scan.tiff",
236
+ config: { forceOcr: true, ocr: { backend: "tesseract", language: ["deu"] } },
237
+ },
238
+ ],
239
+ { outputFormat: "markdown" },
240
+ );
241
+ ```
242
+
243
+ All `ExtractionConfig` fields except batch-level concerns (`max_concurrent_extractions`, `use_cache`, `acceleration`, `security_limits`) can be overridden per input. Unset (`None`/omitted) fields inherit the batch default.
244
+
245
+ ## Embeddings
246
+
247
+ Generate vector embeddings for text chunks using ONNX models. Embeddings enable semantic search, clustering, and similarity over extracted content. Available presets: `fast` (384 dims), `balanced` (768 dims), `quality` (1024 dims), `multilingual` (768 dims).
248
+
249
+ === "Python"
250
+
251
+ ```python
252
+ from xberg import (
253
+ ExtractInput, extract, ExtractionConfig,
254
+ ChunkingConfig, EmbeddingConfig, EmbeddingModelType,
255
+ )
256
+
257
+ # Preset model (recommended)
258
+ config = ExtractionConfig(
259
+ chunking=ChunkingConfig(
260
+ max_characters=512,
261
+ overlap=100,
262
+ embedding=EmbeddingConfig(
263
+ model=EmbeddingModelType.preset("balanced"),
264
+ normalize=True,
265
+ batch_size=32,
266
+ ),
267
+ )
268
+ )
269
+
270
+ # Custom ONNX model from HuggingFace
271
+ config = ExtractionConfig(
272
+ chunking=ChunkingConfig(
273
+ embedding=EmbeddingConfig(
274
+ model=EmbeddingModelType.custom(
275
+ model_id="sentence-transformers/all-MiniLM-L6-v2",
276
+ dimensions=384,
277
+ ),
278
+ )
279
+ )
280
+ )
281
+
282
+ result = await extract(ExtractInput(uri="document.pdf"), config)
283
+ for chunk in result.results[0].chunks or []:
284
+ print(f"{chunk.content[:50]}... -> {len(chunk.embedding) if chunk.embedding else 0} dims")
285
+ ```
286
+
287
+ === "TypeScript"
288
+
289
+ ```typescript
290
+ import { extract } from "@xberg-io/xberg";
291
+
292
+ const config = {
293
+ chunking: {
294
+ maxCharacters: 512,
295
+ overlap: 100,
296
+ embedding: { model: { type: "preset", name: "balanced" } },
297
+ },
298
+ };
299
+
300
+ const output = await extract({ kind: "uri", uri: "document.pdf" }, config);
301
+ for (const chunk of output.results[0].chunks ?? []) {
302
+ console.log(`Embedding dims: ${chunk.embedding?.length ?? 0}`);
303
+ }
304
+ ```
305
+
306
+ ## Keyword Extraction
307
+
308
+ Extract important keywords using YAKE or RAKE.
309
+
310
+ === "Python"
311
+
312
+ ```python
313
+ from xberg import (
314
+ ExtractInput, extract, ExtractionConfig,
315
+ KeywordConfig, KeywordAlgorithm, YakeParams, RakeParams,
316
+ )
317
+
318
+ # YAKE
319
+ config = ExtractionConfig(
320
+ keywords=KeywordConfig(
321
+ algorithm=KeywordAlgorithm.YAKE,
322
+ max_keywords=15,
323
+ min_score=0.1,
324
+ language="en",
325
+ yake_params=YakeParams(window_size=1),
326
+ )
327
+ )
328
+
329
+ # RAKE
330
+ config = ExtractionConfig(
331
+ keywords=KeywordConfig(
332
+ algorithm=KeywordAlgorithm.RAKE,
333
+ max_keywords=10,
334
+ min_score=5.0,
335
+ language="en",
336
+ rake_params=RakeParams(min_word_length=1, max_words_per_phrase=3),
337
+ )
338
+ )
339
+
340
+ result = await extract(ExtractInput(uri="document.pdf"), config)
341
+ for keyword in result.results[0].extracted_keywords or []:
342
+ print(f"{keyword.text}: {keyword.score} ({keyword.algorithm})")
343
+ ```
344
+
345
+ === "TypeScript"
346
+
347
+ ```typescript
348
+ import { extract } from "@xberg-io/xberg";
349
+
350
+ const config = {
351
+ keywords: {
352
+ algorithm: "yake",
353
+ maxKeywords: 15,
354
+ minScore: 0.1,
355
+ language: "en",
356
+ yakeParams: { windowSize: 1 },
357
+ },
358
+ };
359
+
360
+ const output = await extract({ kind: "uri", uri: "document.pdf" }, config);
361
+ for (const keyword of output.results[0].extractedKeywords ?? []) {
362
+ console.log(`${keyword.text}: ${keyword.score} (${keyword.algorithm})`);
363
+ }
364
+ ```
365
+
366
+ ## Language Detection
367
+
368
+ Detect document language(s) using ISO 639-1 codes.
369
+
370
+ === "Python"
371
+
372
+ ```python
373
+ from xberg import ExtractInput, extract, ExtractionConfig, LanguageDetectionConfig
374
+
375
+ config = ExtractionConfig(
376
+ language_detection=LanguageDetectionConfig(
377
+ enabled=True,
378
+ min_confidence=0.8,
379
+ detect_multiple=False,
380
+ )
381
+ )
382
+
383
+ result = await extract(ExtractInput(uri="multilingual.pdf"), config)
384
+ for lang in result.results[0].detected_languages or []:
385
+ print(f"Detected language: {lang}") # e.g. "en", "de", "fr"
386
+ ```
387
+
388
+ === "TypeScript"
389
+
390
+ ```typescript
391
+ import { extract } from "@xberg-io/xberg";
392
+
393
+ const config = {
394
+ languageDetection: { enabled: true, minConfidence: 0.8, detectMultiple: false },
395
+ };
396
+
397
+ const output = await extract({ kind: "uri", uri: "multilingual.pdf" }, config);
398
+ for (const lang of output.results[0].detectedLanguages ?? []) {
399
+ console.log(`Detected language: ${lang}`);
400
+ }
401
+ ```
402
+
403
+ ## Token Reduction
404
+
405
+ Reduce token count in extracted content to lower LLM costs. Higher modes are more aggressive but may discard more information.
406
+
407
+ === "Python"
408
+
409
+ ```python
410
+ from xberg import ExtractInput, extract, ExtractionConfig, TokenReductionOptions
411
+
412
+ config = ExtractionConfig(
413
+ token_reduction=TokenReductionOptions(
414
+ mode="moderate", # "off", "light", "moderate", "aggressive", "maximum"
415
+ preserve_important_words=True,
416
+ )
417
+ )
418
+
419
+ result = await extract(ExtractInput(uri="document.pdf"), config)
420
+ print(f"Reduced content length: {len(result.results[0].content)}")
421
+ ```
422
+
423
+ === "TypeScript"
424
+
425
+ ```typescript
426
+ import { extract } from "@xberg-io/xberg";
427
+
428
+ const config = { tokenReduction: { mode: "moderate", preserveImportantWords: true } };
429
+ const output = await extract({ kind: "uri", uri: "document.pdf" }, config);
430
+ console.log(`Reduced content length: ${output.results[0].content.length}`);
431
+ ```
432
+
433
+ **Modes:** `off` (default), `light`, `moderate`, `aggressive`, `maximum`.
434
+
435
+ ## Page Extraction
436
+
437
+ Track per-page content separately.
438
+
439
+ === "Python"
440
+
441
+ ```python
442
+ from xberg import ExtractInput, extract, ExtractionConfig, PageConfig
443
+
444
+ config = ExtractionConfig(
445
+ pages=PageConfig(
446
+ extract_pages=True,
447
+ insert_page_markers=True,
448
+ marker_format="\n\n<!-- PAGE {page_num} -->\n\n",
449
+ )
450
+ )
451
+
452
+ result = await extract(ExtractInput(uri="multi_page.pdf"), config)
453
+ for page in result.results[0].pages or []:
454
+ print(f"Page {page.page_number}: {len(page.content)} chars, {len(page.tables)} tables")
455
+ ```
456
+
457
+ === "TypeScript"
458
+
459
+ ```typescript
460
+ import { extract } from "@xberg-io/xberg";
461
+
462
+ const config = {
463
+ pages: {
464
+ extractPages: true,
465
+ insertPageMarkers: true,
466
+ markerFormat: "\n\n<!-- PAGE {page_num} -->\n\n",
467
+ },
468
+ };
469
+
470
+ const output = await extract({ kind: "uri", uri: "multi_page.pdf" }, config);
471
+ for (const page of output.results[0].pages ?? []) {
472
+ console.log(`Page ${page.pageNumber}: ${page.content.length} chars`);
473
+ }
474
+ ```
475
+
476
+ ## Element-Based Output
477
+
478
+ Extract semantic elements (Unstructured-compatible) instead of unified content.
479
+
480
+ === "Python"
481
+
482
+ ```python
483
+ from xberg import ExtractInput, extract, ExtractionConfig
484
+
485
+ config = ExtractionConfig(result_format="element_based")
486
+ result = await extract(ExtractInput(uri="document.pdf"), config)
487
+
488
+ for element in result.results[0].elements or []:
489
+ print(f"Type: {element.element_type}") # title, heading, narrative_text, ...
490
+ print(f"Text: {element.text}")
491
+ ```
492
+
493
+ === "TypeScript"
494
+
495
+ ```typescript
496
+ import { extract } from "@xberg-io/xberg";
497
+
498
+ const config = { resultFormat: "element_based" };
499
+ const output = await extract({ kind: "uri", uri: "document.pdf" }, config);
500
+
501
+ for (const element of output.results[0].elements ?? []) {
502
+ console.log(`Type: ${element.elementType}`);
503
+ console.log(`Text: ${element.text}`);
504
+ }
505
+ ```
506
+
507
+ **Element types:** `title`, `heading`, `narrative_text`, `list_item`, `table`, `image`, `page_break`, `code_block`, `block_quote`, `footer`, `header`.
508
+
509
+ ## Djot Content
510
+
511
+ Output extracted content in Djot markup.
512
+
513
+ === "Python"
514
+
515
+ ```python
516
+ from xberg import ExtractInput, extract, ExtractionConfig
517
+
518
+ config = ExtractionConfig(output_format="djot")
519
+ result = await extract(ExtractInput(uri="document.pdf"), config)
520
+ print(result.results[0].content) # Djot-formatted content
521
+ ```
522
+
523
+ === "TypeScript"
524
+
525
+ ```typescript
526
+ import { extract } from "@xberg-io/xberg";
527
+
528
+ const config = { outputFormat: "djot" };
529
+ const output = await extract({ kind: "uri", uri: "document.pdf" }, config);
530
+ console.log(output.results[0].content);
531
+ ```
532
+
533
+ ## Security Limits
534
+
535
+ Cap resource consumption for untrusted input (especially archives).
536
+
537
+ === "Python"
538
+
539
+ ```python
540
+ from xberg import ExtractInput, extract, ExtractionConfig, SecurityLimits
541
+
542
+ config = ExtractionConfig(
543
+ security_limits=SecurityLimits(
544
+ max_archive_size=524_288_000, # 500 MB uncompressed
545
+ max_compression_ratio=100, # flag potential zip bombs
546
+ max_files_in_archive=10_000,
547
+ max_nesting_depth=1024,
548
+ max_content_size=104_857_600, # 100 MB
549
+ max_table_cells=100_000,
550
+ )
551
+ )
552
+
553
+ result = await extract(ExtractInput(uri="archive.zip"), config)
554
+ ```
555
+
556
+ === "TypeScript"
557
+
558
+ ```typescript
559
+ import { extract } from "@xberg-io/xberg";
560
+
561
+ const config = {
562
+ securityLimits: {
563
+ maxArchiveSize: 524_288_000,
564
+ maxCompressionRatio: 100,
565
+ maxFilesInArchive: 10_000,
566
+ maxNestingDepth: 1024,
567
+ maxContentSize: 104_857_600,
568
+ maxTableCells: 100_000,
569
+ },
570
+ };
571
+
572
+ const output = await extract({ kind: "uri", uri: "archive.zip" }, config);
573
+ ```
574
+
575
+ ## Caching
576
+
577
+ Extraction results are cached by default (content-hash keyed) to speed up repeated extractions of identical documents.
578
+
579
+ === "Python"
580
+
581
+ ```python
582
+ from xberg import ExtractInput, extract, ExtractionConfig
583
+
584
+ # Caching on (default)
585
+ result = await extract(ExtractInput(uri="document.pdf"), ExtractionConfig(use_cache=True))
586
+
587
+ # Disable caching
588
+ result = await extract(ExtractInput(uri="document.pdf"), ExtractionConfig(use_cache=False))
589
+ ```
590
+
591
+ === "TypeScript"
592
+
593
+ ```typescript
594
+ import { extract } from "@xberg-io/xberg";
595
+
596
+ const output = await extract({ kind: "uri", uri: "document.pdf" }, { useCache: false });
597
+ ```
598
+
599
+ **CLI cache management:**
600
+
601
+ xberg cache stats
602
+ xberg cache clear
603
+
604
+ ## API Server
605
+
606
+ Run Xberg as an HTTP API server.
607
+
608
+ # Default port 8000
609
+ xberg serve
610
+
611
+ # Custom host and port
612
+ xberg serve --host 0.0.0.0 --port 9000
613
+
614
+ **Example:**
615
+
616
+ curl -X POST -F "files=@document.pdf" http://localhost:8000/extract
617
+
618
+ ## MCP Server
619
+
620
+ Run Xberg as a Model Context Protocol server for AI integrations.
621
+
622
+ # stdio transport
623
+ xberg mcp
624
+
625
+ # HTTP transport
626
+ xberg mcp --transport http --host 127.0.0.1 --port 8001
627
+
628
+ The MCP server exposes extraction tools (extract, extract_batch, detect_mime_type, cache management, and more) to AI models.