Skip to content

MCP Server

A Fullfinity workspace can present itself over the Model Context Protocol, so an assistant — Claude, or any other MCP client — can look things up, answer questions from live data, create and change records, and run the same actions a person would.

It is a licensed module (mcp). Once installed, switch it on under Integrations → MCP Server; the switch is per company.

The endpoint is POST /mcp, speaking JSON-RPC 2.0 over HTTP. There are two ways to authenticate, and which one you want depends on who is asking.

Sign in as yourself (OAuth) — for people

Section titled “Sign in as yourself (OAuth) — for people”

This is the right choice whenever a person is talking to the assistant, and it is what Claude’s connector flow uses. Each person authorises separately and gets their own access, so what the assistant can see and do is exactly what that person can.

An administrator registers the assistant once under Settings → General → MCP Clients, which produces a Client ID (and, for a server-side client, a secret). Whoever is connecting enters those in the assistant’s connector settings alongside the URL; the sign-in happens in a browser, and no credential is copied around.

Discovery is automatic. An unauthenticated call to /mcp answers 401 with a WWW-Authenticate header naming /.well-known/oauth-protected-resource, which names the authorization server, whose metadata lives at /.well-known/oauth-authorization-server. Clients walk that chain themselves.

Authorization Code with PKCE (S256) is required of every client, public or confidential. Access tokens last an hour; refresh tokens last 30 days and both halves rotate on every refresh, so a stolen refresh token stops working as soon as the real client uses its own. Tokens are audience-bound to this MCP endpoint — one will not authenticate anything else.

Dynamic Client Registration is not implemented. It is a SHOULD in the specification, and the alternative it names — the person entering a client id issued by the server — is what the register above provides.

For a script or a scheduled job with no person behind it, an ordinary API key on the Authorization header still works, exactly as in API Overview:

{
"mcpServers": {
"fullfinity": {
"url": "https://your-workspace.example.com/mcp",
"headers": {
"Authorization": "Bearer ff_your_key_here",
"X-DB-NAME": "your_database"
}
}
}
}

Be deliberate about this one: a key authenticates its service account, so everyone sharing a key shares one identity, one set of permissions, and one line in the activity log. That is right for an unattended integration and wrong for a team of people — use OAuth for them.

X-DB-NAME names the database to serve. Omit it if the server sets DEFAULT_DB or hosts exactly one database; if it hosts several and the request names none, the call is refused with REQUEST_NOT_TIED_TO_DB rather than guessing.

The server is stateless — it issues no session id, and each call stands alone. It also sends no unsolicited messages, so GET /mcp is declined rather than held open.

initialize reports protocol version 2025-06-18 (falling back to 2025-03-26) and declares tools only. Resources and prompts are deliberately not advertised, because none are served — a client that listed them would find nothing.

A fixed set of tools, not one per model. A workspace holds hundreds of models, each with create, read, update, delete, its own methods and its own documents; published individually that is thousands of tools, which no client will render and no assistant can hold in view. So the workspace is reached by discovery instead: ask what exists, ask what one thing looks like, then read or act on it by name.

ToolWhat it does
whoamiThe account, the company it acts for, and what it may do
list_modelsThe kinds of record this workspace holds
describe_modelOne model’s fields, choice values, and whether you may write it
search_recordsRecords matching a filter (at most 50)
get_recordOne record by id
aggregate_recordsCounts, sums and averages, grouped — including by period
find_capabilityWhich app or setting covers a topic, installed or not
list_actionsThe buttons and documents a model has
render_reportProduce a document for some records, returned as a file
create_recordCreate one record
update_recordChange one record
delete_recordDelete records by id
run_actionPress one of the buttons list_actions reports

Filters are Q expressions, the same ones the data API takes: Q(state='Confirmed') & Q(amount_total__gt=5000). uid is the calling account’s user id and cid its company. Choice values are the exact words describe_model reports, capitals included — a Selection stores what it displays, so "draft" is not "Draft" and is refused rather than stored.

A key carries exactly the authority of its service account — its groups, its companies, its record rules. Access rights are already expressed per model and per operation, so a narrower connection is a narrower account, not a second permission system.

That is also how a read-only connection is made: grant the service account read and nothing else.

Permission is then checked in two places, answering two different questions:

  • The tool list. create_record, update_record, delete_record and run_action are withheld from an account that cannot write anywhere, and refused if asked for anyway.
  • The call itself. Passing the first check only means the account can write something — every internal account can post a message or add an attachment, which is what working in the product means. So each write is also checked against the record type it names, before it runs.

describe_model reports that per-record-type answer as you_may, which is what an assistant should read before writing rather than discovering it by being refused.

Everything runs as that account. Record rules narrow what a search returns, so a total is that account’s total; a record it may not see is absent rather than refused, and “not found” may mean “not yours”.

A tool that refuses — an unknown field, a record that isn’t there — returns a result marked as an error, not a transport error, so the assistant can correct itself and carry on.

The one to watch is run_action. A method that answers with a wizard has not run; it has asked a question. The result says "status": "needs_input" and carries the fields it wants, which are then passed back as inputs. Treating that as success is the mistake the shape exists to prevent.

Every call is recorded — reads included, since reading is how a workspace leaks — with the tool, the arguments as given, the outcome, and which credential was presented. Settings → General → MCP Clients also lists who has connected and through which client; archiving a connection cuts it off immediately, since tokens are checked on every call. Review it under Settings → General → Assistant Activity. Entries older than the retention window set on the integration are removed nightly.

  • No Dynamic Client Registration. An administrator registers each assistant once and hands out its Client ID; clients cannot register themselves.
  • No resources or prompts. Documents are returned by render_report as file content on a tool result instead.
  • search_records returns at most 50 rows. For anything countable, use aggregate_records — adding up a capped page is wrong on the row after the cap.