Connecting a client
Connect any MCP client over Streamable HTTP, or through the local npm server, and authenticate with OAuth or an API key.
Any MCP client that supports remote servers over Streamable HTTP can connect: the server implements revision 2026-07-28 and also serves clients on 2025-11-25 and 2025-06-18 (see protocol and transport). Clients that only launch local processes use the local stdio server.
Connection settings
The endpoint is https://mcp.factuarea.com (/mcp remains an alias). Configure HTTP transport and either OAuth 2.1 or an Authorization: Bearer API key, plus the X-Company-Id header when the key reaches several tax IDs. The protocol guide gives an executable discovery request; resources and prompts explain the fiscal guidance.
Catalog and compatibility
Current-revision clients start with server/discover and earlier ones with initialize; either way, follow tools/list cursors with the same credential. The complete catalog has 668 tools; OAuth can reach 541. No handshake creates a session that replaces the credential. For Claude Code, the official plugin registers the server for you.
Local stdio server
Clients that only launch local processes — the Claude Desktop configuration file, agent frameworks, CI runners — can use the official npm package @factuarea/mcp. It is a small stdio server that forwards every request to https://mcp.factuarea.com with an API key, so tools, scopes, guardrails and test mode behave exactly as over HTTP. It needs Node 20 or later.
{
"mcpServers": {
"factuarea": {
"command": "npx",
"args": ["-y", "@factuarea/mcp"],
"env": { "FACTUAREA_API_KEY": "fact_test_xxxxxxxxxxxxxxxxxxxxxxxx" }
}
}
}Source code, configuration and releases: github.com/factuarea/factuarea-mcp.
Authenticate
The MCP server accepts two kinds of credential on the same
https://mcp.factuarea.com endpoint, for two different audiences. Both arrive as
Authorization: Bearer <token>; the server tells them apart by the token's
shape (fact_* → API key, anything else → OAuth access token).
Channel policy
This is the single most important rule of the MCP surface:
| Channel | Who | How scopes are chosen | Reachable tools |
|---|---|---|---|
| API key | The holder of an account automating its tax IDs, or a provider operating the accounts of its clients | You pick the scopes when you create the key — up to the super-scope *, which never covers the privileged accounts:* | 668 (everything), plus the nine managed-account tools when the key lists accounts:* |
| OAuth 2.1 | A third-party app acting on a user's behalf, on the one tax ID chosen at consent | The user grants scopes on the consent screen, from a curated catalog | 541 |
The model mirrors GitHub: a personal access token (API key) is the credential of the account itself and may hold any permission of it, while an OAuth app is external and is limited to a vetted set of scopes the user explicitly approves.
The 127 tools an OAuth app can never reach (only an API key can) are the most sensitive fiscal and privacy operations, plus the first-party administration of the account:
- VeriFactu writes (
verifactu:write) — 11 tools: register/retry/subsanar VeriFactu records and events (including retrying every blocked record at once), register/revoke the representation that lets Factuarea remit on your behalf, upload/activate/revoke FNMT certificates, update VeriFactu settings. These touch AEAT compliance and are owner-only. - GDPR erasure (
delivery_notes:gdpr_forget) — 1 tool: erase signature-audit PII (Art. 17). Privileged, admin-only. - Payments, gateways and stores (
stripe_autoinvoicing:*,payouts:read,integration_events:*,stores:*,woocommerce_store:write,shopify_store:write) — 20 tools: 13 for Stripe auto-invoicing, connected accounts, payouts and the gateway-event inbox/replay, plus 7 for store management and connection diagnostics. Fine-grained, API-key-only scopes with no OAuth consent equivalent. - Emails and API request logs (
emails:read,developers:read) — 7 tools: the sent-email history and the request logs, of a tax ID and of the account. - Tax ID portfolio (
companies:*) — 15 tools: list, read, register, edit, activate, deactivate and archive the tax IDs of the account, and the consolidated workforce overview. - Keys and personalization (
account:write) — 5 tools: create, rotate and revoke the API keys of the account, change the tax IDs a key reaches and update the personalization of the account. - Ownership and tax ID transfers (
account:owner,nif_transfers:*) — Tools to transfer the ownership of the account and issue, accept, revoke and list tax ID transfers. - Team (
members:*,invitations:*) — Tools for members, their grants on each tax ID, their module restrictions and the invitations. - Account webhooks (
webhooks:*) — 10 tools: the webhook endpoints of the account and their deliveries. The scope is grantable by consent, but these tools are excluded from OAuth one by one. - Privileged workforce writes/transitions — 33 tools using
time_entries:write,absences:write,absences:transitionorwork_schedules:write; workforce reads andemployees:writeremain OAuth-reachable. - Automation deletion (
automations:delete) — 1 tool: delete an automation rule, which cannot be undone through the API.
These groups add up to the 127 tools. Ten more tools, the
managed-account tools, are outside both counts: they need the privileged
accounts:read or accounts:write, which the super-scope * does not cover and
consent never grants, so only an API key of an account that holds the privilege
reaches them (managed accounts).
Everything else — all 541 read/write/transition/send tools — is available to both channels. See Scopes & permissions for the catalog.
Tools of the account
The account model adds tools to administer the account and its tax IDs. This is the channel that reaches each one:
| What | Tools | Scope | Channel |
|---|---|---|---|
| Ownership and tax ID transfers | transfer_account_ownership, create_nif_transfer, accept_nif_transfer, revoke_nif_transfer, list_nif_transfers (details) | account:owner, nif_transfers:* | API key only |
| Team: members, grants, module restrictions and invitations | Tools such as list_account_members, set_account_member_grant and create_account_invitation (details) | members:*, invitations:* | API key only |
| Tax ID portfolio | 15 tools, such as list_companies, create_company and get_consolidated_workforce (details) | companies:* | API key only |
| Usage of the account | get_account_usage | account:read | API key and OAuth |
| The tax IDs that a key reaches | update_api_key_nif_scope | account:write | API key only |
| Managed accounts | 9 tools, such as provision_managed_account and end_account_management | accounts:read, accounts:write | API key only, and only with the privilege |
| Webhooks and request logs of the account | 10 webhook tools (details) and 2 log tools | webhooks:*, developers:read | API key only, excluded from OAuth tool by tool |
The account tools need an API key, and with one they also check who is calling: the webhook and log tools of the account need an owner or admin of the account behind the key, and the privileged scopes need the privilege of the account on every call. Scopes & permissions describes both.
Some things have no MCP tool, by design:
- The bulk import of managed accounts. Its three operations are privileged and
read uploaded files, which the MCP server does not carry. Use the REST API or the
app, or provision the accounts one at a time with
provision_managed_account: bulk import. - Managing a granted access. Granting, changing, revoking, accepting, declining and renouncing the access of one account to the tax IDs of another are decisions of a person of each account, which the app confirms: no credential gives or accepts access to the tax IDs of its account. Granted access.
- The client's side of a managed account. Consulting the provider, raising or lowering its level, ending the management, taking over the payment and claiming the account are done by the holder in the app. Managed accounts.
API keys
An API key is an opaque Bearer token that belongs to your account and reaches the tax IDs in its scope, created in the developer dashboard at Settings → Developers → API Keys. The format and rules are identical to the REST API:
fact_live_<24 alphanumeric characters> → production company
fact_test_<24 alphanumeric characters> → isolated sandbox companyThe prefix is the source of truth for the environment — see Test mode. The secret is shown only once at creation; the backend stores only a keyed hash (HMAC-SHA256) of it. Pass it to your MCP client as:
Authorization: Bearer fact_test_xxxxxxxxxxxxxxxxxxxxxxxxFor the full key lifecycle — creation, scopes, rotation with grace period,
revocation, IP allowlist, expires_at — see the canonical
Authentication guide. Keys are shared across the REST
and MCP surfaces.
OAuth 2.1
For third-party apps, Factuarea is a full OAuth 2.1 Authorization Server. It supports Dynamic Client Registration, the authorization-code flow with PKCE, and refresh-token rotation. No pre-registration or manual app approval is required — a client registers itself and the user authorizes it. (Interactive clients like Claude Code and the MCP Inspector drive this whole flow for you; the steps below are for building your own client.)
Discovery
Clients discover the server's capabilities through standard metadata endpoints
(no /api prefix):
| Endpoint | RFC | Purpose |
|---|---|---|
/.well-known/oauth-authorization-server | 8414 | Authorization Server Metadata — lists the authorize/token/register/introspect/revoke endpoints, supported scopes, code_challenge_methods_supported: ["S256"]. |
/.well-known/oauth-protected-resource | 9728 | Protected Resource Metadata — declares the MCP resource and which authorization server issues valid tokens. |
When an unauthenticated request hits the endpoint, the server responds 401
with a WWW-Authenticate: Bearer ..., resource_metadata="<url>" header so RFC
9728 clients can find the authorization server without guessing.
1. Dynamic Client Registration (RFC 7591)
A client registers itself by POSTing its metadata; the server returns a
client_id (and a client_secret for confidential clients):
curl -X POST https://mcp.factuarea.com/api/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Invoicing Assistant",
"redirect_uris": ["https://myapp.example.com/callback"],
"token_endpoint_auth_method": "none"
}'Public clients (browser/native apps) register with
token_endpoint_auth_method: "none" and rely on PKCE; confidential clients use
client_secret_basic. Registration is rate-limited to 60 per minute per IP. Dynamic registration accepts only these two methods; for private_key_jwt see clients identified by a metadata document.
2. Authorization with PKCE
Send the user to the authorize endpoint with a PKCE challenge
(code_challenge_method=S256 is the only method accepted):
GET https://mcp.factuarea.com/api/oauth/authorize
?response_type=code
&client_id=<client_id>
&redirect_uri=https://myapp.example.com/callback
&scope=factuarea.read invoices.write
&state=<opaque>
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256This renders the consent screen, where the user:
- Picks the account and company (tax ID) to grant access to. The screen lists your own
companies and those of your clients' accounts, each with the account it belongs to and your
access level (View or Operate). The token is bound to that one company and
X-Company-Idcannot widen it. - Picks the environment — Live (the real company) or Test (an isolated sandbox), Stripe-style. Test is opt-in; absent ⇒ live.
- Reviews and selects the scopes to grant. Sensitive scopes are flagged and
not pre-checked. With View access only read scopes can be granted (those ending in
.readand thefactuarea.readmacro), and in Test the scopes of modules the sandbox does not offer are not granted; see what the screen grants by level and environment.
The consent request expires 5 minutes after it opens, and the screen shows how many minutes are left. If it expires, start the authorization again from your app.
On approval the server redirects back with a single-use code (and your
state). Sensitive operations are filtered out of the catalog the user can
approve — see the channel policy.
3. Token exchange
Exchange the code for an access token, sending the PKCE verifier:
curl -X POST https://mcp.factuarea.com/api/oauth/token \
-d grant_type=authorization_code \
-d code=<code> \
-d redirect_uri=https://myapp.example.com/callback \
-d client_id=<client_id> \
-d code_verifier=<verifier>{
"access_token": "<opaque>",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "<opaque>",
"scope": "profile.read contacts.read invoices.read invoices.write"
}The token persists the expanded, fine-grained scopes (macros like
factuarea.read are expanded at issue time). Use the access token as the Bearer
credential. The token endpoint is rate-limited to 60 per minute per (client,
IP) and requires client authentication (HTTP Basic for confidential clients,
client_id in the body for public ones).
4. Refresh-token rotation
Refresh tokens rotate by family: each refresh issues a new access token and a new refresh token, and invalidates the one you used.
curl -X POST https://mcp.factuarea.com/api/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=<refresh_token> \
-d client_id=<client_id>If a refresh token is replayed (used after rotation — the classic sign of a leak), the server detects the reuse, revokes the entire token family and raises a security alert. Always store and use the latest refresh token only.
Revocation & introspection
| Endpoint | RFC | Purpose |
|---|---|---|
POST /api/oauth/revoke | 7009 | Revoke an access or refresh token. |
POST /api/oauth/introspect | 7662 | Check whether a token is active and read its scopes/metadata. |
Both require client authentication. Users can also review and revoke connected apps from the Factuarea dashboard, and a company admin who loses access has their tokens revoked automatically on the next call.
Choosing the tax ID
A credential reaches one tax ID or several. The scope of an API key is fixed when you
issue it, and it can hold tax IDs of your own account, tax IDs that another account
grants to yours (granted access) and tax IDs of the managed
accounts you provision (managed accounts). The REST API
names the tax ID in the {company} segment of the path
(reach a tax ID through the company segment). A JSON-RPC
call has no path, so the session names it with an HTTP header:
X-Company-Id: 01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8b11The value is the public id of the tax ID, the UUID v7 that the {company} segment
accepts and that GET /v1/me
lists for every tax ID the credential reaches. Declare it once in the configuration of
your server, next to Authorization: it is never an argument of a tool. A header that
is present but empty counts as absent.
| Credential | X-Company-Id | What happens |
|---|---|---|
| API key that reaches one tax ID | absent | The session works on that tax ID. |
| API key that reaches several | absent | The call fails before any tool runs, with a -32008 error whose data.code is company_selection_required and data.http_status is 400. data.companies lists the tax IDs of the scope with their id and name (the first 20, and the hint says how many more). |
| API key | names a tax ID of its scope | The session works on that tax ID. |
| API key | names a tax ID outside its scope, one that does not exist or one of the other environment | 404 profile_not_found, the same answer for the three, so the answer never confirms that a tax ID exists. |
| OAuth connection | absent, or names the tax ID chosen at consent | The session works on that tax ID. |
| OAuth connection | names another tax ID | 404 profile_not_found, with a hint to authorize again choosing that tax ID. |
A tax ID of the scope that is deactivated or archived answers
403 company_inactive, named or not. All of these are -32008
errors (see Errors & rate limits): data.code carries the code and
data.http_status the status, and every code except company_selection_required is the
same as in the REST API.
What the session can do on the tax ID depends on the level that your credential has on
it. With view, every tool that writes to the tax ID answers
403 company_read_only, and with operate it writes, as
in the REST API (levels). The level is the lowest of the
one stored in the key for that tax ID and the current one of the person who issued the
key; an OAuth connection takes the level of the person.
Clients identified by a metadata document
Some clients, such as ChatGPT or Claude, do not register through /register: their client_id is an HTTPS URL that points to a public metadata document (CIMD). These clients can authenticate at /api/oauth/token, /api/oauth/revoke and /api/oauth/introspect with private_key_jwt (RFC 7523): instead of a secret, they send a JWT signed with a key published at the jwks_uri of their document.
curl -X POST https://mcp.factuarea.com/api/oauth/token \
-d grant_type=authorization_code \
-d code=<code> \
-d redirect_uri=https://myapp.example.com/callback \
-d client_id=https://myapp.example.com/oauth/client.json \
-d code_verifier=<verifier> \
-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
-d client_assertion=<signed JWT>The assertion must meet these conditions:
| Requirement | Detail |
|---|---|
| Algorithm | RS256 or ES256. |
iss and sub | Both equal to the client_id. |
aud | The URL of the token endpoint (or the issuer). |
exp | At most 300 seconds from now; we allow 60 seconds of clock skew. |
jti | Unique: each value is accepted only once. |
If the kid of the assertion is not in the cached JWKS, we reload it once before rejecting. A request cannot combine client_secret with client_assertion: it is answered with 400 invalid_request. If the document declares a method we do not support but also lists none in token_endpoint_auth_methods_supported, the client is treated as a public one and PKCE becomes mandatory.
/.well-known/oauth-authorization-server announces private_key_jwt in token_endpoint_auth_methods_supported, revocation_endpoint_auth_methods_supported and introspection_endpoint_auth_methods_supported (["none", "client_secret_basic", "private_key_jwt"]), and token_endpoint_auth_signing_alg_values_supported is ["RS256", "ES256"].
When /api/oauth/authorize cannot accept a client's document, it answers 400 with error: "invalid_client" and a readable error_reason:
error_reason | Meaning |
|---|---|
client_metadata_unsupported | The document is not compatible with what the server supports. |
client_metadata_unavailable | We could not download the document. |
client_metadata_disabled | Metadata-document clients are turned off. |
In a browser, the user is redirected instead to our own screen at /oauth/authorize?authorize_error=<reason>&client_id=…, which explains the problem in Spanish, English and Catalan.
Test mode
The MCP server runs against the same two environments as the REST API — live (your real company) and test (an isolated sandbox) — so you can build and validate an agent integration without touching production data, the AEAT, or your clients' inboxes.
How you select test mode depends on the channel:
| Channel | How to use test mode |
|---|---|
| API key | Authenticate with a fact_test_ key. The prefix is the source of truth — a fact_test_ token always operates on the sandbox. |
| OAuth 2.1 | On the consent screen, pick the Test environment (Stripe-style, test). Absent ⇒ live. The issued token is bound to that environment. |
A test credential operates on a dedicated sandbox company — a technical twin of your real company, provisioned automatically and inheriting its plan, so module/plan gating behaves faithfully. Isolation is structural (test and live data live in separate companies), and external effects are switched off:
| Effect | In live | In test |
|---|---|---|
| VeriFactu | The Alta record is created and transmitted to the AEAT. | Created locally, but never transmitted to the AEAT. |
| Document emails reach real recipients. | Not delivered to real recipients. | |
| Webhooks | Subscribed events are delivered to your endpoints. | Recorded with livemode: false, but not delivered. |
| FACe (FacturaE) | Submissions are presented to the real FACe web service. | Simulated — no SOAP call leaves Factuarea; the registry number is synthetic (FACE-SANDBOX-*). |
A test session reaches only its sandbox. A fact_test_ key, or a connection that chose the
Test environment, cannot name a real tax ID with X-Company-Id, and a
live one cannot name a sandbox: both answer 404 profile_not_found.
Everything else behaves exactly as in production, and the full set of 668 tools
is available in both environments (subject to your scopes and plan), with one exception:
the tools that write to the account itself — team, ownership, tax ID transfers, portfolio,
keys and webhooks — answer 403 live_credential_required when a test key calls them on
a live account. Testing managed accounts shows what a
test key does with accounts. When your flow works end-to-end, switch to live: create a
fact_live_ key, or re-run the consent flow and select the Live environment.
This is the same sandbox mechanism as the REST API. See the canonical Test mode & sandbox guide for how the sandbox company is provisioned and queried.