MCP server (AI agents)
The Contractor Codex ships its own Model Context Protocol server. Point Claude Desktop, Claude Code, or any MCP-compatible client at it, hand it an API key, and the model can run your portal — clock in and out, send invoices, draft quotes and contracts, mark payments paid, refund mistakes, post updates to your clients — without you opening the admin UI.
It sits on the same backend the admin and client portals use. No separate database, no separate auth. Every agent call lands an audit-log row, so you can see exactly what the model did and when.
- Enable MCP. Settings → AI & Agents → MCP Server (inbound) → toggle on.
- Create a key. Same screen → Manage API Keys → New key. Pick the write groups to grant. The full key shows once — copy it now.
- Wire it into your client. Drop this into your MCP client config:
{
"mcpServers": {
"contractor-codex": {
"url": "https://contractorcodex.com/api/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}Using the Claude Code CLI instead?
claude mcp add contractor-codex --transport http \
--url https://contractorcodex.com/api/mcp \
--header "Authorization: Bearer YOUR_API_KEY"Once connected, the client lists all 54 tools automatically and the agent can start working.
Until MCP is enabled and a valid key authenticates, every request is rejected — 401 without a valid key, 403 for your org until the toggle is on. A leaked key alone can't do anything if MCP is off.
How it works
Claude (or any MCP client)
↓ JSON-RPC over HTTPS, Bearer-token auth
https://contractorcodex.com/api/mcp
↓ validates token, checks your org's mcp_enabled flag
runAgentTool() dispatcher
↓ same handler as the /api/agent/<tool> HTTP route
your data
The MCP transport and the HTTP /api/agent/* routes call the same handler functions. Adding a tool exposes it on both surfaces at once — they never drift apart.
The mental model — three layers
Reading top to bottom, you get safer:
| Layer | What it does | Examples |
|---|---|---|
| Read | Surface data; no side effects. | get_context, get_projects, find_client, daily_briefing |
| Write (no comms) | Mutate state; no email, no money. | start_work_session, add_session_note, create_project, update_client |
| Write (comms or money) | Sends email, moves money, or destroys state. Two-step confirm required. | send_invoice, mark_paid, send_quote, refund_invoice, archive_client |
How the agent asks the right questions
Every write tool that takes a "which one?" parameter returns a structured needs_input response when called without it — instead of erroring. The response carries questions (what to ask), inventory (the real options — recent clients, draft invoices, available templates…), and next_step. The agent grounds in your actual data instead of inventing an ID.
AGENT → mark_paid() (no args)
← needs_input · inventory.open_invoices: [Henderson $2,400, Mosi LLC $800, …]
AGENT "I see two open invoices for Henderson — $2,400 and $1,100. Which one?"
USER "The 2400."
AGENT → mark_paid(snapshotId="…", method="check")
← confirmation_required · preview + confirmation_token
AGENT "Mark the $2,400 Henderson invoice paid via check?"
USER "Yes."
AGENT → mark_paid(snapshotId="…", method="check", confirmation_token="ct_…")
← marked_paid
Two structured round-trips before anything changes. No hallucinated IDs, no guessed amounts.
The two-step confirmation rule
Every tool that sends an email, moves money, or destructively changes state goes through a mandatory two-step confirm, regardless of dollar amount:
- First call returns
confirmation_required+ aconfirmation_token+ a preview the agent reads aloud. - Second call repeats the params plus the token. The tool executes.
Tokens expire after 60 seconds and are bound to (keyId, action, paramsHash) — they can't be replayed or reused for a different action.
send_client_message is intentionally single-call — voice agents read the body aloud before sending, and the round-trip would slow down high-frequency client comms.
Auth model
| Concept | Detail |
|---|---|
| Scope | admin (any client in your org) or client (bound to one client). Keys minted in this screen are admin-scoped. |
| Storage | Stored as SHA-256 hashes. The full key shows once at creation. |
| Expiry | Set per key via expiresAt. No auto-rotation — revoke and reissue. |
| Org gate | The endpoint returns 403 for your org until mcp_enabled is on (and 401 for any request without a valid key). |
| Audit | Every call writes an audit-log row (key, tool, status, IP, duration). |
| Rate limit | 30 calls/min per admin key, 60/min per client key (per-tool overrides available). String numeric params are accepted everywhere. |
For a read-only key, untick every write group at creation — read tools require no write permission.
The tool catalog
54 tools, grouped by what they touch. Expand a group to see its tools.
Context & identity
| Tool | Purpose |
|---|---|
get_context | The boot call. Org name, your scope, system instructions, workflows, an inventory snapshot, and the endpoint catalog. Call this first. |
whoami | The key's scope + bound client, if any. Lightweight identity check. |
get_help | Keyword search across this docs site. |
get_branding | Business name, support email, portal greeting, accent color, logo, default currency. |
Admin overview — read
| Tool | Purpose |
|---|---|
admin_get_clients | All active clients with health scores and project counts. |
admin_get_insights | Revenue (net of refunds), overdue counts, dunning stats, health distribution. |
admin_get_activity | Org-wide activity log. |
daily_briefing | "What needs my attention today?" — pending approvals, drafts, overdue invoices, payments collected (net of refunds). |
overdue_ar | Overdue invoices with aging buckets. |
quote_status_check | Quote pipeline: open, signed, expiring. |
Search & lookup
| Tool | Purpose |
|---|---|
find_client | Resolve a natural-language name into a customerId. (admin) |
find_project | Resolve a project name into a projectId. |
find_recent | Last N quotes / contracts / invoices / clients / projects, newest first. (admin) |
find_active_session | The work session currently running, if any. |
Client data — read
| Tool | Purpose |
|---|---|
get_projects | A client's projects with session status, hours, billing profile. |
get_billing | A client's invoices plus a canonical summary (owed, net paid, invoiced, refunded) and retainer usage. |
get_notifications | A client's recent notifications. |
get_activity | A client's own activity log. |
Settings & status — read
| Tool | Purpose |
|---|---|
get_settings | Full admin-settings map (timezone, invoice defaults, payment methods, dunning, late fees, quote defaults). |
get_billing_profile | Legal name, DBA, entity type, EIN last-4, address, tax status, plus a complete boolean. |
get_stripe_connect_status | DB flags plus a live Stripe diagnostics call (charges / details enabled). Handles webhook lag. |
Catalog
| Tool | Purpose |
|---|---|
list_quote_templates | Saved whole-quote templates. |
list_line_item_templates | The line-item library (built-in starter items + your own). |
list_surcharges | Surcharge templates. |
list_contract_templates | The contract-template library. Built-in starter templates auto-seed the first time it's called. |
save_to_catalog | Promote freeform line items / surcharges from a quote into reusable templates. |
Clients — write
| Tool | Purpose |
|---|---|
create_client | Find-or-create by (org, email). Readies a portal-signup invite. |
update_client | Patch name, phone, company, subscription, industry, timezone. Elicits a recent-clients list if customerId is missing. |
archive_client | Soft-archive. Two-step confirm. Elicits if customerId missing. |
restore_client | Clears archive/delete flags. Lists archived clients if customerId missing. |
Projects — write
| Tool | Purpose |
|---|---|
create_project | Locks a pricing snapshot. Elicits clients + billing profiles if anything's missing. |
update_project | Patch name, description, billing profile, budget. |
complete_project | Marks done + completion note. Refuses if a session is still running. |
add_project_surcharge | Mid-job surcharge. Auto-approved when pre-disclosed, otherwise emails the client for approval. Confirm. |
Sessions
| Tool | Purpose |
|---|---|
start_work_session | Clock in. Elicits active projects if projectId missing; returns a conflict if already running elsewhere. |
stop_work_session | Clock out. Auto-resolves the active session if sessionId omitted. |
add_session_note | Append a note to a session. |
set_session_billable | Flip a session's billable flag. Refuses if the session is already on a sent invoice — void and re-issue first. |
correct_session | Adjust a session's start / end times. Same invoiced-session guard. |
Project updates & notes
| Tool | Purpose |
|---|---|
list_project_updates | Newest-first thread with replies. |
post_project_update | Admin status note — emails the client and shows on their dashboard. Two-step confirm. (admin) |
reply_to_project_update | Threaded reply. Notifies admins for client replies; no email. |
Quotes & contracts
| Tool | Purpose |
|---|---|
create_quote | The marquee tool. Cold-call mode: tell it the client wants a quote and it asks every question (recipient, line items, billing mode, deposit %, expiry) with tailored catalog suggestions. |
send_quote | Email the signing link, pre-signed with the key-owner's saved signature. Two-step confirm. Elicits draft quotes. |
create_contract | Draft an agreement or change order from a template. Elicits a template picker (built-ins included) when templateId is missing; find-or-creates the client. |
send_contract | Same as send_quote for agreements / change orders. Two-step confirm. |
request_approval | Client approval flow for budget overruns or undisclosed surcharges. Two-step confirm. |
Invoices & payments
| Tool | Purpose |
|---|---|
send_invoice | Finalize + email a draft. Two-step confirm. Elicits drafts. Supports dry_run. |
mark_paid | Out-of-band payment (check, wire, cash). Supports partial amounts (amount_paid_cents) — adds to prior payments and flips to paid only when fully covered. Two-step confirm. |
list_invoice_drafts | Snapshots with no Stripe invoice or draft status. Filterable by client. |
void_invoice | Void an open invoice in Stripe. Two-step confirm. (Refuses a paid invoice — refund instead.) |
refund_invoice | Refund a paid invoice; partials supported; platform fee reverses proportionally. Two-step confirm. |
mark_invoice_uncollectible | Write off an open invoice as bad debt. Two-step confirm. |
Client comms
| Tool | Purpose |
|---|---|
send_client_message | Free-form email to a client. Single-call (no token) — the agent reads the body aloud first. Elicits recipient + subject + body when missing. |
Both look up the saved signature of the admin who minted the key and stamp it before the client email goes out — matching the human composer flow. The signer of record is whoever owns the key, so multi-admin teams get correct attribution. No saved signature → the send still goes through unsigned and the response returns a warning the agent should read back. Save one at Settings → Signatures and every future send is pre-signed. (Change orders skip pre-sign — they amend an already-signed parent.)
Not on the agent surface yet
Visible to humans in /admin but no MCP tool yet: calendar events, accepting the inbound approval queue, project-level discounts, recurring product costs, delivery requests, settings writes, and org-team CRUD. Need one end-to-end? Contact support — we ship new tools fast.
Troubleshooting
If your client cached the tool list before a platform update, restart the MCP connection — the next initialize refreshes everything.
- Confirmation token expired — tokens TTL at 60s. Re-call the same tool without the token; the response carries a fresh preview.
- Insufficient permissions — the key lacks the right write group. Edit it under Settings → API Keys.
- Customer not found in this organization — an admin key passed a
customerIdfrom a different org. Keys never see across orgs. - Stripe Connect is not configured — the org hasn't finished Connect onboarding. Call
get_stripe_connect_statusto see where they are; you can't send or collect invoices without it.
Best practices for client developers
- Call
get_contextfirst. It returns the workflows map, inventory snapshot, system instructions, and endpoint catalog — the whole session leans on it. - Never pre-fill IDs from training data. When a tool returns
needs_inputwith an inventory, use those IDs verbatim. Hallucinated IDs are the #1 failure mode. - Read previews back to the user word-for-word before the second (committing) call.
- On second thoughts, start fresh. If the user changes their mind between calls, re-ask the params and issue a new token.
- Use
dry_run="true"onsend_invoice/mark_paidto preview the dollar amount without locking a confirmation token.
Audit & observability
Every call lands an AgentAuditLog row — tool name, status, IP + user agent, duration, and the linked key. View recent calls under Settings → API Keys → Activity. The same data feeds daily_briefing's activity rollup (calls per day, error rate, top tools).
