Skip to content

Interactive elements: migrate to mcp 2.x, then MRTR, then one MCP App #126

Description

@rutgerhofste

Working checklist for ADR 0004. The ADR is the decision, this is the ticking.

Order is a gate, not a preference: phase 1 unblocks 2 and 3, and phase 3 only starts if the last box of phase 0 comes back yes.

Status 2026-09-05: all three phases are on main and released as 3.1.0 (e7838fe, #130). What is left needs a hand outside this session: the tag, the admin pin, and the one real-customer test that decides whether the app widens.

Left to do, in order

  • Tag v3.1.0 on e7838fe. The remote session cannot push tags (403 from the proxy); from any checkout: git fetch origin main && git tag -a v3.1.0 e7838fe4d1b760e118e48e7ffdfee1eb797f1a80 -m "v3.1.0 -- interactive elements" && git push origin v3.1.0. (3.0.0 was released at f26966a but never tagged; 3.1.0 carries everything, so one tag is enough.)
  • odoo-mcp-pro-admin: bump the git pin to v3.1.0, confirm the create_fastmcp_app call site and the streamable_http_app(**STREAMABLE_HTTP_OPTIONS) call, staging before prod. Done 2026-09-05: pantalytics/odoo-mcp-pro-admin#217, merged as 8035839, live on staging and prod. create_fastmcp_app survived unchanged; stateless_http/json_response moved onto streamable_http_app() together with host and transport_security (app.settings no longer carries those three, log_level is still there). FastMCP -> MCPServer in two modules and one test. The admin registers no tool overrides, so OdooToolHandler.tool() needed nothing. Verified in the deployed prod container: 23 tools, register_payment among them, public SERVER_VERSION 3.1.0
  • Smoke stdio against a live Odoo 19 (no Odoo in the session that did the migration)
  • Verify in Claude on Odoo 19: backorder confirm and register payment through the 2026-07-28 form, both round trips, and check which protocol version claude.ai negotiates today
  • Try the register-payment app in Claude with one real customer on Odoo 19
  • Decide from that: widen to the other four wizards, or stop and stay on MRTR

Phase 0 -- answer before committing (half a day)

Answered 2026-09-05 from the installed mcp 2.1.1, not from the docs.

  • Read the v1 -> v2 migration guide end to end, note every item touching our seams
  • Where do server instructions live now that the initialize handshake is gone? Still a constructor argument of MCPServer; delivered via server/discover. Verified the client receives them.
  • What replaces auth= and token_verifier= on the new server class? Nothing: same names, same position on MCPServer. The admin call site should survive unchanged.
  • Are stateless_http and json_response still parameters, or the default now that sessions are gone? Still parameters, but of streamable_http_app() / run_streamable_http_async(), both defaulting to False. Hence STREAMABLE_HTTP_OPTIONS.
  • Gate for phase 3: can a Python server declare _meta.ui.resourceUri on a tool and serve a ui:// resource at all? Yes. mcp.server.apps.Apps is in the SDK: @apps.tool(resource_uri="ui://..."), add_html_resource(uri, html, csp=..., permissions=...), passed as MCPServer(extensions=[apps]). Also client_supports_apps(ctx) for the text fallback SEP-2133 requires.

Found on the way, not in the list: on 2.x only a ToolError / ResourceError carries its message to the model. Handled in phase 1 (as_tool_error / as_resource_error).

Phase 1 -- the SDK v2 migration

PR #125 (merged), release #127 (merged, 3.0.0).

  • Bump the pin: mcp>=1.27.2,<2 -> mcp>=2.1.1,<3. mcp-types comes along as an exact-pinned dependency, no separate install
  • server.py: FastMCP -> MCPServer. create_fastmcp_app keeps its exported name
  • Same import swap in tools/handler.py, resources/handler.py, skills.py
  • mcp.types -> no change needed: it survives in 2.x as a mirror of mcp_types
  • McpError -> MCPError: we never imported the SDK's; our own error_handling.MCPError is unrelated and untouched. ADR 0003's contract is unaffected
  • snake_case sweep: 68 ToolAnnotations kwargs in 8 files. No model_dump of protocol models in our code
  • TransportSecuritySettings still at mcp.server.transport_security, now passed to streamable_http_app() instead of set on settings
  • 9 test files importing mcp.server.fastmcp, plus test_server_foundation (settings object gone) and test_tools_crud (registered functions raise ToolError with ours as __cause__)
  • Full suite green: 579 passed, same as baseline. ruff, check_max_lines.py clean. ty from 23 to 2 pre-existing warnings
  • Smoke both transports: in-process and over streamable-http against run_http, with the real 2.x client, protocol 2026-07-28 negotiated, error text identical to 1.x
  • Release commit 3.0.0 merged (release: 3.0.0 #127, f26966a)

Phase 2 -- MRTR

PR #128 (merged). Design note from phase 0 held up and is pinned by a test: the SDK rejects an InputRequiredResult as an invalid tools/call result for a pre-2026-07-28 client, so the two-step decision / followup is not only the deprecation-window fallback, it is the only thing an older client can use. Gated on ctx.protocol_version and the client's form-elicitation capability.

  • tools/methods.py: return the SDK's InputRequiredResult instead of the hand-rolled followup dict, when the client can answer it (tools/input_required.py)
  • Keep accepting the current decision shape, and the followup dict for older clients; a test per shape (tests/test_execute_method_input_required.py, four in-process round trips over the real 2.x client)
  • tools/wizards.py schemas unchanged: the same primitive-only pydantic models render straight into the elicitation schema (render_elicitation_schema)
  • Declining the form: result_kind: declined, nothing touched, the method is not even re-run
  • Not asked as a form on purpose: a chained second wizard. The retry re-runs the method and gets the first wizard back, so the answer would land on the wrong one. Stays the dict.

Phase 3 -- exactly one MCP App

PR #129 (merged). Read-side apps (search results as tables, dashboards) are dropped on purpose.

  • ui://odoo/register-payment.html via Apps.add_html_resource, one document, served as text/html;profile=mcp-app; the wheel carries it
  • The tool: register_payment (= execute_method fixed to account.move.action_register_payment), bound through _meta.ui.resourceUri. An Apps client gets the followup dict the form reads; a client without Apps gets execute_method's behaviour, dict or 2026-07-28 form (client_supports_apps)
  • CSP: every asset inline, no external origin (pinned by a test), so the host's default CSP is enough
  • The form completes the wizard by calling register_payment back from inside the iframe, then ui/update-model-context so the model learns the outcome. Verified in headless Chromium against a fake host, including the one thing the docs do not say: a sandbox without allow-forms blocks native form submission, so the submit is a click handler on a type=button

Release

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions