Skip to content

Add access.modelcontextprotocol.io: GitHub sign-in for Google Workspace - #230

Open
sambhav wants to merge 10 commits into
mainfrom
sambhav/access-site
Open

sambhav wants to merge 10 commits into
mainfrom
sambhav/access-site

Conversation

@sambhav

@sambhav sambhav commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Maintainers sign in to MCP Google Workspace with GitHub instead of a Google password.

Google stays the IdP for everything. Workspace's built-in "SSO with a third-party IdP" feature hands off only the password step:

App → Google → access.modelcontextprotocol.io → GitHub

access-site/ is a small Cloudflare Worker (Hono, jose, D1) that acts as the OIDC provider for Google's custom OIDC SSO profile. It signs the user in with GitHub and returns an ID token whose subject is their numeric GitHub user ID and whose email is their Workspace address.

Accounts stay infrastructure as code. The address comes from users.ts (googleEmailPrefix). A member can use a provisioned or existing Workspace account once their verified numeric githubUserId is recorded in the same config. The Worker bundles src/config and redeploys on every config change, so removing someone from users.ts stops their sign-in on the next deploy. It holds no Google credentials and creates nothing in Google. GitHub users without an account get a page explaining how to request one.

Provisioning is in GitHub Actions too: on merge to main, access-site/scripts/deploy.sh finds or creates the D1 database, applies migrations and deploys the Worker with secrets from the production environment. Non-secret settings are committed in access-site/wrangler.toml. Until all four required secrets exist, the deploy job is skipped with a notice, so this can merge before setup.

Security

  • The GitHub login and verified numeric ID must match users.ts before any token is issued. Recycled logins are refused from the first attempt.
  • GitHub callbacks use OAuth state and S256 PKCE bound to the host-only pending request cookie. Cancellation returns to Google with its original state.
  • Authorization codes are single-use (D1 DELETE ... RETURNING), stored hashed, 2 minute TTL; PKCE enforced when sent and downgrades rejected (RFC 9700); exact redirect URI match; client credentials form-decoded (RFC 6749) and compared in constant time
  • Per-IP rate limit (30/min) on /authorize and /login
  • The Worker reads the basic GitHub user profile and revokes the token. The GitHub App needs no repository, organization, or account permissions.
  • The Worker holds no Google credentials; D1 holds short-lived sign-in state
  • CI: actions pinned to commit SHAs, Git credentials not persisted, deploy secrets exposed only to the deploy step. The deploy gate requires the GitHub client secret.
  • Super admins are exempt from the SSO profile and retain the break-glass Google sign-in
  • Changes to githubUserId and googleEmailPrefix require core maintainer review through CODEOWNERS.
  • Signing key rotation pre-publishes the next public key and retains the previous one through the cutover.

Testing

  • 23 Worker tests and Worker typecheck passed on Node 24. Tests cover a recycled login’s first sign-in attempt, existing Workspace accounts, denied or crossed OAuth callbacks, both PKCE flows, and JWKS overlap during rotation.
  • Repository formatting, config validation, and 79 repository tests passed. Wrangler’s dry run built the Worker.
  • A stubbed deploy verified the missing-secret guard, D1 creation on first run, reuse on repeat deploys, and the secret payload.

Rollout

Sign-in starts as a pilot for one person: PILOT_GITHUB_LOGINS = "sambhav" in wrangler.toml means only @sambhav ([email protected]) can get a token, even if the Google SSO profile is assigned more widely. A test checks that each pilot login maps to an account with a verified GitHub ID. Before expanding the pilot, verify each additional member’s GitHub ID in a reviewed PR and include only those users in the Google SSO profile.

Setup

Secrets go straight from the command line into GitHub secrets (needs openssl and gh), so none pass between people. This PR can merge first: the deploy job skips until the secrets exist.

@sambhav

  1. Cloudflare token: on the MCP Domain Account, scoped to Workers Scripts edit, D1 edit, and Workers Routes and DNS edit on modelcontextprotocol.io. Store as CLOUDFLARE_ACCESS_SITE_TOKEN in the production environment.
  2. OIDC signing key:
    openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
      | gh secret set ACCESS_SITE_OIDC_SIGNING_KEY --env production -R modelcontextprotocol/access
  3. After David's steps 1 and 2: PR the GitHub App client ID into wrangler.toml, merge, and check https://access.modelcontextprotocol.io/.well-known/openid-configuration.
  4. After David's step 3: PR the redirect URI into wrangler.toml, then test sign-in.

@dsp-ant (org owner and Workspace super admin)

  1. GitHub App at https://github.com/organizations/modelcontextprotocol/settings/apps/new:
    • Name: anything unique, e.g. "MCP Workspace Sign-in"
    • Homepage URL https://access.modelcontextprotocol.io
    • Callback URL https://access.modelcontextprotocol.io/login; leave "Expire user authorization tokens" on; device flow off
    • Webhook: untick Active
    • Permissions: leave repository, organization, and account permissions unset.
    • "Only on this account"; it doesn't need installing
    • Then Generate a new client secret and store it:
      gh secret set ACCESS_SITE_GITHUB_CLIENT_SECRET --env production -R modelcontextprotocol/access
    • Post the Client ID (not secret) on this PR
  2. OIDC client secret (you keep it, for step 3):
    openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n' > oidc-client-secret.txt
    gh secret set ACCESS_SITE_OIDC_CLIENT_SECRET --env production -R modelcontextprotocol/access < oidc-client-secret.txt
  3. Google SSO profile, once the discovery URL above is live: in the Admin console, Security → Authentication → SSO with third-party IdP → add a custom OIDC profile:
    • Issuer URL https://access.modelcontextprotocol.io
    • Client ID google-workspace; client secret from oidc-client-secret.txt (delete the file after)
    • Change password URL https://github.com/settings/security
    • Post the Redirect URI Google shows on this PR
    • Create a pilot sub-OU under Model Context Protocol containing only sambhav@, and assign the profile to it. Super admins are exempt, so your login is unaffected.

Before assigning the profile to Model Context Protocol and removing PILOT_GITHUB_LOGINS, verify GitHub IDs and Workspace mappings for everyone affected, including members with existing Workspace accounts.

Runbook, including key rotation: access-site/README.md.

https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn


Generated by Claude Code

A Cloudflare Worker (Hono, jose, D1) in access-site/ that acts as the OIDC
provider for Google Workspace's custom OIDC SSO profile, keyed on the
numeric GitHub user ID, and lets eligible maintainers create or link their
own @modelcontextprotocol.io account. Eligibility comes from the same
provisionUser rule Pulumi uses. A nightly job refreshes the account cache
and suspends accounts of people who left eligible teams (report-only by
default, capped per run, fail-safe on lookup errors).

Also:
- scripts/backfill-github-ids.ts: stamp existing accounts with GitHub IDs
- scripts/sync-team-groups.ts + workflow: mirror GitHub teams as Google groups
- src/google.ts: retainOnDelete on Workspace users (suspend, never delete)
- src/config/utils.ts: githubTeamsFor, shared by the Worker and the sync
- .github/workflows/access-site.yml: test on PR, migrate and deploy on main

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
@sambhav
sambhav requested a review from a team as a code owner October 24, 2026 16:34
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Pulumi Preview

