Skip to main content
Glama
WavingCatApps

FreeAgent MCP Server

README.md
# FreeAgent MCP Server

A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for the [FreeAgent](https://www.freeagent.com) accounting API. Enables LLMs to manage contacts, invoices, estimates, bills, expenses, timeslips, projects, tasks, bank accounts, and more.

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FWavingCatApps%2Ffreeagent-mcp-vercel&project-name=freeagent-mcp&repository-name=freeagent-mcp&env=FREEAGENT_CLIENT_ID,FREEAGENT_CLIENT_SECRET,JWT_SECRET,FREEAGENT_USE_SANDBOX&envDefaults=%7B%22FREEAGENT_USE_SANDBOX%22%3A%22true%22%7D&envDescription=FreeAgent%20OAuth%20Client%20ID%2FSecret%20from%20https%3A%2F%2Fdev.freeagent.com.%20Set%20JWT_SECRET%20with%3A%20openssl%20rand%20-hex%2032.%20After%20deploy%2C%20add%20https%3A%2F%2FYOUR_PROJECT.vercel.app%2Foauth%2Fcallback%20as%20a%20FreeAgent%20redirect%20URI.&envLink=https%3A%2F%2Fgithub.com%2FWavingCatApps%2Ffreeagent-mcp-vercel%2Fblob%2Fmaster%2FOAUTH_SETUP.md)

> By my own admission, most of this project is vibe coded.

## Features

- **Broad FreeAgent coverage**: contacts, invoices (incl. transitions, email, duplicate, direct debit), estimates, bills, recurring invoices, price list items, expenses, timeslips, projects, tasks, bank accounts/feeds/statements (JSON lines), credit notes + reconciliations, notes, attachments (metadata), journals, capital assets, accounting reports (P&L, balance sheet, trial balance, cashflow, ledger), VAT/corp tax/income tax/final accounts, payroll, properties/stock/hire purchases, CIS, sales tax periods, account locks, categories, company info, and users
- **Intent-bundle tools**: `reconcile_bank_transaction`, `log_expense`, and `invoice_from_timeslips` collapse multi-call sequences into single tool calls and resolve human-friendly hints (names, codes, references) to FreeAgent URLs server-side
- **Tool-search mode** (`FREEAGENT_TOOL_SEARCH=true`): optional — collapses the catalog behind `freeagent_search_tools` / `freeagent_call_tool` for a smaller `tools/list`. Default is the full catalog (including on Vercel)
- **MCP elicitation**: `create_invoice` falls back to a form elicitation when `contact` is omitted (on clients that support it)
- **Two deployment modes**: local (stdio) or cloud (Vercel serverless via Streamable HTTP)
- **OAuth 2.0**: stateless JWT-based auth for serverless, or direct token for local use
- **Tool annotations**: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` on every tool
- **Zod validation**: strict input schemas with `.describe()` on all fields
- **Dual response formats**: Markdown (human-readable) or JSON (structured)
- **Pagination**: proper header parsing with `x-total-count` and `Link` headers
- **Rate limit handling**: clear error messages with retry-after guidance
- **Sandbox support**: test safely against FreeAgent's sandbox environment

## Deployment Options

### Local (stdio) - for Claude Desktop

1. Install and build:
   ```bash
   bun install
   bun run build
   ```

2. Set environment variables:
   ```bash
   export FREEAGENT_ACCESS_TOKEN="your_access_token"
   export FREEAGENT_USE_SANDBOX="true"  # optional

   # optional: refresh the access token automatically when it expires
   export FREEAGENT_REFRESH_TOKEN="your_refresh_token"
   export FREEAGENT_CLIENT_ID="your_client_id"
   export FREEAGENT_CLIENT_SECRET="your_client_secret"
   export FREEAGENT_TOKEN_CACHE="$HOME/.config/freeagent-mcp/tokens.json"  # optional, this is the default
   ```

   With the refresh variables set, a 401 triggers a token refresh and one retry. The newest tokens are saved to the cache file (mode 600) and used on the next start, so you only need to repeat the OAuth flow if the refresh token itself is revoked.

3. Add to Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
   ```json
   {
     "mcpServers": {
       "freeagent": {
         "command": "node",
         "args": ["/path/to/freeagent-mcp-server/dist/index.js"],
         "env": {
           "FREEAGENT_ACCESS_TOKEN": "your_token",
           "FREEAGENT_USE_SANDBOX": "true"
         }
       }
     }
   }
   ```

### Vercel (Streamable HTTP) - for cloud access

One-click deploy (clones this repo into your Git account and prompts for env vars):

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FWavingCatApps%2Ffreeagent-mcp-vercel&project-name=freeagent-mcp&repository-name=freeagent-mcp&env=FREEAGENT_CLIENT_ID,FREEAGENT_CLIENT_SECRET,JWT_SECRET,FREEAGENT_USE_SANDBOX&envDefaults=%7B%22FREEAGENT_USE_SANDBOX%22%3A%22true%22%7D&envDescription=FreeAgent%20OAuth%20Client%20ID%2FSecret%20from%20https%3A%2F%2Fdev.freeagent.com.%20Set%20JWT_SECRET%20with%3A%20openssl%20rand%20-hex%2032.%20After%20deploy%2C%20add%20https%3A%2F%2FYOUR_PROJECT.vercel.app%2Foauth%2Fcallback%20as%20a%20FreeAgent%20redirect%20URI.&envLink=https%3A%2F%2Fgithub.com%2FWavingCatApps%2Ffreeagent-mcp-vercel%2Fblob%2Fmaster%2FOAUTH_SETUP.md)

After deploy, register `https://YOUR_PROJECT.vercel.app/oauth/callback` on your FreeAgent OAuth app (see [OAUTH_SETUP.md](./OAUTH_SETUP.md)). Full walkthrough: [VERCEL_DEPLOYMENT.md](./VERCEL_DEPLOYMENT.md).

