jsonloupe

io.github.priyanshuN由 GitHub 帳號 priyanshuN 發布v1.9.2更新於 Nov 3, 2026

Open huge JSON locally and give agents its shape, queries and exact values, never the whole blob.

概覽

AI 產生的概覽

本機 MCP 伺服器,把超大或需保真的 JSON 留在助理上下文之外,回答結構、查詢、統計、比對、匯出與表格轉換問題。

可以做什麼
  • 載入並檢視結構:開啟檔案路徑或內嵌文字(含 Zstd、Base64-Zstd 與 PostgreSQL bytea 內容),取得欄位名稱與型別或真實值樣本。
  • 查詢與統計:執行 JSONPath 風格篩選、計數、分組、去重、排名與欄位投影,或分析欄位涵蓋率與統計量,int64 與小數位保持精確。
  • 比對與匯出:依識別鍵對齊陣列比對兩份已載入文件,並把完整查詢結果寫入 CSV 或 JSONL 檔案,資料列不進入對話。
  • 轉為表格:找出可重複的陣列與物件對應,草擬可審閱的轉換規格,再輸出一个 xlsx 活頁簿或 CSV 壓縮檔。
  • 驗證文件:執行 playbook 中的檢查,逐項給出通過、失敗或無法判定。
適用情境
適合 JSON 檔案過大或精度過高、無法直接貼進對話時;需要先解碼壓縮或 bytea 內容時;int64 ID 與小數必須原樣保留時;或需要把巢狀 JSON 變成互相關聯的表格時。也適合對文件做可重複的資料檢查。
執行需求
以本機 stdio 程序執行,透過 npx 從 jsonloupe npm 套件啟動,需要 Node.js。未宣告帳號、API 金鑰或環境變數。文件從本機檔案路徑讀取,匯出與轉換工具會把檔案寫到本機路徑。
安裝前請注意
匯出與轉換工具會寫入你指定的路徑,draft_spec 可能覆寫既有的輸出檔案。文件在本機處理,README 稱不會上傳;僅可選的英文轉查詢功能會聯絡模型,且在主動連線前不會生效。檢查工具不會執行 playbook 中的函式。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 jsonloupe,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

[jsonloupe — a loupe for large JSON: lossless, local, fast]

[CI] [OpenSSF Scorecard] [npm] [jsonloupe MCP server – quality and maintenance score on Glama] [OpenSSF Best Practices] [Mozilla Observatory grade] [License: MIT] [TypeScript strict] [Local-first] [Lossless int64]

[Demo: the sample opens unfolded with exact int64 ids, the command palette finds compare, the comparison opens on the changed values, a local query runs, the document previews as linked spreadsheet tables, and it encodes as Base64-Zstd. Click to view full size.]

jsonloupe is a browser-based workbench for inspecting, diffing, editing, and querying JSON documents that are too big or too precise for ordinary viewers — and for turning them into spreadsheets: nested JSON goes in, linked tables come out, as one .xlsx or a zip of CSVs. The questions you ask of every file can be kept as checks in a playbook: one press judges them all over the open document, and the same file gates CI through jsonloupe check. Nothing leaves your machine: in the browser, documents are parsed in a web worker, stored in IndexedDB, and never uploaded anywhere. No backend, no account, no telemetry.

Run it

Use the hosted app at jsonloupe.dev, rebuilt from main on every merge — or run the latest npm release from your own machine, fully offline:

bash
npx jsonloupe

That starts a loopback-only static server (127.0.0.1:5199, or the next free port) serving the prebuilt app from the package — no runtime dependencies, no network calls, one auditable file of about 200 lines with its comments: bin/jsonloupe.mjs. --port <n> and --no-open do what they say; --key-file <path> serves a model key from that file to the page, over loopback only. The same command carries the converter's verbs (inspect, draft, convert, below) and the CI gate (check, below); npx jsonloupe --help lists them.

