oddyssey-mcp 0.1.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.
- oddyssey_mcp-0.1.0/PKG-INFO +8 -0
- oddyssey_mcp-0.1.0/app/__init__.py +1 -0
- oddyssey_mcp-0.1.0/app/server.py +45 -0
- oddyssey_mcp-0.1.0/app/stack.py +111 -0
- oddyssey_mcp-0.1.0/oddyssey_mcp.egg-info/PKG-INFO +8 -0
- oddyssey_mcp-0.1.0/oddyssey_mcp.egg-info/SOURCES.txt +10 -0
- oddyssey_mcp-0.1.0/oddyssey_mcp.egg-info/dependency_links.txt +1 -0
- oddyssey_mcp-0.1.0/oddyssey_mcp.egg-info/entry_points.txt +2 -0
- oddyssey_mcp-0.1.0/oddyssey_mcp.egg-info/requires.txt +2 -0
- oddyssey_mcp-0.1.0/oddyssey_mcp.egg-info/top_level.txt +1 -0
- oddyssey_mcp-0.1.0/pyproject.toml +31 -0
- oddyssey_mcp-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""oddyssey MCP server package."""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""The oddyssey MCP server: pilots the local observability stack over stdio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# mcp 2.0 renamed the high-level server: `mcp.server.fastmcp.FastMCP` is gone,
|
|
6
|
+
# the equivalent is `mcp.server.MCPServer` (canonically
|
|
7
|
+
# `mcp.server.mcpserver.MCPServer`). The `@server.tool()` decorator and the
|
|
8
|
+
# synchronous `run()` entrypoint (transport defaults to "stdio") are unchanged.
|
|
9
|
+
from mcp.server import MCPServer
|
|
10
|
+
|
|
11
|
+
from . import stack as stack_ops
|
|
12
|
+
|
|
13
|
+
mcp = MCPServer("oddyssey")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mcp.tool()
|
|
17
|
+
def odd_stack_up() -> dict:
|
|
18
|
+
"""Start the local LGTM observability stack (Grafana, Tempo, Prometheus, Loki, OTLP)."""
|
|
19
|
+
return stack_ops.stack_up()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
def odd_stack_down() -> dict:
|
|
24
|
+
"""Stop and remove the local LGTM stack; stored telemetry does not survive."""
|
|
25
|
+
return stack_ops.stack_down()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@mcp.tool()
|
|
29
|
+
def odd_stack_status() -> dict:
|
|
30
|
+
"""Check whether the local LGTM stack is up (Prometheus and Tempo ready)."""
|
|
31
|
+
return stack_ops.stack_status()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@mcp.tool()
|
|
35
|
+
def odd_stack_reset() -> dict:
|
|
36
|
+
"""Wipe all stored telemetry (traces, metrics, logs, profiles) and return a fresh, ready stack."""
|
|
37
|
+
return stack_ops.stack_reset()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main() -> None:
|
|
41
|
+
mcp.run()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""LGTM stack lifecycle: the server drives Docker directly, no compose file.
|
|
2
|
+
|
|
3
|
+
The container definition is embedded here (image pin, name, ports), so the
|
|
4
|
+
server is standalone: Docker is the only prerequisite.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
IMAGE = "grafana/otel-lgtm:0.30.2"
|
|
15
|
+
CONTAINER_NAME = "oddyssey-lgtm"
|
|
16
|
+
PORTS = ("3000:3000", "4317:4317", "4318:4318")
|
|
17
|
+
|
|
18
|
+
# Readiness is probed through the Grafana datasource proxy: one request
|
|
19
|
+
# checks both Grafana and the backend behind it, and only Grafana's port
|
|
20
|
+
# needs to be exposed.
|
|
21
|
+
PROMETHEUS_READY = "http://localhost:3000/api/datasources/proxy/uid/prometheus/-/ready"
|
|
22
|
+
TEMPO_READY = "http://localhost:3000/api/datasources/proxy/uid/tempo/ready"
|
|
23
|
+
GRAFANA_URL = "http://localhost:3000"
|
|
24
|
+
OTLP_ENDPOINT = "http://localhost:4317"
|
|
25
|
+
STARTUP_TIMEOUT_S = 120
|
|
26
|
+
POLL_INTERVAL_S = 2
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_args() -> list[str]:
|
|
30
|
+
"""The docker run command that creates the stack container."""
|
|
31
|
+
port_flags = [flag for mapping in PORTS for flag in ("-p", mapping)]
|
|
32
|
+
return ["docker", "run", "-d", "--name", CONTAINER_NAME, *port_flags, IMAGE]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _docker(*args: str) -> subprocess.CompletedProcess:
|
|
36
|
+
return subprocess.run(
|
|
37
|
+
["docker", *args],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=False,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _container_state() -> str:
|
|
45
|
+
"""One of "running", "stopped", or "absent"."""
|
|
46
|
+
result = _docker("inspect", "--format", "{{.State.Running}}", CONTAINER_NAME)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
return "absent"
|
|
49
|
+
return "running" if result.stdout.strip() == "true" else "stopped"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _probe(client: httpx.Client, url: str) -> bool:
|
|
53
|
+
try:
|
|
54
|
+
return client.get(url).status_code == 200
|
|
55
|
+
except httpx.TransportError:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def stack_status(transport: httpx.BaseTransport | None = None) -> dict:
|
|
60
|
+
"""Probe readiness endpoints; a down stack is a status, not an error."""
|
|
61
|
+
with httpx.Client(timeout=3.0, transport=transport) as client:
|
|
62
|
+
prometheus = _probe(client, PROMETHEUS_READY)
|
|
63
|
+
tempo = _probe(client, TEMPO_READY)
|
|
64
|
+
return {"running": prometheus and tempo, "prometheus": prometheus, "tempo": tempo}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def stack_up() -> dict:
|
|
68
|
+
"""Start the stack container (idempotent) and wait until it is ready."""
|
|
69
|
+
state = _container_state()
|
|
70
|
+
if state == "stopped":
|
|
71
|
+
result = _docker("start", CONTAINER_NAME)
|
|
72
|
+
if result.returncode != 0:
|
|
73
|
+
raise RuntimeError(f"docker start failed: {result.stderr.strip()}")
|
|
74
|
+
elif state == "absent":
|
|
75
|
+
result = subprocess.run(run_args(), capture_output=True, text=True, check=False)
|
|
76
|
+
if result.returncode != 0:
|
|
77
|
+
raise RuntimeError(f"docker run failed: {result.stderr.strip()}")
|
|
78
|
+
status = stack_status()
|
|
79
|
+
deadline = time.monotonic() + STARTUP_TIMEOUT_S
|
|
80
|
+
while time.monotonic() < deadline:
|
|
81
|
+
if status["running"]:
|
|
82
|
+
return {
|
|
83
|
+
"running": True,
|
|
84
|
+
"grafana_url": GRAFANA_URL,
|
|
85
|
+
"otlp_endpoint": OTLP_ENDPOINT,
|
|
86
|
+
}
|
|
87
|
+
time.sleep(POLL_INTERVAL_S)
|
|
88
|
+
status = stack_status()
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
f"stack did not become ready within {STARTUP_TIMEOUT_S}s: {status}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def stack_down() -> dict:
|
|
95
|
+
"""Destroy the stack container (and its data); absent is already down."""
|
|
96
|
+
result = _docker("rm", "--force", "--volumes", CONTAINER_NAME)
|
|
97
|
+
if result.returncode != 0 and "No such container" not in result.stderr:
|
|
98
|
+
raise RuntimeError(f"docker rm failed: {result.stderr.strip()}")
|
|
99
|
+
return {"running": False}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def stack_reset() -> dict:
|
|
103
|
+
"""Wipe all stored telemetry and return a fresh, ready stack.
|
|
104
|
+
|
|
105
|
+
The stack runs without volumes, so destroying the container erases every
|
|
106
|
+
stored signal (traces, metrics, logs, profiles) by construction; a new
|
|
107
|
+
container then starts from the image. After a reset, everything the
|
|
108
|
+
stack contains IS the next run - no window arithmetic needed.
|
|
109
|
+
"""
|
|
110
|
+
stack_down()
|
|
111
|
+
return stack_up()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
app/__init__.py
|
|
3
|
+
app/server.py
|
|
4
|
+
app/stack.py
|
|
5
|
+
oddyssey_mcp.egg-info/PKG-INFO
|
|
6
|
+
oddyssey_mcp.egg-info/SOURCES.txt
|
|
7
|
+
oddyssey_mcp.egg-info/dependency_links.txt
|
|
8
|
+
oddyssey_mcp.egg-info/entry_points.txt
|
|
9
|
+
oddyssey_mcp.egg-info/requires.txt
|
|
10
|
+
oddyssey_mcp.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
oddyssey_mcp
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oddyssey-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Oddyssey MCP server: pilots the local observability stack for CLI coding agents"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mcp==2.0.0",
|
|
9
|
+
"httpx==0.28.1",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
oddyssey-mcp = "oddyssey_mcp.server:main"
|
|
14
|
+
|
|
15
|
+
[dependency-groups]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest==9.1.1",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools==84.0.0"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
packages = ["oddyssey_mcp"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.package-dir]
|
|
28
|
+
oddyssey_mcp = "app"
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
addopts = ""
|