phlo-mcp 0.2.0__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.
- phlo_mcp-0.2.0/PKG-INFO +18 -0
- phlo_mcp-0.2.0/README.md +185 -0
- phlo_mcp-0.2.0/pyproject.toml +52 -0
- phlo_mcp-0.2.0/setup.cfg +4 -0
- phlo_mcp-0.2.0/src/phlo_mcp/__init__.py +5 -0
- phlo_mcp-0.2.0/src/phlo_mcp/api_client.py +189 -0
- phlo_mcp-0.2.0/src/phlo_mcp/cli.py +55 -0
- phlo_mcp-0.2.0/src/phlo_mcp/config.py +41 -0
- phlo_mcp-0.2.0/src/phlo_mcp/run_analysis.py +231 -0
- phlo_mcp-0.2.0/src/phlo_mcp/server.py +630 -0
- phlo_mcp-0.2.0/src/phlo_mcp/tracing.py +155 -0
- phlo_mcp-0.2.0/src/phlo_mcp.egg-info/PKG-INFO +18 -0
- phlo_mcp-0.2.0/src/phlo_mcp.egg-info/SOURCES.txt +17 -0
- phlo_mcp-0.2.0/src/phlo_mcp.egg-info/dependency_links.txt +1 -0
- phlo_mcp-0.2.0/src/phlo_mcp.egg-info/entry_points.txt +2 -0
- phlo_mcp-0.2.0/src/phlo_mcp.egg-info/requires.txt +9 -0
- phlo_mcp-0.2.0/src/phlo_mcp.egg-info/top_level.txt +1 -0
- phlo_mcp-0.2.0/tests/test_phlo_mcp.py +544 -0
- phlo_mcp-0.2.0/tests/test_stack_smoke.py +77 -0
phlo_mcp-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phlo-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server for Phlo observability and lakehouse operations
|
|
5
|
+
Author-email: Phlo Team <team@phlo.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/plain
|
|
9
|
+
Requires-Dist: phlo>=0.1.0
|
|
10
|
+
Requires-Dist: phlo-api>=0.1.0
|
|
11
|
+
Requires-Dist: httpx>=0.28.1
|
|
12
|
+
Requires-Dist: mcp[cli]>=1.20.0
|
|
13
|
+
Requires-Dist: opentelemetry-sdk>=1.38.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
17
|
+
|
|
18
|
+
MCP server for Phlo observability and lakehouse operations.
|
phlo_mcp-0.2.0/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# phlo-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for Phlo observability and lakehouse operations.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`phlo-mcp` exposes curated read-only MCP tools over Phlo's observability and
|
|
8
|
+
operator surfaces. It sits on top of `phlo-api` and gives MCP clients a stable,
|
|
9
|
+
agent-friendly surface for lakehouse inspection.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv pip install -e packages/phlo-mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Exposed tools
|
|
18
|
+
|
|
19
|
+
- `get_platform_health`
|
|
20
|
+
- `get_service_status`
|
|
21
|
+
- `get_recent_alerts`
|
|
22
|
+
- `get_dashboard_links`
|
|
23
|
+
- `get_logs_query_link`
|
|
24
|
+
- `get_metrics_query_link`
|
|
25
|
+
- `get_materialization_history`
|
|
26
|
+
- `get_run_logs`
|
|
27
|
+
- `get_run_trace_spans`
|
|
28
|
+
- `get_trace_spans`
|
|
29
|
+
- `render_trace_spans_tree`
|
|
30
|
+
- `inspect_materialization`
|
|
31
|
+
- `get_asset_materialization_trace`
|
|
32
|
+
- `render_materialization_trace_tree`
|
|
33
|
+
- `render_run_trace_tree`
|
|
34
|
+
|
|
35
|
+
Optional guarded operational tools:
|
|
36
|
+
|
|
37
|
+
- `materialize_asset`
|
|
38
|
+
- `retry_failed_run`
|
|
39
|
+
- `get_dagster_run_status`
|
|
40
|
+
|
|
41
|
+
Resources:
|
|
42
|
+
|
|
43
|
+
- `phlo://runtime/config`
|
|
44
|
+
- `phlo://runtime/services`
|
|
45
|
+
- `phlo://runtime/services/{service_name}`
|
|
46
|
+
- `phlo://runtime/plugins`
|
|
47
|
+
- `phlo://runtime/assets`
|
|
48
|
+
- `phlo://runtime/assets/{asset_key_path}`
|
|
49
|
+
- `phlo://runtime/schemas/{asset_key_path}`
|
|
50
|
+
- `phlo://runtime/contracts`
|
|
51
|
+
- `phlo://runtime/contracts/{table_name}`
|
|
52
|
+
- `phlo://runtime/dashboards`
|
|
53
|
+
- `phlo://docs/packages/{package_name}`
|
|
54
|
+
|
|
55
|
+
Run-level and materialization tools rely on the backing `phlo-api` having access
|
|
56
|
+
to Dagster asset history, Loki log queries, and ClickStack OTEL trace storage.
|
|
57
|
+
Trace tools can be filtered by run id, asset key, job name, service name, span
|
|
58
|
+
name, status code, and start/end time.
|
|
59
|
+
Resource URIs are read-only and deterministic so MCP clients can attach Phlo
|
|
60
|
+
runtime context without invoking parameterized tools.
|
|
61
|
+
|
|
62
|
+
When real spans are available, rendered trees include:
|
|
63
|
+
- span kind
|
|
64
|
+
- status code
|
|
65
|
+
- duration
|
|
66
|
+
- selected Phlo attributes like stage, asset key, job name, and operation
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
Start a backing API first:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv run --package phlo-api uvicorn phlo_api.main:app --host 127.0.0.1 --port 4000
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Run the MCP server over stdio:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
phlo-mcp --api-base-url http://127.0.0.1:4000
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For protected `phlo-api` instances, provide a bearer token:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
phlo-mcp \
|
|
86
|
+
--api-base-url http://127.0.0.1:4000 \
|
|
87
|
+
--api-token "$PHLO_API_TOKEN"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Guarded operational tools are disabled by default. To expose asset
|
|
91
|
+
materialization and run retry tools, set `PHLO_MCP_ENABLE_WRITE_TOOLS=true` or
|
|
92
|
+
pass `--enable-write-tools` with an API token. Write tools return structured
|
|
93
|
+
`audit_context` metadata and default to `dry_run=true` where supported. Current
|
|
94
|
+
`phlo-api` operation routes support dry-run validation and run status; live
|
|
95
|
+
Dagster launch and retry are intentionally not implemented yet.
|
|
96
|
+
|
|
97
|
+
Claude Code example:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"phlo": {
|
|
103
|
+
"command": "uv",
|
|
104
|
+
"args": ["run", "--package", "phlo-mcp", "phlo-mcp", "--api-base-url", "http://127.0.0.1:4000"]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Example prompts once connected:
|
|
111
|
+
|
|
112
|
+
- "Get the latest materializations for `silver/orders`."
|
|
113
|
+
- "Fetch logs for run `abc-123`."
|
|
114
|
+
- "Inspect the latest materialization for `silver/orders`."
|
|
115
|
+
- "Get the latest materialization trace for `silver/orders`."
|
|
116
|
+
- "Render the materialization trace tree for `silver/orders`."
|
|
117
|
+
- "Get OTEL spans for run `abc-123`."
|
|
118
|
+
- "Get failed Dagster spans for asset `silver/orders` in the last hour."
|
|
119
|
+
- "Render the run trace tree for `abc-123`."
|
|
120
|
+
- "Render a trace tree for job `daily_orders`."
|
|
121
|
+
|
|
122
|
+
Run it over streamable HTTP:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
phlo-mcp \
|
|
126
|
+
--transport streamable-http \
|
|
127
|
+
--host 127.0.0.1 \
|
|
128
|
+
--port 8000 \
|
|
129
|
+
--path /mcp \
|
|
130
|
+
--api-base-url http://127.0.0.1:4000
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Optional local span capture:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
phlo-mcp --trace-file .phlo/phlo-mcp-trace.jsonl
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Live stack smoke
|
|
140
|
+
|
|
141
|
+
Run the MCP smoke against a live `phlo-api` service and its configured capability backends:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
uv run python packages/phlo-mcp/tests/smoke_stack.py --start-stack
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The smoke checks live `phlo-api`, orchestration connectivity, trace filtering
|
|
148
|
+
through the observability capability, MCP tool registration, MCP resource
|
|
149
|
+
registration, representative MCP resource reads, and a generated
|
|
150
|
+
`mcp_smoke_asset` capability fixture.
|
|
151
|
+
With `--start-stack`, the fixture is written to `.phlo/mcp-smoke-project`,
|
|
152
|
+
which is ignored by git.
|
|
153
|
+
|
|
154
|
+
When the configured observability backend requires credentials, pass them to
|
|
155
|
+
the backing `phlo-api` process with that backend's environment variables.
|
|
156
|
+
|
|
157
|
+
To verify guarded write-tool registration without calling mutation endpoints:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
uv run python packages/phlo-mcp/tests/smoke_stack.py \
|
|
161
|
+
--enable-write-tools \
|
|
162
|
+
--api-token "$PHLO_MCP_SMOKE_API_TOKEN"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
To call guarded write tools in dry-run mode, add `--exercise-write-tools` and
|
|
166
|
+
`--start-stack`, or provide `--asset-key` when testing an existing project.
|
|
167
|
+
This requires the backing `phlo-api` write endpoints to be available.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
uv run python packages/phlo-mcp/tests/smoke_stack.py \
|
|
171
|
+
--start-stack \
|
|
172
|
+
--enable-write-tools \
|
|
173
|
+
--api-token "$PHLO_MCP_SMOKE_API_TOKEN" \
|
|
174
|
+
--exercise-write-tools
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Use real data assertions when you have a known run or asset:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
uv run python packages/phlo-mcp/tests/smoke_stack.py \
|
|
181
|
+
--run-id abc-123 \
|
|
182
|
+
--require-run-spans \
|
|
183
|
+
--asset-key silver/orders \
|
|
184
|
+
--require-materialization
|
|
185
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "setuptools.build_meta"
|
|
3
|
+
requires = [
|
|
4
|
+
"setuptools>=45",
|
|
5
|
+
"wheel",
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"phlo>=0.1.0",
|
|
11
|
+
"phlo-api>=0.1.0",
|
|
12
|
+
"httpx>=0.28.1",
|
|
13
|
+
"mcp[cli]>=1.20.0",
|
|
14
|
+
"opentelemetry-sdk>=1.38.0",
|
|
15
|
+
]
|
|
16
|
+
description = "MCP server for Phlo observability and lakehouse operations"
|
|
17
|
+
name = "phlo-mcp"
|
|
18
|
+
requires-python = ">=3.11"
|
|
19
|
+
version = "0.2.0"
|
|
20
|
+
|
|
21
|
+
[[project.authors]]
|
|
22
|
+
email = "team@phlo.dev"
|
|
23
|
+
name = "Phlo Team"
|
|
24
|
+
|
|
25
|
+
[project.license]
|
|
26
|
+
text = "MIT"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.0",
|
|
31
|
+
"ruff>=0.1.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.readme]
|
|
35
|
+
content-type = "text/plain"
|
|
36
|
+
text = "MCP server for Phlo observability and lakehouse operations."
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
phlo-mcp = "phlo_mcp.cli:main"
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 100
|
|
43
|
+
target-version = "py311"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools]
|
|
46
|
+
include-package-data = true
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.package-dir]
|
|
49
|
+
"" = "src"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
phlo_mcp-0.2.0/setup.cfg
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""HTTP client helpers for phlo-mcp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from opentelemetry import trace
|
|
9
|
+
|
|
10
|
+
from phlo_mcp.config import McpConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PhloApiClient:
|
|
14
|
+
"""Small typed wrapper around phlo-api observability routes."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: McpConfig, *, tracer_name: str = "phlo.mcp") -> None:
|
|
17
|
+
self._config = config
|
|
18
|
+
self._tracer = trace.get_tracer(tracer_name)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def api_base_url(self) -> str:
|
|
22
|
+
return self._config.api_base_url
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def headers(self) -> dict[str, str]:
|
|
26
|
+
if self._config.api_token:
|
|
27
|
+
return {"Authorization": f"Bearer {self._config.api_token}"}
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
def get_platform_health(self) -> dict[str, Any]:
|
|
31
|
+
return self._get_json("/api/observability/health")
|
|
32
|
+
|
|
33
|
+
def get_config(self) -> dict[str, Any] | list[dict[str, Any]]:
|
|
34
|
+
return self._get_json("/api/config")
|
|
35
|
+
|
|
36
|
+
def get_plugins(self) -> dict[str, Any] | list[dict[str, Any]]:
|
|
37
|
+
return self._get_json("/api/plugins")
|
|
38
|
+
|
|
39
|
+
def get_services(self) -> dict[str, Any] | list[dict[str, Any]]:
|
|
40
|
+
return self._get_json("/api/services")
|
|
41
|
+
|
|
42
|
+
def get_service_info(self, service_name: str) -> dict[str, Any] | list[dict[str, Any]]:
|
|
43
|
+
return self._get_json(f"/api/services/{service_name}")
|
|
44
|
+
|
|
45
|
+
def get_assets(self) -> dict[str, Any] | list[dict[str, Any]]:
|
|
46
|
+
return self._get_json("/api/dagster/assets")
|
|
47
|
+
|
|
48
|
+
def get_asset_details(self, asset_key_path: str) -> dict[str, Any] | list[dict[str, Any]]:
|
|
49
|
+
return self._get_json(f"/api/dagster/assets/{asset_key_path}")
|
|
50
|
+
|
|
51
|
+
def get_contracts(self) -> dict[str, Any] | list[dict[str, Any]]:
|
|
52
|
+
return self._get_json("/api/contracts")
|
|
53
|
+
|
|
54
|
+
def get_contract(self, table_name: str) -> dict[str, Any] | list[dict[str, Any]]:
|
|
55
|
+
return self._get_json(f"/api/contracts/{table_name}")
|
|
56
|
+
|
|
57
|
+
def get_service_status(self) -> list[dict[str, Any]] | dict[str, Any]:
|
|
58
|
+
return self._get_json("/api/observability/services")
|
|
59
|
+
|
|
60
|
+
def get_recent_alerts(self, limit: int = 5) -> list[dict[str, Any]] | dict[str, Any]:
|
|
61
|
+
return self._get_json("/api/observability/alerts", params={"limit": limit})
|
|
62
|
+
|
|
63
|
+
def get_dashboard_links(self) -> list[dict[str, Any]] | dict[str, Any]:
|
|
64
|
+
return self._get_json("/api/observability/dashboards")
|
|
65
|
+
|
|
66
|
+
def get_run_logs(
|
|
67
|
+
self,
|
|
68
|
+
run_id: str,
|
|
69
|
+
*,
|
|
70
|
+
level: str | None = None,
|
|
71
|
+
limit: int = 200,
|
|
72
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
73
|
+
params: dict[str, Any] = {"limit": limit}
|
|
74
|
+
if level:
|
|
75
|
+
params["level"] = level
|
|
76
|
+
return self._get_json(f"/api/loki/runs/{run_id}", params=params)
|
|
77
|
+
|
|
78
|
+
def get_materialization_history(
|
|
79
|
+
self,
|
|
80
|
+
asset_key_path: str,
|
|
81
|
+
*,
|
|
82
|
+
limit: int = 10,
|
|
83
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
84
|
+
return self._get_json(
|
|
85
|
+
f"/api/dagster/assets/{asset_key_path}/history", params={"limit": limit}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get_run_trace_spans(
|
|
89
|
+
self,
|
|
90
|
+
run_id: str,
|
|
91
|
+
*,
|
|
92
|
+
limit: int = 500,
|
|
93
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
94
|
+
return self._get_json(f"/api/observability/traces/runs/{run_id}", params={"limit": limit})
|
|
95
|
+
|
|
96
|
+
def get_logs_query_link(self, service: str | None = None) -> dict[str, Any]:
|
|
97
|
+
params = {"service": service} if service else None
|
|
98
|
+
return self._get_json("/api/observability/links/logs", params=params)
|
|
99
|
+
|
|
100
|
+
def get_trace_spans(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
run_id: str | None = None,
|
|
104
|
+
asset_key: str | None = None,
|
|
105
|
+
job_name: str | None = None,
|
|
106
|
+
service_name: str | None = None,
|
|
107
|
+
span_name: str | None = None,
|
|
108
|
+
status_code: str | None = None,
|
|
109
|
+
start_time: str | None = None,
|
|
110
|
+
end_time: str | None = None,
|
|
111
|
+
limit: int = 500,
|
|
112
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
113
|
+
params: dict[str, Any] = {"limit": limit}
|
|
114
|
+
for key, value in {
|
|
115
|
+
"run_id": run_id,
|
|
116
|
+
"asset_key": asset_key,
|
|
117
|
+
"job_name": job_name,
|
|
118
|
+
"service_name": service_name,
|
|
119
|
+
"span_name": span_name,
|
|
120
|
+
"status_code": status_code,
|
|
121
|
+
"start_time": start_time,
|
|
122
|
+
"end_time": end_time,
|
|
123
|
+
}.items():
|
|
124
|
+
if value:
|
|
125
|
+
params[key] = value
|
|
126
|
+
return self._get_json("/api/observability/traces", params=params)
|
|
127
|
+
|
|
128
|
+
def get_metrics_query_link(self, metric: str | None = None) -> dict[str, Any]:
|
|
129
|
+
params = {"metric": metric} if metric else None
|
|
130
|
+
return self._get_json("/api/observability/links/metrics", params=params)
|
|
131
|
+
|
|
132
|
+
def materialize_asset(
|
|
133
|
+
self,
|
|
134
|
+
asset_key_path: str,
|
|
135
|
+
*,
|
|
136
|
+
dry_run: bool = True,
|
|
137
|
+
partition_key: str | None = None,
|
|
138
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
139
|
+
payload: dict[str, Any] = {"dry_run": dry_run}
|
|
140
|
+
if partition_key:
|
|
141
|
+
payload["partition_key"] = partition_key
|
|
142
|
+
return self._post_json(f"/api/dagster/assets/{asset_key_path}/materialize", json=payload)
|
|
143
|
+
|
|
144
|
+
def retry_run(
|
|
145
|
+
self,
|
|
146
|
+
run_id: str,
|
|
147
|
+
*,
|
|
148
|
+
dry_run: bool = True,
|
|
149
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
150
|
+
return self._post_json(f"/api/dagster/runs/{run_id}/retry", json={"dry_run": dry_run})
|
|
151
|
+
|
|
152
|
+
def get_run_status(self, run_id: str) -> dict[str, Any] | list[dict[str, Any]]:
|
|
153
|
+
return self._get_json(f"/api/dagster/runs/{run_id}/status")
|
|
154
|
+
|
|
155
|
+
def _get_json(
|
|
156
|
+
self,
|
|
157
|
+
path: str,
|
|
158
|
+
*,
|
|
159
|
+
params: dict[str, Any] | None = None,
|
|
160
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
161
|
+
url = f"{self.api_base_url}{path}"
|
|
162
|
+
with self._tracer.start_as_current_span(
|
|
163
|
+
"http.client GET",
|
|
164
|
+
attributes={
|
|
165
|
+
"http.request.method": "GET",
|
|
166
|
+
"url.full": url,
|
|
167
|
+
},
|
|
168
|
+
):
|
|
169
|
+
response = httpx.get(url, params=params, headers=self.headers, timeout=10.0)
|
|
170
|
+
response.raise_for_status()
|
|
171
|
+
return response.json()
|
|
172
|
+
|
|
173
|
+
def _post_json(
|
|
174
|
+
self,
|
|
175
|
+
path: str,
|
|
176
|
+
*,
|
|
177
|
+
json: dict[str, Any],
|
|
178
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
179
|
+
url = f"{self.api_base_url}{path}"
|
|
180
|
+
with self._tracer.start_as_current_span(
|
|
181
|
+
"http.client POST",
|
|
182
|
+
attributes={
|
|
183
|
+
"http.request.method": "POST",
|
|
184
|
+
"url.full": url,
|
|
185
|
+
},
|
|
186
|
+
):
|
|
187
|
+
response = httpx.post(url, json=json, headers=self.headers, timeout=30.0)
|
|
188
|
+
response.raise_for_status()
|
|
189
|
+
return response.json()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""CLI entrypoint for phlo-mcp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from phlo_mcp.config import McpConfig, config_from_env
|
|
8
|
+
from phlo_mcp.server import create_server
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(description="Run the Phlo MCP server")
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"--transport",
|
|
15
|
+
choices=("stdio", "streamable-http"),
|
|
16
|
+
help="MCP transport to use (defaults to PHLO_MCP_TRANSPORT or stdio)",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument("--api-base-url", help="Base URL for the backing phlo-api instance")
|
|
19
|
+
parser.add_argument("--api-token", help="Bearer token for authenticated phlo-api requests")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--enable-write-tools",
|
|
22
|
+
action="store_true",
|
|
23
|
+
help="Register guarded operational tools (requires authenticated phlo-api)",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument("--trace-file", help="Optional JSONL file to write local span events")
|
|
26
|
+
parser.add_argument("--host", help="Bind host for streamable-http transport")
|
|
27
|
+
parser.add_argument("--port", type=int, help="Bind port for streamable-http transport")
|
|
28
|
+
parser.add_argument("--path", help="HTTP path for streamable-http transport (default: /mcp)")
|
|
29
|
+
return parser
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_args() -> McpConfig:
|
|
33
|
+
parser = build_parser()
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
env_config = config_from_env()
|
|
36
|
+
return McpConfig(
|
|
37
|
+
api_base_url=(args.api_base_url or env_config.api_base_url).rstrip("/"),
|
|
38
|
+
api_token=args.api_token if args.api_token is not None else env_config.api_token,
|
|
39
|
+
enable_write_tools=args.enable_write_tools or env_config.enable_write_tools,
|
|
40
|
+
trace_file=args.trace_file if args.trace_file is not None else env_config.trace_file,
|
|
41
|
+
transport=args.transport or env_config.transport,
|
|
42
|
+
host=args.host or env_config.host,
|
|
43
|
+
port=args.port if args.port is not None else env_config.port,
|
|
44
|
+
streamable_http_path=args.path or env_config.streamable_http_path,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> None:
|
|
49
|
+
config = parse_args()
|
|
50
|
+
server = create_server(config)
|
|
51
|
+
server.run(transport=config.transport)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Configuration helpers for phlo-mcp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
_DEFAULT_API_BASE_URL = "http://127.0.0.1:4000"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class McpConfig:
|
|
13
|
+
"""Runtime configuration for the Phlo MCP server."""
|
|
14
|
+
|
|
15
|
+
api_base_url: str = _DEFAULT_API_BASE_URL
|
|
16
|
+
api_token: str | None = None
|
|
17
|
+
enable_write_tools: bool = False
|
|
18
|
+
trace_file: str | None = None
|
|
19
|
+
transport: str = "stdio"
|
|
20
|
+
host: str = "127.0.0.1"
|
|
21
|
+
port: int = 8000
|
|
22
|
+
streamable_http_path: str = "/mcp"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _truthy_env(name: str) -> bool:
|
|
26
|
+
return os.environ.get(name, "").lower() in {"1", "true", "yes", "on"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def config_from_env() -> McpConfig:
|
|
30
|
+
"""Load MCP configuration from environment variables."""
|
|
31
|
+
port = int(os.environ.get("PHLO_MCP_PORT", "8000"))
|
|
32
|
+
return McpConfig(
|
|
33
|
+
api_base_url=os.environ.get("PHLO_MCP_API_BASE_URL", _DEFAULT_API_BASE_URL).rstrip("/"),
|
|
34
|
+
api_token=os.environ.get("PHLO_MCP_API_TOKEN") or None,
|
|
35
|
+
enable_write_tools=_truthy_env("PHLO_MCP_ENABLE_WRITE_TOOLS"),
|
|
36
|
+
trace_file=os.environ.get("PHLO_MCP_TRACE_FILE") or None,
|
|
37
|
+
transport=os.environ.get("PHLO_MCP_TRANSPORT", "stdio"),
|
|
38
|
+
host=os.environ.get("PHLO_MCP_HOST", "127.0.0.1"),
|
|
39
|
+
port=port,
|
|
40
|
+
streamable_http_path=os.environ.get("PHLO_MCP_HTTP_PATH", "/mcp"),
|
|
41
|
+
)
|