Why another JSON viewer?

  • Lossless numbers. Int64 IDs beyond 2^53 (snowflake/order/entity IDs) and precise decimals survive paste → view → edit → copy → download with their exact digits. Nothing is ever coerced to a float.
  • Built for huge documents. The tree is virtualized and expansion is chunked ([0 … 9999] range rows), so opening a five-million-element array is instant. Parsing, diffing, search, and compression all run off the UI thread.
  • Compressed payloads are first-class. Paste or drop raw .zst, Base64-Zstd (standard or URL-safe), or PostgreSQL \x… bytea — it's detected, decoded in the worker, and opened as a document with a transformation trace. An encoder and a transport-size inspector (exact UTF-8 / Zstd / Base64 / envelope bytes against editable budgets) round-trip the other way.
  • Nested JSON becomes a spreadsheet, on your machine. Repeating arrays and object maps become their own linked tables — orders and order_items joined by order_id, never items/0/sku columns. You see real rows before anything is written, and the mapping saves as a small file that produces the same columns from next month's file, with no model involved.
  • Semantic diff. Side-by-side compare with identity-based array alignment (match by id, composite keys, or auto-detected), so reordered arrays don't drown you in false changes. Ignore noisy fields by key or path prefix.
  • A real query language. A JSONPath subset with predicates and aggregation pipes ($.tasks[?(@.status == 'FAILED')] | group(@.failureReason)), executed locally with live preview as you type. Numeric literals, predicates, sums, averages, minima, and maxima remain exact for int64 and precise decimals.
  • Checks you can gate on. Keep the questions you ask of every file as checks — a query and the count it should come to — and one press judges them all, naming the first paths behind each failure. A count it could not finish is inconclusive, never a pass it has not earned. The same playbook file gates CI through jsonloupe check and an agent through MCP's run_checks.

Quick start

sh
git clone https://github.com/priyanshuN/jsonloupe && cd jsonloupenpm cinpm run dev        # http://localhost:5199

Paste JSON on the landing page — it parses instantly and is saved to local memory (Recents sidebar: reopen, pin, delete). Malformed input (trailing commas, single quotes, Python literals, truncated log extracts) is auto-repaired and flagged, with the original bytes preserved.

Feature tour

  • Tree / Code / Split views — virtualized tree, editable CodeMirror code view (fold, search, apply-with-⌘S), or both side by side with click-to-line sync. Inline-edit primitives in the tree; full undo/redo across edits. A small document opens unfolded to fill the screen, and scrolling deep keeps the parent keys you are inside pinned at the top of the tree.
  • Command palette — ⌘K / Ctrl K from anywhere: type a few letters of a view, an action, a theme or a recent document's name, and press Enter.
  • Colour schemes — Tokyo Night, Catppuccin Mocha, Dracula, Gruvbox and GitHub Dark for dark; Solarized Light, Catppuccin Latte, Rosé Pine Dawn and GitHub Light for light; plus jsonloupe's own pair. Hover one in the sidebar picker (or arrow through them in ⌘K) to try it on the whole app. Each mode keeps its own scheme, so "follow the system" swaps between your two at sunset. Every scheme meets WCAG AA contrast, checked in CI.
  • Search & filter — / to search (literal or /regex/i), filter mode prunes the tree to matching branches and restores your expansion state after.
  • Path & identity — breadcrumb with one-click copy as JSONPath, JSON Pointer, or JS accessor; "same value" jumps to every node holding a value (schema-free correlation-ID tracing).
  • Value lenses — epoch → your local date and "3 days ago", the same relative time on ISO timestamps, a colour chip on colour strings, latitude/longitude, uuid/url/base64 flags, and byte weight on collapsed containers so the heavy node is findable at a glance.
  • Tables & CSV — any array gets a sortable table view; export exact-digit CSV (RFC 4180) of tables or query results.
  • Playbook — your checks and functions on one sheet — a check is a query and the count it should come to (none, at most 3 groups, between 1 and 10); run all judges every one over the open document and points at the first nodes behind each failure. A function is JavaScript for the questions no query language should have to answer, run in a sandbox with the network removed; tick several and one press answers all of them as a single report. The same checks gate CI through jsonloupe check. (below.)
  • JSON → Excel / CSV converter — rename and reorder columns, choose source fields, add constants, set date and coordinate handling, take the column names from a target CSV header, and save or share the mapping for the next file. (below.)
  • File handles — drop .json/.jsonl/.zst files; reload re-reads from disk and shows what changed since the version you were looking at.
  • Query — run JSON queries locally, save useful ones, or turn a result into a reusable check. Optional English → query mode translates a question using only the document's shape (field names/types — never values), then stops so you can review the generated query before running it locally. Bring your own connection: authorize with OpenRouter without pasting a key, or use the advanced manual-key option — paste a key, or load it from a local file (a raw key or a .env line, read in the page and never uploaded). Until you explicitly connect, the feature is inert. A disclosure panel records exactly what was sent. See SECURITY.md.

