Skip to main content
Glama
duncanmcclean

FreeAgent MCP

README.md
# FreeAgent MCP

> [!WARNING]
> This project is 100% vibe coded. I haven't tested everything or looked at the code.
> Things might not work. Use at your own risk.

A local, **read-only** MCP server that exposes FreeAgent accounting data to AI clients
(Claude Desktop, the ChatGPT desktop app, or anything else that speaks MCP over stdio),
so you can ask questions like *"why is there £4,200 sitting in nominal code 907?"* and
have the model drill down into the underlying transactions.

Read-only is enforced at the HTTP client layer: the client exposes a `get()` method and
nothing else — any attempt to call another verb throws. Every tool is annotated
read-only and idempotent. There is no code path that can modify accounting data, and the
VAT filing / MTD submission endpoints are not touched at all.

The server is built on Laravel, so if you don't have PHP set up locally (with
[Herd](https://herd.laravel.com) or similar) it might not be that useful to you. That
said, you could use something like [Laravel Sail](https://laravel.com/docs/sail) to run
it in a Docker container instead.

## Setup

1. Install and migrate:

    ```sh
    composer install
    cp .env.example .env
    php artisan key:generate
    php artisan migrate
    ```

2. Register an OAuth app on the [FreeAgent Developer Dashboard](https://dev.freeagent.com),
   giving it `http://localhost` as a redirect URI — FreeAgent only accepts redirect URIs
   that exactly match one registered on the app. Then add the keys to `.env`:

    ```dotenv
    FREEAGENT_CLIENT_ID=your-client-id
    FREEAGENT_CLIENT_SECRET=your-client-secret
    FREEAGENT_SANDBOX=false
    ```

    If your app already has a different redirect URI registered, set
    `FREEAGENT_REDIRECT_URI` to that value instead of registering a new one.

3. Authenticate:

    ```sh
    php artisan freeagent:auth
    ```

    Open the printed URL, approve the app, then paste the URL you're redirected to back
    into the terminal. Tokens are stored (encrypted) in SQLite and refreshed
    automatically from then on.

4. Check it worked:

    ```sh
    php artisan freeagent:status
    ```

## Claude Desktop

Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):

```json
{
    "mcpServers": {
        "freeagent": {
            "command": "php",
            "args": ["/absolute/path/to/freeagent-mcp/artisan", "mcp:start", "freeagent"]
        }
    }
}
```

## Claude Code

Add the server from any project (`--scope user` makes it available everywhere, drop it
to register for the current project only):

```sh
claude mcp add freeagent --scope user -- php /absolute/path/to/freeagent-mcp/artisan mcp:start freeagent
```

Or add it to a project's `.mcp.json` by hand:

```json
{
    "mcpServers": {
        "freeagent": {
            "command": "php",
            "args": ["/absolute/path/to/freeagent-mcp/artisan", "mcp:start", "freeagent"]
        }
    }
}
```

Check it's connected with `/mcp` inside a session.

## ChatGPT desktop app

Add to `~/.codex/config.toml` (or via Settings → MCP servers → Add server → STDIO):

```toml
[mcp_servers.freeagent]
command = "php"
args = ["/absolute/path/to/freeagent-mcp/artisan", "mcp:start", "freeagent"]
cwd = "/absolute/path/to/freeagent-mcp"
startup_timeout_sec = 20
tool_timeout_sec = 120
default_tools_approval_mode = "writes"
```

`writes` mode auto-approves read-only tools and prompts for anything else — since every
tool here is marked read-only, that means no prompts, with a hard backstop. Note this
only works in the ChatGPT **desktop app**; ChatGPT web won't see it.

## Tools

| Tool | What it does |
| --- | --- |
| `get_company_context` | Company details, fiscal year end, currency, users and the FreeAgent subdomain. Cheap orientation call. |
| `list_categories` | The full chart of accounts with nominal codes, including bank account and user sub-accounts. Optional search. |
| `get_balance_sheet` | Balance sheet as at a date, or the opening balances. |
| `get_trial_balance` | Trial balance summary for a date range, or the opening balances. |
| `get_profit_and_loss` | P&L summary for a date range: income, expenses, deductions, retained profit. |
| `get_cashflow` | Cash in/out by month with the net balance. |
| `explain_category` | Every ledger transaction that hit a nominal code in a range, with source documents, deep links and a running total — reconciled against the trial balance. |
| `search_transactions` | Free-text search across ledger transactions with date, amount, bank account and nominal code filters. |
| `list_records` | Generic list endpoint for 35 other read-only resources (banking, documents, contacts, projects, tax returns, assets, payroll, time). |
| `get_record` | Full detail of a single record by resource and id. |

## Other commands

```sh
php artisan freeagent:status       # auth state + connected company
php artisan freeagent:clear-cache  # drop cached API responses
php artisan mcp:inspector freeagent # debug the server interactively
```

Responses are cached in the database (an hour for reports and reference data, 15 minutes
for transactions), so restarting the MCP client doesn't burn through FreeAgent's rate
limits (120 requests/minute, 3,600/hour).