Teaching an LLM to use Acumatica

Building an MCP server so Claude can answer real questions against a live Acumatica tenant — a build log, and why purpose-built tools beat SQL-over-API.

Teaching an LLM to use Acumatica

Most questions people ask of an ERP don't need a report. They need an answer. "Do we have stock of ABC-1234?" "Where's SO-001234 up to?" "What's our balance with ACME?" You shouldn't need to remember which screen, which filter, or which Generic Inquiry to run. You should just be able to ask.

For the past few weeks we've been building exactly that at Simple Progress — an MCP server that lets Claude (or any MCP-compatible client) reach into a live Acumatica tenant and answer in plain English.

What this actually does

Model Context Protocol is Anthropic's attempt to standardise how large language models talk to the outside world. You define a set of tools — functions with typed inputs and a JSON response — and the model decides when to call them. No prompt stuffing, no fragile scraping, no vendor lock-in on the chat side. Any MCP-compatible client (Claude Desktop, Cursor, your own agent framework) can use the same server.

The connector ships 28 tools, but most of them you never need to think about. In practice, it handles things like:

  • "What open sales orders do we have?"
  • "Check inventory for ABC-1234 across all warehouses."
  • "Show me AP bills from this month."
  • "Release SO-001234 from hold."
  • "Run the Aged AR report as a PDF."

Under the hood, each resolves to a tool call. Here's what one looks like from the LLM's side:

search_records(endpoint="Customer", search_text="shop", top=5)
→ [
    { "CustomerID": "COFFEESHOP",
      "CustomerName": "FourStar Coffee & Sweets Shop",
      "Status": "Active",
      "matched_on": "CustomerName" },
    ...
  ]

The LLM never sees the OData filter syntax, the OAuth refresh flow, or the server-side retries. It sees a function that takes plain arguments and returns usable rows. When a Generic Inquiry is the right answer, it runs that instead. When the ask is a workflow step — Release, Confirm, Cancel — it invokes the action and polls until it's done.

The bit that was actually hard

Two things ate most of the build time: tenant discovery and Generic Inquiries.

Every Acumatica tenant is a bit different. Different endpoints, different versions, different field names. Hardcoding Default/24.200.001 everywhere and hoping falls apart the first time you point the server at a tenant running a custom endpoint. So the connector probes the tenant once on first use and caches what it learns for 24 hours. That includes which invoice entity exists (ARInvoice, Invoice, or SalesInvoice — yes, it varies) and where the OData GI path lives.

Then the testing. We wrote 118 unit tests. All green. Then we pointed the server at a real tenant and six tools broke immediately. Date comparisons needed datetimeoffset'...' literal wrapping. Numeric filters failed if the value was quoted as a string. One tool asked for a $select field that doesn't exist on every version of the Customer DAC — so every customer lookup returned 500: "The given key was not present in the dictionary". Which is Acumatica's way of saying you asked for something I don't have, and I'd rather not tell you what.

Generic Inquiries were the bigger scar. Queried via the OData _WithParameters() suffix, any non-trivial GI would run for sixty seconds and then time out. The fix: expose the GI as an entity on a Web Service Endpoint in SM207060, then query it via the Contract-Based API. Parameters go in the request body rather than a URL suffix, and Acumatica's query planner stops having an existential crisis.

PUT /entity/MCPSuite/24.200.001/ARAgingDetail?$expand=Result
Body: { "AsOfDate": { "value": "2026-04-19" } }

Same GI, same data — sub-second response instead of a timeout. None of that is documented anywhere we could find.

The moral, which we keep re-learning: mocked tests validate your code. Only real calls validate the contract.

Why this shape matters

There's a parallel product — CData's Acumatica MCP server — that takes the other route. Wrap the thing in a JDBC driver, expose three tools (get_tables, get_columns, run_query), and let the LLM write SQL. It works, and for some use cases it's the right answer. But it has the same problem every SQL-over-API layer has had for twenty years: the abstraction leaks. The moment you need a parameterised inquiry, an action invocation, a file attachment, or anything that isn't a SELECT, you're back to writing glue.

Purpose-built tools beat generic SQL translation for the same reason a well-designed Generic Inquiry beats a free-text query box. The shape of the tool teaches the model what's sensible to ask. check_inventory(inventory_id) tells the LLM something a run_query tool cannot: this is the canonical way to answer that question on this system. If you've already built a good GI, you've done ninety per cent of the work to make it AI-accessible.

What this is not

Not a replacement for Power BI or a proper data warehouse. If you want trend analysis, governed metrics, or period-over-period variance across a dozen dimensions, use the right tool — this isn't it.

Not going to rescue a broken implementation either. If your data is a mess, Claude will tell you your data is a mess, in plain English. That's sometimes useful and sometimes uncomfortable.

What it is good for: quick answers, first-draft investigations, natural-language access for people who don't live in Acumatica all day, and small automations where a human would otherwise be clicking through screens to get a number.

Where it goes next

The next piece of work is a deployable package of eleven pre-built Generic Inquiries — AR ageing, AP ageing, trial balance, cash position, revenue by customer, margin analysis, open orders, shipment status and the rest — plus a single Web Service Endpoint that exposes them all. Install the .zip, point the connector at it, and a tenant goes from AI-curious to AI-ready without writing a line of custom code.

The MCP server is the plumbing. Good GIs are the taps. Neither is visible to the person asking the question, which is exactly as it should be.

Not public yet

The connector isn't open yet. It's running against a handful of tenants while we shake the last bugs out and finish the GI pack — partly because the things you find in UAT round two are always more embarrassing than the things you find in round one, and partly because we want the first public version to feel finished rather than announced.

If you've got an Acumatica tenant, a tolerance for rough edges, and a use case you'd like to throw at it, we're happy to have a conversation. That's either exciting or slightly unnerving, depending on your relationship with your ERP.

Read More

8 Things We Like in Acumatica 2026 R1

8 Things We Like in Acumatica 2026 R1

Acumatica 2026 R1 just dropped with 460 pages of release notes. If you're a manufacturer or distributor running Acumatica — or thinking about it — this is the breakdown that tells you what actually matters and what you can safely ignore.

5 Things to Do Before You Connect Your E-Commerce Site to an ERP

5 Things to Do Before You Connect Your E-Commerce Site to an ERP

More and more e-commerce businesses are reaching the point where their patchwork of tools that simply can't keep up with growth. Integrating with an ERP is the natural next step. But there's a lot of groundwork that needs to happen before you flip that switch.

5 Reasons Your ERP Project Falls Apart Before Go-Live

Companies spend months selecting the right ERP. They go through procurement, get board approval, sign the contract, and everyone gets excited. Then, three months into the project, the wheels start to come off. Not because the software was wrong. Not because the consultants were bad. Because nobody had a proper