mcpField
F
freshbooks-mcp

freshbooks-mcp

Local
github.com/chrischallFinance0Updated 2d ago

About freshbooks-mcp

An MCP server that gives Claude typed access to FreshBooks accounting records and actions, from invoices and clients to expenses and time entries. Write operations require confirmation, and local OAuth tokens are stored privately.

Key features

  1. 01Browse invoices, clients, estimates, and payments
  2. 02Create and update accounting records
  3. 03Record expenses and project time
  4. 04Confirm writes before sending them
  5. 05Persist rotating OAuth tokens locally

Use cases

  1. 01Review invoice and payment details
  2. 02Create invoices after approval
  3. 03Record expenses and tracked project time

MCP support

Exposes FreshBooks data and operations as typed tools for MCP clients. Write tools use confirmation prompts or a preview-and-token flow depending on client support.

Configuration

Local server config is the same for every client. Copy it into your client’s MCP config file.

mcp.json · Universal config
{
"mcpServers": {
"chrischall-freshbooks": {
"command": "npx",
"args": [
"-y",
"@chrischall/freshbooks-mcp"
],
"env": {
"FRESHBOOKS_CLIENT_ID": "<FRESHBOOKS_CLIENT_ID>",
"FRESHBOOKS_CLIENT_SECRET": "<FRESHBOOKS_CLIENT_SECRET>",
"FRESHBOOKS_REFRESH_TOKEN": "<FRESHBOOKS_REFRESH_TOKEN>",
"FRESHBOOKS_REDIRECT_URI": "<FRESHBOOKS_REDIRECT_URI>",
"FRESHBOOKS_TOKEN_STORE": "<FRESHBOOKS_TOKEN_STORE>",
"FRESHBOOKS_BUSINESS_ID": "<FRESHBOOKS_BUSINESS_ID>",
"FRESHBOOKS_ACCOUNT_ID": "<FRESHBOOKS_ACCOUNT_ID>",
"FRESHBOOKS_REQUEST_TIMEOUT_MS": "<FRESHBOOKS_REQUEST_TIMEOUT_MS>"
}
}
}
}
Run commandnpx -y @chrischall/freshbooks-mcp
Environment variables to set
FRESHBOOKS_CLIENT_IDOAuth client ID from my.freshbooks.com/#/developer
FRESHBOOKS_CLIENT_SECRETSecretOAuth client secret
FRESHBOOKS_REFRESH_TOKENSecretOptional for local sign-in, which saves tokens privately. Required for hosted or legacy environment-token setup.
FRESHBOOKS_REDIRECT_URIRegistered redirect URI; required on refresh calls too (default https://localhost)
FRESHBOOKS_TOKEN_STORESecretPath for the rotating-refresh-token store (default ~/.freshbooks-mcp/session.json)
FRESHBOOKS_BUSINESS_IDThe integer businessId to work in when the authenticated identity belongs to several businesses. Without it, reads use the first business FreshBooks lists and writes are refused rather than landing in the wrong books.
FRESHBOOKS_ACCOUNT_IDOverrides the account id normally resolved from /auth/api/v1/users/me. Set it explicitly when the authenticated identity belongs to no business membership and resolution fails, or, together with FRESHBOOKS_BUSINESS_ID, when FreshBooks reports no account_id for the chosen business (accounting writes are refused until the pairing is confirmed this way).
FRESHBOOKS_REQUEST_TIMEOUT_MSPer-request deadline for API calls, in milliseconds (default 30000)

Local servers need a runtime on your machine (e.g. Node.js 18+). Config file locations differ by client; see your client’s docs.

Packages
npm@chrischall/freshbooks-mcp1.2.2

Tools

34

Saved tool list · updated 2w ago

This tool takes no parameters.

Details

From the project README

freshbooks-mcp

MCP server for FreshBooks — invoices, clients, estimates and payments, exposed to Claude as typed tools.

This project was developed and is maintained by AI (Claude Code). Use at your own discretion.

Install

npm install -g @chrischall/freshbooks-mcp

Setup

FreshBooks is OAuth2 only — there is no API key and no personal access token, so a one-time browser authorization is required.

  1. Register an app at https://my.freshbooks.com/#/developer. The redirect URI must be HTTPS with no query string; https://localhost works and never needs to resolve.
  2. Note the Client ID and Client Secret.
  3. Complete the one-time authorization:
    • From the server itself (no script): set FRESHBOOKS_CLIENT_ID and FRESHBOOKS_CLIENT_SECRET, start it, then call freshbooks_auth_url, open the URL it returns, approve, and pass the whole URL you land on to freshbooks_auth_exchange. Its state must match a consent URL the server issued (valid 15 minutes, single-use), so a bare code, or a redirect URL from anyone else's authorization, is refused before its code is spent. Local sessions save the tokens directly to FRESHBOOKS_TOKEN_STORE and return only a success message. Restart the local MCP server after that message. No token needs to be copied into chat or configuration. Keep the same app credentials and store path after restart.
    • Hosted connections: mcp-host's authFlow still captures the refresh token from the exchange response and supplies it through its secret store. MCP_DATA_DIR identifies this hosted environment. Do not set it for a local coding-agent connection: hosted mode intentionally returns the token. Issued states are kept as digests under MCP_DATA_DIR, so a child that is respawned while the person approves can still complete the login. The registration's flow must show the person the authorize_url that freshbooks_auth_url returns. That URL carries the one-time state, and the exchange refuses any redirect without it. A static consent link typed into a step description has no state, so every new login or repair through it fails. Carry authorize_url from the freshbooks_auth_url step and show it with a step link (link: {from: 'authorize_url', hosts: ['auth.freshbooks.com']}, mcp-host 2.30.0 or later) on the step that prompts. That prompt must collect the whole redirect URL, not just its code. Make this change before a release with state checking reaches the hosted pin. Existing connections keep refreshing; only new logins and repairs depend on it.
    • Legacy/manual setup: the scripts in skills/freshbooks-curl print tokens. Run them only in a private terminal, never in an agent's captured terminal.
  4. Configure:
FRESHBOOKS_CLIENT_ID=...
FRESHBOOKS_CLIENT_SECRET=...
FRESHBOOKS_REFRESH_TOKEN=...       # optional locally; if set during reauth, keep it set
FRESHBOOKS_REDIRECT_URI=https://localhost   # optional; must match what you registered
FRESHBOOKS_TOKEN_STORE=~/.freshbooks-mcp/session.json   # optional
FRESHBOOKS_BUSINESS_ID=...         # optional; required for writes if you belong to several businesses
FRESHBOOKS_ACCOUNT_ID=...          # optional; with FRESHBOOKS_BUSINESS_ID, confirms its accountId when FreshBooks omits it
FRESHBOOKS_REQUEST_TIMEOUT_MS=30000  # optional; per-request deadline for API calls

In Claude Desktop, both are optional fields in the extension's settings. If FreshBooks returns no account_id for the chosen business, invoice/expense writes are refused (projects and time entries still work) until you set both IDs yourself.

⚠️ Refresh tokens rotate

FreshBooks issues a new refresh token on every refresh and immediately invalidates the old one. This server persists each rotation to FRESHBOOKS_TOKEN_STORE (mode 0600) before the refresh is considered complete, and prefers the stored token over the environment value — the stored one has rotated past it.

The login flow also writes a small file of pending consent state digests (mode 0600, entries expire after 15 minutes). Locally it sits beside the token store, at <FRESHBOOKS_TOKEN_STORE>.oauth-state.json (by default ~/.freshbooks-mcp/session.json.oauth-state.json). Hosted, it is $MCP_DATA_DIR/freshbooks-oauth-state.json. It is created on the first freshbooks_auth_url call, even if the login is never finished, and holds no tokens.

Two consequences worth knowing:

  • Do not point two tools at the same store. The MCP server and the freshbooks-curl skill keep separate state files on purpose; sharing one makes them spend each other's tokens and locks both out.
  • If the store is lost, re-run the bootstrap. A spent refresh token cannot be recovered.

Local sign-in without an environment token binds the private store to the app's client ID, secret, and redirect URI. Changing those requires a new sign-in. For existing environment-token setups, the stored rotation still takes precedence; changing FRESHBOOKS_REFRESH_TOKEN to a new value starts a new token chain. If you reauthorize locally while FRESHBOOKS_REFRESH_TOKEN is set, keep that same value configured: the saved login is bound to it. To remove the variable, first unset it and complete local sign-in again so the store is bound to the app instead. The local authorization exchange and save use the same lock as refreshes. A refresh writes only while it holds that lock, so it cannot overwrite a newer login after releasing the lock. Restart every local server using the store after reauthorization.

Tools

ToolPurpose
freshbooks_get_identityResolve accountId / businessId / businessUuid
freshbooks_auth_urlGet the consent URL to authorize this connection
freshbooks_auth_exchangeComplete authorization; save tokens privately in local sessions or return the refresh token to the hosted auth flow
freshbooks_healthcheckVerify the OAuth credential and FreshBooks reachability; distinguishes "no credential" from "rejected" from "FreshBooks is down"
freshbooks_list_invoices / freshbooks_get_invoiceBrowse and fetch invoices
freshbooks_list_clients / freshbooks_get_clientBrowse and fetch clients
freshbooks_list_estimates / freshbooks_get_estimateBrowse and fetch estimates
freshbooks_list_payments / freshbooks_get_paymentBrowse and fetch payments
freshbooks_list_items / freshbooks_get_itemBrowse and fetch catalogue items
freshbooks_create_clientCreate a client — confirmation-gated
freshbooks_create_invoiceCreate an invoice — confirmation-gated
freshbooks_update_invoiceUpdate an invoice — confirmation-gated
freshbooks_record_paymentRecord a payment against an invoice — confirmation-gated
freshbooks_accept_estimateAccept an estimate (action_accept) — confirmation-gated, idempotent
freshbooks_update_estimateUpdate an estimate's lines, notes, terms, presentation — confirmation-gated
freshbooks_send_estimateEmail an estimate to the client (action_email) — confirmation-gated
freshbooks_decline_estimateAlways fails: FreshBooks has no decline. Answers with the alternatives
freshbooks_list_expenses / freshbooks_get_expenseBrowse and fetch expenses
freshbooks_list_expense_categoriesCategories supplying categoryid for new expenses
freshbooks_create_expenseRecord an expense — confirmation-gated
freshbooks_list_projects / freshbooks_get_projectProjects (businessId-keyed)
freshbooks_create_projectCreate a project — confirmation-gated
freshbooks_list_time_entriesTracked time, with total_logged / total_unbilled
freshbooks_create_time_entryLog time in seconds — confirmation-gated
freshbooks_list_servicesBillable work types for projects and time entries
freshbooks_list_records / freshbooks_get_recordGeneric accessor for the accounting long tail (taxes, credit notes, invoice profiles, tasks, staff, gateways, bills, bill vendors, bill payments, other income)

Confirmation-gated means the tool asks you before it writes. A client that can show a confirmation prompt (Claude Code) shows one with exactly what would be sent. Elsewhere (claude.ai, Claude Desktop) the first call makes no network call and returns a preview of the method, path and body that would be sent, plus a single-use confirmToken; only a repeat call with that token and the same arguments performs the write. A token is refused if any argument changed since the preview (DRAFT_CHANGED), if it was already used (TOKEN_REUSED) or once it expires.

Confirmations
variabledefault
MCP_CONFIRM_MODEask-userWhat a write does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). ask-user: two steps — the first call does nothing and returns a preview plus a token, and the model must get your approval in chat before calling again with it. auto: the same two steps, but the model may use the token after reviewing the preview itself. refuse: writes are refused on such clients. A client that can show prompts (Claude Code) always gets the real prompt, unless MCP_CONFIRM_ELICITATION=off. An unrecognised value is treated as refuse.
MCP_CONFIRM_ELICITATIONonoff never shows a confirmation prompt, so every client gets the MCP_CONFIRM_MODE behaviour. Set it for a client that says it can show prompts but never does (the write hangs — opencode 2.0.x). Any other value is treated as on, with a warning on stderr.
MCP_CONFIRM_TTL_SECONDS600How long a token stays valid.
MCP_CONFIRM_SECRETrandom per processSigning key; set it only if tokens must survive a server restart.
Estimate writes

