187 lines
6.3 KiB
Go
187 lines
6.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"forge-tools-ssh/internal/ssh"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
var mgr *ssh.Manager
|
|
|
|
// SetManager binds the single SSH Manager for the process.
|
|
// forge держит 1 MCP-подпроцесс/агента (stdio), поэтому менеджер один.
|
|
func SetManager(m *ssh.Manager) {
|
|
mgr = m
|
|
}
|
|
|
|
// manager возвращает одиночный SSH Manager процесса.
|
|
func manager() *ssh.Manager {
|
|
return mgr
|
|
}
|
|
|
|
// registerCoreTools регистрирует базовые SSH-инструменты.
|
|
func registerCoreTools(s *mcp.Server) {
|
|
// connect
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "connect",
|
|
Description: "Establish an SSH connection to a remote host. Используй profile (имя из ssh.json), чтобы подключиться по предопределённому профилю; либо host+username для ad-hoc (host должен пройти allowlist политики).",
|
|
InputSchema: schema(map[string]any{
|
|
"profile": strProps("Имя профиля из per-agent ssh.json (креды задаёт оператор)", false),
|
|
"host": strProps("Hostname or IP address", false),
|
|
"username": strProps("SSH username", false),
|
|
"port": intProps("SSH port (default: 22)", false),
|
|
"password": strProps("SSH password (optional if using key)", false),
|
|
"private_key_path": strProps("Path to private key file", false),
|
|
"alias": strProps("Connection alias (auto-generated if not provided)", false),
|
|
"via": strProps("Jump host alias for tunneling", false),
|
|
"target": strProps("Optional destination host routed through a PAM gateway (SafeInspect). The gateway receives 'username@target' and proxies the session.", false),
|
|
"target_password": strProps("Password of the target server account, requested by the PAM gateway as a second authentication stage", false),
|
|
}, nil),
|
|
}, connectHandler)
|
|
|
|
// disconnect
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "disconnect",
|
|
Description: "Close an SSH connection",
|
|
InputSchema: schema(map[string]any{
|
|
"alias": strProps("Connection alias to disconnect (all if empty)", false),
|
|
}, nil),
|
|
}, disconnectHandler)
|
|
|
|
// run
|
|
registerPatternTool(s, &mcp.Tool{
|
|
Name: "run",
|
|
Description: "Execute a shell command on the remote host. Use timeout for long-running tasks.",
|
|
InputSchema: schema(map[string]any{
|
|
"command": strProps("Shell command to execute", true),
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
"timeout": intProps("Command timeout in seconds (default: 120)", false),
|
|
}, []string{"command"}),
|
|
}, runPatternsFn, runHandler)
|
|
|
|
// identity
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "identity",
|
|
Description: "Get the server's public SSH key for authorized_keys",
|
|
InputSchema: schema(nil, nil),
|
|
}, identityHandler)
|
|
|
|
// info
|
|
s.AddTool(&mcp.Tool{
|
|
Name: "info",
|
|
Description: "Get remote system information (OS, kernel, hostname)",
|
|
InputSchema: schema(map[string]any{
|
|
"target": strProps("Connection alias (default: primary)", false),
|
|
}, nil),
|
|
}, infoHandler)
|
|
}
|
|
|
|
func connectHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
|
|
args := requestArgs(req)
|
|
opts := ssh.ConnectOptions{
|
|
Host: getString(args, "host", ""),
|
|
Username: getString(args, "username", ""),
|
|
Port: getInt(args, "port", 22),
|
|
Password: getString(args, "password", ""),
|
|
PrivateKeyPath: getString(args, "private_key_path", ""),
|
|
Alias: getString(args, "alias", ""),
|
|
Via: getString(args, "via", ""),
|
|
Target: getString(args, "target", ""),
|
|
TargetPassword: getString(args, "target_password", ""),
|
|
Profile: getString(args, "profile", ""),
|
|
}
|
|
|
|
resultAlias, err := m.Connect(ctx, opts)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
|
|
dest := opts.Host
|
|
if opts.Target != "" {
|
|
dest = fmt.Sprintf("%s via PAM -> %s", opts.Host, opts.Target)
|
|
}
|
|
return textResult(fmt.Sprintf("Connected to %s@%s (alias: %s)", opts.Username, dest, resultAlias)), nil
|
|
}
|
|
|
|
func disconnectHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
|
|
alias := getString(requestArgs(req), "alias", "")
|
|
msg, err := m.Disconnect(alias)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
return textResult(msg), nil
|
|
}
|
|
|
|
func runHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
|
|
args := requestArgs(req)
|
|
command := getString(args, "command", "")
|
|
if command == "" {
|
|
return errorResult("command is required"), nil
|
|
}
|
|
target := getString(args, "target", "primary")
|
|
timeout := getInt(args, "timeout", 120)
|
|
|
|
if timeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
|
|
defer cancel()
|
|
}
|
|
|
|
output, err := m.Execute(ctx, command, target)
|
|
if err != nil {
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return errorResult(fmt.Sprintf("Command timed out after %ds", timeout)), nil
|
|
}
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
return textResult(output), nil
|
|
}
|
|
|
|
func identityHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
|
|
pubKey, err := m.GetPublicKey()
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
formatted := fmt.Sprintf("SSH Public Key:\n\n```\n%s```\n\nAdd this to ~/.ssh/authorized_keys on remote servers.", pubKey)
|
|
return textResult(formatted), nil
|
|
}
|
|
|
|
func infoHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
m := manager()
|
|
if m == nil {
|
|
return errorResult("no SSH manager initialized"), nil
|
|
}
|
|
|
|
target := getString(requestArgs(req), "target", "primary")
|
|
cmd := `echo "Hostname: $(hostname)"; echo "OS: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d'"' -f2 || uname -s)"; echo "Kernel: $(uname -r)"; echo "Arch: $(uname -m)"; echo "Shell: $SHELL"`
|
|
output, err := m.Execute(ctx, cmd, target)
|
|
if err != nil {
|
|
return errorResult(err.Error()), nil
|
|
}
|
|
return textResult(output), nil
|
|
}
|