MCP clients deprecated
This page is for people who want to connect a language-model client, such as Claude, Cursor or VS Code, to OX App Suite: what the client needs, how to get a token for it, and how the common clients are configured. How an operator enables and runs the endpoint, and what it answers in detail, is described in MCP server; the full interface is in the MCP API documentation.
What a client needs
Three things, all of them provided by whoever runs the installation:
- The endpoint URL. It is the App Suite host followed by
/mcp, for examplehttps://mail.example.com/mcp. The endpoint is off by default; if the URL answers404, it has not been enabled for your installation. - A bearer token. Every request carries one. There are two kinds, and which one to use depends on the client:
- a personal access token you mint yourself and paste into the client, for clients configured with a fixed
Authorizationheader; - a token from the installation's authorization server, obtained by the client itself through an OAuth sign-in with your App Suite login. This works where the operator has switched on App Suite's own authorization server or set up an external one.
- a personal access token you mint yourself and paste into the client, for clients configured with a fixed
- The scopes you want to grant. A token only opens what its scopes allow, and the client only sees the tools for those scopes:
| Scope | Tools |
|---|---|
read_mail | mail_search, mail_get, mail_thread_get, mail_attachment_get, mail_folders, mail_scheduled_list, mail_snoozed_list, vacation_get, mailfilter_list |
read_calendar | calendar_list_events, calendar_get_event, calendar_free_busy, calendar_find_slot, calendar_folders, resources_search |
read_contacts | contacts_search, contact_get, users_search, contacts_folders |
read_files | files_search, file_get, files_folders |
read_tasks | tasks_search, task_get, tasks_folders |
read_reminders | reminders_list |
mail_external | no tool of its own: the mail tools reach your other mail accounts as well |
| any | me_get, briefing_get and changes_since (they show only the parts the token may read) |
write_calendar | calendar_create_event, calendar_update_event, calendar_respond, calendar_delete_event, calendar_set_reminder |
write_tasks | task_create, task_update, task_complete |
write_contacts | contact_create, contact_update |
write_mail | mail_draft_create, mail_mark, mail_move, mail_delete, mail_folder_create, mail_folder_rename, mail_send, mail_scheduled_cancel, mail_snooze, mail_unsnooze, vacation_set, mailfilter_create, mailfilter_update, mailfilter_delete |
write_files | file_create |
read_mail and write_files | mail_attachment_save |
write_mail and read_files | mail_draft_attach_file |
The write_ tools exist only where the operator has switched them on, and only for a token that holds the write scope. Ask for a write scope only when the client is to change data. Every change works in two steps: the client first calls the tool with preview: true and shows you what would happen, and only after you confirm it calls the tool again with the confirmToken of that preview. That confirmation is valid for five minutes and for that one change; it has nothing to do with how long your access token lasts. The Drive tools only add files; they never overwrite or delete one.
If your client can ask you itself (MCP elicitation in form mode, over protocol revision 2026-07-28; Claude Code does), App Suite asks you through it before every change: the client shows what the change would do, such as recipients and subject of a mail, and nothing is saved until you accept. Declining or closing the question saves nothing. The model cannot answer that question for you, and a token from a preview alone is no longer enough.
With other clients, the confirmation is no proof that you saw the preview: the model can preview and confirm on its own, and some do. Use a client that asks you before it calls a tool, at least for tools that change data, and read what it shows you. Whether a client asks you is the client's own declaration: App Suite relies on it and cannot tell a client that leaves you out.
What goes out by mail: mail_send sends a draft from your drafts folder, and appointments send invitations and updates to the attendees, as App Suite does. Where your account can schedule mails, a mail does not leave at once: it waits a minute, or as long as your operator set, among your scheduled mails, and the client tells you when it goes out. Until then you can stop it, by asking the client or in App Suite, and it goes back to your drafts. Asked to send a mail later, the client schedules it for that time; mails you schedule or snooze yourself in App Suite are the same ones the client lists and changes. Unless your operator allows it, both reach colleagues of your installation only, never outside addresses; one setting governs both. A vacation notice you switch on replies to everyone who writes, so unless your operator allows it, the client cannot give it a new text; it can switch on the text you set in App Suite. Deleting moves mail to the trash and never deletes it for good. Mail filter rules can move, flag, mark read and move to the trash, but not forward.
Every item in a result carries webUrl, a link that opens it in App Suite.
A scope opens a module, not only the tools. A token with read_mail may also read mail through the regular App Suite HTTP API, and a token with read_files may read Drive there, without the size bounds the MCP tools apply. A write scope opens the module's write actions there as well; sharing Drive files with others takes share_files, which no MCP tool needs. Grant only what the client should be able to read or change, and treat the token like a password.
Getting a personal access token
A personal access token is minted through the App Suite HTTP API with a signed-in session. There is no settings page for it yet, so the steps below use curl. Replace the host and the login data; /appsuite/api/ is the usual path of the HTTP API.
- Sign in and keep the cookies, which the session needs:
HOST=https://mail.example.com
SESSION=$(curl -s -c cookies.txt "$HOST/appsuite/api/login?action=login" \
--data-urlencode "name=anton@example.com" --data-urlencode "password=…" | jq -r .session)
- Mint the token. Choose a label you will recognize later, the scopes, and an expiry in milliseconds since the epoch; the example expires in 90 days:
EXPIRES=$(( ($(date +%s) + 90*86400) * 1000 ))
curl -s -b cookies.txt "$HOST/appsuite/api/accesstoken?action=new&session=$SESSION&label=claude-laptop&scopes=read_mail,read_calendar,read_contacts&expires=$EXPIRES" \
| jq -r .data.secret
The answer contains the secret, oxa_…, once. Copy it into the client now; it cannot be shown again. The same answer says in mail_access whether the token can read mail (see below).
- Sign out of the helper session, which the token does not need:
curl -s -b cookies.txt "$HOST/appsuite/api/login?action=logout&session=$SESSION" >/dev/null && rm cookies.txt
To see your tokens, including which scopes you may grant at all, call accesstoken?action=all; to revoke one, call accesstoken?action=delete&id=<id>, both with a signed-in session as above. A revoked token stops working immediately. Changing your password in App Suite revokes all your tokens.
The installation limits how far ahead expires may lie and how many tokens you may hold; a request beyond either is refused with a message saying so.
When mail does not work with the token. A token reads mail only if the installation could keep a credential for it when it was minted, which mail_access reports. It is false, for example, when you signed in through single sign-on while the mail server expects passwords. The token still works for calendar, contacts, files and tasks; for mail, ask the operator, who can tell which setup applies (see Personal access tokens). If you sign in through single sign-on only, you have no password for step 1; the OAuth sign-in below is then the way in, where the operator offers it.
Signing in through OAuth
Clients that run the OAuth flow themselves need nothing but the URL. On the first request without a token the endpoint answers 401 with a WWW-Authenticate header naming its metadata document; from there the client finds the installation's authorization server, registers itself if the server allows it, and sends you to its sign-in page, where you confirm the scopes. In detail:
POST /mcpwithout a token is answered with401andWWW-Authenticate: Bearer resource_metadata="https://mail.example.com/.well-known/oauth-protected-resource/mcp", scope="…".GETon that URL, without a token, returns the resource metadata (RFC 9728):resource,authorization_serversandscopes_supported, the scopes this installation's tools require.- The client reads the authorization server's own metadata (RFC 8414 or OpenID Connect discovery), registers dynamically (RFC 7591) where the server permits it or uses a client configured in advance, and runs the authorization code flow with PKCE (
S256) and theresourceparameter set to the endpoint URL. - The resulting access token is sent as
Authorization: Bearer …; when it expires, the client refreshes it and retries.
Whether this works depends on the operator. Where App Suite's own authorization server is switched on, the sign-in is the regular App Suite login, followed by a page that asks which scopes to grant: the read scopes are checked, the write scopes are not and come with a warning. The page names the client as it described itself, which App Suite cannot verify, and the host your browser returns to; continue only if you just started connecting an app you trust at that address. If you are not signed in to App Suite in that browser, the page asks you to sign in in another tab and continues once you have.
Each app you connect this way appears once in your access token list, under the name it gave itself; deleting it there disconnects the app for good. Disconnecting in the app may not be enough: claude.ai, for instance, keeps its access when you click Disconnect. Changing your password disconnects every app.
With an external authorization server instead, its sign-in page, client registration and scopes are set up outside App Suite. If authorization_servers is missing from the metadata document, the installation offers personal access tokens only.
Configuring clients
In every example, replace the URL and oxa_… with your own values. Keep the token out of files you share or commit.
Claude Code
claude mcp add --transport http appsuite https://mail.example.com/mcp \
--header "Authorization: Bearer oxa_…"
claude mcp list then shows the server as connected, and /mcp inside a session lists its tools.
claude.ai
Under Customize, Connectors, add a custom connector with the endpoint URL. The dialog offers two ways in:
- OAuth sign-in (Sign in now or Sign in when needed). claude.ai signs you in at the installation's authorization server and asks you to confirm the scopes. With App Suite's own authorization server, keep Register automatically, which claude.ai detects; Use Claude's published identity is not supported. With an external server, keep claude.ai's own client settings unless the operator gave you a client id and secret for claude.ai. This works only where the operator runs an authorization server.
- A personal access token (No sign-in, then Request headers). Add the header
Authorizationwith the valueBearer oxa_…. claude.ai sends the value exactly as entered, so theBearerprefix has to be part of it. Request headers are a beta that not every account shows yet.
Use a personal access token only in a connector you add for yourself, as on the Free, Pro and Max plans. On Team and Enterprise plans the owner adds the connector and its header is shared by every member, so a personal token there would give all of them access to your data.
Cursor
In ~/.cursor/mcp.json, or .cursor/mcp.json inside a project:
{
"mcpServers": {
"appsuite": {
"url": "https://mail.example.com/mcp",
"headers": { "Authorization": "Bearer oxa_…" }
}
}
}
VS Code
In .vscode/mcp.json; the token is asked for once and kept by VS Code instead of being written into the file:
{
"inputs": [
{ "type": "promptString", "id": "appsuite-token", "description": "App Suite access token", "password": true }
],
"servers": {
"appsuite": {
"type": "http",
"url": "https://mail.example.com/mcp",
"headers": { "Authorization": "Bearer ${input:appsuite-token}" }
}
}
}
Clients without HTTP transport
A client that starts MCP servers as local processes only, such as older desktop apps, can reach the endpoint through mcp-remote, which forwards between the two:
{
"mcpServers": {
"appsuite": {
"command": "npx",
"args": ["mcp-remote", "https://mail.example.com/mcp", "--header", "Authorization:${APPSUITE_AUTH}"],
"env": { "APPSUITE_AUTH": "Bearer oxa_…" }
}
}
}
The header value is passed through the environment because some platforms split arguments at spaces.
Checking a token by hand
T=oxa_…
curl -s https://mail.example.com/mcp \
-H 'Content-Type: application/json' -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/list' \
-H "Authorization: Bearer $T" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' \
| jq -r '.result.tools[].name'
The list shows exactly the tools the token's scopes allow.
When something goes wrong
| What you see | What it means |
|---|---|
404 on the URL | The endpoint is not enabled for this installation, or the URL is wrong. |
| "Cannot connect the app" after signing in | The request to connect has expired (ten minutes) or was answered already, or it was continued in another browser. Start connecting again from the app. |
The app reports access_denied | You chose Deny or granted no scope, or connecting apps this way is not enabled for your account or for the address the app returns to; the page said which. |
401, invalid_token | The token is unknown, mistyped, revoked or expired. Mint a new one. |
403, insufficient_scope | The token lacks the scope of the tool that was called. Mint one with that scope. |
403, "The user may not use the MCP endpoint" | The token is valid, but your account may not use the endpoint: it lacks the capability mcp, or it or its context is disabled. A new token does not help; ask the operator. |
403 for a browser-based client | The client sends an Origin the installation does not allow; the operator decides which. The endpoint sends no CORS headers, so a browser client on another origin also needs CORS set up at the operator's ingress. |
| A tool call fails with "retry in … seconds" | Too many calls in a short time, or too many at once. The call is answered with a tool error and a Retry-After header saying when to try again. |
429 | The same for a resource read, which has no tool error to carry it. |
503 | The token could not be checked right now. Keep it and retry. |
| Mail tools fail, everything else works | The token has mail_access: false; see above. |
| A tool is missing from the list | Its scope is not in the token, or the operator has not installed that part. A tool that changes data also needs the operator to switch writing on. |
A change fails with confirm_required, confirm_mismatch, confirm_expired, confirm_used or object_changed | The change was not confirmed as previewed: no token, a token for other arguments or another access token, one older than five minutes, one used already, or the item changed since the preview. Nothing was saved; preview again and confirm. |
A change fails with confirm_declined or confirm_canceled | You declined the question your client showed, or closed it without answering. Nothing was saved; ask again if you want the change. |
Every call is logged with the client name, the tool and the outcome, but not with the arguments' values or the content that was read. What a tool returned is passed to the language model and, from there, is subject to that model provider's terms.