netherbymultimodal
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@netherbymultimodal Fuse this document into ordered, labelled reading blocks."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
netherbymultimodal (Project Netherby)
Air-gapped multimodal document processing for Harrow Risk. It fuses document layout parsing with text extraction: given a page's extracted text spans and the layout regions detected on that page, it produces ordered, labelled reading blocks and reconstructed text.
The defining constraint is that it runs fully air-gapped. Nothing reaches the
network at runtime. That is enforced in code (netherbymultimodal/airgap.py) and
proven by the test suite, not just asserted in prose.
Layout
netherbymultimodal/
__init__.py public API: fuse_page, fuse_document, domain types
types.py BBox, TextSpan, LayoutRegion, Page, FusedBlock, FusedPage
airgap.py no_network(): blocks sockets/DNS for a code section
fusion.py the core algorithm (assignment, ordering, reconstruction)
server.py MCP server over stdio (JSON-RPC 2.0)
service.py validate payload -> fuse -> plain data; timing + logging
config.py Config with env overrides and validation
logging.py structured JSON logging to stderr
errors.py error taxonomy (InvalidInputError, ResourceLimitError)
providers/
base.py LayoutProvider interface + ProviderUnavailable
stub.py deterministic, dependency-free provider (tests + offline)
real.py local-ONNX provider (loads under the air-gap guard)
tests/Related MCP server: Xberg MCP Server
The algorithm
fuse_page(page, regions) in fusion.py:
Assignment — each text span is attached to the region that contains the largest fraction of the span's area, above a threshold (
min_containment). Containment, not IoU: a small span inside a large region should score 1.0.Orphans — spans no region claims are grouped by proximity into their own blocks. Extracted text is never silently dropped (an audit requirement).
Intra-block order — spans are grouped into visual lines (by vertical centre) and ordered top-to-bottom, then left-to-right within a line.
Inter-block order — blocks are grouped into columns by left-edge clustering and read column-by-column, so two-column pages read correctly.
fuse_document(pages, provider) sources regions from a LayoutProvider and runs
the whole thing inside no_network().
Providers
All model behaviour goes through LayoutProvider so the core never depends on a
model and the suite runs offline with no API key.
StubLayoutProvider— derives regions from span geometry. Deterministic, no dependencies. Default for offline runs and the basis of the tests.OnnxLayoutProvider— loads a local ONNX detector. The model must already be on disk; there is no download path. Loading happens insideno_network(), so any library that tries to fetch weights or send telemetry fails loudly. The tensor inference is a marked seam (real.pyTODO) pending Harrow Risk's model choice; a missing or unloadable model raisesProviderUnavailablerather than returning an empty layout.
Threat model (air-gap)
no_network() patches the standard socket API (socket, create_connection,
getaddrinfo, gethostbyname[_ex]). It stops accidental egress from our code
and from libraries that use standard sockets. It is defence in depth, not a
sandbox: it does not contain code that bypasses socket or calls the OS
directly. Production deployment still isolates the process at the network layer.
Quick start
make venv
make install
make testmake uses PY ?= .venv/bin/python; override it, e.g. make test PY=python3.12.
MCP server
The entry point is an MCP server speaking JSON-RPC 2.0 over stdio, one JSON object per line. Logs go to stderr so they never corrupt the protocol stream on stdout. No socket is opened.
make run
# or: python -m netherbymultimodal.serverMethods: initialize, tools/list, and tools/call for the one tool,
fuse_document. Example exchange (request in, response out):
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"fuse_document","arguments":{"pages":[{"width":600,"height":800,"spans":[{"text":"Certificate of Insurance","bbox":[50,20,300,40]}]}]}}}The result's content[0].text is a JSON string with provider, elapsed_ms,
and per-page blocks.
Configuration
Read from the environment (NETHERBY_ prefix); all values are validated at
startup and a bad value exits non-zero with a message.
Variable | Default | Meaning |
|
|
|
| — | required when provider is |
|
| degrade to stub if the model won't load |
|
| reject payloads with more pages |
|
| reject payloads with more spans per page |
|
| reject spans with longer text |
|
| span->region assignment threshold |
|
|
|
Failure handling and degradation
Malformed payloads (wrong types, non-finite numbers, inverted boxes, missing fields) return JSON-RPC
-32602with a message pointing at the offending path; they never crash the loop.Payloads over a configured limit return
-32602as aResourceLimitError.Unparseable input lines return
-32700and the server keeps serving.If the
onnxprovider cannot load or run (missing/unstaged model — the expected air-gapped case), the service logs a degradation record and falls back to the deterministic stub, unlessALLOW_PROVIDER_FALLBACKis false.
Using it as a library
from netherbymultimodal import fuse_document
from netherbymultimodal.providers.stub import StubLayoutProvider
from netherbymultimodal.types import BBox, Page, TextSpan
page = Page(number=1, width=600, height=800, spans=[
TextSpan("Certificate of Insurance", BBox(50, 20, 300, 40)),
TextSpan("This policy covers the named insured.", BBox(50, 100, 320, 115)),
])
fused = fuse_document([page], StubLayoutProvider())
for block in fused[0].blocks:
print(block.reading_order, block.label, repr(block.text))Development notes
No third-party runtime dependencies.
pytestis the only dev dependency;onnxruntimeis an optional extra (pip install -e '.[real]') for the real provider.Known limit: full-width elements (banners, page-spanning tables) are bucketed into the leftmost column during column detection. See the TODO in
fusion.py(_detect_columns).The
onnxprovider's tensor inference is not wired up (see the TODO inproviders/real.py); loading and air-gap enforcement are done, inference awaits a model choice. Use the stub provider until then.
Design decisions
Architecture decision records for the contested calls live in
docs/adr/: in-process air-gap enforcement, containment
vs. IoU for assignment, hand-rolled JSON-RPC, and provider degradation.
Harrow Risk is an illustrative client; this repository is a self-directed reference implementation built to work end to end.
Available Tools
1 toolfuse_documentA
Fuse layout regions with extracted text spans into ordered, labelled reading blocks. Runs fully offline.
Inputs: pages (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 1 tool
With only one tool, there is no possibility of overlapping purposes or misselection. The single tool's purpose is clearly stated.
The single tool name 'fuse_document' follows a clear verb_noun snake_case convention and is readable. However, one tool alone does not provide enough surface to fully verify a naming pattern.
For a server named 'multimodal', a single tool feels too few for the implied scope. One focused fusion step may be useful, but the server's naming suggests a broader tool surface is expected.
The server only offers a single fusion operation and lacks supporting tools for extraction, layout analysis, document management, or other multimodal workflows. This creates significant workflow gaps and likely dead ends for an agent.
Related MCP Connectors
Document-to-Markdown MCP server — convert PDF, Office and HTML into LLM-ready Markdown.
Hosted MCP server: convert PDFs to clean, LLM-ready Markdown with tables, formulas and OCR.
Document conversion MCP server: PDF to Markdown, image OCR, spreadsheet parsing.
MCP server for detecting and redacting PII (Personally Identifiable Information) in PDF documents.
Related MCP Servers
- AlicenseAqualityDmaintenanceFast, local PDF parsing as an MCP server with text extraction, bounding boxes, OCR, and visual citations. No cloud or API key required.5MIT
- AlicenseNot gradedqualityDmaintenanceMCP server that extracts clean text, tables, and structured data from documents, images, code, and audio files, supporting 97 formats with OCR, transcription, and code intelligence.MIT
- AlicenseAqualityBmaintenanceModular OCR MCP server supporting Apple Vision, PaddleOCR, and PaddleOCR-VL backends. Enables text, layout, table, formula, and chart extraction from images via natural language.15MIT
- FlicenseBqualityDmaintenanceEnables structured document understanding of local PDFs via a dual-extract MCP server, combining MinerU text/layout and Qwen3-VL vision with fusion adjudication for field extraction, tables, formulas, and validation. Fully local and offline.81-