From d35a93c6e8d83daf54c43d4dae54ba79578949a6 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 7 Sep 2026 19:34:31 +0300 Subject: [PATCH] Avoid reserved MCP session argument --- docs/automation/README.md | 10 +++++----- docs/automation/mcp_eval.py | 12 ++++++------ docs/automation/mcp_smoke.py | 4 ++-- src/mcp.rs | 29 +++++++++++++++++++---------- 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/docs/automation/README.md b/docs/automation/README.md index d812c6244..c1e200e3b 100644 --- a/docs/automation/README.md +++ b/docs/automation/README.md @@ -17,11 +17,11 @@ The server provides four tools: - `ocs_execute` performs one operation or a sequential batch against the real editor. - `ocs_capture` returns a bounded PNG of the drawing viewport or complete window. -The normal flow is to call `ocs_sessions`, read the chosen session with `ocs_read`, then pass the returned document state into `ocs_execute`. For example, an undo request has this shape: +The normal flow is to call `ocs_sessions`, pass its returned `session_id` as `ocs_session_id` to the other tools, read the chosen session with `ocs_read`, then pass the returned document state into `ocs_execute`. For example, an undo request has this shape: ```json { - "session_id": "SESSION_FROM_OCS_SESSIONS", + "ocs_session_id": "SESSION_FROM_OCS_SESSIONS", "request": { "op": "undo", "request_id": "undo-1", @@ -37,7 +37,7 @@ A complete command can be sent with `run`. Prompt answers are separated by space ```json { - "session_id": "SESSION_FROM_OCS_SESSIONS", + "ocs_session_id": "SESSION_FROM_OCS_SESSIONS", "request": { "op": "run", "request_id": "polyline-1", @@ -52,7 +52,7 @@ Use `batch` when the steps are already known. OCS supplies each step with the st ```json { - "session_id": "SESSION_FROM_OCS_SESSIONS", + "ocs_session_id": "SESSION_FROM_OCS_SESSIONS", "response_detail": "changed_entities", "request": { "op": "batch", @@ -71,7 +71,7 @@ Execute responses use `response_detail: "compact"` by default and return only th ```json { - "session_id": "SESSION_FROM_OCS_SESSIONS", + "ocs_session_id": "SESSION_FROM_OCS_SESSIONS", "op": "query", "parameters": {"intersections": ["2A", "31"]} } diff --git a/docs/automation/mcp_eval.py b/docs/automation/mcp_eval.py index 956633c4f..db904bbb9 100644 --- a/docs/automation/mcp_eval.py +++ b/docs/automation/mcp_eval.py @@ -98,14 +98,14 @@ def main() -> None: if active.get("start"): client.tool( "ocs_execute", - {"session_id": session, "request": {"op": "new", "request_id": "eval-new"}}, + {"ocs_session_id": session, "request": {"op": "new", "request_id": "eval-new"}}, ) base = 1_000_000 + int(time.time()) % 100_000 draw = client.tool( "ocs_execute", { - "session_id": session, + "ocs_session_id": session, "response_detail": "changed_entities", "wait_seconds": 0, "request": { @@ -129,19 +129,19 @@ def main() -> None: crossings = client.tool( "ocs_read", - {"session_id": session, "op": "query", "parameters": {"intersections": line_handles}}, + {"ocs_session_id": session, "op": "query", "parameters": {"intersections": line_handles}}, ) assert crossings["count"] == 1 and crossings["intersections"][0]["point"] == [base, 0.0] nearest = client.tool( "ocs_read", - {"session_id": session, "op": "query", "parameters": {"near": [base + 20, 0], "handles": handles, "limit": 1}}, + {"ocs_session_id": session, "op": "query", "parameters": {"near": [base + 20, 0], "handles": handles, "limit": 1}}, ) assert nearest["entities"][0]["handle"] == circle_handles[0] measured = client.tool( "ocs_read", - {"session_id": session, "op": "measure", "parameters": {"handles": circle_handles}}, + {"ocs_session_id": session, "op": "measure", "parameters": {"handles": circle_handles}}, ) assert abs(measured["measurements"][0]["curve"]["area"] - 12.566370614359172) < 1e-9 succeeded = True @@ -151,7 +151,7 @@ def main() -> None: client.tool( "ocs_execute", { - "session_id": session, + "ocs_session_id": session, "request": { "op": "batch", "request_id": f"eval-clean-{base}", diff --git a/docs/automation/mcp_smoke.py b/docs/automation/mcp_smoke.py index e28fb6867..f523b21d0 100644 --- a/docs/automation/mcp_smoke.py +++ b/docs/automation/mcp_smoke.py @@ -110,7 +110,7 @@ def modern(server: Path) -> None: "params": { "name": "ocs_execute", "arguments": { - "session_id": "missing", + "ocs_session_id": "missing", "request": {"op": "run", "request_id": "run-1"}, }, "_meta": meta, @@ -126,7 +126,7 @@ def modern(server: Path) -> None: "method": "tools/call", "params": { "name": "ocs_execute", - "arguments": {"session_id": "missing", "request": {"op": "undo"}}, + "arguments": {"ocs_session_id": "missing", "request": {"op": "undo"}}, "_meta": meta, }, }) diff --git a/src/mcp.rs b/src/mcp.rs index 887528b62..0435147a2 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -22,7 +22,7 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28"; const MAX_REQUEST: usize = 1_048_576; const MAX_RESPONSE: u64 = 16 * 1024 * 1024; const CACHE_TTL_MS: u64 = 3_600_000; -const INSTRUCTIONS: &str = "Call ocs_sessions, then ocs_read state before editing. Preserve session_id, document_id, revision, selection and request_id. Use ocs_read commands with parameters.name for a command manifest. Use ocs_execute batch when several steps are known, and request changed_entities when the resulting geometry is needed. For interactive work, call start and follow state.command.accepts, options and input_example. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. Verify important results with queries and a viewport capture, and save only to an explicit path."; +const INSTRUCTIONS: &str = "Call ocs_sessions, then pass its session_id as ocs_session_id to ocs_read, ocs_execute and ocs_capture. Preserve document_id, revision, selection and request_id. Use ocs_read commands with parameters.name for a command manifest. Use ocs_execute batch when several steps are known, and request changed_entities when the resulting geometry is needed. For interactive work, call start and follow state.command.accepts, options and input_example. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. Verify important results with queries and a viewport capture, and save only to an explicit path."; const READ_OPS: &[&str] = &[ "state", "hello", @@ -756,7 +756,7 @@ fn call_tool( Ok(Value::Array(sessions(launch)?)) } "ocs_read" => { - let session_id = required_string(arguments, "session_id")?; + let session_id = required_string(arguments, "ocs_session_id")?; let op = arguments["op"].as_str().unwrap_or("state"); if !READ_OPS.contains(&op) { return Err("Use ocs_execute for mutations".into()); @@ -769,7 +769,7 @@ fn call_tool( client(clients, session_id)?.request(Value::Object(request), 30.0) } "ocs_execute" => { - let session_id = required_string(arguments, "session_id")?; + let session_id = required_string(arguments, "ocs_session_id")?; let request = arguments["request"] .as_object() .cloned() @@ -795,7 +795,7 @@ fn call_tool( shape_execute_response(response, detail, gui) } "ocs_capture" => { - let session_id = required_string(arguments, "session_id")?; + let session_id = required_string(arguments, "ocs_session_id")?; let path = std::env::temp_dir().join(format!("ocs-capture-{}.png", random_id()?)); let scope = arguments["scope"].as_str().unwrap_or("viewport"); let max_dimension = arguments["max_dimension"].as_u64().unwrap_or(1600); @@ -944,21 +944,21 @@ fn tool_definitions() -> Value { { "name":"ocs_read", "description":"Read state, command manifests, entities, layers, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.", - "inputSchema":{"type":"object","properties":{"session_id":{"type":"string","minLength":1,"description":"Session returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name for detailed commands help."},"search":{"type":"string","description":"Case-insensitive command-name search."},"document_id":{"type":"integer","minimum":0},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity handles for query or measure."},"type":{"type":"string","description":"Entity type filter for query."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["session_id"],"additionalProperties":false}, + "inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name for detailed commands help."},"search":{"type":"string","description":"Case-insensitive command-name search."},"document_id":{"type":"integer","minimum":0},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity handles for query or measure."},"type":{"type":"string","description":"Entity type filter for query."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false}, "outputSchema":read_output_schema(), "annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false} }, { "name":"ocs_execute", "description":"Execute semantic OCS actions. Use current state fields and a unique request_id. Use run for one complete command, batch to remove round trips, or start plus input for guided steps. accepted, running and waiting_input are not completion.", - "inputSchema":{"type":"object","properties":{"session_id":{"type":"string","minLength":1,"description":"Session returned by ocs_sessions."},"request":execute_request_schema(),"wait_seconds":{"type":"number","minimum":0,"maximum":60,"default":30,"description":"Total time to wait for completion before returning."},"response_detail":{"type":"string","enum":["compact","changed_entities","full"],"default":"compact","description":"compact returns only state needed for the next edit; changed_entities also returns current geometry for changed handles; full preserves the complete editor state."}},"required":["session_id","request"],"additionalProperties":false}, + "inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"request":execute_request_schema(),"wait_seconds":{"type":"number","minimum":0,"maximum":60,"default":30,"description":"Total time to wait for completion before returning."},"response_detail":{"type":"string","enum":["compact","changed_entities","full"],"default":"compact","description":"compact returns only state needed for the next edit; changed_entities also returns current geometry for changed handles; full preserves the complete editor state."}},"required":["ocs_session_id","request"],"additionalProperties":false}, "outputSchema":execute_output_schema(), "annotations":{"title":"Execute OCS action","readOnlyHint":false,"destructiveHint":true,"idempotentHint":true,"openWorldHint":false} }, { "name":"ocs_capture", "description":"Capture the actual current OCS drawing viewport or window as a bounded PNG for visual verification.", - "inputSchema":{"type":"object","properties":{"session_id":{"type":"string","minLength":1,"description":"Session returned by ocs_sessions."},"scope":{"type":"string","enum":["viewport","window"],"default":"viewport","description":"Capture only the drawing viewport by default, or the complete application window."},"max_dimension":{"type":"integer","minimum":256,"maximum":4096,"default":1600,"description":"Resize the longest image edge to at most this many pixels."}},"required":["session_id"],"additionalProperties":false}, + "inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"scope":{"type":"string","enum":["viewport","window"],"default":"viewport","description":"Capture only the drawing viewport by default, or the complete application window."},"max_dimension":{"type":"integer","minimum":256,"maximum":4096,"default":1600,"description":"Resize the longest image edge to at most this many pixels."}},"required":["ocs_session_id"],"additionalProperties":false}, "annotations":{"title":"Capture OCS window","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false} } ]) @@ -1271,6 +1271,15 @@ mod tests { ["ocs_sessions", "ocs_read", "ocs_execute", "ocs_capture"] ); assert_eq!(tools[0]["annotations"]["readOnlyHint"], false); + for tool in [&tools[1], &tools[2], &tools[3]] { + assert!(tool["inputSchema"]["properties"] + .get("session_id") + .is_none()); + assert!(tool["inputSchema"]["required"] + .as_array() + .unwrap() + .contains(&json!("ocs_session_id"))); + } assert_eq!( tools[2]["inputSchema"]["properties"]["request"]["required"], json!(["op", "request_id"]) @@ -1368,7 +1377,7 @@ mod tests { fn read_tool_rejects_mutations_before_connecting() { let mut clients = HashMap::new(); let called = handle_message( - json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ocs_read","arguments":{"session_id":"missing","op":"save"}}}), + json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ocs_read","arguments":{"ocs_session_id":"missing","op":"save"}}}), &mut clients, &mut TaskStore::default(), ) @@ -1397,7 +1406,7 @@ mod tests { fn execute_requires_a_visible_request_id() { let mut clients = HashMap::new(); let called = handle_message( - json!({"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"ocs_execute","arguments":{"session_id":"missing","request":{"op":"undo"}}}}), + json!({"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"ocs_execute","arguments":{"ocs_session_id":"missing","request":{"op":"undo"}}}}), &mut clients, &mut TaskStore::default(), ) @@ -1415,7 +1424,7 @@ mod tests { fn execute_errors_explain_missing_operation_fields() { let mut clients = HashMap::new(); let called = handle_message( - json!({"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"ocs_execute","arguments":{"session_id":"missing","request":{"op":"run","request_id":"run-1"}}}}), + json!({"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"ocs_execute","arguments":{"ocs_session_id":"missing","request":{"op":"run","request_id":"run-1"}}}}), &mut clients, &mut TaskStore::default(), )