Skip to main content
Glama
expensify-mcp

expensify-mcp

by BilalAtique

expensify-mcp

Full-capability MCP server for Expensify, built on the Integration Server API.

Expensify's official hosted MCP (expensify.com/mcp) is deliberately read-only — it "cannot approve transactions, edit data, or move money." This server adds the write surface the Integration Server API actually exposes: creating expenses and reports, managing policies, categories, tags, members, approval routing, and expense rules.

What this can and cannot do

Can do (17 tools):

Tool

Type

Purpose

expensify_list_policies

read

List workspaces + IDs

expensify_get_policy

read

Categories, tags, report fields, tax rates, employees

expensify_get_domain_cards

read

Corporate card assignments

expensify_export_reports

read

Export reports → filename

expensify_export_card_reconciliation

read

Export card transactions → filename

expensify_download_file

read

Fetch an exported file's contents

expensify_create_expenses

write

Create expenses on an account

expensify_create_report

write

Create a report, optionally with expenses

expensify_mark_reports_reimbursed

write

Approved → Reimbursed

expensify_create_policy

write

New workspace

expensify_update_policy_categories

write

Merge/replace categories

expensify_update_policy_tags

write

Merge/replace tag groups

expensify_update_employees

write

Add/update members, roles, routing

expensify_remove_employees

write

Remove members

expensify_update_tag_approvers

write

Per-tag approvers

expensify_create_expense_rule

write

Auto-tag / billable rules

expensify_update_expense_rule

write

Modify a rule

Cannot do — no supported API exists:

  • Approving a report. There is no approve endpoint. mark_reports_reimbursed only moves Approved → Reimbursed; getting a report to Approved is app-only.

  • Moving money. Marking Reimbursed is a bookkeeping flag recording that you paid outside Expensify. No ACH, no payment.

  • Submitting a report into the approval workflow, SmartScan OCR, card issuance/limits, bank account setup, most workspace settings, Concierge chat.

The practical ceiling is "create and configure everything, read everything, but cannot approve or move money."

Related MCP server: Concur Expense MCP Server

Setup

npm install
npm run build

Generate credentials at https://www.expensify.com/tools/integrations/. They are shown once.

cp .env.example .env   # then fill in the two credential values

Safety model

These tools write to real financial records. Expensify has no sandbox tier, so protection is enforced locally:

  • EXPENSIFY_DRY_RUN defaults to true. Mutating tools return a preview of the exact payload instead of sending it. Only the literal string false disables this — a typo fails closed.

  • EXPENSIFY_ALLOWED_POLICY_IDS (optional) refuses any mutation touching a policy outside the list.

  • EXPENSIFY_MAX_BATCH_SIZE (default 100) caps records per write.

  • Guards run before the dry-run check, so a blocked write is never even previewed.

  • The partner secret is redacted from every preview and error message.

Start with dry-run on, read the previews, then flip it off for the specific operation you intend.

Hosted deployment (optional)

The server also ships an HTTP transport at api/mcp.ts, so it can run on Vercel and be added as a custom connector instead of a local subprocess.

This hosts your credentials behind your token — it is not multi-tenant. Expensify's Integration Server API has no OAuth and no delegated access, so there is no way for other users to connect their own accounts through a hosted instance. Anyone with the URL and the bearer token acts as the account whose credentials are in the environment.

Required environment variables:

Variable

Purpose

EXPENSIFY_PARTNER_USER_ID

Your Expensify credential

EXPENSIFY_PARTNER_USER_SECRET

Your Expensify credential

MCP_AUTH_TOKEN

Bearer token gating the endpoint. Generate with openssl rand -hex 32

EXPENSIFY_DRY_RUN

Recommended true until you have tested the deployment

The auth gate fails closed: if MCP_AUTH_TOKEN is unset, every request is refused with a 503 rather than exposing an unauthenticated write endpoint. Requests without a valid Authorization: Bearer <token> header get a 401.

Add it as a custom connector with the deployment's /mcp URL and the bearer token. Rotate the token by updating the env var and redeploying.

Client configuration

Claude Code:

claude mcp add expensify -- node /absolute/path/to/expensify-mcp/dist/index.js

Or in .mcp.json / claude_desktop_config.json:

{
  "mcpServers": {
    "expensify": {
      "command": "node",
      "args": ["/absolute/path/to/expensify-mcp/dist/index.js"],
      "env": {
        "EXPENSIFY_PARTNER_USER_ID": "...",
        "EXPENSIFY_PARTNER_USER_SECRET": "...",
        "EXPENSIFY_DRY_RUN": "true"
      }
    }
  }
}

Live verification status

Verified against the real API on 2026-07-27 using a throwaway workspace.

Tool

Status

list_policies

verified

get_policy

verified

create_policy

verified — created the test workspace

create_expenses

verified

