oxml-mcp

io.github.sebastienrousseauv0.0.9更新于 Oct 4, 2026

XML tools for agents: XPath queries, inspection, and schema validation in Rust.

已验证STDIO仅桌面Developer ToolsData & Analytics

概览

AI 生成的概览

让助手以字符串形式查询、检查、校验 XML 文档并做 XSD 模式验证,无需把文档放进上下文窗口。

功能
提供四个只读工具:xml_query 执行 XPath 1.0 表达式并返回匹配值,xml_inspect 汇总根元素、最大深度、元素名称及命名空间,xml_check 报告文档是否格式良好,xml_validate 按 XSD 模式校验。文档以字符串传入而非文件路径,结果同时以结构化内容返回。服务器在调用之间不保存状态。
适用场景
适合 XML 文档过大、无法粘贴进模型上下文,或需要精确答案(如统计元素数量、按 XPath 提取节点)的场景。写查询前可先用 xml_inspect 了解元素名称。文档本身能放进上下文、或需要模型编写 XML 时则不必使用。
运行要求
本地进程。可用 cargo install oxml-mcp 安装,或用 Docker 运行镜像 ghcr.io/sebastienrousseau/oxml-mcp:0.0.9。默认使用 stdio 传输,可通过命令行参数启用 streamable HTTP 或 SSE 传输。未声明需要账号、API 密钥或环境变量。
安装前请注意
HTTP 传输默认绑定回环地址且不做身份验证,绑定可路由地址前应置于可信网关之后。工具为只读,不会打开文件或套接字,也不解析外部实体。文档会被完整解析,超大输入会占用内存。

安装

在 SourceWeft 中

  1. 打开 控制台中的 oxml-mcp,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

oxml-mcp

A Model Context Protocol server that lets a model query XML instead of reading it — powered by oxml, with zero unsafe code.

[Build] [Crates.io] [Docs.rs] [OpenSSF Scorecard] [OpenSSF Best Practices] [Glama MCP server score]

[oxml-mcp Demo]


Contents

Getting started

The oxml ecosystem

Reference

  • Tools — xml_query, xml_validate, xml_check, xml_inspect
  • Protocol — MCP 2025-11-25 and 2026-07-28, and the two kinds of failure
  • Errors — the two kinds, and which is which
  • Design — why four tools, and why documents are strings
  • Capabilities in 0.0.9 — release inventory
  • Ecosystem comparison — how this compares to the alternatives
  • Benchmarks — latency per request, measured in pairs

Practical


Why a model wants this

A 40 MB XML file does not fit in a context window, and pasting a fraction of it produces confident answers about the fraction.

count(//record) fits in twelve characters and returns a number. That is the whole argument: give the model a query interface and the document stays on disk.

The secondary argument is arithmetic. A model asked to count elements in a document it can see will approximate. xml_query with count(//record) will not.

Install

bash
cargo install oxml-mcp

Without a Rust toolchain, the same binary ships as an image on GHCR (the MCP registry entry points at it):

bash
docker run --rm -i ghcr.io/sebastienrousseau/oxml-mcp:0.0.9

Transports

One binary, three ways to reach it. Every server in the suite takes the same flags.

CommandTransportEndpointProtocol revisions
oxml-mcpstdiostdin and stdout2024-11-05 to 2026-07-28
oxml-mcp --transport streamable-http --host 127.0.0.1 --port 8000Streamable HTTPhttp://127.0.0.1:8000/mcp2025-11-25 and 2026-07-28
oxml-mcp --transport sse --port 8001HTTP+SSE (legacy)http://127.0.0.1:8001/sse, /messages/2024-11-05

Streamable HTTP serves both current revisions on the one endpoint: a client that sends initialize gets a session and an Mcp-Session-Id; a client that names 2026-07-28 in each request's _meta is served statelessly, with server/discover in place of the handshake. Responses stream as server-sent events, and GET /mcp opens the server-to-client stream. The SSE transport is the older one, for hosts that still expect an endpoint event and a message URL.

The HTTP transports bind the loopback interface unless --host says otherwise, and they do not authenticate. Put the server behind a gateway you trust before binding a routable address. --version and --help do what they say.

Quick Start

The server speaks JSON-RPC 2.0 over stdio, so three lines are enough to see it work — the handshake, then a call — and no client is required:

bash
printf '%s\n' \  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"me","version":"0"}}}' \  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"xml_query","arguments":{"xml":"<library><book><title>Dune</title></book></library>","xpath":"//title"}}}' \  | oxml-mcp | tail -n 1
json
{"jsonrpc":"2.0","id":2,"result":{"resultType":"complete","content":[{"type":"text","text":"Dune"}],"structuredContent":{"count":1,"values":["Dune"]},"isError":false}}

