MCP tools & resources
The complete surface valem-mcp exposes to an agent — over stdio and, identically, over Streamable
HTTP at /mcp. To install and run the server see
Running the MCP server.
- Model tools
- Authoring & verification tools
- Model cards (MCP Apps)
- Spreadsheet compilation
- Token economy
- Tool metadata
- Result shape
- Resources
- Protocol notes
- Worked example (raw protocol)
Model tools
Each tool maps one-to-one to a ModelService operation (and to a REST endpoint / console command —
see api-reference.md). Tool arguments are addressed by canonical JSON Path,
exactly as the REST API and console are.
| Tool | Maps to | Required args | Optional |
|---|---|---|---|
list_models |
GET /models |
— | — |
create_model |
POST /models |
spec (a full ModelSpec) |
— |
get_model_info |
GET /models/{id} |
id |
— |
get_spec |
GET /models/{id}/spec |
id |
— |
get_state |
GET /models/{id}/state |
id |
at (ISO-8601), paths (project subtrees), depth (cap nesting) |
get_field |
GET /models/{id}/state/{path} |
id, path |
— |
get_effective_schema |
GET /models/{id}/schema/{path} |
id, path |
— |
mutate |
POST /models/{id}/mutations |
id, mutations ({"$.path": value}) |
includeTraces |
patch_model |
POST /models/{id}/mutations/patch |
id, patch (RFC 6902 JSON Patch) |
includeTraces |
explain |
GET /models/{id}/explain/{path} |
id, path |
limit |
get_history |
GET /models/{id}/history |
id |
— |
get_audit |
GET /models/{id}/audit |
id |
pathPrefix, from, to, limit |
verify_audit |
GET /models/{id}/audit/verify |
id |
— |
snapshot |
POST /models/{id}/snapshot |
id |
— |
restore |
POST /models/{id}/restore |
id, snapshot |
— |
evolve_spec |
POST /models/{id}/spec/evolve |
id, evolution (a SpecEvolution) |
— |
delete_model |
DELETE /models/{id} |
id |
— |
get_view |
GET /models/{id}/view[/{viewId}] |
id |
viewId |
get_library |
GET /models/{id}/library |
id |
— |
upload_blob |
POST /models/blobs |
data (base64) |
mediaType |
download_blob |
GET /models/{id}/blobs/{blobId} |
blobId |
modelId |
pair_browser |
POST /sandbox/pair + /token |
— | — |
patch_model closes the array-manipulation gap the flat mutations map cannot express: its patch
is an RFC 6902 JSON Patch (add/remove/replace/move/copy/test), so an agent can insert,
remove, or reorder array elements ({"op":"add","path":"/items/-","value":{…}}). Note the path
fields there are RFC 6901 JSON Pointer (/items/0, - for append), not the $.-rooted address
form the other tools use.
get_effective_schema returns a field’s schema overlaid with live meta-derived constraints
(current min/max/required/…), so an agent can check what the pipeline will accept before writing
rather than discovering an invalid value only by getting a schema-violation error.
snapshot/restore are a matched pair: snapshot captures an immutable point-in-time copy of a
model’s state, and restore rolls the state back to a snapshot handed back verbatim — a natural
safety step around a risky evolve_spec.
get_audit/verify_audit expose the durable, append-only audit trail — one record per committed
reactive cycle (mutations, derivedUpdated, traces, flagged constraints, dispatched effects, source,
sequence), the queryable superset of the bounded get_history/explain ring buffer. get_audit
filters by pathPrefix + ISO-8601 from/to window + limit; verify_audit walks the tamper-evident
hash chain and reports the first altered/reordered/deleted record. In remote/paired mode these read the
server’s durable AuditStore; the embedded server keeps an in-memory trail for the session (no hash
chain, so verify_audit reports valid).
upload_blob/download_blob move binary content (base64 in data) in and out of the
content-addressed blob store — niche, for specs that reference binary fields; large blobs may trip the
result-size guard, so this channel suits small binaries only.
pair_browser only appears in remote_with_browser mode (--url ... --browser) — embedded mode and
plain --url remote mode never advertise it. The handshake it drives is described in
Connect your agent.
create_model returns the model info (id + version + derivation/meta/constraint/effect counts);
evolve_spec returns the new version.
Authoring & verification tools
Valem does not run its own LLM to generate specs — the connected agent is already a capable
model. Instead the server hands that agent the same verification substrate the runtime’s internal
SpecGenerator loop uses, so the agent authors and certifies a spec itself: the agent generates,
Valem verifies. None of these touch the live registry, and all of them run against the local
core even in remote mode.
| Tool | Backed by | Purpose |
|---|---|---|
get_domain_guidance |
DomainGuidanceCatalog |
Read the model description in any language, pass the matching topic ids (a fixed menu enumerated in the tool’s schema — amortization schedules, progressive tax/fee charges, classification, date math, percentages, unit conversion, compound growth, weighted averages, eligibility, proration, …) → vetted “hard shape” instructions to follow. Call it before authoring. Topics are extensible via valem.llm.domain-guidance.topics-file / VALEM_DOMAIN_GUIDANCE_TOPICS_FILE (see configuration). |
validate_spec |
ModelSpecValidator |
Validate a spec without creating it → {valid, errors[], warnings[]} (each finding has a location + message). Draft → validate → fix → repeat. |
eval_expression |
the runtime’s JSONata compiler | Evaluate one JSONata expression against a sample input → the computed value or the exact compile/eval error. Verify a derivation/constraint expression before committing it. Pass library (a definition expression) and constants when the expression calls $myFn(...) — without them every library call fails as undefined. |
test_spec |
TestCaseRunner |
Run a spec’s embedded tests (or ad-hoc given→expect cases, keyed by canonical JSON Path) in a throwaway runtime → pass/fail + per-field failures. Certify domain behavior before promotion. |
dry_run |
throwaway ModelService |
Compile a candidate spec in an isolated runtime, apply optional sample mutations, return the resulting merged state — preview the full reactive cascade without registering anything. |
Model cards (MCP Apps)
These tools let a chat assistant hand the user a Valem model as an interactive card inside the
chat (Claude, ChatGPT, VS Code — any host that supports the
MCP Apps extension, io.modelcontextprotocol/ui).
All computation runs server-side in the Java engine; the card only renders. There are two ways in:
- A spec the card holds (
render_model, below). Stateless: nothing is registered, the spec travels with the card, and the server keeps compiled specs only in a short-lived cache keyed by the spec’s SHA-256 hash (StatelessRunner; 200 specs, one hour idle). This is all the hostedvalem.run/mcp/appendpoint offers. - A model the server keeps (
show_model, further down). The stdio server andvalem-web’s/mcpalso offer it: the card shows a model made withcreate_model, and the user’s edits are mutations on that model.
| Tool | Purpose |
|---|---|
valem_spec_guide |
The spec format, view system, JSONata gotchas and card notes in one text — what an assistant reads before authoring. |
render_model |
Args: spec (a full ModelSpec with a viewDefinition), optional inputs (canonical path → value). Validates the spec and runs its embedded tests (a failing test is an error), computes it, and returns structuredContent {modelId, version, specHash, inputs, outputs, flags, warnings} plus the card payload under _meta["valem/card"]. An invalid spec returns isError with {valid:false, errors[]} so the assistant can fix it. Inputs that a rollback rule or the schema rejects fall back to the defaults and are reported as rejectedInputs. Limits: 64 KB spec, 200 expressions. |
explain_value |
Args: path (canonical, e.g. $.total or $.items[0].lineTotal), specHash, optional inputs, and spec when the server answers spec_unknown. Returns how Valem computed that value for those inputs: structuredContent {path, kind, label, value, expression, inputs[], baseInputs[]} (inputs = the values the expression read, baseInputs = the editable inputs it ultimately comes from, present when some direct input is itself computed) and a one-line text the assistant can quote. A row of a list is explained through its [*] rule with that row’s values. An input the user sets is reported as such; an unknown path lists the model’s fields. |
valem_recompute |
App-only (_meta.ui.visibility: ["app"]; hidden from the assistant). The card calls it on every edit with {specHash, inputs} → {specHash, state, meta, flags}, or {specHash, rejected, issues} when a rollback rule or the schema rejects the edit. When the cache no longer holds the hash it answers isError with {error:"spec_unknown"}, and the card repeats the call with spec attached. |
For a model the server keeps (the stdio server, valem-web’s /mcp, a paired sandbox session), two
more tools are listed, only to a client that shows the card:
| Tool | Purpose |
|---|---|
show_model |
Args: id. Shows that model as the same card, with its current state, the flag rules failing now, and its graph. structuredContent {modelId, version, outputs, flags}; the card payload carries modelId. A model without a viewDefinition is an error that says to add one with evolve_spec. |
valem_card_edit |
App-only. The card calls it with {id, specHash, mutations}: the mutations go through mutate (so get_state, explain and the audit trail see them) → {specHash, state, meta, flags}, plus spec and graph when the model’s spec no longer matches specHash (someone ran evolve_spec), or {specHash, rejected, issues} when a rollback rule or the schema rejects the edit, leaving the model unchanged. With no mutations it just reads the model back; the card does that when it opens, so a card reopened from the chat’s history shows the model as it is now, not as it was. |
Edits in a stored model’s card are not inputs layered on defaults, as in render_model: each one is
saved to the model at once, and changes made elsewhere (the assistant’s own mutate, another client
on the same server) show the next time the card saves or reopens.
Share links. A published artifact or document cannot run a model, so a server that can open one
from a link passes a ModelCardTools.ShareLinks to call(name, args, shareLinks). render_model then
adds shareUrl (a live copy anyone can open in a browser) and embedUrl (for an <iframe>) to its
result and text, and the card gets an Open in browser button, which asks valem_recompute (with the
spec attached) for a link carrying the values on screen. The link format is the hosting server’s; the
hosted endpoint uses valem.run’s existing share and embed links. Links over 8000 characters are dropped.
The card itself is ui://valem/model-card.html (text/html;profile=mcp-app), a single
self-contained HTML file built from valem-mcp-app (React + valem-view-react) and packaged into the
valem-mcp jar (skip with -Dskip.frontend=true). After each recompute it tells the assistant the
current inputs and results (ui/update-model-context), so follow-up questions use the values on
screen. render_model also hands the card the model’s dependency graph (_meta["valem/card"].graph:
nodes and edges, as GET /models/{id}/graph returns them), so hovering a computed value shows its
formula and inputs (the same Why is this number? popover as the developer UI, built by
valem-view-react’s createGraphProvenance). A model with several views gets tabs (views reached only
as a list’s itemView are left out), and a host that offers fullscreen gets a Full screen button
(ui/request-display-mode). The server offers the card only to a client that advertises the UI extension, or that sends
no extensions map at all; a client that lists other extensions but not this one gets render_model
as a text-only tool and no valem_recompute.
Spreadsheet compilation
Unlike every tool above, this one does not follow “the agent generates, Valem verifies” — the formula is already machine-readable, so Valem compiles it itself, deterministically. No LLM call is involved anywhere, and none is needed: there is nothing to interpret, only to transpile.
| Tool | Backed by | Purpose |
|---|---|---|
convert_spreadsheet |
SpreadsheetCompiler |
Compile an uploaded .xlsx workbook’s own formulas into a ready ModelSpec (Excel formula language → JSONata). Args: data (base64), filename, modelId. One worksheet, one header-row table, a bounded function set — numeric/boolean (IF/AND/OR/NOT/ROUND/ABS/SUM/AVERAGE/MIN/MAX/COUNT) plus text (CONCATENATE/LEFT/RIGHT/MID/LEN/UPPER/LOWER/TRIM, the & operator, and double-quoted string literals, with the derived field’s schema type inferred accordingly) plus exact-match VLOOKUP against a $-locked side table (embedded in the returned spec’s library, not registered separately) plus SUMPRODUCT of two $-locked whole columns — anything else (approximate-match VLOOKUP, HLOOKUP/INDEX/MATCH, a cross-sheet reference, a legacy CSE array formula, a cross-row reference, a non-uniform per-row formula, …) is named and listed in rejectedColumns rather than guessed; the rest of the sheet still compiles. Every computed value in the returned spec is checked to exactly match what the source workbook itself computed (POI’s own formula evaluator, not Valem’s own translation, is the oracle). Returns {valid:true, spec, rejectedColumns} or {valid:false, reason, error} (UNSUPPORTED_FORMAT/FILE_TOO_LARGE/TOO_MANY_ROWS/TOO_MANY_COLUMNS/PARSE_FAILED/or a column-rejection reason when every column failed). The spec is not registered — review it, then call create_model yourself. |
Token economy
Response size — not latency — is the dominant cost of driving a model over a long agent session, so several tools let the agent hold down the bytes it pulls back:
get_stateprojection. On a large modelget_stateis the biggest context burner. Passpaths(canonical addresses, e.g.["$.order","$.totals"]) to return only those subtrees, spliced back into a pruned document at their addresses; absent addresses are skipped. Passdepth(an integer ≥ 0) to cap nesting — containers deeper than the limit collapse to a compact marker ("<object: 5 fields>"/"<array: 3 items>"). The two combine (pathsfirst, thendepth).- Opt-in
mutate/patch_modeltraces. By default a write returns only the actionable summary (success,mutatedPaths,derivedUpdated,metaUpdated,flaggedConstraints,dispatchedEffects). The full derivation/constrainttraces— the same payloadexplainserves — are included only withincludeTraces: true. Callexplainwhen a value looks wrong; don’t pay for the trace on every write. explainlimit. Trace records can be bulky;limitreturns only the most recent N.- Result-size guard. Any tool result whose pretty-printed JSON would exceed ~50 KB is withheld and
replaced with a compact note (
{"error":"Result too large", …, "hint": …}) telling the agent how to narrow it, so one oversized read can’t wreck the session.
Tool metadata
Each tool declares a human-readable title and MCP annotations so a client can reason about
safety before calling — readOnlyHint (all list_*/get_*/explain), destructiveHint
(delete_model and evolve_spec, which can drop sections), idempotentHint, and openWorldHint
(always false — every tool operates on this server’s in-memory registry, not an external world).
create_model and evolve_spec embed the full ModelSpec / SpecEvolution JSON Schema (reused from
the runtime’s own SpecGenerationSchema) as their inputSchema, so a generating model gets the exact
structure it must produce rather than a bare "type":"object" — the single biggest lever on
first-try generation success.
Tools whose result is always a JSON object also declare an outputSchema (a 2025-06-18 feature),
so a client can validate the structuredContent and the model knows the result shape before calling
— e.g. mutate/patch_model (the mutation-result fields), create_model/get_model_info
(ModelInfo), validate_spec, test_spec, eval_expression. Tools that return an array or scalar
(list_models, get_history, explain, a scalar get_field) carry no outputSchema, since they
emit no structuredContent.
Result shape
A successful tools/call returns a text content block whose text is the operation’s JSON result
(pretty-printed); when the result is a JSON object it is also returned as structuredContent, so a
client can consume it without re-parsing the text. (Array/scalar results — list_models,
get_history, explain, a scalar get_field — are conveyed via the text block only, since MCP
structured content is defined as an object.)
A tool-level failure — an unknown model id, a schema violation, a ROLLBACK constraint — is returned as
a result with isError: true (per the MCP tool-error convention), not as a JSON-RPC protocol
error, so the model sees the failure and can react. Three failure kinds an agent needs to react to
programmatically are surfaced structurally — both the text block and structuredContent carry
the parsed shape rather than a flattened string:
- ROLLBACK constraint —
{ "error": "Constraint violation", "violations": [ { "constraintId", "message", "policy" } ] }. - Schema violation (
mutate/patch_model) —{ "error": "Schema violation", "violations": [ { "path", "keyword", "message" } ] }, so the agent can fix the offending field and retry. - Version conflict (
evolve_specwith a staleexpectedVersion) —{ "error": "Version conflict", "expected", "actual" }, so the agent can re-read the current version and retry instead of guessing.
Malformed protocol requests (bad method, missing name) return JSON-RPC errors.
Resources
The server also advertises the MCP resources capability — readable reference material the agent can pull in while authoring a spec (the same substrate the runtime’s own generator feeds its LLM):
| URI | mimeType | Content |
|---|---|---|
valem://guide/model-spec-format |
text/markdown |
The ModelSpec authoring guide (sections, path notation, JSONata rules and gotchas). |
valem://guide/jsonata-gotchas |
text/markdown |
JSONata pitfalls cheatsheet (bindings, singleton sequences, missing-vs-null, path dialect). |
valem://guide/spec-evolution |
text/markdown |
How to author a SpecEvolution diff (targeted upserts vs wholesale, expectedVersion). |
valem://guide/view-system |
text/markdown |
View-system orientation (component tree, bind, what get_view returns). |
valem://schema/model-spec |
application/json |
The full ModelSpec JSON Schema. |
valem://schema/spec-evolution |
application/json |
The SpecEvolution JSON Schema. |
valem://examples/<name> |
application/json |
Each bundled example spec (e.g. insurance-quote, car-loan-calculator) — working models to learn from. |
Standard MCP flow: resources/list enumerates them, resources/read (with a uri) returns
{ contents: [ { uri, mimeType, text } ] }. An unknown uri returns JSON-RPC error -32002. The
bundled examples and the model-state resource are advertised as resource templates —
resources/templates/list returns valem://examples/{name} and valem://state/{modelId} — so a client
need not enumerate them by name.
Subscribing to model state
The server advertises resources.subscribe, and a model’s live merged state is a subscribable resource:
resources/readonvalem://state/<modelId>returns the current merged state.resources/subscribeon that uri opens a change subscription; every committed mutation then emits anotifications/resources/updatedfor the uri, andresources/unsubscribestops it.
This closes the loop in a paired session: when the developer edits the model in the browser, the
agent gets a resources/updated push and can re-read without polling — instead of only seeing the
change on its next get_state. (It also fires in embedded/plain-remote mode for the agent’s own
mutations.)
Protocol notes
- stdio framing — one JSON message per line; messages never contain embedded newlines (JSON string values escape them). Only protocol messages go to stdout; all diagnostics go to stderr.
- Handshake —
initialize→ capabilities + server info + usageinstructions;notifications/initializedis accepted with no reply;pingreturns{}. A leading UTF-8 BOM on the input stream (some Windows launchers prepend one) is tolerated. - Protocol version — the server negotiates
2024-11-05,2025-03-26,2025-06-18, or2025-11-25(the current revision, the default), echoing the client’s requested version when supported and otherwise falling back to the newest. - Capabilities —
tools,resources(withsubscribe), andloggingare advertised (no prompts). - Logging — the server advertises the
loggingcapability and emitsnotifications/message(a one-line readiness/mode log afternotifications/initialized); a client can raise the minimum severity withlogging/setLevel(below-floor messages are suppressed). - Progress & cancellation —
pair_browser(the one long-poll tool) runs off the read loop: it emitsnotifications/progresswhile it waits (when the client passed aprogressToken) and honoursnotifications/cancelledto abort the wait promptly. - URL-mode elicitation — when the client advertises the
elicitationcapability with URL mode (2025-11-25),pair_browserhands the developer the verification link via a URL-modeelicitation/createrequest (and anotifications/elicitation/completeonce paired) instead of relaying the link and code through the model.
Worked example (raw protocol)
Piping newline-delimited JSON-RPC to the jar (what the client does under the hood):
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"valem","version":"1.0.0"},"instructions":"..."}}
→ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"create_model","arguments":{"spec":{"id":"demo","version":"1","schema":{},"derivations":[{"path":"$.total","expr":"price * qty"}],"constraints":[],"metaDerivations":[],"tests":[]}}}}
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{ \"id\": \"demo\", \"version\": \"1\", \"derivationCount\": 1, ... }"}],"structuredContent":{"id":"demo","version":"1","derivationCount":1},"isError":false}}
→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"mutate","arguments":{"id":"demo","mutations":{"$.price":10,"$.qty":3}}}}
← {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{ \"success\": true, \"derivedUpdated\": [\"$.total\"], ... }"}],"structuredContent":{"success":true,"derivedUpdated":["$.total"]},"isError":false}}
→ {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_state","arguments":{"id":"demo"}}}
← {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"{ \"price\": 10, \"qty\": 3, \"total\": 30 }"}],"structuredContent":{"price":10,"qty":3,"total":30},"isError":false}}
The reactive pipeline runs on every mutate: $.total recomputes to 30 with no extra call. Each
object result carries both the text block and structuredContent.