shinymcp (development version)
This version makes shinymcp about MCP Apps built from R functions and Shiny UI, and rewrites the protocol layer, the page’s JavaScript, the hosts, and the documentation. Code written for earlier versions needs changes.
Serving an existing Shiny app live, with as_mcp_app(), still works, but it is transitional: Shiny is gaining MCP support of its own (rstudio/shiny#4407), and once that is released shinymcp will stop serving live apps. The reference pages it affects have a “Shiny’s own MCP support” section.
Apps built from tools
- Tool arguments from the model are checked against declared schemas; a missing or mistyped argument, including one value where the schema asks for an array, goes back to the model as a tool error. Arguments passed from R count as a client would send them: write an array of one value as
list(x). -
mcp_tool_result()sets a result’s text and structured data. - A tool that takes a button’s id runs when the button is pressed, not whenever its other inputs change. The page doesn’t run tools annotated as changing something on its own, to fill in outputs or because an input they take changed: they run when a button they take is pressed.
-
mcp_submit_button()places the apply button formcp_app(trigger = "submit"). -
mcp_app(www = )writes a folder of scripts, stylesheets, and images into the page. A relativewwwis read from the working directory the app is made in. - An app built in an
app.Rand loaded from its directory (serve(),as_mcp_app(),mcp_endpoint()) runs its tools and builds its page in that directory, as a Shiny app’s code runs in its own. -
mcp_app(theme = )refuses a UI that is already a page, instead of nesting two pages. - Plots are drawn at the size of their output. The page tells each tool call how big its plot outputs are and how dense the screen is, and calls again when an output changes size; an
mcp_plot()without a height keeps the plot’s shape.mcp_result_plot()’swidth,height, andscaleset a size and density of their own.
Protocol and serving
- The server speaks MCP 2024-11-05 through 2025-11-25 and the stateless 2026-07-28 revision, and MCP Apps 2026-01-26. It was tested with the official MCP TypeScript client and the MCP Apps host SDK.
- New
mcp_endpoint()turns apps into a Shiny app that answers MCP at/mcp, for Posit Connect, Shiny Server, and shinyapps.io. Given a Shiny app andapps, it serves the Shiny app to browsers and the apps to chat clients, from one deployment; given a Shiny app alone, it serves that app live to chat clients and as usual to browsers. On Posit Connect, tools and server functions see the signed-in user. - The HTTP transport checks
Originand, on a local server,Host(against DNS rebinding);serve()gainsallowed_originsandallowed_hosts. - Results leave out libraries the page already has, and results for the model name large libraries instead of carrying them.
- A client that doesn’t show MCP Apps is neither told of nor allowed to call tools that only an app may call.
Hosts
- New
mcp_chat_host()hosts apps in a shinychat conversation. The model opens them with their tools; before each message the person sends, it is told what each open app reports about what it shows; a message an app suggests goes to the chat’s input box; and a restored conversation shows its apps again without calling their tools. Several chats in one session each keep to their own apps. - New
mcp_client()connects to any MCP server over HTTP, in either protocol era, keeping the_metaMCP Apps rely on. Every host takes a client where it takes an app, so a Shiny app can show apps deployed on Posit Connect, Shiny apps served with Shiny’s own MCP support, and apps written in other languages. The connection and its credentials stay in -
mcp_host_ui()andmcp_host_server()host an app in a pane of any Shiny app. The pane’s tool is called in R, andopen()calls it again with other arguments.as_shinychat_tool()makes the cards on their own. Panes and cards never hold up the session waiting on a remote server: for a remote server, callas_shinychat_tool()where the app starts, since in a session it takes the tools only from the list the client already has.McpClient$tools(wait = FALSE)returns that list without asking the server. -
mcp_host_server(),mcp_embed(),as_shinychat_tool(), andmcp_content_result()take asource, an app or anmcp_client(), where they tookapp. A pane’sexecute()takesinputs, andvalue_fn’sraw_resultis there only for apps in the same process.mcp_content_result()needs itstoolnamed for a remote server, and in a Shiny session it calls the tool first and returns a promise of the card, so the card is saved with the app’s result. - A host passes on only the requests an app’s page may make: tools visible to the app, resource reads, and
ping. -
mcp_host_server(),mcp_embed(),mcp_chat_host(),as_shinychat_tool(), andmcp_content_result()takeon_app_call, a function that sees each tool call an app’s page makes before it’s sent, to let it through, refuse it with a reason the app is given, or record who did what. It can return a promise, to ask someone first. A function that fails, or doesn’t answerTRUE,FALSE, or a reason, refuses the call. - A chat host passes the model’s arguments on as it sent them, so an array of one value reaches the app, or a remote server, as an array.
value_fngets them as parsed JSON, with arrays as lists. -
preview_app()shows the app as a chat client does, with panels for what the model receives, the context the app sends, tool calls, protocol messages, and the server’s tools.
Rewriting apps as tools
-
convert_app(),as_mcp_apps(), and the functions behind them (parse_shiny_app(),analyze_reactive_graph(), andgenerate_mcp_app()) are removed. Their drafts guessed at an app’s structure from its code and still left every tool to be written.vignette("rewriting-as-tools")and the bundled skill take a person or a coding agent through the rewrite instead. -
mcp_tool_module()serves a module’s UI with its server function, or with ahandlerfunction in its place.
Serving Shiny apps as they are
- The model’s tool takes the app’s inputs as arguments, described from the UI (select inputs as choices, sliders as bounded numbers, dates as dates, tabsets as the tab to show). Its result reports what each output shows, with tables (DT’s included) as rows and plots as images. Passing
viewchanges a view that is already open.bindMcp()narrows what the model sees. - New helpers for server functions:
mcp_model_context()andmcp_send_message()to reach the model,mcp_host_context()for the client’s theme and display mode,mcp_request()for the caller (and the signed-in user on Posit Connect), andis_mcp_session(). - The app starts as
shiny::runApp()would start it:global.R, the files inR/, andonStartrun before the UI is built, the app’s code runs in its directory, andonStopruns when the server stops. Files the UI loads fromwww/oraddResourcePath()paths are written into the page. - Sessions are limited by the
shinymcp.max_viewsandshinymcp.view_timeoutoptions (seehelp("shinymcp-options")). A view whose session is gone starts a new one from the page’s inputs, whether the page was sending a change, downloading a file, or fetching a table’s rows, and the page then shows what the new session shows. - An error in an observer ends the view’s session, as it ends a browser’s. The model, or the page, gets the error, and the page’s next change starts a new session.
What works on a live app’s page
- Packages written for Shiny’s JavaScript work, because the page provides
window.Shiny: input bindings (shinyWidgets, bslib’s sidebars, accordions, switches, and task buttons),Shiny.setInputValue()(DT row selection,plotly::event_data(), leaflet events), custom message handlers (shinyjs), and theshiny:*events (shinycssloaders). -
conditionalPanel()works. Chat clients forbideval(), so shinymcp reads conditions itself; it understands comparisons, logic, arithmetic,input.x,output.x,.length, regular expressions, and common string and array methods. - Clicks, double clicks, hovers, and brushes on plots send what Shiny’s client sends, so
nearPoints()andbrushedPoints()work. -
renderUI(),insertUI(), modals, notifications, downloads, andinsertTab(),removeTab(),hideTab(), andshowTab()reach the page. -
fileInput()uploads work, withinshiny.maxRequestSize. -
updateSelectizeInput(server = TRUE)works: the page shows the first 1,000 choices, with a search box for the rest. -
invalidateLater()andreactivePoll()run while the app is open, and anExtendedTask’s result appears when the task finishes. -
varSelectInput()values arrive as symbols, and outputs set to areactive()work. - Leaflet’s default markers show. The leaflet package loads their images from a CDN, which chat clients block; the page carries them instead.
