SetuSSH logo SetuSSH

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

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 top or vim;
  • 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

  1. Open Settings (Ctrl+,, or Cmd+, on macOS) and click AI access.
  2. Turn on Let AI clients use this SetuSSH. The Status line at the bottom reads Listening on 127.0.0.1:30838.
  3. 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_servers works. 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.

  1. In Claude Desktop, open Settings, then Developer, and click Edit Config. That opens claude_desktop_config.json.
  2. Add setussh under mcpServers:
    {
      "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"
          }
        }
      }
    }
  3. 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”

  1. connect_server with server set to prod-web-01. A tab for it opens in SetuSSH, or an open one is reused.
  2. run_command with that terminal and df -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”

  1. connect_server for db-primary.
  2. run_command with your project’s status command, such as php artisan migrate:status or rails db:migrate:status, in the right folder. If the client does not know the command, it asks you or looks for it with list_dir and read_file. Name the command in your request to save a step.
  3. It reads the output and tells you which migrations have not run.

“Tail the nginx error log on staging for the last 50 lines”

  1. run_command with server set to staging and tail -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”

  1. connect_server for staging.
  2. send_input with text set to ./deploy.sh and enter true. The reply ends with the last lines on the screen, for example Continue? [y/N].
  3. send_input with text y and enter true, then read_terminal until the script has finished.
  4. If something hangs, send_input with keys ["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.1 only. 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 Origin header, 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.