From 476f77e1a8fc52cdfbf38565b97c63bbd9e5ed26 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sat, 28 Feb 2026 10:53:51 +0530 Subject: [PATCH] Add vCon-MCP REST storage module for vCon server - Introduced a new storage module that integrates with the vcon-mcp project via its REST API. - Implemented methods for saving, retrieving, and deleting vCons using the vcon-mcp endpoints. - Added configuration options for base URL, API key, and request timeout. - Included comprehensive documentation in README.md detailing usage, features, and error handling. --- server/storage/vcon_mcp/README.md | 66 +++++++++++ server/storage/vcon_mcp/__init__.py | 169 ++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 server/storage/vcon_mcp/README.md create mode 100644 server/storage/vcon_mcp/__init__.py diff --git a/server/storage/vcon_mcp/README.md b/server/storage/vcon_mcp/README.md new file mode 100644 index 0000000..7124157 --- /dev/null +++ b/server/storage/vcon_mcp/README.md @@ -0,0 +1,66 @@ +# vCon-MCP REST Storage Module for vCon Server + +This module implements the vcon-server storage interface by delegating to the **vcon-mcp** project via its REST API. For a given `vcon_id`, it saves, gets, and deletes vCons using vcon-mcp’s HTTP endpoints. + +## Features + +- **save(vcon_id, opts)** – Loads the vCon from Redis and POSTs it to vcon-mcp `POST /vcons`. +- **get(vcon_id, opts)** – Returns the vCon by calling vcon-mcp `GET /vcons/:uuid`; returns `None` if not found. +- **delete(vcon_id, opts)** – Removes the vCon via vcon-mcp `DELETE /vcons/:uuid`; returns `True` if deleted, `False` if not found. + +## Configuration + +Options (e.g. in `config.yml` under `storages..options`): + +| Option | Description | Default | +| --------- | ------------------------------------------------ | ------------------------------ | +| base_url | vcon-mcp REST API base (e.g. `/api/v1` included) | `http://127.0.0.1:3000/api/v1` | +| api_key | Optional. Sent as `Authorization: Bearer ` | `""` | +| timeout | Request timeout in seconds | `30` | + +Example in `config.yml`: + +```yaml +storages: + vcon_mcp: + module: storage.vcon_mcp + options: + base_url: http://127.0.0.1:3000/api/v1 + api_key: "" # set if vcon-mcp API_AUTH_REQUIRED is true + timeout: 30 +``` + +Add the storage name (e.g. `vcon_mcp`) to a chain’s `storages` list so vCons are written to vcon-mcp after processing. + +## vcon-mcp REST API + +The module uses these endpoints (relative to `base_url`): + +- **POST /vcons** – Create/ingest a single vCon (body: vCon JSON). +- **GET /vcons/:uuid** – Get a vCon by UUID (response: `{ "success": true, "vcon": {...} }`). +- **DELETE /vcons/:uuid** – Delete a vCon by UUID. + +Ensure vcon-mcp is running with HTTP transport (`MCP_TRANSPORT=http`) and that `base_url` matches its `REST_API_BASE_PATH` (default `/api/v1`). + +## Dependencies + +- `requests` – HTTP client for calling vcon-mcp. + +## Error Handling + +- **save**: Raises if the vCon is missing in Redis or if the vcon-mcp request fails (e.g. 4xx/5xx). +- **get**: Returns `None` on 404 or other request errors (errors are logged). +- **delete**: Returns `False` on 404; raises on other request errors. + +## Usage + +Used like other storages via the storage base class: + +```python +from storage.base import Storage + +storage = Storage("vcon_mcp") +storage.save(vcon_id) +vcon_dict = storage.get(vcon_id) +deleted = storage.delete(vcon_id) +``` diff --git a/server/storage/vcon_mcp/__init__.py b/server/storage/vcon_mcp/__init__.py new file mode 100644 index 0000000..d08c81b --- /dev/null +++ b/server/storage/vcon_mcp/__init__.py @@ -0,0 +1,169 @@ +""" +vCon-MCP REST storage module for vcon-server + +This module provides integration with the vcon-mcp project via its REST API. +It implements the storage interface (save, get, delete) by calling vcon-mcp +endpoints so that vCons are stored and retrieved through the MCP service. + +Endpoints used: +- POST {base_url}/vcons - Create/ingest a vCon +- GET {base_url}/vcons/:uuid - Get a vCon by UUID +- DELETE {base_url}/vcons/:uuid - Delete a vCon by UUID + +Configuration options: +- base_url: Base URL of vcon-mcp REST API (e.g. http://localhost:3000/api/v1) +- api_key: Optional. API key for Authorization: Bearer +- timeout: Optional. Request timeout in seconds (default: 30) +""" + +from typing import Optional, Dict, Any +import requests +from lib.logging_utils import init_logger +from server.lib.vcon_redis import VconRedis + +logger = init_logger(__name__) + +default_options: Dict[str, Any] = { + "base_url": "http://127.0.0.1:3000/api/v1", + "api_key": "", + "timeout": 30, +} + + +def _headers(opts: Dict[str, Any]) -> Dict[str, str]: + """Build request headers, including optional Bearer token.""" + h = {"Content-Type": "application/json", "Accept": "application/json"} + api_key = opts.get("api_key") or "" + if api_key: + h["Authorization"] = f"Bearer {api_key}" + return h + + +def _url(opts: Dict[str, Any], path: str) -> str: + """Build full URL for the given path (no leading slash).""" + base = (opts.get("base_url") or default_options["base_url"]).rstrip("/") + return f"{base}/{path.lstrip('/')}" + + +def save(vcon_uuid: str, opts: Dict[str, Any] = None) -> None: + """ + Save a vCon to vcon-mcp via REST API. + + Fetches the vCon from Redis, then POSTs it to vcon-mcp /vcons endpoint. + + Args: + vcon_uuid: UUID of the vCon to save + opts: Options (base_url, api_key, timeout). Defaults to default_options. + + Exception: If vCon cannot be read from Redis or vcon-mcp request fails. + """ + opts = opts or default_options + logger.info("Starting vcon-mcp storage save for vCon: %s", vcon_uuid) + try: + vcon_redis = VconRedis() + vcon = vcon_redis.get_vcon(vcon_uuid) + if not vcon: + raise ValueError(f"vCon {vcon_uuid} not found in Redis") + payload = vcon.to_dict() + url = _url(opts, "vcons") + timeout = opts.get("timeout", default_options["timeout"]) + resp = requests.post( + url, + json=payload, + headers=_headers(opts), + timeout=timeout, + ) + resp.raise_for_status() + logger.info("Finished vcon-mcp storage save for vCon: %s", vcon_uuid) + except requests.RequestException as e: + logger.error( + "vcon-mcp storage: failed to save vCon: %s, error: %s", + vcon_uuid, + e, + ) + raise + except Exception as e: + logger.error( + "vcon-mcp storage: failed to save vCon: %s, error: %s", + vcon_uuid, + e, + ) + raise + + +def get(vcon_uuid: str, opts: Dict[str, Any] = None) -> Optional[dict]: + """ + Get a vCon from vcon-mcp by UUID via REST API. + + Args: + vcon_uuid: UUID of the vCon to retrieve + opts: Options (base_url, api_key, timeout). Defaults to default_options. + + Returns: + The vCon as a dict if found, None if not found or on error. + """ + opts = opts or default_options + logger.info("Starting vcon-mcp storage get for vCon: %s", vcon_uuid) + try: + url = _url(opts, f"vcons/{vcon_uuid}") + timeout = opts.get("timeout", default_options["timeout"]) + resp = requests.get( + url, + headers=_headers(opts), + timeout=timeout, + ) + if resp.status_code == 404: + logger.info("vCon %s not found in vcon-mcp storage", vcon_uuid) + return None + resp.raise_for_status() + data = resp.json() + vcon = data.get("vcon") if isinstance(data, dict) else None + if vcon is not None: + logger.info("Finished vcon-mcp storage get for vCon: %s", vcon_uuid) + return vcon + except requests.RequestException as e: + logger.error( + "vcon-mcp storage: failed to get vCon: %s, error: %s", vcon_uuid, e + ) + return None + + +def delete(vcon_uuid: str, opts: Dict[str, Any] = None) -> bool: + """ + Delete a vCon from vcon-mcp by UUID via REST API. + + Args: + vcon_uuid: UUID of the vCon to delete + opts: Options (base_url, api_key, timeout). Defaults to default_options. + + Returns: + True if the vCon was deleted, False if it was not found. + + Raises: + Exception: On request errors other than 404. + """ + opts = opts or default_options + logger.info("Starting vcon-mcp storage delete for vCon: %s", vcon_uuid) + try: + url = _url(opts, f"vcons/{vcon_uuid}") + timeout = opts.get("timeout", default_options["timeout"]) + resp = requests.delete( + url, + headers=_headers(opts), + timeout=timeout, + ) + if resp.status_code == 404: + logger.info("vCon %s not found in vcon-mcp storage", vcon_uuid) + return False + resp.raise_for_status() + logger.info( + "Successfully deleted vCon %s from vcon-mcp storage", vcon_uuid + ) + return True + except requests.RequestException as e: + logger.error( + "vcon-mcp storage: failed to delete vCon: %s, error: %s", + vcon_uuid, + e, + ) + raise