Table of Contents

Connecting an MCP client through your identity provider

Warning

Some of the steps on this page are performed in a product this project does not control. Any screen, menu, or field named here can be renamed or moved there at any time. Where this page and that product's own documentation disagree, the product's documentation is right.

You have MailFathom running and you want an MCP client signed in through the identity provider you already operate. This page is the order those steps happen in, and it is written from the provider's side. Two other pages own the rest: the MCP endpoint is the reference for every setting named here and for what a token has to prove, and connecting the chat client you already use is where each popular client's own dialog, address kind, and accepted credentials are. This page links into both rather than restating either.

Almost all of the work is outside MailFathom. Configuring MailFathom is one JSON block. What a first connection actually costs is a resource identifier chosen once, an application registered per client, a callback URL copied out of the client rather than invented, a token-endpoint authentication method that has to agree on both sides, one switch in the provider that makes the token's audience come out right, and knowing where to find a subject identifier. None of that is MailFathom's to own, and all of it decides whether MailFathom answers.

Whether you need any of this is decided by the client, not by MailFathom: two of the popular chat clients offer no field for a static header, so an API key cannot reach them and OAuth is the only shape left. The client page above says which ones, and reading it first is what tells you whether this page is optional.

What MailFathom is not

MailFathom is an OAuth 2.1 protected resource and never an authorization server. If you arrive from a product that does both, four things you will look for are not here and never will be:

Not here Where it is instead
A login page, a consent screen, or a password prompt Your authorization server's, reached by the client's browser
Token issuing, code redemption, refresh tokens Your authorization server's; none of them ever reaches MailFathom
A user store, user records, group membership Your authorization server's; MailFathom keeps an issuer, a subject, and the scopes, and drops every other claim
Client registration, client secrets An arrangement between the client and your authorization server

So there is nothing in MailFathom to create a user in, and no place to type a password. What MailFathom holds is a list of the issuers it trusts and, per issuer, the subjects it serves.

Who chooses how the client registers

Three registration shapes are in use, and which are available is the client's decision rather than MailFathom's — the client's own dialog is where it is made. MailFathom neither advertises nor constrains the choice, because the registration is between the client and your authorization server and MailFathom sees only the token that comes out.

Shape What the client does What your authorization server needs
Static credentials You paste a client ID, and a secret where the server requires one, into the client's dialog An application you registered by hand, with the client's callback URL allowed
Dynamic Client Registration (RFC 7591) Registers itself the first time it connects, and reuses what came back A registration_endpoint in its metadata, reachable from the client's network, and a registration policy that permits it
Client ID Metadata Document Sends a URL it hosts as the client_id; the server fetches it to learn the client's redirect URIs and authentication method client_id_metadata_document_supported in its metadata, and outbound access to fetch that URL

Static credentials are the shape to reach for when the other two are unavailable, or when you want one application per organization that you can see and revoke in your own console. Dynamic registration asks the least of an operator, at the cost of a fresh client registration per connection accumulating in the provider. A metadata document asks about as little and registers nothing, which is why a client that supports both usually prefers it.

Which shape a named client uses, and where its dialog asks for the client identifier, is on the client page rather than here.

Which steps repeat and which do not

The second client is minutes of work, and this is why. Everything in the left column is done once for the deployment, whatever connects to it afterwards:

Once per deployment Once per client Once per person
Choose the resource identifier Register an application in the provider Find the subject identifier
Make the token's audience match it Allow that client's callback URL Add it to AuthorizedSubjects
Define the scope, if you require one Set the token-endpoint authentication method
Write the OAuth entry in MailFathom Paste the credentials into the client, where it takes them
Verify the endpoint answers Point the client at the server URL

Adding a second person to an existing client is the right-hand column alone: one sub in AuthorizedSubjects and a restart. Adding a second client is the middle column. Nothing in the left column is touched again unless the deployment moves to a new public address.

The sequence

The worked example throughout is Keycloak, chosen because it is open source and self-hostable so you can follow it without an account anywhere. Field names in bold are its console's. Every host, realm, identifier, and subject below is a placeholder — substitute your own. Another provider is the same sequence with different names.