Acceptance is an action on the estimate, not a status field: status (int), display_status and ui_status are computed and read-only, and they disagree with each other by design (a viewed estimate reads status: 3, display_status: "viewed", ui_status: "open"). Accepting is PUT estimates/estimates/{id} with {"estimate": {"action_accept": true}} — see docs/FRESHBOOKS-API.md for where that shape comes from.

  • Accept is idempotent. An estimate already accepted (or invoiced) comes back with changed: false and no write is sent — acceptance cannot be undone through the API, so a repeat call must not re-fire it.
  • There is no decline. FreshBooks' estimate statuses are draft / sent / viewed / replied / accepted / invoiced; no declined state, no action_deny, no estimate.decline webhook. freshbooks_decline_estimate exists only to say so and point at the alternatives, rather than leave an agent to invent a write that changes nothing.
  • Every write returns the re-fetched estimate, plus before / after state and changed / changedFields, so success is verified against the record rather than inferred from a 200. changed covers the status fields and the fields that write actually set, so a successful notes edit reports changed: true even though no status moves. On freshbooks_send_estimate it describes the record only — emailing an already-sent estimate moves nothing, and retrying on changed: false would send the client a second copy.

Writes require an owner/admin accounting account

FreshBooks separates the role you hold on a business from the role you hold on an accounting account. You can own a business that has no accounting account (account_id: null) while being only a client on the account you can actually see — in which case reads succeed and every write returns 403 Permission Denied, even though your OAuth token carries all the :write scopes.

