Skip to main content
Glama
rodolfostoicus

mcp-access-oauth

Model Context Protocol (MCP) Server + Access OAuth

This is a Model Context Protocol (MCP) server that supports remote MCP connections, with Access OAuth built-in.

You can deploy it to your own Cloudflare account, and after you create your own Access for SaaS OIDC app, you'll have a fully functional remote MCP server that you can build off. Users will be able to connect to your MCP server by signing in with your connected Access Identity Provider.

The MCP server (powered by Cloudflare Workers):

  • Acts as OAuth Server to your MCP clients

  • Acts as OAuth Client to your real OAuth server (in this case, Access)

Getting Started

Clone the repo & install dependencies: npm install

For Production

Create a new Access for SaaS OIDC App:

  • For the Authorization callback URL, specify https://mcp-access-oauth.<your-subdomain>.workers.dev/callback and http://localhost:8788/callback if you are developing locally.

  • Note your Client ID and Client secret.

  • Set secrets via Wrangler

wrangler secret put ACCESS_CLIENT_ID
wrangler secret put ACCESS_CLIENT_SECRET
wrangler secret put ACCESS_TOKEN_URL
wrangler secret put ACCESS_AUTHORIZATION_URL
wrangler secret put ACCESS_JWKS_URL
wrangler secret put COOKIE_ENCRYPTION_KEY # add any random string here e.g. openssl rand -hex 32
wrangler secret put META_ACCESS_TOKEN # paste only in Wrangler's hidden prompt

Never put a real Meta token in this repository, a shell command, a support ticket, or a chat. META_ACCESS_TOKEN must exist only as a Cloudflare Worker secret.

Set up a KV namespace

  • Create the KV namespace: wrangler kv:namespace create "OAUTH_KV"

  • Update the Wrangler file with the KV ID

Deploy & Test

Deploy the MCP server to make it available on your workers.dev domain wrangler deploy --keep-vars

Test the remote server using Inspector:

npx @modelcontextprotocol/inspector@latest

Enter https://mcp-access-oauth.<your-subdomain>.workers.dev/mcp and hit connect. Once you go through the authentication flow, you'll see the Tools working:

You now have a remote MCP server deployed!

Access Control

This MCP server uses Access for authentication. All authenticated Access users can access basic tools like "add".

The "generateImage" tool is restricted to specific Access users listed in the ALLOWED_USERNAMES configuration:

// Add user emails for image generation access
const ALLOWED_EMAILS = new Set(["employee1@mycompany.com", "teammate1@mycompany.com"]);

Access the remote MCP server from Claude Desktop

Open Claude Desktop and navigate to Settings -> Developer -> Edit Config. This opens the configuration file that controls which MCP servers Claude can access.

Replace the content with the following configuration. Once you restart Claude Desktop, a browser window will open showing your OAuth login page. Complete the authentication flow to grant Claude access to your MCP server. After you grant access, the tools will become available for you to use.

{
  "mcpServers": {
    "math": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://mcp-access-oauth.<your-subdomain>.workers.dev/mcp"
      ]
    }
  }
}

Once the Tools (under 🔨) show up in the interface, you can ask Claude to use them. For example: "Could you use the math tool to add 23 and 19?". Claude should invoke the tool and show the result generated by the MCP server.

For Local Development

If you'd like to iterate and test your MCP server, you can do so in local development.

  • For the Homepage URL, specify http://localhost:8788

  • For the Authorization callback URL, specify http://localhost:8788/callback

  • Note your Client ID and generate a Client secret.

  • Create a .dev.vars file in your project root with:

ACCESS_CLIENT_ID=<your client id>
ACCESS_CLIENT_SECRET=<your client secret>
ACCESS_TOKEN_URL=<your Access for SaaS token url>
ACCESS_AUTHORIZATION_URL=<your Access for SaaS authorization url>
ACCESS_JWKS_URL=<your Access for SaaS JWKS url>
COOKIE_ENCRYPTION_KEY=COOKIE_ENCRYPTION_KEY
META_ACCESS_TOKEN=<Meta Graph access token; never commit a real token>

Develop & Test

Run the server locally to make it available at http://localhost:8788 wrangler dev

