Build a Multi-Tenant MCP Server with OAuth (Clerk and Arcade) A developer at Zenith, an AEO service for technical products, built a multi-tenant MCP server that lets customers query their SaaS dashboard through Claude and ChatGPT, deploying it on Arcade.dev Cloud with OAuth delegated through Clerk so each tool call carries the user's own token and the backend re-applies workspace and plan checks. The writeup walks through the full setup — a Python MCP server, a demo reporting API with long-running jobs, and the three separate credentials involved in sign-in — with the complete example published on GitHub. Our customers kept asking the same question: can we use Zenith with Claude and ChatGPT? Zenith is an AEO service for technical products, and customers use our SaaS dashboard to monitor their share of voice. The dashboard is multi-tenant: every customer workspace has its own data and its own plan, so agent access is a tenant-isolation problem before it’s anything else. So we built a custom MCP server that calls our existing backend and deployed it on Arcade.dev Cloud. Customers sign in with their normal Zenith login we already use Clerk as an IdP , without creating an Arcade account. Every tool call reaches our API with an OAuth token delegated by that user, and the API applies the same workspace and plan checks it always uses for standard web or mobile users. The model never handles a token, and it doesn’t get to claim it’s user 123. This post builds the same setup from scratch: a Python MCP server, a small reporting app standing in for your backend, OAuth wired through Clerk and Arcade, deployed to Arcade Cloud and connected to Claude. The demo also covers the shape most real backends have: long-running jobs. The agent starts a report, polls with the job ID, and retrieves the result, with workspace membership and plan tier re-checked on every call. The part that took us longest to untangle, and the part this post spends the most time on, is that “signing in” here means three separate credentials. Download the complete example https://github.com/manveer/multi-tenant-mcp-server-oauth . It includes the Python MCP server, the web app, configuration templates, and tests, runnable with your own Clerk and Arcade developer accounts. MCP Server Architecture: Gateway, Tools, and Backend API Calling our existing backend lets agent requests use the application’s access rules. The MCP server exposes operations to the agent, while the API decides whether the user can perform them. Keeping those checks in the backend avoids maintaining another set of membership and subscription rules in the MCP server. Arcade Cloud runs the Python MCP server. The gateway identifies the user, and Arcade supplies the API access token when an authorized tool runs. A separate web application handles the reporting API, background jobs, browser sign-in, and custom user verifier. Clerk supplies identity and OAuth services. The download includes that web application so you can follow along without an existing backend. Its synthetic reports and permissions stand in for your application’s operations and access rules. The client calls MCP tools through the gateway. Those tools then make ordinary HTTPS requests to the reporting API. You’ll run the demo web application locally behind an ngrok HTTPS tunnel so Arcade Cloud can reach it. If your API and browser verifier already run on deployed HTTPS URLs, use those directly. You don’t need a tunnel. Why Not Just Run the MCP Server Yourself? Fair question, and nothing in MCP forces a gateway: the protocol supports OAuth-protected remote servers. Implement that spec from scratch, and you’re building everything between “the user clicked authorize” and “the right token arrives in the right tool call”: - Running the authorization-code flow against Clerk, with PKCE and consent, for every customer who connects an agent - Storing each user’s delegated token, refreshing it via offline access, and keeping it away from the model and the MCP client - Matching an incoming tool call to that caller’s token at execution time - Signing customers in through your existing IdP, so they connect with the login they already have instead of a new account - Binding tool authorization to a real browser session, so one user can’t complete another user’s flow In this tutorial, Arcade handles that list. Your API keeps what should never leave your code: deciding what the authenticated user can actually do. There’s also a middle path, and since we already use Clerk, it deserves a straight answer. Clerk can act as the authorization server for an MCP server directly https://clerk.com/docs/guides/configure/auth-strategies/oauth/how-clerk-implements-oauth : it supports dynamic client registration https://modelcontextprotocol.io/specification/draft/basic/authorization/client-registration dynamic-client-registration and a consent screen, so an MCP client registers itself with Clerk, completes the flow there, and presents a token that your self-hosted server verifies. Your code never touches authorization codes. That architecture is genuinely simpler: one credential instead of three, no gateway sign-in, and no browser verifier, because there is no second flow to bind. The trade-off is that you host and operate the server yourself, and Clerk’s MCP helper library https://github.com/clerk/mcp-tools targets JavaScript frameworks such as Next.js and Hono while our tools are Python. If you’re exposing one TypeScript server from an application you already run, Clerk-direct is a reasonable choice. We wanted the gateway anyway, and not only for the token plumbing. A gateway is a surface you can grow: it can federate tools from multiple MCP servers https://docs.arcade.dev/en/home/mcp-gateways behind the same URL, so we can add more servers later, or prebuilt integrations and newly launched tool recommendations from from Arcade’s catalog, without customers reconfiguring their connector. As that catalog grows, Tool Recommendation https://docs.arcade.dev/en/operate/governance/mcp-gateways/tool-recommendation matches each task to the few tools the agent needs, so it doesn’t carry every definition in context. It’s also a control point: you select exactly which tools each gateway exposes, with contextual access policies https://docs.arcade.dev/en/operate/governance/contextual-access on top, so the gateway also governs what agents can reach. Here, that’s just the four reporting tools. One constraint worth knowing during development: tools that declare requires auth or requires secrets need a verified caller identity before they run, and the SDK’s default local HTTP mode has none, so it rejects those tools while an unprotected greeting works fine. Arcade’s transport guidance https://docs.arcade.dev/en/build/create-tools/tool-basics/build-mcp-server run-your-mcp-server documents this. Local stdio supports protected tools with configured developer credentials, and the server can also run outside Arcade Cloud behind additional resource-server authentication https://docs.arcade.dev/en/build/create-tools/secure-your-server/secure-your-mcp-server . This tutorial stays on Arcade Cloud throughout. How MCP Gateway Authentication and API OAuth Fit Together A user connecting Claude needs to sign into the gateway and authorize tools to call the application API on their behalf. Their browser also has an application sign-in session managed by Clerk. These connections use separate credentials: Customers don’t need Arcade accounts for this setup. A User Source lets them sign into the gateway using their existing application login, with Clerk as the identity provider. They don’t need to become Arcade project members. You still need an Arcade account to create and deploy the server. Arcade documents the distinction between administrators and gateway users https://docs.arcade.dev/en/operate/identity/user-sources . A custom OAuth provider tells Arcade how to obtain the user’s authorization for your API. When the tool runs, Arcade supplies that API access token. A custom user verifier confirms that the person completing tool authorization matches the application user who began the flow. The verifier runs in your browser application, where it can read the Clerk session. You still need this step after configuring the gateway’s User Source. Arcade’s production-auth guide https://docs.arcade.dev/en/build/user-facing-agents/secure-auth-production covers this verification boundary. The API identifies the user by verifying the OAuth access token, then checks the application’s membership and plan rules for that user. The API doesn’t trust a caller simply claiming to be user 123. 1. Set Up the Python MCP Server You need Python 3.11–3.13, uv https://docs.astral.sh/uv/getting-started/installation/ , Node.js 22–24, and npm. Python 3.12 and Node.js 22 work well. You also need a Clerk development instance, a separate Arcade demo project, and an MCP client with remote connector support. Any client that supports the Streamable HTTP transport can use the gateway URL, including Cursor, Claude Code, Codex, and custom agents. The steps below use Claude Desktop. Python runs the MCP tools. Node.js runs the included API, browser sign-in, and verifier. You don’t need an existing backend to follow along. For the local demo, you’ll also need an ngrok account and CLI. We’ll set those up in step 3. Extract the download. Its two application directories: report-mcp-demo/ mcp/ Python MCP tools to deploy on Arcade Cloud web/ Application sign-in, user verifier, and reporting API From the MCP directory, install the pinned dependencies: cd report-mcp-demo/mcp uv sync --locked --extra dev Start with hello.py to see how a tool registers: python from typing import Annotated from arcade mcp server import MCPApp app = MCPApp name="hello demo", version="1.0.0" @app.tool def greet name: Annotated str, "Name to include in the greeting." - str: """Return a greeting to verify that a client can call this local tool.""" return f"Hello, {name} " if name == " main ": app.run transport="stdio" MCPApp owns the server. The decorator registers the function as a tool. Its docstring tells the client what the tool does, and the parameter annotation describes the input. Arcade’s server guide https://docs.arcade.dev/en/build/create-tools/tool-basics/build-mcp-server explains this interface. Optional: Try the Local Greeting From the same mcp/ directory, run the included greeting test. The test starts the server, connects an MCP client, and calls greet with the name Reader: uv run pytest tests/test stdio.py::test hello tool over stdio -q The test checks that the tool returns Hello, Reader . Its summary should show 1 passed . The greeting gets checked internally rather than printed. Checkpoint: Your local MCP server registered a tool and answered a real MCP call. This check needs no Clerk or Arcade account configuration. Next, you’ll set up authentication for the reporting tools. Customers will use the hosted gateway without running a local MCP process. 2. Create Test Users and OAuth Applications in Clerk Signing into the gateway identifies the user connecting their agent. Calling the backend requires a separate grant to act on the user’s behalf. You’ll configure an OAuth application for each connection in the same Clerk instance, using the user’s existing identity for both. In the Clerk dashboard, create an application and select its development instance . Enable email/password sign-in, then create three test users for the demo application: Alice, Bob, and Carol. Copy their Clerk user IDs from the Users page. Each workspace in the demo represents a separate customer organization. Alice and Bob belong to team-red, on the Pro tier. Carol belongs to team-blue, on Basic. Alice and Bob demonstrate colleagues sharing access within one customer workspace. Carol lets you test the boundary between customers. Basic permits basic reports. Pro also permits advanced reports. These simplified plans demonstrate that an agent can only create reports the customer’s subscription allows. The demo treats reports as workspace resources, so Bob can retrieve a report Alice created in their shared workspace. The names, memberships, and tiers are fixtures in the application code. You don’t need to configure Clerk Organizations or billing. Zenith also supports internal staff with access across customer workspaces. To try that optional case, create a fourth test user in the same Clerk instance and keep its ID for DEMO STAFF USER ID. The demo grants that account access to both workspaces while retaining their tier restrictions. Create two OAuth applications in that same Clerk instance under Configure → Developers → OAuth Applications → Add OAuth application . Give them distinct names and leave Public off so they remain confidential clients: Save each application’s client ID and client secret. Keep PKCE enabled. The API application requests offline access for refresh access and retains its consent screen. For the first-party gateway identity application, Arcade’s Clerk guide describes disabling Clerk’s additional consent screen. That setting doesn’t disable the gateway’s own consent screen. Open the OAuth application’s Discovery URL and copy the issuer, authorization endpoint, and token endpoint. Both apps use the same Clerk instance, but their client credentials and callbacks differ. Use the exact issuer value, without the /.well-known/openid-configuration suffix or an added trailing slash. Clerk User Source setup https://docs.arcade.dev/en/operate/identity/user-sources/clerk and Clerk’s OAuth implementation https://clerk.com/docs/guides/configure/auth-strategies/oauth/how-clerk-implements-oauth document these settings. Also copy the development instance’s publishable key and backend secret key from Clerk’s API keys page. The backend secret key differs from either OAuth application’s client secret. 3. Run the Demo Backend with Clerk Sign-In The included Next.js application supplies the backend for this example: browser sign-in, user verification, and the reporting API. The verifier lives alongside sign-in so it can identify the person completing authorization from their Clerk browser session. Arcade Cloud can’t reach the API on your computer’s localhost:3000. Ngrok gives that local application a public HTTPS address. The same address serves browser sign-in and /auth/verify because this demo bundles them with the API. If you’re adapting an existing application, use its deployed HTTPS API origin and host the verifier alongside browser sign-in. Those can be separate origins. You can skip ngrok once both reach HTTPS. Install the companion dependencies: cd ../web npm ci cp .env.example .env.local Create an ngrok account https://dashboard.ngrok.com/signup and install the CLI using its setup guide https://ngrok.com/docs/share-localhost/quickstart . On macOS with Homebrew: brew install ngrok Copy your authtoken from the ngrok dashboard. In another terminal, connect the CLI to your account and forward port 3000: ngrok config add-authtoken "YOUR NGROK AUTHTOKEN" ngrok http 3000 Copy the HTTPS forwarding origin printed by ngrok and keep this terminal open. The origin will reach the application once you start it below. Use the plain tunnel command shown here. Clerk handles browser and API authentication, so an additional ngrok login policy would interrupt the tool’s API calls. Fill in web/.env.local: APP URL=https://YOUR-NGROK-DOMAIN NEXT PUBLIC CLERK PUBLISHABLE KEY=YOUR CLERK DEVELOPMENT PUBLISHABLE KEY CLERK SECRET KEY=YOUR CLERK DEVELOPMENT SECRET KEY ARCADE API KEY=YOUR ARCADE DEMO PROJECT API KEY REPORTS OAUTH CLIENT ID=YOUR REPORT DEMO API CLIENT ID DEMO ALICE USER ID=YOUR ALICE CLERK USER ID DEMO BOB USER ID=YOUR BOB CLERK USER ID DEMO CAROL USER ID=YOUR CAROL CLERK USER ID DEMO STAFF USER ID= Replace the entire https://YOUR-NGROK-DOMAIN placeholder with your actual HTTPS forwarding origin, without a trailing slash, and fill in the other values. Set REPORTS OAUTH CLIENT ID to the API application’s client ID. The API checks this value so it can reject tokens issued for the gateway application. Leave the staff user empty until you test staff access. Start the app: npm run dev Open APP URL in your browser. If ngrok shows its free-plan warning page, click Visit , then sign in as Alice. This browser warning doesn’t affect programmatic API requests https://ngrok.com/docs/pricing-limits/free-plan-limits . Checkpoint: The homepage recognizes Alice. Browser sign-in works. You’ll test the API’s separate OAuth grant after connecting Claude. Use that public origin for the remaining steps. The app reads its allowed development hostname and browser origin from APP URL. Switching to localhost during authorization can leave you in a different browser session. Keep the app and ngrok running while testing. Ngrok reuses your account’s assigned development domain https://ngrok.com/docs/gateway/domains . If you switch domains, update APP URL, the Arcade verifier URL, and the hosted API base URL, then restart the app. The MCP server deploys separately to Arcade Cloud in step 8. 4. Configure the API OAuth Provider in Arcade The MCP tool needs permission to call your API on the user’s behalf. Configure an OAuth provider so Arcade can obtain that user’s API access token. In your Arcade demo project, open Connections → OAuth providers , add a Custom Provider , and configure: Retain the normal authorization-code and refresh grant parameters. When using Basic client authentication, avoid also adding client credentials to the request body. Arcade’s OAuth-provider reference https://docs.arcade.dev/en/references/auth-providers/oauth2 describes those request settings. Copy the provider’s generated Redirect URI into the allowed redirects for Report demo API in Clerk. Copy it exactly. This callback differs from the gateway’s intermediate callback, and neither callback matches your browser application’s /auth/verify route. The Python server points to that provider with: REPORT AUTH = OAuth2 id="report-demo-clerk", scopes= "openid", "profile", "email", "offline access" , The ID must match the provider you configured. The scopes request identity and refresh access. Workspace membership and subscription permissions remain separate checks in your application. 5. Register the Browser User Verifier The person completing API authorization needs to match the application user who started the flow. Your browser application can confirm that match using the signed-in user’s verified Clerk browser session. Register its verifier route with Arcade to make that check part of tool authorization. In Arcade Auth → Settings , select Custom verifier and enter your public application origin followed by /auth/verify: https://YOUR-NGROK-DOMAIN/auth/verify For the local demo, use the ngrok origin you set in APP URL. For a deployed application, use its browser sign-in origin followed by /auth/verify. The companion includes this route. Here’s what it does: 1. Receive the authorization flow’s flow id from Arcade. 2. Read the verified Clerk browser session. If the visitor signed out, preserve the flow’s return path and send them through sign-in. 3. Confirm the session’s user ID with Arcade from the server, using the project’s API key. 4. Continue the authorization flow or show completion once finished. Report a mismatched or failed flow as an error. The user ID comes from the verified session. The route never takes it from an MCP argument or browser query parameter, and the Arcade API key stays on the server. The implementation includes the sign-in redirect and failure handling. For identity matching, keep both OAuth applications in the same Clerk instance and select sub as the User Source subject. Clerk documents this subject as the authenticated user’s ID. Clerk’s OIDC claims https://clerk.com/docs/guides/configure/auth-strategies/oauth/single-sign-on and Arcade’s verifier contract https://docs.arcade.dev/en/build/user-facing-agents/secure-auth-production describe the user ID Clerk supplies and the identity Arcade expects. This was one detail we had to untangle. Connecting to the gateway doesn’t give the MCP tool your application’s browser session. The verifier uses that session in the browser application. The MCP tool sends the user’s delegated OAuth access token to the API. 6. Configure Clerk Sign-In for the MCP Gateway The User Source connects the gateway to Clerk, so users can sign in with the same login they use for the reporting application. Configure it with the separate gateway OAuth application you created in step 2. In the Arcade demo project’s User Sources , add: Confirm that the Clerk gateway application’s redirect list contains: https://cloud.arcade.dev/oauth2/intermediate callback Gateway login uses the User Source. API access uses the OAuth provider. Tool authorization also uses the browser verifier. Keep their names and callback values separate when troubleshooting. 7. Add OAuth-Protected Tools to the MCP Server The finished server lives in mcp/src/report demo mcp/server.py. It exposes four operations: Start with get current user : @app.tool requires auth=REPORT AUTH, requires secrets= API CONFIG KEY async def get current user context: Context - dict: """Get the API-verified user and their permitted workspaces. Call this to discover the workspace IDs available to the signed-in user. The API derives identity from the OAuth token; the model supplies no user ID. """ return await api request context, "GET", "/api/me", expected statuses={200} The shared api request helper calls context.get auth token or empty , rejects an empty token, and sends it to the configured API in an Authorization: Bearer ... header. The helper limits request time and response size, disables redirects, and returns safe error messages. The download includes the full helper. Arcade injects Context when the tool runs. The model supplies neither the token nor the destination URL. Each tool declares REPORT API BASE URL as required configuration and reads it through context.get secret ... . The origin isn’t confidential. The secrets mechanism supplies it to the hosted tool. On the API side, ask Clerk to verify an OAuth access token: js const identity = await auth { acceptsToken: “oauth token” } ; The API checks the verified OAuth client ID against REPORTS OAUTH CLIENT ID , then looks up the user’s fixture membership. The API accepts OAuth access tokens issued for the Report demo API application and rejects gateway credentials and browser-session tokens. Clerk’s helpers handle both JWT and opaque OAuth formats. Clerk’s OAuth-token verification guide https://clerk.com/docs/guides/configure/auth-strategies/oauth/verify-oauth-tokens covers that boundary. If a tool needs user details, it can call /api/me . You can also configure a provider user info request to put metadata in context.authorization.user info . This example doesn’t need it, and the API still must check access. The OAuth-provider reference https://docs.arcade.dev/en/references/auth-providers/oauth2 user info request describes that optional configuration. 8. Deploy the MCP Server to Arcade Cloud Deploy the mcp/ project so customers can call its protected tools through an Arcade gateway. Keep the separate web/ application running at the HTTPS origin from step 3. From report-mcp-demo/mcp: uv run arcade login uv run arcade project list uv run arcade project set YOUR DEMO PROJECT ID uv run arcade deploy -e src/report demo mcp/server.py --secrets skip Replace YOUR DEMO PROJECT ID with the demo project’s ID from the list. The CLI uses the saved project, so select the one containing your User Source, OAuth provider, and custom verifier. --secrets skip tells the CLI not to upload local secret values. You’ll configure the required API address after the server has been deployed, before calling any reporting tools. The entrypoint defaults to stdio for local development. Arcade supplies the hosted execution settings during deployment. Run the deploy command from the directory containing pyproject.toml , as described in Arcade Deploy https://docs.arcade.dev/en/build/arcade-deploy . Wait for deployment to succeed and keep the returned server name. In the Arcade dashboard, open Servers , select that server, and open its secret configuration. Add the following key and replace the placeholder with your backend’s public HTTPS origin: REPORT API BASE URL=https://YOUR-NGROK-DOMAIN Use the demo’s ngrok origin, or your deployed API’s HTTPS origin, without /api or a trailing slash. Save the value before proceeding. This address may differ from the browser verifier’s origin in an existing application. From Arcade Cloud, localhost doesn’t point to your computer. The configuration splits across four places: Customers never receive your developer Arcade API key. Arcade supplies their API token to the tool after its authorization requirements are satisfied. On the server’s page, confirm that the deployment is healthy and has all four reporting tools. You can also inspect registered servers from this project with: uv run arcade server list The tools may appear with names such as ReportDemo GetCurrentUser. Checkpoint: The intended server is healthy, lists all four operations from step 7, and has REPORT API BASE URL configured. Server health confirms that the deployment started; it doesn’t verify backend access. Next, connect Claude and call get current user to check customer login, tool authorization, and backend access together. 9. Create an MCP Gateway and Connect Claude Desktop The gateway gives Claude one URL for your tools and uses the User Source to sign customers in. Create an MCP Gateway in that project. Select the four reporting tools from your deployment. Under Non-Arcade Users → User Source , choose Report demo Clerk . Copy the resulting gateway URL, which has this form: https://api.arcade.dev/mcp/YOUR-GATEWAY-SLUG In Claude Desktop, use Settings → Connectors → Add custom connector , enter that URL, and retain the detected OAuth settings and Claude’s published client identity. Sign in with Alice’s demo application account. Alice should not be an Arcade project member. Gateway configuration https://docs.arcade.dev/en/operate/governance/mcp-gateways/create-via-dashboard and Claude’s Arcade connection guide https://docs.arcade.dev/en/get-started/mcp-clients/claude-desktop describe these steps. Ask: Use the reporting tools to show my current user and available workspaces. On the first call, follow any tool-authorization link the client presents. Complete API consent and browser verification, then call the tool again if the client hasn’t resumed. Existing sessions can reduce the prompts. Gateway login and the API grant remain separate, and you don’t need to paste a token into the conversation. The response should identify Alice and include team-red . Expected response shape for the Alice fixture: { "user id": "user ALICE CLERK ID", "workspaces": { "workspace id": "team-red", "name": "Team Red", "tier": "pro" } } user ALICE CLERK ID stands for the Clerk user ID you configured for Alice. This illustrates the response, not a captured live result. Checkpoint: The response identifies Alice and includes team-red . Gateway login, tool authorization, and the API now work together. You can use that workspace ID to request a report. 9.1 Start Reports, Check Status, and Retrieve Results Some backend operations continue after the initial request returns. To expose that job workflow through MCP, the agent needs to start work, check progress, and collect the result. A job ID connects those calls. The synthetic report uses this pattern so you can apply it to an existing background workflow in your application. start report sends a request such as: { "workspace id": "team-red", "topic": "Onboarding", "report type": "advanced" } The API returns HTTP 202 with a report id , status , and poll after seconds . The job has been accepted. The report is still pending. The agent waits, checks status, and retrieves the result when the status becomes completed . The agent should stop on failed and limit the number of checks. The web application runs the jobs and stores their state. Each MCP call on Arcade Cloud starts work or reads progress. The job continues in the backend between calls. The sample uses a small in-memory job store and synthetic records. Restarting the web process loses the jobs. Reports also expire after 15 minutes. A production application would use its existing durable job system. If a POST times out, a report might already exist. The helper doesn’t tell the model to submit again blindly. Retrying a read and retrying job creation carry different consequences. Then ask: Start an advanced report about onboarding in team-red. Keep its report ID, wait at least as long as the response requests between status checks, and retrieve the completed result. Stop after ten checks if still pending. The agent can now find an accessible workspace, start a report, and retrieve the result. 10. Test Workspace Permissions and Subscription Tiers Now check that agent access respects the SaaS rules we started with: colleagues can work within their shared workspace, customers remain separated, and report access respects the workspace’s plan. The reporting API checks workspace access and the required tier on every report request. Use separate browser/client profiles, or reconnect when switching identities, so a cached grant doesn’t hide which user you’re testing. The status and result routes look up the report’s workspace and check access again. Knowing a job ID doesn’t grant access to its output. Apply the same check to a download endpoint as to the request that starts the job. Use the optional staff account to check access to both workspaces and confirm Team Blue’s Basic tier still blocks advanced report creation. The model can’t grant staff access by setting an is admin argument. For your application, use the roles and policies your server controls. Run the included local checks: From web/ npm test npm run typecheck From mcp/ uv run pytest These checks cover the synthetic permissions and request behavior. Use the demo application accounts you created to test gateway login, tool consent, and browser verification. Start with the hosted get current user call before creating a report. A local greeting or healthy server alone won’t test that flow. Troubleshoot MCP Authentication and API Errors We hit this during our integration: authentication worked, but the tool couldn’t reach the backend. The problem was on our server. A successful login hadn’t told us whether the API request would work. Check the MCP deployment logs and the web application’s logs separately. For the MCP logs, use the server name returned by deployment: uv run arcade server logs YOUR-SERVER-NAME Log a request outcome and safe error category. Avoid logging bearer tokens, full authorization context, or upstream response bodies that may contain customer data. Connect MCP Tools to Your Existing Backend API To connect your own application, replace the synthetic report API with an operation your backend already provides. Describe the tools precisely: which one starts work, which retrieves results, and which identifiers the agent needs to keep. Replace the fixtures with your existing membership, role, and entitlement checks. Production Extensions: Tool Visibility, Permissions, and Durable Jobs The example gives users the same four tools and enforces their permissions in the API. Further production work could include: - Tool visibility based on permissions. Show clients tools appropriate to the user’s role and plan, for example through Arcade’s Contextual Access https://docs.arcade.dev/en/operate/governance/contextual-access . Keep the API checks when tools execute. Here, basic and advanced reports share start report . Hiding that tool from Basic users would also remove their permitted basic reports. The requested workspace and report type still need authorization. - Changing memberships and plans. Read current access rules from your application instead of static fixtures. Define and test how membership revocation and plan downgrades affect new requests and existing jobs. - Durable jobs and safe retries. Persist job state and make report creation idempotent, so retrying the same submission after a timeout returns its original job instead of creating a duplicate. This enables complex, multi-step workflows where a single timeout or API failure doesn’t crash the entire agentic loop. The MCP server gives the agent operations to call. Your API verifies the user’s delegated credential and decides whether to allow the operation. Clerk supplies the identity. Arcade.dev runs the gateway and MCP tools and supplies the user’s API token after authorization. You can expose more of your backend through the same pattern. Full code: multi-tenant-mcp-server-oauth https://github.com/manveer/multi-tenant-mcp-server-oauth . Clone it, run the tests, swap in your API. Frequently Asked Questions Does MCP support OAuth? Yes. The Model Context Protocol supports OAuth-protected remote servers, and clients like Claude and ChatGPT can complete that flow. This tutorial uses OAuth twice: once to sign the user into the Arcade.dev gateway, and once to obtain the delegated Clerk token that the tools send to the reporting API. Why use an MCP gateway instead of exposing the MCP server directly? A gateway, such as Arcade.dev, handles the plumbing you would otherwise build: sign-in through your existing identity provider, per-user token storage and refresh, and injecting the right user’s token into the right tool call. It also gives customers one stable URL that can federate more MCP servers and enforce tool-level governance as you grow. Why not use Clerk’s MCP support instead of a gateway? You can. Clerk can act as the authorization server for a self-hosted MCP server through dynamic client registration, which collapses this tutorial’s three credentials into one. The trade-off: you host and operate the server yourself, and Clerk’s MCP helpers target JavaScript frameworks while these tools are Python. Note that mcp.clerk.com is a different product: Clerk’s own documentation server for AI coding agents. Do customers need an Arcade.dev account to connect their agent? No. The gateway’s User Source lets customers sign in with their existing application login, with Clerk as the identity provider. Only the developer who deploys the MCP server needs an Arcade.dev account. Does this work with ChatGPT or only Claude? Any MCP client that supports the Streamable HTTP transport can use the same gateway URL. This tutorial connects Claude Desktop. For ChatGPT, add the gateway URL as a custom connector with OAuth, which requires developer mode and a plan that supports custom connectors. How do you stop an AI agent from accessing another tenant’s data? Derive identity from the verified OAuth token and authorize in the API, never from arguments the model supplies. Here, the reporting API checks workspace membership and plan tier on every request, and lookups for a report the caller cannot access return the same response as an unknown ID, so report IDs grant nothing. What’s the difference between authentication and authorization in an MCP server? Authentication proves who is calling; authorization decides what they may do. In this architecture, Clerk and the Arcade gateway handle authentication and token delegation, while authorization stays in the backend API, which applies the same workspace and tier rules it enforces for every other client.