For day-to-day use you want a client to do that for you — see Configure.

Configure

Add it to your MCP client's server list. For Claude Desktop, in claude_desktop_config.json:

json
{  "mcpServers": {    "oxml": {      "command": "oxml-mcp"    }  }}

For Claude Code:

bash
claude mcp add oxml -- oxml-mcp

Without arguments the server speaks JSON-RPC 2.0 over stdin and stdout, one message per line, which is what these clients expect. A client that connects over HTTP instead points at a server started with --transport streamable-http — see Transports. There is no configuration file.

The oxml ecosystem

CrateWhat it is
oxmlThe library: parser, tree, XPath 1.0
xmlschemaXSD validation
oxml-cliThe command line
oxml-wasmWebAssembly bindings
oxml-mcpThis crate — MCP server
oxml-lspLanguage Server Protocol server

All six ship one version number, in steps of 0.0.1.

Tools

xml_query

Evaluate an XPath 1.0 expression and return the matching values.

ArgumentType
xmlstringThe document
xpathstringAn XPath 1.0 expression
namespacesobjectOptional. Prefix-to-URI bindings for the expression
json
{"name":"xml_query","arguments":{"xml":"<r><t>Dune</t><t>Germinal</t></r>","xpath":"//t"}}
DuneGerminal

One value per line. Expressions returning a number, string or boolean return that value directly, so count(//t) gives 2. Every tool also returns its answer as structuredContent, against the outputSchema it advertises — here {"count": 2, "values": ["Dune", "Germinal"]}.

xml_inspect

Summarise a document's shape: root element, maximum depth, every element name with its count, and the namespaces it uses.

json
{"name":"xml_inspect","arguments":{"xml":"<r><t>x</t></r>"}}
Root element: rMaximum depth: 3Elements:  r: 1  t: 1

Call this first. A model that knows the element names can write a query that works; one that guesses writes //item against a document whose elements are called record.

xml_check

Report whether a document is well-formed, with a line and column if it is not.

json
{"name":"xml_check","arguments":{"xml":"<a/>"}}
The document is well-formed (2 nodes).

xml_validate

Validate against an XML Schema, returning every violation with the path to the element it concerns.

ArgumentType
xmlstringThe document
xsdstringThe schema

Protocol

MCP over JSON-RPC 2.0, implemented by rmcp, the official Rust SDK. Two revisions are current and both are served: 2025-11-25, with an initialize handshake, and 2026-07-28, which has no handshake — each request names its revision in _meta and server/discover describes the server. Older revisions back to 2024-11-05 are accepted from a client that asks for them.

Method
initializeCapabilities, server info, the negotiated revision
server/discoverThe same, for the stateless revision
tools/listThe four tools: input schema, output schema, annotations
tools/callInvoke one
pingAnswered

Every tool is annotated read-only, idempotent and closed-world, so a client can call it without asking.

The two kinds of failure are kept apart, because MCP distinguishes them and a model only ever sees one of them.

A tool that ran and could not do the job — a malformed document, an invalid expression — is a successful JSON-RPC response carrying isError: true. The model sees the text and can correct itself.

A request the protocol rejects — malformed JSON, an unknown method, a request with an id but no method — is a JSON-RPC error with the standard code, or an HTTP status over HTTP. Nothing ran, and the client handles it rather than the model.

SituationReply
Malformed documentresult, isError: true
Invalid XPath expressionresult, isError: true
Schema violationresult, isError: true, with the violations as structuredContent
Missing or mistyped argumentresult, isError: true, naming the field
Unknown toolresult, isError: true, naming the four that exist
Unknown methoderror, -32601
id with no methoderror
Malformed JSONHTTP 415 over HTTP; skipped over stdio, the session continues

Errors

A malformed document is not a crash and not a protocol error:

json
{"name":"xml_check","arguments":{"xml":"<a>"}}
{"content":[{"text":"…not well-formed…","type":"text"}],"isError":true}

The model reads that text and can fix the document or tell the user.

Design

Four tools, not fourteen. Every tool description is in the model's context on every request. A server with twenty narrow tools spends more context describing itself than a document would.

Documents are passed as strings, not paths. The server never opens a file. The client decides what the model may read, which is where that decision belongs — a server that took paths would be a way to read any file on the machine.

One protocol implementation, not ours. The JSON-RPC and MCP layers are the official SDK's. Three protocol revisions and three transports are a protocol project, and keeping a hand-written one honest against them is not where the value of an XML server lies. What is this crate's own is the four functions and the text a model reads.

Capabilities in 0.0.9

  • Four tools: query, inspect, check, validate
  • XPath 1.0: ten axes, 25 functions, all four value types
  • XSD validation
  • JSON-RPC 2.0 over stdio, MCP 2024-11-05
  • Escaped surrogate pairs in JSON input, so a document containing an emoji works from a Python client
  • No filesystem access, no network access

Not yet: resources, prompts, streaming, documents by path or URI.

Ecosystem comparison

The alternatives are ways of getting XML in front of a model rather than competing servers:

ApproachDocument sizePrecisionReaches the network
oxml-mcpbounded by the tool call, not the context windowan XPath expression with an exact answernever
Paste into the contextmust fit, and costs tokens every turnthe model pattern-matches by eyeno
A filesystem or shell serverunboundedwhatever grep gives youdepends on the server
A generic HTTP fetch serverunboundednoneyes, by design

The last row is the one worth pausing on. A server that fetches is a server that can be pointed at your internal network by a document it was asked to read. The tools here have no code that opens a socket; the only listener is the one you start with --transport, and it only ever answers.

Benchmarks

bash
cargo bench --bench protocol

Latency per request, since an MCP client sends one and waits. The JSON-RPC layer adds roughly 10–25% over the bare parse on a 200 KB payload and is a few microseconds on a small one. See doc/BENCHMARKS.md, which also explains why that comparison has to be measured in pairs.

Examples

examples/ drives the real binary over stdio and asserts the responses, so the invocations in this README fail CI when they stop being true.

ExampleWhat it shows
session.shA full session: initialise, list, call each tool
errors.shMalformed documents, bad expressions, protocol errors

When not to use oxml-mcp

  • The document fits in context. Paste it; a tool round-trip is slower and adds nothing.
  • You need the model to write XML. These tools read.
  • You need XSLT or XPath 2.0. Neither is available.
  • The document is larger than memory. It is parsed in full.
  • You want the server to fetch documents. It never will; that is the point.

FAQ

Why does the model have to pass the whole document every time?

Because the server holds no state between calls. That keeps it correct when several clients share one binary, and it means there is no cache to invalidate or leak between sessions.

For a large document this is genuinely wasteful, and a future release may add a handle-based flow. Until then, xml_inspect once and a precise xml_query beats several exploratory ones.

Can it read a file from disk?

No, and it will not be able to. The server takes document contents. A server that took paths would let any model with access to it read any file the server process can — the client is where that decision belongs.

Is it safe to point at untrusted XML?

Yes. External entities are never dereferenced, so a document containing <!ENTITY xxe SYSTEM "file:///etc/passwd"> cannot make the server read that file. Entity expansion and nesting depth are bounded.

Why are there only four tools?

Every tool's description occupies context on every request. Four broad tools cost less than twenty narrow ones and cover the same ground, because XPath is already a query language.

Does it work with clients other than Claude?

It implements MCP with no client-specific behaviour, over stdio and both HTTP transports, so any compliant client should work. It is checked against the Python SDK's client over both HTTP transports and scores 100/100 with an independent MCP auditor in both current protocol eras.

My document contains an emoji and the call failed.

That was a bug, fixed in 0.0.3. Python's json.dumps escapes non-ASCII by default, so an emoji arrives as a surrogate pair — 😀 — and the JSON parser rejected escaped surrogate pairs. Any Python client sending an emoji hit it.

How do I query a document with namespaces?

Pass them with the query:

json
{"name":"xml_query","arguments":{  "xml":"…","xpath":"//m:item","namespaces":{"m":"urn:example"}}}

A prefix resolves against these bindings, not against the document, so the same expression works across documents that spell the prefix differently — only the URI has to match. An unbound prefix is an error that names the argument to pass and points at xml_inspect.

xml_inspect reports the namespaces a document uses, which is what makes the argument usable: a model cannot bind a URI it cannot see.

Namespaces (pass these to xml_query as `namespaces`):  urn:example: 12 element(s)

An unprefixed name test matches only nodes in no namespace, which is what XPath 1.0 specifies. namespace-uri() still works and needs no binding.

What happens if the model sends invalid JSON?

A JSON-RPC parse error, -32700. The server does not exit; the next line is read as normal.

Why is an unknown tool isError rather than a JSON-RPC error?

Until 0.0.8 it was -32602. In the stateless HTTP revision the SDK carries that code as an HTTP 400, which a client reports as a transport fault and a model never reads. A model that misspelt a tool name is better served by text naming the four tools that exist. See Protocol.

Development

bash
./scripts/gate.sh

That runs everything CI runs, in the order that fails fastest: format, clippy, tests, rustdoc, the #![forbid(unsafe_code)] check, the examples, the 95% coverage floor and an MSRV build. It pins the toolchain rather than trusting rust-toolchain.toml, because a RUSTUP_TOOLCHAIN in the environment silently overrides that file and a lint that exists in one release and not another then makes a green local run and a red CI one.

The individual steps, if you want them one at a time:

bash
cargo test --all-featurescargo clippy --all-targets --all-features -- -D warningscargo fmt --all --checkcargo bench --bench protocolOXML_MCP="$PWD/target/release/oxml-mcp" ./examples/run-all.sh

CI runs the same set on Linux, macOS and Windows.

Security

The tools never open a file or a socket. External entities are never dereferenced. Entity expansion and recursion are bounded. #![forbid(unsafe_code)]. The HTTP listeners exist only when asked for on the command line, bind loopback by default, and do not authenticate — see Transports.

The threat model is that both the document and the JSON around it are hostile — the document because a model was asked to look at something from the internet, and the JSON because it is the program's entire input. See https://github.com/sebastienrousseau/oxml/blob/main/doc/SECURITY-MODEL.md.

Documentation

Acknowledgements

oxml-mcp exists because of work that came before it:

  • Anthropic — for the Model Context Protocol specification this server implements.
  • lxml and libxml2 — the reference for what an XML toolkit should offer, and decades of hard-won correctness.
  • W3C — for the XPath 1.0 specification that xml_query follows.

License

Dual-licensed under either the Apache License, Version 2.0 or the MIT License, at your option.

来源:README.md,提交 4d13bd8

工具

0
工具元数据尚未被收录。

版本历史

1
  1. v0.0.9最新Oct 4, 2026