token-runway 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.
- token_runway/__init__.py +6 -0
- token_runway/__main__.py +7 -0
- token_runway/cli.py +179 -0
- token_runway/injector/__init__.py +9 -0
- token_runway/injector/config_injector.py +80 -0
- token_runway/server/__init__.py +5 -0
- token_runway/server/advice.py +84 -0
- token_runway/server/protocol.py +38 -0
- token_runway/server/server.py +255 -0
- token_runway/sources/__init__.py +68 -0
- token_runway/sources/base.py +70 -0
- token_runway/sources/claude.py +184 -0
- token_runway/sources/colab.py +98 -0
- token_runway/sources/copilot.py +209 -0
- token_runway/sources/gemini.py +169 -0
- token_runway-0.1.0.dist-info/METADATA +266 -0
- token_runway-0.1.0.dist-info/RECORD +21 -0
- token_runway-0.1.0.dist-info/WHEEL +5 -0
- token_runway-0.1.0.dist-info/entry_points.txt +3 -0
- token_runway-0.1.0.dist-info/licenses/LICENSE +21 -0
- token_runway-0.1.0.dist-info/top_level.txt +1 -0
token_runway/__init__.py
ADDED
token_runway/__main__.py
ADDED
token_runway/cli.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Command-line interface for Token Runway."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from token_runway.injector import get_ide_config_targets, inject_into_file
|
|
11
|
+
from token_runway.server import TokenRunwayServer
|
|
12
|
+
from token_runway.sources import fetch_all_runways
|
|
13
|
+
from token_runway.sources.claude import find_claude_token
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_status_badge(status: str) -> str:
|
|
19
|
+
"""Simple ANSI terminal color badge."""
|
|
20
|
+
if status == "critical":
|
|
21
|
+
return "\033[91m● CRITICAL\033[0m"
|
|
22
|
+
if status == "warning":
|
|
23
|
+
return "\033[93m▲ WARNING\033[0m"
|
|
24
|
+
if status == "ok":
|
|
25
|
+
return "\033[92m✔ OK\033[0m"
|
|
26
|
+
if status == "unlimited":
|
|
27
|
+
return "\033[96m∞ UNLIMITED\033[0m"
|
|
28
|
+
if status == "not_on_plan":
|
|
29
|
+
return "\033[90m- NOT ON PLAN\033[0m"
|
|
30
|
+
return status.upper()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_check() -> int:
|
|
34
|
+
"""Print a terminal table of active token quotas across all providers."""
|
|
35
|
+
print("\n\033[1m=== Token Runway: Live Quota Telemetry ===\033[0m\n")
|
|
36
|
+
readings = fetch_all_runways()
|
|
37
|
+
|
|
38
|
+
for key, r in readings.items():
|
|
39
|
+
title = f"\033[1;34m{r.display_name}\033[0m"
|
|
40
|
+
if r.plan:
|
|
41
|
+
title += f" \033[90m(Plan: {r.plan})\033[0m"
|
|
42
|
+
print(title)
|
|
43
|
+
|
|
44
|
+
if not r.available:
|
|
45
|
+
print(f" \033[90mUnavailable: {r.error_message}\033[0m\n")
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if not r.windows:
|
|
49
|
+
print(" \033[90mNo active quota windows reported.\033[0m\n")
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
for w in r.windows:
|
|
53
|
+
badge = format_status_badge(w.status)
|
|
54
|
+
line = f" • {w.name:<26} {badge:<22}"
|
|
55
|
+
|
|
56
|
+
if w.used_pct is not None:
|
|
57
|
+
line += f" Used: {w.used_pct:>3}%"
|
|
58
|
+
if w.remaining_pct is not None:
|
|
59
|
+
line += f" (Remaining: {w.remaining_pct:>5}%)"
|
|
60
|
+
if w.remaining_count is not None:
|
|
61
|
+
if w.total_entitlement is not None:
|
|
62
|
+
line += f" [{w.remaining_count}/{w.total_entitlement}]"
|
|
63
|
+
else:
|
|
64
|
+
line += f" [{w.remaining_count}]"
|
|
65
|
+
if w.resets_in_minutes is not None:
|
|
66
|
+
if w.resets_in_minutes < 60:
|
|
67
|
+
line += f" (Resets in {w.resets_in_minutes}m)"
|
|
68
|
+
elif w.resets_in_minutes < 1440:
|
|
69
|
+
line += f" (Resets in {w.resets_in_minutes // 60}h {w.resets_in_minutes % 60}m)"
|
|
70
|
+
else:
|
|
71
|
+
line += f" (Resets in {w.resets_in_minutes // 1440}d {(w.resets_in_minutes % 1440) // 60}h)"
|
|
72
|
+
|
|
73
|
+
print(line)
|
|
74
|
+
print()
|
|
75
|
+
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def cmd_doctor() -> int:
|
|
80
|
+
"""Diagnose local CLI tools, credentials, and network connectivity."""
|
|
81
|
+
print("\n\033[1m=== Token Runway: Doctor ===\033[0m\n")
|
|
82
|
+
|
|
83
|
+
# 1. Claude
|
|
84
|
+
token = find_claude_token()
|
|
85
|
+
if token:
|
|
86
|
+
print(" \033[92m✔ Claude Code:\033[0m Credentials found")
|
|
87
|
+
else:
|
|
88
|
+
print(" \033[93m▲ Claude Code:\033[0m No credentials found in ~/.claude/.credentials.json")
|
|
89
|
+
|
|
90
|
+
# 2. Antigravity CLI (`agy`)
|
|
91
|
+
if shutil.which("agy"):
|
|
92
|
+
print(" \033[92m✔ Antigravity CLI (`agy`):\033[0m Found in PATH")
|
|
93
|
+
else:
|
|
94
|
+
print(" \033[90m- Antigravity CLI (`agy`):\033[0m Not installed or not in PATH")
|
|
95
|
+
|
|
96
|
+
# 3. GitHub CLI (`gh`)
|
|
97
|
+
if shutil.which("gh"):
|
|
98
|
+
print(" \033[92m✔ GitHub CLI (`gh`):\033[0m Found in PATH")
|
|
99
|
+
else:
|
|
100
|
+
print(" \033[90m- GitHub CLI (`gh`):\033[0m Not installed or not in PATH")
|
|
101
|
+
|
|
102
|
+
# 4. Google Colab CLI (`colab`)
|
|
103
|
+
if shutil.which("colab"):
|
|
104
|
+
print(" \033[92m✔ Google Colab CLI (`colab`):\033[0m Found in PATH")
|
|
105
|
+
else:
|
|
106
|
+
print(" \033[90m- Google Colab CLI (`colab`):\033[0m Not installed or not in PATH")
|
|
107
|
+
|
|
108
|
+
print("\nChecking live endpoints...")
|
|
109
|
+
readings = fetch_all_runways()
|
|
110
|
+
for name, r in readings.items():
|
|
111
|
+
if r.available:
|
|
112
|
+
print(f" \033[92m✔ {r.display_name}:\033[0m Connected ({len(r.windows)} quota windows active)")
|
|
113
|
+
else:
|
|
114
|
+
print(f" \033[93m▲ {r.display_name}:\033[0m {r.error_message}")
|
|
115
|
+
|
|
116
|
+
print("\nDoctor check completed.\n")
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cmd_inject(mode: str = "auto") -> int:
|
|
121
|
+
"""Inject Token Runway into detected AI IDE configuration files."""
|
|
122
|
+
print("\n\033[1m=== Token Runway: IDE Injector ===\033[0m\n")
|
|
123
|
+
targets = get_ide_config_targets()
|
|
124
|
+
injected_count = 0
|
|
125
|
+
|
|
126
|
+
for ide_name, path in targets:
|
|
127
|
+
try:
|
|
128
|
+
inject_into_file(path, mode=mode)
|
|
129
|
+
print(f" \033[92m✔ {ide_name}:\033[0m Injected configuration into {path}")
|
|
130
|
+
injected_count += 1
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f" \033[91m✖ {ide_name}:\033[0m Failed to update {path}: {e}")
|
|
133
|
+
|
|
134
|
+
print(f"\nInjected token-runway entry into {injected_count} IDE configuration(s).\n")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
139
|
+
parser = argparse.ArgumentParser(
|
|
140
|
+
prog="token-runway",
|
|
141
|
+
description="Model Context Protocol (MCP) server for real-time token runway & quota awareness.",
|
|
142
|
+
)
|
|
143
|
+
parser.add_argument("-V", "--version", action="version", version=f"token-runway {__version__}")
|
|
144
|
+
|
|
145
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
146
|
+
|
|
147
|
+
# Subcommands
|
|
148
|
+
subparsers.add_parser("run", help="Run stdio MCP server (default when no command given).")
|
|
149
|
+
subparsers.add_parser("check", help="Print terminal table of active quotas.")
|
|
150
|
+
subparsers.add_parser("doctor", help="Check local tools, credentials, and API connectivity.")
|
|
151
|
+
|
|
152
|
+
inject_parser = subparsers.add_parser("inject", help="Auto-inject MCP server into Claude Desktop, Cursor, etc.")
|
|
153
|
+
inject_parser.add_argument(
|
|
154
|
+
"--mode",
|
|
155
|
+
choices=["auto", "uvx", "pip"],
|
|
156
|
+
default="auto",
|
|
157
|
+
help="Command launch mode in IDE configs (default: auto).",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
args = parser.parse_args(argv)
|
|
161
|
+
|
|
162
|
+
if args.command == "check":
|
|
163
|
+
return cmd_check()
|
|
164
|
+
elif args.command == "doctor":
|
|
165
|
+
return cmd_doctor()
|
|
166
|
+
elif args.command == "inject":
|
|
167
|
+
return cmd_inject(mode=args.mode)
|
|
168
|
+
else:
|
|
169
|
+
# Default behavior: run MCP stdio server
|
|
170
|
+
server = TokenRunwayServer()
|
|
171
|
+
try:
|
|
172
|
+
server.run_stdio()
|
|
173
|
+
return 0
|
|
174
|
+
except (KeyboardInterrupt, BrokenPipeError):
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
sys.exit(main())
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Multi-IDE configuration detector and one-click injector for Token Runway."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_ide_config_targets() -> List[Tuple[str, Path]]:
|
|
14
|
+
"""Return a list of known (IDE_Name, config_path) tuples for the current OS."""
|
|
15
|
+
targets = []
|
|
16
|
+
home = Path.home()
|
|
17
|
+
|
|
18
|
+
# 1. Claude Desktop
|
|
19
|
+
if sys.platform == "darwin":
|
|
20
|
+
targets.append(("Claude Desktop", home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"))
|
|
21
|
+
elif sys.platform == "win32":
|
|
22
|
+
appdata = os.environ.get("APPDATA")
|
|
23
|
+
if appdata:
|
|
24
|
+
targets.append(("Claude Desktop", Path(appdata) / "Claude" / "claude_desktop_config.json"))
|
|
25
|
+
else:
|
|
26
|
+
targets.append(("Claude Desktop", home / ".config" / "Claude" / "claude_desktop_config.json"))
|
|
27
|
+
|
|
28
|
+
# 2. Cursor
|
|
29
|
+
targets.append(("Cursor", home / ".cursor" / "mcp.json"))
|
|
30
|
+
|
|
31
|
+
# 3. Claude Code
|
|
32
|
+
targets.append(("Claude Code", home / ".claude" / "mcp.json"))
|
|
33
|
+
|
|
34
|
+
# 4. Antigravity
|
|
35
|
+
targets.append(("Antigravity", home / ".gemini" / "antigravity-cli" / "mcp_config.json"))
|
|
36
|
+
|
|
37
|
+
# 5. Windsurf
|
|
38
|
+
targets.append(("Windsurf", home / ".codeium" / "windsurf" / "mcp_config.json"))
|
|
39
|
+
|
|
40
|
+
return targets
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_mcp_entry(mode: str = "auto") -> Dict[str, Any]:
|
|
44
|
+
"""Generate the MCP server entry dictionary."""
|
|
45
|
+
if mode == "uvx" or (mode == "auto" and shutil.which("uvx")):
|
|
46
|
+
return {
|
|
47
|
+
"command": "uvx",
|
|
48
|
+
"args": ["token-runway"],
|
|
49
|
+
"env": {"PYTHONUNBUFFERED": "1"},
|
|
50
|
+
}
|
|
51
|
+
else:
|
|
52
|
+
python_exe = sys.executable
|
|
53
|
+
return {
|
|
54
|
+
"command": python_exe,
|
|
55
|
+
"args": ["-m", "token_runway"],
|
|
56
|
+
"env": {"PYTHONUNBUFFERED": "1"},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def inject_into_file(path: Path, entry_name: str = "token-runway", mode: str = "auto") -> bool:
|
|
61
|
+
"""Inject or update the token-runway MCP server definition into target config."""
|
|
62
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
data: Dict[str, Any] = {}
|
|
65
|
+
if path.is_file():
|
|
66
|
+
try:
|
|
67
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
68
|
+
except Exception:
|
|
69
|
+
data = {}
|
|
70
|
+
|
|
71
|
+
# Root key is typically "mcpServers"
|
|
72
|
+
if "mcpServers" not in data or not isinstance(data["mcpServers"], dict):
|
|
73
|
+
data["mcpServers"] = {}
|
|
74
|
+
|
|
75
|
+
data["mcpServers"][entry_name] = build_mcp_entry(mode)
|
|
76
|
+
|
|
77
|
+
tmp_path = path.with_suffix(".tmp")
|
|
78
|
+
tmp_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
79
|
+
tmp_path.replace(path)
|
|
80
|
+
return True
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Meta-cognitive reasoning engine for assessing action feasibility against active quotas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from token_runway.sources.base import ProviderReading
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def evaluate_action_feasibility(
|
|
11
|
+
readings: Dict[str, ProviderReading],
|
|
12
|
+
action_type: str = "general",
|
|
13
|
+
intensity: str = "medium", # "low", "medium", "high"
|
|
14
|
+
target_provider: Optional[str] = None,
|
|
15
|
+
) -> Dict[str, Any]:
|
|
16
|
+
"""Evaluate whether an agent should proceed with a resource-intensive action."""
|
|
17
|
+
action_type = action_type.lower()
|
|
18
|
+
intensity = intensity.lower()
|
|
19
|
+
|
|
20
|
+
# Identify primary active provider constraints
|
|
21
|
+
providers_to_check = [target_provider] if target_provider and target_provider in readings else list(readings.keys())
|
|
22
|
+
|
|
23
|
+
highest_used_pct = 0
|
|
24
|
+
most_constrained_provider = None
|
|
25
|
+
most_constrained_window = None
|
|
26
|
+
mins_to_reset = None
|
|
27
|
+
|
|
28
|
+
for p_name in providers_to_check:
|
|
29
|
+
reading = readings.get(p_name)
|
|
30
|
+
if not reading or not reading.available:
|
|
31
|
+
continue
|
|
32
|
+
for w in reading.windows:
|
|
33
|
+
if w.used_pct is not None and w.used_pct > highest_used_pct:
|
|
34
|
+
highest_used_pct = w.used_pct
|
|
35
|
+
most_constrained_provider = reading.display_name
|
|
36
|
+
most_constrained_window = w.name
|
|
37
|
+
mins_to_reset = w.resets_in_minutes
|
|
38
|
+
|
|
39
|
+
recommendation = "proceed"
|
|
40
|
+
strategies: List[str] = []
|
|
41
|
+
reasoning: str = ""
|
|
42
|
+
|
|
43
|
+
if highest_used_pct >= 95:
|
|
44
|
+
recommendation = "pause_or_downgrade"
|
|
45
|
+
reasoning = (
|
|
46
|
+
f"Quota is critically constrained ({highest_used_pct}% used on {most_constrained_provider} {most_constrained_window})."
|
|
47
|
+
)
|
|
48
|
+
if mins_to_reset is not None and mins_to_reset <= 30:
|
|
49
|
+
strategies.append(
|
|
50
|
+
f"Window resets in ~{mins_to_reset} minutes. Defer heavy work and perform lightweight local tasks."
|
|
51
|
+
)
|
|
52
|
+
strategies.append("Prohibit parallel subagent fan-outs; execute one step at a time.")
|
|
53
|
+
strategies.append("Read narrow line ranges (under 100 lines) instead of loading full files into context.")
|
|
54
|
+
strategies.append("Route subagent prompts to lightweight models (Flash/Haiku) if model selection is supported.")
|
|
55
|
+
elif highest_used_pct >= 80:
|
|
56
|
+
if intensity == "high":
|
|
57
|
+
recommendation = "downgrade_strategy"
|
|
58
|
+
reasoning = (
|
|
59
|
+
f"Quota headroom is tight ({highest_used_pct}% used on {most_constrained_provider}). High-intensity operations risk lockouts."
|
|
60
|
+
)
|
|
61
|
+
strategies.append("Downgrade multi-agent broad searches to targeted ripgrep / grep commands.")
|
|
62
|
+
strategies.append("Summarize long context before launching further turns.")
|
|
63
|
+
else:
|
|
64
|
+
recommendation = "proceed_with_caution"
|
|
65
|
+
reasoning = f"Headroom is moderate ({highest_used_pct}% used). Standard actions permitted."
|
|
66
|
+
strategies.append("Monitor token usage after this turn.")
|
|
67
|
+
else:
|
|
68
|
+
recommendation = "proceed"
|
|
69
|
+
reasoning = f"Healthy headroom ({highest_used_pct}% maximum utilization across active providers)."
|
|
70
|
+
strategies.append("Full operational capacity available for multi-agent workflows and deep research.")
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"verdict": recommendation,
|
|
74
|
+
"max_utilization_pct": highest_used_pct,
|
|
75
|
+
"bottleneck": {
|
|
76
|
+
"provider": most_constrained_provider,
|
|
77
|
+
"window": most_constrained_window,
|
|
78
|
+
"resets_in_minutes": mins_to_reset,
|
|
79
|
+
}
|
|
80
|
+
if most_constrained_provider
|
|
81
|
+
else None,
|
|
82
|
+
"analysis": reasoning,
|
|
83
|
+
"preservation_strategies": strategies,
|
|
84
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""JSON-RPC 2.0 protocol definitions and message envelopes for MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, Dict, Optional, Union
|
|
8
|
+
|
|
9
|
+
PARSE_ERROR = -32700
|
|
10
|
+
INVALID_REQUEST = -32600
|
|
11
|
+
METHOD_NOT_FOUND = -32601
|
|
12
|
+
INVALID_PARAMS = -32602
|
|
13
|
+
INTERNAL_ERROR = -32603
|
|
14
|
+
|
|
15
|
+
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def make_response(req_id: Union[int, str, None], result: Any) -> Dict[str, Any]:
|
|
19
|
+
"""Construct a JSON-RPC 2.0 success response envelope."""
|
|
20
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": result}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_error(
|
|
24
|
+
req_id: Union[int, str, None], code: int, message: str, data: Optional[Any] = None
|
|
25
|
+
) -> Dict[str, Any]:
|
|
26
|
+
"""Construct a JSON-RPC 2.0 error response envelope."""
|
|
27
|
+
err: Dict[str, Any] = {"code": code, "message": message}
|
|
28
|
+
if data is not None:
|
|
29
|
+
err["data"] = data
|
|
30
|
+
return {"jsonrpc": "2.0", "id": req_id, "error": err}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def write_message(msg: Dict[str, Any], out_stream=None) -> None:
|
|
34
|
+
"""Serialize and write a JSON-RPC message line followed by a newline."""
|
|
35
|
+
stream = out_stream or sys.stdout
|
|
36
|
+
line = json.dumps(msg, ensure_ascii=False)
|
|
37
|
+
stream.write(line + "\n")
|
|
38
|
+
stream.flush()
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Stdio-based Model Context Protocol (MCP) server for Token Runway."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from token_runway.server.advice import evaluate_action_feasibility
|
|
11
|
+
from token_runway.server.protocol import (
|
|
12
|
+
INTERNAL_ERROR,
|
|
13
|
+
INVALID_PARAMS,
|
|
14
|
+
INVALID_REQUEST,
|
|
15
|
+
MCP_PROTOCOL_VERSION,
|
|
16
|
+
METHOD_NOT_FOUND,
|
|
17
|
+
make_error,
|
|
18
|
+
make_response,
|
|
19
|
+
write_message,
|
|
20
|
+
)
|
|
21
|
+
from token_runway.sources import fetch_all_runways
|
|
22
|
+
from token_runway.sources.base import ProviderReading
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TokenRunwayServer:
|
|
26
|
+
"""Zero-dependency stdio MCP server exposing token quotas and advice."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, cache_ttl: float = 30.0):
|
|
29
|
+
self.cache_ttl = cache_ttl
|
|
30
|
+
self._cached_readings: Optional[Dict[str, ProviderReading]] = None
|
|
31
|
+
self._cache_time: float = 0.0
|
|
32
|
+
|
|
33
|
+
def get_readings(self, force_refresh: bool = False) -> Dict[str, ProviderReading]:
|
|
34
|
+
now = time.monotonic()
|
|
35
|
+
if force_refresh or self._cached_readings is None or (now - self._cache_time) > self.cache_ttl:
|
|
36
|
+
self._cached_readings = fetch_all_runways()
|
|
37
|
+
self._cache_time = now
|
|
38
|
+
return self._cached_readings
|
|
39
|
+
|
|
40
|
+
def handle_request(self, req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
41
|
+
if not isinstance(req, dict) or req.get("jsonrpc") != "2.0":
|
|
42
|
+
return make_error(req.get("id") if isinstance(req, dict) else None, INVALID_REQUEST, "Invalid JSON-RPC request")
|
|
43
|
+
|
|
44
|
+
method = req.get("method")
|
|
45
|
+
req_id = req.get("id")
|
|
46
|
+
params = req.get("params") or {}
|
|
47
|
+
|
|
48
|
+
# Notifications (no id)
|
|
49
|
+
if req_id is None:
|
|
50
|
+
if method == "notifications/initialized":
|
|
51
|
+
return None
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# RPC Methods
|
|
55
|
+
if method == "initialize":
|
|
56
|
+
return make_response(
|
|
57
|
+
req_id,
|
|
58
|
+
{
|
|
59
|
+
"protocolVersion": MCP_PROTOCOL_VERSION,
|
|
60
|
+
"capabilities": {
|
|
61
|
+
"tools": {},
|
|
62
|
+
"resources": {},
|
|
63
|
+
"prompts": {},
|
|
64
|
+
},
|
|
65
|
+
"serverInfo": {
|
|
66
|
+
"name": "token-runway",
|
|
67
|
+
"version": "0.1.0",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if method == "ping":
|
|
73
|
+
return make_response(req_id, {})
|
|
74
|
+
|
|
75
|
+
if method == "tools/list":
|
|
76
|
+
return make_response(req_id, {"tools": self._list_tools()})
|
|
77
|
+
|
|
78
|
+
if method == "tools/call":
|
|
79
|
+
return self._call_tool(req_id, params)
|
|
80
|
+
|
|
81
|
+
if method == "resources/list":
|
|
82
|
+
return make_response(req_id, {"resources": self._list_resources()})
|
|
83
|
+
|
|
84
|
+
if method == "resources/read":
|
|
85
|
+
return self._read_resource(req_id, params)
|
|
86
|
+
|
|
87
|
+
if method == "prompts/list":
|
|
88
|
+
return make_response(req_id, {"prompts": self._list_prompts()})
|
|
89
|
+
|
|
90
|
+
if method == "prompts/get":
|
|
91
|
+
return self._get_prompt(req_id, params)
|
|
92
|
+
|
|
93
|
+
return make_error(req_id, METHOD_NOT_FOUND, f"Method not found: {method}")
|
|
94
|
+
|
|
95
|
+
def _list_tools(self) -> List[Dict[str, Any]]:
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
"name": "check_runway",
|
|
99
|
+
"description": (
|
|
100
|
+
"Inspect current token quotas, utilization percentages, remaining capacity, "
|
|
101
|
+
"and reset countdowns across Claude, Gemini, Antigravity, GitHub Copilot, and Colab."
|
|
102
|
+
),
|
|
103
|
+
"inputSchema": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"refresh": {
|
|
107
|
+
"type": "boolean",
|
|
108
|
+
"description": "Bypass cache and force an immediate re-fetch from all provider CLIs/APIs.",
|
|
109
|
+
"default": False,
|
|
110
|
+
},
|
|
111
|
+
"provider": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "Optional specific provider to filter ('claude', 'gemini', 'antigravity', 'copilot', 'colab').",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "assess_action",
|
|
120
|
+
"description": (
|
|
121
|
+
"Meta-cognitive evaluation of whether a proposed action (e.g. running multiple parallel subagents, "
|
|
122
|
+
"reading massive files, or continuous test loops) is justified given current remaining quotas and reset timers."
|
|
123
|
+
),
|
|
124
|
+
"inputSchema": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"action_type": {
|
|
128
|
+
"type": "string",
|
|
129
|
+
"description": "Type of action planned ('subagent_fanout', 'large_file_dump', 'deep_research', 'targeted_edit', 'continuous_loop').",
|
|
130
|
+
"default": "general",
|
|
131
|
+
},
|
|
132
|
+
"intensity": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"enum": ["low", "medium", "high"],
|
|
135
|
+
"description": "Estimated token burn intensity of the action.",
|
|
136
|
+
"default": "medium",
|
|
137
|
+
},
|
|
138
|
+
"provider": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"description": "Target provider whose limits you want to evaluate against (optional).",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
def _call_tool(self, req_id: Any, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
148
|
+
name = params.get("name")
|
|
149
|
+
args = params.get("arguments") or {}
|
|
150
|
+
|
|
151
|
+
if name == "check_runway":
|
|
152
|
+
force = bool(args.get("refresh", False))
|
|
153
|
+
provider_filter = args.get("provider")
|
|
154
|
+
readings = self.get_readings(force_refresh=force)
|
|
155
|
+
|
|
156
|
+
if provider_filter and provider_filter.lower() in readings:
|
|
157
|
+
data = {provider_filter.lower(): readings[provider_filter.lower()].to_dict()}
|
|
158
|
+
else:
|
|
159
|
+
data = {k: v.to_dict() for k, v in readings.items()}
|
|
160
|
+
|
|
161
|
+
text = json.dumps(data, indent=2)
|
|
162
|
+
return make_response(req_id, {"content": [{"type": "text", "text": text}]})
|
|
163
|
+
|
|
164
|
+
if name == "assess_action":
|
|
165
|
+
action_type = args.get("action_type", "general")
|
|
166
|
+
intensity = args.get("intensity", "medium")
|
|
167
|
+
provider = args.get("provider")
|
|
168
|
+
readings = self.get_readings()
|
|
169
|
+
assessment = evaluate_action_feasibility(readings, action_type, intensity, provider)
|
|
170
|
+
text = json.dumps(assessment, indent=2)
|
|
171
|
+
return make_response(req_id, {"content": [{"type": "text", "text": text}]})
|
|
172
|
+
|
|
173
|
+
return make_error(req_id, INVALID_PARAMS, f"Unknown tool name: {name}")
|
|
174
|
+
|
|
175
|
+
def _list_resources(self) -> List[Dict[str, Any]]:
|
|
176
|
+
return [
|
|
177
|
+
{
|
|
178
|
+
"uri": "runway://status",
|
|
179
|
+
"name": "Live Token Runway Telemetry",
|
|
180
|
+
"description": "Real-time snapshot of remaining limits and countdowns across all providers.",
|
|
181
|
+
"mimeType": "application/json",
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
def _read_resource(self, req_id: Any, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
186
|
+
uri = params.get("uri")
|
|
187
|
+
if uri == "runway://status":
|
|
188
|
+
readings = self.get_readings()
|
|
189
|
+
data = {k: v.to_dict() for k, v in readings.items()}
|
|
190
|
+
return make_response(
|
|
191
|
+
req_id,
|
|
192
|
+
{
|
|
193
|
+
"contents": [
|
|
194
|
+
{
|
|
195
|
+
"uri": uri,
|
|
196
|
+
"mimeType": "application/json",
|
|
197
|
+
"text": json.dumps(data, indent=2),
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
return make_error(req_id, INVALID_PARAMS, f"Unknown resource URI: {uri}")
|
|
203
|
+
|
|
204
|
+
def _list_prompts(self) -> List[Dict[str, Any]]:
|
|
205
|
+
return [
|
|
206
|
+
{
|
|
207
|
+
"name": "runway_governance",
|
|
208
|
+
"description": "Self-governance guidelines for operating under token rate-limits and quotas.",
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
def _get_prompt(self, req_id: Any, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
213
|
+
prompt_name = params.get("name")
|
|
214
|
+
if prompt_name == "runway_governance":
|
|
215
|
+
prompt_text = (
|
|
216
|
+
"You are an AI agent equipped with real-time awareness of your token quotas via Token Runway.\n\n"
|
|
217
|
+
"Before initiating resource-heavy operations (e.g. fanning out multiple subagents, dumping whole files >500 lines, "
|
|
218
|
+
"or running unbounded search loops), call `assess_action` or `check_runway`.\n\n"
|
|
219
|
+
"- If utilization is >=95%: Switch to quota-preserving strategies (surgical line edits, targeted ripgrep, sequential steps).\n"
|
|
220
|
+
"- If reset is within 20 minutes and quota is high: Defer heavy work and perform lightweight verification.\n"
|
|
221
|
+
"- If utilization is <80%: You have full operational headroom."
|
|
222
|
+
)
|
|
223
|
+
return make_response(
|
|
224
|
+
req_id,
|
|
225
|
+
{
|
|
226
|
+
"description": "Runway governance instructions",
|
|
227
|
+
"messages": [
|
|
228
|
+
{
|
|
229
|
+
"role": "user",
|
|
230
|
+
"content": {"type": "text", "text": prompt_text},
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
)
|
|
235
|
+
return make_error(req_id, INVALID_PARAMS, f"Unknown prompt: {prompt_name}")
|
|
236
|
+
|
|
237
|
+
def run_stdio(self) -> None:
|
|
238
|
+
"""Run the MCP server event loop over standard input and standard output."""
|
|
239
|
+
for line in sys.stdin:
|
|
240
|
+
line = line.strip()
|
|
241
|
+
if not line:
|
|
242
|
+
continue
|
|
243
|
+
try:
|
|
244
|
+
msg = json.loads(line)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
write_message(make_error(None, -32700, f"Parse error: {e}"))
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
resp = self.handle_request(msg)
|
|
251
|
+
if resp is not None:
|
|
252
|
+
write_message(resp)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
req_id = msg.get("id") if isinstance(msg, dict) else None
|
|
255
|
+
write_message(make_error(req_id, INTERNAL_ERROR, f"Internal server error: {e}"))
|