create_report

verified

update_policy_categories

verified — persistence confirmed by read-back

update_policy_tags

verified — see the data-loss warning below

update_tag_approvers

verified

create_expense_rule

verified — duplicate correctly rejected on re-run

export_reports

verified — exported 18 real reports

download_file

verified — retrieved the exported CSV

update_employees / remove_employees

untested — account returns 403

export_card_reconciliation

untested — needs a card domain

mark_reports_reimbursed

untested — needs an Approved report, unreachable via API

get_domain_cards

untested — needs a verified domain

Four bugs were found and fixed, every one of them a payload-placement mistake that this API reports opaquely:

  1. Categories and tags belong at the top level of the job description, not inside inputSettings. Nested, the API returns 200 and silently discards the change.

  2. The employee updater needs dataSource: "request", entity: "generic", and the roster in a separate data form field.

  3. Export jobs need onReceive.immediateResponse, and fileExtension goes in outputSettings, not inputSettings. Without it the request blocks and then fails with a bare 500 that looks like an outage.

  4. The download job takes fileName / fileSystem at the top level and no inputSettings at all.

All four are regression-tested. The lesson generalises: when this API returns a 500, or a 200 that changes nothing, suspect payload placement before concluding the endpoint is broken or the account is limited. Comparing against a raw curl built straight from the docs is the fastest way to tell.

Data-loss warning: tag merges

Verified against the live API: a tag group is replaced wholesale even with action: "merge". Sending a group with one tag deletes every other tag in that group. merge only protects groups you did not mention.

Always expensify_get_policy first and send the complete tag list plus your additions. Categories do not behave this way — they genuinely merge.

API conventions worth knowing

These bite hard, so the schemas enforce them:

  • Amounts are integer cents. 1234 means $12.34. Floats are rejected outright — passing 12.34 would otherwise post a 100×-wrong expense.

  • Dates are strictly yyyy-MM-dd.

  • Categories and tags must already exist on the policy. Call expensify_get_policy first.

  • action: "replace" on categories/tags deletes everything not in the payload. "merge" is the safe default.

  • Rate limits: 5 requests / 10s and 20 / 60s. Both windows are enforced client-side with a queue; 429s are retried with backoff.

  • responseCode 207 means partial success — check failedReports / skippedReports in the response.

Development

npm run dev          # run from source via bun
npm test             # 27 tests
npm run type-check   # tsc --noEmit, clean
npm run build

Tests cover the rate limiter's dual-window behavior, the write-guard matrix (dry-run, allowlist, batch cap, secret redaction), transport encoding, and error mapping. The server was additionally smoke-tested over the real MCP stdio protocol.

Structure

src/
  index.ts            # MCP server, tool registration, error formatting
  lib/
    config.ts         # env parsing, fail-closed dry-run
    client.ts         # form-encoded transport, 429 retry, error mapping
    rate-limiter.ts   # dual sliding windows, serialized
    write-guard.ts    # the single chokepoint for all mutations
    errors.ts         # typed errors with explicit constructors
    schemas.ts        # shared Zod schemas (cents, dates, currency)
  tools/
    read.ts           # policy + card reads
    export.ts         # report/reconciliation export + download
    write-expenses.ts # expenses, reports, reimbursement status
    write-policy.ts   # policies, categories, tags, members, rules

License

MIT — see LICENSE.

Available Tools

17 tools
expensify_create_expense_ruleB
Destructive

Create an expense rule that automatically applies a tag or billable status for an employee on a policy.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: actions (required), policyID (required), employeeEmail (required).

expensify_create_expensesA
Destructive

Create one or more expenses on a user account. Amounts are INTEGER CENTS (1234 = $12.34) and dates must be yyyy-MM-dd. Category and tag values must already exist on the policy — call expensify_get_policy first to check. Set externalID per expense to make re-runs traceable. Expenses can be attached to an existing report via reportID, or left standalone.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: employeeEmail (required), transactionList (required).

expensify_create_policyA
Destructive

Create a new Expensify policy (workspace). Returns the new policyID.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: policyName (required), type.

expensify_create_reportA
Destructive

Create an expense report on a policy, optionally with expenses attached in the same call. Returns the new reportID. Note: this creates the report in an unsubmitted state — the Expensify API cannot submit it for approval or approve it. Amounts are integer cents; dates are yyyy-MM-dd.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: title (required), policyID (required), employeeEmail (required), fields, +1 more.

expensify_download_fileA
Read-only

Download the contents of a file produced by expensify_export_reports or expensify_export_card_reconciliation, using the filename those tools return.

Inputs: fileName (required), fileSystem.

expensify_export_card_reconciliationA
Read-only

Export company card transactions for a given feed and date range, including transactions not yet attached to a report. Returns a filename to pass to expensify_download_file.

