Skip to content

feat(mcp): replace shiny.mcp.* options with mcpConfigure()#4414

Merged
schloerke merged 19 commits into
mcpfrom
schloerke/replace-shiny-mcp-options
Jul 17, 2026
Merged

feat(mcp): replace shiny.mcp.* options with mcpConfigure()#4414
schloerke merged 19 commits into
mcpfrom
schloerke/replace-shiny-mcp-options

Conversation

@schloerke

Copy link
Copy Markdown
Collaborator

Summary

Replaces the seven options(shiny.mcp.*) MCP App options with a single validated mcpConfigure() function backed by .globals$mcp, tightening how an app opts into and configures MCP support. Also renames the session helper mcpToolInput()mcpUpdates(), and routes the model's app-opening arguments through Shiny's bookmark RestoreContext so inputs initialize flash-free, while post-init argument updates flow through the reactive mcpUpdates() channel.

Motivation: options() is an unregulated, process-global namespace (no validation, no discoverability, silent typos, cross-app leakage). mcpConfigure() is one gated, documented, autocomplete-friendly entry point.

Public API

mcpConfigure(
  appId        = NULL,   # unique id for gateway merging; derives tool name + resource URI
  description  = NULL,   # what the model reads to decide whether to open the app
  arguments    = NULL,   # named list of ellmer type_*() objects — the model-settable allow-list
  direct       = TRUE,
  displayModes = c("inline", "fullscreen", "pip"),
  origin       = NULL,
  stdio        = FALSE,
  enabled      = TRUE    # LAST: an app is served over MCP only if it calls mcpConfigure()
)
  • Opt-in safeguard: no mcpConfigure() call ⇒ MCP is never served. Invalid appId now errors (was warn-and-ignore).
  • Tool name derived from appId (open_shiny_app / open_<appId>_app) — drops the manual tool$name knob; gateway uniqueness is automatic.
  • arguments uses ellmer types (consistent with registerMcpTool()), published as the tool inputSchema and enforced as an allow-list on both the init and update paths.

Argument delivery

  • Init → the model's opening arguments are filtered to the declared allow-list, injected into the init frame, and applied via Shiny's RestoreContext (standard inputs auto-restore via restoreInput(), no flash; onRestore() covers non-input state). Works without the app enabling bookmarking (the restore-callback observers were extracted into createRestoreObservers(); bookmarking behavior is unchanged).
  • After-init → live re-steering flows through the reactive mcpUpdates(), wired by the author with updateXxxInput().

Notes

  • Experimental/unreleased feature: clean removal of options(shiny.mcp.*), no deprecation shims. The three sendCustomMessage channel names (shiny.mcp.updateModelContext/sendMessage/requestDisplayMode) are protocol channels, not options, and are unchanged.
  • Design spec, plan, and spike notes are in mcp/ (design-mcpConfigure.md, plan-mcpConfigure.md, notes-restore-spike.md).

Testing

  • MCP suite: 246 pass / 0 warn / 1 CRAN-skip. Bookmark suite: 54 pass (regression check — extraction is behavior-preserving). JS bridge: 27 pass (incl. init-restore injection seam tests). Full devtools::test(): 3334 pass (14 pre-existing, unrelated test-zzz-st2-download.R failures).
  • NEWS.md bullet references PR #XXXX — replace with this PR number before merge.

🤖 Built with subagent-driven development (9 planned tasks + 1 final-review fix, each independently reviewed).

…estoreContext init path

Real-host testing (Claude Desktop) showed init-arg widget auto-restore is
unreachable (UI renders at resources/read before args exist), hosts re-render
per tool call, and the restore-vs-mcpUpdates split raced. Route every
ontoolinput to mcpUpdates(); remove createRestoreObservers, the server.R MCP
restore branch, the JS init-frame injection + socket-start gate, encodeMcpRestore,
and mcpFilterRestore. Document the constraints in mcp/limitations.md.
@schloerke
schloerke merged commit de085d1 into mcp Jul 17, 2026
12 checks passed
@schloerke
schloerke deleted the schloerke/replace-shiny-mcp-options branch July 17, 2026 17:51
schloerke added a commit that referenced this pull request Jul 17, 2026
The @param arguments roxygen still claimed init args could be read 'via
input/bookmark restoration (init)'. That auto-restore path was proven
unworkable for MCP (the UI renders at resources/read before args exist)
and removed in #4414. Describe the real mechanism instead: a single
mcpUpdates() observer applying updateXxxInput(), with the unavoidable
open flash noted. Handles the leftover doc correction deferred as a
non-goal from #4415's design spec.

Regenerated man/mcpConfigure.Rd.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant