How MCP Tool Permission Deny/Allow Configuration Works in OpenWork

OpenWork implements a four-layer permission system for MCP (Microsoft Copilot Protocol) tools that validates access tokens, filters tool catalogs by user identity, enforces provider-level ACLs, and applies desktop policies to control which tools appear in the UI.

OpenWork's enterprise architecture provides granular control over MCP tool permissions through a sophisticated deny/allow configuration system. This multi-layered approach, implemented in the different-ai/openwork repository, allows administrators to restrict tool discovery and execution at the gateway, user, provider, and desktop levels. Understanding how these permission mechanisms interact is essential for securing MCP deployments in enterprise environments.

Token Scope Validation

The first line of defense occurs in handleMcpRequest within packages/enterprise-mcp-mock-server/src/protocol/mcp-handler.ts. Before any tool interaction, the system validates that the bearer token contains the required OAuth scopes defined in scenario.oauth.requiredResourceScopes.

If the token lacks necessary scopes, the gateway returns a 403 insufficient_scope error with a www-authenticate header:

if (
  faultApplies(context, "reject-scope") ||
  scenario.oauth.requiredResourceScopes.some((scope) => !token.scopes.includes(scope))
) {
  // 403 insufficient_scope → client must request additional scopes
  sendJson(response, 403, { error: "insufficient_scope" },
    { "www-authenticate": `Bearer error="insufficient_scope"` })
  return true
}

This check prevents unauthorized clients from accessing the MCP endpoint entirely. Administrators can simulate scope rejection using the reject-scope fault in the mock server's fault matrix for testing authentication flows.

Per-User Tool Catalog Denial

Even with valid tokens, OpenWork can hide the entire tool catalog from specific users. In handleToolsList (lines 495-500 of mcp-handler.ts), the system checks for the per-user-403 fault before returning any tool definitions:

if (faultApplies(context, "per-user-403")) {
  // 403 before any tools are disclosed
  sendJson(
    context.response,
    403,
    { error: "provider_per_user_forbidden",
      message: "Authorization with MCP server failed — check your credentials and permissions" },
    { "mcp-session-id": session.sessionId,
      "mcp-protocol-version": session.protocolVersion },
  )
  return
}

This mechanism returns a 403 provider_per_user_forbidden error, allowing organizations to block tool discovery for compliance or licensing reasons. While the mock server uses fault injection, production deployments reference organization permission records stored in the Den DB, as verified in ee/apps/den-api/test/org-role-permissions.test.ts.

Provider-Level ACL and Policy Denial

Once a tool call passes gateway validation, the underlying provider (Microsoft Graph, ServiceNow, etc.) may still reject the operation. The providerErrorResult function in mcp-handler.ts (lines 400-420) translates provider denials into structured MCP errors:

if (fault.effect === "provider-authorization-denial" &&
    context.state.shouldApplyFault(fault, context.scenario)) {
  emitFault(context, "Returned a provider ACL denial as an MCP tool error")
  return toolError(
    context.state.issueOpaque("provider-request"),
    "PROVIDER_ACL_DENIED",
    "The synthetic provider denied this operation through roles or ACLs",
    403, "provider_acl", false)
}

The system distinguishes between PROVIDER_ACL_DENIED (role-based access control) and PROVIDER_POLICY_DENIED (tenant policies, throttling, or availability constraints). These errors wrap the provider's response in a toolError object that the MCP client receives as a structured error code, allowing the client to handle provider-specific restrictions appropriately.

Desktop Policy Configuration

The final permission layer controls UI visibility through the allowCustomProviders flag defined in packages/types/src/den/desktop-policies.ts. This policy determines whether tools from custom (non-cloud) providers appear in the model picker:

// UI restriction helper
const restrictToCloud = options.checkDesktopAppRestriction({ restriction: "allowCustomProviders" });
if (!restrictToCloud) {
  // hide custom-provider tools from the model picker UI
}

Administrators toggle this setting via the patchDesktopPolicies API. The policy stores in the Den DB and applies at runtime through checkDesktopAppRestriction, effectively filtering the tool catalog at the presentation layer even when backend permissions would otherwise allow access.

End-to-End Permission Flow

Understanding how these layers interact requires following a complete MCP request lifecycle:

  1. Token Validation: handleMcpRequest validates audience (reject-audience) and scopes (reject-scope), returning 401/403 immediately if either check fails.
  2. Catalog Request: When calling tools/list, handleToolsList checks the per-user-403 fault, returning 403 before disclosing any tools if the user is blocked.
  3. Tool Execution: handleToolCall validates arguments and looks up the requested tool.
  4. Provider Enforcement: Before executing mutations, providerErrorResult checks for provider-level denials, returning PROVIDER_ACL_DENIED or PROVIDER_POLICY_DENIED if the provider rejects the operation.
  5. UI Filtering: Successful calls return results, but the desktop policy (allowCustomProviders) determines whether custom-provider tools render in the UI.

Key Implementation Files

Several source files define the permission architecture:

Summary

OpenWork's MCP tool permission system provides defense-in-depth through four distinct control layers:

  • Token Scope Validation rejects requests at the gateway when OAuth scopes are insufficient, preventing unauthorized endpoint access.
  • Per-User Catalog Denial blocks specific users from discovering tools entirely, supporting compliance and licensing requirements.
  • Provider-Level Enforcement translates external ACL and policy denials into structured MCP errors that clients can handle gracefully.
  • Desktop Policy Gating filters custom providers from the UI based on administrative configuration, controlling visibility separately from execution permissions.

These mechanisms combine to allow fine-grained control over which users can discover, view, and execute MCP tools within enterprise environments.

Frequently Asked Questions

How do I configure which OAuth scopes are required for MCP tool access?

Required scopes are defined in the MCP scenario configuration under scenario.oauth.requiredResourceScopes. The handleMcpRequest function in mcp-handler.ts validates every token against these scopes, rejecting requests with a 403 insufficient_scope error if the token lacks required permissions. For testing, inject the reject-scope fault via the mock server's fault matrix to simulate missing scope scenarios.

What is the difference between per-user denial and provider-level denial?

Per-user denial occurs at the gateway level in handleToolsList and prevents a user from seeing any tools in the catalog (error 403 provider_per_user_forbidden). Provider-level denial happens after tool selection when the underlying service (like Microsoft Graph) rejects the specific operation, resulting in PROVIDER_ACL_DENIED or PROVIDER_POLICY_DENIED errors. The former blocks discovery; the latter blocks execution.

Can users be blocked from seeing custom providers while still accessing cloud-hosted tools?

Yes. The allowCustomProviders flag in the desktop policy configuration (desktop-policies.ts) controls UI visibility independently of backend permissions. When set to false, checkDesktopAppRestriction filters custom providers from the model picker, even if the user's token has valid scopes and the provider would otherwise permit the operation. This is managed via the patchDesktopPolicies API and stored in the Den DB.

How does the mock server simulate different permission denial scenarios?

The enterprise MCP mock server uses a fault matrix system where administrators can inject specific denial conditions. Faults like reject-scope, per-user-403, provider-authorization-denial, and provider-policy-denial trigger corresponding code paths in mcp-handler.ts that simulate production permission failures without requiring actual identity provider configuration.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →