How do I integrate an AI agent with Apier over MCP?
By Antony Richard Grov, founder of Apier
In three steps that each take minutes. Point your MCP client at the hosted endpoint, https://www.apier.no/api/mcp, directly over streamable HTTP or through the published stdio proxy. Present a credential: an API key in the Authorization header when your code controls the request, or an OAuth 2.1 access token when a hosted client signs the user in for you. Then call the authority tool for a company and read the answer as the union it is: every clause comes back either register-coded with a transcribed signing mode or as uninterpreted free text returned whole, and an integration must handle both. Discovery needs no credential, nothing here writes to a government system, and the receipt section is absent on purpose.
Step 1: connect the MCP server
There is one hosted endpoint and two ways to reach it. Clients that speak remote MCP natively, which today means VS Code in agent mode, the OpenAI Agents SDK, Azure AI Foundry and recent Cursor builds, connect straight to the URL and send the key in a Bearer header themselves. Clients that only speak stdio, which means Claude Desktop and older Cursor, launch the published npm package through npx; it reads APIER_API_KEY from the environment and forwards the same header. Both paths return identical answers.
{
"servers": {
"apier": {
"type": "http",
"url": "https://www.apier.no/api/mcp",
"headers": { "Authorization": "Bearer ${env:APIER_API_KEY}" }
}
}
}The direct block above is the shape every native client takes, and it reads the key from the APIER_API_KEY environment variable rather than carrying one, so the file is safe to commit. The stdio block for Claude Desktop and Cursor, with its Windows launcher quirk, is on the guide to connecting a client to Norwegian company data over MCP, and this page does not repeat it. On either path the handshake and the tool catalogue are keyless, so a missing key surfaces at the first real call, never at connection time.
Step 2: the OAuth 2.1 flow for hosted clients
A hosted client such as the Claude.ai or ChatGPT connector cannot set a bearer header of its own, so it needs a way to find an authorization server and sign the user in. The MCP authorization model gives it one, in three moves. First, a tool call without a valid credential returns 401 with a WWW-Authenticate challenge whose resource_metadata parameter points at a protected-resource metadata document. Second, that document names the resource identifier a token must carry as its audience, the trusted authorization server, and the scopes that server can issue. Third, the client registers itself dynamically, sends the user to approve the connection on the consent page, and retries with the access token it receives.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="apier-mcp", error="invalid_token",
error_description="A valid Apier API key or an OAuth 2.1 access token for this resource is required",
resource_metadata="https://www.apier.no/.well-known/oauth-protected-resource/api/mcp"
# The document that URL serves (identity scopes only, never Apier permission scopes):
{
"resource": "https://www.apier.no/api/mcp",
"authorization_servers": ["<the trusted issuer, read from this document>"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["openid", "profile", "email", "phone", "offline_access"]
}Two facts about that document decide how you reason about access. The scopes it advertises are identity scopes only, because a scope the authorization server cannot mint would make the authorize request fail; no Apier permission scope is requested during sign-in. And when the account owner approves the connection, a dedicated read-only API key labeled MCP OAuth connection is minted and linked to the signed-in identity, with no operator step. The token is verified on signature, issuer, audience and lifetime, asymmetric algorithms only, and every call carries that key's scopes, tier and rate limits. The token's own scope claim can narrow that access, never widen it.
Step 3: call the authority endpoint
The question an agent most often needs answered before acting on a company's behalf is who is registered as able to sign for it, and how. One GET on the REST surface answers it, and the get_company_authority tool returns the same payload over MCP. Both need a key with the read:brreg scope, which the default key grants, and both take only the 9-digit organisation number.
# REST: one GET, Bearer key, scope read:brreg.
curl -s https://www.apier.no/api/v1/company/999999999/authority \
-H "Authorization: Bearer apr_live_<your_key_here>"
# MCP: the same answer through tools/call.
curl -s -X POST https://www.apier.no/api/mcp \
-H "Authorization: Bearer apr_live_<your_key_here>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"get_company_authority","arguments":{"org_number":"999999999"}}}'The top of the answer is a deterministic classification, one of sole, joint, by_role, prokura_only, no_authority or unknown, with the registered holders beneath it. Below it sits coded_authority, the part this guide is about: the model over the mirrored signaturrett and prokura clauses, each carried with the stamp from the last read of the register. It is stamp-honest by design: an absence is never collapsed into a no unless the register itself said no.
Read both branches of the response
Each of signatur and prokura is a triple: a state, a record and a verification stamp. The record is the discriminated union your code must switch on. When the register served the clause under a code the closed taxonomy knows, the record is register_coded and every clause carries its verbatim text, its register codes and a transcribed signing mode such as sole, joint or severally. The example below shows that branch beside a positively verified absence of prokura.
"coded_authority": {
"status": "ok",
"signatur": {
"state": "coded_verified",
"record": {
"kind": "register_coded",
"clauses": [
{ "text": "Daglig leder alene", "codes": ["R0002"], "mode": "sole", "truncated": false }
]
},
"verification": { "verified_at": "2026-09-20T10:00:00+00:00", "outcome": "covered" }
},
"prokura": {
"state": "absent_verified",
"record": { "kind": "empty", "reason": "empty_array" },
"verification": { "verified_at": "2026-09-20T10:00:00+00:00", "outcome": "absent" }
}
}When the wording is one the taxonomy does not know, the record is free_text and the clause comes back whole, with codes and mode both null. Nothing is partially parsed or guessed, and the state reads free_text_uninterpreted so it cannot be mistaken for a coded answer. This is a normal outcome for a legitimately registered company, not a failure; the honest handling is to show the wording to a human. Rounding it to yes or no invents a fact the register did not supply.
"coded_authority": {
"status": "ok",
"signatur": {
"state": "free_text_uninterpreted",
"record": {
"kind": "free_text",
"clauses": [
{ "text": "<the register's wording, byte for byte>", "codes": null, "mode": null, "truncated": false }
]
},
"verification": { "verified_at": "2026-09-20T10:00:00+00:00", "outcome": "covered" }
}
}| State | What it asserts | What an agent does |
|---|---|---|
| coded_verified | A register-coded clause the last read confirmed. | Branch on the mode; cite the stamp. |
| coded_unverified | A coded clause with no confirming stamp. | Usable, but say the stamp is missing. |
| free_text_uninterpreted | A wording the taxonomy does not know, returned whole. | Show the text to a human; never infer a mode. |
| absent_verified | The register positively answered that no clause is registered. | The one state that can be read as a no. |
| no_record_unverified | Nothing on record and no positive absence. | Treat as unknown, not as a no. |
What arrives with receipts
One section a full integration guide would carry is missing here on purpose: verifying a signed receipt end to end. Receipts are a later block of work that is not built, and a walkthrough with nothing to verify would read as a capability the service does not have. Cryptographic verification of the authority answer itself is live today. Add ?certificate=true to the same GET and the response wraps the identical answer in a certificate signed with a detached JWS, verifiable offline against the public key set at /.well-known/jwks.json. The verify-certificate guide carries the key-fetching and verification script, so this page does not repeat it.
Frequently asked questions
- Which credential should an agent integration use?
- An API key when your code controls the request, which covers VS Code, the OpenAI and Azure agent runtimes and anything you run yourself. The OAuth 2.1 flow exists for hosted clients that cannot set a bearer header of their own, such as the Claude.ai and ChatGPT connectors: they discover the authorization server from the metadata document, sign the user in, and present an access token instead. Both arrive in the same Authorization header and both keep working.
- Does a wider OAuth scope give the agent more access?
- No. The access a connected agent has comes from the dedicated API key its signed-in identity is mapped to, which is minted with read-only scopes the moment the account owner approves the connection. The token's own scope claim can only narrow that set, never widen it, and no Apier permission scope is requested during sign-in because the authorization server issues identity scopes only.
- What is the difference between the coded and the free-text branch?
- Whether the register served the clause under a code the closed taxonomy knows. A coded clause carries the verbatim wording plus its register codes and a transcribed signing mode, so an agent can branch on the mode. A free-text clause is a wording the taxonomy does not know, returned whole with codes and mode set to null and never partially parsed. The second branch is a normal outcome, not an error, and an agent that treats it as one will refuse legitimate companies.
- Can an absent clause be read as no authority?
- Only when the state says so. absent_verified means the register itself answered that no clause is registered, and it is the one state that asserts an absence. no_record_unverified means nothing is on record and nothing confirmed that, which is a gap in the data rather than a fact about the company. Collapsing the two into a single no is exactly the mistake the stamp pair exists to prevent.
- Where are the JWKS and receipt-verification sections?
- Certificate verification is live and documented on its own guide: add ?certificate=true to the authority GET, fetch the public key set from /.well-known/jwks.json, and verify the detached JWS offline. Receipt verification arrives with receipts, a later block of work that is not built yet, so this page does not describe it. A placeholder that reads as a capability is worse than an honest gap, and the page will gain that section when the receipt work ships.