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
CEO, SaaSNext
- 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:GetQueryResultsonly - 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.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
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.
Build a ServiceNow ITSM MCP Server for Agentic Incident Management & Change Control in 2026
Next Story →Anthropic Ships Claude Code 2.0: Full Codebase Rewriting with 100K File Context Window
Related Intelligence Analysis
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...
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...
NVIDIA Audex vs Qwen3.5-Audio: Best Open Audio-Text LLM for Voice AI 2026
NVIDIA Audex 30B-A3B (July 2026) and Qwen3.5-35B-A3B are the two leading open audio-text LLMs. Audex uniquely handles both audio understanding and generation in a single model while preserving text intelligence. Qwen3.5-...