To test the local server, enter http://localhost:8788/mcp into Inspector and hit connect. Once you follow the prompts, you'll be able to "List Tools".

Using Claude and other MCP Clients

When using Claude to connect to your remote MCP server, you may see some error messages. This is because Claude Desktop doesn't yet support remote MCP servers, so it sometimes gets confused. To verify whether the MCP server is connected, hover over the 🔨 icon in the bottom right corner of Claude's interface. You should see your tools available there.

Using Cursor and other MCP Clients

To connect Cursor with your MCP server, choose Type: "Command" and in the Command field, combine the command and args fields into one (e.g. npx mcp-remote https://<your-worker-name>.<your-subdomain>.workers.dev/mcp).

Note that while Cursor supports HTTP+SSE servers, it doesn't support authentication, so you still need to use mcp-remote (and to use a STDIO server, not an HTTP one).

You can connect your MCP server to other MCP clients like Windsurf by opening the client's configuration file, adding the same JSON that was used for the Claude setup, and restarting the MCP client.

Related MCP server: cloudflare-mcp-google-auth

Meta Ads authentication and operational safety

The connector has two independent authentication layers:

  • Cloudflare Access OAuth authenticates ChatGPT or another MCP client to this Worker. Reconnecting the MCP client refreshes this layer only.

  • Meta Graph authentication uses the separate META_ACCESS_TOKEN Worker secret. Reconnecting ChatGPT, signing in to Facebook, or changing a human Facebook password does not update this secret.

Version 2.3.6 routes Meta Graph requests through META_API_GATE, one named Durable Object per configured ad account. The gate serializes requests across MCP sessions and spaces every request attempt by at least 250 ms. A rate-limited GET receives at most one automatic retry when the required wait is no more than five seconds. A longer wait or a persistent limit starts a cooldown of at least 60 seconds and returns immediately. POST requests are never retried automatically, so an ambiguous write is not repeated. If the binding is missing, Meta calls fail closed instead of bypassing the gate.

The separate META_WRITE_LOCK Durable Object preserves existing 10-minute account leases. In v2.3.5, only meta_set_delivery_status uses a unique, server-generated operation owner instead of an MCP transport-session owner. It acquires the lock before reading the object, sends no POST for an already-matching configured status, and checks ownership again at the gate immediately before dispatch. A confirmed mutation and read-back release only that operation's lease. Pre-POST validation failures and structured gate proof that no POST was dispatched also release it; uncertain POST outcomes retain it until expiry. The protected POST has a 30-second local timeout and requires more than 60 seconds remaining on its lease before dispatch. A timeout cannot prove Meta stopped processing the request. Release failure is reported as a warning without obscuring a verified successful Meta change. A previous active lease is never cleared or replaced by this change.

Other mutators retain their prior session-based leases: a real-write attempt acquires or renews a 10-minute lease, another session is blocked, and a failed or ambiguous call retains the lease. The original session can use meta_release_write_lease, or wait for expiry. Validate-only requests do not acquire leases. The status command's ownership check and automatic release do not add these guarantees to legacy mutators.

Version 2.3.6 adds meta_update_adset_geo with the same operation-owned lease, dispatch fencing and verified release as the status tool. Both geography previews and real updates hold the lease throughout their reads and validation. The tool accepts only Southern Brazil region keys (452 PR, 456 RS, 459 SC), starts with Meta validate_only by default, and sets targeting_automation.individual_setting.geo=0. Exact name and confirmation are required for a real update. Brazil-wide coverage can be restricted to these states; already included states can be narrowed further. City/radius coverage, additional countries and mixed inclusion types fail closed. Exclusions, presence types, age, professions, other automation settings, budgets, schedule, destination, optimization, name and configured status are preserved and compared on read-back. Only region display names/country enrichment and region ordering are normalized. A missing or changed geographic expansion control fails verification. No object is created or activated. Other mutators retain their existing semantics.

Version 2.3.7 omits the obsolete targeting.targeting_optimization=none from geography update payloads because Graph can return it in reads while rejecting it in writes (code 100/subcode 1870197). Only its retention as none or omission are treated as equivalent on read-back; other legacy values fail closed. The effective targeting_optimization_types, individual_setting.geo=0 and every other preserved setting are still compared without relaxation.

Version 2.3.8 adds a separate meta_configure_brevar_adset operation for the authorized BREVAR profile. It requires an owned parent campaign whose exact name identifies BREVAR, accepts only active/paused unexpired objects, and applies ten explicitly selected medical work-position IDs, ages 25–50 with unknown ages disabled, both sexes, all of PR/RS/SC, and daily 06:00–23:00 Brasília. It preserves location-presence types and other placements/restrictions, replaces detailed inclusion filters, removes obsolete age ranges and exclusions of the three newly included states, and disables audience, geographic and lookalike/custom-audience relaxation. Detailed and lookalike diagnostics must already be explicitly zero and remain zero; these read-only fields are never sent as update parameters. The original geography-only tool keeps its narrower behavior.

The new configuration operation verifies the account timezone before and after validation and after mutation. In America/Noronha it writes 07:00–24:00 advertiser-time windows; in America/Sao_Paulo, 06:00–23:00. It adds ad-set day_parting pacing, requires one existing lifetime budget source (CBO or ABO) with no daily budget, and never edits the parent campaign or any budget, status, objective, destination or dates. Meta validation must confirm the specific budget/pacing combination before a real write; a platform restriction is returned without a workaround, retry or budget transfer. The last delivery window remains bounded by the original end date. An optional rename is included in the exact confirmation phrase. Operation leases, concurrent-change checks covering both ad set and campaign, and full read-back verification fence every real write. An uncertain real result retains its lease and requires reconciliation. This does not bypass Instagram native boosted-post restrictions or activate ads.

The gate protects the API from concurrent bursts; locks coordinate connector operations, not edits made directly in Meta. Status operations release after verified completion, so different chats can interleave separate operations. Neither mechanism makes a multi-operation business workflow atomic. Keep one logical write operator per campaign. Other chats may inspect unrelated work, but only one chat should create, edit, pause, or activate objects in the same campaign until its read-after-write audit is complete.

Version 2.3.9 respects the observed per-ad-set configuration limit (Meta code 613/subcode 4841018): the real meta_configure_brevar_adset path waits 31 seconds after successful Meta validation before rechecking its operation lease and re-reading the ad set, parent campaign and account timezone. The wait is outside the shared API gateway, so read-only traffic can continue. Previews do not wait, and no POST is automatically retried. The existing lease retention and reconciliation rules remain unchanged for a failed or uncertain real update. This adds 31 seconds of wall time to real configuration calls; clients need enough time for that wait plus Graph reads, the bounded POST and read-back. The same 31-second separation applies between creative attachment validation and actual attachment to the same ad ID, followed by lease and hierarchy rechecks; distinct creative-create and inline-preview endpoints receive no added wait, and the creative journal is unchanged. A client timeout does not prove the update failed. If a separate preview was just performed, let its per-object cooldown pass before beginning another configuration call.

Version 2.3.10 follows Meta's Advantage campaign budget and pacing/scheduling model: with CBO, pacing belongs to the campaign. Campaign listings now request pacing_type. BREVAR child configuration still writes and audits the exact targeting and weekly windows, but sends no child pacing field under CBO. It preserves any returned child pacing field strictly; an omitted field is never normalized to day_parting. verified=true confirms the saved child settings, while delivery_schedule_verified=false explicitly means parent day-parting remains pending before activation. ABO continues to require child day-parting.

The new meta_configure_brevar_campaign_pacing changes only parent pacing_type to ["day_parting"]. It requires an exactly named owned BREVAR campaign, its explicit existing lifetime cap, no daily budget, and a complete audit of every child (including paused children). All children must already have the 06:00–23:00 Brasília window, unchanged ownership/status/dates and no child budget. A paginated/incomplete child snapshot fails closed. Preview and real calls use operation leases; real writes wait 31 seconds after validation and re-read the parent, all children and account timezone before dispatch and after saving. Any concurrent budget, field, child-list or schedule change stops the operation or fails read-back. No budget, date, target, status or child schedule is written by the parent operation. Configure CBO child windows first, configure and verify parent pacing second, then assess activation separately.

Version 2.3.11 adds an explicit, paused-only preparation option to meta_configure_brevar_campaign_pacing for Meta accounts that require parent day-parting before a missing child calendar can be saved. allow_unscheduled_children defaults to false. When true, the confirmation must additionally end with ALLOW_UNSCHEDULED_CHILDREN_WHILE_PAUSED, and the parent must be PAUSED in the initial read, after the 31-second wait, and in final read-back. Only an absent adset_schedule field is allowed; an empty, malformed or present divergent calendar is still rejected. Every ownership, child-list, budget, status, date and targeting comparison remains unchanged. The sole POST field is parent pacing; no child, budget or status is written. Updated, preview and no-change results return pending_adset_ids; any pending calendar keeps delivery_schedule_verified=false, even if parent pacing is already day_parting. Keep the campaign paused, configure the missing child calendars, and perform the normal complete audit before activation.

Version 2.3.12 corrects the CBO child write payload using observed account behavior: the successful calendar update included child pacing_type=["day_parting"], whereas updates without that parameter returned success but omitted the calendar. The configurator now sends this calendar write parameter under CBO as well as ABO. A CBO change requires parent day_parting before either validation or the real POST, and the full parent snapshot is rechecked after the existing 31-second wait and after the write. Read-back still requires the actual exact child calendar and the parent budget-owned pacing; it preserves the child pacing representation actually read rather than requiring a field Meta omitted. An already configured child remains a no-POST path with the parent scheduling diagnostic. This does not change budgets, statuses, dates, lease retention or retry behavior, and API success alone still cannot verify a saved calendar.

Version 2.3.13 adds the explicit facebook_instagram_only option (default false) to BREVAR ad-set configuration. It writes exactly publisher_platforms=["facebook","instagram"]; its real confirmation must end in PLACEMENTS FACEBOOK_INSTAGRAM_ONLY, after any name suffix. Meta's Basic Targeting reference defines user_age_unknown as applicable only to WhatsApp Status, and the v26 announcement specifies an unknown-age default when that placement is selected. Consequently, this explicit option compares false with omission only when both compared targeting snapshots explicitly contain exactly Facebook and Instagram (in either order) and neither unknown-age value is anything other than false or absent. Missing, duplicate or additional platforms, true, null, and automatic placements retain strict comparison. Without the option, the previous explicit-false verification remains unchanged. The original raw read-back is returned without inventing a saved value, with an unknown_age_note; all other targeting and schedule fields, budgets, dates, states, parent pacing and concurrency checks remain strict. Selecting a WhatsApp chat destination is distinct from enabling a WhatsApp Status placement.

Version 2.3.14 separates creative.name, an administrative label observed to be rewritten by Meta after creative creation, from all visible creative content. Replacement and read-back retain exact comparison of the entire object_story_spec, including link_data.name (the visible headline), message, description, image hash, approved Page, URL and CTA, plus the preserved creative settings. Only the top-level administrative label is allowed to differ; the requested and actual values are returned explicitly. The original proposal and fingerprint are never rewritten to conceal the difference.

meta_get_brevar_creative_operation is read-only and takes an exact ad_id, current expected_name and original request_id. It confirms account/ownership and reads the durable journal, original snapshots, current hierarchy and recorded created creative. It returns full proposed/actual creative structures, administrative-name evidence, content verification, configuration differences, eligibility and the exact resume confirmation. It does not acquire, renew, release or clear a lease, works while another operation holds one, exposes no holder identifier and sends no Meta POST.

meta_resume_brevar_ad_creative adds an explicitly confirmed recovery path for that same request UUID and already-created creative ID. Its required inputs additionally bind creative_id, expected_fingerprint, expected_stage and the inspected expected_creative_name (string or null). Default validate_only=true is a read-only evidence preview, not a Meta validation. From CREATIVE_CREATED, a real resume requires the original ad to remain PAUSED with its original creative and unchanged parent configuration, verifies the recorded new creative content, validates the association, waits 31 seconds, and re-reads the lease, immutable journal, complete hierarchy and new creative before one attachment. It creates no creative or ad. ATTACH_PENDING never sends another Meta POST: it can only advance to COMPLETE after the exact existing association/content/configuration is already verified. COMPLETE supports verified read-only replay; CREATE_PENDING is not resumable. A competing lease is respected. Failed or uncertain attachment retains durable intent and does not release its lease, recreate, retry, roll back or change request IDs. The Durable Object freezes before, proposed, fingerprint and an already-known creative ID across journal transitions.

Version 2.3.15 reconciles two narrowly observed derived changes after attaching a static link-image creative. For tracking_specs, it independently reads the original and replacement creative IDs with account_id and effective_object_story_id, confirms the configured account and Stoicus Page, and permits only the exact post_engagement, post_interaction_gross and link_click rows to reference the verified new post and reorder among themselves. Every other tracking group, pixel/custom-conversion value, field and their relative order remains exact; missing, duplicate, enriched, foreign or unproved derived rows fail closed. The proof is read separately, so legacy immutable snapshots need no added metadata. Results expose tracking_evidence with both verified story IDs.

For degrees of freedom, only the observed omission of video_auto_crop, video_filtering and video_uncrop from prior exact OPT_IN entries can be reconciled, and only when both proposed and actual creatives are static link images without video, dynamic, existing-post or carousel representations and have exact OPT_OUT controls for image_animation, media_type_automation and multi_photo_to_video. Every other option and visible field remains exact. Meta's official Business SDK feature specification defines these separate feature fields; the narrow omission handling is grounded in the observed static-image response, not a claim that absence equals OPT_OUT. Raw proposals, actual values, journal snapshots and fingerprints remain unchanged, and observed_static_video_omissions identifies the omitted keys. Existing operation schemas, leases and 31-second ordering remain unchanged. In ATTACH_PENDING, even this reconciliation sends no Meta POST: only a proven existing association can complete the same journal.

Version 2.3.16 separates two closed derived-post profiles using unchanged before/current ad-set goal and destination. CONVERSATIONS/WHATSAPP retains the three tracking actions above and requires conversion_specs to remain exact. For the observed POST_ENGAGEMENT/ON_POST profile, tracking must contain exactly the two derived actions post_interaction_gross and link_click with their exact Page/post shapes, while conversion_specs must be exactly one post_engagement Page/post row. Only verified original-to-replacement post IDs may differ. All non-derived tracking rows, conversion IDs, row counts, extra fields, and unrelated settings remain strict. The Meta tracking specification reference describes the Page/post engagement conversion structure. Both creative IDs and their post ownership are still proved by separate reads; no conversion, tracking or targeting field is written.

Version 2.3.17 adds four reviewed 9:16 AHA image assets to the immutable public creative manifest. It does not change targeting, budgets, delivery, write-lock behavior or the schemas of existing Meta tools.

Creative operation inspection and resume evidence now begin with diagnostic, before potentially large immutable snapshots. It includes original/current creative IDs, exact tracking and conversion arrays, configured/effective status, optimization goal/destination, content verification, remaining differences, the independently verified post IDs, reviewed label, fingerprint and exact resume confirmation. Verified post proof remains visible even if a subsequent tracking/conversion comparison fails. This changes output only; tool input schemas are unchanged. An already attached ATTACH_PENDING operation still sends zero Meta POSTs and can only complete its original journal after the full strict comparison succeeds.

The same 2.3.8 release adds meta_upload_creative_asset: its default preview reads only the exact public raster manifest entry and returns MIME, byte count and SHA-256 without contacting Meta. The real upload accepts no external URL or caller-supplied bytes, requires exact account-bound confirmation and an operation lease, posts the manifest bytes to the account image library, and verifies the returned hash/account by read-back. It never creates an ad or changes delivery or budget. Uncertain uploads retain their operation lease and are not automatically repeated.

meta_update_brevar_ad_creative corrects one existing, explicitly named BREVAR link-image ad that is configured PAUSED. It requires an account-owned image hash, approved Stoicus Page and either the approved WhatsApp phone or BREVAR course URL. The default preview uses Meta validate_only without creating an ad or creative. A confirmed real operation creates one replacement creative and attaches it to the same ad ID, preserving names, configured status, parent relationships, complete audience/schedule settings and budgets. It replaces message/headline/description/image/destination and removes stale welcome-message and caption copy, while preserving Instagram identity and supported creative settings. It never activates an ad or bypasses native Instagram/existing-post restrictions.

Creative replacement uses a durable, lease-protected journal with atomic stages and a per-ad blocker. Exact confirmation binds the ad, request UUID and SHA-256 of the proposed inputs. Durable intent precedes each real POST; complete replay verifies the current hierarchy and creative content without another mutation. Incomplete outcomes block both the same UUID and a new UUID for the same ad until reconciliation, including after lease expiry. Conservatively, even a gate-proven non-dispatch after durable create intent remains blocked; the error explicitly distinguishes that case. The operation does not automatically recreate, reattach, roll back or release a lease after a partial real-write path.

Recovering access after a password, security, or asset-access change

  1. Stop Meta writes and run the minimal permission diagnostic. Both OAuth scopes and access to the configured ad account must pass; a scope-only success is not write readiness.

  2. When administrative evidence is needed, provide one explicit business_access_diagnostic_id. This bounded, read-only mode returns only matches for the token subject and configured account, skips general inventory and Page/WhatsApp fan-out, and never proves write authority by itself.

  3. In Meta Business Manager, verify the confirmed identity's assignment to the configured account and relevant Page/WhatsApp assets. Do not identify it only by display name, and do not treat an observed ADMIN role as proof of account authorization.

  4. Generate a replacement system-user token only if Meta invalidated the token or access remains denied after the asset assignment and business/app restrictions are verified.

  5. Replace the secret through Cloudflare's hidden prompt: wrangler secret put META_ACCESS_TOKEN. Never paste the token into chat, logs, source code, .dev.vars.example, or Git.

  6. Deploy with wrangler deploy --keep-vars, then rerun the permission diagnostic. Resume reads or writes only when the configured account is accessible and the diagnostic reports the corresponding readiness flag as true.

Version 2.3.4 removes the temporary v2.3.3 account-assignment recovery tool and its META_BUSINESS_ID configuration after access recovery. Asset access must be assigned through Meta Business Settings by an authorized administrator. Bounded read-only business diagnostics, existing campaign tools, and the verified active-lease requirement remain available. Because deployment uses --keep-vars, an old META_BUSINESS_ID binding may remain in Cloudflare until it is explicitly deleted; the Worker no longer reads it. Do not remove --keep-vars or replace unrelated variables or secrets to clean up that unused binding.

Rate limits are handled by waiting and reducing concurrency, not by repeatedly reconnecting or regenerating credentials. If a cooldown is reported, let it expire and retry from one chat.

How does it work?

OAuth Provider

The OAuth Provider library serves as a complete OAuth 2.1 server implementation for Cloudflare Workers. It handles the complexities of the OAuth flow, including token issuance, validation, and management. In this project, it plays the dual role of:

  • Authenticating MCP clients that connect to your server

  • Managing the connection to Access's OAuth services

  • Securely storing tokens and authentication state in KV storage

Durable MCP

Durable MCP extends the base MCP functionality with Cloudflare's Durable Objects, providing:

  • Persistent state management for your MCP server

  • Secure storage of authentication context between requests

  • Access to authenticated user information via this.props

  • Support for conditional tool availability based on user identity

MCP Remote

The MCP Remote library enables your server to expose tools that can be invoked by MCP clients like the Inspector. It:

  • Defines the protocol for communication between clients and your server

  • Provides a structured way to define tools

  • Handles serialization and deserialization of requests and responses

  • Maintains the Server-Sent Events (SSE) connection between clients and your server

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    A Cloudflare Workers-based MCP server that enables secure remote connections using built-in OAuth authentication via Cloudflare Access. It provides identity-based access control for MCP tools and supports persistent state management through Durable Objects and SSE.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    Remote MCP server with built-in OAuth authentication via Cloudflare Access, enabling secure tool access and user identity-based restrictions.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    A remote MCP server with built-in OAuth authentication via Cloudflare Access, providing tools like add and generateImage with user-based access control.
    -