A TypeScript library for executing commands and managing files on remote systems via SSH. Designed for integration with the Claude Agent SDK to provide AI agents with remote system access.
- Bash execution - Run commands in persistent shell sessions with background execution support
- File operations - Read, write, and edit files remotely using SFTP when available, automatically falling back to shell commands if SFTP is unavailable
- Search tools - Grep pattern search and glob file matching
- Type-safe - Full TypeScript support with Zod validation
- Agent SDK integration - Easy integration with Claude Agent SDK via MCP server
- Standalone MCP server - Run as a standalone MCP server with stdio transport
pnpm add @agent-remote/sshThe package includes a standalone MCP server executable that can be run from the command line and communicates over stdio transport. The server is self-contained with all dependencies bundled (except Node.js builtins), making it easy to distribute and run without installing node_modules.
Installation:
npm install -g @agent-remote/sshUsage with command line arguments:
# With password authentication
remote-ssh-mcp --host example.com --username user --password secret
# With private key authentication
remote-ssh-mcp --host example.com --username user --private-key ~/.ssh/id_rsa
# With SSH agent
remote-ssh-mcp --host example.com --username user --agent $SSH_AUTH_SOCKUsage with environment variables:
export SSH_HOST=example.com
export SSH_USERNAME=user
export SSH_PRIVATE_KEY=~/.ssh/id_rsa
remote-ssh-mcpAvailable options:
--host, -h- SSH host (orSSH_HOSTenv var)--port, -p- SSH port, default 22 (orSSH_PORTenv var)--username, -u- SSH username (orSSH_USERNAMEenv var)--password- SSH password (orSSH_PASSWORDenv var)--private-key- Path to private key file (orSSH_PRIVATE_KEYenv var)--passphrase- Passphrase for encrypted private key (orSSH_PASSPHRASEenv var)--agent- SSH agent socket path (orSSH_AUTH_SOCKenv var)--default-keys- Automatically try default SSH keys from~/.ssh, enabled by default (orSSH_DEFAULT_KEYSenv var). Use--no-try-default-keysto disable.--timeout, -t- SSH connection timeout in milliseconds (orSSH_TIMEOUTenv var)
The server exposes all remote tools (bash, read, write, edit, grep, glob, etc.) through the MCP protocol.
Authentication behavior:
The MCP server mimics OpenSSH's authentication behavior:
- If you provide
--passwordor--private-key, it uses that exclusively - If you don't provide explicit credentials, it will:
- Try the SSH agent (from
--agentorSSH_AUTH_SOCK) - Automatically fall back to default SSH keys in
~/.ssh/(id_ed25519, id_ecdsa, id_rsa, id_dsa)
- Try the SSH agent (from
This means you can connect just like OpenSSH without specifying any auth options:
# Just like: ssh user@example.com
remote-ssh-mcp --host example.com --username userTo disable the automatic default key fallback:
remote-ssh-mcp --host example.com --username user --no-default-keysimport { Remote } from '@agent-remote/ssh';
// Connect to remote server
const remote = await Remote.connect({
host: 'example.com',
username: 'user',
privateKey: fs.readFileSync('/path/to/id_rsa'),
});
// Execute commands
const result = await remote.bash.handler({
command: 'ls -la',
});
console.log(result.content[0].text);
// Read files
const fileContent = await remote.read.handler({
file_path: '/etc/hosts',
});
// Write files
await remote.write.handler({
file_path: '/tmp/test.txt',
content: 'Hello, world!',
});
// Clean up
await remote.disconnect();import { Remote } from '@agent-remote/ssh';
const remote = await Remote.connect({
host: 'example.com',
username: 'user',
privateKey: fs.readFileSync('/path/to/id_rsa'),
});
// Create an MCP server with all tools
const server = remote.createSdkMcpServer();
// Use with Agent SDK...const remote = await Remote.connect(config);
// Each tool is accessible as a property with a handler
const tools = [
remote.bash,
remote.bashOutput,
remote.killBash,
remote.grep,
remote.read,
remote.write,
remote.edit,
remote.glob,
];
// You can use individual tools in your own MCP server
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
const customServer = createSdkMcpServer({
name: 'custom-remote-ssh',
version: '1.0.0',
tools: [
tool(
remote.bash.name,
remote.bash.description,
remote.bash.inputSchema,
remote.bash.handler,
),
tool(
remote.read.name,
remote.read.description,
remote.read.inputSchema,
remote.read.handler,
),
tool(
remote.write.name,
remote.write.description,
remote.write.inputSchema,
remote.write.handler,
),
],
});The main class for managing SSH connections and accessing tools.
Establishes a connection to a remote SSH server.
Parameters:
config- SSH connection configuration (see SSH Configuration below)
Returns: A connected Remote instance
Throws: If the SSH connection fails
Note: SFTP is attempted but not required. If SFTP is unavailable, the connection will still succeed but file operations (read, write, edit) will return error messages when called. Bash, grep, and glob tools work without SFTP.
Example:
const remote = await Remote.connect({
host: 'example.com',
username: 'user',
password: 'secret',
});Closes the SSH connection and SFTP session (if present).
Example:
await remote.disconnect();Creates an MCP server with all remote tools from this Remote instance.
Returns: An MCP server instance from the Claude Agent SDK
Example:
const server = remote.createSdkMcpServer();Each tool is accessed via a getter property that returns a tool definition with
name, description, inputSchema, and handler properties.
Executes commands in a persistent shell session.
Input:
{
command: string; // The command to execute
timeout?: number; // Optional timeout in milliseconds (max 600000)
description?: string; // Description of what the command does
run_in_background?: boolean; // Run in background
}Output:
{
output: string; // Combined stdout and stderr
exitCode?: number; // Exit code (optional)
signal?: string; // Signal used to terminate the command
killed?: boolean; // Whether the command was killed due to timeout
shellId?: string; // Shell ID if background execution
}Retrieves output from a running or completed background bash shell.
Input:
{
shell_id: string; // Shell ID to retrieve output from
}Output:
{
output: string; // New output since last check
status: 'running' | 'completed'; // Shell status
exitCode?: number; // Exit code (when completed)
signal?: string; // Signal (when completed)
}Kills a running background shell by its ID.
Input:
{
shell_id: string; // Shell ID to kill
signal?: string; // Signal to send (e.g., 'SIGTERM', 'SIGKILL')
}Output:
{
killed: boolean; // Whether the shell was killed
}Searches for patterns in files or directories.
Input:
{
pattern: string; // Regular expression pattern
path: string; // File or directory to search
glob?: string; // Glob pattern to filter files
output_mode?: 'content' | 'files_with_matches' | 'count';
'-B'?: number; // Lines of context before match
'-A'?: number; // Lines of context after match
'-C'?: number; // Lines of context before and after
'-n'?: boolean; // Show line numbers
'-i'?: boolean; // Case insensitive
head_limit?: number; // Limit output lines
}Output:
{
mode: 'content' | 'files_with_matches' | 'count';
content?: string; // Matching lines (if mode is 'content')
filenames?: string[]; // Matching files (if mode is 'files_with_matches')
numFiles?: number; // Number of files (if mode is 'files_with_matches')
numMatches?: number; // Number of matches (if mode is 'count')
}Reads files from the remote filesystem. Automatically uses SFTP when available,
or falls back to cat command.
Input:
{
file_path: string; // Absolute path to file
offset?: number; // Line number to start reading from
limit?: number; // Number of lines to read
}Output:
{
content: string; // File content
numLines: number; // Number of lines read
startLine: number; // Starting line number
totalLines: number; // Total lines in file
}Implementation:
- SFTP mode: Uses
sftp.readFile()for direct file access - Shell fallback: Uses
catcommand to read file contents
Writes content to files on the remote filesystem. Automatically uses SFTP when
available, or falls back to printf command with proper escaping.
Input:
{
file_path: string; // Absolute path to file
content: string; // Content to write
}Output:
{
content: string; // Content that was written
}Implementation:
- SFTP mode: Uses
sftp.writeFile()for direct file access - Shell fallback: Uses
printf '%s' '...'with single-quote escaping to safely handle special characters, newlines, and unicode
Edits files by replacing text on the remote filesystem. Automatically uses SFTP when available, or falls back to shell commands.
Input:
{
file_path: string; // Absolute path to file
old_string: string; // Text to find
new_string: string; // Text to replace with
replace_all?: boolean; // Replace all occurrences
}Output:
{
replacements: number; // Number of replacements made
diff: StructuredPatch; // Unified diff of changes
}Implementation:
- SFTP mode: Uses
sftp.readFile()andsftp.writeFile()for file access - Shell fallback: Uses
catto read, performs replacement locally, then usesprintfto write back
Searches for files matching glob patterns.
Input:
{
base_path: string; // Absolute base path to search from
pattern: string; // Glob pattern (e.g., '**/*.ts')
include_hidden?: boolean; // Include hidden files
}Output:
{
matches: string[]; // List of matching file paths
count: number; // Number of matches
}The ConnectConfig type from the ssh2
library supports various authentication methods:
const remote = await Remote.connect({
host: 'example.com',
port: 22,
username: 'user',
password: 'secret',
});import fs from 'fs';
const remote = await Remote.connect({
host: 'example.com',
port: 22,
username: 'user',
privateKey: fs.readFileSync('/path/to/id_rsa'),
passphrase: 'optional-passphrase', // If key is encrypted
});const remote = await Remote.connect({
host: 'example.com',
port: 22,
username: 'user',
agent: process.env.SSH_AUTH_SOCK,
});When using the MCP server (not the library directly), if you don't provide
explicit credentials, the server automatically tries default SSH keys from
~/.ssh/ as a fallback. This mimics OpenSSH behavior:
# MCP server will try SSH agent, then fall back to ~/.ssh/id_ed25519, etc.
remote-ssh-mcp --host example.com --username userThis automatic fallback can be disabled with --no-default-keys.
const remote = await Remote.connect({
host: 'example.com',
port: 22,
username: 'user',
privateKey: fs.readFileSync('/path/to/id_rsa'),
keepaliveInterval: 10000,
readyTimeout: 20000,
algorithms: {
kex: ['ecdh-sha2-nistp256'],
cipher: ['aes128-ctr'],
},
});For all available configuration options, see the ssh2 documentation.
try {
const remote = await Remote.connect(config);
try {
const result = await remote.bash.handler({
command: 'some-command',
});
if (result.isError) {
console.error('Command failed:', result.content[0].text);
} else {
console.log('Success:', result.content[0].text);
}
} finally {
await remote.disconnect();
}
} catch (error) {
console.error('Connection failed:', error);
}const remote = await Remote.connect(config);
// Start a long-running command in the background
const startResult = await remote.bash.handler({
command: 'npm run build',
run_in_background: true,
description: 'Building project',
});
const { shellId } = startResult.structuredContent;
// Check output periodically
const checkOutput = async () => {
const output = await remote.bashOutput.handler({ shell_id: shellId });
console.log(output.content[0].text);
if (output.structuredContent.status === 'running') {
setTimeout(checkOutput, 1000);
}
};
checkOutput();const remote = await Remote.connect(config);
// Find all TypeScript files
const files = await remote.glob.handler({
base_path: '/home/user/project',
pattern: '**/*.ts',
});
// Search for a pattern
const matches = await remote.grep.handler({
pattern: 'TODO',
path: '/home/user/project',
glob: '*.ts',
output_mode: 'content',
'-n': true,
});
// Edit a file
await remote.edit.handler({
file_path: '/home/user/project/config.ts',
old_string: 'localhost',
new_string: 'example.com',
replace_all: true,
});Apache-2.0