Browser query cookbook

Open a document, press query, leave JSON query selected, and paste a query. It runs directly in this tab: no API key and nothing sent to a model.

Given this small document:

json
{"tasks":[  {"id":9007199254740992,"status":"FAILED"},  {"id":9007199254740993,"status":"READY"}]}
  • Filter to the failed rows:

    text
    $.tasks[?(@.status == 'FAILED')]

    Result: one match, $.tasks[0].

  • Count rows by status:

    text
    $.tasks[*] | group(@.status)

    Result: FAILED 1, READY 1.

  • Compare an integer beyond JavaScript's safe range without rounding it:

    text
    $.tasks[?(@.id > 9007199254740992)] | count

    Result: 1 — only the exact 9007199254740993 id is greater.

Nested JSON to linked tables

If a spreadsheet is the only thing you came for, jsonloupe.dev/json-to-excel.html is that job on a page of its own: paste, convert, done.

Open a document and press the table glyph in the toolbar's tools group (convert; every glyph there names itself on hover). Every repeating array becomes its own table, joined back to its parent by the id that was already in the data — so this:

json
{ "orders": [    { "id": 7, "cust": "ACME",      "items": [ { "sku": "A", "qty": 2 }, { "sku": "B", "qty": 1 } ] } ] }

comes out as two sheets rather than one row full of items/0/sku columns:

orders                    order_itemsid  cust                  order_id  sku  qty 7  ACME                         7  A      2                                 7  B      1

jsonloupe finds those tables for you but never runs the guess blindly: the mapping and a preview of real rows stay on screen while you rename columns, choose which fields to keep, and say how dates and coordinates should be read. What you approve is saved as a small mapping file — keep it in this browser, export it, or use it from the command line:

bash
jsonloupe inspect payload.json          # what tables are in here?jsonloupe draft   payload.json -o payload.spec.json   # a first mapping, to readjsonloupe convert payload.json --spec payload.spec.json -o payload.xlsx

The same mapping produces the same columns through the browser, the CLI, and the MCP server, with no model in the loop — that is the point of freezing it. What lands in the cells is what was in the document: dates arrive as real dates you can sort and subtract, numbers and coordinates as numbers, and text that only looks numeric stays text, so "1.10" keeps its trailing zero and an int64 id keeps every digit instead of being rounded. Excel's limits and values a column cannot read are reported or refused, never quietly written wrong.

Playbooks: your functions and checks as a file

The document changes daily; the handful of questions you ask of it does not. So playbook in the toolbar (the functions view before 1.8; ⌘K still finds it by that word) opens them beside the document, as one sheet: your checks — a query and the count it should come to — and your functions, each list in name order. run all (⌘⇧↵ / Ctrl ⇧↵ while the playbook is open; press it again to stop) measures every check over whatever is open and paints each row's verdict in place, then runs the ticked functions. The first row that did not pass opens by itself, showing the first nodes it matched; press a path and the tree beside the sheet selects that node, while the sheet stays where it is.

Checks

Press + check in the sheet, or turn a Query result into one with save as check. A check counts matches — groups for | group, distinct values for | distinct — and passes when the count meets its expectation:

Pass whenexpectationPlaybook version
no matches are found{"type": "no-matches"}3
at least one match is found{"type": "at-least-one"}3
exactly N are found{"type": "exact-count", "count": N}3
at most N are found{"type": "at-most", "count": N}4
at least N are found (N ≥ 1){"type": "at-least", "count": N}4
between M and N are found, both included{"type": "between", "min": M, "max": N}4

A count is a whole number within JavaScript's safe range; one past it is refused, never rounded. A query that ends in | sum, | avg, | min or | max yields one value rather than a count, so a check on it says it could not run instead of judging a number it was never meant to count.

Every check comes back as ✓ pass, ✕ fail, ? inconclusive, or ✗ could not run. Inconclusive is the honest third answer: | group and | distinct keep at most 100,000 groups or values (the cardinality cap). Past that, the count is only a lower bound: at least 100,001. A lower bound passes only when it proves the expectation by itself (at least one, or at least N already reached), fails when it is already over the limit (none, or at most 100,000), and is inconclusive otherwise — it is never reported as a pass it has not earned. Narrowing the query decides it.

