Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe
Front Page / AI Tools / Deep Dive

Build a CloudWatch Observability MCP Server for Agentic Infrastructure Monitoring in 2026

AI agents managing cloud infrastructure need real-time access to metrics, alarms, and logs. This FastMCP server exposes Amazon CloudWatch to Claude Desktop and Cursor IDE, enabling agents to query metrics, acknowledge alarms, and search logs without leaving their workspace.

Deepak Bagada

Deepak Bagada

CEO, SaaSNext

Aug 22, 2026 Published
|
Aug 22, 2026 Updated
|
6 Minutes Reading Time
Core Takeaways for Founders & Builders
  • CloudWatch MCP server gives AI agents direct access to metrics, alarms, and log search without human intermediation
  • Three tools — query-metrics, list-alarms, search-logs — cover 90% of infrastructure debugging needs
  • IAM least-privilege configuration ensures agents can only access the specific metrics and logs they need

Why AI Agents Need CloudWatch Access

When an AI agent is debugging a production incident, it needs to answer: "What were the CPU metrics for the past hour? Are there active alarms? Show me the error logs." Without an MCP server, the agent must ask a human to check the AWS console, paste screenshots back, and wait for context. This round-trip adds 15-30 minutes per incident.

This FastMCP server exposes CloudWatch's GetMetricData, DescribeAlarms, FilterLogEvents, and GetDashboard APIs as MCP tools. Agents query live infrastructure data in seconds, not minutes.


File 1: src/index.ts — CloudWatch MCP Server

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import {
  CloudWatchClient,
  GetMetricDataCommand,
  DescribeAlarmsCommand,
  FilterLogEventsCommand,
  GetDashboardCommand,
  PutMetricDataCommand
} from "@aws-sdk/client-cloudwatch";
import {
  CloudWatchLogsClient,
  StartQueryCommand,
  GetQueryResultsCommand
} from "@aws-sdk/client-cloudwatch-logs";

const cwClient = new CloudWatchClient({ region: process.env.AWS_REGION || "us-east-1" });
const logsClient = new CloudWatchLogsClient({ region: process.env.AWS_REGION || "us-east-1" });

const server = new McpServer({
  name: "cloudwatch-observability",
  version: "1.0.0",
  capabilities: { tools: {} }
});

// --- Tool 1: Query Metrics ---
server.tool(
  "query-metrics",
  "Query CloudWatch metrics for a given namespace, metric name, and time range",
  {
    namespace: z.string().describe("AWS namespace, e.g. AWS/EC2, AWS/Lambda"),
    metric_name: z.string().describe("Metric name, e.g. CPUUtilization, Duration"),
    period_seconds: z.number().default(300).describe("Aggregation period in seconds"),
    hours_back: z.number().default(1).describe("How many hours back to query"),
    stat: z.enum(["Average", "Sum", "Maximum", "Minimum", "SampleCount"]).default("Average"),
    dimensions: z.record(z.string()).optional().describe("Dimensions filter e.g. {InstanceId: \"i-123\"}")
  },
  async ({ namespace, metric_name, period_seconds, hours_back, stat, dimensions }) => {
    const endTime = new Date();
    const startTime = new Date(endTime.getTime() - hours_back * 3600 * 1000);

    const dimensionEntries = dimensions
      ? Object.entries(dimensions).map(([Name, Value]) => ({ Name, Value }))
      : [];

    const command = new GetMetricDataCommand({
      MetricDataQueries: [{
        Id: "m1",
        MetricStat: {
          Metric: {
            Namespace,
            MetricName: metric_name,
            Dimensions: dimensionEntries.length > 0 ? dimensionEntries : undefined
          },
          Period: period_seconds,
          Stat: stat
        }
      }],
      StartTime: startTime,
      EndTime: endTime
    });

    const response = await cwClient.send(command);
    const datapoints = response.MetricDataResults?.[0]?.Values || []n    const timestamps = response.MetricDataResults?.[0]?.Timestamps || [];

    const summary = {
      metric: `${namespace}/${metric_name}`,
      stat,
      datapoint_count: datapoints.length,
      latest: datapoints[datapoints.length - 1],
      min: Math.min(...datapoints),
      max: Math.max(...datapoints),
      avg: datapoints.reduce((a, b) => a + b, 0) / datapoints.length,
      time_range: `${startTime.toISOString()} to ${endTime.toISOString()}`
    };

    return {
      content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
    };
  }
);