1. Fix the resource identifier

Everything else is derived from this one value, so choose it first and do not change it afterwards. It is the canonical public URL clients reach the endpoint at, including the fixed /mcp path:

https://mail.example.com/mcp

Three things read it, which is why it has to be exactly right before anything is configured: it becomes McpEndpoint:Authentication[].OAuth.Resource, it is the audience every token must carry, and it decides where the metadata document is published — for the value above, at https://mail.example.com/.well-known/oauth-protected-resource/mcp.

It has to be HTTPS, and it is the address your clients use rather than an internal one. Behind a reverse proxy that is the proxy's public URL; the reference says why it is never derived from a request header.

A client will compare it, so take the value from the metadata document rather than from your configuration file. At least one widely used client requires the resource field of that document to equal the server URL the operator typed into it, character for character and path included — and MailFathom publishes the identifier in a canonical form that need not be the characters you wrote. /mcp and /mcp/ are two different identifiers, so a trailing slash here is a failed connection rather than a cosmetic difference.

2. Register MailFathom as a resource in the provider

The provider needs something to represent the thing being protected, because that is what a token's audience names.

In Keycloak there is no separate resource-server object: an audience is a string a mapper writes, so this step and the next are the same step. Create a client scope under Client scopesCreate client scope, and give it the name you intend to require as a scope — mailfathom.read in the examples below. Leave Protocol as openid-connect and leave Include in token scope on, so the name reaches the token's scope claim. Type here decides only whether clients created later receive this scope automatically; which clients actually get it is the assignment in step 4, so None is the safe answer and nothing depends on it.

One client scope then carries both halves of what a token must prove: the audience, from the mapper added next, and the scope, from its own name.

If you require no scope, create the client scope anyway and call it something descriptive. It exists to carry the mapper.

Decide here whether clients should hold a refresh token. A client asks for one by naming offline_access, and it learns to ask by reading that scope in MailFathom's metadata document — so if you do not advertise it, a client asks only for the scope above and its user signs in again whenever the access token expires, typically hourly. It goes in step 7 as AdvertisedScopes rather than RequiredScopes, because it describes the client's session rather than anything MailFathom protects and is checked on nothing; the reference has the whole rule. In Keycloak the scope exists already, as the built-in offline_access client scope, and what remains is assigning it to the client in step 4 — nothing new is created here.

3. Make the token's audience agree

This is the step that is not guessable, and the one most first connections fail on. An MCP client asks for a token with the RFC 8707 resource parameter. Several widely deployed authorization servers do not act on that parameter at all — they decide the audience from their own configuration — so the client asks correctly, the server issues a token for something else, and MailFathom refuses it with a 401 that by design says nothing about why. There is no setting in MailFathom that relaxes the audience check, deliberately.

In Keycloak, open the client scope from step 2, go to its Mappers tab, choose Configure a new mapper and select Audience. Then:

  • leave Included Client Audience empty — it offers the realm's own clients, and the audience here is a URL rather than a client;
  • write the resource identifier from step 1 into Included Custom Audience;
  • leave Add to access token on.

That is the whole of it. A resource-indicator implementation is open work upstream, so treat the mapper as the arrangement rather than as a workaround for something about to change.

4. Register an application for the client

One application per MCP client, so that revoking one client does not revoke the others.

In Keycloak: ClientsCreate client, Client type OpenID Connect, and a Client ID naming the client rather than MailFathom. On the next step leave Standard flow on — the authorization-code flow is what an MCP client performs — and turn Direct access grants off, since an MCP client never uses it and step 8 uses a client of its own for the one place it is useful.

Client authentication is the switch that has to agree with the client:

The client is Client authentication What the client sends at the token endpoint
A public client — no secret to keep Off Nothing but PKCE; token_endpoint_auth_method is none
A confidential client — an operator pasted a secret On, and the Credentials tab is where the secret is The client ID and secret

A mismatch here is the failure that says least: the sign-in fails without either side reporting which of them expected what. Choose from what the client actually does, not from which sounds safer. A client registering dynamically or through a metadata document is a public client — leave it off.

