Skip to main content
Glama
netherbymultimodal
by J-X0

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:

  1. 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.

  2. Orphans — spans no region claims are grouped by proximity into their own blocks. Extracted text is never silently dropped (an audit requirement).

  3. Intra-block order — spans are grouped into visual lines (by vertical centre) and ordered top-to-bottom, then left-to-right within a line.

  4. 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 inside no_network(), so any library that tries to fetch weights or send telemetry fails loudly. The tensor inference is a marked seam (real.py TODO) pending Harrow Risk's model choice; a missing or unloadable model raises ProviderUnavailable rather 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 test

make 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.server

Methods: 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

NETHERBY_PROVIDER

stub

stub or onnx

NETHERBY_MODEL_PATH

—

required when provider is onnx

NETHERBY_ALLOW_PROVIDER_FALLBACK

true

degrade to stub if the model won't load

NETHERBY_MAX_PAGES

500

reject payloads with more pages

NETHERBY_MAX_SPANS_PER_PAGE

50000

reject payloads with more spans per page

NETHERBY_MAX_TEXT_LEN

100000

reject spans with longer text

NETHERBY_MIN_CONTAINMENT

0.5

span->region assignment threshold

NETHERBY_LOG_LEVEL

info

debug/info/warning/error

Failure handling and degradation

  • Malformed payloads (wrong types, non-finite numbers, inverted boxes, missing fields) return JSON-RPC -32602 with a message pointing at the offending path; they never crash the loop.

  • Payloads over a configured limit return -32602 as a ResourceLimitError.

  • Unparseable input lines return -32700 and the server keeps serving.

  • If the onnx provider 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, unless ALLOW_PROVIDER_FALLBACK is 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. pytest is the only dev dependency; onnxruntime is 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 onnx provider's tensor inference is not wired up (see the TODO in providers/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 tool
fuse_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

    A3.5/5.0

    Scored across 1 tool

    Disambiguation5/5

    With only one tool, there is no possibility of overlapping purposes or misselection. The single tool's purpose is clearly stated.

    Naming Consistency4/5

    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.

    Tool Count2/5

    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.

    Completeness2/5

    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

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      MCP 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
    • A
      license
      A
      quality
      B
      maintenance
      Modular OCR MCP server supporting Apple Vision, PaddleOCR, and PaddleOCR-VL backends. Enables text, layout, table, formula, and chart extraction from images via natural language.
      15
      MIT