← Compendium

Talking to your own numbers

Ask an LLM about your own business figures and it will happily guess, and make up a very plausible story to go with it. The fix is not to throw more data at the model, but to take the arithmetic off its hands: a database delivers the exact figure, the model the language around it. That keeps the raw data confidential and the answers deterministic, and turns rigid dashboards into a conversation with your own numbers.

How it all began

It started with a simple question: how do we make the AWS cost data available so that people really work with it day to day? The AWS Cost Explorer is quite decent for this, but it relies on you actively looking and clicking your way through. Cost alerts matter too, but they work more as an incident trigger that goes off when something gets out of hand, and less as a tool you think with every day.

The modern working world, on the other hand, takes place to a large extent in the LLM chat, so it sounds tempting to have your own cost data available right there, because then you can simply ask instead of opening yet another tool. It makes no sense, however, to let an LLM trawl through several megabytes or even gigabytes of structured data, because that needlessly eats tokens, gets slow and leads to the model merely estimating, although you need exact numbers.

So it was obvious to turn the tables: rather than tipping the raw data into the model, build an MCP server with deterministic functions that accesses the billing data via DuckDB. The LLM phrases the question and provides the context, while the actual calculation happens in the database, so the answer stays fluent in language and exact in numbers, without needing a dashboard or a heavy data platform. The rough target picture was quickly sketched: AWS Data Export writes the billing data to S3, DuckDB reads it from there, an MCP server makes it queryable, and a few generic tools are enough for the LLM to get started.

Rough target picture at the start AWS Data Export FOCUS 1.2, Parquet S3 bucket storage DuckDB + MCP reads Parquet, makes it queryable list_tables query LLM uses the tools
The first, still rough sketch of the goal: AWS Data Export → S3 → DuckDB + MCP with a few generic tools → LLM. Deliberately lean; auth, multiple sources and polish only came in the later phases.

Concretely, you create a data export in AWS for this, choose the FOCUS 1.2 format, Parquet as the file format and a daily export written to S3. That lays the foundation: a clean, standardised cost base on which everything else rests.

The result of this first phase is the following architecture, which deliberately stays lean and does without exotic building blocks, because that is exactly what makes it sustainable:

AWS Data Export automatic, FOCUS 1.2 Parquet writes S3 bucket Parquet storage queries runs in ECS DuckDB MCP server uses Cognito auth login, optional Entra ID group permissions checks asks LLM of choice Claude, Kiro, … Employee chats, asks
AWS Data Export writes FOCUS 1.2 Parquet to S3 (grey). The query runs the other way (green): the employee uses their LLM, the LLM queries the MCP server, which checks Cognito auth on the side (optional Entra ID, group permissions) and uses DuckDB, which queries the data in the S3 bucket.

With that, the cost base is in place. It is the beginning of a development that grows in further phases into a reliable view across products.

Working with the first numbers

Once the base was in place, the first thing was to work with the new insights. The questions were the usual ones you had before in the Cost Explorer: costs per service and region, changes over time, and how consistently things are actually tagged. Right here, something quickly became visible that nobody had noticed before.

Although we had supposedly clean coverage through infrastructure as code, the tag values contained typos, and various resources did not meet the tagging standards. The first step was therefore to rework and clean up these things, so the analyses would stand on a reliable foundation at all.

At this point it also made sense to extend the MCP server with alias logic, so that the historical data remains comparable and the various typo values are merged into a single value. Because tagging changes in AWS do not apply retroactively, this solution was surprisingly pleasant, and it felt good to know that the data base gets a little better with every step.

Since one of the tags named the respective product, the question of which products actually exist arose almost by itself. At first this turned directly into another MCP tool that derives the products from the tags. We dropped this idea again, though, because another route carried much further.

Instead of guessing the products from the tags, we created a products.json of our own that defines the relevant details for every SaaS product operated on AWS. It states which tag values belong to a product, including known typos, along with a description, the lifecycle and the monthly budget. That makes the assignment explicit and traceable, instead of letting it disappear implicitly in the tag muddle.

