From e45fd867ae3a43f45ee2c911d9c0f7a91bfe47c9 Mon Sep 17 00:00:00 2001 From: Dhaval Patel Date: Fri, 19 Jun 2026 18:52:10 -0400 Subject: [PATCH 1/3] adding a write result tool Signed-off-by: Dhaval Patel --- src/couchdb/.allowed_datafiles | 1 + src/couchdb/collections.json | 5 ++ .../scenarios_data/shared/final_result.json | 8 ++ src/servers/utilities/main.py | 78 ++++++++++++++++++- 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 src/couchdb/scenarios_data/shared/final_result.json diff --git a/src/couchdb/.allowed_datafiles b/src/couchdb/.allowed_datafiles index e4f4804d..ff0d0d68 100644 --- a/src/couchdb/.allowed_datafiles +++ b/src/couchdb/.allowed_datafiles @@ -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 \ No newline at end of file diff --git a/src/couchdb/collections.json b/src/couchdb/collections.json index bac2576b..1ecbcf22 100644 --- a/src/couchdb/collections.json +++ b/src/couchdb/collections.json @@ -24,6 +24,11 @@ ] ] }, + "final_result": { + "format": "json", + "doctype": "result", + "indexes": [] + }, "iot": { "format": "json", "primary_key": [ diff --git a/src/couchdb/scenarios_data/shared/final_result.json b/src/couchdb/scenarios_data/shared/final_result.json new file mode 100644 index 00000000..9d08e1f0 --- /dev/null +++ b/src/couchdb/scenarios_data/shared/final_result.json @@ -0,0 +1,8 @@ +[ + { + "_id": "result", + "doctype": "result", + "result": null, + "written_at": null + } +] diff --git a/src/servers/utilities/main.py b/src/servers/utilities/main.py index 42858783..b996ad4d 100644 --- a/src/servers/utilities/main.py +++ b/src/servers/utilities/main.py @@ -4,11 +4,13 @@ from datetime import datetime, timezone from pathlib import Path from uuid import uuid4 - +from typing import Any, Dict, List, Union + import pendulum +import couchdb3 from mcp.server.fastmcp import FastMCP from pydantic import BaseModel - + import os # Setup logging — default WARNING so stderr stays quiet when used as MCP server; @@ -21,9 +23,28 @@ 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 @@ -34,6 +55,14 @@ class TimeEnglishResult(BaseModel): english: str iso: str +class ErrorResult(BaseModel): + error: str + + +class WriteResultResponse(BaseModel): + ok: bool + doc_id: str + message: str # --- Helper Functions --- @@ -65,6 +94,49 @@ def json_reader(file_name: str) -> str: logger.error(f"Error reading JSON file {file_name}: {e}") 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 read_final_result(): + """Grader-side helper (not an MCP tool): return the persisted final payload, or None if the agent + never wrote one (the seeded placeholder has result=null).""" + if _result_db is None: + return None + try: + return _result_db.get(_RESULT_DOC_ID).get("result") + except Exception: # noqa: BLE001 + return None # --- Time Tools --- From b2c74706610c1076540f70522889daa6b0f9299c Mon Sep 17 00:00:00 2001 From: Dhaval Patel Date: Fri, 19 Jun 2026 19:03:54 -0400 Subject: [PATCH 2/3] revised code Signed-off-by: Dhaval Patel --- src/servers/utilities/main.py | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/src/servers/utilities/main.py b/src/servers/utilities/main.py index b996ad4d..dcb56e8b 100644 --- a/src/servers/utilities/main.py +++ b/src/servers/utilities/main.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from pathlib import Path from uuid import uuid4 -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Union, Optional import pendulum import couchdb3 @@ -64,6 +64,13 @@ class WriteResultResponse(BaseModel): 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 --- @@ -128,15 +135,41 @@ def write_final_result( return ErrorResult(error=str(e)) -def read_final_result(): - """Grader-side helper (not an MCP tool): return the persisted final payload, or None if the agent - never wrote one (the seeded placeholder has result=null).""" + +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).get("result") + 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 --- From 32e1c60f4fb88043c2f38dfbbfb9f368c4d38031 Mon Sep 17 00:00:00 2001 From: Dhaval Patel Date: Fri, 19 Jun 2026 19:55:23 -0400 Subject: [PATCH 3/3] revised code Signed-off-by: Dhaval Patel --- INSTRUCTIONS.md | 2 +- docs/mcp-servers.md | 4 +- .../scenarios_data/default/manifest.json | 3 +- src/servers/utilities/main.py | 63 ++++++++++++------- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index 9955691f..9e5d1e91 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -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) | diff --git a/docs/mcp-servers.md b/docs/mcp-servers.md index 726cb915..048ec299 100644 --- a/docs/mcp-servers.md +++ b/docs/mcp-servers.md @@ -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 diff --git a/src/couchdb/scenarios_data/default/manifest.json b/src/couchdb/scenarios_data/default/manifest.json index 06760289..2ec1984e 100644 --- a/src/couchdb/scenarios_data/default/manifest.json +++ b/src/couchdb/scenarios_data/default/manifest.json @@ -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" } \ No newline at end of file diff --git a/src/servers/utilities/main.py b/src/servers/utilities/main.py index dcb56e8b..b2bc6263 100644 --- a/src/servers/utilities/main.py +++ b/src/servers/utilities/main.py @@ -5,12 +5,12 @@ 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 - + import os # Setup logging — default WARNING so stderr stays quiet when used as MCP server; @@ -24,7 +24,7 @@ mcp = FastMCP( "utilities", instructions="General utilities: read JSON files, get current date/time, and write the " - "scenario's final result to CouchDB for the grader.", + "scenario's final result to CouchDB for the grader.", ) # --- CouchDB (final-result store) --- @@ -34,17 +34,20 @@ 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 - +_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 + 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 @@ -55,10 +58,11 @@ class TimeEnglishResult(BaseModel): english: str iso: str + class ErrorResult(BaseModel): error: str - - + + class WriteResultResponse(BaseModel): ok: bool doc_id: str @@ -71,6 +75,7 @@ class ReadResultResponse(BaseModel): written_at: Optional[str] = None message: str + # --- Helper Functions --- @@ -101,14 +106,16 @@ def json_reader(file_name: str) -> str: logger.error(f"Error reading JSON file {file_name}: {e}") 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 @@ -123,19 +130,20 @@ def write_final_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 + 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 + pass # not seeded yet -> create fresh try: _result_db.save(doc) - return WriteResultResponse(ok=True, doc_id=_RESULT_DOC_ID, message="final result written") + 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: @@ -144,32 +152,39 @@ def _get_final_result_doc(): 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).""" + 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") + 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)", + message=( + "final result read" + if res is not None + else "placeholder only (no result written yet)" + ), ) - + # --- Time Tools ---