Key points:

- Uses `StreamableHTTPServerTransport` in stateless mode (no sessions) with `enableJsonResponse: true` (avoids empty SSE bodies on Vercel)
- OAuth 2.0 with PKCE via JWT-encoded tokens (no database needed)
- Handles `POST` (JSON-RPC tool calls); `GET`/`DELETE` return 405 in this serverless/stateless deploy (no durable SSE sessions)
- Set `PRODUCTION_URL` (or rely on `VERCEL_PROJECT_PRODUCTION_URL`) for stable production OAuth callback URLs. Preview OAuth uses the request host (or `VERCEL_URL`) so short per-deploy hosts match FreeAgent `*` wildcards.

Required env vars: `FREEAGENT_CLIENT_ID`, `FREEAGENT_CLIENT_SECRET`, `JWT_SECRET` (stable secret required on Vercel so OAuth JWTs verify across serverless instances)

## Tool-Search Mode (optional)

By default the server registers every catalog tool directly (~165 definitions in `tools/list`), including on Vercel. That path is reliable again after the tools/list schema fixes.

If you want a smaller `tools/list` (e.g. many MCP servers competing for context), opt in:

```bash
export FREEAGENT_TOOL_SEARCH=true   # only freeagent_search_tools + freeagent_call_tool
```

Unset it (or anything other than `true`/`1`) to keep the full catalog.

| Tool | Purpose |
|------|---------|
| `freeagent_search_tools` | Search the catalog and return JSONSchema for matching tools. Query forms: `select:name1,name2` for direct lookup, `+required optional` for scored search with required keywords, or plain keywords for a ranked search. |
| `freeagent_call_tool` | Invoke any catalog tool by name with validated arguments (`{ name, arguments }`). Pair with `search_tools` to discover schemas on demand. |

This mirrors the deferred-loading pattern used by Claude Code's internal `ToolSearch`.

## Available Tools

See [TOOLS.md](./TOOLS.md) for per-tool parameters and examples. Summary:

### Contacts
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_contacts` | List contacts with pagination and sorting | Yes |
| `freeagent_get_contact` | Get contact details by ID | Yes |
| `freeagent_create_contact` | Create a new contact | No |

### Invoices
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_invoices` | List invoices with status/contact/project filters | Yes |
| `freeagent_get_invoice` | Get invoice details (renders computed discount amount) | Yes |
| `freeagent_create_invoice` | Create a draft invoice (supports `discount_percent`; elicits `contact` if omitted) | No |
| `freeagent_transition_invoice` | mark_as_sent / mark_as_cancelled / mark_as_draft / mark_as_scheduled / convert_to_credit_note | No |
| `freeagent_invoice_from_timeslips` | **Intent bundle**: draft an invoice from a contact's unbilled timeslips | No |

