From 34e25aaca29a4ae1abe1f35b28548c54ddd4c643 Mon Sep 17 00:00:00 2001 From: iksnerd Date: Sun, 24 May 2026 05:16:20 +0300 Subject: [PATCH] fix(iot): guard CouchDB construction and add first-run DX scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iot MCP server crashed on startup when COUCHDB_URL was unset: `couchdb3.Database(None, url=None, ...)` raised inside __init__, and the partially-constructed object's __del__ then double-faulted with `AttributeError: 'Database' object has no attribute 'session'`. Guard the construction so the server stays startable for tool discovery even when CouchDB env vars are missing — tool calls that need the DB return a clean error instead. Also bundle three small first-run DX improvements that surfaced while investigating: - Add `VIBRATION_DBNAME=vibration` to `.env.public` (already documented in `docs/mcp-servers.md` but missing from the example env file). - Ship `.mcp.json.example` at repo root so Claude Code / MCP Inspector users have a copy-paste config for the six stdio servers. Gitignore `.mcp.json` so user-local copies stay out of git. - Document an "Inspecting a server directly" recipe in `docs/mcp-servers.md` (Inspector UI, `.mcp.json` template, raw stdio JSON-RPC) and note that `wo` returns `*_not_available` until the CouchDB container's seeding step has run. Signed-off-by: iksnerd --- .env.public | 1 + .gitignore | 3 +++ .mcp.json.example | 35 +++++++++++++++++++++++++++++++++++ docs/mcp-servers.md | 24 +++++++++++++++++++++++- src/servers/iot/main.py | 39 ++++++++++++++++++++++++++------------- 5 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 .mcp.json.example diff --git a/.env.public b/.env.public index 2bc835cde..30c1d8d6e 100644 --- a/.env.public +++ b/.env.public @@ -4,6 +4,7 @@ COUCHDB_USERNAME=admin COUCHDB_PASSWORD=password IOT_DBNAME=iot WO_DBNAME=workorder +VIBRATION_DBNAME=vibration # ── IBM WatsonX (plan-execute runner) ──────────────────────────────────────── WATSONX_APIKEY= diff --git a/.gitignore b/.gitignore index a775beb4f..1a2711802 100644 --- a/.gitignore +++ b/.gitignore @@ -199,6 +199,9 @@ benchmark/cods_track2/.env.local CLAUDE.md mcp/couchdb/sample_data/bulk_docs.json .env + +# Local MCP client config — .mcp.json.example is the committed template. +.mcp.json mcp/servers/tsfm/artifacts/tsfm_models/ src/tmp/ diff --git a/.mcp.json.example b/.mcp.json.example new file mode 100644 index 000000000..7451350eb --- /dev/null +++ b/.mcp.json.example @@ -0,0 +1,35 @@ +{ + "_comment": "Copy to .mcp.json (gitignored) so any MCP client (e.g. MCP Inspector) can launch the six servers as stdio subprocesses. Expects `uv sync` to have installed the console scripts under .venv/bin/. Credentials are loaded from .env by python-dotenv when the servers start — keep secrets out of this file.", + "mcpServers": { + "utilities": { + "type": "stdio", + "command": ".venv/bin/utilities-mcp-server", + "args": [] + }, + "fmsr": { + "type": "stdio", + "command": ".venv/bin/fmsr-mcp-server", + "args": [] + }, + "iot": { + "type": "stdio", + "command": ".venv/bin/iot-mcp-server", + "args": [] + }, + "wo": { + "type": "stdio", + "command": ".venv/bin/wo-mcp-server", + "args": [] + }, + "vibration": { + "type": "stdio", + "command": ".venv/bin/vibration-mcp-server", + "args": [] + }, + "tsfm": { + "type": "stdio", + "command": ".venv/bin/tsfm-mcp-server", + "args": [] + } + } +} diff --git a/docs/mcp-servers.md b/docs/mcp-servers.md index f13cd3077..62b9d4ae9 100644 --- a/docs/mcp-servers.md +++ b/docs/mcp-servers.md @@ -59,7 +59,7 @@ Synthetic motor vibration data (`asset_id: Motor_01`, from `motor_01.json`) ship **Path:** `src/servers/wo/main.py` **Requires:** CouchDB (`COUCHDB_URL`, `COUCHDB_USERNAME`, `COUCHDB_PASSWORD`, `WO_DBNAME`) -**Data init:** Handled automatically by `docker compose -f src/couchdb/docker-compose.yaml up` (runs `src/couchdb/init_wo.py` inside the CouchDB container on every start — database is dropped and reloaded each time) +**Data init:** Handled automatically by `docker compose -f src/couchdb/docker-compose.yaml up` (runs `src/couchdb/init_wo.py` inside the CouchDB container on every start — database is dropped and reloaded each time). If a tool returns `{"error": "..._not_available"}`, the seeding step didn't run — restart the CouchDB container. | Tool | Arguments | Description | | ----------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- | @@ -118,3 +118,25 @@ uv run vibration-mcp-server ``` They speak MCP over stdio, so they're idle until a client connects on stdin. + +## Inspecting a server directly + +To list the tools / resources / prompts a server exposes (and try a tool call) without writing client code: + +**Option 1 — the MCP Inspector UI:** + +```bash +npx @modelcontextprotocol/inspector uv run iot-mcp-server +``` + +**Option 2 — Claude Code or another MCP client:** copy `.mcp.json.example` at the repo root to `.mcp.json` and the client will launch all six servers as stdio subprocesses. The example file points to the `-mcp-server` console scripts installed by `uv sync`. + +**Option 3 — raw stdio JSON-RPC** (useful in scripts/CI): + +```bash +printf '%s\n' \ + '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \ + '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ + '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \ + | uv run iot-mcp-server +``` diff --git a/src/servers/iot/main.py b/src/servers/iot/main.py index 9e4732087..b8169c710 100644 --- a/src/servers/iot/main.py +++ b/src/servers/iot/main.py @@ -1,7 +1,6 @@ import os import logging from datetime import datetime -from functools import lru_cache from typing import Any, Dict, List, Optional, Union from mcp.server.fastmcp import FastMCP from pydantic import BaseModel @@ -24,20 +23,34 @@ COUCHDB_USERNAME = os.environ.get("COUCHDB_USERNAME") COUCHDB_PASSWORD = os.environ.get("COUCHDB_PASSWORD") -# Initialize CouchDB -try: - db = couchdb3.Database( - COUCHDB_DBNAME, - url=COUCHDB_URL, - user=COUCHDB_USERNAME, - password=COUCHDB_PASSWORD, +# Initialize CouchDB. Guard before construction: couchdb3.Database raises inside +# __init__ when COUCHDB_URL is None, leaving a partially-constructed instance +# whose __del__ then raises AttributeError on `self.session.close()`. Skipping +# construction keeps the MCP server startable for tool discovery even when +# CouchDB env vars are unset. +db = None +if COUCHDB_URL and COUCHDB_DBNAME: + try: + db = couchdb3.Database( + COUCHDB_DBNAME, + url=COUCHDB_URL, + user=COUCHDB_USERNAME, + password=COUCHDB_PASSWORD, + ) + logger.info(f"Connected to CouchDB: {COUCHDB_DBNAME}") + except Exception as e: + logger.error(f"Failed to connect to CouchDB: {e}") + db = None +else: + logger.warning( + "CouchDB env vars not set (COUCHDB_URL, IOT_DBNAME); " + "tool calls that need CouchDB will return an error." ) - logger.info(f"Connected to CouchDB: {COUCHDB_DBNAME}") -except Exception as e: - logger.error(f"Failed to connect to CouchDB: {e}") - db = None -mcp = FastMCP("iot", instructions="IoT sensor data: browse sites, assets, sensors, and query historical readings from CouchDB.") +mcp = FastMCP( + "iot", + instructions="IoT sensor data: browse sites, assets, sensors, and query historical readings from CouchDB.", +) # Static site as per original requirement SITES = ["MAIN"]