ifURI examples
← all examples

29 — the desktop as MCP tools: an LLM drives it with native tool-calling

Example 28 used a custom (goal, space) -> steps planner. This one removes the planner entirely: urirun projects the noVNC connector's routes to MCP tools (name + inputSchema), the model is handed those tools, and it decides which to call and with what arguments via native tool-calling — a decision loop where the next call depends on the previous result, exactly like a Claude/MCP client.

desktop connector routes ──► MCP tools (name + JSON Schema)  [zero extra code]
        │
        ▼
   tool-calling loop:  model → tool_call → urirun executes on the desktop → result → model → …
        │
        ▼
   generated/mcp-session-report.md  +  screenshot  +  verdict

Run it

./mcp_serve.sh                         # show the MCP tools/list + A2A card (no LLM)
python3 mcp_agent.py                    # the model drives the desktop via tool-calling

mcp_agent.py needs Docker and a tool-capable model (default openrouter/openai/gpt-4o-mini; override with URIRUN_MCP_MODEL). The configured LLM_MODEL in urirun/.env is an image model and does not support tool use — hence the separate default here.

What actually happened (recorded live)

NL goal: *"On the desktop, open a terminal and run a command that prints 'mcp drove this desktop', then take a screenshot. Stop the session when done."* — the model called the tools itself, filling each call's arguments from the tool's JSON Schema:

#tool → uriokarguments (model-filled)
0desktop_novnc_session_command_start…/session/command/start{}
1desktop_novnc_app_command_launch…/app/command/launch{"command": "lxterminal"}
2desktop_novnc_input_command_type…/input/command/type{"text": "echo 'mcp drove this desktop'", "enter": true}
3desktop_novnc_screen_query_screenshot…/screen/query/screenshot{"name": "screenshot"}
4desktop_novnc_session_command_stop…/session/command/stop{}

!result

The terminal shows echo 'mcp drove this desktop'mcp drove this desktop. Intention realized: YES — every tool call succeeded and the screenshot was captured. Full record: generated/mcp-session-report.md + .json.

Why this is the simplest LLM adaptation

There is no planner and no prompt engineering of a plan format — the model uses its built-in tool-calling. urirun already supplies the only thing MCP needs: each route as a tool with a JSON-Schema parameters object (v2_mcp.to_mcp_tools). So:

zero-code surface. Point any MCP client (Claude Desktop, etc.) at python3 -m urirun.v2_mcp serve <registry> --execute and it can call these tools.

function tools → execute each call with v2_mcp.call_tool (which goes through the same policy-gated urirun.run).

MCP tool names carry the operation

v2_mcp.tool_name builds the name from all URI path segments, so the operation is always in the name: …/session/command/startdesktop_novnc_session_command_start, …/screen/query/screenshotdesktop_novnc_screen_query_screenshot. (Earlier it used only resource+verb and dropped the operation — that was fixed alongside this example.) Combined with each tool's description (the route label) and JSON-Schema parameters, tool selection is unambiguous.

Core vs connector (unchanged from 28)

Nothing new in core was needed for this example — the MCP projection (v2_mcp.to_mcp_tools / serve_mcp) and the schema-in-action-space already existed. The desktop driver stays a connector; core just *describes and routes* it.

Files

Files

.gitignoreREADME.mdmcp_agent.pymcp_serve.shtest_mcp_agent.py.benchmarks/.ruff_cache/docs/

View on GitHub →