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.
Connecting
Section titled “Connecting”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.
Present an API key — for programs
Section titled “Present an API key — for programs”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.
What is negotiated
Section titled “What is negotiated”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.
What the assistant gets
Section titled “What the assistant gets”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.
| Tool | What it does |
|---|---|
whoami | The account, the company it acts for, and what it may do |
list_models | The kinds of record this workspace holds |
describe_model | One model’s fields, choice values, and whether you may write it |
search_records | Records matching a filter (at most 50) |
get_record | One record by id |
aggregate_records | Counts, sums and averages, grouped — including by period |
find_capability | Which app or setting covers a topic, installed or not |
list_actions | The buttons and documents a model has |
render_report | Produce a document for some records, returned as a file |
create_record | Create one record |
update_record | Change one record |
delete_record | Delete records by id |
run_action | Press 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.
Access
Section titled “Access”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_recordandrun_actionare 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”.
Reading the answers
Section titled “Reading the answers”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.
Activity
Section titled “Activity”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.
Limits
Section titled “Limits”- 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_reportas file content on a tool result instead. search_recordsreturns at most 50 rows. For anything countable, useaggregate_records— adding up a capped page is wrong on the row after the cap.