Skill: finishy1995/mongodb-atlas-admin (version 1.0.0)

--- name: mongodb-atlas description: browse MongoDB Atlas Admin API specifications and execute operations (if credentials provided). homepage: https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/ metadata: {"clawdbot":{"emoji":"🍃","requires":{"bins":["node"],"env":["ATLAS_CLIENT_ID","ATLAS_CLIENT_SECRET"]},"primaryEnv":""}} --- # MongoDB Atlas Admin API Tool to browse OpenAPI specifications for MongoDB Atlas. **Note:** If `ATLAS_CLIENT_ID` and `ATLAS_CLIENT_SECRET` are configured in the environment, this tool can also execute live API calls. Without credentials, it functions as a read-only documentation browser. ## Commands ### 1. List API Catalog List all available API categories or filter by keyword. ```bash node {baseDir}/scripts/atlas-api.mjs catalog # list all categories node {baseDir}/scripts/atlas-api.mjs catalog Clusters ``` ### 2. Get API Details Get full endpoint definition (method, path, params) for a specific Operation ID. ```bash node {baseDir}/scripts/atlas-api.mjs detail listClusterDetails ``` ### 3. Get Schema Definition Get the data model schema for complex types. ```bash node {baseDir}/scripts/atlas-api.mjs schema "#/components/schemas/ApiError" ``` ### 4. Execute Live API Calls Execute real HTTP requests against the Atlas API. **Script:** `node {baseDir}/scripts/atlas-call.mjs <METHOD> <ENDPOINT> [flags]` #### ⚠️ Mandatory Safety Protocol **For any state-changing operation (POST, PUT, PATCH, DELETE):** 1. **STOP & REVIEW**: You MUST NOT execute the command immediately. 2. **PREVIEW**: Use `--dry-run` first to verify the payload and endpoint. 3. **CONFIRM**: Display the full command and JSON body to the user. 4. **EXECUTE**: Only run with `--yes` after receiving explicit user approval. #### Usage Examples **1. Read-Only (Safe)** ```bash node {baseDir}/scripts/atlas-call.mjs GET groups/{groupId}/clusters ``` **2. Create/Modify (RISKY - Require Approval)** ```bash node {baseDir}/scripts/atlas-call.mjs POST groups/{groupId}/clusters \ --data '{"name":"DemoCluster", "providerSettings":{...}}' \ --dry-run ``` #### Options * `-d, --data <json>`: Request body string (ensure proper JSON escaping). * `-p, --params <json>`: Query parameters. * `--dry-run`: Print the request details without executing (Recommended for verification). * `--yes`: Skip interactive confirmation (Use CAREFULLY). #### Environment Requires `ATLAS_CLIENT_ID` and `ATLAS_CLIENT_SECRET` to be set. ## Core Categories (Use `catalog` command to see the full list of 50+ categories) * **Clusters** / **Cloud Backups** * **Projects** / **Organizations** * **Database Users** / **Custom Database Roles** * **Alerts** / **Alert Configurations** * **Monitoring and Logs** / **Events** * **Network Peering** / **Private Endpoint Services** * **Serverless Instances** * **Access Tracking** / **Auditing**

path: scripts/atlas-api.mjs
content: #!/usr/bin/env node

import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const SPEC_PATH = path.join(__dirname, "..", "resources", "atlas-admin-api-v2-openapi-source.json");

function usage() {
  console.error(`Usage: atlas-api.mjs <command> [args]

Commands:
  catalog [category]     List all APIs, optionally filtered by category
  detail <operationId>   Get detailed info for a specific API
  schema <schemaName>    Get schema definition (e.g., ClusterDescription)

Examples:
  node atlas-api.mjs catalog
  node atlas-api.mjs catalog Clusters
  node atlas-api.mjs detail listClusterDetails
  node atlas-api.mjs schema "#/components/schemas/ApiError"
`);
  process.exit(2);
}

// Load OpenAPI spec
function loadSpec() {
  if (!fs.existsSync(SPEC_PATH)) {
    throw new Error(`OpenAPI spec not found: ${SPEC_PATH}`);
  }
  const raw = fs.readFileSync(SPEC_PATH, "utf-8");
  return JSON.parse(raw);
}