json
"invoice-radar": {
  "tag_values": ["invoice-radar", "invoice-radr"],
  "description": "Invoice Radar SaaS",
  "lifecycle": "production",
  "budget_monthly": 35
}
A product in products.json: besides the correct value, the tag_values also catch the historical typo “invoice-radr”, so old and new data are merged. Budget and lifecycle give the bare number some context.

After that, it was back to working with the data and seeing what else it revealed. The first skills quickly emerged, such as a cost report and anomaly detection, so you could see the same report repeatedly without putting it together from scratch each time. That was practical, but it felt like a cheap copy of a dashboard, and that very feeling was the reason to move on to the next stage.

Setting expectations: KPIs

What is often misunderstood about KPIs is their nature: a KPI is always a range, never a single concrete number. Only once it is clear which value is expected, from when it becomes risky and from when an opportunity opens up does a metric gain a meaning you can align decisions with. How to set up KPIs properly is covered in a separate article.

So we extended the MCP server with KPI logic and defined for each product where the expectations lie. A KPI consists of a deterministic query and a target corridor with thresholds for risk and opportunity, and because expectations change over time, it is versioned as well.

json
"cost_per_product_invoice_radar": {
  "name": "Invoice Radar Monthly Cost",
  "description": "Projected monthly cost for the Invoice Radar product",
  "sql": "SELECT ROUND(SUM(BilledCost) / COUNT(DISTINCT CAST(ChargePeriodStart AS DATE)) * 30, 2) as value FROM data WHERE Tags->>'billing' = 'invoice-radar' AND ChargeCategory != 'Tax' AND BilledCost > 0",
  "unit": "USD/month",
  "category": "budget",
  "versions": {
    "2026-06": {
      "target_operator": "<=",
      "target_value": 35,
      "risk_value": 50,
      "opportunity_value": 15,
      "reason": "Initial: ECS+CloudFront+Route53 baseline ~$18"
    }
  }
}
A KPI is a corridor, not a point: target value, risk threshold and opportunity threshold span the range. The query is deterministic, and the versioning records which expectation applied from when and why.

Anchored like this, the MCP server no longer just delivers numbers, but an assessment: it can say whether a product is within the expected corridor, is approaching a risk or shows an opportunity. That completed the step from a mere report to a statement that can be evaluated.

So that the LLM not only has to trust these numbers but actually can, the MCP server includes a provenance block with every answer. It describes where the insights come from and makes their origin verifiable instead of keeping it hidden.

The block records which source the data comes from, which period it covers and how recent the latest data point is. It names the filters applied and the projection method, so a projected monthly figure becomes traceable. In addition, it forms a short hash over the config files involved, that is the product catalogue and the KPI definitions, which lets you detect changes to the contracts between two runs. If the latest data point is too old, it adds a warning.

json
"provenance": {
  "source": { "app": "aws-focus", "bucket": "…", "prefix": "…", "format": "parquet" },
  "period_start": "2026-08-01",
  "period_end": "2026-08-31",
  "latest_data_date": "2026-08-31",
  "filters": ["ChargeCategory != 'Tax'", "BilledCost > 0"],
  "projection_method": "linear_monthly_projection_30_days",
  "contract_version": "local",
  "contract_hash": "a1b2c3d4e5f6a7b8",
  "contract_files": ["products.json", "kpis.json"],
  "warnings": ["Latest source data is 5 days old."]
}
The provenance block that the MCP server attaches to every answer: source, period and freshness of the data, the filters applied and the projection method, a hash over the config files to detect drift, and warnings for outdated data.

The practical benefit is obvious: the LLM client can check where the data comes from, recognise outdated results through the warnings and the latest date, and state cleanly in every analysis that the numbers were obtained via the analytics MCP. An answer you have to believe becomes an answer you can check.

Connecting more sources: GSC and GitLab

The products do not only live in the AWS bill, but to a large extent on the web too, which is why the next step was to connect Google Search Console. A data export to S3 as with the billing data made no sense here, because the API requests are free and can be queried directly. So we extended the MCP server with GSC data instead of building another detour via storage.

