diff --git a/.claude/rules/stack-and-commands.md b/.claude/rules/stack-and-commands.md index f35aef1..1923bdb 100644 --- a/.claude/rules/stack-and-commands.md +++ b/.claude/rules/stack-and-commands.md @@ -6,17 +6,16 @@ ## Dev Commands ```bash +# MCP bridge — local (no cluster) +make bridge-install # once +make bridge-test # pytest smoke tests +make bridge-run # uvicorn on :8080 + # Deploy observability stack (from repo root) cd ansible && ansible-playbook -i inventories/local.ini playbook.yml -# MCP bridge (local) -cd mcp-telemetry-bridge -pip install -r requirements.txt -uvicorn app.main:app --reload --port 8080 - # Smoke (requires cluster access) kubectl get pods -n monitoring kubectl port-forward -n mcp svc/mcp-telemetry-bridge 8080:80 -curl http://localhost:8080/healthz -curl http://localhost:8080/mcp/schema | jq . +make bridge-smoke # pytest + live curl against :8080 ``` diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03547ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +*.egg-info/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9ea42a4 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: bridge-install bridge-run bridge-test bridge-verify bridge-smoke + +bridge-install: + cd mcp-telemetry-bridge && python3 -m venv .venv && . .venv/bin/activate && pip install -r requirements-dev.txt + +bridge-run: + cd mcp-telemetry-bridge && ./scripts/run-local.sh + +bridge-test: + cd mcp-telemetry-bridge && ./scripts/verify-local.sh + +bridge-verify: bridge-test + +bridge-smoke: + cd mcp-telemetry-bridge && RUN_LIVE=1 ./scripts/verify-local.sh \ No newline at end of file diff --git a/README.md b/README.md index dac7c2b..a7047ed 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,68 @@ TeleMcp deploys a standard observability stack onto a Linux Kubernetes host via | **OpenTelemetry Collector** | `observability` | Optional OTLP fan-out to Prometheus and Loki | | **mcp-telemetry-bridge** | `mcp` | FastAPI service exposing MCP resources, tools, and prompts | -## Quick Start +## Local development (no cluster) + +Work on the MCP bridge without deploying the full observability stack. + +### Install and verify + +```bash +make bridge-install # venv + deps (once) +make bridge-test # pytest smoke: /healthz, /mcp/schema, /mcp/resource +``` + +Or from `mcp-telemetry-bridge/`: + +```bash +./scripts/verify-local.sh +``` + +### Run locally + +```bash +make bridge-run +# or: cd mcp-telemetry-bridge && ./scripts/run-local.sh +``` + +With the server up, optional live HTTP checks: + +```bash +make bridge-smoke +# or: RUN_LIVE=1 ./mcp-telemetry-bridge/scripts/verify-local.sh +``` + +Manual curls: + +```bash +curl http://127.0.0.1:8080/healthz +curl http://127.0.0.1:8080/mcp/schema | jq . +curl "http://127.0.0.1:8080/mcp/resource?uri=res://dashboards/top-pods-by-cpu.promql" +``` + +Tool calls use `POST /tools/` with a JSON body (Prometheus/Loki/K8s backends are only reachable in-cluster). + +### Agent quickstart + +When changing the bridge, agents should: + +1. Run `make bridge-test` after edits — fast, no cluster needed. +2. Introspect `GET /mcp/schema` for the current tools, resources, and prompts. +3. Call tools via `POST /tools/` (e.g. `POST /tools/promql.query` with `{"expr":"up"}`). +4. Fetch saved queries via `GET /mcp/resource?uri=`. + +Expected smoke-test surface: + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/healthz` | GET | Liveness | +| `/mcp/schema` | GET | MCP catalog (tools, resources, prompts) | +| `/mcp/resource` | GET | Saved PromQL/LogQL query by URI | +| `/tools/*` | POST | Execute a tool (needs in-cluster backends) | + +--- + +## Quick Start (full cluster deploy) ### 0) Prereqs @@ -75,6 +136,8 @@ tele-mcp/ values/ # Chart values for monitoring, logging, OTel mcp-telemetry-bridge/ # Bridge Helm chart mcp-telemetry-bridge/ # FastAPI bridge application + scripts/ # run-local.sh, verify-local.sh + tests/ # pytest smoke tests environments/ # Per-environment overrides wiki/ # Extended project and design docs ``` diff --git a/mcp-telemetry-bridge/requirements-dev.txt b/mcp-telemetry-bridge/requirements-dev.txt new file mode 100644 index 0000000..8477ab7 --- /dev/null +++ b/mcp-telemetry-bridge/requirements-dev.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pytest==8.3.3 \ No newline at end of file diff --git a/mcp-telemetry-bridge/scripts/run-local.sh b/mcp-telemetry-bridge/scripts/run-local.sh new file mode 100755 index 0000000..a90b679 --- /dev/null +++ b/mcp-telemetry-bridge/scripts/run-local.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Run the MCP bridge locally (no Kubernetes required). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +PORT="${PORT:-8080}" +HOST="${HOST:-127.0.0.1}" + +if [[ ! -d .venv ]]; then + python3 -m venv .venv +fi +# shellcheck disable=SC1091 +source .venv/bin/activate + +pip install -q -r requirements.txt + +echo "Starting MCP bridge at http://${HOST}:${PORT}" +echo "Health: curl http://${HOST}:${PORT}/healthz" +echo "Schema: curl http://${HOST}:${PORT}/mcp/schema | jq ." +exec uvicorn app.main:app --reload --host "$HOST" --port "$PORT" \ No newline at end of file diff --git a/mcp-telemetry-bridge/scripts/verify-local.sh b/mcp-telemetry-bridge/scripts/verify-local.sh new file mode 100755 index 0000000..32109ea --- /dev/null +++ b/mcp-telemetry-bridge/scripts/verify-local.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Local verification harness: pytest smoke tests + optional live HTTP checks. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +BASE_URL="${BASE_URL:-http://127.0.0.1:8080}" +RUN_LIVE="${RUN_LIVE:-0}" + +if [[ ! -d .venv ]]; then + python3 -m venv .venv +fi +# shellcheck disable=SC1091 +source .venv/bin/activate + +pip install -q -r requirements-dev.txt + +echo "==> Running pytest smoke tests" +pytest -q + +if [[ "$RUN_LIVE" == "1" ]]; then + echo "==> Live HTTP smoke against ${BASE_URL}" + curl -fsS "${BASE_URL}/healthz" | python3 -m json.tool + curl -fsS "${BASE_URL}/mcp/schema" | python3 -c " +import json, sys +schema = json.load(sys.stdin) +assert 'tools' in schema and 'resources' in schema and 'prompts' in schema +print(f\"tools={len(schema['tools'])} resources={len(schema['resources'])} prompts={len(schema['prompts'])}\") +" + echo "Live smoke passed." +else + echo "Skipping live HTTP checks (set RUN_LIVE=1 to curl a running server)." +fi + +echo "Local verification complete." \ No newline at end of file diff --git a/mcp-telemetry-bridge/tests/__init__.py b/mcp-telemetry-bridge/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mcp-telemetry-bridge/tests/test_smoke.py b/mcp-telemetry-bridge/tests/test_smoke.py new file mode 100644 index 0000000..385775c --- /dev/null +++ b/mcp-telemetry-bridge/tests/test_smoke.py @@ -0,0 +1,62 @@ +"""Smoke tests for the MCP bridge HTTP surface (no cluster required).""" + +from fastapi.testclient import TestClient + +from app.main import PROMPTS, RESOURCES, TOOLS, app + +client = TestClient(app) + +EXPECTED_TOOL_NAMES = { + "promql.query", + "loki.query", + "k8s.get", + "k8s.events", + "inventory.snapshot", +} + + +def test_healthz_returns_ok(): + response = client.get("/healthz") + assert response.status_code == 200 + body = response.json() + assert body["status"] == "ok" + assert isinstance(body["ts"], int) + + +def test_mcp_schema_exposes_resources_tools_and_prompts(): + response = client.get("/mcp/schema") + assert response.status_code == 200 + body = response.json() + + assert "resources" in body + assert "tools" in body + assert "prompts" in body + + assert len(body["resources"]) == len(RESOURCES) + assert len(body["tools"]) == len(TOOLS) + assert len(body["prompts"]) == len(PROMPTS) + + tool_names = {tool["name"] for tool in body["tools"]} + assert tool_names == EXPECTED_TOOL_NAMES + + for tool in body["tools"]: + assert "inputSchema" in tool + assert tool["inputSchema"]["type"] == "object" + + +def test_mcp_resource_returns_saved_query(): + uri = "res://dashboards/top-pods-by-cpu.promql" + response = client.get("/mcp/resource", params={"uri": uri}) + assert response.status_code == 200 + body = response.json() + assert body["uri"] == uri + assert body["mimeType"] == "text/plain" + assert "container_cpu_usage_seconds_total" in body["content"] + + +def test_mcp_resource_unknown_uri(): + response = client.get("/mcp/resource", params={"uri": "res://does-not-exist"}) + assert response.status_code == 200 + body = response.json() + assert body["error"] == "not found" + assert body["uri"] == "res://does-not-exist" \ No newline at end of file diff --git a/workplans/TELE-WP-0002-mcp-bridge-local-verification.md b/workplans/TELE-WP-0002-mcp-bridge-local-verification.md index a30640e..1735103 100644 --- a/workplans/TELE-WP-0002-mcp-bridge-local-verification.md +++ b/workplans/TELE-WP-0002-mcp-bridge-local-verification.md @@ -4,11 +4,11 @@ type: workplan title: "MCP bridge local verification loop" domain: infotech repo: tele-mcp -status: ready +status: finished owner: codex topic_slug: custodian created: "2026-06-22" -updated: "2026-06-22" +updated: "2026-06-24" state_hub_workstream_id: "c617a044-bf57-4671-9afd-0112e7f462fd" --- @@ -20,9 +20,12 @@ Harden the local dev/test loop for `mcp-telemetry-bridge` independent of full cl ```task id: TELE-WP-0002-T01 -status: todo +status: done priority: high state_hub_task_id: "7bad3ebf-f42c-4069-9c13-28bcf231f6f9" ``` +Result 2026-06-24: Added pytest smoke tests, run/verify scripts, Makefile targets, +and README local-dev + agent quickstart sections. + Add documented local run path, health/schema smoke tests, and agent-oriented quickstart in README.