# Build a Multi-Tenant MCP Server with OAuth (Clerk and Arcade)

> Source: <https://manveerc.substack.com/p/multi-tenant-mcp-server-oauth>
> Published: 2026-10-16 16:31:10+00:00

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.
