spooling 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {spooling-0.2.2/spooling.egg-info → spooling-0.2.3}/PKG-INFO +1 -1
- {spooling-0.2.2 → spooling-0.2.3}/pyproject.toml +1 -1
- {spooling-0.2.2 → spooling-0.2.3}/spooling/tunnel.py +91 -19
- {spooling-0.2.2 → spooling-0.2.3/spooling.egg-info}/PKG-INFO +1 -1
- {spooling-0.2.2 → spooling-0.2.3}/LICENSE +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/README.md +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/setup.cfg +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/__init__.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/agent.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/classifiers.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/cli.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/cloud.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/config.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/__init__.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/base.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/bigquery.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/clickhouse.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/factory.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/mongodb.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/mysql.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/postgresql.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/rest/__init__.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/rest/base.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/rest/shopify_connectors.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/snowflake.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/connectors/types.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/db.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/embeddings.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/evals.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/experiments.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/ingest.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/mcp_server.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/parser.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/pricing.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/__init__.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/antigravity.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/base.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/codex.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/copilot.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/cortex_code.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/cursor.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/gemini.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/github.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/gitlab.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/kiro.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/opencode.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/session_file.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/providers/windsurf.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/redact.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/remote_otel.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/schema.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/sdk.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/search.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/server.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/stats.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/subscription_pricing.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/tracing.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/tunnel_auth.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling/watcher.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling.egg-info/SOURCES.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling.egg-info/dependency_links.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling.egg-info/entry_points.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling.egg-info/requires.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/spooling.egg-info/top_level.txt +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/tests/test_mcp_auth_middleware.py +0 -0
- {spooling-0.2.2 → spooling-0.2.3}/tests/test_tunnel_auth.py +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"""Cloudflare tunnel support for exposing local MCP servers."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
3
5
|
import re
|
|
6
|
+
import stat
|
|
4
7
|
import subprocess
|
|
5
8
|
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
6
11
|
from typing import Optional, Tuple
|
|
7
12
|
|
|
8
13
|
from rich.console import Console
|
|
@@ -12,19 +17,85 @@ console = Console()
|
|
|
12
17
|
# Pattern to match Cloudflare quick tunnel URLs
|
|
13
18
|
TUNNEL_URL_PATTERN = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
|
|
14
19
|
|
|
20
|
+
# Where we cache the auto-downloaded binary
|
|
21
|
+
_BIN_DIR = Path.home() / ".spooling" / "bin"
|
|
22
|
+
_CLOUDFLARED_BIN = _BIN_DIR / ("cloudflared.exe" if platform.system() == "Windows" else "cloudflared")
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
# Download URL templates per platform/arch
|
|
25
|
+
_DOWNLOAD_URLS: dict[tuple[str, str], str] = {
|
|
26
|
+
("Darwin", "arm64"): "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64",
|
|
27
|
+
("Darwin", "x86_64"): "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64",
|
|
28
|
+
("Linux", "aarch64"):"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64",
|
|
29
|
+
("Linux", "x86_64"): "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64",
|
|
30
|
+
("Windows", "AMD64"): "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cloudflared_cmd() -> Optional[str]:
|
|
35
|
+
"""Return the path to a usable cloudflared binary, or None."""
|
|
36
|
+
# 1. Already on PATH
|
|
18
37
|
try:
|
|
19
|
-
result = subprocess.run(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
text=True,
|
|
23
|
-
timeout=5,
|
|
24
|
-
)
|
|
25
|
-
return result.returncode == 0
|
|
38
|
+
result = subprocess.run(["cloudflared", "version"], capture_output=True, timeout=5)
|
|
39
|
+
if result.returncode == 0:
|
|
40
|
+
return "cloudflared"
|
|
26
41
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
27
|
-
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# 2. Previously auto-downloaded
|
|
45
|
+
if _CLOUDFLARED_BIN.exists():
|
|
46
|
+
try:
|
|
47
|
+
result = subprocess.run([str(_CLOUDFLARED_BIN), "version"], capture_output=True, timeout=5)
|
|
48
|
+
if result.returncode == 0:
|
|
49
|
+
return str(_CLOUDFLARED_BIN)
|
|
50
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _auto_install_cloudflared() -> Optional[str]:
|
|
57
|
+
"""Download the cloudflared binary for this platform into ~/.spooling/bin/.
|
|
58
|
+
|
|
59
|
+
Returns the path to the binary on success, None on failure.
|
|
60
|
+
"""
|
|
61
|
+
system = platform.system()
|
|
62
|
+
machine = platform.machine()
|
|
63
|
+
url = _DOWNLOAD_URLS.get((system, machine))
|
|
64
|
+
|
|
65
|
+
if not url:
|
|
66
|
+
console.print(f"[red]No cloudflared binary available for {system}/{machine}.[/red]")
|
|
67
|
+
console.print("Install manually: https://developers.cloudflare.com/cloudflared/install/")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
_BIN_DIR.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
console.print(f"[bold]Downloading cloudflared for {system}/{machine}...[/bold]")
|
|
73
|
+
try:
|
|
74
|
+
urllib.request.urlretrieve(url, _CLOUDFLARED_BIN)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
console.print(f"[red]Download failed:[/red] {e}")
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# Make executable on Unix
|
|
80
|
+
if system != "Windows":
|
|
81
|
+
_CLOUDFLARED_BIN.chmod(_CLOUDFLARED_BIN.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
82
|
+
|
|
83
|
+
# Verify
|
|
84
|
+
try:
|
|
85
|
+
result = subprocess.run([str(_CLOUDFLARED_BIN), "version"], capture_output=True, timeout=5)
|
|
86
|
+
if result.returncode == 0:
|
|
87
|
+
console.print(f"[green]cloudflared installed to {_CLOUDFLARED_BIN}[/green]")
|
|
88
|
+
return str(_CLOUDFLARED_BIN)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
console.print("[red]Downloaded binary did not run correctly.[/red]")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def check_cloudflared() -> bool:
|
|
97
|
+
"""Check if cloudflared is available (on PATH or auto-downloaded)."""
|
|
98
|
+
return _cloudflared_cmd() is not None
|
|
28
99
|
|
|
29
100
|
|
|
30
101
|
def _validate_port(port: int) -> bool:
|
|
@@ -49,15 +120,13 @@ def start_tunnel(
|
|
|
49
120
|
console.print(f"[red]Invalid port: {port}. Must be 1-65535.[/red]")
|
|
50
121
|
return None
|
|
51
122
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.print(" Linux: [bold]curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared[/bold]")
|
|
57
|
-
console.print(" Windows: [bold]winget install cloudflare.cloudflared[/bold]")
|
|
123
|
+
cmd_bin = _cloudflared_cmd()
|
|
124
|
+
if not cmd_bin:
|
|
125
|
+
cmd_bin = _auto_install_cloudflared()
|
|
126
|
+
if not cmd_bin:
|
|
58
127
|
return None
|
|
59
128
|
|
|
60
|
-
cmd = [
|
|
129
|
+
cmd = [cmd_bin, "tunnel", "--no-autoupdate", "--url", f"http://localhost:{port}"]
|
|
61
130
|
|
|
62
131
|
console.print(f"[bold]Starting Cloudflare tunnel to localhost:{port}...[/bold]")
|
|
63
132
|
|
|
@@ -142,10 +211,13 @@ def start_tunnel_background(
|
|
|
142
211
|
if not _validate_port(port):
|
|
143
212
|
return None, None
|
|
144
213
|
|
|
145
|
-
|
|
214
|
+
cmd_bin = _cloudflared_cmd()
|
|
215
|
+
if not cmd_bin:
|
|
216
|
+
cmd_bin = _auto_install_cloudflared()
|
|
217
|
+
if not cmd_bin:
|
|
146
218
|
return None, None
|
|
147
219
|
|
|
148
|
-
cmd = [
|
|
220
|
+
cmd = [cmd_bin, "tunnel", "--no-autoupdate", "--url", f"http://localhost:{port}"]
|
|
149
221
|
|
|
150
222
|
process = subprocess.Popen(
|
|
151
223
|
cmd,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|