Suddenly something became possible that was not possible before: putting Google Search metrics and cost metrics for the same product side by side and crossing them. The question of whether a product justifies its costs with corresponding visibility became answerable, because both sides come from the same base.

At this point an earlier decision paid off again. The AWS billing data is available with a delay of at least one day, the GSC data more like three days. This latency is part of the config and therefore part of the provenance too, so the LLM can easily place missing values for the most recent days instead of mistaking them for a sudden slump.

With the Search Console connection working so well, it was natural to include GitLab too. That made visible which product repositories are particularly active, which merge requests are left lying around and how quickly they are processed at all. It also makes it possible to track whether there are bug reports and when certain things were fixed.

The real appeal again comes from connecting them. When a bug has been fixed or a feature shipped, you can ask whether this is reflected in the GSC numbers or changes the costs. The three sources, cost, visibility and development activity, thus turn into a coherent picture of a product instead of three separate views.

Closing the loop: product usage itself

Which brings us to the next phase: understanding our own SaaS products better. Cost, visibility and development activity say a lot, but they say nothing about what happens inside the products themselves. We were interested in which features are used how often and what expectations there are regarding how frequently the product and its individual important features are used.

That is why we extended the products with an anonymised statistics export that stores its data in an S3 bucket, which in turn was made queryable via DuckDB and the MCP server. Here, unlike with the Search Console, the route via S3 made perfect sense, because the usage data comes from the applications themselves and is written there programmatically.

That closes the loop, and you have a 360-degree view of a product: what it costs, how visible it is, how actively it is being developed and how it is actually used. All four perspectives come from the same queryable base and can be crossed at will.

And what does that lead to? It influences decisions. Which features get adjusted or newly built, is a new blog post needed, are the costs under control, does something need to be done about profitability. A collection of numbers becomes a foundation on which concrete steps can be based.

All of this happens in natural language, where you rarely ask the same question twice, but build on what you already know, have found out or derived yourself. It is not a static dashboard that always shows the same tiles, but a living data base you have a conversation with. And because this data is not vanity metrics but numbers that influence decisions, the route there is worth it.

What this means for you

The journey began with a single source, the AWS costs, and grew phase by phase into a 360-degree view that brings together the costs, visibility, development activity and actual usage of a product. What mattered was not a special tool, but the order: first a clean, standardised base, then cleaning up and merging the data, then expectations in the form of KPIs, and only on top of that further sources.

For you this means that the first step is smaller than it looks. You do not need a heavy data platform or yet another dashboard, but a reliable data base that an LLM queries through deterministic functions, so the answers stay fluent in language and exact in numbers. Because every answer carries its provenance, you do not just have to believe the numbers, you can check them.

The real difference shows in everyday work. Instead of static tiles, you have a conversation with your own data, in which you rarely ask the same question twice but build on what you already know. And because this is not about vanity metrics but about numbers that carry decisions, it changes how decisions are made in the company. Whether costs are an acute fire right now or it is about the fundamental direction, we are happy to sort it out together with you.

Frequently asked questions

Does the LLM see my confidential raw data?

No. The MCP server accesses the data and does the calculations; the language model only gets to see the aggregated results, such as a sum or a metric, not the underlying individual records.

Why does the LLM otherwise guess when asked about numbers?

If you throw large amounts of structured data at a language model, it estimates instead of calculating exactly, and phrases the result convincingly. That is why, in our set-up, DuckDB does the actual calculation, so the number comes about deterministically and traceably.

Do I need a heavy data platform for this?

No. The approach deliberately makes do with lean building blocks: a data export to S3, DuckDB and an MCP server. No data warehouse, no additional dashboard product.

How can I tell whether an answer is based on current data?

Every answer carries a provenance block that names the source, period and freshness of the data and warns about outdated states. That makes it possible to check an analysis instead of trusting it blindly.

Does this only work with AWS?

The starting point was AWS cost data in the FOCUS format, but AWS, GCP, Azure and OCI all support this format by now. Further sources such as Google Search Console or your own usage data can be connected via the same mechanism as well.

Related topics