iflow-mcp_mrnugget-tailscale-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: iflow-mcp_mrnugget-tailscale-mcp
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: mcp[cli]>=1.2.0
9
+
10
+ # tailscale-mcp
11
+
12
+ Super small MCP server that allows Claude to query Tailscale status by running
13
+ the `tailscale` CLI on macOS.
14
+
15
+ VERY DRAFTY!
16
+
17
+ ## Requirements
18
+
19
+ - Python
20
+ - Tailscale installed at `/Applications/Tailscale.app/Contents/MacOS/Tailscale`
21
+ - [uv](https://github.com/astral/uv) for dependency management
22
+
23
+ ## Running the Server
24
+
25
+ ### STDIO Transport (Default)
26
+
27
+ Run the server with stdio transport (default):
28
+ ```bash
29
+ python tailscale.py
30
+ ```
31
+
32
+ ### HTTP/SSE Transport
33
+
34
+ Run the server with HTTP transport on a specific port:
35
+ ```bash
36
+ python tailscale.py --transport http --port 4001
37
+ ```
@@ -0,0 +1,6 @@
1
+ tailscale.py,sha256=_dmMJZ46J8fFQIni5o8rMzsa5yTrkgJ9Ld7jkQUB6L8,5946
2
+ iflow_mcp_mrnugget_tailscale_mcp-0.1.0.dist-info/METADATA,sha256=4qypCkBABIIDFHyBJbjiXoRnSxgb6_rjHILmbZIC0Hc,821
3
+ iflow_mcp_mrnugget_tailscale_mcp-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
4
+ iflow_mcp_mrnugget_tailscale_mcp-0.1.0.dist-info/entry_points.txt,sha256=H5dlySkaWdzBcw_p0QcmeFvKT7BxjMYRrpTlRHHG6KE,45
5
+ iflow_mcp_mrnugget_tailscale_mcp-0.1.0.dist-info/top_level.txt,sha256=sSjXPegf2Lhd8o8WmdY7jhvYWb4nrHMGVjOB-P2Qf08,10
6
+ iflow_mcp_mrnugget_tailscale_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tailscale = tailscale:main
tailscale.py ADDED
@@ -0,0 +1,193 @@
1
+ from typing import Any
2
+ import subprocess
3
+ import json
4
+ from dataclasses import dataclass
5
+ from mcp.server.fastmcp import FastMCP
6
+ import argparse
7
+ import os
8
+
9
+ # Initialize FastMCP server
10
+ mcp = FastMCP("tailscale")
11
+
12
+ # Constants
13
+ TAILSCALE_PATH = "/Applications/Tailscale.app/Contents/MacOS/Tailscale"
14
+
15
+ # Mock data for testing when Tailscale CLI is not available
16
+ MOCK_TAILSCALE_STATUS = {
17
+ "Self": {
18
+ "TailscaleIPs": ["100.100.100.100"],
19
+ "HostName": "test-device",
20
+ "DNSName": "test-device.tailnet.ts.net",
21
+ "UserID": 0,
22
+ "OS": "linux",
23
+ "Online": True,
24
+ "RxBytes": 1024000,
25
+ "TxBytes": 2048000
26
+ },
27
+ "Peer": {
28
+ "1": {
29
+ "TailscaleIPs": ["100.100.100.101"],
30
+ "HostName": "peer-device",
31
+ "DNSName": "peer-device.tailnet.ts.net",
32
+ "UserID": 0,
33
+ "OS": "macos",
34
+ "Online": True,
35
+ "LastSeen": "2024-01-01T00:00:00Z",
36
+ "RxBytes": 512000,
37
+ "TxBytes": 1024000
38
+ }
39
+ },
40
+ "User": {
41
+ "0": {
42
+ "LoginName": "testuser@example.com"
43
+ }
44
+ }
45
+ }
46
+
47
+ @dataclass
48
+ class TailscaleDevice:
49
+ ip: str
50
+ name: str
51
+ dns_name: str
52
+ user: str
53
+ os: str
54
+ status: str
55
+ last_seen: str
56
+ rx_bytes: int
57
+ tx_bytes: int
58
+
59
+ def parse_tailscale_status(json_data: dict) -> list[TailscaleDevice]:
60
+ """Parse the JSON output of tailscale status into structured data."""
61
+ devices = []
62
+
63
+ # Add self device
64
+ self_device = json_data["Self"]
65
+ devices.append(TailscaleDevice(
66
+ ip=self_device["TailscaleIPs"][0],
67
+ name=self_device["HostName"],
68
+ dns_name=self_device["DNSName"],
69
+ user=json_data["User"][str(self_device["UserID"])]["LoginName"],
70
+ os=self_device["OS"],
71
+ status="online" if self_device["Online"] else "offline",
72
+ last_seen="current device",
73
+ rx_bytes=self_device["RxBytes"],
74
+ tx_bytes=self_device["TxBytes"]
75
+ ))
76
+
77
+ # Add peer devices
78
+ for peer in json_data["Peer"].values():
79
+ devices.append(TailscaleDevice(
80
+ ip=peer["TailscaleIPs"][0],
81
+ name=peer["HostName"],
82
+ dns_name=peer["DNSName"],
83
+ user=json_data["User"][str(peer["UserID"])]["LoginName"],
84
+ os=peer["OS"],
85
+ status="online" if peer["Online"] else "offline",
86
+ last_seen=peer["LastSeen"],
87
+ rx_bytes=peer["RxBytes"],
88
+ tx_bytes=peer["TxBytes"]
89
+ ))
90
+
91
+ return devices
92
+
93
+ def run_tailscale_command(args: list[str]) -> dict:
94
+ """Run a Tailscale command and return its JSON output."""
95
+ try:
96
+ # Check if Tailscale CLI is available
97
+ if not os.path.exists(TAILSCALE_PATH):
98
+ # Use mock data for testing
99
+ return MOCK_TAILSCALE_STATUS
100
+
101
+ result = subprocess.run(
102
+ [TAILSCALE_PATH] + args + ["--json"],
103
+ capture_output=True,
104
+ text=True,
105
+ check=True
106
+ )
107
+ return json.loads(result.stdout)
108
+ except subprocess.CalledProcessError as e:
109
+ raise Exception(f"Error running Tailscale command: {e.stderr}")
110
+ except json.JSONDecodeError as e:
111
+ raise Exception(f"Error parsing Tailscale JSON output: {str(e)}")
112
+
113
+ @mcp.tool()
114
+ async def get_tailscale_status() -> str:
115
+ """Get the status of all Tailscale devices in your network."""
116
+ try:
117
+ output = run_tailscale_command(["status"])
118
+ devices = parse_tailscale_status(output)
119
+
120
+ if not devices:
121
+ return "No Tailscale devices found."
122
+
123
+ # Format the response in a readable way
124
+ response_parts = ["Your Tailscale Network Devices:"]
125
+ for device in devices:
126
+ traffic = ""
127
+ if device.rx_bytes > 0 or device.tx_bytes > 0:
128
+ traffic = f"\nTraffic: rx {device.rx_bytes:,} bytes, tx {device.tx_bytes:,} bytes"
129
+
130
+ status_info = f"""
131
+ Device: {device.name}
132
+ DNS Name: {device.dns_name}
133
+ IP: {device.ip}
134
+ User: {device.user}
135
+ OS: {device.os}
136
+ Status: {device.status}
137
+ Last seen: {device.last_seen}{traffic}
138
+ """
139
+ response_parts.append(status_info)
140
+
141
+ return "\n---\n".join(response_parts)
142
+ except Exception as e:
143
+ return f"Error getting Tailscale status: {str(e)}"
144
+
145
+ @mcp.tool()
146
+ async def get_device_info(device_name: str) -> str:
147
+ """Get detailed information about a specific Tailscale device.
148
+
149
+ Args:
150
+ device_name: The name of the Tailscale device to query
151
+ """
152
+ try:
153
+ output = run_tailscale_command(["status"])
154
+ devices = parse_tailscale_status(output)
155
+
156
+ for device in devices:
157
+ if device.name.lower() == device_name.lower():
158
+ traffic = ""
159
+ if device.rx_bytes > 0 or device.tx_bytes > 0:
160
+ traffic = f"\nTraffic:\n Received: {device.rx_bytes:,} bytes\n Transmitted: {device.tx_bytes:,} bytes"
161
+
162
+ return f"""
163
+ Detailed information for {device.name}:
164
+ DNS Name: {device.dns_name}
165
+ IP Address: {device.ip}
166
+ User: {device.user}
167
+ Operating System: {device.os}
168
+ Current Status: {device.status}
169
+ Last Seen: {device.last_seen}{traffic}
170
+ """
171
+
172
+ return f"No device found with name: {device_name}"
173
+ except Exception as e:
174
+ return f"Error getting device info: {str(e)}"
175
+
176
+ def main():
177
+ """Main entry point for the MCP server."""
178
+ parser = argparse.ArgumentParser(description='Run Tailscale MCP server')
179
+ parser.add_argument('--transport', choices=['stdio', 'http'], default='stdio',
180
+ help='Transport type (stdio or http)')
181
+ parser.add_argument('--port', type=int, default=3000,
182
+ help='Port for HTTP server (only used with --transport http)')
183
+ args = parser.parse_args()
184
+
185
+ if args.transport == 'http':
186
+ mcp.settings.port = args.port
187
+ mcp.run(transport='sse')
188
+ else:
189
+ # Run with stdio transport
190
+ mcp.run(transport='stdio')
191
+
192
+ if __name__ == "__main__":
193
+ main()