### Estimates
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_estimates` | List estimates with status/contact/project filters | Yes |
| `freeagent_get_estimate` | Get estimate details (renders computed discount amount) | Yes |
| `freeagent_create_estimate` | Draft an estimate (supports `discount_percent`) | No |
| `freeagent_transition_estimate` | mark_as_sent / mark_as_approved / mark_as_rejected / mark_as_cancelled / mark_as_draft / convert_to_invoice | No |

### Bills
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_bills` | List supplier bills with filters | Yes |
| `freeagent_get_bill` | Get bill details | Yes |
| `freeagent_create_bill` | Record a supplier bill | No |

### Recurring Invoices
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_recurring_invoices` | List recurring invoice templates | Yes |
| `freeagent_get_recurring_invoice` | Get template details | Yes |

### Price List Items
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_price_list_items` | List catalog items | Yes |
| `freeagent_get_price_list_item` | Get catalog item details | Yes |
| `freeagent_create_price_list_item` | Add a catalog item | No |

### Expenses
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_expenses` | List expenses with date/view filters | Yes |
| `freeagent_get_expense` | Get expense details (inc. mileage info) | Yes |
| `freeagent_create_expense` | Create expense or mileage claim (with attachments) | No |
| `freeagent_update_expense` | Update an existing expense | No |
| `freeagent_log_expense` | **Intent bundle**: log a regular expense with a positive `amount` + `kind` enum | No |

### Timeslips
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_timeslips` | List time entries with filters | Yes |
| `freeagent_get_timeslip` | Get timeslip details | Yes |
| `freeagent_create_timeslip` | Create a time entry | No |
| `freeagent_update_timeslip` | Update a timeslip (incl. `billed_on_invoice`) | No |

### Bank Accounts & Transactions
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_bank_accounts` | List all bank accounts | Yes |
| `freeagent_get_bank_account` | Get bank account details | Yes |
| `freeagent_list_bank_transactions` | List transactions for an account | Yes |
| `freeagent_get_bank_transaction` | Get bank transaction details | Yes |
| `freeagent_list_bank_transaction_explanations` | List transaction explanations | Yes |
| `freeagent_get_bank_transaction_explanation` | Get explanation details | Yes |
| `freeagent_create_bank_transaction_explanation` | Explain/categorize a bank transaction | No |
| `freeagent_update_bank_transaction_explanation` | Update a transaction explanation | No |
| `freeagent_reconcile_bank_transaction` | **Intent bundle**: explain a transaction with a category name / invoice ref / bill ref | No |

### Projects & Tasks
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_projects` | List projects with status/contact filters | Yes |
| `freeagent_get_project` | Get project details | Yes |
| `freeagent_create_project` | Create a new project | No |
| `freeagent_list_tasks` | List tasks with project/status filters | Yes |
| `freeagent_get_task` | Get task details | Yes |
| `freeagent_create_task` | Create a task within a project | No |

### Categories, Company & Users
| Tool | Description | Read-only |
|------|-------------|-----------|
| `freeagent_list_categories` | List accounting categories | Yes |
| `freeagent_get_category` | Get category by nominal code | Yes |
| `freeagent_get_company` | Get company information | Yes |
| `freeagent_list_users` | List all users | Yes |


## Intentional omissions

- **Timeslip timers** — start/stop timer endpoints are not exposed (poor MCP fit).
- **Binary PDF download tools** — invoice/estimate/credit-note PDF bytes are not returned (no efficient agentic binary channel yet).
- **File-based bank statement upload** — use `freeagent_upload_bank_statement` with JSON lines instead. Nested base64 attachments on expenses/explanations remain supported.
- **Accountancy Practice API** — practice/client endpoints are out of scope for this company-facing server.

## Development

### Prerequisites

