Most problems are easier to see in preview_app() than in
a chat client. It shows the app as clients do, under the same Content
Security Policy, and next to it what the model received, every tool call
with its result, the protocol messages, and the server’s tools. The
browser’s developer console shows the page’s own errors; the preview’s
page is in an iframe, so choose it in the console’s context menu.
In R, app$run_tool("name", list(...)) returns exactly
what a client would receive, and app$call_tool() the tool’s
R value.
The client doesn’t list the app’s tools
The client couldn’t start the server, or the server stopped.
- Run the command the client runs,
Rscript /full/path/to/app.R, in a terminal. It should wait for input without printing anything (Ctrl+C stops it). An error here is the one the client hit. - Use full paths in the client’s settings. Desktop clients don’t start
in your home directory, and on macOS they don’t see your shell’s
PATH, soRscriptmay need its full path too (which Rscript). - The packages the app uses must be installed in the library that
Rscriptuses. With renv, point the client at the project’s library or activate it in the script. - Nothing may print to standard output before
serve()starts: the protocol runs over it. Messages and warnings go to standard error and are fine. - Check the client’s log. Claude Desktop keeps one for each server, on
macOS in
~/Library/Logs/Claude/.
The tools work but the app doesn’t show
The client doesn’t support MCP Apps, or not in this context. Terminal clients such as Claude Code show text only. shinymcp gives those clients the same tools, with text results.
Something on the page is missing or blank
Chat clients block anything the app doesn’t carry itself.
- Content from another site (map tiles, a script from a CDN, a web
font) needs a
cspdeclaration:csp = list(resource_domains = "https://tiles.example.com")for images, scripts, and stylesheets,connect_domainsforfetch(). The preview’s console lists each blocked request. - Files the UI loads by relative path must be in the app’s
www/folder or under a path added withshiny::addResourcePath(). - Fonts don’t load from the app itself, so icon fonts such as Font Awesome show nothing. Use SVG icons (the bsicons package, for example).
An input doesn’t reach the tool
- A tool argument takes the value of the input with the same id.
mcp_input(tag, id = "argument")connects an input whose id differs. - An input from a package works if the package registers a Shiny input
binding for it. If it uses JavaScript without one, call
Shiny.setInputValue("id", value)from that JavaScript. - Inputs in HTML that a tool returns aren’t connected to tools. Put them in the UI from the start.
- In a live Shiny app with anything marked by
bindMcp(), the model can only set marked inputs. The person can still change all of them.
A conditional panel doesn’t show or hide
shinymcp reads a conditionalPanel()’s condition itself,
since chat clients forbid running it as JavaScript. A condition it can’t
read shows its panel and logs a warning in the browser’s console.
Conditions built from comparisons, &&,
||, !, input.x,
output.x, and methods such as indexOf() work;
functions defined elsewhere on the page don’t.
An output doesn’t update
- The names of the list a tool returns must match output ids.
mcp_output(tag, id = "name")connects an output whose id differs. - In a live Shiny app,
req()that fails clears an output, as in Shiny;validate()shows its message in it. An error in a render function shows in the output, and in the text the model receives.
A live Shiny app stops with an error
An error in an observer ends a Shiny session, in a browser too. shinymcp reports it: a tool call from the model returns it as an error, and the page shows it above the app. The R console (or the client’s log, for a stdio server) has the stack trace. The page’s next change starts a new session.
A live Shiny app forgets what the person did
Each view keeps its session in the R process that opened it, for up
to an hour without use, and a process keeps at most 50 views (the
shinymcp.view_timeout and shinymcp.max_views
options). When a view’s session is gone, the page starts a new one from
its inputs: what the server function kept outside its inputs starts
over. On Posit Connect, this also happens when requests reach another
process; set Max processes to 1 for apps that keep such
state.
An upload is refused
Uploads work in live Shiny apps; a tool can’t take a file from the
page. They are limited by Shiny’s shiny.maxRequestSize
option, 5 MB unless the app sets it higher:
options(shiny.maxRequestSize = 30 * 1024^2)The model doesn’t know what the person did
The page tells the model through the client, which must support
ui/update-model-context. The preview’s Context tab shows
what the app sent. mcp_app(model_context = FALSE) turns it
off; in a live Shiny app, mcp_model_context() replaces the
default summary.
In a shinychat conversation, mcp_chat_host() passes it
on before each message the person sends, as a message of its own;
as_shinychat_tool() alone doesn’t. Some providers, such as
AWS Bedrock, refuse two user messages in a row. For those, set
context = FALSE and put host$context() in the
person’s message yourself.
An app in a Shiny app doesn’t show
A card or pane that can’t show its app says why, above where the app would be:
-
Couldn’t reach the app’s server, or Couldn’t load the
app: the URL given to
mcp_client()is wrong, the server isn’t running, or it wants credentials inheaders.client$tools()in the console fails with the same error, in more detail. -
This app isn’t available any more: a card restored with a
saved conversation, whose server this session doesn’t host, or which no
longer has the card’s tool. Give
mcp_chat_host()the same sources in every session, with the same names. A client is named after its URL unless you give it aname. -
… has no tool called …, or The tool … doesn’t show an
app: a pane names a tool its server doesn’t have, or one that
declares no page. For an app in the same R process,
mcp_host_server()stops with this error instead.
An app that holds requests open while it waits for changes, as a
Shiny app served with Shiny’s own MCP support can, needs a client
timeout longer than that wait.