freshbooks_get_identity reports accountRole and businessRole so this is visible up front. If accountRole is client, the invoicing write tools will not work against that account — that is an account permission, not a configuration problem.

Two things the API reports misleadingly
  • total counts records you may not be able to read. Expenses reported total: 16 while returning zero rows. List results attach a note when that happens, so it reads as a permission boundary rather than an empty account.
  • Projects and time tracking are keyed by businessId, not accountId, and paginate under a meta block instead of flat page/pages/total. They also work on a business with no accounting account at all.

The three identifiers

FreshBooks hands out three non-interchangeable ids, and using the wrong one returns a bare 404 that reads like a missing record:

IdentifierUsed by
accountId (alphanumeric)/accounting/account/…, /payments/account/…
businessId (integer)/projects/business/…, /timetracking/business/…
businessUuid (UUID)/accounting/businesses/…

Call freshbooks_get_identity first. Full API notes, including the four different error envelopes, are in docs/FRESHBOOKS-API.md.

Shell access without the server

skills/freshbooks-curl covers the same API from a shell with curl + jq, including the OAuth bootstrap and rotation-safe token handling.

Development

npm install
npm run build
npm test

License

MIT

Comments

Markdown supported

New to MCP?

From what the protocol is to installing your first server, these answers cover most first questions.

An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.