Inputs: endDate (required), startDate (required), domainName (required), feedName, +2 more.

expensify_export_reportsA
Read-only

Start an export of expense reports and return the generated filename. Filter by report IDs, date range, or approval state. Pass the returned filename to expensify_download_file to get the contents. Supply a freemarker template to control the columns, or omit it for a default CSV of reportID, name, status, date, merchant, amount, currency, category and tag.

This is the only API that enumerates reports — there is no list-reports job. Use it to count or inspect reports.

Inputs: state, endDate, template, startDate, +4 more.

expensify_get_domain_cardsA
Read-only

List corporate/domain card assignments, including bank source and import history. Requires domain admin rights on the account.

Inputs: domainName (required).

expensify_get_policyA
Read-only

Fetch configuration for one or more policies: categories, tags, report fields, tax rates, and the employee roster. Use this to discover valid category and tag names before creating expenses, since the API rejects values that do not already exist on the policy.

Inputs: policyIDList (required), fields.

expensify_list_policiesA
Read-only

List Expensify policies (workspaces) the authenticated account can see. Returns id, name, owner, role, type and output currency for each. Start here when you need a policy ID for any other tool.

Inputs: adminOnly, userEmail.

expensify_mark_reports_reimbursedA
Destructive

Mark already-APPROVED reports as REIMBURSED. This records that payment happened outside Expensify — it does NOT move money and does NOT approve anything. Reports not already in Approved state will be rejected by the API. This is the only report-status transition the API supports.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: reportIDList (required), paymentSource.

expensify_remove_employeesA
Destructive

Remove members from a policy. Sets isTerminated on each record, which is how the API removes someone from their assigned policy.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: policyID (required), employees (required), shouldRemoveFromUnassignedPolicies.

expensify_update_employeesA
Destructive

Add or update members on a policy, including role, manager, approval limits and routing. Members are matched by email and updated in place. employeeEmail, managerEmail and employeeID are required for each record. To remove someone use expensify_remove_employees.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: policyID (required), employees (required), notifyEmails.

expensify_update_expense_ruleA
Destructive

Modify an existing expense rule by its ruleID.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: ruleID (required), actions (required), policyID (required), employeeEmail (required).

expensify_update_policy_categoriesA
Destructive

Add, update, or replace expense categories on a policy. action="merge" upserts the supplied categories and leaves others untouched. action="replace" DELETES every category not listed in this call — use with care. maxExpenseAmount is in integer cents.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: action (required), policyID (required), categories (required).

expensify_update_policy_tagsA
Destructive

Add, update, or replace tags on a policy. Tags are grouped into lists; each group has a name and its own tags array.

DATA LOSS WARNING (verified against the live API): a tag group is replaced WHOLESALE even with action="merge". Any tag already in the group but absent from your payload is DELETED. Always call expensify_get_policy first, then send the full existing tag list plus your additions. action="merge" only protects OTHER groups, not tags within the groups you send.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: action (required), policyID (required), tagGroups (required).

expensify_update_tag_approversA
Destructive

Set or clear the approver for individual tags on a policy. Pass an empty string as approver to clear one. Only single-level tags are supported. Tag names must already exist on the policy.

WRITES to Expensify. Dry-run is currently ON — this will preview only.

Inputs: policyID (required), tagApprovers (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.1/5.0

    Scored across 17 tools

    Disambiguation5/5

    Every tool targets a distinct resource or action (e.g., policies, expenses, reports, employees, rules, exports). Even related tools like create/update expense rules are clearly separated, and operations like exporting vs downloading have different names and purposes.

    Naming Consistency5/5

    All tool names follow the exact pattern 'expensify_verb_noun' (e.g., list_policies, get_policy, create_policy, export_reports). The convention is uniform with no mixing of camelCase or other styles.

    Tool Count5/5

    17 tools cover the major workflows in expense management: policy lifecycle, expense/report creation, rule management, employee administration, and data export. The count feels comprehensive without being bloated.

    Completeness4/5

    Core CRUD operations are present for policies, expenses, reports, employees, categories, tags, and rules. The only notable gaps are lacking a policy delete tool and a direct list-reports endpoint (the export tool substitutes). Given API constraints, this is reasonable.

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      MCP server for personal finance management. Enables natural language expense logging, budgeting, recurring charge detection, and statement import with deterministic local calculations.
      -
    • F
      license
      Not graded
      quality
      D
      maintenance
      An MCP server for managing SAP Concur expense reports, allowing AI agents to create and update expenses, attach receipts and attendees, and read report data, while leaving submission and approval to humans.
      -
    • A
      license
      Not graded
      quality
      B
      maintenance
      MCP server for Expense, a receipt tracker that lets AI assistants capture receipts, log mileage, answer spending questions, build reports, and reconcile bank statements from your expense data.
      11 npm
      6
      ISC