Let an agent app on this computer work in a Cellucid viewer
cellucid_mcp.RdA local MCP (Model Context Protocol) server on this process's standard input
and output, for an agent app on this computer – Claude Code, Claude
Desktop, Cursor – to work in a Cellucid viewer: the app starts it, and it
runs until the app ends it. It is the R counterpart of cellucid-python's
cellucid mcp, with the same tools, sentences and status, and either
drives a viewer of either package. Nothing passes through a relay or the
deployed site: the server talks to the viewer on this computer.
The tools are the web app's own (the web connector's, generated into the
package as mcp_tools.json): while a viewer page is open, the page's
list for this connection, each tool saying what the person's permission
settings make of it, and cellucid_status, which says which viewers
are found, which one is driven and what to tell the person when none is
open. The page runs every call as the app's agent (“Claude Code
(local)”) under the person's permission settings: a change runs, or waits
for the person's answer in the viewer's confirm card – asked in the app
too, as an MCP elicitation, when the app can ask – or is refused with one
sentence. Every step is in the person's history, and the person can Stop
the agent's turn or undo it whole.
Usage
cellucid_mcp(
open = NULL,
viewer = NULL,
port = NULL,
browser = TRUE,
web_source_url = "https://www.cellucid.com",
web_cache_dir = NULL
)Arguments
- open
A file or folder to serve and open in a viewer of this process (any path
cellucid_view()takes); its page opens in the browser, and the viewer stops when the server ends.NULLdrives a viewer opened elsewhere on this computer.- viewer
Which published viewer to drive: its viewer id, dataset id or dataset name.
NULLtakes the most recently opened one whose page is open, else the most recently opened one. Not withopen.- port
The port of the viewer
openstarts;NULLtakes 8765, or the next free one.- browser
Whether
openopens the viewer's page in the browser (FALSE: its address is written to standard error).- web_source_url
Where the viewer
openstarts loads its web app from, as forcellucid_view().- web_cache_dir
Where that viewer keeps its copy of the web app;
NULLis the user's cache folder.
Details
Register it with the app, for example
claude mcp add cellucid -- Rscript -e "cellucid::cellucid_mcp()"or, in an app's "mcpServers" settings,
"cellucid": {"command": "Rscript", "args": ["-e", "cellucid::cellucid_mcp()"]}and to have it open a file itself,
Rscript -e "cellucid::cellucid_mcp(open = 'data.h5ad')".
Which viewer. Every viewer with a server –
cellucid_view() outside a notebook, cellucid_serve()
– is published to agent apps on this computer while it runs, as
cellucid-python publishes its viewers: one record in the agents folder
(CELLUCID_AGENTS_DIR when set, an absolute path; else
%LOCALAPPDATA%/cellucid/agents on Windows,
~/Library/Caches/cellucid/agents on macOS,
$XDG_CACHE_HOME/cellucid/agents or ~/.cache/cellucid/agents
elsewhere), readable by this user only (0600, the folder made 0700), naming
this computer, where the viewer is served (never its page address, which
carries the viewer's secret: an agent is not given it), and the viewer's
endpoint on its own server with the secret token it takes. The endpoint
answers requests from this computer only, never a browser's. The record
goes when the viewer stops or R exits. On Linux and macOS a folder another
user owns, or others may write in, is refused: no viewer is published there
or read from it, and the agent is told why. A home folder shared between
computers is fine: a record of another computer is skipped and left alone.
CELLUCID_AGENTS=off (or 0, false, no; any
case) publishes no viewer. A notebook widget's viewer (cellucid_view()
in an IRkernel notebook) has no server and is not published, nor is a viewer
bound to one non-loopback interface (host).
cellucid_mcp() drives the viewer viewer names, else the newest
whose page is open: when the page of the one it drives is gone and another
viewer's is open, it moves to that one; when the one it drives stops, it
finds another. A call whose page goes (reloaded or closed) before it ends
fails with a sentence that says whether it was done is not known, and the
app is told to list the tools again when a page takes the viewer back.
What the agent is sent. What the agent reads (names, counts, summaries, values, a picture of the view) goes to the agent app and from there to the model provider the app uses; nothing passes through a Cellucid server. A dataset's “Agents see names and summaries only” switch, set in the viewer (Permissions in the chat settings or the Agents panel), applies to this agent as to every other: the page leaves values of single cells, extremes (a minimum, a maximum, a median) and pictures out of its results, and refuses summaries of groups an agent made by naming cells. It is not anonymisation: a summary of a very small group can still reveal its cells' values. Text written by others (labels, notes, dataset text) reaches the agent marked as data, never as instructions.
The protocol. JSON-RPC 2.0, one message per line: the handshake of
protocol versions 2024-11-05 to 2025-11-25 (initialize), ping,
tools/list, tools/call and notifications/cancelled (the
call is stopped in the viewer), and the server's elicitation/create
(a form, to an app whose elicitation capability is empty or names forms;
withdrawn with notifications/cancelled when the person answers in the
viewer first), notifications/tools/list_changed (the person's
settings changed, or a page took the viewer) and notifications/progress
(when a call names a progress token, counting 1, 2, ...); and the
per-request protocol 2026-07-28 (server/discover), whose clients are
asked in the viewer's card only. Nothing but protocol messages is written to
standard output: what R prints while the server runs goes to standard error.
One thread. Standard input is read without blocking while R's event
loop runs (later::run_now()), so the viewer open serves keeps
answering its page, and a call waiting for the person never stops the server
from reading the app's next message. A request to another process's viewer
runs R's event loop while it waits (for at most 10 seconds). On Windows the
app's pipe is read without blocking too; a console is refused (the server
is started by the app, not typed into).
See also
cellucid_view() opens a viewer this server can drive.