Skill: jithinm/mongo-db (version 1.0.0)

--- name: mongo-db description: Interact with a MongoDB database for persistent document storage. Supports full CRUD operations (find, insert, update, delete), aggregation pipelines, collection management, and index creation. Use when any agent needs to store or retrieve data in MongoDB — for example, persisting financial records, budgets, watchlists, or any structured data across sessions. metadata: {"openclaw": {"requires": {"bins": ["python3"]}, "emoji": "🍃"}} --- # MongoDB — Document Storage Skill Use this skill whenever an agent needs to read or write persistent data to MongoDB. All operations are performed via a Python CLI script and return JSON output. ## When to use - User asks to "save to the database", "store this in Mongo", "retrieve from MongoDB" - An agent needs to persist data across sessions (budgets, transactions, summaries, watchlists) - An agent needs to query, filter, or aggregate stored records - A new collection or schema (validator) needs to be set up --- ## Setup (first run only) **Local MongoDB:** If you need to install MongoDB on Ubuntu, see [INSTALL-UBUNTU.md](INSTALL-UBUNTU.md). For Atlas, use your connection string in config or env. Run the setup script once from the workspace root to create a virtual environment and install `pymongo`: ```bash bash skills/mongo-db/scripts/setup.sh ``` This creates `skills/mongo-db/scripts/.venv/` and installs dependencies there. After setup, invoke the client using the venv interpreter: ```bash skills/mongo-db/scripts/.venv/bin/python3 skills/mongo-db/scripts/mongo_client.py '<json>' ``` --- ## Configuration Connection is resolved in this order (first match wins): ### Option 1 — Environment variable (recommended) ``` MONGO_URI=mongodb+srv://user:[REDACTED_SECRET]/mydb MONGO_DB=mydb # optional if the db is in the URI ``` ### Option 2 — config.json (local file, gitignored) Copy the example and fill in your values: ```bash cp skills/mongo-db/config.example.json skills/mongo-db/config.json ``` Edit `skills/mongo-db/config.json`: ```json { "uri": "mongodb://localhost:27017", "database": "mydb", "username": "optional", "password": "optional" } ``` ### Option 3 — Individual env vars ``` MONGO_HOST=localhost MONGO_PORT=27017 MONGO_USER=myuser MONGO_PASSWORD=mypassword MONGO_DB=mydb ``` --- ## CLI interface All operations use a single JSON payload argument: ```bash PYTHON=skills/mongo-db/scripts/.venv/bin/python3 $PYTHON skills/mongo-db/scripts/mongo_client.py '<json_payload>' ``` Payload schema: ```json { "operation": "<op>", "database": "<optional override>", "collection": "<collection name>", ...operation-specific fields } ``` Output is always JSON: `{"success": true, ...result_fields}` or `{"success": false, "error": "..."}`. --- ## Operations Reference ### List databases ```json {"operation": "list_databases"} ``` ### List collections in a database ```json {"operation": "list_collections", "database": "mydb"} ``` ### Create a collection (with optional JSON Schema validator) ```json { "operation": "create_collection", "database": "mydb", "collection": "budgets", "validator": { "$jsonSchema": { "bsonType": "object", "required": ["period", "income"], "properties": { "period": {"bsonType": "string"}, "income": {"bsonType": "number"} } } } } ``` ### Drop a collection Always confirm with the user before dropping. Requires `"confirm": true`. ```json {"operation": "drop_collection", "database": "mydb", "collection": "old_data", "confirm": true} ``` ### Create an index ```json { "operation": "create_index", "database": "mydb", "collection": "transactions", "keys": {"date": 1, "category": 1}, "unique": false } ``` --- ### Find documents ```json { "operation": "find", "database": "mydb", "collection": "transactions", "filter": {"category": "groceries"}, "projection": {"_id": 0, "date": 1, "amount": 1, "description": 1}, "sort": {"date": -1}, "limit": 20 } ``` ### Find one document ```json { "operation": "find_one", "database": "mydb", "collection": "transactions", "filter": {"id": "txn_20260131_0001"} } ``` ### Count documents ```json { "operation": "count", "database": "mydb", "collection": "transactions", "filter": {"type": "expense"} } ``` --- ### Insert one document ```json { "operation": "insert_one", "database": "mydb", "collection": "transactions", "document": { "id": "txn_20260201_0001", "date": "2026-02-01", "description": "Supermarket", "amount": -85.50, "category": "groceries", "type": "expense" } } ``` ### Insert many documents ```json { "operation": "insert_many", "database": "mydb", "collection": "transactions", "documents": [ {"date": "2026-02-01", "description": "Coffee", "amount": -4.50, "category": "dining"}, {"date": "2026-02-02", "description": "Salary", "amount": 5000, "category": "income"} ] } ``` --- ### Update one document ```json { "operation": "update_one", "database": "mydb", "collection": "budget", "filter": {"period": "monthly"}, "update": {"$set": {"income": 5500, "last_updated": "2026-02-01"}}, "upsert": false } ``` ### Update many documents ```json { "operation": "update_many", "database": "mydb", "collection": "transactions", "filter": {"category": "food"}, "update": {"$set": {"category": "groceries"}} } ``` ### Replace one document ```json { "operation": "replace_one", "database": "mydb", "collection": "budget", "filter": {"period": "monthly"}, "replacement": {"period": "monthly", "income": 5500, "currency": "USD"}, "upsert": true } ``` --- ### Delete one document Always confirm with the user before deleting. Requires `"confirm": true`. ```json { "operation": "delete_one", "database": "mydb", "collection": "transactions", "filter": {"id": "txn_20260131_0001"}, "confirm": true } ``` ### Delete many documents Always confirm with the user before deleting. Requires `"confirm": true`. ```json { "operation": "delete_many", "database": "mydb", "collection": "transactions", "filter": {"source_file": "january_statement.pdf"}, "confirm": true } ``` --- ### Aggregate ```json { "operation": "aggregate", "database": "mydb", "collection": "transactions", "pipeline": [ {"$match": {"type": "expense"}}, {"$group": {"_id": "$category", "total": {"$sum": "$amount"}, "count": {"$sum": 1}}}, {"$sort": {"total": 1}} ] } ``` --- ## Notes - **Destructive operations** (`delete_one`, `delete_many`, `drop_collection`) require `"confirm": true` in the payload. Always ask the user to confirm before including this flag. - **ObjectId** fields are serialized as strings in all output. - **Database override**: the `"database"` field in the payload overrides the default database from config/env for that single call. - **Schema validation**: use `create_collection` with a `"validator"` to enforce document structure at the MongoDB level. This is the recommended approach for agents that write structured data (e.g. the finance-budget agent's budget and transaction collections). - **Upsert pattern**: for config-like documents (one per type), use `replace_one` with `"upsert": true` to create-or-replace atomically. - If `pymongo` is missing, re-run `bash skills/mongo-db/scripts/setup.sh`.