Finally, assign the client scope: the client's Client scopes tab → Add client scope → the scope from step 2, as Default. Without this the tokens this client receives carry neither the audience nor the scope, and every one of them is refused.

5. Take the callback URL from the client

A callback URL belongs to the client and is generated by it — never invented and never MailFathom's. Some clients publish one fixed address; others mint one per connector when the connector is created, which means the value has to be copied again if the connector is deleted and recreated. Read it out of the client's own dialog, usually under an advanced or developer section, and paste it into the provider.

In Keycloak that goes into the client's Valid redirect URIs, which takes several entries so the surfaces of one client can share an application.

Two shapes are worth expecting, because they are configured differently:

  • A hosted client redirects to one HTTPS address on the vendor's own domain, and it has to match exactly.
  • A client running on the operator's machine uses an RFC 8252 loopback redirect on a port that changes every session, and declares http://localhost/callback and http://127.0.0.1/callback. Your provider has to match those ignoring the port, or the sign-in fails on whichever session picked a new one. Do not respond by listing ports.

Neither address is one MailFathom ever sees. An authorization code is redeemed between the client and the authorization server, and MailFathom is handed the finished token.

A client that registers dynamically or through a metadata document supplies its own redirect URIs as part of that registration, and this step disappears — which is most of why those shapes are less work.

6. Find the subject identifier

AuthorizedSubjects is required and at least one entry is needed, because MailFathom serves one owner's mail to everyone it admits: without the list, every colleague who can obtain a token for this resource reads that mailbox. What goes in it is the sub the authorization server issues, and an email address is not it — a subject is what a server promises never to reuse, and an address is reassigned to whoever holds the mailbox next.

In Keycloak it is the ID shown on the user's own page under Users, a UUID.

The reliable way is to ask the server, because it answers with the value the token will actually carry rather than with whatever the console labels an identifier. With an access token in hand — step 8 is one way to get one — the standard endpoint answers it:

$ curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
    https://sso.example.test/realms/mailfathom/protocol/openid-connect/userinfo | jq -r .sub
9f2c7c1e-8a4d-4c62-9f0b-3d2a1b5e7c04

Do this once per person who will use the deployment.

7. Write the MailFathom entry

Now the part that is MailFathom's, and it is one block:

{
  "McpEndpoint": {
    "Enabled": true,
    "Authentication": [
      {
        "OAuth": {
          "Resource": "https://mail.example.com/mcp",
          "RequiredScopes": [ "mailfathom.read" ],
          "AdvertisedScopes": [ "offline_access" ],
          "AuthorizationServers": [
            {
              "Name": "workforce",
              "Issuer": "https://sso.example.test/realms/mailfathom",
              "AuthorizedSubjects": [ "9f2c7c1e-8a4d-4c62-9f0b-3d2a1b5e7c04" ]
            }
          ]
        }
      }
    ]
  }
}

Resource came from step 1, RequiredScopes and AdvertisedScopes from step 2, and AuthorizedSubjects from step 6; Name is a label of your own that diagnostics report. Drop AdvertisedScopes if you decided against offline access — it publishes a scope for clients to ask for and is checked on nothing, so leaving it out narrows no boundary and only shortens how long a client goes between sign-ins. Issuer is the one new value and the one to be careful with: copy it verbatim from the provider, trailing slash included, because it is compared to a token's iss by exact string equality and is the one identifier MailFathom deliberately never rewrites. Several widely deployed servers publish an issuer whose entire path is one trailing slash, and tidying it by hand produces a deployment that starts cleanly and then refuses every token that server issues. In Keycloak, Realm settingsGeneralEndpointsOpenID Endpoint Configuration opens the discovery document, and its issuer field is the value to copy.

Nothing else about the server is configured here: MailFathom finds the discovery document itself, at addresses it derives from the issuer, and takes the key set address out of it.

The section is read once while the host is composed, so this takes effect on restart.

