MCP gateways
WiseWig deploys MCP as two independent Cloudflare Workers. This is an infrastructure security boundary, not catalog filtering inside one privileged process.
Anonymous public service
@wise-wig/public-mcp-worker serves Streamable HTTP at /mcp. Its Wrangler bindings are intentionally limited to:
PUBLIC_CONTENT, a KV namespace populated only after a successful publish;PUBLIC_MCP, the Agents SDK session Durable Object; andPUBLIC_RATE, an IP-hash rate-limit Durable Object.
It has no D1, tenant workspace, R2, identity secret, form-submission store, outbound plugin runner, or service binding. The core tools read the published structured document and search a bounded index with stable page/slot citations. Pages marked publicDiscovery: false never enter that index. The public schema resource is a publish-time projection of framework definitions, not a live privileged binding. Plugin tools, resource templates, and prompts are indexed only when their manifest declares visibility: public, no private permission, and exactly the public_content data scope. Invocation repeats that check against the published index, validates declared prompt arguments, retains the signed artifact digest as provenance, and bounds output.
OAuth private service
@wise-wig/private-mcp-worker is a separate OAuth 2.1 authorization server and protected MCP resource. It proxies identity to the installation’s configured OAuth/OIDC provider using S256 PKCE and one-time state, then requires an explicit site-scoped consent. Unknown scopes are dropped; site.write is removed when the current membership cannot edit content. Existing grants are displayed and may be revoked from the consent screen.
Tokens contain encrypted site grant properties, but these are never trusted as lasting authorization. Every tool invocation reloads current D1 roles for the exact tenant/site, resolves its persisted ready user Worker, and constructs an immutable dispatch grant containing one framework permission and one OAuth scope. The tenant Worker validates that grant against its deployment-pinned tenant identity before it touches script-local SQLite. It consumes the tenant/user quota and records the audit event locally. Mutations execute the guarded-command path inside the tenant Worker for schema validation, confirmation, idempotency, optimistic versions, authorization, and audit.
AI plan/apply is also resolved inside that tenant Worker. The OAuth gateway transports a bounded plan but cannot change its tenant, actor, permission, or source version. The Worker recomputes the plan digest and validates the current source value before an atomic draft/provenance save. Provenance is not part of the public index and its private resource requires the elevated audit.view permission.
Private draft search is executed after tenant dispatch against only the typed page resource and current draft. Navigation, settings, form definitions, and version metadata have fixed resource handlers. There is no generic table, SQL, R2, submission, engagement, or plugin-storage search primitive; plugins must expose separately declared resources with exact permissions and collection allowlists.
In self-hosted profiles, the private MCP Worker has D1 identity/control-plane access and a direct TENANT_WORKSPACE Durable Object namespace binding. It selects the Object name only from the OAuth-pinned tenant ID after authoritative role lookup; MCP arguments cannot select a Worker, tenant, site, permission, roles, or operation grant. Public MCP receives neither D1 nor the tenant workspace. The v2 managed-isolated adapter replaces that Object binding with its privileged dispatch namespace while preserving the same client contract.
Private plugin tools, resources, and prompts are derived only from enabled installed manifests. Discovery is filtered by current role but is not treated as authorization: use reloads membership and requires the exact declared permission and OAuth scope. Resources may read only collections explicitly named in that resource declaration; personal data therefore remains behind engagement.export even when a less-sensitive resource from the same plugin is visible. A mutation additionally requires a declared write capability, a private tool permission allowed by the current role, a declared storage collection, and explicit confirmation. This enables a plugin such as sermons to offer public research through the public index and authenticated record creation through private tenant storage without sharing bindings.
Deployment invariants
Deploy and review the two generated binding files independently. Never add a privileged binding to the public Worker for convenience. Publishing copies a sanitized document, searchable text, and eligible public plugin descriptors to PUBLIC_CONTENT; no draft, membership, submission, audit, or engagement value belongs in that record.
Both Workers use the Agents SDK McpAgent Streamable HTTP transport. The private Worker uses Cloudflare’s OAuthProvider; access tokens last one hour and refresh grants 30 days by default. Environment-specific operators must configure OAuth endpoints, D1/KV identifiers, remote tenant workspace binding, and secrets before deployment.