Skip to main content
Glama
J-X0
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.

ParametersJSON Schema
NameRequiredDescriptionDefault
pagesYes

TDQS

A3.6/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds a meaningful behavioral trait by stating it runs fully offline. However, with no annotations provided, it does not disclose whether the input is modified, what side effects exist, or how failures are handled, so the description only partially carries the behavioral burden.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two short sentences with no filler. The main operation is front-loaded, and the offline note is a valuable addition without bloating the text.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter tool with no output schema, the description covers the core transformation and the offline characteristic, but it omits return format, side-effect expectations, and input assumptions. It is adequate but not fully self-sufficient for an agent invoking it blindly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, and the description does not mention the 'pages' parameter or explain how the input structure maps to the operation. It loosely evokes layout regions and text spans, but that does not compensate for the absent parameter documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Fuse') and names its resources ('layout regions with extracted text spans') and output ('ordered, labelled reading blocks'). Even without sibling tools, an agent can clearly understand what this tool accomplishes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There are no explicit when-to-use or when-not-to-use instructions, and no sibling tools exist for contrast. The use case is implied by the purpose, and 'Runs fully offline' provides some context but not a clear decision rule.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 1 tool updatev0.1.0
    • First observedfuse_document

TDQS

A3.5/5.0
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.

Maintenance

ActivityInactive
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    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
    A
    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.
    14
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables 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.
    1
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/J-X0/harrow-risk-multimodal-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server