Conversation
* feat(mcp): JSON-RPC endpoint skeleton for MCP Apps
* feat(mcp): mount /mcp handler in app handler chain when enabled
* feat(mcp): postMessage tunnel connection and _shiny_* tools
* feat(mcp): ui://shiny/app single-file resource with inlined assets
* feat(mcp): postMessage bridge bundle overriding Shiny.createSocket
* fix(mcp): use .globals for sweeper flag (assignInMyNamespace fails in later callback)
* docs(mcp): NEWS entry for experimental MCP Apps support
* docs(mcp): mirror MCP Apps plans, research notes, and demo app into mcp/
* feat(mcp): _shiny_http tool tunnels HTTP side channels in-process
* feat(mcp): inline dynamic htmlDependencies for MCP tunnel sessions
* feat(mcp): tunnel XHR side channels, downloads, host theme + context (Phase 2 client)
* docs(mcp): Phase 2 NEWS + mirror plan/demo/screenshot into mcp/
* feat(mcp): inline CSS url() assets as data URIs for the sandbox
* feat(mcp): exported session API — mcpToolInput, mcpUpdateModelContext, mcpSendMessage, mcpHostContext, isMcpSession
* feat(mcp): author-declared MCP tools via options(shiny.mcp.tools)
* feat(mcp): stdio transport for local desktop hosts
* feat(mcp): direct-connect WebSocket fast path + display mode requests
* docs(mcp): direct-connect/display-mode NEWS, mcptools upstream notes, QUESTIONS.md
* docs(mcp): use real PR number in NEWS
* docs(mcp): record decisions in QUESTIONS.md; explain direct-connect default
* fix(mcp): declare processx/pkgload test deps; run stdio subprocess against installed shiny under R CMD check
* feat(mcp): path-aware direct-connect base (shiny.mcp.origin, X-RSC-Request, rsconnect records)
* feat(mcp): discover deployed URL from Quarto _publish.yml and Posit Publisher records
* fix: pkgdown index for mcp-session; wrap-tolerant reactlog Suggests test; drop Quarto _publish.yml source
* `npm run build` (GitHub Actions)
* fix(mcp): use RStudio-Connect-App-Base-Url for the direct-connect base
Verified against a real connect.posit.it deployment: Connect does not
send X-RSC-Request to Shiny content, and rsconnect excludes its own
rsconnect/ records from the bundle. The header Connect actually sends
with every proxied request is RStudio-Connect-App-Base-Url, which is
already the app's external base URL. Honor it first (X-RSC-Request kept
for API content).
* feat(mcp): shinyapps.io direct-connect base via X-Redx-Frontend-Name
Verified against a real shinyapps.io deployment: the proxy passes
POST /mcp through unauthenticated, sends RStudio-Connect-App-Base-Url
with an EMPTY value, and identifies the external base as
X-Redx-Frontend-Name (schemeless host + path). Combine that with
X-Forwarded-Proto for the direct base, and prefer X-Forwarded-Host
over the internal Host for the generic origin fallback and
deployment-record host matching.
* feat(mcp): appId namespacing so a gateway can serve multiple apps
options(shiny.mcp.appId=) prefixes the internal _shiny_* tunnel tools
(demo_shiny_connect, ...) and publishes the UI resource as
ui://shiny/<appId>; the bridge reads appId from __shinyMcpConfig__ and
prefixes its tool calls to match. mcp/gateway/shiny-mcp-gateway.mjs
(zero-dep Node, stdio or HTTP) merges several app endpoints into one MCP
server, so a single connector exposes many Shiny apps. E2E verified in
basic-host with two apps behind one gateway, including full tunnel
reactivity.
* docs(mcp): add mcp/architecture.md describing the system as built
* docs(mcp): compare with JamesHWade/shinymcp
* fix(mcp/gateway): cookie stickiness, cold-start wake retry, drain on close
shinyapps.io rejects POST to a sleeping app with 405 (only GET
cold-starts an instance) and pins clients to an instance via a session
cookie. The gateway now keeps a per-backend cookie jar, and on a failed
POST wakes the app with a GET to its base URL and retries with backoff.
Also drain in-flight requests before exiting when stdin closes.
* chore(mcp): commit demo-app deployment records (no secrets; exercise the DCF direct-base path)
* fix(mcp): inline render-time deps from direct createWebDependency() calls
{htmlwidgets} (and friends) call createWebDependency() directly for the
dependencies attached to a widget's render payload, bypassing the
processDeps() MCP hook — so a dynamically shown plotly widget arrived
with URL-based crosstalk/CSS deps that 404 inside the sandbox
("crosstalk is not defined"). createWebDependency() now diverts
file-based deps to mcpInlineDependency() when the current reactive
domain is an MCP session; href-only deps fall through to avoid
recursion. Demo: cars app gains a checkbox-gated plotly plot,
E2E-verified in basic-host (Plotly + crosstalk load on demand, 32
points rendered).
* test(mcp): write download bytes with writeBin for Windows
writeLines() opens the file path in text mode, so on Windows the CSV's
\n bytes became \r\n and the _shiny_http download body assertion failed
(only Windows R-CMD-check was red). writeBin(charToRaw(...)) writes the
exact bytes on every platform.
* chore: ignore .playwright-mcp scratch dir (git + R build)
---------
Co-authored-by: schloerke <schloerke@users.noreply.github.com>
…() (#4412) Replace options(shiny.mcp.tools=) with registerMcpTool(), accepting ellmer::tool() objects; convert to MCP tools/list schema via the mcptools as_json() path and invoke on tools/call with rlang::exec(). ellmer -> Suggests. ToolDef props read via S7::prop() for R < 4.3. Docs, demos, and mcp/ architecture updated.
* docs(mcp): design spec for mcpConfigure() replacing shiny.mcp.* options * docs(mcp): implementation plan for mcpConfigure() * feat(mcp): add mcpConfigure() with validation and .globals$mcp storage * refactor(mcp): read config from .globals$mcp instead of options() * refactor(mcp): rename mcpToolInput() to mcpUpdates() * test(mcp): drive config via mcpConfigure() instead of options() * docs(mcp): document mcpConfigure()/mcpUpdates(), update demos and NEWS * docs(mcp): update stdio Windows warning to reference mcpConfigure() * docs(mcp): spike notes for init-args-via-RestoreContext * docs(mcp): correct Unknown-3 verdict in spike notes * feat(mcp): restore init arguments via an active RestoreContext * feat(mcp): bridge encodes init args into restore, updates via mcpUpdates() * feat(mcp): filter model arguments to the declared allow-list * fix(mcp): inject init restore args into the init frame so RestoreContext activates * docs(mcp): rename demo 'ti' var to 'args' (post mcpUpdates rename) * feat(mcp): deliver model args via single mcpUpdates() channel; drop RestoreContext 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. * docs(mcp): fold config-API change into the umbrella NEWS entry (unreleased) * docs(mcp): add clock demo (demo-app3) + stdio/TCC-path notes for Claude Desktop * docs(mcp): list mcpConfigure() in pkgdown reference (fix routine CI check)
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.
…_app (#4415) (#4418) * docs(mcp): spec for in-place update of running Shiny MCP App (#4415) * docs(mcp): implementation plan for in-place update of running MCP App (#4415) * feat(mcp): publish update_<appId>_app tool when arguments declared (#4415) * feat(mcp): mcpUpdates() overlays server-pushed args over init args (#4415) * feat(mcp): dispatch update_<appId>_app to push args into a live session (#4415) * feat(mcp): stamp session token into mcpUpdateModelContext payload (#4415) * feat(mcp): announce session token to the model on connect (#4415) * docs(mcp): document update_<appId>_app + update demos (#4415) * fix(mcp): accumulate successive update pushes; review cleanups (#4415) * docs(mcp): mark host-passthrough + multi-instance confirmed in Claude Desktop (#4415)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integration branch
mcp→mainfor the experimental MCP Apps support. The feature was built and reviewed across several PRs squash-merged intomcp(#4407 initial support, #4412registerMcpTool(), #4414mcpConfigure(), #4418 in-placeupdate_<appId>_app); this PR promotes the accumulated work tomain.Summary
Adds experimental support for serving a Shiny app as an MCP App (SEP-1865): calling
mcpConfigure()before the app starts mounts a Model Context Protocol endpoint at/mcp, and the app renders live and reactive inside an MCP host's conversation (Claude Desktop, claude.ai, VS Code Copilot, etc.).How it works, in brief:
mcpConfigure(appId=, description=, arguments=, direct=, displayModes=, origin=, stdio=, enabled=)is the single entry point — an app is served over MCP only when it calls it. (Replaces the originaloptions(shiny.mcp.*)interface.)mcpConfigure(stdio = TRUE)), both through one dispatcher.ui://shiny/<appId>resource is the rendered page as a single self-contained HTML document (assets inlined, CSSurl()→data:URIs). A JS bridge overridesShiny.createSocket: it opens a real WebSocket where the host honors the declared CSP (direct fast path), otherwise it tunnels Shiny's websocket overtools/call(long-poll for server→client). HTTP side channels (upload/download, server-side selectize/DataTables, dynamically inserted htmlDependencies) tunnel through an app-only_shiny_httptool.open_<appId>_app, with an input schema declared viamcpConfigure(arguments = list(...))(ellmer types). Arguments reach the app through the reactivemcpUpdates()channel (a single observer handles both the initial open and later updates). Whenargumentsare declared, a companionupdate_<appId>_apptool is auto-registered so the model can update an already-running instance in place (targeting a specific session by the token the app announces on connect) instead of re-opening it (MCP Apps: tool to update an already-running app in place (auto-registered update_<appId>_app) #4415/feat(mcp): update a running Shiny MCP App in place via update_<appId>_app (#4415) #4418).registerMcpTool(ellmer::tool(...))exposes plain R functions as model-callable MCP tools that run server-side, alongside the app-opening tool.isMcpSession(),mcpUpdates(),mcpHostContext(),mcpUpdateModelContext(),mcpSendMessage(),mcpRequestDisplayMode().RStudio-Connect-App-Base-Url) and shinyapps.io (X-Redx-Frontend-Name), falling back to deployment records / request origin.mcpConfigure(appId = )namespaces each app's internal tools and resource URI; a small Node gateway (mcp/gateway/) merges several app endpoints into a single MCP server.Design and working docs (excluded from the package build) live under
mcp/— seemcp/architecture.mdfor the full "system as built" write-up.Verification
R CMD checkmatrix green (all Ubuntu 4.2→devel, macOS, Windows), plusroutineandpkgdown.claude/Claude Desktop.update_<appId>_app) verified end-to-end in Claude Desktop, including the multi-instance case (updating a specific open instance by its announced session token).Known open item
Experimental (
lifecycle::badge("experimental")); an app is never served over MCP unless it callsmcpConfigure().