A check whose query names a field the open document does not have says so on its row, and its panel adds "this reads status — this document has none of that, so a pass here may prove nothing". A no matches check passes just as well on a file whose field was renamed.

A saved check keeps one last verdict, with up to five matched paths and a short preview of each. It belongs to the document the check last ran on: run it on another document and that run replaces it. Reopen the document it ran on and the verdicts are still there; edit it and they fade, marked "document edited", until you run again. That record stays in this browser: it is never written into an exported playbook, and removing the document removes it.

Functions

A function learns what it reads. The first time one runs it is handed the document through a recording proxy, and the paths it actually touched are kept with it. That is what lets the sheet say

this reads orders, orders[].deliveredInHours — this document has none of that

before you press run, instead of leaving you to read an empty result as "none today". It is always a remark and never a gate: the reading knows only the branch that run took, so the run button still works. Tick several functions and run all answers all of them, as a single object keyed by function name. open report in the functions head shows it: today's report, which downloads and reopens as a document like any other. A function's result lasts while that document stays open. Open another document, or reload, and its row says it has not run. Which functions are ticked is remembered in this browser.

Scripts run in an ephemeral worker with fetch, XMLHttpRequest, WebSockets, IndexedDB, importScripts and nested workers removed before any user code, terminated on its result or after ten seconds (a ticked batch shares those ten seconds, and one that runs past them is stopped whole) — a pasted script cannot reach the network or the documents you have opened. JavaScript number mode preserves the behavior of existing functions and says when a document contains values it will round. exact text mode instead hands those unsafe literals to the function as their original digit strings ("9007199254740993"); ordinary values such as 3 and 1.5 remain numbers, so normal counting and arithmetic still work. The mode is saved and exported with the function rather than left as a browser preference.

The file

A playbook is that reusable work as a file — the questions and expectations, never the data:

json
{  "playbookVersion": 4,  "name": "carrier dumps",  "functions": [    {      "name": "slow orders",      "script": "data.orders.filter(o => o.deliveredInHours > 48)",      "reads": ["orders", "orders[].deliveredInHours"],      "numberMode": "exact-text"    }  ],  "checks": [    {      "name": "no failed orders",      "query": "$.orders[?(@.status == 'FAILED')]",      "expectation": { "type": "no-matches" }    },    {      "name": "a full day of orders",      "query": "$.orders[*]",      "expectation": { "type": "at-least", "count": 1000 }    },    {      "name": "a handful of carriers",      "query": "$.orders[*] | group(@.carrier)",      "expectation": { "type": "between", "min": 1, "max": 5 }    }  ]}

export in the playbook bar writes one, as version 3 unless a check needs version 4: a library with no at-most, at-least or between check exports as version 3, which jsonloupe 1.4 to 1.7 read. import, or dropping the file on the window, adds what it holds. Import merges and never replaces — a name you already use keeps yours and the incoming one lands as slow orders 2, because a duplicate is one forget away and an overwrite is not. Unknown fields are refused by name rather than dropped, so a file from a newer jsonloupe cannot import as a subset of itself and look like it worked. The library keeps 100 functions and 100 checks: saving a 101st is refused with a message saying so, and a playbook that would not fit is refused whole, rather than pushing your oldest ones out. Version 1, 2 and 3 playbooks still import; version 1 functions keep their original JavaScript-number mode.

In CI: jsonloupe check

The same checks gate a pipeline. jsonloupe check runs a playbook's checks against one or more files with the browser's own engine, and exits non-zero when one fails:

bash
npx -y jsonloupe@^1.8.0 check carrier-dumps.playbook.json out/orders.json
out/orders.json · 3 checks · 4 ms  ✕ no failed orders — Fail — 2 matches; expected no matches      first: $.orders[312]             $.orders[877]  ✓ a full day of orders — Pass — 1,204 matches; expected at least 1,000 matches  ✓ a handful of carriers — Pass — 3 groups; expected between 1 and 5 groups1 function not run — functions run only in the browser✕ 1 failed · ✓ 2 passed

Keep the @^1.8.0. jsonloupe before 1.8 has no check verb: it starts the viewer's local server instead, which never exits, so the job hangs until the runner times it out. From 1.8 on, a word jsonloupe does not know exits 1 rather than starting anything.

