Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Six FastMCP servers cover IoT data, time-series ML, work orders, vibration diagn
| Server | Tools | Categories | Backing service |
| ----------- | ----- | ------------------------ | -------------------------------------- |
| `iot` | 4 | read | CouchDB |
| `utilities` | 3 | read | none |
| `utilities` | 5 | read | none |
| `fmsr` | 2 | read, LLM-use | LiteLLM + `failure_modes.yaml` |
| `wo` | 14 | read, write | CouchDB |
| `tsfm` | 6 | read, write, cpu-centric | IBM Granite TinyTimeMixer (torch) |
Expand Down
4 changes: 3 additions & 1 deletion docs/mcp-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ Synthetic motor vibration data (`asset_id: Motor_01`, from `motor_01.json`) ship
## utilities — Utilities

**Path:** `src/servers/utilities/main.py`
**Requires:** nothing (no external services)
**Requires:** CouchDB (`COUCHDB_URL`, `COUCHDB_USERNAME`, `COUCHDB_PASSWORD`, `FINAL_RESULT_DBNAME`)

| Tool | Category | Arguments | Description |
| ---------------------- | -------- | ----------- | ------------------------------------------------------ |
| `json_reader` | read | `file_name` | Read and parse a JSON file from disk |
| `current_date_time` | read | — | Return the current UTC date and time as JSON |
| `current_time_english` | read | — | Return the current UTC time as a human-readable string |
| `write_final_result` | write | `result` | Persist the scenario's final answer to the final_result collection for grading. |
| `read_final_result` | read | - | Read back the stored final result (found=false if none written).|

## fmsr — Failure Mode and Sensor Relations

Expand Down
1 change: 1 addition & 0 deletions src/couchdb/.allowed_datafiles
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ src/couchdb/scenarios_data/shared/iot/hydraulic_pump_1.json
src/couchdb/scenarios_data/shared/iot/metro_pump_1.json
src/couchdb/scenarios_data/shared/iot/motor_01.json
src/couchdb/scenarios_data/shared/work_order/workorders.csv
src/couchdb/scenarios_data/shared/final_result.json
5 changes: 5 additions & 0 deletions src/couchdb/collections.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
]
]
},
"final_result": {
"format": "json",
"doctype": "result",
"indexes": []
},
"iot": {
"format": "json",
"primary_key": [
Expand Down
3 changes: 2 additions & 1 deletion src/couchdb/scenarios_data/default/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"shared/iot/hydraulic_pump_1.json"
],
"vibration": "shared/iot/motor_01.json",
"failurecode": "shared/failure_code/failure_code_sample.csv"
"failurecode": "shared/failure_code/failure_code_sample.csv",
"final_result": "shared/final_result.json"
}
8 changes: 8 additions & 0 deletions src/couchdb/scenarios_data/shared/final_result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"_id": "result",
"doctype": "result",
"result": null,
"written_at": null
}
]
122 changes: 121 additions & 1 deletion src/servers/utilities/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
from typing import Any, Dict, List, Union, Optional

import pendulum
import couchdb3
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel

Expand All @@ -21,9 +23,31 @@

mcp = FastMCP(
"utilities",
instructions="General utilities: read JSON files and get current date/time.",
instructions="General utilities: read JSON files, get current date/time, and write the "
"scenario's final result to CouchDB for the grader.",
)

# --- CouchDB (final-result store) ---
# init_data seeds an EMPTY `final_result` collection (one placeholder doc, id "result") at the start
# of each scenario run and rebuilds DBs from scratch, so the run is already isolated — no run id.
COUCHDB_URL = os.environ.get("COUCHDB_URL")
COUCHDB_USERNAME = os.environ.get("COUCHDB_USERNAME")
COUCHDB_PASSWORD = os.environ.get("COUCHDB_PASSWORD")
FINAL_RESULT_DBNAME = os.environ.get("FINAL_RESULT_DBNAME", "final_result")
_RESULT_DOC_ID = "result" # fixed: one result document per scenario run

try:
_result_db = couchdb3.Database(
FINAL_RESULT_DBNAME,
url=COUCHDB_URL,
user=COUCHDB_USERNAME,
password=COUCHDB_PASSWORD,
)
logger.info("Connected to CouchDB: %s", FINAL_RESULT_DBNAME)
except Exception as e: # noqa: BLE001
logger.error("Failed to connect to final_result DB: %s", e)
_result_db = None


class DateTimeResult(BaseModel):
currentDateTime: str
Expand All @@ -35,6 +59,23 @@ class TimeEnglishResult(BaseModel):
iso: str


class ErrorResult(BaseModel):
error: str


class WriteResultResponse(BaseModel):
ok: bool
doc_id: str
message: str


class ReadResultResponse(BaseModel):
found: bool
result: Optional[Union[Dict[str, Any], List[Any]]] = None
written_at: Optional[str] = None
message: str


# --- Helper Functions ---


Expand Down Expand Up @@ -66,6 +107,85 @@ def json_reader(file_name: str) -> str:
return json.dumps({"error": str(e)})


# --- Final Result Tool ---


@mcp.tool(title="Write Final Result")
def write_final_result(
result: Union[Dict[str, Any], List[Any]],
) -> Union[WriteResultResponse, ErrorResult]:
"""Persist this scenario's FINAL answer as a JSON payload to CouchDB so the grader can read it.

Call this exactly once, at the end, with your final answer. `result` is the JSON the task
description asks for — a JSON object, or a list of JSON objects. There is no task id: the run is
scoped to a single scenario (init_data seeds an empty `final_result` collection per run), so it is
one document. Calling again overwrites it.
"""
if _result_db is None:
return ErrorResult(error="CouchDB not connected")
doc = {
"_id": _RESULT_DOC_ID,
"doctype": "result",
"result": result,
"written_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
}
try:
existing = _result_db.get(_RESULT_DOC_ID) # seeded placeholder after init_data
doc["_rev"] = existing["_rev"]
except Exception: # noqa: BLE001
pass # not seeded yet -> create fresh
try:
_result_db.save(doc)
return WriteResultResponse(
ok=True, doc_id=_RESULT_DOC_ID, message="final result written"
)
except Exception as e: # noqa: BLE001
logger.error("write_final_result failed: %s", e)
return ErrorResult(error=str(e))


def _get_final_result_doc():
"""Fetch the raw result document (or None). Shared by the read tool and the grader helper below."""
if _result_db is None:
return None
try:
return _result_db.get(_RESULT_DOC_ID)
except Exception: # noqa: BLE001
return None


def _get_final_result_payload():
"""Grader-side helper (importable by the offline grader): return just the persisted payload, or
None if nothing was written."""
doc = _get_final_result_doc()
return doc.get("result") if doc else None


@mcp.tool(title="Read Final Result")
def read_final_result() -> Union[ReadResultResponse, ErrorResult]:
"""Read back the scenario's final result payload (what write_final_result stored). Returns
found=false if nothing has been written yet (the seeded placeholder has result=null).
"""
if _result_db is None:
return ErrorResult(error="CouchDB not connected")
doc = _get_final_result_doc()
if doc is None:
return ReadResultResponse(
found=False, result=None, message="no result document yet"
)
res = doc.get("result")
return ReadResultResponse(
found=res is not None,
result=res,
written_at=doc.get("written_at"),
message=(
"final result read"
if res is not None
else "placeholder only (no result written yet)"
),
)


# --- Time Tools ---


Expand Down