token-cut-cli 0.1.0__tar.gz → 0.1.2__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.
@@ -15,3 +15,4 @@ graphify-out/
15
15
  .cache/
16
16
  data/
17
17
  *.db
18
+ .cursor/mcp.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: token-cut-cli
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Verify Token-Cut API integration from the terminal (no repo clone required)
5
5
  Project-URL: Homepage, https://www.token-cut.com
6
6
  Project-URL: Documentation, https://www.token-cut.com
@@ -1,58 +1,58 @@
1
- # token-cut-cli
2
-
3
- Public **client** for [Token-Cut](https://www.token-cut.com). Your proprietary API stays private; customers only get this thin CLI + MCP shim.
4
-
5
- ## Business model
6
-
7
- | What | Public? | Paid? |
8
- |------|---------|-------|
9
- | This PyPI package (`token-cut-cli`) | Yes | Free to install |
10
- | `cc_live_…` API key | — | Yes (your Stripe plans) |
11
- | Optimization + Graphify on **your** API | Private server | Metered per key |
12
-
13
- The CLI cannot optimize without a valid key against **your** deployment.
14
-
15
- ## Install
16
-
17
- ```bash
18
- pip install token-cut-cli
19
- ```
20
-
21
- Not on PyPI yet? Maintainer runs `./scripts/publish-cli.sh` from the repo root.
22
- Until then:
23
-
24
- ```bash
25
- cd cli && pip install .
26
- ```
27
-
28
- ## Verify integration (~1 min)
29
-
30
- ```bash
31
- export TOKEN_CUT_API_KEY=cc_live_your_key
32
- token-cut verify
33
- ```
34
-
35
- Calls `https://www.token-cut.com` (override with `TOKEN_CUT_API_URL`).
36
-
37
- ## IDE setup (Cursor · Claude · Copilot)
38
-
39
- ```bash
40
- export TOKEN_CUT_API_KEY=cc_live_your_key
41
- token-cut mcp-config > ~/.cursor/mcp.json
42
- # merge if file already exists — restart IDE
43
- ```
44
-
45
- Uses `python -m token_cut_cli.mcp_server` from the installed package (no git clone).
46
-
47
- ## Commands
48
-
49
- | Command | Purpose |
50
- |---------|---------|
51
- | `token-cut verify` | Test key + optimize + graph on a public sample repo |
52
- | `token-cut optimize "…"` | One-off optimize JSON |
53
- | `token-cut mcp-config` | Print MCP config for IDEs |
54
- | `token-cut-mcp` | MCP stdio server (called by IDE) |
55
-
56
- ## Publish (maintainers)
57
-
58
- See [../docs/PYPI_PUBLISH.md](../docs/PYPI_PUBLISH.md).
1
+ # token-cut-cli
2
+
3
+ Public **client** for [Token-Cut](https://www.token-cut.com). Your proprietary API stays private; customers only get this thin CLI + MCP shim.
4
+
5
+ ## Business model
6
+
7
+ | What | Public? | Paid? |
8
+ |------|---------|-------|
9
+ | This PyPI package (`token-cut-cli`) | Yes | Free to install |
10
+ | `cc_live_…` API key | — | Yes (your Stripe plans) |
11
+ | Optimization + Graphify on **your** API | Private server | Metered per key |
12
+
13
+ The CLI cannot optimize without a valid key against **your** deployment.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install token-cut-cli
19
+ ```
20
+
21
+ Not on PyPI yet? Maintainer runs `./scripts/publish-cli.sh` from the repo root.
22
+ Until then:
23
+
24
+ ```bash
25
+ cd cli && pip install .
26
+ ```
27
+
28
+ ## Verify integration (~1 min)
29
+
30
+ ```bash
31
+ export TOKEN_CUT_API_KEY=cc_live_your_key
32
+ token-cut verify
33
+ ```
34
+
35
+ Calls `https://www.token-cut.com` (override with `TOKEN_CUT_API_URL`).
36
+
37
+ ## IDE setup (Cursor · Claude · Copilot)
38
+
39
+ ```bash
40
+ export TOKEN_CUT_API_KEY=cc_live_your_key
41
+ token-cut mcp-config > ~/.cursor/mcp.json
42
+ # merge if file already exists — restart IDE
43
+ ```
44
+
45
+ Uses `python -m token_cut_cli.mcp_server` from the installed package (no git clone).
46
+
47
+ ## Commands
48
+
49
+ | Command | Purpose |
50
+ |---------|---------|
51
+ | `token-cut verify` | Test key + optimize + graph on a public sample repo |
52
+ | `token-cut optimize "…"` | One-off optimize JSON |
53
+ | `token-cut mcp-config` | Print MCP config for IDEs |
54
+ | `token-cut-mcp` | MCP stdio server (called by IDE) |
55
+
56
+ ## Publish (maintainers)
57
+
58
+ See [../docs/PYPI_PUBLISH.md](../docs/PYPI_PUBLISH.md).
@@ -1,37 +1,37 @@
1
- [build-system]
2
- requires = ["hatchling>=1.25.0"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "token-cut-cli"
7
- version = "0.1.0"
8
- description = "Verify Token-Cut API integration from the terminal (no repo clone required)"
9
- readme = "README.md"
10
- requires-python = ">=3.10"
11
- license = { text = "MIT" }
12
- authors = [{ name = "Token-Cut" }]
13
- keywords = ["token-cut", "cursor", "copilot", "claude", "llm", "optimization"]
14
- classifiers = [
15
- "Development Status :: 4 - Beta",
16
- "Environment :: Console",
17
- "Intended Audience :: Developers",
18
- "Programming Language :: Python :: 3",
19
- "Programming Language :: Python :: 3.10",
20
- "Programming Language :: Python :: 3.11",
21
- "Programming Language :: Python :: 3.12",
22
- ]
23
- dependencies = ["httpx>=0.27.0"]
24
-
25
- [project.urls]
26
- Homepage = "https://www.token-cut.com"
27
- Documentation = "https://www.token-cut.com"
28
-
29
- [project.scripts]
30
- token-cut = "token_cut_cli.main:main"
31
- token-cut-mcp = "token_cut_cli.mcp_server:main"
32
-
33
- [tool.hatch.build.targets.wheel]
34
- packages = ["src/token_cut_cli"]
35
-
36
- [tool.hatch.build.targets.sdist]
37
- include = ["src/token_cut_cli", "README.md"]
1
+ [build-system]
2
+ requires = ["hatchling>=1.25.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "token-cut-cli"
7
+ version = "0.1.2"
8
+ description = "Verify Token-Cut API integration from the terminal (no repo clone required)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Token-Cut" }]
13
+ keywords = ["token-cut", "cursor", "copilot", "claude", "llm", "optimization"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ ]
23
+ dependencies = ["httpx>=0.27.0"]
24
+
25
+ [project.urls]
26
+ Homepage = "https://www.token-cut.com"
27
+ Documentation = "https://www.token-cut.com"
28
+
29
+ [project.scripts]
30
+ token-cut = "token_cut_cli.main:main"
31
+ token-cut-mcp = "token_cut_cli.mcp_server:main"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/token_cut_cli"]
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ include = ["src/token_cut_cli", "README.md"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -1,4 +1,4 @@
1
- from token_cut_cli.main import main
2
-
3
- if __name__ == "__main__":
4
- raise SystemExit(main())
1
+ from token_cut_cli.main import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -1,82 +1,132 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- from typing import Any
5
-
6
- import httpx
7
-
8
- DEFAULT_API_URL = "https://www.token-cut.com"
9
- # Public OSS repo used only for `token-cut verify` smoke test (no customer code).
10
- DEFAULT_REPO_URL = "https://github.com/fastapi/fastapi"
11
- DEFAULT_TIMEOUT = 120.0
12
-
13
-
14
- def api_base() -> str:
15
- raw = (
16
- os.getenv("TOKEN_CUT_API_URL")
17
- or os.getenv("CONTEXT_CUT_API_URL")
18
- or DEFAULT_API_URL
19
- ).strip().rstrip("/")
20
- return raw
21
-
22
-
23
- def api_key() -> str:
24
- key = (
25
- os.getenv("TOKEN_CUT_API_KEY")
26
- or os.getenv("CONTEXT_CUT_API_KEY")
27
- or ""
28
- ).strip()
29
- return key
30
-
31
-
32
- class TokenCutClient:
33
- def __init__(
34
- self,
35
- base_url: str | None = None,
36
- key: str | None = None,
37
- timeout: float = DEFAULT_TIMEOUT,
38
- ) -> None:
39
- self.base = (base_url or api_base()).rstrip("/")
40
- self.key = (key or api_key()).strip()
41
- self.timeout = timeout
42
-
43
- def _headers(self) -> dict[str, str]:
44
- if not self.key:
45
- raise ValueError("Missing API key")
46
- return {"x-api-key": self.key, "Content-Type": "application/json"}
47
-
48
- def health(self) -> dict[str, Any]:
49
- with httpx.Client(timeout=30.0) as client:
50
- r = client.get(f"{self.base}/health")
51
- r.raise_for_status()
52
- return r.json()
53
-
54
- def optimize(
55
- self,
56
- prompt: str,
57
- *,
58
- repo_url: str | None = None,
59
- repo_path: str | None = None,
60
- use_graph: bool = True,
61
- ) -> dict[str, Any]:
62
- body: dict[str, Any] = {
63
- "prompt": prompt,
64
- "use_graph": use_graph,
65
- "auto_build_graph": True,
66
- }
67
- if repo_url:
68
- body["repo_url"] = repo_url
69
- if repo_path:
70
- body["repo_path"] = repo_path
71
- with httpx.Client(timeout=self.timeout) as client:
72
- r = client.post(
73
- f"{self.base}/v1/optimize",
74
- headers=self._headers(),
75
- json=body,
76
- )
77
- if r.status_code == 401:
78
- raise PermissionError("Invalid or expired API key (401)")
79
- if r.status_code == 429:
80
- raise PermissionError("Plan limit reached (429)")
81
- r.raise_for_status()
82
- return r.json()
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import httpx
7
+
8
+ DEFAULT_API_URL = "https://www.token-cut.com"
9
+ # Public OSS repo used only for `token-cut verify` smoke test (no customer code).
10
+ # Small public repo — fastapi/fastapi OOMs 512MB cloud pods during Graphify.
11
+ DEFAULT_REPO_URL = "https://github.com/pallets/itsdangerous"
12
+ DEFAULT_TIMEOUT = 120.0
13
+
14
+
15
+ def api_base() -> str:
16
+ raw = (
17
+ os.getenv("TOKEN_CUT_API_URL")
18
+ or os.getenv("CONTEXT_CUT_API_URL")
19
+ or DEFAULT_API_URL
20
+ ).strip().rstrip("/")
21
+ return raw
22
+
23
+
24
+ def api_key() -> str:
25
+ key = (
26
+ os.getenv("TOKEN_CUT_API_KEY")
27
+ or os.getenv("CONTEXT_CUT_API_KEY")
28
+ or ""
29
+ ).strip()
30
+ return key
31
+
32
+
33
+ def is_valid_api_key_format(key: str) -> bool:
34
+ """Full cc_live_ key from dashboard — not placeholder or prefix-only."""
35
+ k = key.strip()
36
+ if not k.startswith("cc_live_"):
37
+ return False
38
+ if k in ("cc_live_YOUR_KEY", "cc_live_…", "cc_live_..."):
39
+ return False
40
+ if "…" in k or "..." in k:
41
+ return False
42
+ # generate_api_key uses secrets.token_urlsafe(32) after prefix
43
+ return len(k) >= 40
44
+
45
+
46
+ def key_hint(key: str) -> str:
47
+ k = key.strip()
48
+ if len(k) <= 20:
49
+ return k[:8] + "…"
50
+ return f"{k[:16]}… ({len(k)} chars)"
51
+
52
+
53
+ def parse_api_error(response: httpx.Response) -> str:
54
+ try:
55
+ body = response.json()
56
+ detail = body.get("detail")
57
+ if isinstance(detail, str):
58
+ return detail
59
+ if isinstance(detail, list) and detail:
60
+ first = detail[0]
61
+ if isinstance(first, dict) and "msg" in first:
62
+ return str(first["msg"])
63
+ except Exception:
64
+ pass
65
+ return response.text.strip() or f"HTTP {response.status_code}"
66
+
67
+
68
+ class TokenCutClient:
69
+ def __init__(
70
+ self,
71
+ base_url: str | None = None,
72
+ key: str | None = None,
73
+ timeout: float = DEFAULT_TIMEOUT,
74
+ ) -> None:
75
+ self.base = (base_url or api_base()).rstrip("/")
76
+ self.key = (key or api_key()).strip()
77
+ self.timeout = timeout
78
+
79
+ def _headers(self) -> dict[str, str]:
80
+ if not self.key:
81
+ raise ValueError("Missing API key")
82
+ if not is_valid_api_key_format(self.key):
83
+ raise ValueError(
84
+ "Invalid TOKEN_CUT_API_KEY — need the full cc_live_… key from "
85
+ "Connect & Security (Generate API key). Not the placeholder or prefix only."
86
+ )
87
+ return {"x-api-key": self.key, "Content-Type": "application/json"}
88
+
89
+ def health(self) -> dict[str, Any]:
90
+ with httpx.Client(timeout=30.0) as client:
91
+ r = client.get(f"{self.base}/health")
92
+ r.raise_for_status()
93
+ return r.json()
94
+
95
+ def optimize(
96
+ self,
97
+ prompt: str,
98
+ *,
99
+ repo_url: str | None = None,
100
+ repo_path: str | None = None,
101
+ use_graph: bool = True,
102
+ ) -> dict[str, Any]:
103
+ body: dict[str, Any] = {
104
+ "prompt": prompt,
105
+ "use_graph": use_graph,
106
+ "auto_build_graph": True,
107
+ }
108
+ if repo_url:
109
+ body["repo_url"] = repo_url
110
+ if repo_path:
111
+ body["repo_path"] = repo_path
112
+ with httpx.Client(timeout=self.timeout) as client:
113
+ r = client.post(
114
+ f"{self.base}/v1/optimize",
115
+ headers=self._headers(),
116
+ json=body,
117
+ )
118
+ if r.status_code == 401:
119
+ detail = parse_api_error(r)
120
+ raise PermissionError(f"API key rejected (401): {detail}")
121
+ if r.status_code == 429:
122
+ detail = parse_api_error(r)
123
+ raise PermissionError(f"Plan limit reached (429): {detail}")
124
+ if r.status_code in (502, 503, 504):
125
+ detail = parse_api_error(r)
126
+ raise RuntimeError(
127
+ f"Server overloaded or timed out ({r.status_code}): {detail}. "
128
+ "Cloud graph builds run in the background — retry in 1–2 min, "
129
+ "or use: token-cut verify --no-graph"
130
+ )
131
+ r.raise_for_status()
132
+ return r.json()
@@ -1,116 +1,123 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- import sys
6
-
7
- from token_cut_cli import __version__
8
- from token_cut_cli.client import DEFAULT_API_URL, DEFAULT_REPO_URL, TokenCutClient
9
- from token_cut_cli.verify import run_verify
10
-
11
-
12
- def main(argv: list[str] | None = None) -> int:
13
- parser = argparse.ArgumentParser(
14
- prog="token-cut",
15
- description="Token-Cut CLI — verify integration without cloning the repo",
16
- )
17
- parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
18
- sub = parser.add_subparsers(dest="command", required=True)
19
-
20
- v = sub.add_parser(
21
- "verify",
22
- help="Check API key, optimize, and Graphify on a public repo (~1 min)",
23
- )
24
- v.add_argument(
25
- "--api-url",
26
- default=None,
27
- help=f"API base URL (default: {DEFAULT_API_URL} or TOKEN_CUT_API_URL)",
28
- )
29
- v.add_argument(
30
- "--key",
31
- default=None,
32
- help="cc_live_… key (default: TOKEN_CUT_API_KEY env)",
33
- )
34
- v.add_argument(
35
- "--repo-url",
36
- default=DEFAULT_REPO_URL,
37
- help="Public GitHub/GitLab URL for graph test",
38
- )
39
- v.add_argument(
40
- "--repo-path",
41
- default=None,
42
- help="Local repo path instead of --repo-url (optional)",
43
- )
44
- v.add_argument("-q", "--quiet", action="store_true", help="Minimal output")
45
-
46
- o = sub.add_parser("optimize", help="Run one optimize call and print JSON")
47
- o.add_argument("prompt", help="Prompt text")
48
- o.add_argument("--api-url", default=None)
49
- o.add_argument("--key", default=None)
50
- o.add_argument("--repo-url", default=None)
51
- o.add_argument("--repo-path", default=None)
52
-
53
- m = sub.add_parser(
54
- "mcp-config",
55
- help="Print MCP JSON for Cursor / Claude (uses pip package, no git clone)",
56
- )
57
- m.add_argument("--api-url", default=None)
58
- m.add_argument("--key", default=None)
59
- m.add_argument("--repo-path", default=None, help="Default TOKEN_CUT_REPO_PATH in env")
60
-
61
- args = parser.parse_args(argv)
62
-
63
- try:
64
- client = TokenCutClient(base_url=args.api_url, key=args.key)
65
- except ValueError:
66
- print(
67
- "error: set TOKEN_CUT_API_KEY or pass --key cc_live_…",
68
- file=sys.stderr,
69
- )
70
- return 2
71
-
72
- if args.command == "verify":
73
- return run_verify(
74
- client,
75
- repo_url=args.repo_url,
76
- repo_path=args.repo_path,
77
- quiet=args.quiet,
78
- )
79
-
80
- if args.command == "optimize":
81
- data = client.optimize(
82
- args.prompt,
83
- repo_url=args.repo_url,
84
- repo_path=args.repo_path,
85
- )
86
- print(json.dumps(data, indent=2))
87
- return 0
88
-
89
- if args.command == "mcp-config":
90
- env: dict[str, str] = {
91
- "TOKEN_CUT_API_URL": client.base,
92
- "TOKEN_CUT_API_KEY": client.key,
93
- }
94
- if getattr(args, "repo_path", None):
95
- env["TOKEN_CUT_REPO_PATH"] = args.repo_path
96
- cfg = {
97
- "mcpServers": {
98
- "token-cut": {
99
- "command": sys.executable,
100
- "args": ["-m", "token_cut_cli.mcp_server"],
101
- "env": env,
102
- }
103
- }
104
- }
105
- print(json.dumps(cfg, indent=2))
106
- print(
107
- "\n# Paste into ~/.cursor/mcp.json or Claude Desktop config, then restart IDE.",
108
- file=sys.stderr,
109
- )
110
- return 0
111
-
112
- return 1
113
-
114
-
115
- if __name__ == "__main__":
116
- sys.exit(main())
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ from token_cut_cli import __version__
8
+ from token_cut_cli.client import DEFAULT_API_URL, DEFAULT_REPO_URL, TokenCutClient
9
+ from token_cut_cli.verify import run_verify
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(
14
+ prog="token-cut",
15
+ description="Token-Cut CLI — verify integration without cloning the repo",
16
+ )
17
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
18
+ sub = parser.add_subparsers(dest="command", required=True)
19
+
20
+ v = sub.add_parser(
21
+ "verify",
22
+ help="Check API key, optimize, and Graphify on a public repo (~1 min)",
23
+ )
24
+ v.add_argument(
25
+ "--api-url",
26
+ default=None,
27
+ help=f"API base URL (default: {DEFAULT_API_URL} or TOKEN_CUT_API_URL)",
28
+ )
29
+ v.add_argument(
30
+ "--key",
31
+ default=None,
32
+ help="cc_live_… key (default: TOKEN_CUT_API_KEY env)",
33
+ )
34
+ v.add_argument(
35
+ "--repo-url",
36
+ default=DEFAULT_REPO_URL,
37
+ help="Public GitHub/GitLab URL for graph test",
38
+ )
39
+ v.add_argument(
40
+ "--repo-path",
41
+ default=None,
42
+ help="Local repo path instead of --repo-url (optional)",
43
+ )
44
+ v.add_argument("-q", "--quiet", action="store_true", help="Minimal output")
45
+ v.add_argument(
46
+ "--no-graph",
47
+ action="store_true",
48
+ help="Only check API key + optimize (skip Graphify; safe on small Railway pods)",
49
+ )
50
+
51
+ o = sub.add_parser("optimize", help="Run one optimize call and print JSON")
52
+ o.add_argument("prompt", help="Prompt text")
53
+ o.add_argument("--api-url", default=None)
54
+ o.add_argument("--key", default=None)
55
+ o.add_argument("--repo-url", default=None)
56
+ o.add_argument("--repo-path", default=None)
57
+
58
+ m = sub.add_parser(
59
+ "mcp-config",
60
+ help="Print MCP JSON for Cursor / Claude (uses pip package, no git clone)",
61
+ )
62
+ m.add_argument("--api-url", default=None)
63
+ m.add_argument("--key", default=None)
64
+ m.add_argument("--repo-path", default=None, help="Default TOKEN_CUT_REPO_PATH in env")
65
+
66
+ args = parser.parse_args(argv)
67
+
68
+ try:
69
+ client = TokenCutClient(base_url=args.api_url, key=args.key)
70
+ except ValueError as e:
71
+ print(f"error: {e}", file=sys.stderr)
72
+ print(
73
+ " Set TOKEN_CUT_API_KEY or pass --key with the full key from the dashboard.",
74
+ file=sys.stderr,
75
+ )
76
+ return 2
77
+
78
+ if args.command == "verify":
79
+ return run_verify(
80
+ client,
81
+ repo_url=args.repo_url,
82
+ repo_path=args.repo_path,
83
+ quiet=args.quiet,
84
+ with_graph=not args.no_graph,
85
+ )
86
+
87
+ if args.command == "optimize":
88
+ data = client.optimize(
89
+ args.prompt,
90
+ repo_url=args.repo_url,
91
+ repo_path=args.repo_path,
92
+ )
93
+ print(json.dumps(data, indent=2))
94
+ return 0
95
+
96
+ if args.command == "mcp-config":
97
+ env: dict[str, str] = {
98
+ "TOKEN_CUT_API_URL": client.base,
99
+ "TOKEN_CUT_API_KEY": client.key,
100
+ }
101
+ if getattr(args, "repo_path", None):
102
+ env["TOKEN_CUT_REPO_PATH"] = args.repo_path
103
+ cfg = {
104
+ "mcpServers": {
105
+ "token-cut": {
106
+ "command": sys.executable,
107
+ "args": ["-m", "token_cut_cli.mcp_server"],
108
+ "env": env,
109
+ }
110
+ }
111
+ }
112
+ print(json.dumps(cfg, indent=2))
113
+ print(
114
+ "\n# Paste into ~/.cursor/mcp.json or Claude Desktop config, then restart IDE.",
115
+ file=sys.stderr,
116
+ )
117
+ return 0
118
+
119
+ return 1
120
+
121
+
122
+ if __name__ == "__main__":
123
+ sys.exit(main())
@@ -1,170 +1,170 @@
1
- """Token-Cut MCP server — shipped in token-cut-cli (calls your cloud API with cc_live_ key)."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- import sys
8
-
9
- from token_cut_cli import __version__
10
- from token_cut_cli.client import DEFAULT_API_URL, api_base, api_key
11
-
12
-
13
- def _post(path: str, body: dict) -> dict:
14
- import urllib.request
15
-
16
- base = api_base().rstrip("/")
17
- key = api_key()
18
- if not key:
19
- raise RuntimeError("Set TOKEN_CUT_API_KEY=cc_live_…")
20
-
21
- req = urllib.request.Request(
22
- f"{base}{path}",
23
- data=json.dumps(body).encode(),
24
- headers={"Content-Type": "application/json", "x-api-key": key},
25
- method="POST",
26
- )
27
- timeout = int(os.getenv("TOKEN_CUT_HTTP_TIMEOUT", "360"))
28
- with urllib.request.urlopen(req, timeout=timeout) as resp:
29
- return json.loads(resp.read().decode())
30
-
31
-
32
- def _respond(obj: dict) -> None:
33
- print(json.dumps(obj), flush=True)
34
-
35
-
36
- def _tool_list() -> dict:
37
- return {
38
- "tools": [
39
- {
40
- "name": "optimize_prompt",
41
- "description": (
42
- "Optimize a prompt (Token-Cut cloud). Auto-builds graph.json when missing. "
43
- "Requires cc_live_ API key. IDE still runs the LLM on your plan."
44
- ),
45
- "inputSchema": {
46
- "type": "object",
47
- "properties": {
48
- "prompt": {"type": "string"},
49
- "repo_path": {"type": "string"},
50
- "repo_url": {"type": "string"},
51
- "auto_build_graph": {"type": "boolean"},
52
- },
53
- "required": ["prompt"],
54
- },
55
- },
56
- {
57
- "name": "build_repo_graph",
58
- "description": "Build Graphify graph for a repo (Token-Cut cloud).",
59
- "inputSchema": {
60
- "type": "object",
61
- "properties": {
62
- "repo_path": {"type": "string"},
63
- "repo_url": {"type": "string"},
64
- },
65
- },
66
- },
67
- ]
68
- }
69
-
70
-
71
- def _env_repo_path() -> str | None:
72
- return os.getenv("TOKEN_CUT_REPO_PATH") or os.getenv("CONTEXT_CUT_REPO_PATH")
73
-
74
-
75
- def _env_repo_url() -> str | None:
76
- return os.getenv("TOKEN_CUT_REPO_URL") or os.getenv("CONTEXT_CUT_REPO_URL")
77
-
78
-
79
- def _handle_tools_call(msg: dict) -> None:
80
- params = msg.get("params", {})
81
- name = params.get("name")
82
- args = params.get("arguments", {})
83
- try:
84
- if name == "optimize_prompt":
85
- data = _post(
86
- "/v1/optimize",
87
- {
88
- "prompt": args["prompt"],
89
- "repo_path": args.get("repo_path") or _env_repo_path(),
90
- "repo_url": args.get("repo_url") or _env_repo_url(),
91
- "model": "gpt-4o",
92
- "use_graph": True,
93
- "auto_build_graph": args.get("auto_build_graph", True),
94
- },
95
- )
96
- graph_line = ""
97
- if data.get("graph_ready"):
98
- built = " (built now)" if data.get("graph_built") else " (cached)"
99
- graph_line = f"Graph: {data.get('graph_nodes', 0)} nodes{built}\n"
100
- text = (
101
- f"Reduction: {data['reduction_factor']}×\n"
102
- f"Tokens: {data['original_tokens']} → {data['optimized_tokens']}\n"
103
- f"Saved: ${data['cost_saved_usd']}\n"
104
- f"{graph_line}\n"
105
- f"{data['optimized_prompt'][:8000]}"
106
- )
107
- elif name == "build_repo_graph":
108
- data = _post(
109
- "/v1/graph/build",
110
- {
111
- "repo_path": args.get("repo_path"),
112
- "repo_url": args.get("repo_url"),
113
- },
114
- )
115
- text = json.dumps(data, indent=2)
116
- else:
117
- raise ValueError(f"Unknown tool: {name}")
118
- _respond(
119
- {
120
- "jsonrpc": "2.0",
121
- "id": msg.get("id"),
122
- "result": {"content": [{"type": "text", "text": text}]},
123
- }
124
- )
125
- except Exception as e:
126
- _respond(
127
- {
128
- "jsonrpc": "2.0",
129
- "id": msg.get("id"),
130
- "error": {"code": -32000, "message": str(e)},
131
- },
132
- )
133
-
134
-
135
- def main() -> None:
136
- for line in sys.stdin:
137
- line = line.strip()
138
- if not line:
139
- continue
140
- msg = json.loads(line)
141
- method = msg.get("method")
142
- if method == "initialize":
143
- _respond(
144
- {
145
- "jsonrpc": "2.0",
146
- "id": msg.get("id"),
147
- "result": {
148
- "protocolVersion": "2024-11-05",
149
- "capabilities": {"tools": {}},
150
- "serverInfo": {
151
- "name": "token-cut",
152
- "version": __version__,
153
- },
154
- },
155
- }
156
- )
157
- elif method == "notifications/initialized":
158
- continue
159
- elif method == "tools/list":
160
- _respond(
161
- {"jsonrpc": "2.0", "id": msg.get("id"), "result": _tool_list()}
162
- )
163
- elif method == "tools/call":
164
- _handle_tools_call(msg)
165
- elif "id" in msg:
166
- _respond({"jsonrpc": "2.0", "id": msg["id"], "result": {}})
167
-
168
-
169
- if __name__ == "__main__":
170
- main()
1
+ """Token-Cut MCP server — shipped in token-cut-cli (calls your cloud API with cc_live_ key)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+
9
+ from token_cut_cli import __version__
10
+ from token_cut_cli.client import DEFAULT_API_URL, api_base, api_key
11
+
12
+
13
+ def _post(path: str, body: dict) -> dict:
14
+ import urllib.request
15
+
16
+ base = api_base().rstrip("/")
17
+ key = api_key()
18
+ if not key:
19
+ raise RuntimeError("Set TOKEN_CUT_API_KEY=cc_live_…")
20
+
21
+ req = urllib.request.Request(
22
+ f"{base}{path}",
23
+ data=json.dumps(body).encode(),
24
+ headers={"Content-Type": "application/json", "x-api-key": key},
25
+ method="POST",
26
+ )
27
+ timeout = int(os.getenv("TOKEN_CUT_HTTP_TIMEOUT", "360"))
28
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
29
+ return json.loads(resp.read().decode())
30
+
31
+
32
+ def _respond(obj: dict) -> None:
33
+ print(json.dumps(obj), flush=True)
34
+
35
+
36
+ def _tool_list() -> dict:
37
+ return {
38
+ "tools": [
39
+ {
40
+ "name": "optimize_prompt",
41
+ "description": (
42
+ "Optimize a prompt (Token-Cut cloud). Auto-builds graph.json when missing. "
43
+ "Requires cc_live_ API key. IDE still runs the LLM on your plan."
44
+ ),
45
+ "inputSchema": {
46
+ "type": "object",
47
+ "properties": {
48
+ "prompt": {"type": "string"},
49
+ "repo_path": {"type": "string"},
50
+ "repo_url": {"type": "string"},
51
+ "auto_build_graph": {"type": "boolean"},
52
+ },
53
+ "required": ["prompt"],
54
+ },
55
+ },
56
+ {
57
+ "name": "build_repo_graph",
58
+ "description": "Build Graphify graph for a repo (Token-Cut cloud).",
59
+ "inputSchema": {
60
+ "type": "object",
61
+ "properties": {
62
+ "repo_path": {"type": "string"},
63
+ "repo_url": {"type": "string"},
64
+ },
65
+ },
66
+ },
67
+ ]
68
+ }
69
+
70
+
71
+ def _env_repo_path() -> str | None:
72
+ return os.getenv("TOKEN_CUT_REPO_PATH") or os.getenv("CONTEXT_CUT_REPO_PATH")
73
+
74
+
75
+ def _env_repo_url() -> str | None:
76
+ return os.getenv("TOKEN_CUT_REPO_URL") or os.getenv("CONTEXT_CUT_REPO_URL")
77
+
78
+
79
+ def _handle_tools_call(msg: dict) -> None:
80
+ params = msg.get("params", {})
81
+ name = params.get("name")
82
+ args = params.get("arguments", {})
83
+ try:
84
+ if name == "optimize_prompt":
85
+ data = _post(
86
+ "/v1/optimize",
87
+ {
88
+ "prompt": args["prompt"],
89
+ "repo_path": args.get("repo_path") or _env_repo_path(),
90
+ "repo_url": args.get("repo_url") or _env_repo_url(),
91
+ "model": "gpt-4o",
92
+ "use_graph": True,
93
+ "auto_build_graph": args.get("auto_build_graph", True),
94
+ },
95
+ )
96
+ graph_line = ""
97
+ if data.get("graph_ready"):
98
+ built = " (built now)" if data.get("graph_built") else " (cached)"
99
+ graph_line = f"Graph: {data.get('graph_nodes', 0)} nodes{built}\n"
100
+ text = (
101
+ f"Reduction: {data['reduction_factor']}×\n"
102
+ f"Tokens: {data['original_tokens']} → {data['optimized_tokens']}\n"
103
+ f"Saved: ${data['cost_saved_usd']}\n"
104
+ f"{graph_line}\n"
105
+ f"{data['optimized_prompt'][:8000]}"
106
+ )
107
+ elif name == "build_repo_graph":
108
+ data = _post(
109
+ "/v1/graph/build",
110
+ {
111
+ "repo_path": args.get("repo_path"),
112
+ "repo_url": args.get("repo_url"),
113
+ },
114
+ )
115
+ text = json.dumps(data, indent=2)
116
+ else:
117
+ raise ValueError(f"Unknown tool: {name}")
118
+ _respond(
119
+ {
120
+ "jsonrpc": "2.0",
121
+ "id": msg.get("id"),
122
+ "result": {"content": [{"type": "text", "text": text}]},
123
+ }
124
+ )
125
+ except Exception as e:
126
+ _respond(
127
+ {
128
+ "jsonrpc": "2.0",
129
+ "id": msg.get("id"),
130
+ "error": {"code": -32000, "message": str(e)},
131
+ },
132
+ )
133
+
134
+
135
+ def main() -> None:
136
+ for line in sys.stdin:
137
+ line = line.strip()
138
+ if not line:
139
+ continue
140
+ msg = json.loads(line)
141
+ method = msg.get("method")
142
+ if method == "initialize":
143
+ _respond(
144
+ {
145
+ "jsonrpc": "2.0",
146
+ "id": msg.get("id"),
147
+ "result": {
148
+ "protocolVersion": "2024-11-05",
149
+ "capabilities": {"tools": {}},
150
+ "serverInfo": {
151
+ "name": "token-cut",
152
+ "version": __version__,
153
+ },
154
+ },
155
+ }
156
+ )
157
+ elif method == "notifications/initialized":
158
+ continue
159
+ elif method == "tools/list":
160
+ _respond(
161
+ {"jsonrpc": "2.0", "id": msg.get("id"), "result": _tool_list()}
162
+ )
163
+ elif method == "tools/call":
164
+ _handle_tools_call(msg)
165
+ elif "id" in msg:
166
+ _respond({"jsonrpc": "2.0", "id": msg["id"], "result": {}})
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import time
5
+ from typing import Any
6
+
7
+ from token_cut_cli.client import (
8
+ DEFAULT_REPO_URL,
9
+ TokenCutClient,
10
+ is_valid_api_key_format,
11
+ key_hint,
12
+ )
13
+
14
+ AUTH_PROMPT = "Token-Cut verify: confirm API key and run prune/compress on this line."
15
+
16
+ GRAPH_PROMPT = (
17
+ "List the main modules in this repository and how they connect. "
18
+ "Focus on entrypoints only."
19
+ )
20
+
21
+
22
+ def _ok(msg: str) -> None:
23
+ print(f" ✓ {msg}")
24
+
25
+
26
+ def _fail(msg: str) -> None:
27
+ print(f" ✗ {msg}", file=sys.stderr)
28
+
29
+
30
+ def run_verify(
31
+ client: TokenCutClient,
32
+ *,
33
+ repo_url: str = DEFAULT_REPO_URL,
34
+ repo_path: str | None = None,
35
+ quiet: bool = False,
36
+ with_graph: bool = True,
37
+ ) -> int:
38
+ if not quiet:
39
+ print(f"Token-Cut verify → {client.base}")
40
+ print(f" repo: {repo_path or repo_url}")
41
+ print(f" key: {key_hint(client.key)}")
42
+ print()
43
+
44
+ if not is_valid_api_key_format(client.key):
45
+ _fail(
46
+ "TOKEN_CUT_API_KEY is missing or not a full cc_live_… key "
47
+ "(dashboard shows only a prefix after signup — generate a new key)"
48
+ )
49
+ print(
50
+ "\n1. https://www.token-cut.com → Connect & Security → Log in\n"
51
+ "2. Click “Generate API key” and copy the full key (shown once)\n"
52
+ "3. export TOKEN_CUT_API_KEY='cc_live_…'\n"
53
+ "4. token-cut verify",
54
+ file=sys.stderr,
55
+ )
56
+ return 2
57
+
58
+ try:
59
+ health = client.health()
60
+ version = health.get("version", "?")
61
+ _ok(f"API reachable (version {version})")
62
+ except Exception as e:
63
+ if not quiet:
64
+ print(f" … health check skipped ({e})")
65
+
66
+ # Phase 1: auth + optimize without graph (fast, no clone on cloud)
67
+ try:
68
+ auth_data = client.optimize(
69
+ AUTH_PROMPT,
70
+ repo_url=None,
71
+ repo_path=None,
72
+ use_graph=False,
73
+ )
74
+ except PermissionError as e:
75
+ _fail(str(e))
76
+ print(
77
+ "\nFix: https://www.token-cut.com → Connect & Security → Generate API key\n"
78
+ " export TOKEN_CUT_API_KEY='cc_live_…' (full key, not prefix or JWT)",
79
+ file=sys.stderr,
80
+ )
81
+ return 2
82
+ except Exception as e:
83
+ _fail(f"Auth/optimize check failed: {e}")
84
+ return 3
85
+
86
+ _ok("API key accepted")
87
+ o1 = int(auth_data.get("original_tokens") or 0)
88
+ o2 = int(auth_data.get("optimized_tokens") or 0)
89
+ _ok(f"Optimize (no graph): {o1} → {o2} tokens")
90
+
91
+ if not with_graph:
92
+ if not quiet:
93
+ print("\n Skipped graph test (--no-graph). For full Graphify: token-cut verify")
94
+ return 0
95
+
96
+ # Phase 2: graph path (cloud may build in background)
97
+ try:
98
+ data = client.optimize(
99
+ GRAPH_PROMPT,
100
+ repo_url=repo_url if not repo_path else None,
101
+ repo_path=repo_path,
102
+ use_graph=True,
103
+ )
104
+ except Exception as e:
105
+ _fail(f"Graph optimize failed: {e}")
106
+ print(
107
+ "\n Production builds graphs in the background to avoid OOM.\n"
108
+ " Wait 1–2 minutes and run token-cut verify again,\n"
109
+ " or: token-cut verify --no-graph",
110
+ file=sys.stderr,
111
+ )
112
+ return 4
113
+
114
+ graph_ready = bool(data.get("graph_ready"))
115
+ graph_built = bool(data.get("graph_built"))
116
+ nodes = int(data.get("graph_nodes") or 0)
117
+ msg = str(data.get("graph_message") or data.get("message") or "")
118
+
119
+ if graph_ready:
120
+ cache = "built now" if graph_built else "cached"
121
+ _ok(f"Graphify: {nodes:,} nodes ({cache})")
122
+ elif "background" in msg.lower():
123
+ _ok("Graphify queued on server (background — avoids OOM)")
124
+ if not quiet:
125
+ print(" … waiting 90s then retrying once …")
126
+ time.sleep(90)
127
+ try:
128
+ retry = client.optimize(
129
+ GRAPH_PROMPT,
130
+ repo_url=repo_url if not repo_path else None,
131
+ repo_path=repo_path,
132
+ use_graph=True,
133
+ )
134
+ if retry.get("graph_ready"):
135
+ nodes = int(retry.get("graph_nodes") or 0)
136
+ _ok(f"Graphify ready after background build: {nodes:,} nodes")
137
+ graph_ready = True
138
+ except Exception:
139
+ pass
140
+ if not graph_ready:
141
+ print(
142
+ "\n Graph still building. Re-run: token-cut verify (in a few minutes)",
143
+ file=sys.stderr,
144
+ )
145
+ return 0
146
+ else:
147
+ _fail(f"Graph not ready — {msg or 'check repo URL'}")
148
+ return 4
149
+
150
+ if not quiet:
151
+ print()
152
+ print(" Next: token-cut mcp-config → paste into Cursor / Claude MCP settings")
153
+ print(" Your IDE subscription still runs the LLM.")
154
+
155
+ return 0
156
+
157
+
158
+ def format_result_summary(data: dict[str, Any]) -> str:
159
+ return (
160
+ f"tokens={data.get('original_tokens')}→{data.get('optimized_tokens')} "
161
+ f"graph_nodes={data.get('graph_nodes')}"
162
+ )
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
@@ -1,93 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
- from typing import Any
5
-
6
- from token_cut_cli.client import DEFAULT_REPO_URL, TokenCutClient
7
-
8
- VERIFY_PROMPT = (
9
- "Explain how authentication and the optimize pipeline work in this repository. "
10
- "Focus on api/app/ and the main entrypoints."
11
- )
12
-
13
-
14
- def _ok(msg: str) -> None:
15
- print(f" ✓ {msg}")
16
-
17
-
18
- def _fail(msg: str) -> None:
19
- print(f" ✗ {msg}", file=sys.stderr)
20
-
21
-
22
- def run_verify(
23
- client: TokenCutClient,
24
- *,
25
- repo_url: str = DEFAULT_REPO_URL,
26
- repo_path: str | None = None,
27
- quiet: bool = False,
28
- ) -> int:
29
- if not quiet:
30
- print(f"Token-Cut verify → {client.base}")
31
- print(f" repo: {repo_path or repo_url}")
32
- print()
33
-
34
- # 1. Health (non-fatal — some deployments only expose /v1/*)
35
- try:
36
- health = client.health()
37
- version = health.get("version", "?")
38
- _ok(f"API reachable (version {version})")
39
- except Exception as e:
40
- if not quiet:
41
- print(f" … health check skipped ({e})")
42
-
43
- # 2. Auth + optimize + graph
44
- try:
45
- data = client.optimize(
46
- VERIFY_PROMPT,
47
- repo_url=repo_url if not repo_path else None,
48
- repo_path=repo_path,
49
- use_graph=True,
50
- )
51
- except PermissionError as e:
52
- _fail(str(e))
53
- print(
54
- "\nGet a key: https://www.token-cut.com → Connect & Security",
55
- file=sys.stderr,
56
- )
57
- return 2
58
- except Exception as e:
59
- _fail(f"Optimize failed: {e}")
60
- return 3
61
-
62
- original = int(data.get("original_tokens") or 0)
63
- optimized = int(data.get("optimized_tokens") or 0)
64
- graph_ready = bool(data.get("graph_ready"))
65
- graph_built = bool(data.get("graph_built"))
66
- nodes = int(data.get("graph_nodes") or 0)
67
-
68
- _ok(f"API key accepted")
69
- _ok(f"Optimize: {original} → {optimized} tokens")
70
-
71
- if graph_ready:
72
- cache = "built now" if graph_built else "cached"
73
- _ok(f"Graphify: {nodes:,} nodes ({cache})")
74
- else:
75
- _fail("Graph not ready — check repo URL or Graphify on server")
76
- return 4
77
-
78
- if not quiet:
79
- print()
80
- if optimized < original:
81
- pct = round(100 * (1 - optimized / max(original, 1)))
82
- print(f" Token reduction on prompt alone: ~{pct}%")
83
- print(" Next: token-cut mcp-config → paste into Cursor / Claude MCP settings")
84
- print(" Your IDE subscription still runs the LLM.")
85
-
86
- return 0
87
-
88
-
89
- def format_result_summary(data: dict[str, Any]) -> str:
90
- return (
91
- f"tokens={data.get('original_tokens')}→{data.get('optimized_tokens')} "
92
- f"graph_nodes={data.get('graph_nodes')}"
93
- )