An AI agent works in the same live R session as the person: through the dsr command, or through the MCP server
bin/dsr-mcp.js that gives the same tools as typed MCP tools. The person sees every run, log and stop in the editor.
Below, <dsr> is the folder of DataStep Studio for R (the extension's folder, or a clone of this repository) and
<project> is the R project. dsr doctor (or the MCP tool dsr_doctor) prints both paths and checks the machine.
Connect through MCP
The server speaks MCP over stdio. It works in the folder named by DSR_ROOT (else the folder it starts in).
In the editor, DataStep R: Set Up Agent Tools writes .mcp.json and .vscode/mcp.json for the open project.
Claude Code (tried with Claude Code 2.1):
cd <project>
claude mcp add --scope project dsr -e DSR_ROOT=<project> -- node <dsr>/bin/dsr-mcp.js
It writes .mcp.json in the project; Claude Code asks once to approve it. Without --scope project it is for you
only. The file it writes:
{
"mcpServers": {
"dsr": {
"type": "stdio",
"command": "node",
"args": ["<dsr>/bin/dsr-mcp.js"],
"env": { "DSR_ROOT": "<project>" }
}
}
}
Codex CLI (tried with codex-cli 0.159):
codex mcp add dsr --env DSR_ROOT=<project> -- node <dsr>/bin/dsr-mcp.js
It adds to ~/.codex/config.toml:
[mcp_servers.dsr]
command = "node"
args = ["<dsr>/bin/dsr-mcp.js"]
[mcp_servers.dsr.env]
DSR_ROOT = "<project>"
Cursor (tried with cursor-agent in round 1: cursor-agent mcp list-tools dsr listed the tools; there are 46 now): .cursor/mcp.json in the
project, or ~/.cursor/mcp.json:
{
"mcpServers": {
"dsr": { "command": "node", "args": ["<dsr>/bin/dsr-mcp.js"], "env": { "DSR_ROOT": "<project>" } }
}
}
VS Code (Copilot agent mode): .vscode/mcp.json, as Set Up Agent Tools writes it (VS Code's documented form;
not tried with Copilot here):
{
"servers": {
"dsr": { "type": "stdio", "command": "node", "args": ["<dsr>/bin/dsr-mcp.js"], "env": { "DSR_ROOT": "<project>" } }
}
}
Any other MCP client: the command is node <dsr>/bin/dsr-mcp.js with DSR_ROOT=<project>. Use one server per
project, or one server and the root argument that every tool takes.
Connect through the command line
An agent with a shell needs no MCP: put <dsr>/bin on its PATH (the editor does this in its terminals) and let it
read dsr guide once. dsr tools lists the tools, dsr help <tool> explains one.
The working method
dsr guide (MCP: dsr_guide) is the method on one page: the goal as a fact, look, lint, run, read the verdict,
find the cause with the tools, the smallest fix, then batch and tests. dsr guide --full adds which tool answers
which question. The MCP server's instructions carry the short form.
What a tool gives back
Every CLI tool takes --json and then prints one JSON object. Every MCP tool returns the same object as
structuredContent (each tool declares it as its outputSchema) and the text in content.
| Field | |
|---|---|
schema |
"dsr/1" |
tool |
the tool's name (run, query, ...) |
exit |
the exit code, below |
ok |
exit is 0 |
verdict |
the verdict word: CLEAN, CHECK, ERROR, PRY (the run waits at a stop), STOPPED (a run ended at its stop), PASS, FAIL, MATCH, OK, RUNNING, REFUSED (format: the proof failed), ... |
summary |
the verdict line, e.g. RESULT: ERROR | FIRST CAUSE: prog.R:5: n_obese == 2 is not TRUE |
fix |
what to do next when dsr knows it (dsr start, an install command, dsr help run), else null |
text |
what the tool printed, as a person reads it |
truncated, more |
the text was cut; more is the call for the rest (dsr more --from 401) |
data |
the tool's own result as data, e.g. run: events (each message with its line), workspace (new, changed, removed objects), suspicious (each message that matters with its rule, line, why, fix), at a stop live, reason, stop (the frame, its code, its objects); data: columns and rows (column -> values); query: rows as display text (hidden characters visible); complete: items (label, kind, detail), signature; format: per file its proof (tokens, comments, program, first difference); coverage: per file lines with n, never; test: per_file, details |
Exit codes:
| Code | Meaning | What the agent does |
|---|---|---|
| 0 | the tool did its work and it went well | go on |
| 1 | the tool says it did not go well: RESULT: ERROR, TESTS FAIL, LINT: PROBLEMS, GATE FAIL, ... |
read the FIRST CAUSE |
| 2 | the call was wrong, or there is no session | read fix (dsr start, dsr help <tool>) |
| 3 | still running after --timeout |
dsr wait --timeout 600, then dsr log; or dsr stop |
In MCP, isError is true for 1 and 2. A call that still runs (3) is no error. dsr status answers 1 when no
session runs (its fix is dsr start).
Debugging live: a stop that waits
dsr run <program> --stop-at FILE:LINE (repeat it; 'FILE:LINE if <condition>'; --stop-on-warning), a pry() in
the program and a breakpoint of the editor stop the run and keep it alive: the answer is RESULT: PRY with where it
stopped, and the session goes on answering (dsr pry ls, dsr pry eval, the data tools on @pry:<object>). Then:
Command (MCP dsr_pry command) |
What happens |
|---|---|
dsr pry next [n] (next, frame: n) |
the next line of this frame (or of the frame it returns to) |
dsr pry step |
the next line anywhere: into a function of the project this line calls |
dsr pry finish |
the first line after this frame returns |
dsr pry continue |
to the next stop, or to the end of the run with its verdict |
dsr pry quit |
ends the run here: RESULT: STOPPED (a new dsr run, or dsr stop, does the same) |
dsr pry break 'R/f.R:12 if x > 3' (target) |
one more stop |
dsr pry watch 'x' (target) |
stop at the line after x changes; unwatch |
dsr pry source <fn>, run <file.R>, pipe [line] |
a function's code with file:line; code run in the frame; the pipeline at the stop step by step on the frame's data |
Each of next, step, finish, continue, quit answers at the next stop or at the end of the run, with the log
since the last answer (in JSON: events; the final answer has sent, the number of events shown before).
dsr run <program> --coverage, then dsr coverage [file]: which lines ran, how often, which never ran.
Long runs, cancel, several sessions
--timeout Sonrun,eval,batch,testandcheck(MCP:timeout): the call comes back after S seconds with exit 3 while the work goes on.dsr statusshows what runs and its last message;dsr waitwaits;dsr loggives the log when it is done;dsr stopstops it like Ctrl+C (the objects of the session stay).- In MCP,
dsr_status,dsr_stopanddsr_waitnever wait behind a running call, and a request the client cancels stops its run. - A long text is cut:
--max-lines Non the CLI, 400 lines in MCP (DSR_MCP_MAX_LINES).dsr more --from N(MCP:dsr_more) gives the rest. dsr sessionslists the sessions above and below a folder;--root DIR(MCP:root) chooses one.
When something does not work
dsr startsays why a session cannot start and the fix (for example: R cannot load httpuv; install it into the project's library, or point.libPaths()in the project's.Rprofileto a library that has it).dsr doctorchecks Node, Rscript, R, the library the project uses, every package and which tool needs it, Chrome (fordsr app check) and pdftotext (fordsr numberson a PDF).- A harness that judges R runs itself can use dsr's rule:
source("<dsr>/r/verdict.R"), thendsr_condition_kind(condition)anddsr_verdict(conditions).
Tests of this surface
npm run test:cli (with DSR_TEST_LIB=<an R library with httpuv, evaluate, jsonlite, testthat>) runs the CLI
contract and an MCP scenario over stdio: start, a program with a deliberate error, the log, a stop at the wrong
line with pry, the fix, the tests, a cancel, the end. It needs no editor.