{
  "markdown": "# freshbooks-mcp\n\nMCP server for [FreshBooks](https://www.freshbooks.com) — invoices, clients, estimates and\npayments, exposed to Claude as typed tools.\n\n> This project was developed and is maintained by AI (Claude Code). Use at your own discretion.\n\n## Install\n\n```sh\nnpm install -g @chrischall/freshbooks-mcp\n```\n\n## Setup\n\nFreshBooks is **OAuth2 only** — there is no API key and no personal access token, so a\none-time browser authorization is required.\n\n1. Register an app at <https://my.freshbooks.com/#/developer>. The redirect URI must be\n   **HTTPS with no query string**; `https://localhost` works and never needs to resolve.\n2. Note the **Client ID** and **Client Secret**.\n3. Obtain a refresh token, either way:\n   - **From the server itself** (no script): set `FRESHBOOKS_CLIENT_ID` and\n     `FRESHBOOKS_CLIENT_SECRET`, start it, then call `freshbooks_auth_url`, open\n     the URL it returns, approve, and pass the URL you land on to\n     `freshbooks_auth_exchange`. Those two tools need no refresh token — minting\n     one is what they are for. This is also the path mcp-host's `authFlow`\n     drives, so a hosted connector can do it without you pasting anything.\n   - **From the script**, if you prefer it outside the server — see\n     [`skills/freshbooks-curl`](skills/freshbooks-curl/SKILL.md).\n4. Configure:\n\n```sh\nFRESHBOOKS_CLIENT_ID=...\nFRESHBOOKS_CLIENT_SECRET=...\nFRESHBOOKS_REFRESH_TOKEN=...       # from the bootstrap\nFRESHBOOKS_REDIRECT_URI=https://localhost   # optional; must match what you registered\nFRESHBOOKS_TOKEN_STORE=~/.freshbooks-mcp/session.json   # optional\n```\n\n### ⚠️ Refresh tokens rotate\n\nFreshBooks issues a **new refresh token on every refresh and immediately invalidates the\nold one**. This server persists each rotation to `FRESHBOOKS_TOKEN_STORE` (mode `0600`)\nbefore the refresh is considered complete, and prefers the stored token over the\nenvironment value — the stored one has rotated past it.\n\nTwo consequences worth knowing:\n\n- **Do not point two tools at the same store.** The MCP server and the `freshbooks-curl`\n  skill keep separate state files on purpose; sharing one makes them spend each other's\n  tokens and locks both out.\n- **If the store is lost, re-run the bootstrap.** A spent refresh token cannot be\n  recovered.\n\nChanging `FRESHBOOKS_REFRESH_TOKEN` to a freshly bootstrapped value is detected and\nadopted, so re-bootstrapping is the supported recovery path.\n\n## Tools\n\n| Tool | Purpose |\n| --- | --- |\n| `freshbooks_get_identity` | Resolve accountId / businessId / businessUuid |\n| `freshbooks_auth_url` | Get the consent URL to authorise this connection |\n| `freshbooks_auth_exchange` | Exchange the authorization code (or pasted redirect URL) for a refresh token |\n| `freshbooks_healthcheck` | Verify the OAuth credential and FreshBooks reachability; distinguishes \"no credential\" from \"rejected\" from \"FreshBooks is down\" |\n| `freshbooks_list_invoices` / `freshbooks_get_invoice` | Browse and fetch invoices |\n| `freshbooks_list_clients` / `freshbooks_get_client` | Browse and fetch clients |\n| `freshbooks_list_estimates` / `freshbooks_get_estimate` | Browse and fetch estimates |\n| `freshbooks_list_payments` / `freshbooks_get_payment` | Browse and fetch payments |\n| `freshbooks_list_items` / `freshbooks_get_item` | Browse and fetch catalogue items |\n| `freshbooks_create_client` | Create a client — confirm-gated |\n| `freshbooks_create_invoice` | Create an invoice — confirm-gated |\n| `freshbooks_update_invoice` | Update an invoice — confirm-gated |\n| `freshbooks_record_payment` | Record a payment against an invoice — confirm-gated |\n| `freshbooks_accept_estimate` | Accept an estimate (`action_accept`) — confirm-gated, idempotent |\n| `freshbooks_update_estimate` | Update an estimate's lines, notes, terms, presentation — confirm-gated |\n| `freshbooks_send_estimate` | Email an estimate to the client (`action_email`) — confirm-gated |\n| `freshbooks_decline_estimate` | Always fails: FreshBooks has no decline. Answers with the alternatives |\n| `freshbooks_list_expenses` / `freshbooks_get_expense` | Browse and fetch expenses |\n| `freshbooks_list_expense_categories` | Categories supplying `categoryid` for new expenses |\n| `freshbooks_create_expense` | Record an expense — confirm-gated |\n| `freshbooks_list_projects` / `freshbooks_get_project` | Projects (businessId-keyed) |\n| `freshbooks_create_project` | Create a project — confirm-gated |\n| `freshbooks_list_time_entries` | Tracked time, with `total_logged` / `total_unbilled` |\n| `freshbooks_create_time_entry` | Log time in seconds — confirm-gated |\n| `freshbooks_list_services` | Billable work types for projects and time entries |\n| `freshbooks_list_records` / `freshbooks_get_record` | Generic accessor for the accounting long tail (taxes, credit notes, invoice profiles, tasks, staff, gateways, bills, bill vendors, bill payments, other income) |\n\n**Confirm-gated** means the tool makes *no* network call unless `confirm: true` is passed;\nwithout it you get a dry-run preview of exactly what would be sent.\n\n### Estimate writes\n\nAcceptance is an **action on the estimate**, not a status field: `status` (int),\n`display_status` and `ui_status` are computed and read-only, and they disagree with each\nother by design (a viewed estimate reads `status: 3`, `display_status: \"viewed\"`,\n`ui_status: \"open\"`). Accepting is `PUT estimates/estimates/{id}` with\n`{\"estimate\": {\"action_accept\": true}}` — see\n[`docs/FRESHBOOKS-API.md`](docs/FRESHBOOKS-API.md) for where that shape comes from.\n\n- **Accept is idempotent.** An estimate already accepted (or invoiced) comes back with\n  `changed: false` and no write is sent — acceptance cannot be undone through the API, so\n  a repeat call must not re-fire it.\n- **There is no decline.** FreshBooks' estimate statuses are draft / sent / viewed /\n  replied / accepted / invoiced; no declined state, no `action_deny`, no\n  `estimate.decline` webhook. `freshbooks_decline_estimate` exists only to say so and\n  point at the alternatives, rather than leave an agent to invent a write that changes\n  nothing.\n- **Every write returns the re-fetched estimate**, plus `before` / `after` state and\n  `changed` / `changedFields`, so success is verified against the record rather than\n  inferred from a `200`. `changed` covers the status fields *and* the fields that write\n  actually set, so a successful notes edit reports `changed: true` even though no status\n  moves. On `freshbooks_send_estimate` it describes the record only — emailing an\n  already-sent estimate moves nothing, and retrying on `changed: false` would send the\n  client a second copy.\n\n## Writes require an owner/admin accounting account\n\nFreshBooks separates the role you hold on a *business* from the role you hold on an\n*accounting account*. You can own a business that has **no** accounting account\n(`account_id: null`) while being only a **client** on the account you can actually see —\nin which case reads succeed and every write returns `403 Permission Denied`, even though\nyour OAuth token carries all the `:write` scopes.\n\n`freshbooks_get_identity` reports `accountRole` and `businessRole` so this is visible up\nfront. If `accountRole` is `client`, the invoicing write tools will not work against that\naccount — that is an account permission, not a configuration problem.\n\n### Two things the API reports misleadingly\n\n- **`total` counts records you may not be able to read.** Expenses reported `total: 16`\n  while returning zero rows. List results attach a `note` when that happens, so it reads\n  as a permission boundary rather than an empty account.\n- **Projects and time tracking are keyed by `businessId`, not `accountId`**, and paginate\n  under a `meta` block instead of flat `page`/`pages`/`total`. They also work on a\n  business with no accounting account at all.\n\n## The three identifiers\n\nFreshBooks hands out three non-interchangeable ids, and using the wrong one returns a bare\n**404** that reads like a missing record:\n\n| Identifier | Used by |\n| --- | --- |\n| `accountId` (alphanumeric) | `/accounting/account/…`, `/payments/account/…` |\n| `businessId` (integer) | `/projects/business/…`, `/timetracking/business/…` |\n| `businessUuid` (UUID) | `/accounting/businesses/…` |\n\nCall `freshbooks_get_identity` first. Full API notes, including the four different error\nenvelopes, are in [`docs/FRESHBOOKS-API.md`](docs/FRESHBOOKS-API.md).\n\n## Shell access without the server\n\n[`skills/freshbooks-curl`](skills/freshbooks-curl/SKILL.md) covers the same API from a\nshell with `curl` + `jq`, including the OAuth bootstrap and rotation-safe token handling.\n\n## Development\n\n```sh\nnpm install\nnpm run build\nnpm test\n```\n\n## License\n\nMIT\n",
  "bytes": 8686,
  "sha": "eaa131536f21b7d3a6888bf9340e587e278aa58d1020d4fa59b9417041ed4c9f",
  "repo_slug": "chrischall/freshbooks-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_chrischall_freshbooks_mcp_5663a861/readme"
}