Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@
^LICENSE\.md$
^docs$
^\.context$
^mcp$
^\.playwright-mcp$
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ madge.svg
.claude/settings.local.json
/docs/
.context
.playwright-mcp/
7 changes: 7 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ Suggests:
markdown,
mirai,
otelsdk (>= 0.2.0),
pkgload,
processx,
ragg,
reactlog (>= 1.0.0),
rmarkdown,
Expand Down Expand Up @@ -177,6 +179,11 @@ Collate:
'insert-ui.R'
'jqueryui.R'
'knitr.R'
'mcp-app.R'
'mcp-server.R'
'mcp-session.R'
'mcp-stdio.R'
'mcp-tunnel.R'
'middleware-shiny.R'
'middleware.R'
'timer.R'
Expand Down
6 changes: 6 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export(is.reactive)
export(is.reactivevalues)
export(is.shiny.appobj)
export(is.singleton)
export(isMcpSession)
export(isRunning)
export(isTruthy)
export(isolate)
Expand All @@ -171,6 +172,11 @@ export(makeReactiveBinding)
export(markRenderFunction)
export(markdown)
export(maskReactiveContext)
export(mcpHostContext)
export(mcpRequestDisplayMode)
export(mcpSendMessage)
export(mcpToolInput)
export(mcpUpdateModelContext)
export(memoryCache)
export(modalButton)
export(modalDialog)
Expand Down
40 changes: 40 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# shiny (development version)

* Experimental support for serving a Shiny app as an MCP App (the Model
Context Protocol Apps extension, SEP-1865). Setting `options(shiny.mcp =
TRUE)` before the app starts mounts an MCP endpoint at `/mcp` on the app's
own port; MCP hosts that support the Apps extension (Claude, Claude
Desktop, VS Code Copilot, MCPJam, ...) can then call the app's tool and
render the live, fully reactive application in a sandboxed iframe, with
Shiny's websocket traffic tunneled over the host's postMessage channel.
HTTP side channels (file upload/download, server-side DataTables and
selectize, and dynamically inserted HTML dependencies) are tunneled too,
the host's theme and style variables are applied to the app, and assets
referenced by `url()` in stylesheets (e.g. icon webfonts) are embedded as
data URIs so they render inside the sandbox. Customize the model-visible
tool with `options(shiny.mcp.tool = list(name=, description=,
inputSchema=))`. New session API: `isMcpSession()`, `mcpToolInput()`,
`mcpHostContext()`, `mcpUpdateModelContext()`, and `mcpSendMessage()` let
apps react to the model's tool arguments and host theme, keep the model's
context up to date, and send messages to the conversation. Apps can also
expose additional model-callable R functions with
`options(shiny.mcp.tools = list(list(name=, description=, inputSchema=,
handler=)))`. Adding `options(shiny.mcp.stdio = TRUE)` also speaks MCP
over stdin/stdout, so local desktop hosts can launch the app process
directly, e.g. `claude mcp add my-app -- Rscript -e
"options(shiny.mcp=TRUE, shiny.mcp.stdio=TRUE);
shiny::runApp('app', launch.browser=FALSE)"` (not supported on Windows;
stdout is reserved for the protocol). On hosts that honor the declared
content security policy, the app connects over a real WebSocket for
native-latency reactivity and falls back to the tunnel automatically
(disable with `options(shiny.mcp.direct = FALSE)`). The websocket base
URL is path-aware, so apps deployed under a sub-path (e.g. Posit
Connect's `/content/<guid>`) connect directly too — derived from
`options(shiny.mcp.origin=)`, Connect's `RStudio-Connect-App-Base-Url`
(or `X-RSC-Request`) header, shinyapps.io's `X-Redx-Frontend-Name`
header, or a
deployment record next to the app (rsconnect `.dcf` or Posit Publisher
`.toml`). Apps can also request
fullscreen/picture-in-picture with `mcpRequestDisplayMode()`. Setting a
unique `options(shiny.mcp.appId=)` per app namespaces each app's
internal tools and resource URI so a gateway can merge several Shiny
apps into a single MCP server (one connector, many apps). (#4407)

* `{watcher}` is now a required dependency and is always used for autoreload file watching, so it no longer needs to be installed separately. The legacy polling-based file watcher has been removed. (#4403)

# shiny 1.14.0
Expand Down
22 changes: 21 additions & 1 deletion R/html-deps.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ createWebDependency <- function(dependency, scrubFile = TRUE) {
if (!inherits(dependency, "html_dependency"))
stop("Unexpected non-html_dependency type")

# Packages like {htmlwidgets} call createWebDependency() directly for
# render-time dependencies, bypassing processDeps(). Inside an MCP Apps
# sandbox those URLs can't be fetched, so divert file-based dependencies
# to the inline representation. (href-only dependencies fall through:
# mcpInlineDependency() would send them right back here.)
if (
!is.null(dependency$src$file) &&
is.null(dependency$src$href) &&
isMcpSession(getDefaultReactiveDomain())
) {
return(mcpInlineDependency(dependency))
}

if (is.null(dependency$src$href)) {
prefix <- paste(dependency$name, "-", dependency$version, sep = "")
addResourcePath(prefix, dependency$src$file)
Expand All @@ -46,9 +59,16 @@ processDeps <- function(tags, session) {
tags <- utils::getFromNamespace("tagify", "htmltools")(tags)
ui <- takeSingletons(tags, session$singletons, desingleton = FALSE)$ui
ui <- surroundSingletons(ui)
# Sessions running inside an MCP Apps sandbox can't fetch dependency
# files over HTTP, so inline them instead (see mcp-app.R).
processDep <- if (isMcpSession(session)) {
mcpInlineDependency
} else {
createWebDependency
}
dependencies <- lapply(
resolveDependencies(findDependencies(ui, tagify = FALSE)),
createWebDependency
processDep
)
names(dependencies) <- NULL

Expand Down
Loading
Loading