path: INSTALL-UBUNTU.md
content: # Install MongoDB on Ubuntu

Steps to install **MongoDB Community Server** on Ubuntu so the mongo-db skill can connect to a local instance. If you use MongoDB Atlas, you don't need to install the server — just set `MONGO_URI` to your Atlas connection string.

---

## 1. Import the official MongoDB GPG key and add the repo

```bash
# Install prerequisites
sudo apt-get install -y gnupg curl

# Import MongoDB public GPG key
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
   sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor

# Add MongoDB repo for your Ubuntu version (e.g. 24.04)
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | \
   sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
```

Replace `noble` with your Ubuntu codename if different:

- **24.04** → `noble`
- **22.04** → `jammy`
- **20.04** → `focal`

---

## 2. Install MongoDB

```bash
sudo apt-get update
sudo apt-get install -y mongodb-org
```

---

## 3. Start and enable the service

```bash
sudo systemctl start mongod
sudo systemctl enable mongod
```

---

## 4. Verify it's running

```bash
sudo systemctl status mongod
```

Or connect with the shell:

```bash
mongosh
```

---

## Service reference

| Action   | Command                          |
|----------|----------------------------------|
| Start    | `sudo systemctl start mongod`    |
| Stop     | `sudo systemctl stop mongod`     |
| Restart  | `sudo systemctl restart mongod`  |
| Status   | `sudo systemctl status mongod`   |
| Shell    | `mongosh`                        |

---

## Using with this skill

After MongoDB is running, configure the skill with a local URI:

- **Environment:** `MONGO_URI=mongodb://localhost:27017` (and optionally `MONGO_DB=mydb`)
- **config.json:** `"uri": "mongodb://localhost:27017"` (see `config.example.json`)

Then run the skill setup and use the client as described in `SKILL.md`.