// Resolve $ref references
function resolveRef(spec, obj, maxDepth = 5, currentDepth = 0, shallow = false) {
  if (currentDepth >= maxDepth) return obj;
  
  if (typeof obj === "object" && obj !== null) {
    if (Array.isArray(obj)) {
      return obj.map(item => resolveRef(spec, item, maxDepth, currentDepth, shallow));
    }
    
    if (obj.$ref && typeof obj.$ref === "string") {
      const refPath = obj.$ref;
      if (refPath.startsWith("#/")) {
        const parts = refPath.slice(2).split("/");
        let resolved = spec;
        for (const part of parts) {
          resolved = resolved?.[part];
        }
        
        if (shallow && currentDepth > 0) {
          return obj;
        }
        
        return resolveRef(spec, resolved, maxDepth, currentDepth + 1, shallow);
      }
      return obj;
    }
    
    const result = {};
    for (const [key, value] of Object.entries(obj)) {
      result[key] = resolveRef(spec, value, maxDepth, currentDepth, shallow);
    }
    return result;
  }
  
  return obj;
}

// Smart trim - remove verbose fields
function smartTrim(obj) {
  if (typeof obj === "object" && obj !== null) {
    if (Array.isArray(obj)) {
      return obj.map(item => smartTrim(item));
    }
    
    const trimKeys = ["example", "examples", "default", "x-xgen-version"];
    const result = {};
    
    for (const [key, value] of Object.entries(obj)) {
      // Skip extension fields
      if (key.startsWith("x-") && key !== "x-enum-descriptions") {
        continue;
      }
      // Skip trim keys
      if (trimKeys.includes(key)) {
        continue;
      }
      // Truncate long descriptions
      if (key === "description" && typeof value === "string" && value.length > 300) {
        result[key] = value.slice(0, 300) + "...";
      } else {
        result[key] = smartTrim(value);
      }
    }
    return result;
  }
  return obj;
}

// YAML stringify (simple implementation)
function toYaml(obj, indent = 0) {
  const spaces = "  ".repeat(indent);
  let result = "";
  
  if (obj === null || obj === undefined) {
    return "null";
  }
  
  if (typeof obj === "string") {
    // Quote strings that need it
    if (obj.includes(":") || obj.includes("#") || obj.includes("'") || obj.includes('"') || obj.startsWith(" ") || obj.endsWith(" ")) {
      return `"${obj.replace(/"/g, '\\"')}"`;
    }
    return obj;
  }
  
  if (typeof obj === "number" || typeof obj === "boolean") {
    return String(obj);
  }
  
  if (Array.isArray(obj)) {
    if (obj.length === 0) return "[]";
    for (const item of obj) {
      const yamlItem = toYaml(item, indent + 1);
      if (typeof item === "object" && item !== null && !Array.isArray(item)) {
        result += `${spaces}-\n${yamlItem}\n`;
      } else {
        result += `${spaces}- ${yamlItem.replace(new RegExp(`^${" ".repeat(indent + 1)}`), "")}\n`;
      }
    }
    return result.trimEnd();
  }
  
  for (const [key, value] of Object.entries(obj)) {
    const yamlValue = toYaml(value, indent + 1);
    if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0) {
      result += `${spaces}${key}:\n${yamlValue}\n`;
    } else if (Array.isArray(value) && value.length > 0) {
      result += `${spaces}${key}:\n${yamlValue}\n`;
    } else {
      result += `${spaces}${key}: ${yamlValue}\n`;
    }
  }
  
  return result.trimEnd();
}

// Get list of unique categories from spec
function getCategories(spec) {
  const paths = spec.paths || {};
  const categories = new Set();
  
  for (const methods of Object.values(paths)) {
    for (const details of Object.values(methods)) {
      if (typeof details !== "object" || details === null) continue;
      const tags = details.tags || ["Uncategorized"];
      tags.forEach(tag => categories.add(tag));
    }
  }
  
  return Array.from(categories).sort();
}

// Get catalog of APIs
function getCatalog(spec, category = null) {
  // If no category specified, return list of categories
  if (!category) {
    const categories = getCategories(spec);
    const lines = categories.map(c => `- ${c}`);
    return `--- Atlas API Categories (${categories.length} total) ---\nUse: node atlas-api.mjs catalog <category> to list APIs in a category\n\n` + lines.join("\n");
  }
  
  const paths = spec.paths || {};
  const results = [];
  const targetCat = category.toLowerCase();
  
  for (const [pathStr, methods] of Object.entries(paths)) {
    for (const [method, details] of Object.entries(methods)) {
      if (typeof details !== "object" || details === null) continue;
      
      const tags = details.tags || ["Uncategorized"];
      
      if (tags.some(t => t.toLowerCase() === targetCat)) {
        const summary = details.summary || "N/A";
        const opId = details.operationId || "N/A";
        results.push(`- [${opId}] ${method.toUpperCase()} ${pathStr}\n  Summary: ${summary}`);
      }
    }
  }
  
  if (results.length === 0) {
    return `Category '${category}' not found.`;
  }
  
  return `--- Atlas API List (${category}) ---\n` + results.join("\n");
}

