Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,82 @@ graph TB
style Row1 fill:none,stroke:none
style Row2 fill:none,stroke:none
```

## Where Each Component Runs

Understanding where the **client**, **MCP server**, and **target Linux system** run is helpful for deployment and development alike.

| Component | Location | Description |
|-----------|----------|-------------|
| **Client** | User's machine | The MCP client (Cursor, Claude Desktop, Goose, etc.) runs on the machine where the user works. It spawns the Linux MCP Server as a **subprocess** and communicates over **stdio** (stdin/stdout) using the MCP protocol (JSON-RPC). |
| **MCP Server** | Same host as client, or in a container | The server process is started by the client (e.g. `linux-mcp-server` or `podman run ... linux-mcp-server`). With a native install it runs on the **same machine as the client**. With a container deploy it runs **inside the container** on that same machine. In both cases the server receives tool calls over stdio and performs command execution. |
| **Target Linux system** | Same host as client, or any host reachable via SSH | Commands are executed either **locally** on the same host where the MCP server is running (subprocess or container), or **remotely** on another machine. Remote execution is done by the server opening an **SSH connection from the server host to the target host** and running commands there. The client never talks to the target directly. |

This means:

- **Client ↔ Server**: Communication is **always** over stdio, using the MCP JSON-RPC protocol. The client does not connect to the target directly.
- **Server ↔ Target**: If `host` is omitted, execution is **local** (same host as the server). If `host` is set, the server **SSHs from its own host** to that host to run commands.
- **Containers**: When the server runs in a container, “local” means inside the container. Local execution can be disabled via the `disallow_local_execution_in_containers` decorator (tools then require a `host` parameter for remote SSH).

## Detailed Tool Call Flow

End-to-end flow of a single MCP tool call: from the client request through the server to command execution on the target, and back.

```mermaid
sequenceDiagram
participant User
participant Client as MCP Client<br/>(e.g. Cursor)
participant LLM as LLM Service
participant Server as MCP Server Process<br/>(same host or container)
participant FastMCP as FastMCP
participant Tool as Tool (e.g. get_system_information)
participant Cmd as CommandSpec / COMMANDS
participant Exec as execute_command
participant Target as Target Linux System<br/>(local or remote)

User->>Client: "What's my system info?"
Client->>LLM: Send prompt request
LLM->>Client: Decide to call tool
Client->>Server: MCP JSON-RPC: tools/call get_system_information
Note over Client,Server: stdio (stdin/stdout)

Server->>FastMCP: Dispatch tool call
FastMCP->>Tool: get_system_information(host=...)
Tool->>Tool: @log_tool_call, @disallow_local_execution_in_containers
Tool->>Cmd: get_command_group("system_info") etc.
Tool->>Cmd: cmd.run(host=host) for each subcommand

Cmd->>Exec: execute_command(command, host=host)

alt host is None (local)
Exec->>Target: asyncio.create_subprocess_exec (local)
Note over Server,Target: Target = same host as server
else host is set (remote)
Exec->>Server: SSHConnectionManager.get_connection(host)
Exec->>Target: conn.run(cmd) over SSH
Note over Server,Target: Target = remote host
end

Target-->>Exec: return_code, stdout, stderr
Exec-->>Cmd: return code, stdout, stderr
Cmd-->>Tool: results
Tool->>Tool: parse_*, format_*
Tool-->>FastMCP: formatted string
FastMCP-->>Server: MCP response
Server-->>Client: JSON-RPC result
Client->>LLM: Tool call result
LLM->>Client: Inference completion
Client->>User: Answer with system info
```

1. User asks a question in the MCP client (e.g. Cursor).
2. Client sends the prompt to the LLM; the LLM decides to call a tool.
3. Client sends MCP JSON-RPC `tools/call` (e.g. `get_system_information`) over **stdio** to the MCP server (same host or container).
4. FastMCP dispatches the call to the tool.
5. Tool runs decorators (`@log_tool_call`, `@disallow_local_execution_in_containers`), resolves commands via `CommandSpec`/`COMMANDS`, and invokes `cmd.run(host=host)` for each subcommand.
6. `execute_command` runs on the target: **local** (`asyncio.create_subprocess_exec` when `host` is omitted) or **remote** (SSH via `SSHConnectionManager` when `host` is set).
7. Target returns return code, stdout, and stderr to `execute_command`.
8. Results flow back: Exec → Cmd → Tool; tool parses and formats (e.g. `parse_*`, `format_*`).
9. Tool returns formatted string to FastMCP; server sends MCP response over stdio to the client.
10. Client passes the tool result to the LLM; LLM produces the answer; client presents it to the user.