The playbook comes first, then any number of files: .json, .jsonl/.ndjson, raw Zstd, or a Base64-Zstd / bytea payload, detected as they are in the browser. A file that is not valid JSON fails the gate rather than being repaired behind your back; --allow-repair checks the repaired document instead. Every query is checked before any file is read, so a typo in the last check costs no parsing. Functions are JavaScript and never run outside the browser's sandbox, so check counts them and says it did not run them. The text report prints matched paths and never values, since CI logs are often public. A path still names the keys it passes through, so on a document keyed by e-mail address or account id, pass --no-evidence. --format json writes one object, with a short preview of each matched node, for a program to read; --max-evidence N (0–100, default 3) or --no-evidence sets how many paths a check that did not pass names.

Exit statusMeaning
0every check passed on every file
1usage error, a file or playbook that could not be read, or a file that is not valid JSON — including one that would need repairing
2the playbook is not valid JSON or not a playbook, has no checks or more than 100 (what a library holds), or has a query no check can count; nothing was checked
3at least one check failed
4nothing failed, but a check was inconclusive or could not run

With several files the most actionable status wins: 3, then 1, then 4. Undecided has its own status so a pipeline can tolerate it on purpose (… || test $? -eq 4).

In GitHub Actions, --format github adds annotations after the text report: one on the playbook for each check that did not pass — a warning when it was inconclusive, an error otherwise — and an error on each file it could not check, or on the playbook when it is invalid:

yaml
jobs:  checks:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v7      - uses: actions/setup-node@v7        with:          node-version: 24      # … the steps that write out/orders.json      - run: npx -y jsonloupe@^1.8.0 check carrier-dumps.playbook.json out/orders.json --format github

An agent gets the same gate from the MCP server's run_checks (below), with a playbook's path or the checks inline. It refuses a document that is not valid JSON in the same way; allowRepair: true checks the repaired document and says so.

Use with AI agents

The same engine runs as an MCP server, so an agent can work on a document that would never fit in its context:

bash
claude mcp add jsonloupe -- npx -y -p jsonloupe jsonloupe-mcp

If you have jq and plain JSON, use jq. This is for the cases jq is awkward about: compressed payloads (.zst, Base64-Zstd, PostgreSQL \x… bytea) that have to be decoded first, int64 and decimal digits that must survive the whole pipeline exactly, identity-keyed semantic diff between two documents, and MCP hosts that have no shell to run jq in.

Twelve tools (run_checks from 1.8): load_doc, get_schema, run_query, profile, sample, diff_docs, export_csv, export_result, plus inspect, draft_spec, and convert for the spreadsheet path above, and run_checks, which gates a document on a playbook's checks and never runs its functions. For one question, pass filePath straight to get_schema, run_query, profile, sample, export_csv, export_result or run_checks; it opens the document and returns a reusable docId in the same call. For a sequence of questions, and for diff_docs and the converter tools, which take a docId, call load_doc once and reuse that docId. Up to eight documents stay open; past that the least recently used is closed, and the next response says so. Query details default to 10 rows (use limit=0 for summary only, or offset + limit to page), and text and structured MCP results are both bounded. A typical run over a 37 MB routing payload:

run_query filePath=payload.json "$.tasks[?(@.status == 'FAILED')] | count"                                  → docId: d1 · count: 17500profile d1 "$.tasks[*]"          → auto-discovered field coverage, null/missing/types,                                    exact sums/stats, lengths, distincts and top valuesrun_query d1 "$.tasks[?(@.status == 'FAILED')] | group(@.region, @.failureReason)"                                  → composite breakdown, exact complete countsrun_query d1 "$.tasks[*] | top(@.delayMinutes, @.id, @.status)" limit=5                                  → only the five highest rows; the full array never enters contextsample d1 "$.tasks[0]"           → the whole element, id 9007199254740993 intactexport_result d1 "…| pluck(@.id, @.failureReason)" format=csv outPath=failed.csv                                  → complete, atomic, rows + UTF-8 bytes onlyinspect d1                       → candidate tables, fields, types, deterministic suggestionsdraft_spec d1 outPath=tasks.spec.json                                  → reviewable mapping on diskconvert d1 specPath=tasks.spec.json outPath=tasks.xlsx                                  → workbook path + row/skipped/warning counts, never its rowsrun_checks d1 playbookPath=tasks.playbook.json                                  → pass, fail or inconclusive per check (or could not run);                                    a failed or inconclusive check names its first paths                                    ($.tasks[3]); functions counted, never run

