feat(mcp): add side-effect-free request preflight

Add local ocs_read validate calls that check operation parameters, nested definitions, request envelopes and batch bindings without dispatching. Report document-dependent checks and unresolved result bindings explicitly. Share validation with execution and validate inline JSON Schema fragments before dispatch.

Validation: 66 targeted tests, desktop build, WebAssembly app/worker check, schema validation and MCP stdio smoke tests.
This commit is contained in:
Karim Jerbi committed 2026-10-13 09:33:49 +01:00
1 parent 97afb57333
commit 006be8909e
4 files changed
+293 -188

No files matched your search

+14 -1
View File
@@ -8,7 +8,20 @@ Before creating an unfamiliar entity, use `ocs_read` with `op: entity_schema` an
`parameters.type` (for example, `Circle`) to get its creation fields, required
values, units, defaults and example. Omit type for the catalog. Creation schemas
describe the inputs to `entities_create`; `record_schema` describes stored records.
Schema discovery runs locally and can omit `ocs_session_id`.
Use `ocs_read` with `op: validate` and `parameters.request` containing the proposed
execute request to preflight it without running it. These local operations can
omit `ocs_session_id`; no GUI is launched or contacted. `request_id` may be omitted
for preflight, but is required for actual execution. Static validation checks
operation parameters, nested entity and inline data definitions, and batch
bindings. It uses the real entity constructor for geometry checks. Results
include `valid`, `executed: false`, warnings, unresolved bindings and checks that
still require execution. Live revisions, handles, styles, file access and command
behavior are not certified by static preflight. Batches are not simulated: later
bindings are fully validated when their values become available during execution.
Invalid requests return a tool error with a JSON Pointer identifying the bad
field where available. Unknown top-level operation parameters remain warnings;
unknown entity fields and invalid nested data are rejected before dispatch.
For interactive work, call start and follow state.command.accepts, options and input_example. To have the person at the screen pick entities for you, call user_select and keep polling until it completes; running means they are still picking. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z.
+20
View File
@@ -144,6 +144,26 @@ def legacy(server: Path) -> None:
assert not entity_schema["result"].get("isError"), entity_schema
contract = entity_schema["result"]["structuredContent"]["schema"]
assert contract["required"] == ["type", "center", "radius"]
validated = request(process, {
"jsonrpc": "2.0", "id": "preflight", "method": "tools/call",
"params": {"name": "ocs_read", "arguments": {
"op": "validate", "parameters": {"request": {
"op": "entities_create", "entities": contract["examples"],
}},
}},
})
assert validated["result"]["structuredContent"]["valid"] is True
assert validated["result"]["structuredContent"]["executed"] is False
rejected = request(process, {
"jsonrpc": "2.0", "id": "bad-preflight", "method": "tools/call",
"params": {"name": "ocs_read", "arguments": {
"op": "validate", "parameters": {"request": {
"op": "entities_create", "entities": [{"type": "Circle", "center": ["bad", 0], "radius": 5}],
}},
}},
})
assert rejected["result"]["isError"] is True
assert "/entities/0/center/0" in rejected["result"]["content"][0]["text"]
close(process)