path: config.example.json
content: {
  "_comment": "Copy this file to config.json and fill in your values. config.json is gitignored.",
  "uri": "mongodb://localhost:27017",
  "database": "mydb",
  "username": "",
  "password": "",
  "_alternatives": {
    "atlas": "mongodb+srv://<user>:<password>@<cluster>.mongodb.net/<database>?retryWrites=true&w=majority",
    "with_auth": "mongodb://<user>:<password>@<host>:<port>/<database>",
    "env_var": "Set MONGO_URI in your environment instead of using this file"
  }
}


path: scripts/setup.sh
content: #!/usr/bin/env bash
# Sets up a Python virtual environment and installs required dependencies
# for the mongo-db skill. Run once from the workspace root:
#   bash skills/mongo-db/scripts/setup.sh

set -euo pipefail

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="$SKILL_DIR/.venv"

echo "==> mongo-db skill setup"

if ! command -v python3 &>/dev/null; then
  echo "ERROR: python3 is not installed or not on PATH" >&2
  exit 1
fi

if [ ! -d "$VENV_DIR" ]; then
  echo "==> Creating virtual environment at $VENV_DIR"
  python3 -m venv "$VENV_DIR"
else
  echo "==> Virtual environment already exists, updating packages"
fi

echo "==> Installing dependencies"
"$VENV_DIR/bin/pip" install --quiet --upgrade pip
"$VENV_DIR/bin/pip" install --quiet pymongo

echo ""
echo "Setup complete. Dependencies installed:"
"$VENV_DIR/bin/pip" show pymongo | grep -E "^(Name|Version):"
echo ""
echo "Python interpreter: $VENV_DIR/bin/python3"
echo "Run the client with:"
echo "  $VENV_DIR/bin/python3 skills/mongo-db/scripts/mongo_client.py '{\"operation\":\"list_databases\"}'"


path: scripts/mongo_client.py
content: #!/usr/bin/env python3
"""
MongoDB CLI client for the mongo-db OpenClaw skill.

Usage:
    python3 mongo_client.py '<json_payload>'

The JSON payload must include "operation" and (for most ops) "collection".
All output is JSON written to stdout. Errors are JSON on stderr with exit code 1.

Connection resolution order:
  1. MONGO_URI env var  (full connection string)
  2. skills/mongo-db/config.json  (relative to workspace root or SKILL_DIR)
  3. Individual env vars: MONGO_HOST, MONGO_PORT, MONGO_USER, MONGO_PASSWORD, MONGO_DB
"""

import json
import os
import sys
from pathlib import Path
from typing import Any


def _find_config_path() -> Path | None:
    """Search for config.json starting from the script directory up to workspace root."""
    candidates = [
        Path(__file__).parent.parent / "config.json",
        Path.cwd() / "skills" / "mongo-db" / "config.json",
    ]
    for p in candidates:
        if p.exists():
            return p
    return None


def _build_uri_from_parts() -> str:
    host = os.environ.get("MONGO_HOST", "localhost")
    port = os.environ.get("MONGO_PORT", "27017")
    user = os.environ.get("MONGO_USER", "")
    [REDACTED_SECRET]"MONGO_PASSWORD", "")
    if user and [REDACTED_SECRET] f"mongodb://{user}:{password}@{host}:{port}"
    return f"mongodb://{host}:{port}"


def _resolve_connection() -> tuple[str, str]:
    """Returns (uri, default_database)."""
    uri = os.environ.get("MONGO_URI", "")
    database = os.environ.get("MONGO_DB", "")

    if uri:
        return uri, database

    config_path = _find_config_path()
    if config_path:
        with open(config_path) as f:
            cfg = json.load(f)
        uri = cfg.get("uri", "")
        if not uri:
            user = cfg.get("username", "") or cfg.get("user", "")
            [REDACTED_SECRET]"password", "")
            host = cfg.get("host", "localhost")
            port = cfg.get("port", 27017)
            if user and password:
                uri = f"mongodb://{user}:{password}@{host}:{port}"
            else:
                uri = f"mongodb://{host}:{port}"
        database = database or cfg.get("database", "")
        return uri, database

    return _build_uri_from_parts(), database


def _error(msg: str, **extra: Any) -> None:
    print(json.dumps({"success": False, "error": msg, **extra}), file=sys.stderr)
    sys.exit(1)


def _success(data: Any) -> None:
    print(json.dumps(data, default=str))