// Get API details by operationId
function getApiDetail(spec, operationId) {
  const paths = spec.paths || {};
  let targetOp = null;
  
  for (const [pathStr, methods] of Object.entries(paths)) {
    for (const [method, details] of Object.entries(methods)) {
      if (details?.operationId === operationId) {
        targetOp = { ...details, _path: pathStr, _method: method };
        break;
      }
    }
    if (targetOp) break;
  }
  
  if (!targetOp) {
    return `Error: API with operationId '${operationId}' not found.`;
  }
  
  const keysToKeep = ["summary", "description", "parameters", "requestBody", "responses"];
  const filteredOp = {};
  
  for (const key of keysToKeep) {
    if (key in targetOp) {
      filteredOp[key] = targetOp[key];
    }
  }
  
  // Add path and method for context
  filteredOp.path = targetOp._path;
  filteredOp.method = targetOp._method.toUpperCase();
  
  const resolvedOp = resolveRef(spec, filteredOp, 2, 0, true);
  const trimmedOp = smartTrim(resolvedOp);
  
  return toYaml(trimmedOp);
}

// Get schema details
function getSchemaDetail(spec, schemaRef) {
  // Handle shorthand form
  let fullRef = schemaRef;
  if (!schemaRef.startsWith("#/")) {
    fullRef = `#/components/schemas/${schemaRef}`;
  }
  
  if (!fullRef.startsWith("#/")) {
    return `Error: Invalid reference path '${schemaRef}', must start with '#/' or be a schema name.`;
  }
  
  const parts = fullRef.slice(2).split("/");
  let resolved = spec;
  
  for (const part of parts) {
    if (part in resolved) {
      resolved = resolved[part];
    } else {
      return `Error: Schema '${schemaRef}' not found.`;
    }
  }
  
  const resolvedSchema = resolveRef(spec, resolved, 3);
  const trimmedSchema = smartTrim(resolvedSchema);
  
  return toYaml({ [fullRef]: trimmedSchema });
}

// Main
async function main() {
  const args = process.argv.slice(2);
  
  if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
    usage();
  }
  
  const command = args[0];
  
  try {
    const spec = loadSpec();
    
    switch (command) {
      case "catalog": {
        const category = args[1] || null;
        console.log(getCatalog(spec, category));
        break;
      }
      
      case "detail": {
        if (!args[1]) {
          console.error("Error: operationId required");
          usage();
        }
        console.log(getApiDetail(spec, args[1]));
        break;
      }
      
      case "schema": {
        if (!args[1]) {
          console.error("Error: schemaName required");
          usage();
        }
        console.log(getSchemaDetail(spec, args[1]));
        break;
      }
      
      default:
        console.error(`Unknown command: ${command}`);
        usage();
    }
  } catch (err) {
    console.error(`Error: ${err.message}`);
    process.exit(1);
  }
}

main();


path: scripts/atlas-call.mjs
content: #!/usr/bin/env node

/**
 * MongoDB Atlas API Client
 * 
 * Makes actual API calls to MongoDB Atlas.
 * WARNING: POST/PUT/DELETE/PATCH operations modify resources and require USER approval.
 * 
 * Environment variables required:
 * - ATLAS_CLIENT_ID: Service Account Client ID
 * - ATLAS_CLIENT_SECRET: Service Account Client Secret
 * - ATLAS_GROUP_ID: (Optional) Default project/group ID
 * - ATLAS_API_BASE_URL: (Optional) Default: https://cloud.mongodb.com
 */

import fs from "fs";
import path from "path";
import os from "os";

const CACHE_DIR = path.join(os.homedir(), ".openclaw", ".cache", "mongodb-atlas");
const TOKEN_CACHE_FILE = path.join(CACHE_DIR, "token.json");

// Ensure cache directory exists
function ensureCacheDir() {
  if (!fs.existsSync(CACHE_DIR)) {
    fs.mkdirSync(CACHE_DIR, { recursive: true });
  }
}

