Connect an AI client to your SSH servers with MCP
SetuSSH can run a Model Context Protocol server, so an AI client you already use works through your SetuSSH. This page covers turning it on, connecting Claude Code, Claude Desktop, Cursor or any other client, every tool the client gets, and how to keep it safe. For the rest of the app, see How to use.
- What it does
- Turn it on
- Address and token
- Connect a client
- The tools
- Example requests
- Full access and safety
- Troubleshooting
What it does
An AI client such as Claude Code works through the servers you have saved in SetuSSH. You ask it for something in plain language, and it uses SetuSSH to do the work on your servers. It can:
- see your saved servers and the SSH terminals you have open;
- open a server as a tab in SetuSSH, which you can watch. The tab you are working in stays in front, and a notice names the server it opened;
- run commands in that tab and read back the output and exit code;
-
type into a terminal and read the screen, for prompts and full-screen programs
such as
toporvim; - read and write files on a server, and copy files and folders between this computer and a server.
It connects with the keys, agent and saved passwords SetuSSH already has, so you do not set up SSH again for the AI, and the client never receives your saved passwords or keys. Only SSH terminals are reachable: a local terminal (a shell on this computer) is never listed, read or typed into. AI access works on every plan, including the free tier.
Turn it on
- Open Settings (Ctrl+,, or Cmd+, on macOS) and click AI access.
-
Turn on Let AI clients use this SetuSSH. The
Status line at the bottom reads
Listening on 127.0.0.1:30838. - Choose what a client may do in Access. It starts at Read-only. Choosing Full access, no prompts asks you to confirm first.
When an action needs your say, SetuSSH shows a dialog titled An AI client is asking to act, with the tool name and the exact command, keys or path, and the server it is for. Click Allow once (Run it anyway when it looks destructive) or Decline. There is no “always allow”: to stop being asked, choose a higher access level.
The dialog waits up to 10 minutes. No answer by then counts as Decline, and if the client stops waiting sooner, the dialog closes by itself. The dialog needs the SetuSSH window. While the window is still starting or reloading, the call waits up to 15 seconds for it. With no window by then, nobody can answer, so an action that would ask you is refused.
| Access | Runs without asking | Asks you first | Refused |
|---|---|---|---|
| Read-only | Listing servers and terminals, connecting, reading screens and files | Nothing | Running commands, typing into terminals, writing, uploading and downloading files, closing terminals |
| Ask before each action | Listing servers and terminals, connecting, reading screens and files | Every command, everything typed into a terminal, every write, upload and download, closing a terminal | Nothing |
| Run routine actions, ask before destructive ones | Everything that does not look destructive |
Commands, and lines typed into a terminal, that look destructive:
rm, dd, mkfs,
shutdown, reboot, removing packages,
DROP TABLE, docker rm,
kubectl delete and the like
|
Nothing |
| Full access, no prompts | Everything, destructive commands included | Nothing | Nothing |
The access level is one setting for every saved server. Three rules sit above it:
- A server outside your plan is refused.
-
While SetuSSH is locked with its master password, only
list_serversworks. Everything else is refused until you unlock it. - The AI panel’s own permissions still count. In the panel’s settings (the gear in its header), Permissions for the active server and Default permissions (all servers) set auto, ask or deny for each kind of action. A kind of action set to deny is refused at every level, Full access included (see Deny). At the first two levels, reading asks you first when the reading permission is set to ask; it is auto unless you changed it.
Changing the access level restarts the server and stops every call still running, so nothing carries on under permission you have just taken away. The client is told that SetuSSH stopped its MCP server. A command is interrupted as if you pressed Ctrl+C, a file transfer stops, and an approval dialog still on screen closes.
The address and the token
With AI access on, the pane shows two things a client needs. The
Address row holds http://127.0.0.1:30838/mcp and a
Copy button. 30838 is the default port, so always use the
address the pane shows.
The Access token row keeps the token hidden until you click
Reveal, which also brings up its Copy button.
Hide covers it again. The client sends the token with every
request as the header Authorization: Bearer <token>.
Keep the token secret
Treat the token like a password. Any program on this computer that has it can reach your servers through SetuSSH, as far as the access level allows. Do not paste it into a chat, a ticket or a file you commit to a repository; the configs below show how to keep it in an environment variable instead. SetuSSH stores it encrypted with the OS keychain where one is available.
Rotate it
Click Issue a new token and confirm. The old token stops working at once, even for a client that is connected, so paste the new one into every client you use. Rotate whenever the token may have been seen by someone else.
If SetuSSH ever cannot read its saved token, it issues a new one, and the pane says The saved token could not be read, so a new one was issued. Copy the new token into your clients in the same way.
Connect a client
In every example, replace YOUR_TOKEN with the token from Settings,
and change the port if the Address row shows a different one. SetuSSH must be
running with AI access on while the client uses it.
Claude Code
One command adds SetuSSH for every project (--scope user). Leave
the scope out to add it for the current project only.
claude mcp add --transport http --scope user \
setussh http://127.0.0.1:30838/mcp \
--header "Authorization: Bearer YOUR_TOKEN"
claude mcp list checks it from the shell; inside Claude Code,
/mcp shows whether setussh is connected.
To share the setup with a project, put it in .mcp.json at the
project root instead. Claude Code fills in ${SETUSSH_MCP_TOKEN}
from the environment, so the token stays out of the file:
{
"mcpServers": {
"setussh": {
"type": "http",
"url": "http://127.0.0.1:30838/mcp",
"headers": {
"Authorization": "Bearer ${SETUSSH_MCP_TOKEN}"
}
}
}
}
Set SETUSSH_MCP_TOKEN in your shell before you start Claude Code.
If a long command is cut off at about 5 minutes, see
A command never returns, or stops too early.
Claude Desktop
Claude Desktop’s config file starts local programs rather than connecting to
an address, so a small bridge, mcp-remote, connects it to SetuSSH.
It runs through npx, which comes with
Node.js.
-
In Claude Desktop, open Settings, then
Developer, and click Edit Config. That opens
claude_desktop_config.json. -
Add
setusshundermcpServers:{ "mcpServers": { "setussh": { "command": "npx", "args": [ "-y", "mcp-remote", "http://127.0.0.1:30838/mcp", "--header", "Authorization:${AUTH_HEADER}" ], "env": { "AUTH_HEADER": "Bearer YOUR_TOKEN" } } } } - Quit Claude Desktop completely and open it again.
Write the header exactly like that, with no space after the colon and the
Bearer part in env. Claude Desktop on Windows does not
escape spaces inside args, which breaks a header written as
Authorization: Bearer YOUR_TOKEN there; a space inside env is
fine. mcp-remote accepts a plain http:// address for
127.0.0.1, so no extra flag is needed.
Cursor
Add SetuSSH to ~/.cursor/mcp.json for every project, or to
.cursor/mcp.json in one project:
{
"mcpServers": {
"setussh": {
"url": "http://127.0.0.1:30838/mcp",
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}
}
}
To keep the token out of the file, write
"Bearer ${env:SETUSSH_MCP_TOKEN}" and set that variable. Check the
server is enabled under Customize in Cursor’s sidebar.
Cursor asks before it uses an MCP tool unless you change its run mode.
Any other client
Any client that speaks MCP over Streamable HTTP can connect with two settings:
the URL http://127.0.0.1:30838/mcp and the header
Authorization: Bearer YOUR_TOKEN. SetuSSH answers protocol versions
from 2024-11-05 to 2026-07-28. A client that can only start local programs can
use mcp-remote, as Claude Desktop does.
Answers are plain JSON, with one exception. A tool call whose
Accept header lists text/event-stream gets an event
stream: a keep-alive every 15 seconds while the tool runs, plus a progress
notification each time when the request carried a progress token, and then the
result. A client that sends initialize (protocol 2025-11-25 or
earlier) gets an Mcp-Session-Id header back. Sending it on later
requests makes sure its notifications/cancelled only stops its own
calls when several clients are connected. It is never required. A request body
over 1 MB is refused, which also limits what write_file can write
in one call.
To check the server by hand, list the tools with curl from a macOS
or Linux terminal (or Git Bash on Windows). No handshake is needed first:
curl -s http://127.0.0.1:30838/mcp \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
And call one:
curl -s http://127.0.0.1:30838/mcp \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"list_servers","arguments":{}}}'
The tools
These are the tools a client sees, in the order it sees them. Wherever a tool
takes server, give the saved server’s name (not
case-sensitive) or its id. Wherever it takes terminal, give the
terminalId that connect_server or
list_terminals returned. Access needed is the lowest
access level that allows the tool.
| Tool | What it does | Arguments | Access needed |
|---|---|---|---|
list_servers |
Lists every saved server: id, name, host, port, username, folder, tags, favourite, jump host, auth method, last connected, open terminals, whether the plan locks it, and whether its AI permissions deny reading. Never passwords, passphrases or key paths. | None | Read-only |
list_terminals |
Lists the SSH terminals in SetuSSH, including ones you opened, with each one’s terminalId, server, status and whether it is the one you are looking at. | None | Read-only |
connect_server |
Opens a server as a tab and waits until its shell is ready. Reuses a
terminal already open for that server unless new_tab is true.
Returns the terminalId.
|
server, new_tab (default false),
timeout_seconds (1 to 180, default 45)
|
Read-only |
run_command |
Runs a command and returns its output and exit code. With
terminal it runs in that tab, where you see it. With only
server it uses a free open terminal for that server, or else a
background channel you do not see; it never opens a tab. The command must
finish on its own.
|
command, terminal or server,
timeout_seconds (1 to 600, default 50), force
(with terminal only, default false)
|
Ask before each action |
send_input |
Types into a terminal: the text as given, then each named key, then Enter if asked. Returns what was sent and the last 20 lines of the screen. |
terminal, text, keys,
enter (default false)
|
Ask before each action |
read_terminal |
Returns what a terminal shows as plain text, plus scrollback lines if asked, the size, the cursor position and whether a full-screen program is showing. A long line the terminal wrapped onto several rows comes back as one line, so fewer lines than rows can come back. Works on a closed terminal too, to see why it closed. |
terminal, scrollback (0 to 2000, default 0)
|
Read-only |
disconnect |
Closes a terminal and ends its shell. The saved server and other terminals for it are not touched. | terminal |
Ask before each action |
read_file |
Reads a text file over SFTP. A file over 4 MB is cut off, and the result says so. | server, path |
Read-only |
list_dir |
Lists a folder over SFTP. | server, path |
Read-only |
write_file |
Creates a file over SFTP, or replaces one without a backup. The folder must
already exist. The content must fit in a request of 1 MB. Refused when the
content holds redaction markers, so a file read with
read_file is never written back over its real secrets.
|
server, path, content |
Ask before each action |
upload_file |
Copies a file or a folder from this computer into a folder on the server. A file of the same name there is replaced. If the call is stopped, what was already copied stays on the server. |
server, local_path (absolute),
remote_dir
|
Ask before each action |
download_file |
Copies a file or a folder from the server into a folder on this computer. Refused if something of that name is already there, so nothing here is overwritten. Links, pipes, sockets and devices inside a folder are skipped. A download that fails or is stopped removes its partial copy. |
server, remote_path, local_dir
(absolute, must exist)
|
Ask before each action |
Remote paths can be absolute or relative to the login folder.
read_file, list_dir and write_file also
accept target as an older name for server. Passwords,
passphrases, tokens, API keys and private keys are replaced by
‹redacted› markers in what comes back.
upload_file and download_file each open an SFTP channel
of their own, so a stopped transfer ends part way through a file. A server allows
only so many channels on one connection (10 by default in OpenSSH), and every
terminal tab for that server uses one. When there is no room for another, the
transfer shares the channel the file browser uses. A stop then takes effect
between files, so the file being copied still finishes.
Commands in a terminal
A command run_command types into a terminal can span several lines,
hold a heredoc, or end with & to leave a job running. That is
true in POSIX shells such as bash, zsh, dash, ksh and busybox sh.
When the server’s login shell is csh or tcsh, the command runs in
sh, so the same syntax and heredocs work there too, but a
cd or export in it does not carry over to the next
command. When the login shell is fish, fish runs the command itself, so it must
be written in fish syntax: a heredoc does not work there.
If you start fish inside a bash terminal, or bash inside fish, the first command typed there is rejected by that shell before any of it runs. SetuSSH notices and types it again in the other syntax, and remembers the choice for that terminal.
Some shells hold only a short line: busybox sh (Alpine and many
containers) takes about 1024 bytes, and a longer line never starts. SetuSSH does
not type a command of more than 64 KB into any terminal. When the login shell is
not bash, zsh or fish, it also does not type a command with a line longer than
about 750 bytes. It cannot see the limit of a small shell started inside a bash
or zsh terminal, such as busybox sh through docker exec,
so keep commands typed there with force (below) short. For long file
content, use write_file (up to 1 MB) or upload_file.
When a program other than the shell has the terminal (vim,
top, less, a REPL, a password prompt),
run_command does not type into it. Named by
terminal, the call is refused; named by server, it runs
on a background channel instead and says so. SetuSSH notices a full-screen
program in any shell. A password prompt or a REPL it notices in bash 5.1 or
later, zsh and fish, but not always in older shells.
A shell started inside bash 5.1 or later, zsh or fish can look like a program
too: sh or dash, a shell from docker exec or
kubectl exec, or bash 5.0 or older on a server reached with
ssh from the terminal. When read_terminal shows that
shell’s prompt, the client can call run_command again with that
terminal and force set to true. An approval for it
reads even if another program seems to have it. If a program really has
the terminal, the command’s text goes to that program. Given only
server, the command runs on a background channel to the saved
server itself, not inside the container or the inner shell.
Keys
The key names send_input understands are enter,
tab, esc, backspace, delete,
up, down, left, right,
home, end, pageup,
pagedown, space, ctrl+a to
ctrl+z, ctrl+[, ctrl+\,
ctrl+] and f1 to f12. An unknown name is
an error, and then nothing at all is sent. Arrows, Home and End are sent in the
form the program on screen has asked for, as your keyboard would send them.
Timeouts and stopping a call
run_command waits 50 seconds by default and
connect_server 45, because many clients give up on a tool call after
60 seconds. The run_command wait starts when the command starts.
Connecting, waiting for your approval and, for the first command since SetuSSH
connected to the server, up to 5 seconds spent finding out which shell it uses
all come before it. For anything slower, see
A command never returns, or stops too early.
A client stops a call by closing its request. A client on protocol 2025-11-25 or
earlier can also send notifications/cancelled. A command is then
interrupted as if with
Ctrl+C, a transfer stops, and an approval still waiting
for you closes. A command that ignores Ctrl+C keeps its
terminal busy until it ends, for up to 3 minutes.
Example requests
You talk to the AI client as usual; it picks the tools. The server names below are examples: use the names of your own saved servers. Under each request is what a client typically does with it. All four run commands or type, so they need Access set to Ask before each action or higher.
“Connect to prod-web-01 and show disk usage”
-
connect_serverwithserverset toprod-web-01. A tab for it opens in SetuSSH, or an open one is reused. -
run_commandwith thatterminalanddf -h. The command appears in the tab, and the client reads the output and sums it up for you.
“Open db-primary, run the migration status command, and tell me if anything is pending”
connect_serverfordb-primary.-
run_commandwith your project’s status command, such asphp artisan migrate:statusorrails db:migrate:status, in the right folder. If the client does not know the command, it asks you or looks for it withlist_dirandread_file. Name the command in your request to save a step. - It reads the output and tells you which migrations have not run.
“Tail the nginx error log on staging for the last 50 lines”
-
run_commandwithserverset tostagingandtail -n 50 /var/log/nginx/error.log. With no terminal open for staging, this runs on a background channel and no tab opens.
tail -n 50 finishes on its own. tail -f never does:
ask the client to watch the log live and it should open a terminal and use
send_input and read_terminal instead.
“On staging, run ./deploy.sh and answer yes when it asks to continue”
connect_serverforstaging.-
send_inputwithtextset to./deploy.shandentertrue. The reply ends with the last lines on the screen, for exampleContinue? [y/N]. -
send_inputwithtextyandentertrue, thenread_terminaluntil the script has finished. -
If something hangs,
send_inputwithkeys["ctrl+c"]stops it.
The same pattern drives top, less or
vim: type, press keys, read the screen, then leave the program the
usual way. For top and less that is q sent
as text; for vim it is the key esc, then
:q with Enter. When the work is done, ask the client to disconnect,
or close the tab yourself.
Full access and safety
Full access, no prompts is for when you want the client to work on its own. With it on:
- any program on this computer that has the token can connect to any of your saved servers;
- it can run any command, including ones that delete data;
- it can type into your terminals, the ones you opened included;
- it can upload and download files.
A model can misread a request, and text it reads on a server or a web page can try to steer it. Full access applies to every saved server at once, and a server’s AI permission set to ask does not make it ask. Turn it on only when every server whose permissions are not set to deny is one you can afford to have changed. Otherwise set the AI permissions of the others to deny, or use Ask before each action or Run routine actions, ask before destructive ones.
Host keys
At the other levels, the first connection to a server shows the usual Verify host key dialog and the client waits for your answer. In full access, a host key SetuSSH has never seen is trusted without asking for a connection the client starts, and remembered. A key that has changed is never accepted on its own at any level: the red Host key has CHANGED dialog appears. Either way, the connection gives up if nobody answers within about 45 seconds.
Deny
Set a server’s AI permissions to deny and full access does not override it. Each permission covers only its own kind of action on that server.
| Permission set to deny | What is refused on that server |
|---|---|
| Read files / list / read memory |
Connecting, reading its screens, read_file,
list_dir, download_file, and read-only commands
such as df or ls. list_servers marks
the server as denied.
|
| Run non-read-only commands | Every other command, and typing into its terminals |
| Write / edit files |
write_file, upload_file and
download_file
|
| Manage SetuSSH (sessions, tunnels) | Closing its terminals |
Denying reading alone does not block other commands, uploads or typing: typing
with send_input needs the command permission, and its reply includes
the last 20 lines of the screen. To wall a server
off completely, deny all four permissions above. list_servers and
list_terminals follow Default permissions (all
servers) instead, so they still show that server and its terminals. The
AI panel is part of the trial and of a licensed install; permissions you set
there stay in force on any plan.
The plan limit
On the free tier, the 10 oldest saved servers are connectable, and that limit
applies to AI clients exactly as it does to you. list_servers shows
the others as locked, and connecting, running commands or moving files on them is
refused with a message that says why.
What stays true at every level
-
The server listens on
127.0.0.1only. Nothing on your network can reach it, and that is not a setting. - Every request must carry the token; one without it is refused.
-
Requests from web pages are refused. A request carrying an
Originheader, which every browser adds, is turned away before the token is even checked, so a page you visit cannot drive it. - While SetuSSH is locked with its master password, a client can list your servers and nothing more.
- Every call is written to SetuSSH’s audit log on this computer: the tool, the exact command, keys or path, the server, whether it ran on its own, was allowed, declined or refused, and whether it worked. Text typed at a password prompt is logged by its length only. A host key trusted without asking is logged too.
- Turning AI access off stops the server at once, along with every call still running.
Troubleshooting
401: the access token is missing or wrong
Reveal the token in Settings, copy it again and paste it into the client. If you
clicked Issue a new token, or the pane says a new one was
issued, every client needs the new one. The header must read
Authorization: Bearer followed by the token; for
mcp-remote, use the form shown under Claude Desktop.
Connection refused, or the client cannot connect
Nothing is listening at that address. Check that SetuSSH is running, that Let AI clients use this SetuSSH is on, and that the port in the client matches the Address row. On Windows and Linux, closing the SetuSSH window quits the app, which stops the server. If Status says the port is already in use, another copy of SetuSSH is probably running: close it, then turn AI access off and on again.
403: Requests from web pages are refused
The client sent an Origin header, as browser-based tools do. Use a
desktop or command-line client instead.
“SetuSSH is locked with its master password”
Unlock SetuSSH with your master password. Until then, every tool except
list_servers is refused.
A server is locked, or connecting to it is refused
If list_servers shows locked: true, or the refusal
mentions the free plan, that server is outside the 10 your plan connects to. A
licence key lifts the limit; see
Licence and plans. If it shows
denied: true, its AI permissions deny reading.
“Refused” or “declined”
A refusal that names Read only means the access level does not allow that tool: choose a higher one. One that names the AI permissions means a deny is set for that server or for all servers. “Nobody can approve this” means the call needed your answer, but the SetuSSH window was closed, or still starting after 15 seconds. “The person at this computer declined the request” means Decline was clicked, nobody answered within 10 minutes, or the SetuSSH window closed or reloaded while the dialog was showing.
“SetuSSH stopped its MCP server”
The call was still running when the server stopped: SetuSSH quit, AI access was turned off, or the access level changed. SetuSSH stopped the call the way it stops one a client cancels (see Timeouts and stopping a call). With AI access on again, ask the client to repeat it.
The host key changed
connect_server fails after about 45 seconds, and its message says
the server’s host key has changed. In SetuSSH, a dialog titled
Host key has CHANGED, with a red header, is waiting. Accept it
only if you know why the key changed: tick Replace the saved key for
this host, click Accept anyway, then ask the client to
connect again. Without the tick nothing is saved, and the next attempt asks
again.
“A program other than the shell” has the terminal
Something such as vim, top, a REPL or a password
prompt is in front in that terminal, so run_command will not type
there. Ask the client to finish with it through send_input, close
the program yourself, or have the client give server instead of
terminal to run the command on a background channel. If the
terminal is really at the prompt of a shell started inside it, such as
sh from docker exec, the client can pass
force set to true with that terminal.
“The SetuSSH window is not open”
Tabs live in the window. With no window, list_terminals shows none,
and reading or closing a terminal is refused. Anything that needs your approval
waits up to 15 seconds for the window, then is refused. Keep SetuSSH open while
the client works. On macOS, where the app keeps running with no window,
connect_server opens the window again.
A command never returns, or stops too early
run_command waits for the command to finish, 50 seconds by default.
A command that keeps running (top, tail -f,
watch, vim, less, a REPL, a prompt waiting
for input) times out. On a background channel it is then stopped. In a terminal
it is still running, so for up to 3 minutes the next command there is refused as
busy. After that, bash 5.1 or later, zsh and fish still refuse it because a
program has the terminal, but in an older shell the next command would be typed
into the running program. Ask the client to stop it with
send_input and ctrl+c, or press
Ctrl+C in the tab yourself. Interactive programs belong
in send_input and read_terminal.
For a slow command that does finish, the client passes a longer
timeout_seconds, up to 600. Many clients built on the MCP TypeScript
SDK give up on a tool call after 60 seconds whatever
timeout_seconds says, and giving up stops the command. In those,
look for a tool or request timeout in their MCP settings and set it a little
over the longest command, such as 11 minutes.
Claude Code lets a call run for about 28 hours, but stops a call to an HTTP
server that has sent no result and no progress update for 5 minutes. If a long
command is cut off at about 5 minutes, add a timeout in
milliseconds to the setussh entry in .mcp.json, next to
url:
"timeout": 660000
Claude Code 2.1.203 or later then waits that long before it stops a quiet call.
The timeout is also a hard limit on every call, so set it longer than your
longest command plus the time you may take to approve it. 660000 is 11 minutes.
claude mcp add has no timeout option, so if you
added SetuSSH that way, remove it and add it again with the timeout included,
from a macOS or Linux shell:
claude mcp remove setussh
claude mcp add-json --scope user setussh '{
"type": "http",
"url": "http://127.0.0.1:30838/mcp",
"headers": { "Authorization": "Bearer YOUR_TOKEN" },
"timeout": 660000
}'
Another way is to start Claude Code with the environment variable
CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT=660000. It changes the 5-minute
window for every MCP server at once.