compresh-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,26 @@
1
+ """compresh-mcp — MCP server for Compresh paid tier.
2
+
3
+ Bundles the open-source tulbase compression core (MIT, vendored as
4
+ ``compresh_mcp.tulbase``) and adds the proprietary TUL 1.0 layer:
5
+
6
+ - Q-protective sentence ranking (Q1-Q4 categorization)
7
+ - Epistemic marker classification (VR/HR/CR/UC)
8
+ - Semantic store (cross-turn Q3 dedup)
9
+ - Auth + saving telemetry to Compresh dashboard
10
+
11
+ The vendored tulbase code is the canonical reference (MIT, © Compresh Ltd 2026,
12
+ see ``tulbase/LICENSE``). For the standalone tulbase distribution, see
13
+ https://github.com/compresh/tulbase.
14
+
15
+ For pricing and account management, see https://compre.sh.
16
+ """
17
+
18
+ from . import tulbase
19
+
20
+ __version__ = "0.1.0"
21
+ __author__ = "Compresh Ltd"
22
+ __license__ = "BUSL-1.1"
23
+
24
+ from .server import main as run_server
25
+
26
+ __all__ = ["__version__", "run_server", "tulbase"]
compresh_mcp/auth.py ADDED
@@ -0,0 +1,176 @@
1
+ """Compresh API key validation + usage telemetry.
2
+
3
+ The MCP server validates the user's COMPRESH_API_KEY against the
4
+ Compresh production API at startup, then reports per-session savings
5
+ back so the dashboard reflects local usage.
6
+
7
+ The Compresh API base URL is configurable via COMPRESH_API_BASE
8
+ (default: https://api.compre.sh) — useful for staging/dev environments.
9
+
10
+ Validation flow:
11
+
12
+ 1. Read COMPRESH_API_KEY from environment.
13
+ 2. If missing/empty -> raise NoApiKey (caller triggers onboarding flow).
14
+ 3. POST to /v1/auth/verify with Bearer token.
15
+ 4. If 200 -> cache validated key + user info for session lifetime.
16
+ 5. If 401/403 -> raise InvalidApiKey (caller may re-prompt).
17
+ 6. If network failure -> raise AuthNetworkError (caller may proceed
18
+ in offline mode with a warning).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ import os
25
+ from dataclasses import dataclass
26
+ from typing import Optional
27
+
28
+ import httpx
29
+
30
+ logger = logging.getLogger("compresh-mcp.auth")
31
+
32
+ DEFAULT_API_BASE = os.environ.get("COMPRESH_API_BASE", "https://api.compre.sh")
33
+ DEFAULT_TIMEOUT = float(os.environ.get("COMPRESH_AUTH_TIMEOUT", "10"))
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Exceptions
38
+ # ---------------------------------------------------------------------------
39
+
40
+
41
+ class AuthError(Exception):
42
+ """Base class for auth failures."""
43
+
44
+
45
+ class NoApiKey(AuthError):
46
+ """COMPRESH_API_KEY environment variable missing or empty."""
47
+
48
+
49
+ class InvalidApiKey(AuthError):
50
+ """API key was rejected by the Compresh server (401/403)."""
51
+
52
+
53
+ class AuthNetworkError(AuthError):
54
+ """Network or unexpected error while contacting Compresh server."""
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Result dataclass
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ @dataclass(slots=True)
63
+ class AuthResult:
64
+ """Outcome of a successful key validation."""
65
+
66
+ ok: bool
67
+ api_key: str
68
+ email: Optional[str] = None
69
+ tier: Optional[str] = None # "free" | "pro"
70
+ free_credit_remaining: Optional[float] = None
71
+ budget_remaining: Optional[float] = None
72
+ error: Optional[str] = None
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Public API
77
+ # ---------------------------------------------------------------------------
78
+
79
+
80
+ def get_api_key() -> Optional[str]:
81
+ """Read COMPRESH_API_KEY from environment, return None if missing/empty."""
82
+ key = (os.environ.get("COMPRESH_API_KEY") or "").strip()
83
+ return key or None
84
+
85
+
86
+ def verify_api_key(
87
+ api_key: Optional[str] = None,
88
+ *,
89
+ api_base: str = DEFAULT_API_BASE,
90
+ timeout: float = DEFAULT_TIMEOUT,
91
+ ) -> AuthResult:
92
+ """Validate an API key against the Compresh server.
93
+
94
+ Raises:
95
+ NoApiKey: key is missing/empty.
96
+ InvalidApiKey: key was rejected (401/403).
97
+ AuthNetworkError: transport-level failure (caller decides whether
98
+ to proceed in offline mode).
99
+ """
100
+ key = api_key or get_api_key()
101
+ if not key:
102
+ raise NoApiKey("COMPRESH_API_KEY environment variable is not set")
103
+
104
+ url = f"{api_base.rstrip('/')}/v1/auth/verify"
105
+ headers = {"Authorization": f"Bearer {key}", "Accept": "application/json"}
106
+
107
+ try:
108
+ with httpx.Client(timeout=timeout) as client:
109
+ resp = client.get(url, headers=headers)
110
+ except httpx.HTTPError as e:
111
+ logger.warning("auth network error: %s", e)
112
+ raise AuthNetworkError(f"network error: {type(e).__name__}: {e}") from e
113
+
114
+ if resp.status_code in (401, 403):
115
+ logger.info("auth rejected: %d", resp.status_code)
116
+ raise InvalidApiKey(f"server rejected key (HTTP {resp.status_code})")
117
+
118
+ if resp.status_code != 200:
119
+ logger.warning("auth unexpected status: %d", resp.status_code)
120
+ raise AuthNetworkError(f"unexpected HTTP {resp.status_code}")
121
+
122
+ data = resp.json() if resp.content else {}
123
+ return AuthResult(
124
+ ok=True,
125
+ api_key=key,
126
+ email=data.get("email"),
127
+ tier=data.get("tier"),
128
+ free_credit_remaining=data.get("free_credit_remaining"),
129
+ budget_remaining=data.get("budget_remaining"),
130
+ )
131
+
132
+
133
+ def report_usage(
134
+ api_key: str,
135
+ *,
136
+ session_id: str,
137
+ saved_input_tokens: int,
138
+ saved_chars: int,
139
+ n_turns: int,
140
+ n_compressed_entries: int,
141
+ provider_hint: Optional[str] = None,
142
+ model_hint: Optional[str] = None,
143
+ api_base: str = DEFAULT_API_BASE,
144
+ timeout: float = DEFAULT_TIMEOUT,
145
+ ) -> bool:
146
+ """Send a per-session usage report to Compresh for billing/telemetry.
147
+
148
+ Returns True on 2xx, False otherwise. Failures are non-fatal — local
149
+ compression continues, telemetry catches up at the next successful
150
+ report.
151
+ """
152
+ url = f"{api_base.rstrip('/')}/v1/usage/report"
153
+ headers = {
154
+ "Authorization": f"Bearer {api_key}",
155
+ "Content-Type": "application/json",
156
+ }
157
+ payload = {
158
+ "session_id": session_id,
159
+ "saved_input_tokens": saved_input_tokens,
160
+ "saved_chars": saved_chars,
161
+ "n_turns": n_turns,
162
+ "n_compressed_entries": n_compressed_entries,
163
+ "provider_hint": provider_hint,
164
+ "model_hint": model_hint,
165
+ "source": "compresh-mcp",
166
+ }
167
+
168
+ try:
169
+ with httpx.Client(timeout=timeout) as client:
170
+ resp = client.post(url, headers=headers, json=payload)
171
+ if 200 <= resp.status_code < 300:
172
+ return True
173
+ logger.warning("usage report rejected: %d", resp.status_code)
174
+ except httpx.HTTPError as e:
175
+ logger.warning("usage report network error: %s", e)
176
+ return False
@@ -0,0 +1,95 @@
1
+ """Onboarding flow for new compresh-mcp users.
2
+
3
+ When the user starts the server without a valid COMPRESH_API_KEY, this
4
+ module:
5
+
6
+ 1. Prints a friendly help message to stderr (so MCP stdio is not
7
+ polluted).
8
+ 2. Opens the signup page in the default browser.
9
+ 3. Exits with a non-zero status so the MCP client surfaces the
10
+ failure to the user.
11
+
12
+ Existing users (anyone with an API key already in hand) skip this flow
13
+ by setting COMPRESH_API_KEY in their MCP client's environment config.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import sys
20
+ import textwrap
21
+ import webbrowser
22
+
23
+ DEFAULT_SIGNUP_URL = os.environ.get(
24
+ "COMPRESH_SIGNUP_URL", "https://api.compre.sh/signup?source=compresh-mcp"
25
+ )
26
+ DEFAULT_DOCS_URL = os.environ.get(
27
+ "COMPRESH_DOCS_URL", "https://compre.sh/docs/mcp"
28
+ )
29
+
30
+
31
+ def _eprint(msg: str) -> None:
32
+ """Print to stderr (stdin/stdout reserved for MCP stdio transport)."""
33
+ print(msg, file=sys.stderr, flush=True)
34
+
35
+
36
+ def show_welcome_and_open_signup(
37
+ *,
38
+ signup_url: str = DEFAULT_SIGNUP_URL,
39
+ docs_url: str = DEFAULT_DOCS_URL,
40
+ open_browser: bool = True,
41
+ ) -> None:
42
+ """Display onboarding help and optionally launch the browser.
43
+
44
+ Always prints help text; ``open_browser`` controls whether to invoke
45
+ ``webbrowser.open``. Set ``COMPRESH_NO_BROWSER=true`` to disable
46
+ auto-launch (useful in headless CI or remote terminals).
47
+ """
48
+ no_browser = os.environ.get("COMPRESH_NO_BROWSER", "").lower() in (
49
+ "true",
50
+ "1",
51
+ "yes",
52
+ )
53
+
54
+ msg = textwrap.dedent(
55
+ f"""
56
+ ╭─────────────────────────────────────────────────────────────╮
57
+ │ │
58
+ │ compresh-mcp needs a Compresh API key to run. │
59
+ │ │
60
+ │ If you have an account: │
61
+ │ Set COMPRESH_API_KEY in your MCP client config: │
62
+ │ │
63
+ │ {{ │
64
+ │ "mcpServers": {{ │
65
+ │ "compresh": {{ │
66
+ │ "command": "compresh-mcp", │
67
+ │ "env": {{ "COMPRESH_API_KEY": "sk-comp_..." }} │
68
+ │ }} │
69
+ │ }} │
70
+ │ }} │
71
+ │ │
72
+ │ New users — sign up + add $10 budget: │
73
+ │ {signup_url:<53s} │
74
+ │ │
75
+ │ Every new signup gets $30 free credit (90-day expiry). │
76
+ │ Welcome email + API key arrive automatically. │
77
+ │ │
78
+ │ Docs: │
79
+ │ {docs_url:<53s} │
80
+ │ │
81
+ ╰─────────────────────────────────────────────────────────────╯
82
+ """
83
+ ).strip()
84
+
85
+ _eprint(msg)
86
+
87
+ if open_browser and not no_browser:
88
+ try:
89
+ opened = webbrowser.open(signup_url, new=2)
90
+ if opened:
91
+ _eprint(f"\n→ Opened {signup_url} in your default browser.")
92
+ else:
93
+ _eprint(f"\n→ Could not auto-open browser. Visit {signup_url} manually.")
94
+ except Exception as e:
95
+ _eprint(f"\n→ Browser launch failed ({e!r}). Visit {signup_url} manually.")