shinymcp builds MCP Apps in R. An MCP App is a tool with a page: a chat client that supports the MCP Apps extension, such as Claude, ChatGPT, or VS Code, shows the page in the conversation, and the person can keep working with it. In shinymcp the tools are R functions and the page is Shiny UI. The model can call the tools on their own, in any MCP client, and the page calls the same tools as the person uses it.
# install.packages("pak")
pak::pak("JamesHWade/shinymcp")library(shiny)
library(shinymcp)
faithful_histogram <- ellmer::tool(
function(bins = 20) {
list(
histogram = mcp_result_plot(
function() hist(faithful$waiting, breaks = bins, main = NULL, xlab = "Minutes"),
text = "Histogram of waiting times between eruptions."
),
caption = sprintf("%d eruptions; median wait %d minutes.", nrow(faithful), median(faithful$waiting))
)
},
name = "faithful_histogram",
description = "Show a histogram of waiting times between Old Faithful eruptions.",
arguments = list(
bins = ellmer::type_integer("Number of bins, 5 to 50.", required = FALSE)
),
annotations = ellmer::tool_annotations(read_only_hint = TRUE)
)
app <- mcp_app(
ui = fluidPage(
sliderInput("bins", "Number of bins", min = 5, max = 50, value = 20),
plotOutput("histogram"),
textOutput("caption")
),
tools = list(faithful_histogram),
name = "faithful"
)Names connect the two halves. The tool’s argument bins takes its value
from the input with that id, and the list it returns fills the outputs
histogram and caption. When the person moves the slider, the page
calls the tool again.
The model calls the same tool, and reads what it returns, whether or not its client can show the page:
result <- app$run_tool("faithful_histogram", list(bins = 30))
cat(result$content[[1]]$text)
#> histogram: Histogram of waiting times between eruptions.
#>
#> caption: 272 eruptions; median wait 76 minutes.The plot reaches it as an image, too. Each result is split three ways: text and compact structured data for the model, and what the page draws (HTML, images, widget data), which clients pass to the page and keep from the model. Results can hold text, tables, plots, images, HTML, and htmlwidgets.
preview_app(app) opens the app in your browser the way a chat client
shows it, next to what the model receives from each call.
Save the app in a file that ends with serve(app) and register the file
with your client. For Claude Desktop, in Settings > Developer > Edit
Config:
{
"mcpServers": {
"faithful": {
"command": "Rscript",
"args": ["/path/to/app.R"]
}
}
}Other ways to run an app:
serve(app, type = "http")serves it over HTTP, for clients that connect to a URL.mcp_endpoint(app)makes it a Shiny app that serves MCP at/mcp, to deploy on Posit Connect, Shiny Server, or shinyapps.io. Apps built from tools keep nothing between calls, so they run in as many processes as the server starts.
shinymcp also does the chat client’s part, in Shiny. mcp_chat_host()
puts apps in a shinychat
conversation: the model opens them, and what the person does in them
reaches the model with their next message. mcp_host_ui() and
mcp_host_server() put an app in a pane of any Shiny app. The apps can
be built with shinymcp, or come from any MCP server through
mcp_client(), including Shiny apps served with Shiny’s own MCP
support.
The page is the UI you give mcp_app(), with shinymcp’s JavaScript in
place of Shiny’s. It takes Shiny’s inputs and bslib’s, inputs from
packages that register a Shiny input binding (shinyWidgets, for
example), and the small mcp_*() components shinymcp provides. It draws
every kind of output: text, tables, plots, HTML, and htmlwidgets such as
plotly, DT, and leaflet. Conditional panels show and hide, buttons run
the tools that take their id, and the page tells the model what the
person chose.
Anything from another website, such as map tiles or a script from a CDN,
needs a csp declaration: chat clients block the rest.
There are two ways to put an existing Shiny app in a chat:
- Rewrite the part the model should use as tools. The UI stays as it is; the server function becomes R functions. See Rewriting a Shiny app as tools.
- Serve the whole app as it is with
as_mcp_app(). Its server function keeps running in R, one session each time the app opens. Shiny is gaining MCP support of its own (rstudio/shiny#4407). Once it is released, it will be the way to do this, and shinymcp will stop serving live apps. See Serving a Shiny app as it is.
shinymcp comes with a
skill
that takes a coding agent through either one. For Claude Code, copy its
folder,
system.file("skills", "convert-shiny-app", package = "shinymcp"), into
~/.claude/skills/.
- Get started: a first app, from preview to Claude Desktop.
- Building an app from tools: inputs, outputs, results, and tools only the page may call.
- Running an app: chat clients, HTTP, and Posit Connect.
- Hosting MCP Apps in Shiny: in a shinychat conversation or a pane, from any MCP server.
- Rewriting a Shiny app as tools.
- Serving a Shiny app as it is.
- How shinymcp works.
- Troubleshooting.
The examples folder has runnable apps for each of these.