// Load cached token
function loadCachedToken() {
  try {
    if (fs.existsSync(TOKEN_CACHE_FILE)) {
      const data = JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, "utf-8"));
      if (data.expires_at && Date.now() < data.expires_at) {
        return data.access_token;
      }
    }
  } catch (e) {
    // Ignore cache errors
  }
  return null;
}

// Save token to cache
function saveToken(accessToken, expiresIn) {
  ensureCacheDir();
  const data = {
    access_token: accessToken,
    expires_at: Date.now() + (expiresIn - 60) * 1000 // Refresh 60s early
  };
  fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(data, null, 2));
}

// Clear cached token
function clearCachedToken() {
  try {
    if (fs.existsSync(TOKEN_CACHE_FILE)) {
      fs.unlinkSync(TOKEN_CACHE_FILE);
    }
  } catch (e) {
    // Ignore
  }
}

// Get access token (with caching)
async function getAccessToken() {
  const clientId = process.env.ATLAS_CLIENT_ID;
  const clientSecret = process.env.ATLAS_CLIENT_SECRET;
  const baseUrl = process.env.ATLAS_API_BASE_URL || "https://cloud.mongodb.com";

  if (!clientId || !clientSecret) {
    throw new Error(
      "Missing Atlas Service Account credentials. Please set environment variables:\n" +
      "  ATLAS_CLIENT_ID\n" +
      "  ATLAS_CLIENT_SECRET"
    );
  }

  // Check cache first
  const cached = loadCachedToken();
  if (cached) {
    return cached;
  }

  // Request new token using OAuth 2.0 client credentials flow
  const tokenUrl = `${baseUrl}/api/oauth/token`;
  const auth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
  
  try {
    const response = await fetch(tokenUrl, {
      method: "POST",
      headers: {
        "Authorization": `Basic ${auth}`,
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body: "grant_type=client_credentials"
    });

    if (!response.ok) {
      const text = await response.text();
      throw new Error(`Failed to get access [REDACTED_SECRET] - ${text}`);
    }

    const data = await response.json();
    saveToken(data.access_token, data.expires_in || 3600);
    return data.access_token;

  } catch (e) {
    throw new Error(`Error getting Atlas access [REDACTED_SECRET]`);
  }
}

// Call Atlas API
async function callAtlasApi(method, endpoint, data = null, params = null) {
  const baseUrl = process.env.ATLAS_API_BASE_URL || "https://cloud.mongodb.com";
  const groupId = process.env.ATLAS_GROUP_ID;

  // Replace {groupId} placeholder if env var is set
  let path = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
  if (groupId) {
    path = path.replace(/{groupId}/g, groupId);
  }

  const url = new URL(`/api/atlas/v2/${path}`, baseUrl);
  
  // Add query params
  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value !== undefined && value !== null) {
        url.searchParams.append(key, String(value));
      }
    }
  }

  const token = await getAccessToken();

  const headers = {
    "Authorization": `Bearer ${token}`,
    "Accept": "application/vnd.atlas.2024-08-05+json"
  };

  if (data) {
    headers["Content-Type"] = "application/vnd.atlas.2024-08-05+json";
  }

  try {
    const response = await fetch(url.toString(), {
      method: method.toUpperCase(),
      headers,
      body: data ? JSON.stringify(data) : undefined
    });

    // Handle token expiration
    if (response.status === 401) {
      clearCachedToken();
      // Could retry once here if needed
      throw new Error("Authentication failed. Token may have expired. Please retry.");
    }

    // Parse response
    let result;
    const contentType = response.headers.get("content-type");
    if (contentType?.includes("application/json")) {
      result = await response.json();
    } else if (response.status === 204) {
      result = { success: true, message: "Operation completed successfully" };
    } else {
      const text = await response.text();
      result = { status: response.status, body: text };
    }

    if (!response.ok) {
      return {
        error: true,
        status_code: response.status,
        message: `API call failed: ${response.status}`,
        details: result
      };
    }

    return result;

  } catch (e) {
    if (e.message.includes("fetch failed") || e.message.includes("ENOTFOUND")) {
      return {
        error: true,
        message: `Network error: ${e.message}`
      };
    }
    throw e;
  }
}

// Check if operation is risky (modifies resources)
function isRiskyOperation(method) {
  return ["POST", "PUT", "DELETE", "PATCH"].includes(method.toUpperCase());
}

