Using Arin from an agent
Arin speaks MCP over stdio. An MCP client launches arin mcp as a subprocess, so any
client that can run a local stdio server can drive it. Most of them take the same JSON:
{
"mcpServers": {
"arin": { "command": "arin", "args": ["mcp"] }
}
}
arin mcp starts whether or not the daemon is running, and connects to its socket on the
first tool call. A call made with no daemon comes back as an error naming the socket, and
the next call after the daemon starts goes through, with no need to restart the agent. The
same goes for a daemon that restarts partway through a session.
Setting up a client
Whichever client you use, register Arin for your user rather than for one project. Arin draws on your screen rather than on a repository, so a server registered against one directory leaves every session started anywhere else with no Arin at all: no tools, and no way to know it was meant to have any.
A client launched as an app rather than from a terminal may not see your shell's PATH,
and then it cannot find arin. If one says it cannot start the server, replace arin with
the full path that command -v arin prints. A Homebrew install puts it at
/opt/homebrew/bin/arin on Apple silicon and /usr/local/bin/arin on Intel.
Claude Code
claude mcp add --scope user arin -- arin mcp
--scope user is the part worth getting right. Leave it off and the server is registered
against whichever directory you happened to run the command in.
Claude Desktop
Add Arin to ~/Library/Application Support/Claude/claude_desktop_config.json, which
Settings, then Developer, then Edit Config opens for you. Claude Desktop starts from the
Dock without your shell's PATH, so give the full path:
{
"mcpServers": {
"arin": { "command": "/opt/homebrew/bin/arin", "args": ["mcp"] }
}
}
Quit and reopen the app to load it.
Cursor
The standard JSON above, in ~/.cursor/mcp.json. A .cursor/mcp.json inside a project
works too, for that project alone.
Codex
codex mcp add arin -- arin mcp
That writes ~/.codex/config.toml, which the Codex CLI, the IDE extension and the desktop
app all read. By hand it is:
[mcp_servers.arin]
command = "arin"
args = ["mcp"]
/mcp inside a session lists the servers Codex connected to.
opencode
opencode keeps servers under mcp rather than mcpServers, and takes the command as one
array. In ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"arin": { "type": "local", "command": ["arin", "mcp"], "enabled": true }
}
}
The OpenCode 2 docs show servers nested one level deeper, under mcp.servers. Use the shape
above unless your version rejects it.
Kilo Code
The Kilo CLI and the VS Code extension share ~/.config/kilo/kilo.jsonc, in the same shape
as opencode:
{
"mcp": {
"arin": { "type": "local", "command": ["arin", "mcp"], "enabled": true }
}
}
In the extension, Settings, then MCP, then Add Server with Local (stdio) writes the same
entry. Older releases kept servers in mcp_settings.json, in the standard JSON above.
pi
MCP is built into pi from 0.99.0.
pi mcp add arin --exposure direct -- arin mcp
pi mcp list
That writes ~/.pi/agent/mcp.json. Pass --exposure direct. Without it pi hands MCP tools
to its codemode sandbox instead of to the model, which then has to search for them before it
can point at anything. Pi also does not pass the instructions Arin sends at startup on to
the model, so what it knows about aiming comes from the tool descriptions alone.
Ollama
Ollama runs the model and is not an MCP client itself, so there is nothing to set up in it.
Set Arin up in the agent instead, and point that agent at Ollama: ollama launch codex,
ollama launch opencode, ollama launch pi and ollama launch claude each start one with
Ollama as the provider, and each picks Arin up from its own configuration above.
The model is what decides how well that works. It needs tool calling to reach Arin at all,
and vision to aim off a screenshot. One without vision can still point at a named position
like "top-right", which gets near the target but not onto it.
The tools
Named after what an agent is trying to do rather than after the message underneath, so a model reaches for the right one without being told.
| Tool | What it does |
|---|---|
point_at |
Puts the orb on a position, with an optional caption |
highlight |
Outlines a region, with an optional caption |
annotate |
Places a block of explanatory text, as a quiet note or a louder guide |
draw_arrow |
Draws an arrow between two places, curved unless asked straight |
clear |
Removes one mark, or every mark the agent drew |
bring_to_front |
Raises an application, when the daemon allows activation |
wait_until_showing |
Waits until the user has an application's window in front of them |
Every call reports back the display's size and scale, so an agent working from a screenshot can convert pixels to logical points without asking twice.
How an agent aims
Nothing above can see your screen. There is no capture tool and no displays tool, on
purpose: Arin holds the Screen Recording grant and a client does not, so letting a client
read the screen through Arin is the one capability worth gating. That leaves three ways to
say where, and they cost different amounts to set up.
| Form | Needs | Precision |
|---|---|---|
at as percentages, "27%,9%" |
a screenshot of one whole display | exact, to your measurement |
at as a name, "top-right" |
nothing | a region of the screen, not a spot |
x and y |
the display's logical size | exact |
query, "the Submit button" |
a resolver you configured | whatever the model gets right |
The percentage form is the one most agents should reach for. An agent holding a
screenshot can measure the target as a fraction of the image and send that, with no idea
how big the display is and no resolver configured. Both sides need the sign, so "27,9" is
refused rather than read as points. It aims point_at and either end of draw_arrow: a
name is a spot rather than an area, so highlight has to be measured.
Aim at the display the screenshot came from. A capture of one screen on a machine with three does not describe the others, and a percentage sent to the wrong one lands somewhere confidently wrong.
The server tells the agent all of this itself, in the instructions it sends at startup, so
there is nothing to paste into a prompt. Resolvers covers query,
which is off until you name one.
Marks live until they are cleared, the content scrolls out from under them, or the client
disconnects. Pass ttl_seconds to have one remove itself instead.
When a mark goes away for a reason the agent did not ask for, that arrives as a gone
field on the next tool result. There is no way for an MCP server to interrupt a model
mid-thought, so the news waits for the next exchange. A daemon restart shows up the same
way, as one session_end with no annotation id, since every mark went with it.