Authentication and permissions
The MCP server uses OAuth 2.1, as the MCP specification describes. A person signs in to neoo in the browser, chooses one company and what the assistant may do there, and the client receives tokens for that one connection. MCP clients such as Claude handle all of this on their own; this page is for anyone building or debugging a client.
The flow
-
The client sends an MCP request without a token. The server answers
401with aWWW-Authenticateheader pointing to its protected-resource metadata:WWW-Authenticate: Bearer resource_metadata="https://<api host>/.well-known/oauth-protected-resource/mcp/acme", scope="accounting:read accounting:write …" -
The client reads the protected-resource metadata (RFC 9728), then the authorization-server metadata at
https://<api host>/.well-known/oauth-authorization-server(RFC 8414). They list every endpoint and scope. -
The client sends the browser to the authorization endpoint with PKCE (
code_challenge_method=S256) and theresourceparameter set to the server URL (RFC 8707). Both are required; a request withoutresourceis refused withinvalid_target. -
The person signs in to neoo (with their second factor) and decides on the consent page — see Consent below.
-
The client exchanges the code at the token endpoint and sends the access token as
Authorization: Bearer …on every MCP request.
| Endpoint | |
|---|---|
GET /.well-known/oauth-protected-resource/mcp/{dataset} |
Protected-resource metadata of the server URL |
GET /.well-known/oauth-authorization-server |
Authorization-server metadata |
GET /oauth/authorize |
Authorization endpoint — response_type=code only |
POST /oauth/token |
Grants authorization_code and refresh_token |
POST /oauth/revoke |
Token revocation (RFC 7009) |
POST /oauth/register |
Dynamic client registration (RFC 7591) |
All clients are public clients: there are no client secrets (token_endpoint_auth_method is none), and PKCE protects the code exchange.
Register your client
Your client identifies itself in one of two ways:
- Client ID Metadata Document (preferred). Use an
httpsURL as yourclient_id, serving a JSON document withclient_id(the same URL),client_nameandredirect_uris(up to 20). neoo fetches it when the person connects and caches it for up to 12 hours. The consent page shows the document’s domain as Verified domain. - Dynamic client registration.
POST /oauth/registerwithredirect_urisand aclient_namereturns aclient_id. Registration is limited to 10 per hour per IP address. A client registered this way is shown as Verified domain only when all its redirect URIs arehttpson one host; otherwise the person sees an Unverified app warning.
Redirect URIs must match exactly what was registered. Allowed are https URIs, http on localhost, 127.0.0.1 or [::1] (the port may change between requests, for native apps), and custom app schemes. A client whose name suggests a well-known assistant but whose domain doesn’t belong to it gets a prominent warning on the consent page.
Consent
On the consent page the person sees who is asking, where they’ll be sent back to, and chooses:
- Dataset — the company the server URL belongs to. It is shown for checking and can’t be changed. The person must be owner or admin there, and AI connections must be switched on.
- Access per area — No access, Read or Read and change.
Whatever scopes the client asks for, the person decides what is granted. The server’s challenge asks for every scope, so all areas are offered; reading is preselected for everything except the bank.
Scopes
Each area has a read scope and, where changes are possible, a write scope. A write scope always includes reading.
| Area | Read scope | Write scope | Covers |
|---|---|---|---|
| Accounting | accounting:read |
accounting:write |
Reports (trial balance, income statement, balance sheet, VAT), chart of accounts, tax codes, fiscal years, journal entries and ledger lines. Write: post and change journal entries. |
| Invoices and bills | invoices:read |
invoices:write |
Customer invoices and vendor bills, aging, services, workstations, invoice PDFs. Write: create and change invoices and bills, record payments, e-mail invoices and reminders. |
| Contacts | contacts:read |
contacts:write |
Customers and vendors. Write: create and update them. |
| Bank | bank:read |
— | Imported bank transactions, including IBANs and counterparties. |
| Documents | documents:read |
documents:write |
Attached documents and the AI inbox. Write: upload documents to the AI inbox. |
A scope is the upper limit, not a grant on its own: what a request may do is the intersection of the connection’s scopes and the permissions of the person’s role in the company. Only owners and admins can connect, so in practice the scopes decide.
tools/list only lists the tools the connection can use. Calling a tool outside the granted scopes returns 403 with an insufficient_scope challenge naming the missing scope. To get more access, the person disconnects and connects again with more areas ticked.
Tokens and how long a connection lasts
| Access token | Valid for 1 hour. |
| Refresh token | Rotates on every use; the connection stays alive as long as it is used at least every 90 days. Presenting an already used refresh token ends the whole connection. |
| Authorization code | Valid for 60 seconds, single use. |
Tokens are tied to the server URL they were issued for. A token for one company’s URL is refused at another company’s URL.
A connection ends — and its tokens stop working immediately — when:
- the person disconnects it under My Profile → AI connections, or someone disconnects it under System → Management → AI connections
- an owner or admin switches AI connections off for the company
- it was not used for 90 days
- the person is no longer owner or admin of the company, loses access to it, or their account is deactivated
An ended connection is never restored, even when the access comes back; the person connects again.
Errors
| Status | Meaning |
|---|---|
401 with error="invalid_token" |
The token expired or the connection ended. Refresh, or sign in again. |
403 with error="insufficient_scope" |
The tool needs a scope the connection wasn’t granted. |
429 |
More than 120 requests in a minute on this connection. Wait for Retry-After seconds. |
Errors of the token endpoint follow OAuth (invalid_grant, invalid_target, …) with a short error_description.