def _get_collection(db, name: str):
    if not name:
        _error("'collection' field is required for this operation")
    return db[name]


def _serialize_doc(doc: Any) -> Any:
    """Recursively convert non-JSON-serializable types (ObjectId, datetime) to strings."""
    if doc is None:
        return None
    if isinstance(doc, dict):
        return {k: _serialize_doc(v) for k, v in doc.items()}
    if isinstance(doc, list):
        return [_serialize_doc(i) for i in doc]
    try:
        json.dumps(doc)
        return doc
    except (TypeError, ValueError):
        return str(doc)


def run(payload: dict) -> None:
    try:
        from pymongo import MongoClient, ASCENDING, DESCENDING
        from pymongo.errors import PyMongoError
    except ImportError:
        _error(
            "pymongo is not installed. Run: bash skills/mongo-db/scripts/setup.sh",
            hint="or: pip install pymongo",
        )

    op = payload.get("operation", "").strip()
    if not op:
        _error("'operation' field is required")

    uri, default_db = _resolve_connection()
    if not uri:
        _error(
            "No MongoDB connection configured. Set MONGO_URI or create skills/mongo-db/config.json"
        )

    db_name = payload.get("database") or default_db
    collection_name = payload.get("collection", "")

    try:
        client = MongoClient(uri, serverSelectionTimeoutMS=5000)
        db = client[db_name] if db_name else None

        # ── Database-level operations ─────────────────────────────────────────

        if op == "list_databases":
            dbs = [d["name"] for d in client.list_databases()]
            return _success({"success": True, "databases": dbs})

        if op == "list_collections":
            if db is None:
                _error("'database' is required for list_collections")
            cols = db.list_collection_names()
            return _success({"success": True, "database": db_name, "collections": cols})

        if op == "create_collection":
            if db is None:
                _error("'database' is required for create_collection")
            if not collection_name:
                _error("'collection' is required for create_collection")
            kwargs: dict[str, Any] = {}
            validator = payload.get("validator")
            if validator:
                kwargs["validator"] = validator
            capped = payload.get("capped")
            if capped:
                kwargs["capped"] = True
                kwargs["size"] = payload.get("size", 10_000_000)
                if "max" in payload:
                    kwargs["max"] = payload["max"]
            db.create_collection(collection_name, **kwargs)
            return _success(
                {
                    "success": True,
                    "message": f"Collection '{collection_name}' created in '{db_name}'",
                }
            )

        if op == "drop_collection":
            if db is None:
                _error("'database' is required for drop_collection")
            if not collection_name:
                _error("'collection' is required for drop_collection")
            if not payload.get("confirm"):
                _error(
                    "drop_collection requires explicit confirmation",
                    hint="Add 'confirm': true to the payload after confirming with the user",
                )
            db.drop_collection(collection_name)
            return _success(
                {"success": True, "message": f"Collection '{collection_name}' dropped from '{db_name}'"}
            )

        if op == "create_index":
            if db is None:
                _error("'database' is required for create_index")
            col = _get_collection(db, collection_name)
            keys_raw = payload.get("keys")
            if not keys_raw:
                _error("'keys' is required for create_index — dict of {field: 1|-1} or [[field, order], ...]")
            if isinstance(keys_raw, dict):
                keys = list(keys_raw.items())
            else:
                keys = [(k, v) for k, v in keys_raw]
            index_opts: dict[str, Any] = {}
            for opt in ("unique", "sparse", "name", "expireAfterSeconds", "background"):
                if opt in payload:
                    index_opts[opt] = payload[opt]
            index_name = col.create_index(keys, **index_opts)
            return _success({"success": True, "index_name": index_name, "collection": collection_name})

        # ── Collection required from here ─────────────────────────────────────

        if db is None:
            _error("'database' is required for this operation")

        col = _get_collection(db, collection_name)

        if op == "count":
            filt = payload.get("filter", {})
            n = col.count_documents(filt)
            return _success({"success": True, "count": n, "filter": filt})

        if op == "find":
            filt = payload.get("filter", {})
            projection = payload.get("projection")
            limit = int(payload.get("limit", 0))
            sort_raw = payload.get("sort")
            cursor = col.find(filt, projection)
            if sort_raw:
                if isinstance(sort_raw, dict):
                    sort_list = [(k, v) for k, v in sort_raw.items()]
                else:
                    sort_list = [(k, v) for k, v in sort_raw]
                cursor = cursor.sort(sort_list)
            if limit:
                cursor = cursor.limit(limit)
            docs = [_serialize_doc(d) for d in cursor]
            return _success({"success": True, "count": len(docs), "documents": docs})

        if op == "find_one":
            filt = payload.get("filter", {})
            projection = payload.get("projection")
            doc = col.find_one(filt, projection)
            return _success({"success": True, "document": _serialize_doc(doc)})

        if op == "insert_one":
            document = payload.get("document")
            if document is None:
                _error("'document' is required for insert_one")
            result = col.insert_one(document)
            return _success(
                {"success": True, "inserted_id": str(result.inserted_id)}
            )

        if op == "insert_many":
            documents = payload.get("documents")
            if not documents:
                _error("'documents' (non-empty list) is required for insert_many")
            result = col.insert_many(documents)
            return _success(
                {
                    "success": True,
                    "inserted_count": len(result.inserted_ids),
                    "inserted_ids": [str(i) for i in result.inserted_ids],
                }
            )

        if op == "update_one":
            filt = payload.get("filter")
            update = payload.get("update")
            if filt is None or update is None:
                _error("'filter' and 'update' are required for update_one")
            upsert = payload.get("upsert", False)
            result = col.update_one(filt, update, upsert=upsert)
            return _success(
                {
                    "success": True,
                    "matched_count": result.matched_count,
                    "modified_count": result.modified_count,
                    "upserted_id": str(result.upserted_id) if result.upserted_id else None,
                }
            )

        if op == "update_many":
            filt = payload.get("filter")
            update = payload.get("update")
            if filt is None or update is None:
                _error("'filter' and 'update' are required for update_many")
            upsert = payload.get("upsert", False)
            result = col.update_many(filt, update, upsert=upsert)
            return _success(
                {
                    "success": True,
                    "matched_count": result.matched_count,
                    "modified_count": result.modified_count,
                    "upserted_id": str(result.upserted_id) if result.upserted_id else None,
                }
            )

        if op == "replace_one":
            filt = payload.get("filter")
            replacement = payload.get("replacement")
            if filt is None or replacement is None:
                _error("'filter' and 'replacement' are required for replace_one")
            upsert = payload.get("upsert", False)
            result = col.replace_one(filt, replacement, upsert=upsert)
            return _success(
                {
                    "success": True,
                    "matched_count": result.matched_count,
                    "modified_count": result.modified_count,
                    "upserted_id": str(result.upserted_id) if result.upserted_id else None,
                }
            )

        if op == "delete_one":
            filt = payload.get("filter")
            if filt is None:
                _error("'filter' is required for delete_one")
            if not payload.get("confirm"):
                _error(
                    "delete_one requires explicit confirmation",
                    hint="Add 'confirm': true after confirming the deletion with the user",
                )
            result = col.delete_one(filt)
            return _success({"success": True, "deleted_count": result.deleted_count})

        if op == "delete_many":
            filt = payload.get("filter")
            if filt is None:
                _error("'filter' is required for delete_many")
            if not payload.get("confirm"):
                _error(
                    "delete_many requires explicit confirmation",
                    hint="Add 'confirm': true after confirming the deletion with the user",
                )
            result = col.delete_many(filt)
            return _success({"success": True, "deleted_count": result.deleted_count})

        if op == "aggregate":
            pipeline = payload.get("pipeline")
            if not isinstance(pipeline, list):
                _error("'pipeline' (list of stage objects) is required for aggregate")
            docs = [_serialize_doc(d) for d in col.aggregate(pipeline)]
            return _success({"success": True, "count": len(docs), "documents": docs})

        _error(
            f"Unknown operation '{op}'",
            supported=[
                "list_databases", "list_collections", "create_collection", "drop_collection",
                "create_index", "count",
                "find", "find_one",
                "insert_one", "insert_many",
                "update_one", "update_many", "replace_one",
                "delete_one", "delete_many",
                "aggregate",
            ],
        )

    except Exception as exc:  # noqa: BLE001
        _error(str(exc), operation=op)


def main() -> None:
    if len(sys.argv) < 2:
        _error(
            "Usage: mongo_client.py '<json_payload>'",
            example='{"operation":"list_databases"}',
        )
    try:
        payload = json.loads(sys.argv[1])
    except json.JSONDecodeError as exc:
        _error(f"Invalid JSON payload: {exc}")

    run(payload)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)