The reference is where the rules on these five live: what happens with several entries, why every entry must agree on Resource, why Name and Issuer are unique across the whole list, what leaving RequiredScopes empty means, and why a scope you advertise can never turn a caller away. Two more things it records are worth knowing before the next step. A deployment behind a TLS-terminating proxy must name that proxy in ReverseProxy:TrustedProxies — an access token is refused outright when the request did not arrive over TLS, and with no proxy named that refusal stops working rather than becoming stricter. And an OAuth entry may sit beside an ApiKey or PublicKey entry: a request is served when it satisfies any one of them, which is how a scheduled job keeps its own credential while people sign in.

8. Verify before you touch the client

Three commands, in this order. Each isolates one thing, so a failure at step n means everything before it is right — which is worth much more than discovering the same failure through a client dialog that reports "couldn't connect".

The metadata document answers, and describes this deployment:

$ curl -sS https://mail.example.com/.well-known/oauth-protected-resource/mcp | jq
{
  "resource": "https://mail.example.com/mcp",
  "authorization_servers": [ "https://sso.example.test/realms/mailfathom" ],
  "scopes_supported": [ "mailfathom.read", "offline_access" ],
  "bearer_methods_supported": [ "header" ],
  "resource_name": "MailFathom"
}

Check resource against what you will type into the client, and authorization_servers against your issuer. A deployment configuring several authorization servers publishes them all here, and a client may use only the first — order them accordingly. scopes_supported is what a client will ask for, which is why offline_access appears in it without being required: an absence here is a client that never asks for a refresh token.

An unauthenticated request is refused, and says where to authorize:

$ curl -sS -i -X POST https://mail.example.com/mcp \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mail.example.com/.well-known/oauth-protected-resource/mcp"

That header is what starts the whole flow: the client reads the pointer, fetches the document, discovers the authorization server, and signs in. A 401 carrying no resource_metadata parameter, or a 404 on the address above, means the client has nothing to follow.

A token is accepted. Getting one by hand needs a grant that runs with nobody at a browser, and the direct grant is the short way where your provider permits it. Use a client of its own for this rather than the one from step 4 — in Keycloak a public client with Direct access grants on and Standard flow off — so the registration your MCP client depends on is never modified for a probe, and delete it when you are done:

$ read -rs PROVIDER_PASSWORD
$ ACCESS_TOKEN=$(printf '%s' "$PROVIDER_PASSWORD" | curl -sS -X POST \
    https://sso.example.test/realms/mailfathom/protocol/openid-connect/token \
    -d grant_type=password \
    -d client_id=mailfathom-connection-check \
    -d username=operator \
    --data-urlencode 'scope=openid mailfathom.read' \
    --data-urlencode 'password@-' | jq -r .access_token)

Assign the client scope from step 2 to that probe client as well, or the token comes back without the audience and the check below fails for a reason that has nothing to do with MailFathom.

The password goes in over standard input for a reason. --data-urlencode 'password@-' reads the value from there and URL-encodes it, which is what keeps it out of curl's argument list; writing -d password="$PROVIDER_PASSWORD" would not, because the shell expands the variable before curl starts and the plaintext then sits in /proc/<pid>/cmdline for the life of the request, readable by anything else on the machine exactly as a typed literal would be. read -rs is what keeps it out of the shell history and off the screen, which is a different exposure and the one a variable does answer. Then:

$ curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://mail.example.com/mcp \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
200

200 means the token satisfied the issuer, the audience, the subject, and the scopes, and the transport answered. Drop -o /dev/null -w to read the tool listing itself; the transport may frame the JSON-RPC message on a data: line of an event stream rather than returning it as the body, which is its choice and not a fault. Verifying an enabled endpoint says which tools should be in that listing and why one of them may legitimately be absent.

A 401 or 403 here is a configuration answer, not a mystery — the table below maps each to the setting behind it.

9. Connect the client

The client asks for one fact, and it is the value from step 1:

https://mail.example.com/mcp

Everything else it discovers. Where the client also asks for a client ID, and a secret if you configured a confidential client, those are the ones from step 4. Then the client opens a browser, the person signs in against your provider, and the tool listing appears.

What a failure looks like

