# How MCP Tool Permission Deny/Allow Configuration Works in OpenWork

> Understand OpenWork's four-layer MCP tool permission deny/allow configuration. Learn how access tokens, user identity, provider ACLs, and desktop policies control tool visibility.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-09

---

**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`](https://github.com/different-ai/openwork/blob/main/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:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/mcp-handler.ts)), the system checks for the `per-user-403` fault before returning any tool definitions:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/mcp-handler.ts) (lines 400-420) translates provider denials into structured MCP errors:

```typescript
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`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/desktop-policies.ts). This policy determines whether tools from custom (non-cloud) providers appear in the model picker:

```typescript
// 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:

- **[`packages/enterprise-mcp-mock-server/src/protocol/mcp-handler.ts`](https://github.com/different-ai/openwork/blob/main/packages/enterprise-mcp-mock-server/src/protocol/mcp-handler.ts)**: Core logic for token validation, per-user denial, and provider error generation.
- **[`packages/types/src/den/desktop-policies.ts`](https://github.com/different-ai/openwork/blob/main/packages/types/src/den/desktop-policies.ts)**: Schema definition for `allowCustomProviders` and other desktop restrictions.
- **[`ee/apps/den-api/test/org-role-permissions.test.ts`](https://github.com/different-ai/openwork/blob/main/ee/apps/den-api/test/org-role-permissions.test.ts)**: Integration tests verifying organization-level permission enforcement.
- **[`ee/packages/enterprise-mcp-mock-server/test/fault-matrix.test.ts`](https://github.com/different-ai/openwork/blob/main/ee/packages/enterprise-mcp-mock-server/test/fault-matrix.test.ts)**: Test suite demonstrating fault injection for `per-user-403`, `reject-scope`, and provider denials.

## 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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/mcp-handler.ts) that simulate production permission failures without requiring actual identity provider configuration.