// Main
async function main() {
  const args = process.argv.slice(2);
  
  if (args.length < 2 || args[0] === "-h" || args[0] === "--help") {
    console.log(`Usage: atlas-call.mjs <method> <endpoint> [options]

Make API calls to MongoDB Atlas.

Arguments:
  method      HTTP method: GET, POST, PUT, DELETE, PATCH
  endpoint    API endpoint path, e.g., 'groups/{groupId}/clusters'

Options:
  -d, --data <json>      Request body (JSON string)
  -p, --params <json>    Query parameters (JSON string)
  -f, --file <path>      Read request body from JSON file
  --dry-run              Show what would be called without making the request
  --yes                  Skip confirmation for risky operations (NOT RECOMMENDED)

Environment:
  ATLAS_CLIENT_ID        Service Account Client ID (required)
  ATLAS_CLIENT_SECRET    Service Account Client Secret (required)
  ATLAS_GROUP_ID         Default project/group ID (optional, replaces {groupId})
  ATLAS_API_BASE_URL     API base URL (default: https://cloud.mongodb.com)

Examples:
  # List clusters (safe read operation)
  node atlas-call.mjs GET groups/{groupId}/clusters

  # Create cluster (RISKY - modifies resources)
  node atlas-call.mjs POST groups/{groupId}/clusters -d '{"name":"myCluster"}'

  # Delete cluster (RISKY - deletes resources)
  node atlas-call.mjs DELETE groups/{groupId}/clusters/myCluster

⚠️  WARNING: POST/PUT/DELETE/PATCH operations modify or delete resources.
    These operations require explicit USER approval before execution.
    See SKILL.md for safety guidelines.
`);
    process.exit(2);
  }

  const method = args[0].toUpperCase();
  const endpoint = args[1];
  
  let data = null;
  let params = null;
  let dryRun = false;
  let skipConfirm = false;

  // Parse options
  for (let i = 2; i < args.length; i++) {
    const arg = args[i];
    if ((arg === "-d" || arg === "--data") && i + 1 < args.length) {
      try {
        data = JSON.parse(args[i + 1]);
      } catch (e) {
        console.error(`Error parsing JSON data: ${e.message}`);
        process.exit(1);
      }
      i++;
    } else if ((arg === "-p" || arg === "--params") && i + 1 < args.length) {
      try {
        params = JSON.parse(args[i + 1]);
      } catch (e) {
        console.error(`Error parsing JSON params: ${e.message}`);
        process.exit(1);
      }
      i++;
    } else if ((arg === "-f" || arg === "--file") && i + 1 < args.length) {
      try {
        const filePath = args[i + 1];
        const fileContent = fs.readFileSync(filePath, "utf-8");
        data = JSON.parse(fileContent);
      } catch (e) {
        console.error(`Error reading/parsing file: ${e.message}`);
        process.exit(1);
      }
      i++;
    } else if (arg === "--dry-run") {
      dryRun = true;
    } else if (arg === "--yes") {
      skipConfirm = true;
    }
  }

  // Check for risky operations
  if (isRiskyOperation(method)) {
    console.log("\n" + "=".repeat(60));
    console.log("⚠️  RISKY OPERATION DETECTED");
    console.log("=".repeat(60));
    console.log(`\nMethod: ${method}`);
    console.log(`Endpoint: ${endpoint}`);
    if (data) {
      console.log(`\nRequest Body:\n${JSON.stringify(data, null, 2)}`);
    }
    console.log("\n" + "-".repeat(60));
    console.log("This operation will MODIFY or DELETE resources.");
    console.log("You MUST review and approve this operation.");
    console.log("-".repeat(60) + "\n");

    if (!skipConfirm && !dryRun) {
      console.error("ERROR: Risky operations require USER approval.");
      console.error("\nTo execute this operation:");
      console.error("1. Review the operation details above");
      console.error("2. If you approve, run with --yes flag (NOT RECOMMENDED for automation)");
      console.error("\nAlternatively, use --dry-run to preview the request without executing.");
      process.exit(1);
    }
  }

  // Show dry run info
  if (dryRun) {
    console.log("DRY RUN - Request details:");
    console.log(`  Method: ${method}`);
    console.log(`  Endpoint: ${endpoint}`);
    if (data) console.log(`  Body: ${JSON.stringify(data, null, 2)}`);
    if (params) console.log(`  Params: ${JSON.stringify(params, null, 2)}`);
    console.log("\nNo request was made.");
    return;
  }

  // Execute the API call
  try {
    const result = await callAtlasApi(method, endpoint, data, params);
    console.log(JSON.stringify(result, null, 2));
  } catch (e) {
    console.error(`Error: ${e.message}`);
    process.exit(1);
  }
}

main();


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