- [Bun](https://bun.sh) (used for package management and running scripts)
- Node.js 24.x

### Setup

```bash
bun install
```

### Scripts

| Command | Description |
|---------|-------------|
| `bun run build` | Compile TypeScript |
| `bun run dev` | Watch mode (auto-recompile) |
| `bun run start` | Run the compiled server |
| `bun run lint` | Run ESLint |
| `bun run test` | Run tests |
| `bun run test:watch` | Run tests in watch mode |
| `bun run test:coverage` | Run tests with coverage |

### Project Structure

```
freeagent-mcp-server/
├── src/
│   ├── index.ts                       # Stdio server entry point
│   ├── constants.ts                   # Configuration constants & shared utilities
│   ├── types.ts                       # TypeScript type definitions
│   ├── schemas/
│   │   ├── index.ts                   # Zod validation schemas for all tools
│   │   ├── projects.ts                # Project-specific schemas
│   │   └── schemas.test.ts            # Schema validation tests
│   ├── services/
│   │   ├── api-client.ts              # FreeAgent API client (Axios)
│   │   ├── api-client.test.ts         # API client tests
│   │   ├── formatter.ts               # Response formatting utilities (incl. discount amount helper)
│   │   ├── formatter.test.ts          # Formatter tests
│   │   ├── resolvers.ts               # Shared resolvers (category / user / contact / bill hints → URLs)
│   │   ├── oauth-jwt.ts               # JWT-based OAuth provider (Vercel)
│   │   └── freeagent-auth.ts          # Token validation
│   └── tools/
│       ├── register.ts                # Shared tool definitions, registration, ToolContext (elicitation)
│       ├── contacts.ts                # Contact CRUD
│       ├── invoices.ts                # Invoice management (incl. elicitation fallback)
│       ├── transition-invoice.ts      # Invoice lifecycle transitions
│       ├── invoice-from-timeslips.ts  # Intent bundle: draft an invoice from unbilled time
│       ├── estimates.ts               # Estimates + transition_estimate
│       ├── bills.ts                   # Supplier bills
│       ├── recurring-invoices.ts      # Recurring invoice templates (read-only)
│       ├── price-list-items.ts        # Catalog items
│       ├── expenses.ts                # Expense & mileage tracking
│       ├── log-expense.ts             # Intent bundle: positive-amount expense logging
│       ├── timeslips.ts               # Time tracking (incl. update_timeslip)
│       ├── bank-accounts.ts           # Bank accounts & transactions
│       ├── bank-transactions.ts       # Transaction explanations
│       ├── reconcile.ts               # Intent bundle: reconcile a transaction in one call
│       ├── projects.ts                # Project management
│       ├── tasks.ts                   # Task management
│       ├── categories.ts              # Accounting categories
│       └── company.ts                 # Company info & users
├── api/
│   └── index.ts              # Vercel serverless entry point
├── .github/
│   └── workflows/
│       └── ci.yml            # GitHub Actions CI (lint, test, build)
├── eslint.config.js          # ESLint flat config
├── vitest.config.ts          # Vitest configuration
├── vercel.json               # Vercel deployment config
├── package.json
└── tsconfig.json
```

### CI

GitHub Actions runs on every push to `main` and on pull requests:
- **Lint**: ESLint with TypeScript rules
- **Test**: Vitest unit tests (no external API calls)
- **Build**: TypeScript compilation check

## Rate Limiting

| Environment | Limit |
|-------------|-------|
| Production | 15 requests / 60 seconds |
| Sandbox | 5 requests / 60 seconds |

The server returns clear error messages with retry-after timing when rate limited.

## Error Handling

All tool handlers return structured errors via `{ isError: true, content: [...] }` (never thrown exceptions). Error messages are designed for LLM consumption with actionable guidance:

| Status | Meaning |
|--------|---------|
| 401 | Token expired - refresh OAuth token |
| 403 | Insufficient permissions |
| 404 | Resource not found or deleted |
| 422 | Validation error with field-level details |
| 429 | Rate limited - retry after N seconds |

## Security

- Access tokens are never logged or committed
- JWT tokens use HS256 signing with configurable secret
- PKCE is used for the OAuth authorization flow
- Strict Zod schemas reject unexpected input fields
- Bearer auth middleware protects all MCP endpoints

## License

MIT

## Links

- [FreeAgent API Docs](https://dev.freeagent.com/docs)
- [MCP Specification](https://modelcontextprotocol.io)
- [FreeAgent Developer Dashboard](https://dev.freeagent.com)