#
tokens: 1523/50000 5/5 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .python-version
├── pyproject.toml
├── README.md
├── tailscale.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
3.13

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv
.aider.*

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# tailscale-mcp

Super small MCP server that allows Claude to query Tailscale status by running
the `tailscale` CLI on macOS.

VERY DRAFTY!

## Requirements

- Python
- Tailscale installed at `/Applications/Tailscale.app/Contents/MacOS/Tailscale`
- [uv](https://github.com/astral/uv) for dependency management

## Running the Server

### STDIO Transport (Default)

Run the server with stdio transport (default):
```bash
python tailscale.py
```

### HTTP/SSE Transport

Run the server with HTTP transport on a specific port:
```bash
python tailscale.py --transport http --port 4001
```

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "tailscale-mcp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "httpx>=0.28.1",
    "mcp[cli]>=1.2.0",
]

```

--------------------------------------------------------------------------------
/tailscale.py:
--------------------------------------------------------------------------------

```python
from typing import Any
import subprocess
import json
from dataclasses import dataclass
from mcp.server.fastmcp import FastMCP
import argparse

# Initialize FastMCP server
mcp = FastMCP("tailscale")

# Constants
TAILSCALE_PATH = "/Applications/Tailscale.app/Contents/MacOS/Tailscale"

@dataclass
class TailscaleDevice:
    ip: str
    name: str
    dns_name: str
    user: str
    os: str
    status: str
    last_seen: str
    rx_bytes: int
    tx_bytes: int

def parse_tailscale_status(json_data: dict) -> list[TailscaleDevice]:
    """Parse the JSON output of tailscale status into structured data."""
    devices = []

    # Add self device
    self_device = json_data["Self"]
    devices.append(TailscaleDevice(
        ip=self_device["TailscaleIPs"][0],
        name=self_device["HostName"],
        dns_name=self_device["DNSName"],
        user=json_data["User"][str(self_device["UserID"])]["LoginName"],
        os=self_device["OS"],
        status="online" if self_device["Online"] else "offline",
        last_seen="current device",
        rx_bytes=self_device["RxBytes"],
        tx_bytes=self_device["TxBytes"]
    ))

    # Add peer devices
    for peer in json_data["Peer"].values():
        devices.append(TailscaleDevice(
            ip=peer["TailscaleIPs"][0],
            name=peer["HostName"],
            dns_name=peer["DNSName"],
            user=json_data["User"][str(peer["UserID"])]["LoginName"],
            os=peer["OS"],
            status="online" if peer["Online"] else "offline",
            last_seen=peer["LastSeen"],
            rx_bytes=peer["RxBytes"],
            tx_bytes=peer["TxBytes"]
        ))

    return devices

def run_tailscale_command(args: list[str]) -> dict:
    """Run a Tailscale command and return its JSON output."""
    try:
        result = subprocess.run(
            [TAILSCALE_PATH] + args + ["--json"],
            capture_output=True,
            text=True,
            check=True
        )
        return json.loads(result.stdout)
    except subprocess.CalledProcessError as e:
        raise Exception(f"Error running Tailscale command: {e.stderr}")
    except json.JSONDecodeError as e:
        raise Exception(f"Error parsing Tailscale JSON output: {str(e)}")

@mcp.tool()
async def get_tailscale_status() -> str:
    """Get the status of all Tailscale devices in your network."""
    try:
        output = run_tailscale_command(["status"])
        devices = parse_tailscale_status(output)

        if not devices:
            return "No Tailscale devices found."

        # Format the response in a readable way
        response_parts = ["Your Tailscale Network Devices:"]
        for device in devices:
            traffic = ""
            if device.rx_bytes > 0 or device.tx_bytes > 0:
                traffic = f"\nTraffic: rx {device.rx_bytes:,} bytes, tx {device.tx_bytes:,} bytes"

            status_info = f"""
Device: {device.name}
DNS Name: {device.dns_name}
IP: {device.ip}
User: {device.user}
OS: {device.os}
Status: {device.status}
Last seen: {device.last_seen}{traffic}
"""
            response_parts.append(status_info)

        return "\n---\n".join(response_parts)
    except Exception as e:
        return f"Error getting Tailscale status: {str(e)}"

@mcp.tool()
async def get_device_info(device_name: str) -> str:
    """Get detailed information about a specific Tailscale device.

    Args:
        device_name: The name of the Tailscale device to query
    """
    try:
        output = run_tailscale_command(["status"])
        devices = parse_tailscale_status(output)

        for device in devices:
            if device.name.lower() == device_name.lower():
                traffic = ""
                if device.rx_bytes > 0 or device.tx_bytes > 0:
                    traffic = f"\nTraffic:\n  Received: {device.rx_bytes:,} bytes\n  Transmitted: {device.tx_bytes:,} bytes"

                return f"""
Detailed information for {device.name}:
DNS Name: {device.dns_name}
IP Address: {device.ip}
User: {device.user}
Operating System: {device.os}
Current Status: {device.status}
Last Seen: {device.last_seen}{traffic}
"""

        return f"No device found with name: {device_name}"
    except Exception as e:
        return f"Error getting device info: {str(e)}"

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Run Tailscale MCP server')
    parser.add_argument('--transport', choices=['stdio', 'http'], default='stdio',
                      help='Transport type (stdio or http)')
    parser.add_argument('--port', type=int, default=3000,
                      help='Port for HTTP server (only used with --transport http)')
    args = parser.parse_args()

    if args.transport == 'http':
        mcp.settings.port = args.port
        mcp.run(transport='sse')
    else:
        # Run with stdio transport
        mcp.run(transport='stdio')
```