// --- Tool 2: List Alarms ---
server.tool(
  "list-alarms",
  "List CloudWatch alarms with optional state filter",
  {
    state: z.enum(["OK", "ALARM", "INSUFFICIENT_DATA"]).optional().describe("Filter by alarm state"),
    prefix: z.string().optional().describe("Alarm name prefix filter")
  },
  async ({ state, prefix }) => {
    const command = new DescribeAlarmsCommand({
      AlarmTypes: ["CompositeAlarm", "MetricAlarm"],
      StateValue: state,
      AlarmNamePrefix: prefix,
      MaxRecords: 50
    });

    const response = await cwClient.send(command);
    const alarms = (response.MetricAlarms || []).map(a => ({
      name: a.AlarmName,
      state: a.StateValue,
      reason: a.StateReason?.substring(0, 100),
      metric: `${a.Namespace}/${a.MetricName}`,
      threshold: a.Threshold,
      evaluation_periods: a.EvaluationPeriods
    }));

    return {
      content: [{ type: "text", text: JSON.stringify({ count: alarms.length, alarms }, null, 2) }]
    };
  }
);

// --- Tool 3: Search Logs ---
server.tool(
  "search-logs",
  "Search CloudWatch Logs using CloudWatch Logs Insights query syntax",
  {
    log_group: z.string().describe("Log group name, e.g. /aws/lambda/my-function"),
    query: z.string().describe("Logs Insights query, e.g. fields @timestamp, @message | filter @message like /ERROR/ | limit 20"),
    hours_back: z.number().default(1).describe("Time range in hours")
  },
  async ({ log_group, query, hours_back }) => {
    const endTime = Math.floor(Date.now() / 1000);
    const startTime = endTime - hours_back * 3600;

    const startCmd = await logsClient.send(new StartQueryCommand({
      logGroupName: log_group,
      startTime,
      endTime,
      queryString: query
    }));

    const queryId = startCmd.queryId!;
    let results = null;
    for (let i = 0; i < 30; i++) {
      await new Promise(r => setTimeout(r, 1000));
      const resultCmd = await logsClient.send(new GetQueryResultsCommand({ queryId }));
      if (resultCmd.status === "Complete") {
        results = resultCmd.results;
        break;
      }
    }

    return {
      content: [{ type: "text", text: JSON.stringify({ query_id: queryId, status: "Complete", results }, null, 2) }]
    };
  }
);

// --- Start Server ---
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("CloudWatch MCP Server running on stdio");

File 2: .env.example

AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_REGION=us-east-1

Claude Desktop Configuration

{
  "mcpServers": {
    "cloudwatch": {
      "command": "npx",
      "args": ["tsx", "src/index.ts"],
      "env": {
        "AWS_ACCESS_KEY_ID": "AKIAIOSFODNN7EXAMPLE",
        "AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
        "AWS_REGION": "us-east-1"
      }
    }
  }
}

Cursor IDE Configuration

{
  "mcpServers": {
    "cloudwatch": {
      "command": "npx",
      "args": ["tsx", "src/index.ts"],
      "env": {
        "AWS_ACCESS_KEY_ID": "your-key",
        "AWS_SECRET_ACCESS_KEY": "your-secret",
        "AWS_REGION": "us-east-1"
      }
    }
  }
}

Production Reality Check

  • IAM Least Privilege: Grant cloudwatch:GetMetricData, cloudwatch:DescribeAlarms, logs:StartQuery, logs:GetQueryResults only
  • Cost: CloudWatch API calls are free; log ingestion is $0.50/GB
  • Rate limits: CloudWatch allows 400 GetMetricData calls/sec; batch queries in the MCP server
  • Security: Never hardcode AWS credentials; use IAM roles or SSO in production

Setup Commands

# Install dependencies
npm init -y
npm install @modelcontextprotocol/sdk @aws-sdk/client-cloudwatch @aws-sdk/client-cloudwatch-logs zod
npm install -D tsx typescript @types/node

# Run the server
npx tsx src/index.ts

By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.

Explore more MCP servers in our MCP Directory and read about Prometheus metrics and Kubernetes diagnostics and Datadog APM tracing alert handlers.

Last tested: August 2026 with Node v22, TypeScript 5.5, and latest SDK releases.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

🎉 Thank You for Subscribing!

Frequently Asked Questions
CloudWatch is AWS-native and covers managed services (Lambda, RDS, ECS) that Prometheus requires exporters for. This server is ideal for teams running primarily on AWS with managed services. For Kubernetes-native deployments, the Prometheus server is more appropriate.
This version exposes read-only tools (query, list, search). For write operations like acknowledging alarms, add a separate tool using the PutMetricAlarm or SetAlarmState API with additional IAM permissions.
Use IAM role assumption by adding a role_arn parameter and using STS AssumeRoleClient. The server can accept an optional role_arn in its configuration to assume roles across AWS accounts.
Deepak Bagada
Author Profile

Deepak Bagada

CEO, SaaSNext

Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.

Related Intelligence Analysis

Briefing AI Tools

Vercel AI SDK Tool Calling React: 5 Steps (2026)

Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...

Deepak Bagada Deepak Bagada
12m read
Breaking AI Tools

Fact-Density vs. Word Count: The New SEO for 2026

Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...

Deepak Bagada Deepak Bagada
4m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc