{
  "source_file": "0930",
  "source_platform": "Dev.to",
  "author": "xbill",
  "title_listed": "Put the Arithmetic in the Tool: an MCP Server for an AWS Waste Scanner",
  "keywords": "",
  "domain": "文本处理",
  "content_type": "工程实践",
  "is_paid_flag": "否",
  "url": "https://dev.to/aws-builders/put-the-arithmetic-in-the-tool-an-mcp-server-for-an-aws-waste-scanner-3n79",
  "publish_date": "2026-09-21",
  "corpus_stage": "",
  "notes": "",
  "id": "0f63927bceae",
  "collected_at": "2026-10-02T13:51:19",
  "title": "Put the Arithmetic in the Tool: an MCP Server for an AWS Waste Scanner - DEV Community",
  "body_markdown": "This article provides a step by step guide to adding an MCP server to an AWS cost scanner so an agent drives the same engine a terminal does. Every tool returns figures the engine computed, and the two front doors are held to the same number by an end-to-end test.\nhttps://github.com/xbill9/zombiescan\nA Report Gets Read Twice\nA scan produces one JSON document. A person reads it through a terminal table; an agent reads it through whatever tool it is given.\nBoth quote dollar figures to whoever asked. When the two disagree, one of them is wrong in a way that survives review, because each looks sourced.\nA model handed a list of rows and asked \"how much is this costing\" does the addition in the answer. Eleven small integers are enough to break that: one small model asked how many of eleven ids were 10 or more answered wrong in twenty calls out of twenty. Push the count into the engine and the same question is right every time.\nSo the tools here return totals, counts, breakdowns, minima and maxima, and the filter that produced them.\nAt This Point You Should Have…\nAn AWS account with credentials your shell can use, from\naws login\n, a profile, SSO or environment variables\nPython 3.12 and\nuv\nRead-only access wide enough to describe resources in every region you scan\nce:GetCostAndUsage\nfor the calibration step\nClaude Code, for the plugin at the end\nStep 1 — Scan From the Terminal\nThe scan is read-only: Describe, List and Get calls, across every region the account has enabled.\nuv run zombiescan scan\n--all-regions\n--limit\n8\nEnter fullscreen mode\nExit fullscreen mode\nScanning as arn:aws:iam::<account>:root\n17 region(s), 30 check(s) — read-only\n\nzombiescan — 120 findings across 17 regions\n\nCheck                   Found  Monthly\necr-stale-images           33    $6.40\nunused-route53-zone         1    $0.50\nempty-vpc                   1    $0.00\nlog-group-no-retention     67    $0.00\nunused-security-group      18    $0.00\n\nEstimated waste: $6.90/month ($82.80/year)\nEnter fullscreen mode\nExit fullscreen mode\nOne hundred and twenty findings, thirty-one of them priced. That JSON document is what everything below reads.\nStep 2 — Serve the Same Engine Over stdio\nMCP over stdio is line-delimited JSON-RPC 2.0. A server answers\ninitialize\n,\ntools/list\nand\ntools/call\n, and that is enough for a client to drive it.\nprintf\n'%s\\n'\n'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\"}}'\n\\\n| uv run zombiescan-mcp\nEnter fullscreen mode\nExit fullscreen mode\n{\n\"protocolVersion\"\n:\n\"2025-06-18\"\n,\n\"capabilities\"\n:\n{\n\"tools\"\n:\n{\n\"listChanged\"\n:\nfalse\n}},\n\"serverInfo\"\n:\n{\n\"name\"\n:\n\"zombiescan\"\n,\n\"version\"\n:\n\"0.1.0\"\n},\n\"instructions\"\n:\n\"zombiescan finds AWS resources nobody is using and prices them. Every tool is read-only; nothing here deletes anything. Run scan_account once, then ask estimate_savings for any total, count or breakdown of the report it writes -- do not add up findings yourself.\"\n}\nEnter fullscreen mode\nExit fullscreen mode\nThe\ninstructions\nfield is served to the client on connect and carries the operating rule for the whole server.\ntools\n/\nlist\n->\n[\n'\nscan_account\n'\n,\n'\nestimate_savings\n'\n,\n'\nexplain_finding\n'\n,\n'\nlist_checks\n'\n,\n'\nplan_cleanup\n'\n]\nEnter fullscreen mode\nExit fullscreen mode\nFive tools, about a hundred lines of framing, and no MCP SDK in the dependency list. A cost scanner's install has one job, and a second package to keep current is a second thing to break.\n🔎 Tip: answer\ninitialize\nwith the client's own\nprotocolVersion\nwhen you recognise it. A client that asks for a version it gets back proceeds; one that gets a different string negotiates again.\nStep 3 — Return Figures, Not Rows\nestimate_savings\ntakes a report path and a filter, and answers with the arithmetic already done.\n{\n\"matched\"\n:\n{\n\"count\"\n:\n120\n,\n\"monthly_cost\"\n:\n6.9\n,\n\"annual_cost\"\n:\n82.8\n,\n\"free_count\"\n:\n89\n,\n\"approximate_count\"\n:\n100\n,\n\"costliest\"\n:\n{\n\"check\"\n:\n\"ecr-stale-images\"\n,\n\"resource_id\"\n:\n\"ai-course-creator-app\"\n,\n\"region\"\n:\n\"us-east-1\"\n,\n\"monthly_cost\"\n:\n0.52\n}\n},\n\"by_check\"\n:\n[\n{\n\"check\"\n:\n\"ecr-stale-images\"\n,\n\"count\"\n:\n33\n,\n\"monthly_cost\"\n:\n6.4\n},\n{\n\"check\"\n:\n\"unused-route53-zone\"\n,\n\"count\"\n:\n1\n,\n\"monthly_cost\"\n:\n0.5\n}\n]\n}\nEnter fullscreen mode\nExit fullscreen mode\nEvery how-much, how-many and which-is-biggest question is answered from that one call. The costliest row is picked in code, the per-check and per-region sections are grouped in code, and the model quotes what came back.\nscan_account\nfollows the same rule from the other direction: it returns the totals, the breakdowns and the costliest few findings inline, and leaves all hundred and twenty rows in the report file. A busy account's rows would bury the total in the context window.\nStep 4 — Echo the Filter That Produced the Number\nA filter naming a check the report does not contain returns a precise zero with a citation, which reads as good news.\nestimate_savings\n(\nreport_path\n=\n...,\nchecks\n=\n[\n\"\nunattached-ebs-volumes\n\"\n])\nEnter fullscreen mode\nExit fullscreen mode\n{\n\"filter_applied\"\n:\n{\n\"checks\"\n:\n[\n\"unattached-ebs-volumes\"\n],\n\"findings_in_report\"\n:\n120\n,\n\"findings_matched\"\n:\n0\n,\n\"no_such_checks_in_report\"\n:\n[\n\"unattached-ebs-volumes\"\n]\n},\n\"matched\"\n:\n{\n\"count\"\n:\n0\n,\n\"monthly_cost\"\n:\n0\n},\n\"not_matched\"\n:\n{\n\"count\"\n:\n120\n,\n\"monthly_cost\"\n:\n6.9\n}\n}\nEnter fullscreen mode\nExit fullscreen mode\nThe check is called\nunattached-ebs\n, so that filter matched nothing.\nno_such_checks_in_report\nsays so in the same response, and\nnot_matched\nshows what the filter excluded.\n🔎 Tip: check the predicate a model sent, not only the number it quoted. A wrong filter returns an exact figure with a source attached, which is harder to catch than a miscount.\nStep 5 — Keep Every Tool Read-Only\nScanning and cleaning are separate commands in this tool, and the MCP server exposes only the first.\nplan_cleanup\nbuilds the same plan the\nclean\ncommand shows on a dry run: the API calls in order, the backup step before the destruction where AWS allows one, and an\nirreversible\nflag on every step with no recovery window. It stops there. Applying a plan stays\nzombiescan clean --apply\nat a terminal, where the per-resource prompt lives.\nConvention is weak, so the suite holds the boundary:\ndef\ntest_the_server_cannot_apply_a_cleanup\n():\nsource\n=\npathlib\n.\nPath\n(\nmcp_server\n.\n__file__\n).\nread_text\n()\nassert\n\"\napply_outcome\n\"\nnot\nin\nsource\nassert\nset\n(\nre\n.\nfindall\n(\nr\n\"\n\\bclean\\.(\\w+)\n\"\n,\nsource\n))\n==\n{\n\"\nplan_for\n\"\n,\n\"\nUNSUPPORTED\n\"\n}\nEnter fullscreen mode\nExit fullscreen mode\nclean.plan_for\nyields\nStep\nobjects.\nclean.apply_outcome\nis the one function that sends a step to AWS, and a tool that reaches it fails the suite instead of shipping.\nStep 6 — Make the Total the Sum of the Rows\nAn end-to-end test runs both front doors over one account and compares them:\nscan_account\nwrites the report,\nestimate_savings\nreads it back, and the totals must match.\nTwo summation orders over the same one hundred and twenty findings:\nOrder\nMonthly\nEach finding rounded to the cent, then added\n$6.90\nExact costs added, then rounded once\n$6.92\nThe report published every finding rounded to the cent and computed its headline from the unrounded values, so the document disagreed with its own rows by two cents. An agent reading the published rows can only reach the first figure.\nThe rule that follows: a report's total is the sum of the rows it prints.\nScanResult.total_monthly_cost\nrounds each finding before adding, with the same\nround\nthat publishes it, and the per-check sections in the JSON, the terminal table and the HTML report do the same.\nresult\n.\nfindings\n=\n[\n_finding\n(\n\"\nhalf-cent\n\"\n,\nf\n\"\nr-\n{\nindex\n}\n\"\n,\n0.125\n)\nfor\nindex\nin\nrange\n(\n4\n)]\nassert\nresult\n.\ntotal_monthly_cost\n==\n0.48\nassert\nround\n(\nsum\n(\nf\n.\nmonthly_cost\nfor\nf\nin\nresult\n.\nfindings\n),\n2\n)\n==\n0.50\nEnter fullscreen mode\nExit fullscreen mode\nArithmetic on four findings priced at one half-cent: at $0.125 each they publish as $0.12 and total $0.48, where adding first and rounding once gives $0.50. The truer figure loses, because a total nobody can reproduce from the rows in front of them is worth less than one that is two cents off.\nStep 7 — Calibrate the Estimate Against the Bill\nThirty-three stale container repositories carry the whole dollar total. Cost Explorer knows what they charged.\naws ce get-cost-and-usage\n--granularity\nMONTHLY\n--metrics\nUnblendedCost UsageQuantity\n\\\n--group-by\nType\n=\nDIMENSION,Key\n=\nUSAGE_TYPE\n\\\n--filter\n'{\"And\":[{\"Dimensions\":{\"Key\":\"RECORD_TYPE\",\"Values\":[\"Usage\"]}},\n                    {\"Dimensions\":{\"Key\":\"SERVICE\",\"Values\":[\"Amazon EC2 Container Registry (ECR)\"]}}]}'\nEnter fullscreen mode\nExit fullscreen mode\nFigure\nValue\nBilled ECR storage, 1–21 September, 20 days\n19.96 GB-Month\nProjected over 30 days\n29.94 GB-Month\nAt $0.10 per GB-month\n$2.99/month\necr-stale-images\nestimate, 33 repositories\n$6.40/month\nImplied storage at the same rate\n64.0 GB\nRatio\n2.1x\nECR bills unique layers once. The check sums each image's full size, so thirty-three repositories built from the same base layer are counted thirty-three times over. Every one of those findings carries\napproximate_cost: true\nand an\napproximate_reason\nof\nshared-layers\n, and\nexplain_finding\nreturns the reason with the finding.\nAn upper bound stated as an upper bound is useful. The calibration puts a number on it for this account.\nStep 8 — Wire It Up as a Claude Code Plugin\nThe plugin is the server plus two slash commands and a skill, installed from the same repository.\n/plugin marketplace add xbill9/zombiescan\n/plugin\ninstall\nzombiescan@zombiescan\nEnter fullscreen mode\nExit fullscreen mode\n.mcp.json\nat the plugin root starts the server:\n{\n\"mcpServers\"\n:\n{\n\"zombiescan\"\n:\n{\n\"command\"\n:\n\"uv\"\n,\n\"args\"\n:\n[\n\"run\"\n,\n\"--quiet\"\n,\n\"--project\"\n,\n\"${CLAUDE_PLUGIN_ROOT}/..\"\n,\n\"zombiescan-mcp\"\n]\n}\n}\n}\nEnter fullscreen mode\nExit fullscreen mode\n${CLAUDE_PLUGIN_ROOT}\nresolves to the installed plugin directory, so the same file works for a local checkout and for an install from GitHub.\nThe skill carries the rules a client cannot infer from the schemas: scan once and keep the report path, read\nfilter_applied\nbefore quoting a zero, treat\ncomplete: false\nas an incomplete scan instead of an all-clear, and hand the operator the\nclean --apply\ncommand instead of running it.\nCompare and Contrast\nTerminal\nMCP server\nEntry point\nzombiescan scan\nscan_account\nOutput\nrich table, JSON, HTML, cleanup script\nJSON per tool call\nFindings shown\n25 by default,\n--limit 0\nfor all\ncostliest 10, full set in the report file\nTotals\nprinted\nreturned computed, with breakdowns\nFiltering\n--check\n,\n--region\n,\n--min-cost\none filter argument, echoed back\nCleanup\ndry run, then\n--apply\nplan only\nDependencies\nclick, rich, boto3\nthe same, plus nothing\nSo, Which One?\n🥇\nThe terminal\nfor the first look at an account. The table ranks and groups in one screen, and the HTML report goes in a ticket.\n🥈\nThe MCP server\nfor the follow-up questions. \"Which region holds most of it\", \"what would removing the ECR repositories save\", \"why is that zone waste\" are three tool calls against one report, with no second scan and no second set of numbers.\nBoth read the same engine, so a disagreement between them is a defect in one of the two.\nStep 9 — Run the Tests\nThe offline suite drives every check against recorded AWS API responses, so it needs no credentials.\nuv run pytest\n-q\nEnter fullscreen mode\nExit fullscreen mode\n343 passed, 3 deselected in 0.39s\nEnter fullscreen mode\nExit fullscreen mode\nThe three deselected are the live tests, which need a real account:\nZOMBIESCAN_LIVE\n=\n1 uv run pytest\n-m\nlive\nEnter fullscreen mode\nExit fullscreen mode\n3 passed, 343 deselected in 25.58s\nEnter fullscreen mode\nExit fullscreen mode\nThe third of those is the one that compares the two front doors.\nSummary\nThe goal of this article was to give a local AWS cost scanner an agent front door without giving up the numbers. The key to the solution was making every tool return figures the engine computed, and holding both front doors to one total in a live test. The results were:\n🟢 Five read-only MCP tools over stdio, JSON-RPC 2.0, standard library only\n🟢 One scan of 17 regions: 120 findings, $6.90 a month, $82.80 a year\n🟢\nestimate_savings\nanswers totals, counts, extremes and three breakdowns from one call\n🟢 A filter that matches nothing reports which of its terms the report never held\n🟢 A report's headline, its per-check sections and its rows all add to $6.90\n⚠️ Rounding each finding before adding costs two cents against the exact sum on this account\n⚠️ The ECR estimate is 2.1x the billed figure: $6.40 against $2.99 projected from 19.96 GB-Month, because shared layers are counted once per image\n❌ The server plans cleanups and performs none;\nclean --apply\nstays a terminal command\nOne AWS account, 17 enabled regions, scanned on 2026-09-21 with a price table generated the same day. Prices are on-demand USD list prices from the AWS Price List API; they ignore private pricing, savings plans and credits, so they will not match an invoice to the cent. The ECR comparison projects 20 days of billed usage to 30 and compares it against a single scan taken inside that window. The model-arithmetic figures quoted in the opening were measured on 2026-09-15 with eleven ids and one API call per run, twenty runs per cell.\nThe strategy for using MCP for AWS cost reporting was validated with an incremental step by step approach.\nReferences\nzombiescan —\nhttps://github.com/xbill9/zombiescan\nModel Context Protocol specification —\nhttps://modelcontextprotocol.io/specification/2025-06-18\nClaude Code plugins —\nhttps://docs.claude.com/en/docs/claude-code/plugins\nCost Explorer GetCostAndUsage —\nhttps://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html\nAmazon ECR pricing —\nhttps://aws.amazon.com/ecr/pricing/\nAWS Price List API, GetProducts —\nhttps://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_pricing_GetProducts.html",
  "metadata": {
    "title": "Put the Arithmetic in the Tool: an MCP Server for an AWS Waste Scanner - DEV Community",
    "description": "A cost report gets read twice: once by a person in a terminal, once by an agent through an MCP server. Making the server compute every total instead of handing back rows keeps the two answers the same, and measures the report's own arithmetic against the AWS bill. Tagged with aws, mcp, python, devops.",
    "keywords": "aws, mcp, python, devops, software, coding, development, engineering, inclusive, community",
    "author": null,
    "og:type": "article",
    "og:url": "https://dev.to/aws-builders/put-the-arithmetic-in-the-tool-an-mcp-server-for-an-aws-waste-scanner-3n79",
    "og:title": "Put the Arithmetic in the Tool: an MCP Server for an AWS Waste Scanner",
    "og:description": "A cost report gets read twice: once by a person in a terminal, once by an agent through an MCP server. Making the server compute every total instead of handing back rows keeps the two answers the same, and measures the report's own arithmetic against the AWS bill.",
    "og:site_name": "DEV Community",
    "og:image": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxbill9%2Fzombiescan%2Fmain%2Farticles%2Fzombiescan-mcp%2Fdevto-cover.cde3f396.jpg",
    "twitter:site": "@thepracticaldev",
    "twitter:creator": "@",
    "twitter:title": "Put the Arithmetic in the Tool: an MCP Server for an AWS Waste Scanner",
    "twitter:description": "A cost report gets read twice: once by a person in a terminal, once by an agent through an MCP server. Making the server compute every total instead of handing back rows keeps the two answers the same, and measures the report's own arithmetic against the AWS bill.",
    "twitter:card": "summary_large_image",
    "twitter:widgets:new-embed-design": "on",
    "twitter:image:src": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fxbill9%2Fzombiescan%2Fmain%2Farticles%2Fzombiescan-mcp%2Fdevto-cover.cde3f396.jpg"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [
    {
      "file": "attachments/0f63927bceae/code/body-000.txt",
      "chars": 47,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-001.txt",
      "chars": 402,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-002.txt",
      "chars": 133,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-003.txt",
      "chars": 435,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-004.txt",
      "chars": 101,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-005.txt",
      "chars": 465,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-006.txt",
      "chars": 69,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-007.txt",
      "chars": 289,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-008.txt",
      "chars": 231,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-009.txt",
      "chars": 197,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-010.txt",
      "chars": 315,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-011.txt",
      "chars": 80,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-012.txt",
      "chars": 167,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-013.txt",
      "chars": 17,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-014.txt",
      "chars": 34,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-015.txt",
      "chars": 40,
      "language": "text"
    },
    {
      "file": "attachments/0f63927bceae/code/body-016.txt",
      "chars": 35,
      "language": "text"
    }
  ],
  "images": [],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://github.com/xbill9/zombiescan",
      "label": "https://github.com/xbill9/zombiescan",
      "type": "a"
    },
    {
      "href": "https://modelcontextprotocol.io/specification/2025-06-18",
      "label": "https://modelcontextprotocol.io/specification/2025-06-18",
      "type": "a"
    },
    {
      "href": "https://docs.claude.com/en/docs/claude-code/plugins",
      "label": "https://docs.claude.com/en/docs/claude-code/plugins",
      "type": "a"
    },
    {
      "href": "https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html",
      "label": "https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetCostAndUsage.html",
      "type": "a"
    },
    {
      "href": "https://aws.amazon.com/ecr/pricing/",
      "label": "https://aws.amazon.com/ecr/pricing/",
      "type": "a"
    },
    {
      "href": "https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_pricing_GetProducts.html",
      "label": "https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_pricing_GetProducts.html",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:48:28Z",
  "source_url": "https://dev.to/aws-builders/put-the-arithmetic-in-the-tool-an-mcp-server-for-an-aws-waste-scanner-3n79",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 13950,
  "expected_body_images": 0,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/0f63927bceae/pages/source.html",
  "local_html": "attachments/0f63927bceae/pages/article.html",
  "body_base_path": "attachments/0f63927bceae/pages/",
  "body_markdown_path": "attachments/0f63927bceae/pages/article.md"
}