codex-flow 2.1.13__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.
- codex_flow/__init__.py +28 -0
- codex_flow/__main__.py +9 -0
- codex_flow/cli.py +242 -0
- codex_flow/data/LICENSE +21 -0
- codex_flow/data/README.en.md +303 -0
- codex_flow/data/README.md +305 -0
- codex_flow/data/VERSION +1 -0
- codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
- codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
- codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
- codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
- codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
- codex_flow/data/apps/macos-overlay/README.en.md +121 -0
- codex_flow/data/apps/macos-overlay/README.md +123 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
- codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
- codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
- codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
- codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
- codex_flow/data/apps/macos-overlay/build.sh +75 -0
- codex_flow/data/benchmark/corpus.json +103 -0
- codex_flow/data/benchmark/manifest.example.json +41 -0
- codex_flow/data/benchmark/manifest.schema.json +137 -0
- codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
- codex_flow/data/benchmark/profiles.json +90 -0
- codex_flow/data/benchmark/schema.json +77 -0
- codex_flow/data/benchmark/tasks.json +50 -0
- codex_flow/data/completions/codex-flow.bash +34 -0
- codex_flow/data/completions/codex-flow.zsh +52 -0
- codex_flow/data/glama.json +6 -0
- codex_flow/data/install-release.ps1 +126 -0
- codex_flow/data/install-release.sh +155 -0
- codex_flow/data/install.ps1 +349 -0
- codex_flow/data/install.sh +362 -0
- codex_flow/data/policy/benchmark.toml +49 -0
- codex_flow/data/policy/defaults.toml +70 -0
- codex_flow/data/scripts/analyze-benchmark.py +510 -0
- codex_flow/data/scripts/benchmark-local.py +171 -0
- codex_flow/data/scripts/check-recommendation.py +277 -0
- codex_flow/data/scripts/doctor.py +449 -0
- codex_flow/data/scripts/generate-release-manifest.py +74 -0
- codex_flow/data/scripts/localization.py +192 -0
- codex_flow/data/scripts/manage-hooks.py +448 -0
- codex_flow/data/scripts/manage-instructions.py +389 -0
- codex_flow/data/scripts/manage-shell.py +151 -0
- codex_flow/data/scripts/materialize-corpus.py +193 -0
- codex_flow/data/scripts/menu.py +646 -0
- codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
- codex_flow/data/scripts/package-release.py +132 -0
- codex_flow/data/scripts/render-benchmark-report.py +292 -0
- codex_flow/data/scripts/run-benchmark.py +829 -0
- codex_flow/data/scripts/strategies/__init__.py +28 -0
- codex_flow/data/scripts/strategies/balanced.py +115 -0
- codex_flow/data/scripts/strategies/base.py +363 -0
- codex_flow/data/scripts/strategies/efficient.py +158 -0
- codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
- codex_flow/data/scripts/strategies/quality.py +209 -0
- codex_flow/data/scripts/strategies/speed.py +108 -0
- codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
- codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
- codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
- codex_flow/data/scripts/strategy_runtime.py +1091 -0
- codex_flow/data/scripts/telemetry.py +400 -0
- codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
- codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
- codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
- codex_flow/data/scripts/telemetry_core/common.py +421 -0
- codex_flow/data/scripts/telemetry_core/latency.py +593 -0
- codex_flow/data/scripts/telemetry_core/query.py +427 -0
- codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
- codex_flow/data/scripts/telemetry_core/render.py +460 -0
- codex_flow/data/scripts/telemetry_core/repair.py +223 -0
- codex_flow/data/scripts/ui.py +266 -0
- codex_flow/data/scripts/update-homebrew-formula.py +146 -0
- codex_flow/data/scripts/update_runtime_config.py +134 -0
- codex_flow/data/scripts/updater.py +1718 -0
- codex_flow/data/smithery.yaml +18 -0
- codex_flow/data/templates/agents/worker-explorer.toml +24 -0
- codex_flow/data/templates/agents/worker-implementer.toml +49 -0
- codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
- codex_flow/data/templates/flow-pilot-instructions.md +35 -0
- codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
- codex_flow/mcp.py +35 -0
- codex_flow-2.1.13.dist-info/METADATA +342 -0
- codex_flow-2.1.13.dist-info/RECORD +113 -0
- codex_flow-2.1.13.dist-info/WHEEL +5 -0
- codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
- codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
- codex_flow-2.1.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Minimal loopback MCP Streamable HTTP server for FlowPilot telemetry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import ipaddress
|
|
7
|
+
import json
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from http import HTTPStatus
|
|
12
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_MODULE_DIR = Path(__file__).resolve().parent
|
|
18
|
+
if __package__:
|
|
19
|
+
from . import adapter
|
|
20
|
+
else:
|
|
21
|
+
if str(_MODULE_DIR) not in sys.path:
|
|
22
|
+
sys.path.insert(0, str(_MODULE_DIR))
|
|
23
|
+
import adapter # type: ignore
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
JSONRPC_VERSION = "2.0"
|
|
27
|
+
SERVER_NAME = "chatgpt-codex-flow"
|
|
28
|
+
SERVER_VERSION = "2.1.13"
|
|
29
|
+
MAX_REQUEST_BYTES = 1024 * 1024
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _response(request_id: Any, result: Any = None, error: Any = None) -> Dict[str, Any]:
|
|
33
|
+
payload: Dict[str, Any] = {"jsonrpc": JSONRPC_VERSION, "id": request_id}
|
|
34
|
+
if error is not None:
|
|
35
|
+
payload["error"] = error
|
|
36
|
+
else:
|
|
37
|
+
payload["result"] = result
|
|
38
|
+
return payload
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _error(code: int, message: str) -> Dict[str, Any]:
|
|
42
|
+
return {"code": code, "message": message}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _valid_id(value: Any) -> bool:
|
|
46
|
+
return value is None or isinstance(value, (str, int, float)) and not isinstance(value, bool)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _initialize_result(params: Any) -> Dict[str, Any]:
|
|
50
|
+
requested = params.get("protocolVersion") if isinstance(params, dict) else None
|
|
51
|
+
# Echo versions understood by current MCP clients. If a future client
|
|
52
|
+
# version is sent, fall back to this server's known Streamable HTTP version.
|
|
53
|
+
supported = {"2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"}
|
|
54
|
+
protocol_version = requested if requested in supported else "2025-03-26"
|
|
55
|
+
return {
|
|
56
|
+
"protocolVersion": protocol_version,
|
|
57
|
+
"capabilities": {
|
|
58
|
+
"tools": {"listChanged": False},
|
|
59
|
+
"resources": {"subscribe": False, "listChanged": False},
|
|
60
|
+
},
|
|
61
|
+
"serverInfo": {
|
|
62
|
+
"name": SERVER_NAME,
|
|
63
|
+
"title": "FlowPilot telemetry",
|
|
64
|
+
"version": SERVER_VERSION,
|
|
65
|
+
},
|
|
66
|
+
"instructions": "Use flowpilot_get_telemetry for read-only run telemetry.",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _dispatch(request: Any) -> Optional[Dict[str, Any]]:
|
|
71
|
+
"""Dispatch one JSON-RPC value; ``None`` means notification."""
|
|
72
|
+
|
|
73
|
+
if not isinstance(request, dict):
|
|
74
|
+
return _response(None, error=_error(-32600, "Invalid Request"))
|
|
75
|
+
request_id = request.get("id")
|
|
76
|
+
has_id = "id" in request
|
|
77
|
+
if request.get("jsonrpc") != JSONRPC_VERSION:
|
|
78
|
+
return _response(request_id if has_id else None, error=_error(-32600, "Invalid Request"))
|
|
79
|
+
if has_id and not _valid_id(request_id):
|
|
80
|
+
return _response(None, error=_error(-32600, "Invalid Request"))
|
|
81
|
+
method = request.get("method")
|
|
82
|
+
if not isinstance(method, str):
|
|
83
|
+
return _response(request_id if has_id else None, error=_error(-32600, "Invalid Request"))
|
|
84
|
+
params = request.get("params")
|
|
85
|
+
|
|
86
|
+
if method == "notifications/initialized":
|
|
87
|
+
# MCP sends this as a notification (without an id). If a caller
|
|
88
|
+
# incorrectly supplies an id, preserve JSON-RPC request semantics
|
|
89
|
+
# instead of silently dropping that id.
|
|
90
|
+
return None if not has_id else _response(request_id, result={})
|
|
91
|
+
if method == "initialize":
|
|
92
|
+
if params is not None and not isinstance(params, dict):
|
|
93
|
+
result = _response(request_id if has_id else None, error=_error(-32602, "Invalid params"))
|
|
94
|
+
else:
|
|
95
|
+
result = _response(request_id if has_id else None, result=_initialize_result(params or {}))
|
|
96
|
+
elif method == "ping":
|
|
97
|
+
result = _response(request_id if has_id else None, result={})
|
|
98
|
+
elif method == "tools/list":
|
|
99
|
+
result = _response(request_id if has_id else None, result={"tools": [adapter.tool_descriptor()]})
|
|
100
|
+
elif method == "resources/list":
|
|
101
|
+
result = _response(
|
|
102
|
+
request_id if has_id else None,
|
|
103
|
+
result={"resources": [adapter.resource_descriptor()]},
|
|
104
|
+
)
|
|
105
|
+
elif method == "resources/read":
|
|
106
|
+
if not isinstance(params, dict) or not isinstance(params.get("uri"), str):
|
|
107
|
+
result = _response(request_id if has_id else None, error=_error(-32602, "Invalid params"))
|
|
108
|
+
elif params.get("uri") != adapter.RESOURCE_URI:
|
|
109
|
+
result = _response(request_id if has_id else None, error=_error(-32602, "Unknown resource"))
|
|
110
|
+
else:
|
|
111
|
+
try:
|
|
112
|
+
text = adapter.read_widget()
|
|
113
|
+
except adapter.WidgetResourceError:
|
|
114
|
+
result = _response(request_id if has_id else None, error=_error(-32002, "Resource unavailable"))
|
|
115
|
+
else:
|
|
116
|
+
result = _response(
|
|
117
|
+
request_id if has_id else None,
|
|
118
|
+
result={
|
|
119
|
+
"contents": [
|
|
120
|
+
{
|
|
121
|
+
"uri": adapter.RESOURCE_URI,
|
|
122
|
+
"mimeType": adapter.RESOURCE_MIME_TYPE,
|
|
123
|
+
"text": text,
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
elif method == "tools/call":
|
|
129
|
+
if not isinstance(params, dict) or not isinstance(params.get("name"), str):
|
|
130
|
+
result = _response(request_id if has_id else None, error=_error(-32602, "Invalid params"))
|
|
131
|
+
elif params.get("name") != adapter.TOOL_NAME:
|
|
132
|
+
result = _response(request_id if has_id else None, error=_error(-32602, "Unknown tool"))
|
|
133
|
+
else:
|
|
134
|
+
arguments = params.get("arguments")
|
|
135
|
+
if arguments is not None and not isinstance(arguments, dict):
|
|
136
|
+
result = _response(request_id if has_id else None, error=_error(-32602, "Invalid params"))
|
|
137
|
+
else:
|
|
138
|
+
try:
|
|
139
|
+
tool_value = adapter.tool_result(arguments or {})
|
|
140
|
+
except Exception:
|
|
141
|
+
# Telemetry is fail-open; a read adapter failure must not
|
|
142
|
+
# terminate the HTTP process or touch the collector.
|
|
143
|
+
tool_value = {
|
|
144
|
+
"content": [
|
|
145
|
+
{
|
|
146
|
+
"type": "text",
|
|
147
|
+
"text": "FlowPilot telemetry is temporarily unavailable.",
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"structuredContent": adapter.unavailable_telemetry(),
|
|
151
|
+
"isError": True,
|
|
152
|
+
}
|
|
153
|
+
result = _response(request_id if has_id else None, result=tool_value)
|
|
154
|
+
else:
|
|
155
|
+
result = _response(
|
|
156
|
+
request_id if has_id else None,
|
|
157
|
+
error=_error(-32601, "Method not found"),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return None if not has_id else result
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def dispatch_json(value: Any) -> Optional[Any]:
|
|
164
|
+
"""Dispatch a single request or a JSON-RPC batch."""
|
|
165
|
+
|
|
166
|
+
if isinstance(value, list):
|
|
167
|
+
if not value:
|
|
168
|
+
return _response(None, error=_error(-32600, "Invalid Request"))
|
|
169
|
+
responses = [_dispatch(item) for item in value]
|
|
170
|
+
responses = [item for item in responses if item is not None]
|
|
171
|
+
return responses or None
|
|
172
|
+
return _dispatch(value)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _is_loopback_host(host: str) -> bool:
|
|
176
|
+
normalized = host.strip().lower()
|
|
177
|
+
if normalized == "localhost":
|
|
178
|
+
return True
|
|
179
|
+
try:
|
|
180
|
+
return ipaddress.ip_address(normalized).is_loopback
|
|
181
|
+
except ValueError:
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class LoopbackHTTPServer(ThreadingHTTPServer):
|
|
186
|
+
daemon_threads = True
|
|
187
|
+
allow_reuse_address = True
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class LoopbackHTTPServerV6(LoopbackHTTPServer):
|
|
191
|
+
address_family = __import__("socket").AF_INET6
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class MCPRequestHandler(BaseHTTPRequestHandler):
|
|
195
|
+
protocol_version = "HTTP/1.1"
|
|
196
|
+
|
|
197
|
+
def log_message(self, format_string: str, *args: Any) -> None:
|
|
198
|
+
# A long-running local adapter should not fill stderr with access logs.
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
def _headers(self) -> None:
|
|
202
|
+
self.send_header("Cache-Control", "no-store")
|
|
203
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
204
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
205
|
+
self.send_header(
|
|
206
|
+
"Access-Control-Allow-Headers",
|
|
207
|
+
"Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _send_json(self, status: int, value: Any) -> None:
|
|
211
|
+
data = json.dumps(value, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
|
212
|
+
self.send_response(status)
|
|
213
|
+
self._headers()
|
|
214
|
+
self.send_header("Content-Type", "application/json")
|
|
215
|
+
self.send_header("Content-Length", str(len(data)))
|
|
216
|
+
self.end_headers()
|
|
217
|
+
self.wfile.write(data)
|
|
218
|
+
|
|
219
|
+
def _send_empty(self, status: int) -> None:
|
|
220
|
+
self.send_response(status)
|
|
221
|
+
self._headers()
|
|
222
|
+
self.send_header("Content-Length", "0")
|
|
223
|
+
self.end_headers()
|
|
224
|
+
|
|
225
|
+
def do_OPTIONS(self) -> None:
|
|
226
|
+
if self.path not in ("/mcp", "/healthz"):
|
|
227
|
+
self._send_empty(HTTPStatus.NOT_FOUND)
|
|
228
|
+
return
|
|
229
|
+
self._send_empty(HTTPStatus.NO_CONTENT)
|
|
230
|
+
|
|
231
|
+
def do_GET(self) -> None:
|
|
232
|
+
if self.path == "/healthz":
|
|
233
|
+
self._send_json(
|
|
234
|
+
HTTPStatus.OK,
|
|
235
|
+
{"ok": True, "service": SERVER_NAME, "version": SERVER_VERSION},
|
|
236
|
+
)
|
|
237
|
+
return
|
|
238
|
+
self._send_json(HTTPStatus.NOT_FOUND, {"error": "Not found"})
|
|
239
|
+
|
|
240
|
+
def _read_json(self) -> Tuple[Optional[Any], Optional[Tuple[int, str]]]:
|
|
241
|
+
raw_length = self.headers.get("Content-Length")
|
|
242
|
+
if raw_length is None:
|
|
243
|
+
return None, (HTTPStatus.LENGTH_REQUIRED, "Content-Length is required")
|
|
244
|
+
try:
|
|
245
|
+
length = int(raw_length)
|
|
246
|
+
except (TypeError, ValueError):
|
|
247
|
+
return None, (HTTPStatus.BAD_REQUEST, "Invalid Content-Length")
|
|
248
|
+
if length < 0 or length > MAX_REQUEST_BYTES:
|
|
249
|
+
return None, (HTTPStatus.REQUEST_ENTITY_TOO_LARGE, "Request body is too large")
|
|
250
|
+
try:
|
|
251
|
+
raw = self.rfile.read(length)
|
|
252
|
+
value = json.loads(raw.decode("utf-8"))
|
|
253
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
254
|
+
return None, (HTTPStatus.BAD_REQUEST, "Parse error")
|
|
255
|
+
return value, None
|
|
256
|
+
|
|
257
|
+
def do_POST(self) -> None:
|
|
258
|
+
if self.path != "/mcp":
|
|
259
|
+
self._send_json(HTTPStatus.NOT_FOUND, {"error": "Not found"})
|
|
260
|
+
return
|
|
261
|
+
value, read_error = self._read_json()
|
|
262
|
+
if read_error is not None:
|
|
263
|
+
status, message = read_error
|
|
264
|
+
self._send_json(
|
|
265
|
+
status,
|
|
266
|
+
_response(None, error=_error(-32700, message)),
|
|
267
|
+
)
|
|
268
|
+
return
|
|
269
|
+
try:
|
|
270
|
+
result = dispatch_json(value)
|
|
271
|
+
except Exception:
|
|
272
|
+
# Never propagate an adapter or dispatch failure into the server
|
|
273
|
+
# loop. The collector is a separate process and remains untouched.
|
|
274
|
+
result = _response(None, error=_error(-32603, "Internal error"))
|
|
275
|
+
if result is None:
|
|
276
|
+
self._send_empty(HTTPStatus.NO_CONTENT)
|
|
277
|
+
else:
|
|
278
|
+
self._send_json(HTTPStatus.OK, result)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _make_server(host: str, port: int) -> LoopbackHTTPServer:
|
|
282
|
+
if not _is_loopback_host(host):
|
|
283
|
+
raise ValueError("--host must be a loopback address")
|
|
284
|
+
server_class = LoopbackHTTPServerV6 if ":" in host else LoopbackHTTPServer
|
|
285
|
+
return server_class((host, port), MCPRequestHandler)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
289
|
+
parser = argparse.ArgumentParser(description="FlowPilot telemetry MCP server")
|
|
290
|
+
parser.add_argument("--host", default="127.0.0.1", help="loopback bind address")
|
|
291
|
+
parser.add_argument("--port", default=8787, type=int, help="TCP port (default: 8787)")
|
|
292
|
+
parser.add_argument("--stdio", action="store_true", help="run MCP server over standard I/O (JSON-RPC lines)")
|
|
293
|
+
return parser
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def run_stdio() -> int:
|
|
297
|
+
"""Run standard JSON-RPC loop over stdin/stdout."""
|
|
298
|
+
for line in sys.stdin:
|
|
299
|
+
raw = line.strip()
|
|
300
|
+
if not raw:
|
|
301
|
+
continue
|
|
302
|
+
try:
|
|
303
|
+
value = json.loads(raw)
|
|
304
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
305
|
+
resp = _response(None, error=_error(-32700, "Parse error"))
|
|
306
|
+
sys.stdout.write(json.dumps(resp) + "\n")
|
|
307
|
+
sys.stdout.flush()
|
|
308
|
+
continue
|
|
309
|
+
try:
|
|
310
|
+
result = dispatch_json(value)
|
|
311
|
+
except Exception:
|
|
312
|
+
result = _response(None, error=_error(-32603, "Internal error"))
|
|
313
|
+
if result is not None:
|
|
314
|
+
sys.stdout.write(json.dumps(result) + "\n")
|
|
315
|
+
sys.stdout.flush()
|
|
316
|
+
return 0
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
320
|
+
args = build_parser().parse_args(argv)
|
|
321
|
+
if args.stdio:
|
|
322
|
+
return run_stdio()
|
|
323
|
+
if args.port < 0 or args.port > 65535:
|
|
324
|
+
print("--port must be between 0 and 65535", file=sys.stderr)
|
|
325
|
+
return 2
|
|
326
|
+
try:
|
|
327
|
+
server = _make_server(args.host, args.port)
|
|
328
|
+
except (OSError, ValueError) as exc:
|
|
329
|
+
print("Unable to bind FlowPilot MCP server: %s" % exc, file=sys.stderr)
|
|
330
|
+
return 1
|
|
331
|
+
|
|
332
|
+
def stop(signum: int, frame: Any) -> None:
|
|
333
|
+
# ``shutdown`` must run off the serve_forever thread to avoid waiting
|
|
334
|
+
# on itself when SIGINT/SIGTERM arrives in the main thread.
|
|
335
|
+
threading.Thread(target=server.shutdown, daemon=True).start()
|
|
336
|
+
|
|
337
|
+
previous: Dict[int, Any] = {}
|
|
338
|
+
for signal_number in (signal.SIGINT, signal.SIGTERM):
|
|
339
|
+
try:
|
|
340
|
+
previous[signal_number] = signal.signal(signal_number, stop)
|
|
341
|
+
except (ValueError, OSError):
|
|
342
|
+
pass
|
|
343
|
+
try:
|
|
344
|
+
server.serve_forever(poll_interval=0.2)
|
|
345
|
+
except KeyboardInterrupt:
|
|
346
|
+
pass
|
|
347
|
+
finally:
|
|
348
|
+
server.server_close()
|
|
349
|
+
for signal_number, handler in previous.items():
|
|
350
|
+
try:
|
|
351
|
+
signal.signal(signal_number, handler)
|
|
352
|
+
except (ValueError, OSError):
|
|
353
|
+
pass
|
|
354
|
+
return 0
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
if __name__ == "__main__":
|
|
358
|
+
raise SystemExit(main())
|