thred-mcp
v0.9.4
Published
The tool layer behind Ask Thred — Ask, Do, and (soon) Watch over your finances: connects AI assistants to the Thred Partner API for accounting, invoicing, and financial reporting
Readme
thred-mcp
The tool layer behind Ask Thred — an AI finance assistant for business owners. This MCP server connects AI assistants like Claude and ChatGPT to the Thred Partner API, giving them direct access to accounting, invoicing, and financial reporting.
Ask Thred is organized around three capability categories: Ask (read-only questions about your business), Do (commands that change state), and Watch (proactive, system-initiated alerts — a future roadmap phase). This package ships the atomic tools that power Ask and Do today.
What you can do
Ask — questions about your business (read-only, user-initiated)
Ask Claude or ChatGPT things like:
- "How is the business doing?" — a plain-language business overview
- "What does our cash flow look like this quarter?"
- "What's pending right now?" — open invoices, unpaid bills, items needing attention
- "Who owes us money?" — accounts receivable and collections
- "How much are we spending with each vendor?" — accounts payable and vendor spend
- "What happens to cash if revenue drops 20%?" — scenario analysis
- "Show me a dashboard of revenue vs. expenses for the last 6 months" — custom, natural-language dashboards
- "Prepare my board pack for Q1" — a composed financial summary for stakeholders
Do — commands that change state (write/execute, user-initiated)
- "Create an invoice for Acme for $500"
- "Send the invoice to Acme"
- "Send a payment reminder to customers who are overdue"
- "Reach out to Office Supplies Co about the open bill"
- "Export the P&L report for Q1 2026"
- "Upload this invoice PDF" — just the file, no other data needed; AI classifies it as AR or AP, extracts amounts and counterparty, and creates the invoice/bill automatically
Do actions are governed by behavioral rules that the AI assistant applies on top of these tools:
- Confirmation before high-risk actions (sending money-related communications, voiding records).
- Slot-filling for missing required details (e.g. amount, due date) before executing.
- AI matching of spoken/typed customer and vendor names to the right Thred records.
- AI-composed emails for reminders and vendor outreach.
- Action chaining — suggesting the natural next step (e.g. after creating an invoice, offer to send it).
Watch — proactive alerts (future roadmap, not yet available)
Watch is the product direction for Ask Thred and is not implemented in this MCP server today. It describes system-initiated behavior, not capabilities you can call right now.
Planned Watch capabilities include:
- Overdue payment alerts
- Payment received notifications with auto-reconciliation
- Periodic business updates
- Cash-low warnings
- Unusual transaction flags
Voice (product direction)
Voice is a first-class input modality for the Ask Thred product (speech → transcription → the same intent pipeline that handles typed requests). The MCP server itself is text/tool based; voice is handled upstream in the product, not in this package.
Tools included
The server exposes a focused set of atomic tools. Composed capabilities — board packs, natural-language dashboards, payment reminders, vendor outreach — are not single dedicated endpoints; they are achieved by the AI assistant orchestrating these atomic tools and composing the content (narrative, emails, layouts) around the returned data.
| Category | Tools | Powers |
|----------|-------|--------|
| Businesses | list, create, get, update, archive | Ask (overview), Do (setup) |
| Customers | list (name_contains narrows by name), create, get, update, archive, unarchive | Do (name matching, AR), Ask (collections) |
| Invoices | list (customer_id, a due-date range, due_before, search and updated_since narrow it; there is no issue-date filter), list overdue (every overdue invoice with money still owed, oldest due date first, in one unpaged response), create, get, update, void | Do (create/send), Ask (pending, overdue) |
| Invoice Payments | list (customer_id or invoice_id narrows it), create, get, update, delete | Ask (cash in), Do (record payment) |
| Vendors | list (name_contains narrows by name), create, get, update, archive, unarchive | Do (vendor outreach), Ask (AP/spend) |
| Bills | list (vendor_id, a due-date range, search and updated_since narrow it; there is no bill-date filter), create, get, update, void | Ask (pending), Do (manage bills) |
| Bill Payments | list (vendor_id or bill_id narrows it), create, get, update, delete | Ask (cash out), Do (record payment) |
| Credits, refunds, receipts and payouts | list vendor credits, list customer credits, list vendor refunds, list receipts, list payouts | Ask (credits owed/held, refunds, receipts, payout timing) |
| Banking | list bank accounts (paged), list bank transactions (bank_account_id restricts to one account; the latter two tools need a business-scoped token — Friday refuses the partner-wide key), get bank transactions by category (same filters; totals cover only the returned page since Friday groups after paginating) | Ask (cash position, transaction search, spend by category) |
| Chart of Accounts | list, create, get, update, archive, hierarchy | Ask (reporting structure) |
| Financial Reports | P&L, balance sheet, cash flow, AR aging, AP aging, financial summaryget_expense_breakdown: expenses for a business over a date range, broken down by category (direct costs, operating expenses, finance costs, income tax) with each category's share of the total and its underlying GL accounts.get_ar_stats: entity counts across accounts receivable, i.e. invoices (all, unpaid, paid), customer refunds (all, processed), and credit notes (all, draft, applied). Counts, not amounts.get_ap_stats: entity counts across accounts payable, i.e. bills (all, unpaid, paid), vendor credits (all, unapplied, applied), vendor refunds (all, pending), and receipts (all, by payment method). Counts, not amounts. | Ask (overview, cash flow, AR/AP, board pack), Do (export) |
| Journal Entries | list, get (read-only) | Ask (what got booked to the ledger, and why) |
| Documents | upload, list financial documents, get financial document | Do (drop in an invoice/bill/receipt PDF or photo — AI classifies AR/AP, extracts data, creates the record) |
| Tasks | list tasks, get task (with Q&A thread), reply to task (with file attachments), attach files to a task message | Ask ("what is my bookkeeper waiting on?"), Do (answer the question, send the missing receipt) |
Three binaries: gated (default), privileged, and assist
The package ships three entry points over one codebase. The first two expose the same 71 tools with identical names and schemas and differ only in what the bill tools can see. The third exposes a narrower surface for assistants.
| Binary | Tier | Tools | Bill visibility |
|--------|------|-------|-----------------|
| thred-mcp | Gated (default) | all 71 | Approved bills only |
| thred-mcp-privileged | Privileged | all 71 | Everything, including pending and rejected bills |
| thred-mcp-assist | Gated, assist surface | 43: every read, plus upload_document and reply_to_task | Approved bills only |
thred-mcp — the gated default
This is the binary AI agents should run. When a business has bill approvals enabled, the bill and bill-payment tools serve approved bills only. The gate covers those tools and nothing else: the journal-entry tools are not gated, and the journal entry of a bill pending approval or rejected is visible through them, because Friday books a bill when it arrives, before any approval (THR-608). Extending the gate to journal entries is a follow-up. For the bill tools:
- "Approved" means either of Friday's two approved states:
approved(a person approved it) andai_approved(the business's approval policy approved it). Both are visible to every gated tool below;pending_approval,rejectedand any other state are not. list_billsreturns approved bills only (the server-sideapproval_status=approved,ai_approvedfilter is hard-appended).approval_statusis not an argument of this tool on either tier: since 0.9.1 an argument a tool does not declare is refused before dispatch, so a call that sends one gets an error naming it rather than a filtered answer. Reading pending and rejected bills is what the privileged binary is for, and it needs no such argument.get_bill,update_bill,void_bill, andcreate_bill_paymentagainst a bill that is pending approval or rejected return a 404 indistinguishable from a nonexistent ID, so these tools cannot tell a hidden bill from one that does not exist.- Every ID a tool writes into a request path (
business_id,bill_id,bill_payment_idand the rest) must be a UUID, on every binary, and is refused before any request otherwise. The account tools (get_account,update_account,archive_account) also take an account code made of letters, digits,_and-, because Friday's account routes resolve one; a code with any other character must be given as the account's UUID. Each path segment is also percent-encoded. Before 0.9.4 an ID holding path or query characters could steer a gated read to another route. - The gate fails closed on what comes back: a list body that is not a page is refused, a page is rebuilt from Friday's three page keys, a read by ID must return the entity asked for, and a bill or payment it cannot show to be approved is withheld.
- The
list_billsfilters (vendor_id, the due-date range,search,updated_since) narrow the approved bills only. The approval filter is added after them from a constant, so none of them can remove or replace it. - A bill payment with any allocation referencing a non-approved bill is hidden whole from
list_bill_payments/get_bill_payment/update_bill_payment/delete_bill_payment(partial redaction would leak the hidden amount arithmetically). list_bill_paymentswith abill_idfirst reads that bill. When it is pending approval or rejected, the payment list is asked for the nil UUID rather than that bill, so the rows,countandpage_infoare exactly what an ID no bill has gets. Otherwise Friday's count, which covers pages not fetched, could show that the hidden bill exists and how many payments it has.- That protection covers a hidden
bill_idonly. Withvendor_id, Friday counts every payment to that vendor, including a payment on a hidden bill, whether it was made wholly against that bill or split with one the tool shows; with thebill_idof an approved bill, it counts a payment split with a hidden bill. The gate withholds those rows, butcountandpage_info.total_pagescan include payments on bills the tool does not show. Closing that needs a Friday-side filter on the payment list. - When the gate withholds rows from a list page,
countandpage_info.total_countare rewritten to the same value. It is exact when Friday's page held the whole list (page_info.total_pagesis 1); with more pages it is an upper bound, because rows withheld on pages not fetched are still in it.page_info.total_pagesis Friday's, so walking every page still reaches every row. create_billstill works — but when approvals are enabled, the newly created bill enters the approval queue and will not be visible to the read tools until it is approved. That is expected, not an error.
A bill with no approval_status field is withheld, not passed through: since 0.9.4 the gate shows only what it can see is approved. Every current Friday deployment returns the field on every bill, and with the approval flag off bills are instantly approved, so nothing changes for a business that has not turned approvals on. A Friday that predates bill approvals is no longer supported on the gated binaries.
thred-mcp-privileged — deliberate ops use
Full, ungated visibility — including pending and rejected bills. This is the legitimate debug/ops path when the gated tier answers 404. Spawn it only by explicit configuration; never wire it as a workflow engine's entry point. It self-identifies as thred-mcp-privileged in the MCP handshake, so a miswired consumer shows up in mcp doctor output and connection logs.
The tier is decided once, by which binary was spawned — never by an environment variable and never by anything a caller sends.
thred-mcp-assist — the assistant surface
The binary to point an assistant at. It serves every read tool plus the two writes an assistant needs day to day — dropping a document into Thred and answering a bookkeeper's task — and nothing else. Creating or changing invoices, bills, payments, customers, vendors, accounts or businesses is not muted by instruction; those tools are simply not there. tools/list does not show them, and a call to one by name gets the same Unknown tool error a nonexistent name gets, so the surface cannot be probed. Also absent: list_tax_codes (its endpoint no longer exists) and attach_files_to_task_message (covered by reply_to_task). The list lives in src/tools/surfaces.ts and is asserted by a test; a tool reaches this surface only when someone adds it there in a reviewed change.
The surface, like the tier, is decided by which binary was spawned and is carried by both the tool list and the dispatch.
To run it from a client config, name the package and the command:
"command": "npx",
"args": ["-y", "-p", "thred-mcp", "thred-mcp-assist"]Setup
1. Get your Thred credentials
Log in to the Thred Partner Portal and copy your Partner UUID and API Key from the API Keys section.
2. Connect to Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (create it if it doesn't exist):
{
"mcpServers": {
"thred": {
"command": "npx",
"args": ["-y", "thred-mcp"],
"env": {
"THRED_PARTNER_UUID": "your-partner-uuid-here",
"THRED_API_KEY": "your-api-key-here",
"THRED_ENV": "sandbox"
}
}
}
}Restart Claude Desktop (Cmd+Q, then reopen). A hammer icon will appear — click it to see available Thred tools.
3. Connect to Cursor / VS Code / other MCP clients
Same config pattern — add to your MCP client's settings file with the same command, args, and env.
4. Run manually (for testing)
THRED_PARTNER_UUID=your-uuid THRED_API_KEY=your-key THRED_ENV=sandbox npx thred-mcpEnvironment
There is no default environment. The server refuses to start unless you name a target via THRED_ENV or THRED_BASE_URL — a misconfigured client used to fall back to production, which is the one target nobody should reach by accident. Actions you take (creating invoices, recording payments, etc.) affect real data in the environment you name, so the AI assistant confirms high-risk actions before executing them.
Set one of these alongside your credentials:
| Variable | Values | Effect |
|----------|--------|--------|
| THRED_ENV | production · sandbox (required unless THRED_BASE_URL is set) | Selects the API environment |
| THRED_BASE_URL | full origin, e.g. https://sandbox.thredfi.com | Explicit override; takes precedence over THRED_ENV |
"env": {
"THRED_PARTNER_UUID": "your-sandbox-uuid",
"THRED_API_KEY": "your-sandbox-key",
"THRED_ENV": "sandbox"
}Use your sandbox Partner UUID / API Key with THRED_ENV=sandbox, and your production credentials with THRED_ENV=production. Credentials are environment-specific — they are not interchangeable.
File attachments
reply_to_task, attach_files_to_task_message and upload_document read files from the machine the server runs on. Reads are confined to a small set of directories so that a task description written by someone else cannot talk an assistant into uploading an arbitrary file.
| Variable | Values | Effect |
|----------|--------|--------|
| THRED_ALLOWED_UPLOAD_DIRS | path-delimiter-separated absolute directories, e.g. /Users/me/Downloads:/Users/me/receipts | Directories attachments may be read from. Defaults to ~/Downloads, ~/Desktop, ~/Documents and the system temp directory |
Limits are Thred's, stated here from the snapshot in the repository (src/platform-limits.snapshot.json, not shipped in the npm package): .pdf 50 MB; images 25 MB; .csv 50 MB; .xlsx/.xls 100 MB; .pptx/.ppt 100 MB; .docx/.doc 50 MB; .txt 10 MB; .rtf 25 MB; .xltx/.xltm 50 MB; .dotx 50 MB; .xaf 100 MB; at most 10 attachments and 200 MB in total per request; per business, 30 document uploads and 60 task messages per minute across all callers (429 with Retry-After beyond that). upload_document accepts only .pdf and the image types above; the office and ledger types are for reply_to_task / attach_files_to_task_message. Uploads create drafts awaiting human review; nothing is booked by an upload.
Files inside a hidden directory (~/.ssh, ~/.aws, ~/.config, …) are always refused, symlinks are resolved before the check, and the extension and per-type size limit are validated before anything is read.
This bounds the blast radius; it is not a substitute for approving write actions. A file that sits inside an allowed directory is still readable, so keep your MCP client's confirmation prompts on for the task tools.
Retrying a reply, a bill, a payment — and why an invoice is different
reply_to_task sends an Idempotency-Key derived from the reply itself (business, task, text, attached file contents). If the connection drops and the assistant re-issues the call, Friday recognises the repeat and returns the original message instead of posting it twice. Sending the identical reply to the same task again within Friday's retention window (ten minutes by default) is therefore treated as the same reply.
create_bill works the same way through a different door. Pass your own external_id — the vendor invoice number, or the bill's ID in your system — and a repeat call returns the bill already recorded. Omit it and the key is derived from the bill itself (vendor, dates, and each line's amount, description and quantity), so a retry describing the same bill is recognised as the same write rather than recorded a second time. A key you supply always wins.
create_invoice is not covered, and the reason is worth knowing. Friday's invoice endpoint does not read external_id back, and that column carries no unique constraint, so a key derived from the invoice's content would buy nothing. What it does deduplicate on is the invoice number: a repeat of a number already in use on an active invoice returns that invoice instead of creating another. The connector generates a random invoice number when you omit one, so a retry after a timeout creates a second invoice. Pass your own invoice_number when you may retry.
create_bill_payment is covered too, through the door Friday opens for it: that endpoint deduplicates on an Idempotency-Key header rather than on the body, so the connector sends one — your external_id when you pass it, otherwise a key derived from the payment. A retry describing the same payment returns the payment already recorded. A 409 means that key has already been recorded against a different payment; do not retry it with changed arguments, because that records a second payment. Read back what was recorded first.
Seeing the connector in Friday's logs
Every request the server makes to Friday carries a User-Agent of the form thred-mcp-assist/0.6.0 (tool=list_bills) and a W3C traceparent. Any access log that prints the user agent, or any OpenTelemetry-instrumented server, records both, which attributes calls per binary, version and tool and lets a trace_id in a Friday error body be matched to the call that produced it. Friday has to be configured to record them: an access log, a request-logging middleware, or tracing enabled.
Timeouts and retries
Reads are abandoned after 30 seconds, writes after 60, multipart uploads after 180. A read that fails on the wire or gets a 502, 503 or 504 is retried once. Writes are never retried by the server: a write that failed on the wire may have landed, and it is Friday's idempotency support — used by reply_to_task and create_bill, as above — that makes repeating one safe.
Development
npm test # vitest, no network: fetch is stubbed throughout
npx tsc --noEmit # types
npm run build # tsup, into dist/The report tools copy four enums out of Friday's source — detail_level, granularity, and
the AR and AP group_by values — and a copy drifts. src/tools/friday-enums.test.ts
re-reads the originals when you point it at a Friday checkout, and compares them with what
the tools publish:
| Variable | Values | Effect |
|----------|--------|--------|
| FRIDAY_SRC | absolute path to a Friday checkout, e.g. /Users/me/src/friday | Runs the enum drift check against that checkout's source |
FRIDAY_SRC=/path/to/friday npm testWithout it the check skips rather than passes, so a run on a machine with no Friday checkout says nothing about drift either way.
License
MIT
