Fusion Holds & Funds Desk MCP
# Fusion Holds & Funds Desk MCP
A Model Context Protocol (MCP) server for the Oracle Fusion Cloud ERP holds and funds desk. It answers the question an AP analyst asks a dozen times a day — *why is this invoice not paying?* — by pulling the invoice, the holds on it, the accounting period status, and the budgetary control result into one place, with the resolution steps and the owning team attached to each hold.
It is **read-only**. Nothing here releases a hold, opens a period, or reserves funds.
## The one rule: funds balances are never invented
This server reports funds *statuses* and *failure reasons*. It reports a funds **balance** only when Oracle Budgetary Control hands one over.
It never estimates a balance, never derives one by arithmetic from invoice amounts, never carries one forward from an earlier call, and never emits a placeholder number that a reader could mistake for a real funds position. When Budgetary Control does not supply a figure, every numeric field comes back `null` alongside `dataAvailability: "unavailable"`, the reason it is unavailable, and instructions on where to get the real number.
The rule is enforced in code, not just documented. Every balance record passes through `sealBalances` in [`src/domain/fundsPolicy.ts`](src/domain/fundsPolicy.ts), which strips numeric fields unless the record was built directly from a Budgetary Control API response, and `get_budgetary_control_impacts` re-asserts the invariant before returning. Mock mode drops balances unconditionally, even if a fixture tries to supply them.
## Tools
| Tool | What it answers |
| --- | --- |
| `get_invoice` | The facts: header, amounts, validation/approval/payment/accounting status, matched purchase orders, lines. |
| `get_invoice_holds` | What is holding the invoice, what each hold code means, who owns the fix, whether revalidation clears it, and the steps that do. |
| `get_accounting_period_status` | Whether the AP and GL periods allow accounting for a given period or accounting date. |
| `get_budgetary_control_impacts` | The funds check result, the control budgets involved, and which lines and distributions failed and why. |
A typical run: `get_invoice` to establish the facts, `get_invoice_holds` to see what is blocking it, then `get_accounting_period_status` or `get_budgetary_control_impacts` depending on which category of hold came back.
### Identifying an invoice
Pass `invoiceId` when you have it. An invoice number is unique only within a supplier and business unit, so add `supplierName`, `supplierNumber`, or `businessUnit` to disambiguate. If more than one invoice still matches, the tool returns an `AMBIGUOUS_MATCH` error listing the candidates rather than picking one for you.
### Result shape
Every tool returns a readable text summary plus `structuredContent` validated against a published output schema:
- `meta` — which tool ran, whether it was mock or live, the source resource, and the retrieval timestamp.
- The payload — invoice, holds, periods, or budgetary control result.
- Guidance — `nextSteps` or `recommendedActions`, prioritised, with the owning team named.
- `disclaimers` — including the balance rule above, and a mock-mode warning when applicable.
Failures come back as MCP tool errors (`isError: true`) carrying a machine-readable code (`NOT_FOUND`, `AMBIGUOUS_MATCH`, `AUTH_FAILED`, `RESOURCE_UNAVAILABLE`, `TIMEOUT`, `RATE_LIMITED`, `UPSTREAM_ERROR`, `INVALID_ARGUMENT`, `CONFIG_INVALID`) and remediation steps — never a fabricated result.
## Install and build
Requires Node.js 18.17 or newer (Node 20+ recommended).
```bash
git clone https://github.com/kumr192/fusion-holds-funds-desk-mcp.git
cd fusion-holds-funds-desk-mcp
npm install
npm run build
```
Then verify the build:
```bash
npm test # unit and integration tests
npm run smoke # starts the built server over stdio and calls every tool
npm run doctor # prints the resolved configuration; probes the pod in live mode
```
## Running on Windows
The steps below are what to run on a Windows machine from PowerShell. Everything is cross-platform — no WSL, no build tools, no Python.
**1. Install Node.js.** Get the current LTS from [nodejs.org](https://nodejs.org/), then confirm it in a *new* PowerShell window:
```powershell
node --version
npm --version
```
**2. Clone and build.**
```powershell
cd $HOME\code
git clone https://github.com/kumr192/fusion-holds-funds-desk-mcp.git
cd fusion-holds-funds-desk-mcp
npm install
npm run build
```
`npm install && npm run build` is the whole setup. The build writes `dist\index.js`, which is the file the MCP client launches.
**3. Confirm it works before wiring it into a client.**
```powershell
npm test
npm run smoke
```
`npm run smoke` starts the server exactly as an MCP client would, calls all four tools, and checks that no funds balance leaks out of mock mode. All checks should pass.
**4. Note the absolute path to `dist\index.js`.**
```powershell
(Resolve-Path .\dist\index.js).Path
```
**5. Add it to `mcp.json`.** Use the path from the previous step. In JSON, backslashes must be doubled (`\\`), or you can use forward slashes.
For Cursor, the file is `%USERPROFILE%\.cursor\mcp.json` (or `.cursor\mcp.json` inside a project, for a project-scoped server). For Claude Desktop it is `%APPDATA%\Claude\claude_desktop_config.json`. Both use the same `mcpServers` shape:
```json
{
"mcpServers": {
"fusion-holds-funds-desk": {
"command": "node",
"args": ["C:\\Users\\shiv\\code\\fusion-holds-funds-desk-mcp\\dist\\index.js"],
"env": {
"FUSION_MODE": "mock"
}
}
}
}
```
Ready-to-edit copies are in [`examples/mcp.mock.json`](examples/mcp.mock.json) and [`examples/mcp.live.json`](examples/mcp.live.json).
**6. Restart the MCP client** so it picks up the change. The server should appear with four tools.
**7. Try it.** Ask: *"Why is invoice INV-1001 on hold?"* or *"Is the GL period open for FEB-26?"*
**Switching to live.** Change the `env` block to point at your pod:
```json
{
"mcpServers": {
"fusion-holds-funds-desk": {
"command": "node",
"args": ["C:\\Users\\shiv\\code\\fusion-holds-funds-desk-mcp\\dist\\index.js"],
"env": {
"FUSION_MODE": "live",
"FUSION_BASE_URL": "https://your-pod.fa.us2.oraclecloud.com",
"FUSION_USERNAME": "AP_INTEGRATION_USER",
"FUSION_PASSWORD": "...",
"FUSION_DEFAULT_LEDGER": "US Primary Ledger",
"FUSION_DEFAULT_BUSINESS_UNIT": "US1 Business Unit"
}
}
}
}
```
Before restarting the client, check the credentials and resource paths from the terminal — `npm run doctor` validates the configuration and probes the invoices resource, so a bad password or a wrong path surfaces as a clear message instead of a failed tool call:
```powershell
$env:FUSION_MODE="live"
$env:FUSION_BASE_URL="https://your-pod.fa.us2.oraclecloud.com"
$env:FUSION_USERNAME="AP_INTEGRATION_USER"
$env:FUSION_PASSWORD="..."
npm run doctor
```
**Windows troubleshooting**
- *`npm : File ... npm.ps1 cannot be loaded`* — PowerShell's execution policy is blocking npm. Run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` in an elevated window, or use `npm.cmd` instead.
- *Client shows the server as failed* — the path in `args` is usually the cause. It must be absolute, must point at `dist\index.js` (not `src`), and backslashes must be doubled in JSON. Confirm the file exists with `Test-Path C:\...\dist\index.js`.
- *`node` not recognised* — open a new terminal after installing Node so `PATH` is refreshed, or use the full path to `node.exe` as `command`.
- *Works in the terminal, not in the client* — the client does not inherit your shell environment. Every variable the server needs must be in the `env` block of `mcp.json`.
## Mock mode vs live mode
**Mock mode** (`FUSION_MODE=mock`, the default) serves deterministic fixtures: no pod, no credentials, no network. The fixture set covers a matching hold (`INV-1001`), a funds hold with an invalid account (`2026-0234`), a clean paid invoice in a closed period (`AUR-55120`), a manual hold plus a tax variance (`TRI-2026-88`), and an invoice dated into a period that was never opened (`VE-4471`). Mock results are labelled as such in `meta.mode` and in the disclaimers, and mock mode never returns funds balances.
**Live mode** (`FUSION_MODE=live`) calls Oracle Fusion Cloud ERP REST with basic auth or a bearer token, with a configurable timeout and bounded retries on 429 and 5xx responses. Fusion resource names vary by release and by which offerings are enabled, so every resource path is overridable — see [`.env.example`](.env.example). When a resource is missing or forbidden, the affected data is reported as unavailable with the HTTP diagnostic; no value is assumed in its place.
## Configuration
All configuration is environment variables; [`.env.example`](.env.example) documents every one. The essentials:
| Variable | Default | Purpose |
| --- | --- | --- |
| `FUSION_MODE` | `mock` | `mock` or `live`. |
| `FUSION_BASE_URL` | — | Pod origin. Required in live mode. |
| `FUSION_USERNAME` / `FUSION_PASSWORD` | — | Basic auth for the integration user. |
| `FUSION_TOKEN` | — | Bearer token; takes precedence over basic auth. |
| `FUSION_DEFAULT_LEDGER` | — | Ledger used when a call omits `ledgerName`. |
| `FUSION_DEFAULT_BUSINESS_UNIT` | — | Business unit used when a call omits `businessUnit`. |
| `FUSION_BC_BALANCES_ENABLED` | `false` | Opt in to querying Budgetary Control balances. Off means balances are always reported as unavailable. |
| `FUSION_TIMEOUT_MS` | `30000` | Per-request timeout. |
| `FUSION_MAX_RETRIES` | `2` | Retries for 429 and 5xx responses. |
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error`, or `silent`. Logs go to stderr. |
Secrets are never logged: `describeConfig` reports the auth *kind* and the username, never the password or token.
## Development
```
src/
index.ts stdio entrypoint
server.ts MCP server assembly and tool registration
config.ts environment parsing and validation
errors.ts error taxonomy with remediation
logger.ts stderr JSON logging
domain/
fundsPolicy.ts the never-invent-balances rule, enforced
holdCatalog.ts hold code reference: meaning, owner, resolution steps
format.ts text summary helpers
fusion/
types.ts domain model shared by both clients
httpClient.ts live Oracle Fusion REST client
mockClient.ts fixture-backed client
createClient.ts mode-based factory
fixtures/desk.ts deterministic fixtures
tools/ the four tools, their schemas, and shared plumbing
tests/ vitest suite
scripts/ clean, doctor, smoke
```
| Script | Purpose |
| --- | --- |
| `npm run build` | Compile TypeScript to `dist/`. |
| `npm test` | Run the vitest suite. |
| `npm run test:coverage` | Run tests with coverage. |
| `npm run typecheck` | Typecheck sources and tests without emitting. |
| `npm run check` | Typecheck, then test. |
| `npm run smoke` | Start the built server over stdio and exercise every tool. |
| `npm run doctor` | Print the resolved configuration; probe the pod in live mode. |
| `npm run clean` | Remove `dist/` and `coverage/`. |
The test suite covers configuration parsing, the hold catalog, both clients (the live one against a stubbed `fetch`), all four tools, and the full MCP handshake over an in-memory transport. Several tests exist purely to pin the balance rule: fixtures carry no amounts, mock mode strips injected balances, the seal drops unsourced numbers, and no numeric balance survives a mock-mode tool call.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 4 tools
Each tool addresses a distinct diagnostic question: invoice facts, hold explanations, period status, and budget impacts. The descriptions explicitly reference the recommended sequence, eliminating boundary confusion.
All tool names follow a uniform get_ + noun phrase pattern in snake_case, making the resource and action clear for every tool. The naming convention is perfectly consistent.
Four tools cover a tightly scoped diagnostic domain without redundancy. Each tool earns its place and together they form a complete workflow for investigating stuck invoices.
The set covers the full diagnostic surface for the stated purpose: invoice retrieval, holds analysis, period status, and budgetary control impacts. No obvious missing operation within the read-only holds desk scope.