netherbymultimodal
Click on "Install 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., "@netherbymultimodalFuse 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.
| Name | Required | Description | Default |
|---|---|---|---|
| pages | Yes |
TDQS
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.
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.
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.
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.
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.
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 tool update
v0.1.0- First observed
fuse_document
TDQS
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.
Maintenance
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
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.
MCP server for detecting and redacting PII (Personally Identifiable Information) in PDF documents.
OCR, transcription, file extraction, and image generation for AI agents via MCP.
Related MCP Servers
- AlicenseAqualityCmaintenanceFast, local PDF parsing as an MCP server with text extraction, bounding boxes, OCR, and visual citations. No cloud or API key required.5MIT
- AlicenseNot gradedqualityBmaintenanceMCP 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
- AlicenseAqualityAmaintenanceModular OCR MCP server supporting Apple Vision, PaddleOCR, and PaddleOCR-VL backends. Enables text, layout, table, formula, and chart extraction from images via natural language.14MIT
- FlicenseNot gradedqualityBmaintenanceEnables 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
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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