Every refusal is deliberately uninformative to the caller — an expired token, a wrong audience, an unknown issuer, and an invalid signature are one answer, because a protected resource that explained itself would be explaining itself to whoever was probing it. The server log is where they differ, and it is the first place to look. This table is the second:

What you see What it usually is
401 on every call, sign-in itself succeeded The audience. The token was issued for something other than Resourcestep 3
401, and the log names an unknown issuer Issuer does not match the token's iss exactly. Check the trailing slash — step 7
401 after a successful sign-in, on a deployment behind a proxy The token was refused because the request did not arrive over TLS. The scheme is read after forwarded headers are applied, so either the proxy sends no X-Forwarded-Proto or its address falls outside ReverseProxy:TrustedProxies. Do not answer it by emptying that list — that trusts every peer and turns the refusal off rather than fixing the hop
403 naming no scope The subject is not in AuthorizedSubjects for that issuer. Signing in again cannot fix it, which is why it is not a 401step 6
403 naming a scope in WWW-Authenticate The token is missing a required scope. The client scope is not assigned, or not Default, or Include in token scope is off — step 4
404 on the metadata address, everything else working The request did not arrive under the scheme and host Resource names. Behind a proxy, the forwarded scheme and host are missing or the peer is outside TrustedProxiesdiscovery a client uses
The client reports it cannot reach the server, and the provider logs no traffic at all Discovery never completed, so the client never learned where to authorize. Run the first two commands of step 8
The sign-in page never appears, or the redirect is rejected The callback URL is not allowed, or not matched ignoring the port for a loopback client — step 5
The sign-in fails at the token exchange The token-endpoint authentication method disagrees. A public client sending nothing to a confidential registration, or the reverse — step 4
Everything works, and the person is asked to sign in again every hour The client was issued no refresh token, because nothing told it to ask for one. Advertise offline_accessstep 2 — and assign the scope to the client at the provider
429 where you expected 401 The rate limiter ran out of capacity before authorization was reached. A flood of bad credentials is meant to cost the sender something
A startup failure naming McpEndpoint:Authentication:0 The entry itself. The reference lists what is refused before anything binds

Another provider

The sequence is the same and the names differ. Microsoft Entra ID, for example, has an app registration in place of a client and an Application ID URI in place of the audience mapper — and it is that URI, rather than a mapper, that has to be the resource identifier from step 1, or the token request is refused by the provider before MailFathom sees anything. Auth0 issues a sub of the form auth0|… for step 6.

This page walks one provider on purpose. A per-vendor matrix would be a page that ages every time a console is rearranged, and the part worth writing down is the order — which is the same everywhere, because it comes from the protocol rather than from the vendor. Take the sequence, and read your provider's own documentation for the names. That is also why the illustration is a named field and a menu path rather than a screenshot: a picture of somebody else's console goes stale without saying so, and a field name can be checked against that vendor's own documentation.

  • The MCP endpoint — every setting named here, what a token must prove, and what the endpoint publishes. The reference for this page's whole subject.
  • Signing in with OAuth — the administrative endpoint takes the same entries and its own audience rule, which is a Resource ending in /api/admin. mfctl performs that sign-in for you.
  • Connecting the chat client you already use — the client-side half: each popular client's dialog, where it runs, and which credentials it will accept, including the two that accept only OAuth.
  • Mailbox OAuth — the other direction entirely: MailFathom authenticating to a mail provider. A different key ring, a different set of registrations, and nothing on this page applies to it.
  • Getting started — the guided path from an empty deployment to a first tool call.

Trademarks. The product, service, and company names on this page are their owners' trademarks and are used solely to identify the identity providers and client applications a MailFathom deployment can be connected through. Their use implies no affiliation with, sponsorship by, endorsement by, or certification from those owners, in either direction, and this page reproduces no third-party logo, icon, wordmark, or screenshot.

Keycloak is a trademark of the Linux Foundation. Microsoft and Microsoft Entra ID are trademarks of the Microsoft group of companies. Auth0 and Okta are trademarks of Okta, Inc. THIRD_PARTY_LICENSES.md records the per-owner review this statement comes out of, and why it sits here rather than in NOTICE.