An MCP App is an interactive page that an AI chat client shows inside the conversation. The page belongs to an MCP server, which also offers tools the model can call. When the model calls a tool that has a page, the client shows the page with the result, and the person can keep working with it.
This article builds a small app from one tool and takes it from your R session to Claude Desktop.
A tool and a page
The tool is an R function. The model will call it with arguments, so describe it and them for the model:
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)
)The page is Shiny UI. mcp_app() puts the two
together:
app <- mcp_app(
ui = fluidPage(
titlePanel("Old Faithful eruptions"),
sliderInput("bins", "Number of bins", min = 5, max = 50, value = 20),
plotOutput("histogram"),
textOutput("caption")
),
tools = list(faithful_histogram),
name = "faithful",
title = "Old Faithful"
)
app
#> <McpApp> faithful 0.1.0
#> Title: Old Faithful
#> UI resource: ui://faithful
#> Tools:
#> * faithful_histogramNames connect the tool and the page. The argument bins
takes its value from the input with id bins, and the list
the tool returns fills the outputs histogram and
caption. When the person moves the slider, the page calls
the tool with the new value and draws what it returns. There is no
server function: every call stands alone, and nothing is kept in R
between calls.
Preview it
preview_app(app)preview_app() starts a local MCP server for the app and
opens a page that plays the part of a chat client. It calls the tool the
way a model would and shows the app in the same kind of sandboxed frame
a client uses, under the same content security policy. Next to the app
it shows:
- Model: what the model received from the call.
- Context: what the app told the model after the person changed something.
- Calls: every tool call, from the model and from the page.
- Messages: the protocol messages between the page and the host.
- Server: the tools and resources the server offers.
Use the arguments box to call the tool again with other values, and the theme and width menus to see the app in a dark chat or a narrow one.
What the model sees
$run_tool() returns what an MCP client receives:
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 text is written for the model, and for clients that can’t show the page. The result also carries:
-
structuredContent, one value per output:str(result$structuredContent) #> List of 2 #> $ histogram: chr "Histogram of waiting times between eruptions." #> $ caption : chr "272 eruptions; median wait 76 minutes." an image of the plot, so a model that reads images can see it;
in
_meta, everything the page needs to draw the outputs. Clients pass this to the page and keep it from the model.
$call_tool() returns the tool’s R value instead, which
is what you want in tests.
Put it in Claude Desktop
A chat client runs your app as a program. Save the app in a file that
ends with serve():
# ~/apps/faithful/app.R
library(shiny)
library(shinymcp)
faithful_histogram <- ellmer::tool(...)
app <- mcp_app(
ui = fluidPage(...),
tools = list(faithful_histogram),
name = "faithful",
title = "Old Faithful"
)
serve(app)serve() speaks MCP on standard input and output. Test
the file in a terminal first: Rscript ~/apps/faithful/app.R
should sit waiting for input (press Ctrl+C to stop it).
Then add it in Claude Desktop under Settings > Developer > Edit Config:
{
"mcpServers": {
"faithful": {
"command": "Rscript",
"args": ["/Users/you/apps/faithful/app.R"]
}
}
}Use full paths: Claude Desktop doesn’t start in your home directory
and, on macOS, doesn’t see your shell’s PATH, so
Rscript may need its full path too
(which Rscript prints it). Restart Claude Desktop and ask
it to “show the Old Faithful waiting times with 30 bins”.
vignette("deployment") covers other clients, HTTP, and
Posit Connect.
Next steps
-
vignette("tools")covers inputs, outputs, and results in more depth, and tools only the page may call. -
vignette("hosting")shows apps, yours or any MCP server’s, in shinychat conversations and Shiny dashboards. -
vignette("rewriting-as-tools")turns a Shiny app you already have into an app like this one.
