First in a series on how we built Airbyte's hosted MCP server, the service that lets agents pull data from your CRM, warehouse, or SaaS stack through authenticated Airbyte connectors.
When beginning work on the hosted MCP at Airbyte, we had two existing pieces of software that guided our requirements:
We already had a number of FastAPI servers, making FastMCP a logical choice for MCP implementation. Keycloak is used as our auth backend, and we had been supporting this for years now.Because the MCP spec requires servers to support OAuth 2.1, using a Keycloak client and sessions in tandem with FastMCP would work well. Here’s how we did it.
FastMCP’s OIDCProxy FastMCP has a ton of built in support for MCP authentication and various auth methods. We use the OIDCProxy class, which effectively turns the FastMCP server into an OAuth provider itself, acting as a middleman between MCP Clients and the Airbyte Keycloak instance.
Note: We chose the OIDCProxy instead of the OauthProxy class, because our Keycloak instance is already exposing OIDC discovery through standard .well-known endpoints, simplifying the setup.
First-time auth looks like this:
MCP Client attempts to connect to the server and receives a 401 response from /mcp. Client follows that pointer to our /.well-known/oauth-authorization-server document and reads our authorize and token endpoints. Client starts an OAuth flow against our /authorize route with PKCE. OIDCProxy generates its own PKCE challenge and redirects the browser to Keycloak's authorization endpoint (this is found at /.well-known/openid-configuration, a route that we pass in to the OIDCProxy. The user logs in through Keycloak, and is then redirected back to a callback url requested by the client. OIDCProxy exchanges the code for Keycloak tokens and stores them encrypted. OIDCProxy mints its own auth code, redirects back to the MCP client's callback. Client exchanges that code at /token, and the OIDCProxy issues FastMCP-signed JWTs. The MCP client never sees a Keycloak token. When its access token expires, it refreshes against us, and we refresh against Keycloak behind the scenes. Users only re-auth when the Keycloak refresh token expires.
Configuring the FastMCP instance and the proxy then looks like this:
def create_auth() -> OIDCProxy:
return OIDCProxy(
config_url=settings.OIDC_CONFIG_URL,
client_id=settings.OIDC_CLIENT_ID,
client_secret=settings.OIDC_CLIENT_SECRET,
...
)
mcp = FastMCP(
name="Airbyte Agent Engine",
auth=create_auth(),
...
)
mcp.run(...)
OIDC_CONFIG_URL is Keycloak's OIDC discovery URL (the /.well-known/openid-configuration mentioned earlier). OIDCProxy fetches it at startup and pulls the authorization, token, JWKS, and revocation endpoints straight from the document.
client_id and client_secret identify us as a client to Keycloak. Note that we do not support Dynamic Client Registration, so these values are static and are stored as server environment variables. Each request uses the same Keycloak client.
A Note on Token Storage By default, FastMCP’s OIDCProxy stores OAuth Client config and tokens in-memory. This means that whenever the server process restarts (for example, on a deploy), the tokens are lost, forcing MCP Clients to re-authenticate. To avoid this, the OIDCProxy takes a client_storage param , which allows for storing tokens in a separate backend, persisting sessions across server restarts.
We use a redis cluster and the py-key-value package for this:
def create_client_storage() -> FernetEncryptionWrapper:
redis_client = Redis.from_url(settings.REDIS_URL, decode_responses=True, health_check_interval=30)
store = RedisStore(client=redis_client)
return FernetEncryptionWrapper(
key_value=store,
source_material=settings.OIDC_CLIENT_SECRET
)
# and then pass it to the OIDCProxy for token storage:
def create_auth() -> OIDCProxy:
return OIDCProxy(
config_url=settings.OIDC_CONFIG_URL,
client_id=settings.OIDC_CLIENT_ID,
client_secret=settings.OIDC_CLIENT_SECRET,
client_storage=create_client_storage(),
...
)
Keycloak Client Setup In Keycloak, we use a dedicated client to handle hosted MCP sessions, separate from the clients used to handle logins from other surfaces like admin consoles and web apps. This allows for a bunch of key configurations:
Custom callback URLs that are required by specific MCP Clients that shouldn’t exist in other clients. Specific Scopes that should exist only for users of the MCP and not other clients (for example, offline_access, if offline sessions are desired). Configuration of token lifetimes that are more appropriate for an MCP session that may not be useful for other clients. MCP users don’t tend to be interacting constantly with the session, and may even be connected to run an automated task daily or weekly. For these use cases, we don’t want to expire refresh tokens too quickly and force re-authentications, rendering the automations useless. Disabling backchannel logouts . With this setting enabled, when a user logs out or their session expires, Keycloak will notify all connected clients for that session and log all of them out. So, for example, if a user is logged into a session from the Airbyte Cloud web app and the MCP, and they log out of the web app, they will also be kicked from the MCP. This is not the desired behavior, but is the default for a Keycloak client! Challenges Integrating OAuth end-to-end can be a bit tricky, especially using the MCP protocol, which is evolving quickly. A few general issues we ran into:
OIDCProxy is a great library and it abstracts away auth-related logic. The tradeoff here, as always, is that the implementation becomes opaque and harder to understand. It’s easy to simply pass an OIDCProxy object to your MCP, but when we get bug reports like “I was logged into the MCP yesterday and today I’m not,” we need to be able to dig deeper into what exactly is happening. Adding more server-side logging, request tracing, and understanding FastMCP’s token exchange logic were all helpful. Debugging MCP clients can also be thorny. Each potential client may be slightly different, and may handle authentication in a special way, and server side logging may not make this apparent. Claude Code has a debug mode and Claude Desktop is an Electron app so Chrome dev tools are invaluable, but for other clients observability can be difficult. And, of course, if you don’t have access to the user’s client, this is impossible! Conclusion Most of what we shipped isn't necessarily new: it’s standard OAuth and OIDC stuff. What’s tricky is understanding the token flow, getting the Keycloak settings right and the token storage set up correctly to handle the whole process. Debugging can also be hard, given that your choice of MCP Client may not have good visibility into errors and what part is failing. But FastMCP’s implementation makes this far easier than starting from scratch, and Keycloak enables a better user experience through client settings and token management.
Next up: the tool layer — turning Airbyte connectors into MCP tools without losing safety or schema fidelity.