The common schema → profile → query loop is deliberately cheaper than loading JSON or writing a one-off Python script: counts and aggregates stream over every match without materializing the result list; present, missing, and isNull keep false/null/absent distinct; composite grouping and bounded top/bottom replace local sort scripts; and profiles inspect multiple or automatically discovered fields in one scan. Complete CSV/JSONL exports stream to a temporary file and publish atomically. Existing paths are refused unless the agent passes overwrite=true explicitly.

The server makes no network calls and accesses only explicitly supplied input/output paths. A bad query returns the grammar and a suggestion rather than an empty result. The reproducible agent-choice evaluation measures whether an agent selects these operations when Python is also available.

If an agent knows the MCP is installed but still writes one-off scripts, put this routing rule in its project instructions (for example AGENTS.md or CLAUDE.md):

For small plain JSON, use jq when it is already the shortest safe option. Otherwise prefer the JsonLoupe MCP over ad-hoc Python whenever it supports the operation; use custom shell code only when JsonLoupe cannot answer it.

In the browser: WebMCP

Where the browser exposes WebMCP (document.modelContext — a Chrome origin trial from Chrome 149, and the ChatGPT desktop app's built-in browser), jsonloupe.dev registers the same bounded verbs pointed at the document already on screen, so a browser agent and the person watching share one document instead of mailing copies back and forth. Eight tools: run_query, get_schema, profile and sample work as they do over stdio, and four act on the viewer itself, which no headless server can do: load_doc opens a document in it, reveal_path scrolls the tree to a row and flashes it, and highlight_matches/clear_highlights drive the toolbar's own filter state — visible, counted, and reversible by the human's own button. Every tool that returns document content is annotated untrustedContentHint: values are data, never instructions. Browsers without the API fetch nothing and run the same app they always did. Details, testing instructions, and provenance in WEBMCP.md.

Privacy & security

Documents never leave your machine. The complete network-call inventory (four fetch calls, all in opt-in model connection or English query translation), the shape-only LLM contract, and the XSS posture are documented in SECURITY.md. The corresponding threat model, trust boundaries, and assurance argument are in SECURITY-ASSURANCE.md.

Deploying

jsonloupe builds to a fully static site:

sh
npm run build      # dist/

Serve dist/ from any static host (GitHub Pages, Cloudflare Pages, Netlify…). The app needs nothing server-side to work. To match jsonloupe.dev's protection, set the response headers in SECURITY-HEADERS.md at the host — above all the Run sandbox's own CSP on /assets/run-sandbox-*.js, without which a pasted script's import() can reach any host. The local /__api-key convenience endpoint (dev mode, or npx jsonloupe --key-file) simply doesn't exist in a static deploy, and English translation activates only when a visitor connects their own model account or key.

Development

sh
npm run dev        # Vite on :5199, with the loopback-only /__api-key endpointnpm test           # vitest — engine, worker, converter, playbook, CLI, MCP, WebMCP, NL and DOM suitesnpm run lint:security  # FLOSS security analysis + checked TypeScript stylenpm run lint:contract  # the UI contract in src/style.css: tokens, contrast, motion, type rampnpm run lint:headers   # every source file carries the MIT SPDX and copyright noticenpm run coverage   # same suite with enforced 90% statement / 80% branch floorsnpm run build      # type checks + vite build + the MCP and CLI bundlesnpm run build:mcp  # or build:cli — one target's type check and bundle onlynpm run test:a11y  # real Chromium: axe, keyboard/focus, playbook and WebMCP journeys (run after build)npm run check:site-headers # verify the live OpenSSF hardening headersnpm run check:reproducible-build  # clean locked installs must produce identical bytesnpm run smoke:mcp  # drives the built MCP server over real stdio against a 37 MB fixture (run after build)npm run eval:agent -- --help  # black-box MCP-vs-Python agent-choice benchmarknpm run eval:ask -- --dry-run  # Ask correctness/injection suite; --dry-run costs nothing

Start with CONTRIBUTING.md. Architecture and internals are documented in ARCHITECTURE.md and SPEC.md; the converter has its own SPEC-converter.md, including what it deliberately refuses to do. Project decisions and the next twelve months are in GOVERNANCE.md and ROADMAP.md. Official publishing and consumer verification are documented in RELEASING.md. What changed in each release is in CHANGELOG.md.

License

MIT. Bundled third-party packages are listed in THIRD_PARTY_LICENSES.md.

Software is provided "as is", without warranty of any kind — see the license text. If you connect a model for English queries, your use of the configured LLM provider is governed by that provider's terms.

來源:README.md,提交 94fc337

工具

12

load_doc

Open JSON once for agent analysis

Open a JSON document and return its size and top-level shape. Accepts a file path or inline text, and transparently decodes Zstd, Base64-Zstd and PostgreSQL bytea (\x…) payloads. Malformed JSON is auto-repaired and flagged. Returns a docId (d1, d2, …) that every other tool takes. The document itself is never returned — read it with get_schema, run_query, profile or sample instead. Prefer path for an existing or large document. For a single question, skip this step: those tools also take filePath.

輸入參數pathtext

inspect

Find convertible tables in JSON

First step toward a spreadsheet: find the repeatable arrays and object maps in a loaded document that can become linked tables. Returns table anchors, row counts, fields, types, and deterministic format suggestions; document values are not returned. Next, draft_spec writes a mapping and convert writes the file. For field names and types use get_schema, for coverage and statistics profile, and for real values sample.

輸入參數docId必填

draft_spec

Draft a reviewable converter spec

Draft a reviewable converter spec for a loaded document, usually after inspect shows its tables. This does not convert anything: pass the spec to convert, inline or as the file you saved. Save it to outPath for human editing, or omit outPath to return the JSON under the response cap; an existing outPath is refused unless overwrite=true. format picks one .xlsx workbook or a .zip of CSVs; baseDate gives time-only columns a date.

輸入參數docId必填formatoutPathbaseDateoverwrite

convert

Convert JSON to XLSX or CSV

Write a loaded document out as linked tables — one .xlsx workbook, or a .zip of CSVs — by running a frozen converter spec deterministically. The spec comes from draft_spec, reviewed or edited: pass it inline as spec or as a file in specPath, exactly one of the two. format overrides the format the spec names. Returns row/skipped/warning counts, never the output rows. An existing outPath is refused unless overwrite=true. For one query's result as a single CSV or JSONL file, use export_result instead.

輸入參數specdocId必填formatoutPath必填specPathoverwrite

get_schema

Discover JSON fields and types

Field names and types of the document — never values. With no path, describes the whole document; with a path, describes just what it selects, merged across matches (so $.tasks[*] describes an element, not the first element). Array shapes are inferred from up to 30 elements; use profile for exact coverage/counts. Start here: it is how you learn which paths run_query can use. For one question, pass filePath directly; it opens the file and returns a reusable docId in the same call. A path that matches nothing returns an error naming that path.

輸入參數pathdocIdfilePath

run_query

Count, group, rank, or aggregate JSON

Use this instead of ad-hoc Python for JSON counts, filtered exact sums/averages/min/max, composite groups, distinct values, top/bottom ranking, and field projection. It scans server-side and returns only the bounded answer. Pass filePath for a one-call answer, or docId to reuse an open document. For field names and types use get_schema; for coverage and statistics of many fields at once, profile; to judge counts against expectations, run_checks. Grammar (a JSONPath subset with aggregation pipes): Paths: $.key $['odd key'] $[3] $[-1] $[1:5] $.arr[*] $..key (recursive) A [...] step attaches directly to what precedes it: $['odd key'], $.a['b c'] — never $.['odd key']. Any key that is not a plain identifier (space, '-', '.') must be bracketed: $['line-items'], @['unit price']. $.arr is the array itself; $.arr[*] is its elements — $.orders[*] | count counts the orders, $.orders | count is 1. Predicates filter the CHILDREN of the current node: $.tasks[?(...)] A [?(...)] step already yields the matched elements — never follow it with [*]. Operators: == != > >= < <= (numbers and strings) Boolean: && || ! Grouping: ( ) — there is no !~; negate with !( … ) Strings: @.name contains 'x' | startsWith | endsWith | @.name =~ /safe-regex/i Membership: @.status in ['A','B'] Arrays: @.tags contains 'x' Set: @.field — true unless the key is missing, null or false ('' and 0 count as set) Explicit: @.field present (key exists) | missing (no key) | isNull (value is null) Fields compare to fields: @.capacity.used > @.capacity.max A predicate holds one level only: no predicate inside a predicate, no $ path inside it Pipes: append AT MOST ONE, at the very end. Pipes never chain — `| top(...) | pluck(...)` is invalid. | count | sum(@.x) | avg(@.x) | min(@.x) | max(@.x) | distinct | group(@.x, @.y) | top(@.score, @.id) | bottom(@.score, @.id) | pluck(@.a, @.b.c) top/bottom/pluck already name their output columns — never add a se

輸入參數docIdlimitquery必填offsetfilePath

profile

Profile unfamiliar JSON without code

Profile one or more fields across every selected record in one server-side scan; this replaces ad-hoc Python loops for discovering or summarizing unfamiliar JSON fields. Pass filePath for a one-call answer, or docId to reuse an open document. Returns present/missing/null counts, type counts, distinct count, exact numeric sum/min/max/average, lengths, and top values. Use fields like "status" or "capacity.used" relative to each selected record; omit fields to auto-discover up to 20 fields on selected records or profile selected scalar values. For a filtered count or a group breakdown use run_query; for whole records use sample.

輸入參數topdocIdquery必填fieldsfilePath

sample

Sample JSON values

Read n real values at a path, exactly as they were parsed (int64 and decimal digits intact). A path that selects one container samples its children; a path that selects many nodes samples those nodes. Pass filePath for a one-call sample, or docId to reuse an open document. Use it to see what records really hold; to count or aggregate use run_query, for statistics profile, and for names and types get_schema.

輸入參數npath必填docIdfilePath

diff_docs

Compare JSON documents

Compare two documents opened with load_doc and return change counts plus the first 25 changes. There is no filePath shortcut: load both first. docIdA is the older side: additions and removals read A → B. Give keySpec to align arrays by identity (e.g. "id,orderId") so reordered elements are not reported as wholesale changes.

輸入參數docIdA必填docIdB必填keySpec

export_csv

Export query to CSV

The CSV-only shortcut for export_result: run a query and write its complete table to a CSV file (RFC 4180, exact digits, formula-injection safe). Returns only the path, row count and byte size — the rows themselves never enter the conversation. The query must produce a table: use `| pluck(…)`, `| group(…)` or `| distinct`. The file is streamed and published atomically, with the same 50 MB limit; an existing path is refused unless overwrite=true. Pass filePath to open and export an input file in one call, or docId to reuse an open document. For JSONL or nested values use export_result; for linked multi-table spreadsheets, convert.

輸入參數docIdquery必填outPath必填filePathoverwrite

export_result

Export complete query result

Write every query match to a local file without sending the data through the conversation. JSONL accepts bare filtered matches and preserves nested values; CSV requires `| pluck(…)`, `| group(…)` or `| distinct`. The response contains only format, path, exact row count, byte size and complete=true. Exports refuse rather than silently truncate when the 50 MB output safety limit would be exceeded. Output is streamed to a temporary file and published atomically; an existing path is refused unless overwrite=true. Pass filePath to open and export an input file in one call, or docId to reuse an open document. export_csv is this tool fixed to CSV. Use convert for linked multi-table spreadsheets, and run_query when a bounded answer in the conversation is enough.

輸入參數docIdquery必填format必填outPath必填filePathoverwrite

run_checks

Run playbook checks against JSON

Run deterministic checks — a query plus the count it expects — against a document; one verdict per check: pass, fail, or inconclusive (a | group or | distinct hit its 100,000-value cap, so the count is only a lower bound; never reported as a pass). Pass playbookPath for a jsonloupe playbook file, or checks inline. Playbook functions are JavaScript and are never run here. Expectations: {"type":"no-matches"}, {"type":"at-least-one"}, {"type":"exact-count","count":N}, {"type":"at-most","count":N}, {"type":"at-least","count":N}, {"type":"between","min":M,"max":N}. A check counts matches (groups/distinct values for | group / | distinct); value pipes like | sum are refused. A document that is not valid JSON is refused unless allowRepair is true. Failed and inconclusive checks list their first matched paths. Pass filePath for a one-call answer, or docId to reuse an open document. Use it to gate a document on counts you expect; for an open-ended count or breakdown use run_query.

輸入參數docIdchecksfilePathallowRepairmaxEvidenceplaybookPath

版本歷史

3
  1. v1.9.2最新Oct 29, 2026
  2. v1.9.1Oct 26, 2026
  3. v1.9.0Oct 26, 2026

評分

已滿足適用檢查項的 90%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分
  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分