Concur Expense MCP Server
Provides tools for managing SAP Concur expense reports, including creating and updating reports, creating and filling expenses, attaching receipts and attendees, reading expense data, and searching attendee/location catalogs, while leaving submission and approval to human users.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Concur Expense MCP ServerCreate a new expense for taxi to airport"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Concur Expense MCP Server
An HTTP-transport MCP server that lets an AI agent (primarily Cursor) file and manage SAP Concur expense reports on your behalf — discovering expense types and form fields, creating and filling out expenses, attaching receipts and attendees, and reading everything back. It never submits or approves reports; that step is deliberately left to a human in the Concur web UI.
Prerequisites
Node.js ≥ 20.
Concur app registration (admin-side): a client ID/secret with redirect URI
http://127.0.0.1:8765/concur/callbackand the scopes your tenant grants (this design was verified against 17 scopes, including the legacyEXPRPT+IMAGEscopes).Credentials in Keeper: a record holding the client ID, client secret, and a company refresh token; note its record UID.
Keeper Commander installed (
pip install keepercommander) with persistent login enabled, so commands run non-interactively:keeper shell > this-device register > this-device persistent-login onPort 8765 free on the machine running the server — the Concur redirect URI pins it.
Related MCP server: Extend AI Toolkit MCP Server
Setup
npm install
cp .env.example .envEdit .env. Each secret can be a plain value or a *_COMMAND whose stdout (trimmed) is used as the value — priority is command > plain value > (for the company refresh token only) the persisted store in .secrets/tokens.json:
CONCUR_GEOLOCATION=https://us2.api.concursolutions.com
CONCUR_CLIENT_ID_COMMAND="keeper get <record-uid> --format=json --unmask | jq -r '.custom[]|select(.name==\"client_id\").value'"
CONCUR_CLIENT_SECRET_COMMAND="keeper get <record-uid> --format=json --unmask | jq -r '.custom[]|select(.name==\"client_secret\").value'"
CONCUR_COMPANY_REFRESH_TOKEN_COMMAND="keeper get <record-uid> --format=json --unmask | jq -r '.custom[]|select(.name==\"refresh_token\").value'"
# Plain-value fallbacks also accepted: CONCUR_CLIENT_ID=..., CONCUR_CLIENT_SECRET=..., CONCUR_COMPANY_REFRESH_TOKEN=...The refresh-token command is bootstrap-only: once the server has run once, it persists token rotations to .secrets/tokens.json and prefers the stored value over the command on every subsequent start.
Setting up the Keeper record
Find an existing record's UID, or create one with the expected custom fields:
keeper search concur # or: keeper list — note the record UID
keeper record-add --title "Concur MCP" \
"custom.client_id=<your client id>" \
"custom.client_secret=<your client secret>" \
"custom.refresh_token=<your refresh token>"The *_COMMAND examples above assume custom fields named exactly client_id, client_secret, and refresh_token. If your record uses different names, inspect it and adjust the select(.name==...) filters:
keeper get <record-uid> --format=json --unmask | jq '.custom'Start the server. The start script does not auto-load .env, so export the variables into the environment first:
set -a; source .env; set +a
npm startThis serves http://127.0.0.1:8765/mcp and prints the bearer-key fallback URL (http://127.0.0.1:8765/auth/login) to the console.
Cursor configuration
Add to ~/.cursor/mcp.json (global) or <project>/.cursor/mcp.json:
{ "mcpServers": { "concur-expense": { "url": "http://127.0.0.1:8765/mcp" } } }First connect: Cursor gets a 401 challenge, discovers the server's authorization metadata, registers itself via Dynamic Client Registration, and opens a browser for you to sign in to Concur once. The session then binds to that Concur user; your user refresh token is stored, so subsequent connects are silent — no repeated logins.
Fallback for clients whose MCP OAuth is broken, using the static bearer key from http://127.0.0.1:8765/auth/login:
{ "mcpServers": { "concur-expense": {
"url": "http://127.0.0.1:8765/mcp",
"headers": { "Authorization": "Bearer <key from http://127.0.0.1:8765/auth/login>" } } } }Tool catalog
21 tools, registered in src/mcp/buildServer.ts, discoveryTools.ts, writeTools.ts, and readTools.ts.
Session/identity
concur_whoami— profile, expense policies, and who the user may act for as a delegate. Call this first in any session.
Discovery
concur_list_expense_types— usable (leaf, non-broken) expense types for the acting user's default policy.concur_get_expense_form_fields— writable form fields for a specific expense, with per-field guidance.concur_get_report_form_fields— report-header form fields (name, business purpose, custom fields) for a policy.concur_get_list_values— valid values for a list-type form field, bylistId.concur_list_payment_types— payment types (e.g. Cash, Company Card) with the v3 ID needed to create an expense.concur_list_attendee_types— attendee types configured for the acting user (e.g. Business Guest, Spouse).concur_search_locations— search the location catalog (city/venue) to fill the Location field.
Write
concur_create_report— create a new expense report header (echoes the created header back).concur_update_report— update an existing report's name and/or business purpose.concur_create_expense— create an expense inside a report; returns the type's form fields.concur_update_expense_fields— fill or change any writable fields on an expense (one or many at once); routes each field to its verified write path and returns a filled-field manifest + web link.concur_add_attendees— add one or more attendees to an expense, merge-preserving anyone already associated (Concur's association POST is a replace). Entries take either anemail(matched to an employee via their login, else to a guest viaexternalId, else creates a guest whenlastName+attendeeTypeCodeare also given) orlastName+attendeeTypeCode;transactionAmountis optional (omitted amounts auto-split evenly).concur_remove_attendees— remove specific attendees (attendeeIds) or all of them (all: true); Concur has no per-attendee delete, so removal replace-POSTs the remaining list (amounts re-split evenly).concur_attach_receipt— attach a receipt image or PDF to an expense.
Read
concur_list_reports— list the acting user's expense reports (legacy v3 listing), filterable by status or creation-date floor.concur_get_report— a report's header, its expenses, and a web deep link.concur_get_expense— a single expense, optionally with a filled/empty field manifest against its type's form.concur_get_expense_attendees— attendees currently associated with an expense.concur_search_attendees— search the attendee catalog: exact by type and/orexternalId(the only filters Concur honors), or fuzzy viasearch=<fragment>(substring match on name/company/externalId over a short-lived guest cache — the API itself has no name search).
Delegate
concur_list_delegators— users the session user delegates for, with permission flags (canPrepare,canSubmit,canViewReceipts).Every read/write tool above also accepts an optional
onBehalfOfUserId, resolved through the delegate permission gate before anything runs on that person's data.
Deliberately absent: there is no submit or approve tool, at any point in the tool list. Filing stops at a complete, reviewable draft.
The guided flow
Every tool result returns {data, nextSteps: [string], warnings?: [string]}, so the calling agent always knows what to call next and why. The intended sequence:
concur_whoami ─► concur_list_expense_types ─► concur_create_report (or pick via concur_list_reports)
─► concur_create_expense (v3 POST: type, date, amount, currency, payment type, core fields)
─► concur_get_expense_form_fields (formFields?expenseTypeId=…) ◄── uses the just-created expense as seed
─► concur_get_list_values (per listId field; agent picks by code/value)
─► concur_update_expense_fields (v3 PUT for core fields; compact v4 PATCH customData for list/custom fields)
─► concur_add_attendees (create/search + associate) [types whose form requires attendees]
─► concur_attach_receipt (Image v1 with v3 key)
─► concur_get_expense (verify manifest) ─► repeat for next expense ─► done (never submit)On create/update, the response includes a filled-field manifest (every field set — label, value, and for list fields the chosen item's code/value — assembled from a post-write read-back) and a web deep link (https://us2.concursolutions.com/nui/expense/report/{reportId}) so the agent can tell the user exactly what was filed and hand them a link to review it.
Testing
npm test # 126 unit tests (vitest)
npm run typecheckTwo smoke suites validate the design end-to-end against the live Concur stage tenant, run as the logged-in user and as a delegate acting for a permitted delegator (plus a negative case for someone who isn't a delegate):
python3 scripts/smoke-preflight.py self # API-level preflight, as the logged-in user
python3 scripts/smoke-preflight.py delegate # API-level preflight, on behalf of a permitted delegator
npx tsx scripts/smoke-mcp.ts self # MCP-tool-level, against the running server, as the logged-in user
npx tsx scripts/smoke-mcp.ts delegate # MCP-tool-level, on behalf of a delegator + a negative refusal casesmoke-preflight.py proved the underlying Concur routing directly against the API before implementation (5/5 self, 5/5 delegate: 5 distinct leaf expense types × 5 distinct departments each, every writable required+optional field filled, receipts bound, attendees added where required, deep links generated). smoke-mcp.ts is the MCP-level counterpart — it drives the running server as a real MCP client (no raw Concur HTTP), exercising the actual registered tools end to end, and passed 5/5 self, 5/5 delegate, plus the negative delegate case correctly refused.
Security notes
Localhost-only bind: the server listens on
127.0.0.1:8765only — never on all interfaces — because that's the address baked into the Concur app's registered redirect URI (http://127.0.0.1:8765/concur/callback).Secrets on disk:
.secrets/tokens.json(company + per-user tokens) and.secrets/mcp-clients.json(DCR registrations + issued MCP tokens) are written with restrictive permissions (directory0700, files0600), atomically, and are git-ignored.Token policy: user-scoped reads and writes always use the session user's own token. The company token is used only for two verified exceptions: (1) non-user config/catalog reads, and (2) the compact v4 expense
PATCHfor list/custom/orgUnit fields, which Concur's API rejects for user tokens outright (401, "only accessible by a valid company") — the server wraps that call so it only ever runs for the session's authenticated user, or for a delegator after a permission check. The company token never performs user activity outside those two exceptions.Per-session user binding: no tool call executes without an authenticated Concur user bound to the MCP session — company credentials alone never grant tool access.
/auth/loginreuses the existing session for any local caller: once a Concur user has completed the browser login once, hittinghttp://127.0.0.1:8765/auth/loginagain — from any process on the machine — mints a fresh 30-day bearer for that same logged-in user, with no re-authentication. This is deliberate (login-once-reuse UX, not a bug), but it means anyone able to reach127.0.0.1:8765can obtain a bearer once a session exists. The mitigation is not to run this server on a shared or multi-user machine.Delegate gating with cross-user refusal: acting on behalf of a delegator requires
concur_list_delegatorsto showcanPreparetrue for that person first; the server refuses with a clear tool error for anyone else (not a delegate at all, or a delegate withoutcanPrepare), so one user's session can never read or write another user's expense data without a verified delegate relationship.No submit/approve tools, by design: the tool catalog stops at a complete, filled draft. There is no code path anywhere in this server that submits or approves a report — that action is left to a human in the Concur web UI.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityDmaintenanceAn MCP server that helps AI assistants manage expense-sharing for social events, enabling the creation of gatherings, tracking of expenses, and calculation of fair reimbursements among participants.Last updatedApache 2.0
- Alicense-qualityBmaintenanceAn MCP server that enables AI agents to interact with Extend's spend management APIs, allowing virtual card management, transaction tracking, and receipt processing through natural language.Last updated17MIT
- AlicenseAqualityDmaintenanceAn MCP server for Splitwise that enables users to manage shared expenses, friends, and groups directly through AI assistants. It allows for creating, deleting, and listing expenses while providing tools to track net balances and group debts.Last updated8483MIT
- Flicense-qualityCmaintenanceAn AI-powered expense management server that enables adding, searching, and analyzing expenses using natural language through the Model Context Protocol.Last updated
Related MCP Connectors
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
Log, query, and edit expenses, budgets, and accounts in Ledgy from any MCP-compatible AI assistant.
A paid remote MCP for AI agent browser approval MCP, built to return verdicts, receipts, usage logs,
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/bharath2020/concur-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server