Click to expand preview output
Previewing update (prod):
  pulumi:pulumi:Stack: (same)
    [urn=urn:pulumi:prod::mcp-access::pulumi:pulumi:Stack::mcp-access-prod]
    ~ googleworkspace:index/orgUnit:OrgUnit: (update)
        [id=id:03ph8a2z0nc6rsr]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/orgUnit:OrgUnit::mcp-org-unit]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        description      : "Model Context Protocol"
        name             : "Model Context Protocol"
        parentOrgUnitPath: "/"
    ~ googleworkspace:index/groupSettings:GroupSettings: (update)
        [[email protected]]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/groupSettings:GroupSettings::lead-maintainers]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        allowExternalMembers : false
        email                : "[email protected]"
        whoCanContactOwner   : "ALL_OWNERS_CAN_CONTACT"
        whoCanJoin           : "INVITED_CAN_JOIN"
        whoCanLeaveGroup     : "NONE_CAN_LEAVE"
        whoCanModerateMembers: "NONE"
        whoCanPostMessage    : "ALL_OWNERS_CAN_POST"
        whoCanViewGroup      : "ALL_OWNERS_CAN_VIEW"
        whoCanViewMembership : "ALL_IN_DOMAIN_CAN_VIEW"
    ~ googleworkspace:index/groupSettings:GroupSettings: (update)
        [[email protected]]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/groupSettings:GroupSettings::catch-all]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        allowExternalMembers : true
        email                : "[email protected]"
        whoCanContactOwner   : "ALL_OWNERS_CAN_CONTACT"
        whoCanJoin           : "INVITED_CAN_JOIN"
        whoCanLeaveGroup     : "NONE_CAN_LEAVE"
        whoCanModerateMembers: "NONE"
        whoCanPostMessage    : "ANYONE_CAN_POST"
        whoCanViewGroup      : "ALL_MEMBERS_CAN_VIEW"
        whoCanViewMembership : "ALL_IN_DOMAIN_CAN_VIEW"
    ~ googleworkspace:index/groupSettings:GroupSettings: (update)
        [[email protected]]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/groupSettings:GroupSettings::antitrust]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        allowExternalMembers : true
        email                : "[email protected]"
        whoCanContactOwner   : "ALL_OWNERS_CAN_CONTACT"
        whoCanJoin           : "INVITED_CAN_JOIN"
        whoCanLeaveGroup     : "NONE_CAN_LEAVE"
        whoCanModerateMembers: "NONE"
        whoCanPostMessage    : "ANYONE_CAN_POST"
        whoCanViewGroup      : "ALL_MEMBERS_CAN_VIEW"
        whoCanViewMembership : "ALL_IN_DOMAIN_CAN_VIEW"
    ~ googleworkspace:index/groupSettings:GroupSettings: (update)
        [[email protected]]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/groupSettings:GroupSettings::moderators]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        allowExternalMembers : false
        email                : "[email protected]"
        whoCanContactOwner   : "ALL_OWNERS_CAN_CONTACT"
        whoCanJoin           : "INVITED_CAN_JOIN"
        whoCanLeaveGroup     : "NONE_CAN_LEAVE"
        whoCanModerateMembers: "NONE"
        whoCanPostMessage    : "ALL_OWNERS_CAN_POST"
        whoCanViewGroup      : "ALL_OWNERS_CAN_VIEW"
        whoCanViewMembership : "ALL_IN_DOMAIN_CAN_VIEW"
    ~ googleworkspace:index/groupSettings:GroupSettings: (update)
        [[email protected]]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/groupSettings:GroupSettings::registry-wg]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        allowExternalMembers : true
        email                : "[email protected]"
        whoCanContactOwner   : "ALL_OWNERS_CAN_CONTACT"
        whoCanJoin           : "INVITED_CAN_JOIN"
        whoCanLeaveGroup     : "NONE_CAN_LEAVE"
        whoCanModerateMembers: "NONE"
        whoCanPostMessage    : "ALL_OWNERS_CAN_POST"
        whoCanViewGroup      : "ALL_OWNERS_CAN_VIEW"
        whoCanViewMembership : "ALL_IN_DOMAIN_CAN_VIEW"
    ~ googleworkspace:index/groupSettings:GroupSettings: (update)
        [[email protected]]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/groupSettings:GroupSettings::core-maintainers]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        allowExternalMembers : false
        email                : "[email protected]"
        whoCanContactOwner   : "ALL_OWNERS_CAN_CONTACT"
        whoCanJoin           : "INVITED_CAN_JOIN"
        whoCanLeaveGroup     : "NONE_CAN_LEAVE"
        whoCanModerateMembers: "NONE"
        whoCanPostMessage    : "ALL_OWNERS_CAN_POST"
        whoCanViewGroup      : "ALL_OWNERS_CAN_VIEW"
        whoCanViewMembership : "ALL_IN_DOMAIN_CAN_VIEW"
    ~ googleworkspace:index/groupSettings:GroupSettings: (update)
        [[email protected]]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/groupSettings:GroupSettings::maintainers]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        allowExternalMembers : true
        email                : "[email protected]"
        whoCanContactOwner   : "ALL_OWNERS_CAN_CONTACT"
        whoCanJoin           : "INVITED_CAN_JOIN"
        whoCanLeaveGroup     : "NONE_CAN_LEAVE"
        whoCanModerateMembers: "NONE"
        whoCanPostMessage    : "ALL_OWNERS_CAN_POST"
        whoCanViewGroup      : "ALL_OWNERS_CAN_VIEW"
        whoCanViewMembership : "ALL_IN_DOMAIN_CAN_VIEW"
    ~ googleworkspace:index/groupSettings:GroupSettings: (update)
        [[email protected]]
        [urn=urn:pulumi:prod::mcp-access::googleworkspace:index/groupSettings:GroupSettings::appeals]
        [provider=urn:pulumi:prod::mcp-access::pulumi:providers:googleworkspace::default_0_11_2::2a78e29b-bbb9-425e-a8de-1a161a6a7d9f]
        allowExternalMembers : false
        email                : "[email protected]"
        whoCanContactOwner   : "ALL_OWNERS_CAN_CONTACT"
        whoCanJoin           : "INVITED_CAN_JOIN"
        whoCanLeaveGroup     : "NONE_CAN_LEAVE"
        whoCanModerateMembers: "NONE"
        whoCanPostMessage    : "ANYONE_CAN_POST"
        whoCanViewGroup      : "ALL_MEMBERS_CAN_VIEW"
        whoCanViewMembership : "ALL_IN_DOMAIN_CAN_VIEW"
    --outputs:--
  ~ cloudflareAccessPolicies : {
      ~ security-room-maintainers: {
            accountId      : "c39d6379ccfdb20a3627eeddffa80ec9"
            appCount       : 1
            connectionRules: {
                rdp: {}
            }
            createdAt      : "2026-08-30T07:04:32Z"
            decision       : "allow"
            id             : "11a6814e-bb85-4ea2-aa12-6b081b09aa56"
            includes       : [
                [0]: {
                    githubOrganization: {
                        identityProviderId: "1e53bc11-a504-4572-92d1-4c8d56fc198b"
                        name              : "modelcontextprotocol"
                        team              : "core-maintainers"
                    }
                }
                [1]: {
                    githubOrganization: {
                        identityProviderId: "1e53bc11-a504-4572-92d1-4c8d56fc198b"
                        name              : "modelcontextprotocol"
                        team              : "lead-maintainers"
                    }
                }
                [2]: {
                    githubOrganization: {
                        identityProviderId: "1e53bc11-a504-4572-92d1-4c8d56fc198b"
                        name              : "modelcontextprotocol"
                        team              : "security-managers"
                    }
                }
                [3]: {
                    githubOrganization: {
                        identityProviderId: "1e53bc11-a504-4572-92d1-4c8d56fc198b"
                        name              : "modelcontextprotocol"
                        team              : "security-team"
                    }
                }
            ]
            name           : "Maintainers"
            reusable       : true
            updatedAt      : "2026-09-29T22:44:09Z"
            urn            : "urn:pulumi:prod::mcp-access::cloudflare:index/zeroTrustAccessPolicy:ZeroTrustAccessPolicy::access-policy-security-room-maintainers"
        }
    }
  ~ discordChannels          : {
      ~ thread-only-test: {
            channel  : {
                defaultAutoArchiveDuration: 10080
                defaultForumLayout        : "list"
                defaultSortOrder          : "latest_activity"
                name                      : "thread-only-test"
                parentId                  : "1358869848138059967"
                topic                     : "Test channel for Pulumi-managed thread-only (forum) channels. Every post opens a thread."
                type                      : "forum"
            }
            channelId: "1568254786619314198"
            guildId  : "1358869848138059966"
            id       : "1568254786619314198"
            state    : {
                availableTags                : []
                defaultAutoArchiveDuration   : 10080
                defaultForumLayout           : "list"
                defaultReactionEmoji         : <null>
                defaultSortOrder             : "latest_activity"
                defaultThreadRateLimitPerUser: 0
                flags                        : 0
                name                         : "thread-only-test"
                nsfw                         : false
                parentId                     : "1358869848138059967"
                position                     : 1
                rateLimitPerUser             : 0
                requireTag                   : false
                topic                        : "Test channel for Pulumi-managed thread-only (forum) channels. Every post opens a thread."
                type                         : "forum"
            }
            token    : [secret]
            urn      : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-channel-thread-only-test"
        }
    }
  ~ discordRoles             : {
      ~ administrators (synced)                                 : {
            guildId : "1358869848138059966"
            id      : "1460760673498959945"
            roleId  : "1460760673498959945"
            roleName: "administrators (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-administrators"
        }
      ~ agents interest group (synced)                          : {
            guildId : "1358869848138059966"
            id      : "1564385055172861966"
            roleId  : "1564385055172861966"
            roleName: "agents interest group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-agents-ig"
        }
      ~ auth fine-grained authorization working group (synced)  : {
            guildId : "1358869848138059966"
            id      : "1539652788047577128"
            roleId  : "1539652788047577128"
            roleName: "auth fine-grained authorization working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-auth-wg-fine-grained-authz"
        }
      ~ auth interest group (synced)                            : {
            guildId : "1358869848138059966"
            id      : "1460760779107209370"
            roleId  : "1460760779107209370"
            roleName: "auth interest group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-auth-ig"
        }
      ~ auth tool scopes working group (synced)                 : {
            guildId : "1358869848138059966"
            id      : "1539652784092090369"
            roleId  : "1539652784092090369"
            roleName: "auth tool scopes working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-auth-wg-tool-scopes"
        }
      ~ c# sdk maintainers (synced)                             : {
            guildId : "1358869848138059966"
            id      : "1460760658848252036"
            roleId  : "1460760658848252036"
            roleName: "c# sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-csharp-sdk"
        }
      ~ client implementor interest group (synced)              : {
            guildId : "1358869848138059966"
            id      : "1460760806584094783"
            roleId  : "1460760806584094783"
            roleName: "client implementor interest group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-client-implementor-ig"
        }
      ~ community managers (synced)                             : {
            guildId : "1358869848138059966"
            id      : "1461488567162503189"
            roleId  : "1461488567162503189"
            roleName: "community managers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-community-managers"
        }
      ~ community moderators (synced)                           : {
            guildId : "1358869848138059966"
            id      : "1460760656226680903"
            roleId  : "1460760656226680903"
            roleName: "community moderators (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-moderators"
        }
      ~ core maintainers (synced)                               : {
            guildId : "1358869848138059966"
            id      : "1460760682076307516"
            roleId  : "1460760682076307516"
            roleName: "core maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-core-maintainers"
        }
      ~ core primitive working group (synced)                   : {
            guildId : "1358869848138059966"
            id      : "1540128486218539018"
            roleId  : "1540128486218539018"
            roleName: "core primitive working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-core-primitive-wg"
        }
      ~ enterprise-managed authorization interest group (synced): {
            guildId : "1358869848138059966"
            id      : "1539652812743381082"
            roleId  : "1539652812743381082"
            roleName: "enterprise-managed authorization interest group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-enterprise-managed-authorization-ig"
        }
      ~ file uploads working group (synced)                     : {
            guildId : "1358869848138059966"
            id      : "1497125934359249018"
            roleId  : "1497125934359249018"
            roleName: "file uploads working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-file-uploads-wg"
        }
      ~ filesystems working group (synced)                      : {
            guildId : "1358869848138059966"
            id      : "1547367326343630958"
            roleId  : "1547367326343630958"
            roleName: "filesystems working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-filesystems-wg"
        }
      ~ financial services interest group (synced)              : {
            guildId : "1358869848138059966"
            id      : "1460760782605123726"
            roleId  : "1460760782605123726"
            roleName: "financial services interest group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-financial-services-ig"
        }
      ~ gateways interest group (synced)                        : {
            guildId : "1358869848138059966"
            id      : "1461148028646854859"
            roleId  : "1461148028646854859"
            roleName: "gateways interest group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-gateways-ig"
        }
      ~ go sdk maintainers (synced)                             : {
            guildId : "1358869848138059966"
            id      : "1460760804134752443"
            roleId  : "1460760804134752443"
            roleName: "go sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-go-sdk"
        }
      ~ governance working group (synced)                       : {
            guildId : "1358869848138059966"
            id      : "1561817661711720488"
            roleId  : "1561817661711720488"
            roleName: "governance working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-governance-wg"
        }
      ~ infrastructure working group (synced)                   : {
            guildId : "1358869848138059966"
            id      : "1551964002974830692"
            roleId  : "1551964002974830692"
            roleName: "infrastructure working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-infrastructure-wg"
        }
      ~ inspector maintainers (synced)                          : {
            guildId : "1358869848138059966"
            id      : "1460760676367597740"
            roleId  : "1460760676367597740"
            roleName: "inspector maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-inspector-maintainers"
        }
      ~ interceptors working group (synced)                     : {
            guildId : "1358869848138059966"
            id      : "1496277766046355539"
            roleId  : "1496277766046355539"
            roleName: "interceptors working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-interceptors-wg"
        }
      ~ java sdk maintainers (synced)                           : {
            guildId : "1358869848138059966"
            id      : "1460760695821041767"
            roleId  : "1460760695821041767"
            roleName: "java sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-java-sdk"
        }
      ~ kotlin sdk maintainers (synced)                         : {
            guildId : "1358869848138059966"
            id      : "1460760698316652557"
            roleId  : "1460760698316652557"
            roleName: "kotlin sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-kotlin-sdk"
        }
      ~ lead maintainers (synced)                               : {
            guildId : "1358869848138059966"
            id      : "1460760679064535238"
            roleId  : "1460760679064535238"
            roleName: "lead maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-lead-maintainers"
        }
      ~ maintainers (synced)                                    : {
            guildId : "1358869848138059966"
            id      : "1460760670395039755"
            roleId  : "1460760670395039755"
            roleName: "maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-maintainers"
        }
      ~ mcp apps working group (synced)                         : {
            guildId : "1358869848138059966"
            id      : "1461149104573907161"
            roleId  : "1461149104573907161"
            roleName: "mcp apps working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-mcp-apps-wg"
        }
      ~ members (synced)                                        : {
            guildId : "1358869848138059966"
            id      : "1568252368624033867"
            roleId  : "1568252368624033867"
            roleName: "members (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-members"
        }
      ~ php sdk maintainers (synced)                            : {
            guildId : "1358869848138059966"
            id      : "1460760711796887821"
            roleId  : "1460760711796887821"
            roleName: "php sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-php-sdk"
        }
      ~ primitive grouping interest group (synced)              : {
            guildId : "1358869848138059966"
            id      : "1471155142358339685"
            roleId  : "1471155142358339685"
            roleName: "primitive grouping interest group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-primitive-grouping-ig"
        }
      ~ python sdk maintainers (synced)                         : {
            guildId : "1358869848138059966"
            id      : "1460760717434163281"
            roleId  : "1460760717434163281"
            roleName: "python sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-python-sdk"
        }
      ~ reference servers maintainers (synced)                  : {
            guildId : "1358869848138059966"
            id      : "1460760661985464506"
            roleId  : "1460760661985464506"
            roleName: "reference servers maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-reference-servers-maintainers"
        }
      ~ registry maintainers (synced)                           : {
            guildId : "1358869848138059966"
            id      : "1460760690611720404"
            roleId  : "1460760690611720404"
            roleName: "registry maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-registry-maintainers"
        }
      ~ ruby sdk maintainers (synced)                           : {
            guildId : "1358869848138059966"
            id      : "1460760769758101595"
            roleId  : "1460760769758101595"
            roleName: "ruby sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-ruby-sdk"
        }
      ~ rust sdk maintainers (synced)                           : {
            guildId : "1358869848138059966"
            id      : "1460760775705493770"
            roleId  : "1460760775705493770"
            roleName: "rust sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-rust-sdk"
        }
      ~ sdk maintainers (synced)                                : {
            guildId : "1358869848138059966"
            id      : "1460760687444758623"
            roleId  : "1460760687444758623"
            roleName: "sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-sdk-maintainers"
        }
      ~ sdk working group (synced)                              : {
            guildId : "1358869848138059966"
            id      : "1569321944212308039"
            roleId  : "1569321944212308039"
            roleName: "sdk working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-sdk-wg"
        }
      ~ server card working group (synced)                      : {
            guildId : "1358869848138059966"
            id      : "1493598979357671554"
            roleId  : "1493598979357671554"
            roleName: "server card working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-server-card-wg"
        }
      ~ skills over mcp working group (synced)                  : {
            guildId : "1358869848138059966"
            id      : "1539652804610621562"
            roleId  : "1539652804610621562"
            roleName: "skills over mcp working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-skills-over-mcp-wg"
        }
      ~ swift sdk maintainers (synced)                          : {
            guildId : "1358869848138059966"
            id      : "1460760701344813136"
            roleId  : "1460760701344813136"
            roleName: "swift sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-swift-sdk"
        }
      ~ tasks & events working group (synced)                   : {
            guildId : "1358869848138059966"
            id      : "1564385052232654888"
            roleId  : "1564385052232654888"
            roleName: "tasks & events working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-tasks-events-wg"
        }
      ~ tool annotations interest group (synced)                : {
            guildId : "1358869848138059966"
            id      : "1483420327001329685"
            roleId  : "1483420327001329685"
            roleName: "tool annotations interest group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-tool-annotations-ig"
        }
      ~ transports working group (synced)                       : {
            guildId : "1358869848138059966"
            id      : "1460760773134651505"
            roleId  : "1460760773134651505"
            roleName: "transports working group (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-transport-wg"
        }
      ~ typescript sdk maintainers (synced)                     : {
            guildId : "1358869848138059966"
            id      : "1460760714607071465"
            roleId  : "1460760714607071465"
            roleName: "typescript sdk maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-typescript-sdk"
        }
      ~ use-mcp maintainers (synced)                            : {
            guildId : "1358869848138059966"
            id      : "1460760684643221556"
            roleId  : "1460760684643221556"
            roleName: "use-mcp maintainers (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-use-mcp-maintainers"
        }
      ~ wg/ig facilitators (synced)                             : {
            guildId : "1358869848138059966"
            id      : "1466097565048246467"
            roleId  : "1466097565048246467"
            roleName: "wg/ig facilitators (synced)"
            token   : [secret]
            urn     : "urn:pulumi:prod::mcp-access::pulumi-nodejs:dynamic:Resource::discord-role-wg-ig-facilitators"
        }
    }
  ~ githubRepositories       : {
      ~ core-primitives-wg           : {
            allowAutoMerge                     : false
            allowForking                       : true
            allowMergeCommit                   : false
            allowRebaseMerge                   : false
            allowSquashMerge                   : true
            allowUpdateBranch                  : true
            archiveOnDestroy                   : true
            archived                           : false
            autoInit                           : true
            defaultBranch                      : "main"
            deleteBranchOnMerge                : true
            description                        : "Coordination and proposals for the MCP Core Primitives Working Group."
            etag                               : "W/\"c858987f9e3cff829afa3e1a596bfcdc5d2c5a1b110ddc38e8c18c41107135e9\""
            fork                               : "false"
            fullName                           : "modelcontextprotocol/core-primitives-wg"
            gitCloneUrl                        : "git://github.com/modelcontextprotocol/core-primitives-wg.git"
            hasDiscussions                     : false
            hasDownloads                       : false
            hasIssues                          : true
            hasProjects                        : false
            hasWiki                            : false
            htmlUrl                            : "https://github.com/modelcontextprotocol/core-primitives-wg"
            httpCloneUrl                       : "https://github.com/modelcontextprotocol/core-primitives-wg.git"
            id                                 : "core-primitives-wg"
            ignoreVulnerabilityAlertsDuringRead: false
            isTemplate                         : false
            licenseTemplate                    : "apache-2.0"
            mergeCommitMessage                 : "PR_TITLE"
            mergeCommitTitle                   : "MERGE_MESSAGE"
            name                               : "core-primitives-wg"
            nodeId                             : "R_kgDOVlMGhA"
            private                            : false
            repoId                             : 1448281732
            securityAndAnalysis                : {
                advancedSecurity                 : <null>
                codeSecurity                     : <null>
                secretScanning                   : {
                    status: "enabled"
                }
                secretScanningAiDetection        : <null>
                secretScanningNonProviderPatterns: <null>
                secretScanningPushProtection     : {
                    status: "enabled"
                }
            }
            squashMergeCommitMessage           : "PR_BODY"
            squashMergeCommitTitle             : "PR_TITLE"
            sshCloneUrl                        : "[email protected]:modelcontextprotocol/core-primitives-wg.git"
            svnUrl                             : "https://github.com/modelcontextprotocol/core-primitives-wg"
            urn                                : "urn:pulumi:prod::mcp-access::github:index/repository:Repository::repository-core-primitives-wg"
            visibility                         : "public"
            vulnerabilityAlerts                : true
            webCommitSignoffRequired           : false
        }
      ~ experimental-ext-file-uploads: {
            allowAutoMerge                     : false
            allowForking                       : true
            allowMergeCommit                   : false
            allowRebaseMerge                   : false
            allowSquashMerge                   : true
            allowUpdateBranch                  : true
            archiveOnDestroy                   : true
            archived                           : false
            autoInit                           : true
            defaultBranch                      : "main"
            deleteBranchOnMerge                : true
            description                        : "Experimental MCP extension for uploading files to tools over MCP. Maintained by the File Uploads Working Group."
            etag                               : "W/\"558e4fe3b4e02108c8a0ec9817496f1144fc6e310900229ac81529163e80a469\""
            fork                               : "false"
            fullName                           : "modelcontextprotocol/experimental-ext-file-uploads"
            gitCloneUrl                        : "git://github.com/modelcontextprotocol/experimental-ext-file-uploads.git"
            hasDiscussions                     : false
            hasDownloads                       : false
            hasIssues                          : true
            hasProjects                        : false
            hasWiki                            : false
            htmlUrl                            : "https://github.com/modelcontextprotocol/experimental-ext-file-uploads"
            httpCloneUrl                       : "https://github.com/modelcontextprotocol/experimental-ext-file-uploads.git"
            id                                 : "experimental-ext-file-uploads"
            ignoreVulnerabilityAlertsDuringRead: false
            isTemplate                         : false
            licenseTemplate                    : "apache-2.0"
            mergeCommitMessage                 : "PR_TITLE"
            mergeCommitTitle                   : "MERGE_MESSAGE"
            name                               : "experimental-ext-file-uploads"
            nodeId                             : "R_kgDOVIvM3A"
            private                            : false
            repoId                             : 1418448092
            securityAndAnalysis                : {
                advancedSecurity                 : <null>
                codeSecurity                     : <null>
                secretScanning                   : {
                    status: "enabled"
                }
                secretScanningAiDetection        : <null>
                secretScanningNonProviderPatterns: <null>
                secretScanningPushProtection     : {
                    status: "enabled"
                }
            }
            squashMergeCommitMessage           : "PR_BODY"
            squashMergeCommitTitle             : "PR_TITLE"
            sshCloneUrl                        : "[email protected]:modelcontextprotocol/experimental-ext-file-uploads.git"
            svnUrl                             : "https://github.com/modelcontextprotocol/experimental-ext-file-uploads"
            urn                                : "urn:pulumi:prod::mcp-access::github:index/repository:Repository::repository-experimental-ext-file-uploads"
            visibility                         : "public"
            vulnerabilityAlerts                : true
            webCommitSignoffRequired           : false
        }
      ~ experimental-ext-filesystems : {
            allowAutoMerge                     : false
            allowForking                       : true
            allowMergeCommit                   : false
            allowRebaseMerge                   : false
            allowSquashMerge                   : true
            allowUpdateBranch                  : true
            archiveOnDestroy                   : true
            archived                           : false
            autoInit                           : true
            defaultBranch                      : "main"
            deleteBranchOnMerge                : true
            description                        : "Experimental MCP extension for filesystem operations on Resources. Maintained by the Filesystems Working Group."
            etag                               : "W/\"eb23d7e32f02ab7f316f22bb86aa1a7f7600902eb9c69fc0a4348961eb928aee\""
            fork                               : "false"
            fullName                           : "modelcontextprotocol/experimental-ext-filesystems"
            gitCloneUrl                        : "git://github.com/modelcontextprotocol/experimental-ext-filesystems.git"
            hasDiscussions                     : false
            hasDownloads                       : false
            hasIssues                          : true
            hasProjects                        : false
            hasWiki                            : false
            htmlUrl                            : "https://github.com/modelcontextprotocol/experimental-ext-filesystems"
            httpCloneUrl                       : "https://github.com/modelcontextprotocol/experimental-ext-filesystems.git"
            id                                 : "experimental-ext-filesystems"
            ignoreVulnerabilityAlertsDuringRead: false
            isTemplate                         : false
            licenseTemplate                    : "apache-2.0"
            mergeCommitMessage                 : "PR_TITLE"
            mergeCommitTitle                   : "MERGE_MESSAGE"
            name                               : "experimental-ext-filesystems"
            nodeId                             : "R_kgDOU_4rAQ"
            primaryLanguage                    : "Python"
            private                            : false
            repoId                             : 1409166081
            securityAndAnalysis                : {
                advancedSecurity                 : <null>
                codeSecurity                     : <null>
                secretScanning                   : {
                    status: "enabled"
                }
                secretScanningAiDetection        : <null>
                secretScanningNonProviderPatterns: <null>
                secretScanningPushProtection     : {
                    status: "enabled"
                }
            }
            squashMergeCommitMessage           : "PR_BODY"
            squashMergeCommitTitle             : "PR_TITLE"
            sshCloneUrl                        : "[email protected]:modelcontextprotocol/experimental-ext-filesystems.git"
            svnUrl                             : "https://github.com/modelcontextprotocol/experimental-ext-filesystems"
            urn                                : "urn:pulumi:prod::mcp-access::github:index/repository:Repository::repository-experimental-ext-filesystems"
            visibility                         : "public"
            vulnerabilityAlerts                : true
            webCommitSignoffRequired           : false
        }
      ~ governance-wg                : {
            allowAutoMerge                     : false
            allowForking                       : true
            allowMergeCommit                   : false
            allowRebaseMerge                   : false
            allowSquashMerge                   : true
            allowUpdateBranch                  : true
            archiveOnDestroy                   : true
            archived                           : false
            autoInit                           : true
            defaultBranch                      : "main"
            deleteBranchOnMerge                : true
            description                        : "Governance documentation and working materials for the MCP Governance Working Group."
            etag                               : "W/\"7726087ece6255cc3ad62ed9c2abb12c9be9ea1a0cc1f43514107240f5f96fee\""
            fork                               : "false"
            fullName                           : "modelcontextprotocol/governance-wg"
            gitCloneUrl                        : "git://github.com/modelcontextprotocol/governance-wg.git"
            hasDiscussions                     : false
            hasDownloads                       : false
            hasIssues                          : true
            hasProjects                        : false
            hasWiki                            : false
            htmlUrl                            : "https://github.com/modelcontextprotocol/governance-wg"
            httpCloneUrl                       : "https://github.com/modelcontextprotocol/governance-wg.git"
            id                                 : "governance-wg"
            ignoreVulnerabilityAlertsDuringRead: false
            isTemplate                         : false
            licenseTemplate                    : "apache-2.0"
            mergeCommitMessage                 : "PR_TITLE"
            mergeCommitTitle                   : "MERGE_MESSAGE"
            name                               : "governance-wg"
            nodeId                             : "R_kgDOVQAxJQ"
            private                            : false
            repoId                             : 1426075941
            securityAndAnalysis                : {
                advancedSecurity                 : <null>
                codeSecurity                     : <null>
                secretScanning                   : {
                    status: "enabled"
                }
                secretScanningAiDetection        : <null>
                secretScanningNonProviderPatterns: <null>
                secretScanningPushProtection     : {
                    status: "enabled"
                }
            }
            squashMergeCommitMessage           : "PR_BODY"
            squashMergeCommitTitle             : "PR_TITLE"
            sshCloneUrl                        : "[email protected]:modelcontextprotocol/governance-wg.git"
            svnUrl                             : "https://github.com/modelcontextprotocol/governance-wg"
            urn                                : "urn:pulumi:prod::mcp-access::github:index/repository:Repository::repository-governance-wg"
            visibility                         : "public"
            vulnerabilityAlerts                : true
            webCommitSignoffRequired           : false
        }
      ~ php-sdk-client               : {
            allowAutoMerge                     : false
            allowForking                       : true
            allowMergeCommit                   : false
            allowRebaseMerge                   : false
            allowSquashMerge                   : true
            allowUpdateBranch                  : true
            archiveOnDestroy                   : true
            archived                           : false
            autoInit                           : true
            defaultBranch                      : "main"
            deleteBranchOnMerge                : true
            description                        : "Client component of the official PHP SDK for Model Context Protocol. Maintained in collaboration with The PHP Foundation."
            etag                               : "W/\"9959725135cb154dc7451baf330ed757030cc18382bab90dc2ed8aa60a4ed64e\""
            fork                               : "false"
            fullName                           : "modelcontextprotocol/php-sdk-client"
            gitCloneUrl                        : "git://github.com/modelcontextprotocol/php-sdk-client.git"
            hasDiscussions                     : false
            hasDownloads                       : false
            hasIssues                          : true
            hasProjects                        : false
            hasWiki                            : false
            htmlUrl                            : "https://github.com/modelcontextprotocol/php-sdk-client"
            httpCloneUrl                       : "https://github.com/modelcontextprotocol/php-sdk-client.git"
            id                                 : "php-sdk-client"
            ignoreVulnerabilityAlertsDuringRead: false
            isTemplate                         : false
            licenseTemplate                    : "apache-2.0"
            mergeCommitMessage                 : "PR_TITLE"
            mergeCommitTitle                   : "MERGE_MESSAGE"
            name                               : "php-sdk-client"
            nodeId                             : "R_kgDOVlNBng"
            private                            : false
            repoId                             : 1448296862
            securityAndAnalysis                : {
                advancedSecurity                 : <null>
                codeSecurity                     : <null>
                secretScanning                   : {
                    status: "enabled"
                }
                secretScanningAiDetection        : <null>
                secretScanningNonProviderPatterns: <null>
                secretScanningPushProtection     : {
                    status: "enabled"
                }
            }
            squashMergeCommitMessage           : "PR_BODY"
            squashMergeCommitTitle             : "PR_TITLE"
            sshCloneUrl                        : "[email protected]:modelcontextprotocol/php-sdk-client.git"
            svnUrl                             : "https://github.com/modelcontextprotocol/php-sdk-client"
            urn                                : "urn:pulumi:prod::mcp-access::github:index/repository:Repository::repository-php-sdk-client"
            visibility                         : "public"
            vulnerabilityAlerts                : true
            webCommitSignoffRequired           : false
        }
      ~ php-sdk-schema               : {
            allowAutoMerge                     : false
            allowForking                       : true
            allowMergeCommit                   : false
            allowRebaseMerge                   : false
            allowSquashMerge                   : true
            allowUpdateBranch                  : true
            archiveOnDestroy                   : true
            archived                           : false
            autoInit                           : true
            defaultBranch                      : "main"
            deleteBranchOnMerge                : true
            description                        : "Schema component of the official PHP SDK for Model Context Protocol. Maintained in collaboration with The PHP Foundation."
            etag                               : "W/\"433811265626a6c4e53ba1c8008d0271e029e374aafa4b6410228e14b07b8f72\""
            fork                               : "false"
            fullName                           : "modelcontextprotocol/php-sdk-schema"
            gitCloneUrl                        : "git://github.com/modelcontextprotocol/php-sdk-schema.git"
            hasDiscussions                     : false
            hasDownloads                       : false
            hasIssues                          : true
            hasProjects                        : false
            hasWiki                            : false
            htmlUrl                            : "https://github.com/modelcontextprotocol/php-sdk-schema"
            httpCloneUrl                       : "https://github.com/modelcontextprotocol/php-sdk-schema.git"
            id                                 : "php-sdk-schema"
            ignoreVulnerabilityAlertsDuringRead: false
            isTemplate                         : false
            licenseTemplate                    : "apache-2.0"
            mergeCommitMessage                 : "PR_TITLE"
            mergeCommitTitle                   : "MERGE_MESSAGE"
            name                               : "php-sdk-schema"
            nodeId                             : "R_kgDOVlNCBg"
            private                            : false
            repoId                             : 1448296966
            securityAndAnalysis                : {
                advancedSecurity                 : <null>
                codeSecurity                     : <null>
                secretScanning                   : {
                    status: "enabled"
                }
                secretScanningAiDetection        : <null>
                secretScanningNonProviderPatterns: <null>
                secretScanningPushProtection     : {
                    status: "enabled"
                }
            }
            squashMergeCommitMessage           : "PR_BODY"
            squashMergeCommitTitle             : "PR_TITLE"
            sshCloneUrl                        : "[email protected]:modelcontextprotocol/php-sdk-schema.git"
            svnUrl                             : "https://github.com/modelcontextprotocol/php-sdk-schema"
            urn                                : "urn:pulumi:prod::mcp-access::github:index/repository:Repository::repository-php-sdk-schema"
            visibility                         : "public"
            vulnerabilityAlerts                : true
            webCommitSignoffRequired           : false
        }
      ~ php-sdk-server               : {
            allowAutoMerge                     : false
            allowForking                       : true
            allowMergeCommit                   : false
            allowRebaseMerge                   : false
            allowSquashMerge                   : true
            allowUpdateBranch                  : true
            archiveOnDestroy                   : true
            archived                           : false
            autoInit                           : true
            defaultBranch                      : "main"
            deleteBranchOnMerge                : true
            description                        : "Server component of the official PHP SDK for Model Context Protocol. Maintained in collaboration with The PHP Foundation."
            etag                               : "W/\"496a92a3c3f9e8d47951e64ade4c185872f856f4e805453030bcc7974774edf6\""
            fork                               : "false"
            fullName                           : "modelcontextprotocol/php-sdk-server"
            gitCloneUrl                        : "git://github.com/modelcontextprotocol/php-sdk-server.git"
            hasDiscussions                     : false
            hasDownloads                       : false
            hasIssues                          : true
            hasProjects                        : false
            hasWiki                            : false
            htmlUrl                            : "https://github.com/modelcontextprotocol/php-sdk-server"
            httpCloneUrl                       : "https://github.com/modelcontextprotocol/php-sdk-server.git"
            id                                 : "php-sdk-server"
            ignoreVulnerabilityAlertsDuringRead: false
            isTemplate                         : false
            licenseTemplate                    : "apache-2.0"
            mergeCommitMessage                 : "PR_TITLE"
            mergeCommitTitle                   : "MERGE_MESSAGE"
            name                               : "php-sdk-server"
            nodeId                             : "R_kgDOVlNChg"
            private                            : false
            repoId                             : 1448297094
            securityAndAnalysis                : {
                advancedSecurity                 : <null>
                codeSecurity                     : <null>
                secretScanning                   : {
                    status: "enabled"
                }
                secretScanningAiDetection        : <null>
                secretScanningNonProviderPatterns: <null>
                secretScanningPushProtection     : {
                    status: "enabled"
                }
            }
            squashMergeCommitMessage           : "PR_BODY"
            squashMergeCommitTitle             : "PR_TITLE"
            sshCloneUrl                        : "[email protected]:modelcontextprotocol/php-sdk-server.git"
            svnUrl                             : "https://github.com/modelcontextprotocol/php-sdk-server"
            urn                                : "urn:pulumi:prod::mcp-access::github:index/repository:Repository::repository-php-sdk-server"
            visibility                         : "public"
            vulnerabilityAlerts                : true
            webCommitSignoffRequired           : false
        }
    }
  ~ githubTeams              : {
      ~ agents-ig                    : {
            createDefaultMaintainer: false
            description            : "Agents Interest Group \n(Managed by github.com/modelcontextprotocol/access)"
            etag                   : "W/\"e2bb12eb4590b68012d45a260f2c0fcab8dc6b8d4988b22578a90be044763b1a\""
            id                     : "20346310"
            membersCount           : 4
            name                   : "agents-ig"
            nodeId                 : "T_kwDOCt2Azc4BNnXG"
            notificationSetting    : "notifications_enabled"
            parentTeamId           : "14435869"
            parentTeamReadId       : "14435869"
            parentTeamReadSlug     : "interest-groups"
            privacy                : "closed"
            slug                   : "agents-ig"
            urn                    : "urn:pulumi:prod::mcp-access::github:index/team:Team::agents-ig"
        }
      ~ auth-maintainers             : {
            createDefaultMaintainer: false
            description            : "Auth Maintainers \n(Managed by github.com/modelcontextprotocol/access)"
            etag                   : "W/\"d553add84bf4d4ac5450bb8df45aa45435b5997ed4fc280469cd964a02382759\""
            id                     : "16083315"
            membersCount           : 7
            name                   : "auth-maintainers"
            nodeId                 : "T_kwDOCt2Azc4A9Wlz"
            notificationSetting    : "notifications_enabled"
            parentTeamId           : "14435868"
            parentTeamReadId       : "14435868"
            parentTeamReadSlug     : "working-groups"
            privacy                : "closed"
            slug                   : "auth-maintainers"
            urn                    : "urn:pulumi:prod::mcp-access::github:index/team:Team::auth-maintainers"
        }
      ~ auth-wg-fine-grained-authz   : {
            createDefaultMaintainer: false
            description            : "Auth Fine-Grained Authorization Working Group \n(Managed by github.com/modelcontextprotocol/access)"
            etag                   : "W/\"9fc9ee29600d06efff29038d402f41b0b9ca132be11132cfe9ff1461438521d4\""
            id                     : "19051555"
            membersCount           : 0
            name                   : "auth-wg-fine-grained-authz"
            nodeId                 : "T_kwDOCt2Azc4BIrQj"
            notificationSetting    : "notifications_enabled"
            parentTeamId           : "14435868"
            parentTeamReadId       : "14435868"
            parentTeamReadSlug     : "working-groups"
            privacy                : "closed"
            slug                   : "auth-wg-fine-grained-authz"
            urn                    : "urn:pulumi:prod::mcp-access::github:index/team:Team::auth-wg-fine-grained-authz"
        }
      ~ auth-wg-tool-scopes          : {
            createDefaultMaintainer: false
            description            : "Auth Tool Scopes Working Group \n(Managed by github.com/modelcontextprotocol/access)"
            etag                   : "W/\"228b093f218df764ed8a9998a4d1c562cbaa51a9908fd38d497155953e35345b\""
            id                     : "19051557"
            membersCount           : 0
            name                   : "auth-wg-tool-scopes"
            nodeId                 : "T_kwDOCt2Azc4BIrQl"
            notificationSetting    : "notifications_enabled"
            parentTeamId           : "14435868"
            parentTeamReadId       : "14435868"
            parentTeamReadSlug     : "working-groups"
            privacy                : "closed"
            slug                   : "auth-wg-tool-scopes"
            urn                    : "urn:pulumi:prod::mcp-access::github:index/team:Team::auth-wg-tool-scopes"
        }
      ~ conformance-auth             : {
            createDefaultMaintainer: false
            description            : "Conformance authorization-server code owners \n(Managed by github.com/modelcontextprotocol/access)"
            etag                   : "W/\"daf624236d91c99fb6a16c6a50c1274ce1ff9bffc0d9131d16028d953c56168a\""
            id                     : "19337897"
            membersCount           : 2
            name                   : "conformance-auth"
            nodeId                 : "T_kwDOCt2Azc4BJxKp"
            notificationSetting    : "notifications_enabled"
            privacy                : "closed"
            slug                   : "conformance-auth"
            urn                    : "urn:pulumi:prod::mcp-access::github:index/team:Team::conformance-auth"
        }
      ~ core-maintainers             : {
            createDefaultMaintainer: false
            description            : "Core maintainers \n(Managed by github.com/modelcontextprotocol/access)"
            etag                   : "W/\"511ce5f21dfa4555681921c271a9a21e5eac55a98a8696f853bf7970aeda3986\""
            id                     : "14435865"
            membersCount           : 12
            name                   : "core-maintainers"
            nodeId                 : "T_kwDOCt2Azc4A3EYZ"
            notificationSetting    : "notifications_enabled"
            parentTeamId           : "14435861"
            parentTeamReadId       : "14435861"
            parentTeamReadSlug     : "steering-committee"
            privacy                : "closed"
            slug                   : "core-maintainers"
            urn                    : "urn:pulumi:prod::mcp-access::github:index/team:Team::core-maintainers"
        }
      ~ core-primitive-wg            : {
            createDefaultMaintainer: false
            description            : "Core Primitive Working Group \n(Managed by github.com/modelcontextprotocol/access)"
            etag                   : "W/\"a72526b21a668289c7473cd509e51f28f343bd948addbaa6c6d581e11c45ef5b\""
            id                     : "19087061"
            membersCount           : 2
            name          

... (truncated)

Account creations, links, suspensions and the nightly report are now
JSON log lines (Workers Logs enabled in wrangler.toml) instead of a D1
audit_log table and an optional Discord webhook.

Also declare @types/node in access-site so the tests typecheck in a clean
CI checkout without the root node_modules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
…of truth

Per Infra WG discussion, account creation and mappings stay infrastructure
as code. The Worker now only signs people in: it maps the GitHub login to
the googleEmailPrefix in users.ts (same provisionUser rule as Pulumi) and
issues the OIDC ID token Google needs. It holds no Google credentials.

Removed: self-service account page, Directory API client, D1 account
cache, nightly offboarding, backfill script, team-group sync, and the
src/google.ts / src/config/utils.ts changes. D1 keeps only short-lived
pending requests and single-use codes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
On merge to main, scripts/deploy.sh finds or creates the D1 database,
applies migrations and deploys the Worker, uploading secrets from the
production environment. Non-secret settings (account, GitHub App client
ID, Google redirect URI) are committed in wrangler.toml. The deploy job
skips with a notice until the secrets exist, so merging early is safe.

Replaces the manual generate-keys script; signing key rotation now goes
through the ACCESS_SITE_OIDC_PREVIOUS_JWK secret, and the Worker ignores a
retired "{}" value.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
Node: CI on Node 24 (current LTS), engines >=24, @types/node 24.

Security review fixes:
- Pin GitHub logins to GitHub user IDs on first sign-in; refuse a login
  that comes back from a different account (freed and re-registered name)
- Per-IP rate limit on /authorize and /login (Workers rate limiting
  binding) so anonymous traffic can't exhaust D1 writes; clean up expired
  rows on 5% of sign-ins instead of every one
- Reject a code_verifier when no PKCE challenge was sent (RFC 9700)
- Form-decode Basic client credentials (RFC 6749 2.3.1)
- Stamp auth_time at GitHub sign-in, not at token exchange
- Revoke the GitHub user token right after reading the user; clear the
  OAuth state cookie
- CI: pin actions to commit SHAs, don't persist Git credentials, expose
  deploy secrets only to the deploy step (never to npm ci)
- Override dev-only undici to the patched 7.30.1 (npm audit: 0)
- Test that users.ts has no case-insensitive duplicate logins or prefixes

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
Google's custom OIDC profile needs a change password URL; point it at
GitHub's password and authentication settings. Replace the 'verify on
first deploy' notes with what Google documents: the redirect URI is
per-profile (set by PR after setup), and PKCE is handled either way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
PILOT_GITHUB_LOGINS in wrangler.toml limits sign-in to the listed GitHub
logins; it starts as just "sambhav" ([email protected] via
users.ts). A test checks every pilot login maps to an account. Remove the
line by PR to open sign-in to everyone in users.ts.

Setup is reordered so each secret is generated by the person who uses it:
the OIDC client secret is created by whoever sets up the Google SSO
profile, and goes straight into GitHub secrets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn

sambhav commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@dsp-ant this is ready for review. Three things need you, all in the Setup → @dsp-ant section of the description:

  1. Create the GitHub App (needs an org owner) and post its client ID here
  2. Generate the OIDC client secret into GitHub secrets (one command; you keep the file for step 3)
  3. Once the discovery URL is live, add the custom OIDC SSO profile in the Admin console, assign it to a pilot sub-OU with only sambhav@, and post the redirect URI here

Sign-in starts as a pilot for me only (PILOT_GITHUB_LOGINS = "sambhav"), so nothing changes for anyone else until we widen it. Super admins are exempt from the SSO profile, so your login is unaffected.

The PR can merge before any of this; the deploy job skips until the secrets exist.


Generated by Claude Code

dsp-ant commented Oct 26, 2026

Copy link
Copy Markdown
Member

Automated review (Claude-generated)

Heads-up: everything below was produced by Claude Code's /code-review running over this PR's diff (two passes, merged and de-duplicated here). The severity labels are also Claude's judgement. The top items were spot-checked against the code, but none of this has been verified by a human yet — please treat each item as a claim to confirm, not an established fact.

Severity key: 🔴 High · 🟠 Medium · 🟡 Low · ⚪ Nit


🔴 High

1. existingGWSUser accounts are locked out — access-site/src/eligibility.ts:30
workspaceEmailFor() requires hasProvisionUserRole(...), but scripts/validate-config.ts and src/google.ts both treat existingGWSUser: true entries as real, kept accounts that are exempt from the role rule. There are ~14 such entries in users.ts today (several with memberOf: []). Once the OIDC profile is assigned to the whole OU, those people hit no_account → 403 and are told to "add a googleEmailPrefix" they already have, with no password fallback for non-super-admins. The README's "same rule Pulumi uses" claim is not currently true.
Suggested fix: export one shared "has a Workspace account" predicate from src/config/utils.ts and use it in google.ts, validate-config.ts, and the Worker (this also closes the firstName/lastName divergence noted under #9).

2. Identity is trust-on-first-use of a mutable GitHub login — access-site/src/db.ts:110
pinGithubIdentity binds login→GitHub ID on first sign-in. On day one the pin table is empty, so any users.ts login whose owner has since renamed their GitHub account can be registered by a stranger, who then completes /login and is issued an ID token for that person's @modelcontextprotocol.io mailbox; the real owner is afterwards refused with identity_mismatch. Impact is full mailbox/Drive access, so rating this High even though it needs a stale login to exist.
Suggested fix: anchor on something GitHub tracks by immutable ID — record the numeric GitHub user ID in users.ts (validated in CI), or require org/team membership at sign-in — rather than TOFU on the login string.

🟠 Medium

3. GitHub error callback causes a redirect loop — access-site/src/index.ts:86
/login?error=access_denied&state=… (user clicks Cancel, app suspended, temporarily_unavailable) has no code, so githubAuth treats it as a fresh start and 302s straight back to GitHub. The denial is never surfaced, Google never gets an error response, nothing is logged, and for non-interactive errors it's a tight loop until the browser gives up or the rate limiter 429s.
Suggested fix: check c.req.query('error') before invoking githubAuth and render an error page / redirect back to Google with error=access_denied.

4. OAuth state cookie is not __Host- and not bound to the pending authorization — access-site/src/index.ts:94
The state cookie from @hono/oauth-providers is a plain host cookie. Anyone who can run script on any *.modelcontextprotocol.io origin can toss a Domain=modelcontextprotocol.io; Path=/login state cookie and splice their own unredeemed GitHub code into a victim's pending Google sign-in (login-CSRF into the attacker's Workspace account). Preconditions are narrow (subdomain script execution), hence Medium rather than High.
Suggested fix: use a __Host- prefixed state cookie and/or bind state to the __Host-mcp_pending value.

5. Key-rotation procedure can break verification for up to the JWKS cache TTL — access-site/README.md:79, access-site/src/oidc.ts:91
/jwks is served with max-age=300, and the documented rotation starts signing with the new key in the same deploy that first publishes it (only a previous slot exists). A relying party holding a cached JWKS sees an unknown kid for up to 5 minutes. Google usually refetches on kid miss, but that isn't guaranteed.
Suggested fix: document "publish new public key via OIDC_PREVIOUS_JWK first → wait past TTL → swap SIGNING_KEY → move old key to previous", or add a NEXT_JWK slot.

6. Deploy gate doesn't require the GitHub client secret — .github/workflows/access-site.yml:68
ready=true needs the Cloudflare token, signing key and OIDC client secret, but not ACCESS_SITE_GITHUB_CLIENT_SECRET. A first deploy with steps 1/2/4 done but not 3 ships a live issuer where every sign-in fails at the GitHub token exchange ("Sign-in was interrupted"), and revokeGitHubToken sends Basic <id>:undefined.
Suggested fix: add HAS_GITHUB_SECRET to the gate (or have deploy.sh require it when no secret exists yet).

7. Test fixtures are derived from live src/config and gate the deploy job — access-site/test/helpers.ts:99
memberWithoutAccount() / ineligibleMember() throw if users.ts happens not to contain a member of that shape. Since the workflow runs these tests on every src/config/** change and deploy has needs: test, an unrelated, valid users.ts PR can break CI and stop the Worker redeploying (so removed users keep signing in) until someone edits the tests.
Suggested fix: use synthetic logins / injected MEMBERS for the negative cases.

🟡 Low

8. Cross-Origin-Opener-Policy: same-origin on the redirect responses — access-site/src/index.ts:25
secureHeaders() defaults add COOP to every response including the 302s on /authorize and /login. If Google ever runs this federated hop inside a popup (GIS popup mode, re-auth popups), the opener is severed and the result can't be posted back. Workspace SSO is normally full-page, so Low, but the fix is one line: crossOriginOpenerPolicy: false.

9. Eligibility predicate is re-implemented instead of shared — access-site/src/eligibility.ts:27
Beyond #1, the Worker's copy also drops the firstName/lastName requirement that src/google.ts applies, so it can assert email_verified: true for an address Pulumi never created. test/helpers.ts copies the predicate a third time. One exported helper in src/config/utils.ts fixes all of these.

10. no_account conflates several distinct situations and is checked before the pilot gate — access-site/src/oidc.ts:168
"Not in users.ts", "opted out via skipGoogleUserProvisioning", "alias entry without a prefix" and "no eligible role" all render noAccountPage telling the user to open a PR adding googleEmailPrefix, which is wrong advice for most of those cases and will prompt bogus config PRs during the pilot.

11. onError turns the deliberate 502 into a generic 500 — access-site/src/index.ts:143
Only HTTPExceptions with status < 500 keep their status/message, so HTTPException(502, 'GitHub did not return a user.') is logged as unexpected and returned as 500 "Something went wrong", making upstream GitHub failures indistinguishable from Worker bugs.

12. pinGithubIdentity does two sequential D1 round-trips — access-site/src/db.ts:116
INSERT OR IGNORE then SELECT on every successful sign-in. One statement (INSERT … ON CONFLICT(login) DO UPDATE SET login=login RETURNING github_id) or a db.batch([...]) halves hot-path latency, and avoids a read-after-write surprise if D1 read replication is ever enabled.

⚪ Nit

13. Hand-rolled base64url — access-site/src/crypto.ts:13: jose (already a dependency) exports base64url.encode(Uint8Array).

14. /authorize is GET-only — access-site/src/index.ts:56: OIDC Core §3.1.2.1 says the OP must accept GET and POST. Google uses GET, so this is spec-compliance only; app.on(['GET','POST'], '/authorize', …) if you want it.


Generated by Claude Code

Bring the PR branch up to date with main. Verify configured GitHub IDs before issuing Workspace credentials, bind the OAuth callback to the pending sign-in, and address deployment, key rotation and test coverage from review.
- Replace the OIDC_PREVIOUS_JWK and OIDC_NEXT_JWK slots with one
  OIDC_ROTATION_JWKS JWK Set, which removes a secret, the per-slot
  parsing and the jq conditionals in deploy.sh
- Check for duplicate GitHub logins in validate-config instead of a
  Worker test that read the live users.ts
- Use nowSeconds() for auth_time and fix stale vitest config comments

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014dNQoC6mUhYz5VKJL2QwiJ

sambhav commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

@dsp-ant thanks for the review. All 14 items are addressed in bf5bde6, and 72b7dcf simplifies the result:

# Item Resolution
1 existingGWSUser accounts locked out hasGoogleWorkspaceAccount() in src/config/utils.ts is the one predicate used by google.ts, validate-config.ts and the Worker, so existing accounts sign in without a provisioning role (tested)
2 Trust-on-first-use of a mutable login Removed the pin table. The numeric githubUserId is recorded in users.ts (validated in CI, unique, behind CODEOWNERS) and must match before any token is issued. Accounts without one get identity_unverified
3 GitHub error callback loop ?error= returns to Google with access_denied / temporarily_unavailable and its original state, and consumes the pending request
4 Unbound state cookie Dropped @hono/oauth-providers. OAuth state and S256 PKCE are both derived from the __Host-mcp_pending value, so there is no separate state cookie
5 Key rotation vs JWKS cache /jwks can publish an extra key before cutover and keep the old one after. 72b7dcf folds the two slots into one OIDC_ROTATION_JWKS JWK Set; runbook in access-site/README.md
6 Deploy gate Gate and deploy.sh now require ACCESS_SITE_GITHUB_CLIENT_SECRET
7 Tests depend on live users.ts Tests mock users.ts with synthetic members. The only live-config test checks that pilot logins have a verified ID. 72b7dcf moves login uniqueness into validate-config.ts
8 COOP on redirects crossOriginOpenerPolicy: false
9 Predicate re-implemented Same fix as 1, including firstName/lastName
10 no_account advice and order Pilot is checked first; refusal pages say "ask the Infrastructure WG to check your entry" rather than suggesting a googleEmailPrefix PR
11 502 turned into 500 HTTPExceptions keep their status; 502s are logged as upstream warnings
12 Two D1 round-trips Gone with the pin table
13 Hand-rolled base64url Uses jose's base64url
14 GET-only /authorize Accepts GET and form POST (303 after POST)

Worker: 23 tests and typecheck pass. Repo: format, config validation and 79 tests pass. wrangler deploy --dry-run builds.


Generated by Claude Code

Record the IDs for the current GitHub logins. Verify ownership with each member before expanding the Workspace sign-in pilot.

sambhav commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

Ready for review. The review fixes are in, and the access-site, repository CI, and Pulumi preview checks all passed. I also backfilled the numeric GitHub IDs for all 139 configured usernames in src/config/users.ts using their current GitHub profiles.

The Workspace SSO pilot is still limited to sambhav. Before widening it, we’ll confirm each account with its maintainer.

This branch was previously deployed

1 inactive deployment
production — 5cb26f17 Deployed Nov 9, 2026 by sambhav via Preview Changes #400
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants