MCP4Acumatica
by NologyAcu
README.md
# MCP4Acumatica
> **Disclaimer:** This project is an independent, community-built integration and is **not affiliated with, endorsed by, or supported by Acumatica, Inc.** "Acumatica" is a registered trademark of Acumatica, Inc. Use of the Acumatica name and API is for interoperability purposes only.
A remote [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that connects Claude to [Acumatica ERP](https://www.acumatica.com) 2025 R2. Runs on Cloudflare Workers with per-user OAuth authentication against your Acumatica instance.
Each user authenticates with their own Acumatica credentials. Their Acumatica role controls which records they can access. The MCP server additionally requires a specific Acumatica role for access, shows a consent interstitial, and automatically redacts sensitive fields before data reaches the AI model.
## Features
- **49 tools** -- 38 read-only lookups + 6 utility/discovery + 4 schema-knowledge + 1 write tool (Customer create/update, disabled by default) (see [Available Tools](#available-tools))
- **Per-user OAuth** -- users log in with their Acumatica credentials (or SSO)
- **Role-based access** -- Acumatica's security model governs what each user sees
- **Access gate** -- only users who can read a designated canary Generic Inquiry can connect (restrict it however you like; a marker role such as `MCP Access` is the recommended way)
- **Consent interstitial** -- users must acknowledge AI data processing before accessing tools
- **Sensitive field redaction** -- SSN, bank accounts, salary, and other PII fields are automatically redacted before data leaves the server
- **Rate limiting** -- 3 concurrent requests and 40 requests/minute per user by default, both adjustable from the admin console. A request that finds all slots busy waits briefly for one instead of failing outright, and a rejection returns a structured `{ error: "rate_limited", retryAfterSeconds, actionRequired }` envelope telling the AI exactly how long to wait rather than to retry in a loop
- **Pagination refusal** -- list/query tools return a structured `{ truncated, paginationSupported: false, actionRequired }` envelope when results hit the record cap, instructing the AI to ask the user for a narrower filter rather than calling the tool again
- **Structured audit logging** -- all tool invocations, auth events, and field redactions are logged
- **Admin console** -- web-based admin interface at `/docs/admin` for viewing logs and managing runtime settings without redeploying
- **Long-term log retention** -- R2-backed log storage via Cloudflare Logpush with searchable log viewer
## Architecture
```
Claude (claude.ai / Desktop / API)
|
v MCP over streamable-http
+----------------------------------+
| Cloudflare Worker |
| OAuth 2.1 Provider |
| /authorize -> Acumatica login |
| /callback <- Acumatica |
| (access gate + OIDC userinfo) |
| /consent -> AI data consent |
| /token, /register (DCR) |
| /mcp -> McpAgent DO (49 tools)|
+---------------+------------------+
| Bearer token (per-user)
v
Acumatica 25R2 SaaS
Contract-Based REST API
Default/25.200.001
```
## Prerequisites
- [Node.js](https://nodejs.org) >= 18
- A [Cloudflare](https://cloudflare.com) account (Workers paid plan for Durable Objects)
- An Acumatica 2025 R2 instance with:
- A **Connected Application** configured in SM303010 with the **Authorization Code** OAuth 2.0 flow (scopes are sent by the server in the request, not configured on the app)
- A redirect URI pointing to your worker's `/callback` endpoint
- An **`MCPAccess` Generic Inquiry** (SM208000) -- a trivial canary GI with **Expose via OData** enabled; the login access gate checks whether the user can read it (see [Architecture docs](docs/architecture.md) for details). The GI name is configurable via `ACUMATICA_CANARY_GI`.
- A way to restrict who can read that GI -- a marker **`MCP Access` role** (SM201005) assigned only to permitted users is the recommended approach
## Setup
There are three install paths. All three rely on the same Acumatica-side prerequisites — finish those first (see "[Acumatica-side configuration](#acumatica-side-configuration)" below) regardless of which path you pick.
| Path | Best for | Terminal needed? |
|------|----------|------------------|
| **A. Deploy to Cloudflare button** | Adopters who want a fully GUI install | No |
| **B. One-line installer** | Developers who already have `git` / `node` / `npm` | Yes (one command) |
| **C. Manual setup** | Anyone who wants to inspect each step | Yes |
### Path A — Deploy to Cloudflare button (no terminal)
[](https://deploy.workers.cloudflare.com/?url=https://github.com/hallboys/MCP4Acumatica)
The button forks this repo to your GitHub account, reads `wrangler.jsonc`, auto-creates the KV namespace and R2 bucket, prompts for secrets, and deploys. Step-by-step:
1. **Click the button.** Cloudflare will ask you to sign in (or create an account) and authorize a GitHub fork.
2. **Confirm bindings.** You'll be prompted to create KV namespaces **twice** — once for the `TOKEN_STORE` binding (app data: tokens, OAuth state, cache, config, admin sessions) and once for `OAUTH_KV` (used internally by the OAuth library). This is expected: they're two separate bindings and never share keys.
> ⚠️ **Give the two namespaces _different_ names** (e.g. `mcp4acumatica-app` for `TOKEN_STORE` and `mcp4acumatica-oauth` for `OAUTH_KV`). Cloudflare auto-provisioning derives the default title from the Worker name, so **both fields default to `mcp4acumatica` — and creating two namespaces with the same title fails** with *"Cannot provision a KV Namespace with the title … because it already exists."* If you already hit that error, a half-finished namespace was left behind: go to **Storage & Databases → KV** and delete the orphaned `mcp4acumatica` namespace, then retry with two distinct names. (Cloudflare's GUI auto-provisioning can't point both bindings at one namespace, and config can't pre-set distinct titles — so two separate namespaces with distinct names is the way. If the GUI keeps failing, use a terminal install path below: `setup.sh` creates one namespace and binds both to it.)
The R2 buckets (`mcp4acumatica-logs`, `mcp4acumatica-index`) are created the same way, but their names are fixed in `wrangler.jsonc` so they don't collide.
3. **Set secrets.** When prompted, paste:
- `ACUMATICA_CLIENT_ID` — from your Connected Application (SM303010)
- `ACUMATICA_CLIENT_SECRET` — from the same screen
- `COOKIE_ENCRYPTION_KEY` — open your browser console on any page and run:
```js
[...crypto.getRandomValues(new Uint8Array(32))].map(b => b.toString(16).padStart(2,'0')).join('')
```
Copy the resulting 64-character hex string.
- `ADMIN_SECRET` — any password you'll remember (protects the `/docs/admin` console). Generate one with `[...crypto.getRandomValues(new Uint8Array(24))].map(b => b.toString(16).padStart(2,'0')).join('')` if you don't have a preference.
4. **Deploy.** Cloudflare connects the fork to Workers Builds and pushes the first deployment.
5. **Update the Acumatica vars.** After the deploy completes, open `Workers & Pages → mcp4acumatica → Settings → Variables and Secrets` in the Cloudflare dashboard and edit:
- `ACUMATICA_URL` (e.g. `https://yourcompany.acumatica.com`)
- `ACUMATICA_TENANT` (your login company)
- Optionally `ACUMATICA_MAX_RECORDS`, `ACUMATICA_CANARY_GI`, `REDACT_PATTERNS`, `REDACT_SKIP`
Click **Save and Deploy** — Cloudflare redeploys with the new values.
6. **Add a redirect URI to your Connected Application.** Your worker is now reachable at `https://mcp4acumatica.<your-account>.workers.dev`. Add `https://<that-host>/callback` to the redirect URIs in Acumatica's SM303010 screen. (To use a custom domain instead, see "[Custom domain](#custom-domain-optional)" below.)
7. **Test the deploy.** Visit `https://<your-host>/docs/admin/preflight`, log in with your `ADMIN_SECRET`, and run the preflight diagnostic. It probes Acumatica connectivity, the OIDC discovery endpoint, the Connected App credentials, the tenant path, and the contract API version — any misconfiguration is called out by name.
After this point Claude can connect (see "[Connecting Claude](#connecting-claude)" below).
### Path B — One-line installer (terminal)
If you already have `git`, `node`, and `npm`, run:
```bash
curl -fsSL https://mcp4acumatica.hallboys.com/install.sh | bash
```
This clones the repo, installs dependencies, and runs `./setup.sh`. The setup script prompts for the Acumatica values you must supply (URL, tenant, Connected App client ID and secret), auto-generates the crypto secrets, creates the KV namespace and R2 bucket, uploads secrets, deploys, and then runs the preflight check.
If you prefer to inspect the script first:
```bash
curl -fsSL https://mcp4acumatica.hallboys.com/install.sh -o install.sh
less install.sh # read it
bash install.sh # then run
```
### Path C — Manual setup (terminal)
#### 1. Clone and install
```bash
git clone https://github.com/hallboys/MCP4Acumatica.git
cd MCP4Acumatica
npm install
```
#### 2. Create KV namespace
```bash
npx wrangler kv namespace create TOKEN_STORE
```
Note the namespace ID from the output — you'll paste it into `wrangler.jsonc` next. The same ID is used for both the `TOKEN_STORE` and `OAUTH_KV` bindings.
#### 3. Configure wrangler
`wrangler.jsonc` is tracked in the repo as the deploy template. Edit it in place and fill in:
- The KV namespace ID from step 2 (both `TOKEN_STORE` and `OAUTH_KV` bindings — same id)
- `ACUMATICA_URL` — your Acumatica instance URL (e.g. `https://yourcompany.acumatica.com`)
- `ACUMATICA_TENANT` — your Acumatica company/tenant name
To keep your local values out of `git status` (so you can still pull updates without conflicts):
```bash
git update-index --skip-worktree wrangler.jsonc
```
#### 4. Set secrets
```bash
npx wrangler secret put ACUMATICA_CLIENT_ID
npx wrangler secret put ACUMATICA_CLIENT_SECRET
npx wrangler secret put COOKIE_ENCRYPTION_KEY # use `openssl rand -hex 32`
npx wrangler secret put ADMIN_SECRET # any password — protects /docs/admin
```
#### 5. Deploy
```bash
npx wrangler deploy
```
#### 6. Local development (optional)
```bash
cp .dev.vars.example .dev.vars
# Edit .dev.vars with your Acumatica credentials
npx wrangler dev
```
### Acumatica-side configuration
These steps are required regardless of which install path you pick. They can't be automated — Acumatica's API doesn't expose them.
#### Connected Application (SM303010)
1. In Acumatica: **System > Integration > Connected Applications (SM303010)**.
2. Create a new Connected Application.
3. Set the **OAuth 2.0 Flow** to **Authorization Code**.
4. Add a redirect URI: `https://<your-worker-url>/callback` (use the `*.workers.dev` hostname or your custom domain).
5. Note the **Client ID** and **Client Secret** — you'll provide these as secrets during deploy.
> There is no scope field to configure here. OAuth scopes (`api openid profile email offline_access`, including the `offline_access` that makes Acumatica issue refresh tokens) are sent by the MCP server in the authorization request — they aren't set on the Connected Application.
#### Access gate: canary Generic Inquiry (SM208000, SM201005)
Before a user can access the AI tools, the login flow runs an **access gate**: it queries a trivial canary Generic Inquiry over OData and checks whether the user's token can read it (200 → allowed, 403 → denied). The server never inspects Acumatica role membership — it only asks "can you see this one GI?". You restrict who can read the canary GI however your security model prefers; a marker role is the recommended, tidiest way.
1. **Create the canary GI:** **System > Customization > Generic Inquiry (SM208000)** → create a GI named `MCPAccess` with any trivial query (a single column from any table is fine). Enable **Expose via OData**.
2. **Restrict who can read it (recommended: a marker role):** **System > Access Rights > User Roles (SM201005)** → create a role named `MCP Access` with no screen permissions, assign the `MCPAccess` GI only to that role, then assign the role to each user who should have AI assistant access. Any other mechanism that controls OData read access to the GI works too.
> The canary GI name is configurable via the `ACUMATICA_CANARY_GI` variable (default `MCPAccess`). Edit it in the Cloudflare dashboard (`Variables and Secrets`) or in `wrangler.jsonc`.
#### Generic Inquiry exposure to AI (strongly recommended)
A mature Acumatica instance can have **hundreds** of Generic Inquiries, most built for human screens (wide report grids, dashboards, ad-hoc queries). Exposing all of them to the assistant floods its context and makes it pick the wrong inquiry — and, worse, a **parameterized GI exposed via OData returns silently wrong data**: queried without its parameters, Acumatica returns default/unfiltered rows with **no error**, which the model cannot detect. The **GI exposure gate** flips this to opt-in: you tag the GIs that are genuinely useful *and correct* for an AI agent to query (`ExposedToMCP`), and the model sees only those.
The gate is **inactive until you configure it** — the server runs, but with no registry the assistant **cannot discover GIs** (`acumatica_list_generic_inquiries` returns nothing; a user can still run a GI by exact name). Configuring it gives the assistant a curated set it can safely discover. Enabling it is a one-time Acumatica **customization project** — bundled in [`acumatica/`](acumatica/), it adds the custom fields `UsrExposedToMCP` / `UsrAIDescription` (`GIDesign`) and `UsrResAIDescription` (`GIResult`) plus the SM208000 form changes — followed by the `MCPGIs` / `MCPGIFields` feed GIs, read access on the feeds for the `MCP Access` role, and tagging the GIs you want exposed. See [docs/generic-inquiries.md](docs/generic-inquiries.md).
> See **[Generic Inquiries](docs/generic-inquiries.md)** for the full rationale, how to decide which GIs to expose, and step-by-step setup.
### Custom domain (optional)
The deploy gives you a `*.workers.dev` hostname out of the box. To attach a branded hostname:
- **Via the Cloudflare dashboard:** `Workers & Pages → mcp4acumatica → Settings → Domains & Routes → Add`. The domain's zone must be on your Cloudflare account.
- **Via `wrangler.jsonc`:** uncomment the `routes` block at the top of the file, edit `pattern` and `zone_name`, redeploy.
If you change hostnames, remember to add the new `https://<host>/callback` to your Connected Application's redirect URIs in SM303010.
## Connecting Claude
### Claude.ai / Claude Desktop
1. Go to **Settings > Connectors**
2. Click **Add Connector** and enter the URL: `https://<your-worker-url>/mcp`
3. On first use, you'll be redirected to your Acumatica login page
4. If your account can read the canary GI (i.e. you've been granted access), you'll see a consent page explaining AI data processing
5. After acknowledging consent, Claude will have access to all 49 tools
### Claude Code (CLI)
```bash
claude mcp add acumatica-erp --transport streamable-http https://<your-worker-url>/mcp
```
### API (via Anthropic SDK)
When using the Anthropic API with MCP, point the MCP client to `https://<your-worker-url>/mcp`. The server supports OAuth 2.1 with Dynamic Client Registration at `/register`.
## Available Tools
### Core
| Tool | Description |
|------|-------------|
| `acumatica_get_customer` | Customer record with contacts, credit rules, balance |
| `acumatica_get_vendor` | Vendor record with contacts, terms, tax info |
| `acumatica_get_sales_order` | Sales order with line items, totals, shipping |
### Financial / Accounting
| Tool | Description |
|------|-------------|
| `acumatica_get_invoice` | AR invoice with line items and tax details |
| `acumatica_get_bill` | AP bill with line items and PO linkage |
| `acumatica_get_journal_transaction` | GL journal batch with debit/credit details |
| `acumatica_get_payment` | AR payment with applied documents and orders |
| `acumatica_get_account` | GL chart of accounts lookup |
| `acumatica_get_check` | AP check/vendor payment with history |
### Inventory & Warehouse
| Tool | Description |
|------|-------------|
| `acumatica_get_stock_item` | Stock item with pricing, warehouse qty, vendors |
| `acumatica_get_non_stock_item` | Non-stock item (service, labor, expense) |
| `acumatica_get_inventory_quantity_available` | Real-time available quantity across warehouses |
| `acumatica_get_inventory_summary` | Aggregated inventory balances by warehouse |
| `acumatica_get_warehouse` | Warehouse with locations and settings |
| `acumatica_get_item_class` | Item classification defaults |
### Purchasing
| Tool | Description |
|------|-------------|
| `acumatica_get_purchase_order` | PO with line items, vendor, totals |
| `acumatica_get_purchase_receipt` | Receipt with received qty and PO linkage |
### Projects
| Tool | Description |
|------|-------------|
| `acumatica_get_project` | Project header, status, financials |
| `acumatica_get_project_task` | Task within a project |
| `acumatica_get_project_budget` | Budget line with actuals vs budgeted |
| `acumatica_get_project_transaction` | Project cost/revenue transaction details |
### Service & Field
| Tool | Description |
|------|-------------|
| `acumatica_get_case` | Support case with SLA, priority, time tracking |
| `acumatica_get_service_order` | Field service order with details and appointments |
| `acumatica_get_appointment` | Scheduled/actual times, staff, cost/profit |
### Sales & CRM
| Tool | Description |
|------|-------------|
| `acumatica_get_contact` | CRM contact with address, phone, owner |
| `acumatica_get_business_account` | Unified prospect/customer/vendor record |
| `acumatica_get_opportunity` | Sales pipeline deal with products and amounts |
| `acumatica_get_lead` | Marketing lead with status and source |
| `acumatica_get_salesperson` | Sales rep with commission settings |
### Shipping & Fulfillment
| Tool | Description |
|------|-------------|
| `acumatica_get_shipment` | Shipment with packages, tracking, freight |
| `acumatica_get_sales_invoice` | Invoice with SO/shipment linkage |
### HR & Payroll
| Tool | Description |
|------|-------------|
| `acumatica_get_employee` | Employee with contact and financial settings |
| `acumatica_get_expense_claim` | Expense report with line items and approval |
| `acumatica_get_time_entry` | Time tracking with project, billable/overtime |
### CRM Activities
| Tool | Description |
|------|-------------|
| `acumatica_get_email` | Email activity with from/to/body |
| `acumatica_get_event` | Calendar event with attendees |
| `acumatica_get_activity` | General CRM activity |
| `acumatica_get_task` | CRM task with related activities |
### Utility / Discovery
| Tool | Description |
|------|-------------|
| `acumatica_run_inquiry` | Execute any configured Generic Inquiry (GI) with filtering |
| `acumatica_list_entities` | List/search any entity with OData filtering, sorting, field selection |
| `acumatica_describe_entity` | Discover fields, types, and sub-entities for any entity |
| `acumatica_list_generic_inquiries` | List available GIs exposed via OData |
| `acumatica_describe_inquiry` | Infer field schema for a GI before running it |
| `acumatica_clear_cache` | Clear cached metadata when schemas change |
> **Tip:** Use `acumatica_describe_entity` first to discover available fields, then `acumatica_list_entities` to search/filter. For Generic Inquiries, use `acumatica_list_generic_inquiries` to find GI names and `acumatica_describe_inquiry` to see available fields. See [docs/example-prompts.md](docs/example-prompts.md) for usage patterns.
## Documentation
Detailed documentation is available in the [`docs/`](docs/) folder:
- **[Tool Reference](docs/tool-reference.md)** -- Complete specification for all 49 tools with parameters and endpoints
- **[Example Prompts](docs/example-prompts.md)** -- Example prompts for Claude and other MCP clients organized by use case
- **[OData Filtering Guide](docs/odata-filtering.md)** -- Guide to `$filter`, `$orderby`, `$select`, `$expand`, and `$top` query parameters
- **[Generic Inquiries](docs/generic-inquiries.md)** -- Why GIs are gated for AI use, which GIs to expose, and how to enable the opt-in registry
- **[Schema Knowledge](docs/schema-discovery.md)** -- Offline schema-discovery tools for building integrations/customizations, and how the schema index is built
- **[Architecture](docs/architecture.md)** -- Detailed architecture, OAuth flow, security model, and design decisions
- **[Self-Hosting Guide](docs/self-hosting-guide.md)** -- How to run the MCP server on Node.js or other platforms outside Cloudflare
- **[Upgrading Acumatica](docs/upgrading-acumatica.md)** -- Steps to take when changing or upgrading the connected Acumatica version
## Skills
Reusable Claude skills that ship with this repo, in [`skills/`](skills/):
- **[acumatica-gi-descriptions](skills/acumatica-gi-descriptions/)** -- End-to-end process for writing AI-facing descriptions for Generic Inquiries and their result columns, grounded in the GI's own design metadata (tables, joins, WHERE conditions, columns) rather than guessed from names. Includes the platform tripwires that make bulk GI metadata work go silently wrong, a checklist of design signals worth hunting, and three scripts for truncation auditing, design briefing, and draft validation.
To use it, point Claude at the skill directory, or copy it into your own `.claude/skills/`.
## Security
- **No stored credentials.** The MCP server does not store Acumatica passwords. It uses OAuth 2.0 authorization code flow -- users authenticate directly with Acumatica.
- **Per-user tokens.** Each user's Acumatica access token is stored in the platform key-value store (Cloudflare KV on the default deployment), scoped to their username. Tokens are automatically refreshed when expired. If a refresh token expires, the connection re-authenticates automatically instead of requiring a manual reconnect.
- **Access gate.** Only users who can read a designated canary Generic Inquiry can connect. The server checks GI-readability over OData during login (not role membership); users without access see an access denied page. Restrict the GI however you like — a marker `MCP Access` role is the recommended way. The GI name is configurable via `ACUMATICA_CANARY_GI`.
- **Consent interstitial.** After passing the access check, users must acknowledge that their data will be processed by an external AI model before the MCP session activates.
- **Sensitive field redaction.** Tool responses are automatically scanned for sensitive field names (SSN, bank accounts, salary, credit card, etc.) and matched values are replaced with `[REDACTED]`. Patterns are configurable via `REDACT_PATTERNS` and `REDACT_SKIP` environment variables.
- **Role-based access.** The user's Acumatica role determines which records they can read. If a user doesn't have access to a record in Acumatica, they won't be able to access it through the MCP server either.
- **Read-only.** All current tools are read-only lookups. No data is created, modified, or deleted.
- **Rate limiting.** 3 concurrent requests, 40 requests per minute, and a 1000-record cap per query by default -- all configurable from the admin console at `/docs/admin/settings` without redeploying. Limits are per user and count HTTP calls to Acumatica (not tool invocations). Rejections return a structured envelope with an exact `retryAfterSeconds` and are logged as `rate_limit_hit` events so you can tell whether the caps are too tight.
- **Pagination refusal.** The list/query tools (`acumatica_list_entities`, `acumatica_run_inquiry`, `acumatica_list_generic_inquiries`) do not support pagination. When a response hits `ACUMATICA_MAX_RECORDS`, the tool returns a structured envelope (`truncated: true`, `paginationSupported: false`, `actionRequired: "..."`) instructing the AI to stop and ask the user for a narrower filter rather than retrieving more records.
- **Audit logging.** All tool invocations, auth events (login success/denied, consent accepted), and field redaction events are logged as structured JSON. View with `npx wrangler tail`.
## Platform Portability
While the default deployment targets Cloudflare Workers, the tool handlers and core libraries are platform-agnostic. A storage abstraction (`IKeyValueStore` interface + `AppEnv` type) decouples tool logic from Cloudflare-specific APIs, enabling self-hosted deployments on Node.js with Redis, SQLite, or other storage backends. See the [Self-Hosting Guide](docs/self-hosting-guide.md) for details.
## Tech Stack
- **Runtime:** [Cloudflare Workers](https://workers.cloudflare.com) + [Durable Objects](https://developers.cloudflare.com/durable-objects/)
- **MCP:** [`agents` SDK](https://www.npmjs.com/package/agents) (McpAgent), [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)
- **Auth:** [`@cloudflare/workers-oauth-provider`](https://www.npmjs.com/package/@cloudflare/workers-oauth-provider)
- **HTTP routing:** [Hono](https://hono.dev)
- **Language:** TypeScript
- **Validation:** [Zod](https://zod.dev)
## Development
```bash
npx wrangler dev # Start local dev server
npx tsc --noEmit # Type check
npx wrangler tail # Stream live logs from deployed worker
```
## License
Apache 2.0 -- Copyright 2026 Hall Boys, Inc.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing