>

Tool-level access control: enforcing which tools each role can reach in MCP

Tool-level access control: enforcing which tools each role can reach in MCP

Last updated: June 2026

Server-level restrictions are the access control equivalent of a building key: it gets you through the front door, but once you're in, you have access to every room. For most teams, that's not the access model you want. A designer connecting to Notion through Claude shouldn't also have access to Notion's user management tools. An engineer with read access to your data warehouse shouldn't have write access through the same connection. An agent running a narrow task shouldn't reach tools outside that task's scope.

Tool-level access control lets you define which tools each role can call on each server, enforced at the proxy layer so restricted tools are structurally absent from the user's tool list rather than just hidden client-side. What follows is an outline of how we’ve built that model at Aptible and what you can do to implement it yourself.

Solutions to consider

There are a few ways to approach tool-level access control, each with real tradeoffs. The simplest is selective configuration: don't add servers to a user's Claude setup that they shouldn't have. That works at very small scale but doesn't survive team growth. It requires maintaining per-person configs with no central enforcement.

A related approach is using separate server instances per role (a read-only Notion server, a full-access one), which keeps configuration simple but becomes a maintenance problem fast as your server and role counts grow. You can also enforce access in application-layer middleware, but that depends on every service applying the controls correctly rather than having one enforcement point that all callers pass through.

Aptible’s solution: the grant model

The model we built at Aptible uses a central proxy layer with three primitives: servers, roles, and grants. The key reasons we went this direction: enforcement happens once regardless of which client is calling (Claude Desktop, Claude Code, or a custom agent), it maps onto existing role structures without requiring a new identity system, and co-locating access control with audit logging means you get both from the same layer.

Here's how it works:

A server is a registered MCP endpoint in your organization's gateway (a Notion server, a GitHub server, a Snowflake server, etc.). A role maps to your existing organizational structure (the same roles you use to control access to other systems). A grant is the mapping between a role and a specific set of tools on a specific server.

When a user connects to the MCP gateway, the system resolves their roles, finds their grants, and presents only the tools they're allowed to call. When they make a tool call, the gateway validates the call against their grants before proxying it upstream. A call to a tool not covered by any of the user's grants is rejected at the proxy layer, not just absent from the tool list.

Here's what a simple grant structure looks like for an engineering team (conceptual structure; see the Django ORM version below for runnable code):

# Conceptual grant configuration
grants = [
    # Everyone can read from Notion
    {"role": "engineering", "server": "notion", "tools": [
        "notion_search",
        "notion_get_page",
        "notion_query_database",
    ]},
    # Engineering leads can also write
    {"role": "engineering_lead", "server": "notion", "tools": [
        "notion_search",
        "notion_get_page",
        "notion_query_database",
        "notion_create_page",
        "notion_update_page",
    ]},
    # Data warehouse access is a separate role
    {"role": "data_access", "server": "snowflake", "tools": ["snowflake_query"]},
    # Admins get everything
    {"role": "account_owner", "server": "notion", "tools": ["*"]},
    {"role": "account_owner", "server": "snowflake", "tools": ["*"]},
]

The wildcard "*" grants access to all tools a server exposes. It's appropriate for administrators but too broad for general use, especially as servers evolve and add new tools.

Choosing between wildcard and explicit grants

The tradeoff between wildcard and explicit grants comes down to convenience versus precision.

A wildcard grant is easy to set up and stays automatically current as a server adds new tools. The downside is that every new tool the server ships is immediately accessible to anyone with that grant, without any review. For a trusted admin role, that's acceptable. For a role that has access to sensitive data, it's a silent expansion of your attack surface.

Explicit grants require more maintenance but give you control over exactly what each role can reach. When a server adds a new tool (say, a Notion MCP update ships notion_delete_page), it doesn't appear for any role until you explicitly add it to a grant. That review step is where you catch tools that shouldn't be accessible to a given role.

The practical approach for most teams: use explicit grants for any role that touches sensitive data, shared production systems, or tools with destructive potential. Use wildcards for admin roles where trust is already established and the overhead of maintaining explicit lists isn't warranted.

Why proxy-layer enforcement beats client-side filtering

Client-side filtering (hiding tools from the Claude tool list) is not access control. A sufficiently capable model, or a direct API call, can still attempt to call a tool that isn't in the presented list. Our approach enforces access at the proxy layer, where a call to an unauthorized tool is rejected before it reaches the upstream server. That’s why we treat proxy enforcement as the security guarantee and client-side filtering as a UX improvement on top of it, not the other way around.

Here's the sequence of what happens when a user calls a tool through the gateway:

  1. The gateway authenticates the request and resolves the caller's identity.

  2. The gateway looks up the caller's grants and builds their allowed tool list.

  3. If the requested tool isn't in the allowed list, the gateway returns a Tool not permitted error and writes an audit log entry. The call never reaches the upstream server.

  4. If the tool is permitted, the gateway writes an audit log entry and proxies the call upstream.

The audit log is written before the proxy call in step 4, not after. If the upstream call fails, the log still exists. If the log write fails, the proxy doesn't proceed silently; the error surfaces. This ordering matters for forensics.

The proxy layer implementation for step 2-3 looks like this:

async def on_call_tool(self, context, call_next):
    auth = _request_auth.get()
    if auth is None:
        return ToolResult(content=[TextContent(type="text", text="Unauthorized")])

    claims, org_id, allowed = auth
    tool_name = context.message.name

    # Enforce at the proxy layer, not just filter from the list
    if tool_name not in allowed:
        return ToolResult(content=[TextContent(type="text", text="Tool not permitted")])

    # Write the audit log before proxying
    ua_headers = get_http_headers(include={"user-agent"})
    await create_audit_log(
        claims=claims,
        org_id=org_id,
        tool_name=tool_name,
        arguments=getattr(context.message, "arguments", {}) or {},
        user_agent=ua_headers.get("user-agent", ""),
        ip_address=_get_client_ip(),
    )

    # Proxy to upstream server
    server, config = await resolve_server_config(tool_name, org_id, claims["id"])
    ...

Note that tool_name not in allowed is a hard rejection, not a filter. The allowed list is built at authentication time from the caller's roles and grants; in our implementation, it's cached for 120 seconds to avoid redundant database lookups on every tool call.

How you could structure this for your team

The right grant structure depends on your team's role model and the tools you're connecting. Most teams find that three to four tiers cover the majority of cases.

Start with a baseline role that covers every team member: read-only access to shared tools like your project management, documentation, and communication servers. On top of that, create additional roles for anyone who needs write access or access to sensitive systems: a role for engineers who can push to GitHub, a separate role for team members who need to query your data warehouse, and a wildcard admin role for account owners. Keep those roles narrow and explicit until you have confidence in what each server actually exposes.

The key question when designing each grant: what's the blast radius if this role is compromised or misused? A role with read access to Notion and query access to Snowflake is a relatively contained risk. A role with write access to GitHub and full access to your data warehouse is a much larger one. Use that framing to decide where to use explicit tool lists versus wildcards, and which roles get access to which servers at all.

For teams handling sensitive data, the most important grant decision is usually keeping data access roles separate from general access roles, not as a matter of trust, but so that the set of identities (human and agent) that can reach sensitive data stays as small as possible. The example in the code above shows one way to do this: data_access is a separate role that can be added to specific users without affecting anyone else's grants.

Keeping your grant configuration current

When a server adds new tools. If the server uses an explicit grant, new tools won't appear for any role until you add them to a grant, which is the behavior you want. If it uses a wildcard grant, new tools appear immediately. Set up a process to review new tool additions for servers on wildcard grants, particularly if those servers touch sensitive data.

Auditing which roles have access to what. Your grant configuration is the source of truth for what each role can reach. Treat it like infrastructure: version it, review changes, and audit it periodically. An access review that can answer "which roles can call snowflake_query?" is significantly more useful than one that can only say "who has Snowflake access."

Onboarding new servers. When you add a new MCP server, the default should be no grants. Define which roles should have access before the server becomes available to the team. This is especially important for servers with shell access or credential access, which are tier-0 supply chain risk.

FAQs

Why enforce at the proxy layer rather than just filtering tools from Claude's list?

Filtering the tool list is advisory: a capable model can still attempt to call a tool that isn't presented, and a direct API call to the gateway bypasses client-side filtering entirely. Proxy-layer enforcement rejects unauthorized calls before they reach the upstream server, regardless of how the request was made. Both are useful (filtering improves the user experience) but only proxy enforcement provides a security guarantee.

Can I safely use wildcard grants?

For administrator roles where trust is established and tool sets are stable, wildcards are practical. For any role that touches sensitive data, has access to production systems, or includes tools with destructive potential, explicit grants are safer. The risk with wildcards is silent access expansion: when a server ships a new tool, every wildcard grantee gets it immediately without review.

How often should I audit my grant configuration?

Periodic audits without clear triggers tend to miss the changes that matter. A better approach is treating grants as infrastructure: version-controlled, reviewed on change, and validated against your role structure whenever your team or tool set changes significantly. Key triggers: an engineer changing roles, a server updating its tool list, a new server being onboarded.

What happens if an MCP server changes its tool list?

With explicit grants, new tools are inaccessible to all roles until you deliberately add them, which is the safe default. With wildcard grants, new tools become immediately accessible to all grantees without any review step. For servers with sensitive data or destructive tools, this is why explicit grants are recommended.

Can agents have different tool grants than the humans on my team?

Yes, and they should. Agents provisioned as robot users are assigned to roles with their own grant sets, separate from any human user's access. This is covered in detail in Agent identity and robot users.

Next steps

If you need to log what gets called against your servers: Audit logging for MCP tool calls: immutable records with encrypted arguments, queryable by user, server, and tool

If you're managing credentials across shared and personal servers: MCP authentication: bearer tokens, OAuth, shared vs. personal credentials, and token lifecycle

If you're giving agents their own permission set: Agent identity and robot users: separate grants and identity for non-human principals

If you're ready to roll this out across your team: Deploying MCP for your whole team: from individual setup to org-wide deployment with MDM

Aptible MCP Gateway gives engineering teams tool-level access control, audit logging, and centralized credential management for MCP without building the proxy infrastructure yourself. Deployed alongside Aptible AI Gateway, it covers both LLM and tool call governance in one place. Join the MCP Gateway waitlist →

>

text

Tool-level access control: enforcing which tools each role can reach in MCP

Last updated: June 2026

Server-level restrictions are the access control equivalent of a building key: it gets you through the front door, but once you're in, you have access to every room. For most teams, that's not the access model you want. A designer connecting to Notion through Claude shouldn't also have access to Notion's user management tools. An engineer with read access to your data warehouse shouldn't have write access through the same connection. An agent running a narrow task shouldn't reach tools outside that task's scope.

Tool-level access control lets you define which tools each role can call on each server, enforced at the proxy layer so restricted tools are structurally absent from the user's tool list rather than just hidden client-side. What follows is an outline of how we’ve built that model at Aptible and what you can do to implement it yourself.

Solutions to consider

There are a few ways to approach tool-level access control, each with real tradeoffs. The simplest is selective configuration: don't add servers to a user's Claude setup that they shouldn't have. That works at very small scale but doesn't survive team growth. It requires maintaining per-person configs with no central enforcement.

A related approach is using separate server instances per role (a read-only Notion server, a full-access one), which keeps configuration simple but becomes a maintenance problem fast as your server and role counts grow. You can also enforce access in application-layer middleware, but that depends on every service applying the controls correctly rather than having one enforcement point that all callers pass through.

Aptible’s solution: the grant model

The model we built at Aptible uses a central proxy layer with three primitives: servers, roles, and grants. The key reasons we went this direction: enforcement happens once regardless of which client is calling (Claude Desktop, Claude Code, or a custom agent), it maps onto existing role structures without requiring a new identity system, and co-locating access control with audit logging means you get both from the same layer.

Here's how it works:

A server is a registered MCP endpoint in your organization's gateway (a Notion server, a GitHub server, a Snowflake server, etc.). A role maps to your existing organizational structure (the same roles you use to control access to other systems). A grant is the mapping between a role and a specific set of tools on a specific server.

When a user connects to the MCP gateway, the system resolves their roles, finds their grants, and presents only the tools they're allowed to call. When they make a tool call, the gateway validates the call against their grants before proxying it upstream. A call to a tool not covered by any of the user's grants is rejected at the proxy layer, not just absent from the tool list.

Here's what a simple grant structure looks like for an engineering team (conceptual structure; see the Django ORM version below for runnable code):

# Conceptual grant configuration
grants = [
    # Everyone can read from Notion
    {"role": "engineering", "server": "notion", "tools": [
        "notion_search",
        "notion_get_page",
        "notion_query_database",
    ]},
    # Engineering leads can also write
    {"role": "engineering_lead", "server": "notion", "tools": [
        "notion_search",
        "notion_get_page",
        "notion_query_database",
        "notion_create_page",
        "notion_update_page",
    ]},
    # Data warehouse access is a separate role
    {"role": "data_access", "server": "snowflake", "tools": ["snowflake_query"]},
    # Admins get everything
    {"role": "account_owner", "server": "notion", "tools": ["*"]},
    {"role": "account_owner", "server": "snowflake", "tools": ["*"]},
]

The wildcard "*" grants access to all tools a server exposes. It's appropriate for administrators but too broad for general use, especially as servers evolve and add new tools.

Choosing between wildcard and explicit grants

The tradeoff between wildcard and explicit grants comes down to convenience versus precision.

A wildcard grant is easy to set up and stays automatically current as a server adds new tools. The downside is that every new tool the server ships is immediately accessible to anyone with that grant, without any review. For a trusted admin role, that's acceptable. For a role that has access to sensitive data, it's a silent expansion of your attack surface.

Explicit grants require more maintenance but give you control over exactly what each role can reach. When a server adds a new tool (say, a Notion MCP update ships notion_delete_page), it doesn't appear for any role until you explicitly add it to a grant. That review step is where you catch tools that shouldn't be accessible to a given role.

The practical approach for most teams: use explicit grants for any role that touches sensitive data, shared production systems, or tools with destructive potential. Use wildcards for admin roles where trust is already established and the overhead of maintaining explicit lists isn't warranted.

Why proxy-layer enforcement beats client-side filtering

Client-side filtering (hiding tools from the Claude tool list) is not access control. A sufficiently capable model, or a direct API call, can still attempt to call a tool that isn't in the presented list. Our approach enforces access at the proxy layer, where a call to an unauthorized tool is rejected before it reaches the upstream server. That’s why we treat proxy enforcement as the security guarantee and client-side filtering as a UX improvement on top of it, not the other way around.

Here's the sequence of what happens when a user calls a tool through the gateway:

  1. The gateway authenticates the request and resolves the caller's identity.

  2. The gateway looks up the caller's grants and builds their allowed tool list.

  3. If the requested tool isn't in the allowed list, the gateway returns a Tool not permitted error and writes an audit log entry. The call never reaches the upstream server.

  4. If the tool is permitted, the gateway writes an audit log entry and proxies the call upstream.

The audit log is written before the proxy call in step 4, not after. If the upstream call fails, the log still exists. If the log write fails, the proxy doesn't proceed silently; the error surfaces. This ordering matters for forensics.

The proxy layer implementation for step 2-3 looks like this:

async def on_call_tool(self, context, call_next):
    auth = _request_auth.get()
    if auth is None:
        return ToolResult(content=[TextContent(type="text", text="Unauthorized")])

    claims, org_id, allowed = auth
    tool_name = context.message.name

    # Enforce at the proxy layer, not just filter from the list
    if tool_name not in allowed:
        return ToolResult(content=[TextContent(type="text", text="Tool not permitted")])

    # Write the audit log before proxying
    ua_headers = get_http_headers(include={"user-agent"})
    await create_audit_log(
        claims=claims,
        org_id=org_id,
        tool_name=tool_name,
        arguments=getattr(context.message, "arguments", {}) or {},
        user_agent=ua_headers.get("user-agent", ""),
        ip_address=_get_client_ip(),
    )

    # Proxy to upstream server
    server, config = await resolve_server_config(tool_name, org_id, claims["id"])
    ...

Note that tool_name not in allowed is a hard rejection, not a filter. The allowed list is built at authentication time from the caller's roles and grants; in our implementation, it's cached for 120 seconds to avoid redundant database lookups on every tool call.

How you could structure this for your team

The right grant structure depends on your team's role model and the tools you're connecting. Most teams find that three to four tiers cover the majority of cases.

Start with a baseline role that covers every team member: read-only access to shared tools like your project management, documentation, and communication servers. On top of that, create additional roles for anyone who needs write access or access to sensitive systems: a role for engineers who can push to GitHub, a separate role for team members who need to query your data warehouse, and a wildcard admin role for account owners. Keep those roles narrow and explicit until you have confidence in what each server actually exposes.

The key question when designing each grant: what's the blast radius if this role is compromised or misused? A role with read access to Notion and query access to Snowflake is a relatively contained risk. A role with write access to GitHub and full access to your data warehouse is a much larger one. Use that framing to decide where to use explicit tool lists versus wildcards, and which roles get access to which servers at all.

For teams handling sensitive data, the most important grant decision is usually keeping data access roles separate from general access roles, not as a matter of trust, but so that the set of identities (human and agent) that can reach sensitive data stays as small as possible. The example in the code above shows one way to do this: data_access is a separate role that can be added to specific users without affecting anyone else's grants.

Keeping your grant configuration current

When a server adds new tools. If the server uses an explicit grant, new tools won't appear for any role until you add them to a grant, which is the behavior you want. If it uses a wildcard grant, new tools appear immediately. Set up a process to review new tool additions for servers on wildcard grants, particularly if those servers touch sensitive data.

Auditing which roles have access to what. Your grant configuration is the source of truth for what each role can reach. Treat it like infrastructure: version it, review changes, and audit it periodically. An access review that can answer "which roles can call snowflake_query?" is significantly more useful than one that can only say "who has Snowflake access."

Onboarding new servers. When you add a new MCP server, the default should be no grants. Define which roles should have access before the server becomes available to the team. This is especially important for servers with shell access or credential access, which are tier-0 supply chain risk.

FAQs

Why enforce at the proxy layer rather than just filtering tools from Claude's list?

Filtering the tool list is advisory: a capable model can still attempt to call a tool that isn't presented, and a direct API call to the gateway bypasses client-side filtering entirely. Proxy-layer enforcement rejects unauthorized calls before they reach the upstream server, regardless of how the request was made. Both are useful (filtering improves the user experience) but only proxy enforcement provides a security guarantee.

Can I safely use wildcard grants?

For administrator roles where trust is established and tool sets are stable, wildcards are practical. For any role that touches sensitive data, has access to production systems, or includes tools with destructive potential, explicit grants are safer. The risk with wildcards is silent access expansion: when a server ships a new tool, every wildcard grantee gets it immediately without review.

How often should I audit my grant configuration?

Periodic audits without clear triggers tend to miss the changes that matter. A better approach is treating grants as infrastructure: version-controlled, reviewed on change, and validated against your role structure whenever your team or tool set changes significantly. Key triggers: an engineer changing roles, a server updating its tool list, a new server being onboarded.

What happens if an MCP server changes its tool list?

With explicit grants, new tools are inaccessible to all roles until you deliberately add them, which is the safe default. With wildcard grants, new tools become immediately accessible to all grantees without any review step. For servers with sensitive data or destructive tools, this is why explicit grants are recommended.

Can agents have different tool grants than the humans on my team?

Yes, and they should. Agents provisioned as robot users are assigned to roles with their own grant sets, separate from any human user's access. This is covered in detail in Agent identity and robot users.

Next steps

If you need to log what gets called against your servers: Audit logging for MCP tool calls: immutable records with encrypted arguments, queryable by user, server, and tool

If you're managing credentials across shared and personal servers: MCP authentication: bearer tokens, OAuth, shared vs. personal credentials, and token lifecycle

If you're giving agents their own permission set: Agent identity and robot users: separate grants and identity for non-human principals

If you're ready to roll this out across your team: Deploying MCP for your whole team: from individual setup to org-wide deployment with MDM

Aptible MCP Gateway gives engineering teams tool-level access control, audit logging, and centralized credential management for MCP without building the proxy infrastructure yourself. Deployed alongside Aptible AI Gateway, it covers both LLM and tool call governance in one place. Join the MCP Gateway waitlist →

>

text

Tool-level access control: enforcing which tools each role can reach in MCP

Last updated: June 2026

Server-level restrictions are the access control equivalent of a building key: it gets you through the front door, but once you're in, you have access to every room. For most teams, that's not the access model you want. A designer connecting to Notion through Claude shouldn't also have access to Notion's user management tools. An engineer with read access to your data warehouse shouldn't have write access through the same connection. An agent running a narrow task shouldn't reach tools outside that task's scope.

Tool-level access control lets you define which tools each role can call on each server, enforced at the proxy layer so restricted tools are structurally absent from the user's tool list rather than just hidden client-side. What follows is an outline of how we’ve built that model at Aptible and what you can do to implement it yourself.

Solutions to consider

There are a few ways to approach tool-level access control, each with real tradeoffs. The simplest is selective configuration: don't add servers to a user's Claude setup that they shouldn't have. That works at very small scale but doesn't survive team growth. It requires maintaining per-person configs with no central enforcement.

A related approach is using separate server instances per role (a read-only Notion server, a full-access one), which keeps configuration simple but becomes a maintenance problem fast as your server and role counts grow. You can also enforce access in application-layer middleware, but that depends on every service applying the controls correctly rather than having one enforcement point that all callers pass through.

Aptible’s solution: the grant model

The model we built at Aptible uses a central proxy layer with three primitives: servers, roles, and grants. The key reasons we went this direction: enforcement happens once regardless of which client is calling (Claude Desktop, Claude Code, or a custom agent), it maps onto existing role structures without requiring a new identity system, and co-locating access control with audit logging means you get both from the same layer.

Here's how it works:

A server is a registered MCP endpoint in your organization's gateway (a Notion server, a GitHub server, a Snowflake server, etc.). A role maps to your existing organizational structure (the same roles you use to control access to other systems). A grant is the mapping between a role and a specific set of tools on a specific server.

When a user connects to the MCP gateway, the system resolves their roles, finds their grants, and presents only the tools they're allowed to call. When they make a tool call, the gateway validates the call against their grants before proxying it upstream. A call to a tool not covered by any of the user's grants is rejected at the proxy layer, not just absent from the tool list.

Here's what a simple grant structure looks like for an engineering team (conceptual structure; see the Django ORM version below for runnable code):

# Conceptual grant configuration
grants = [
    # Everyone can read from Notion
    {"role": "engineering", "server": "notion", "tools": [
        "notion_search",
        "notion_get_page",
        "notion_query_database",
    ]},
    # Engineering leads can also write
    {"role": "engineering_lead", "server": "notion", "tools": [
        "notion_search",
        "notion_get_page",
        "notion_query_database",
        "notion_create_page",
        "notion_update_page",
    ]},
    # Data warehouse access is a separate role
    {"role": "data_access", "server": "snowflake", "tools": ["snowflake_query"]},
    # Admins get everything
    {"role": "account_owner", "server": "notion", "tools": ["*"]},
    {"role": "account_owner", "server": "snowflake", "tools": ["*"]},
]

The wildcard "*" grants access to all tools a server exposes. It's appropriate for administrators but too broad for general use, especially as servers evolve and add new tools.

Choosing between wildcard and explicit grants

The tradeoff between wildcard and explicit grants comes down to convenience versus precision.

A wildcard grant is easy to set up and stays automatically current as a server adds new tools. The downside is that every new tool the server ships is immediately accessible to anyone with that grant, without any review. For a trusted admin role, that's acceptable. For a role that has access to sensitive data, it's a silent expansion of your attack surface.

Explicit grants require more maintenance but give you control over exactly what each role can reach. When a server adds a new tool (say, a Notion MCP update ships notion_delete_page), it doesn't appear for any role until you explicitly add it to a grant. That review step is where you catch tools that shouldn't be accessible to a given role.

The practical approach for most teams: use explicit grants for any role that touches sensitive data, shared production systems, or tools with destructive potential. Use wildcards for admin roles where trust is already established and the overhead of maintaining explicit lists isn't warranted.

Why proxy-layer enforcement beats client-side filtering

Client-side filtering (hiding tools from the Claude tool list) is not access control. A sufficiently capable model, or a direct API call, can still attempt to call a tool that isn't in the presented list. Our approach enforces access at the proxy layer, where a call to an unauthorized tool is rejected before it reaches the upstream server. That’s why we treat proxy enforcement as the security guarantee and client-side filtering as a UX improvement on top of it, not the other way around.

Here's the sequence of what happens when a user calls a tool through the gateway:

  1. The gateway authenticates the request and resolves the caller's identity.

  2. The gateway looks up the caller's grants and builds their allowed tool list.

  3. If the requested tool isn't in the allowed list, the gateway returns a Tool not permitted error and writes an audit log entry. The call never reaches the upstream server.

  4. If the tool is permitted, the gateway writes an audit log entry and proxies the call upstream.

The audit log is written before the proxy call in step 4, not after. If the upstream call fails, the log still exists. If the log write fails, the proxy doesn't proceed silently; the error surfaces. This ordering matters for forensics.

The proxy layer implementation for step 2-3 looks like this:

async def on_call_tool(self, context, call_next):
    auth = _request_auth.get()
    if auth is None:
        return ToolResult(content=[TextContent(type="text", text="Unauthorized")])

    claims, org_id, allowed = auth
    tool_name = context.message.name

    # Enforce at the proxy layer, not just filter from the list
    if tool_name not in allowed:
        return ToolResult(content=[TextContent(type="text", text="Tool not permitted")])

    # Write the audit log before proxying
    ua_headers = get_http_headers(include={"user-agent"})
    await create_audit_log(
        claims=claims,
        org_id=org_id,
        tool_name=tool_name,
        arguments=getattr(context.message, "arguments", {}) or {},
        user_agent=ua_headers.get("user-agent", ""),
        ip_address=_get_client_ip(),
    )

    # Proxy to upstream server
    server, config = await resolve_server_config(tool_name, org_id, claims["id"])
    ...

Note that tool_name not in allowed is a hard rejection, not a filter. The allowed list is built at authentication time from the caller's roles and grants; in our implementation, it's cached for 120 seconds to avoid redundant database lookups on every tool call.

How you could structure this for your team

The right grant structure depends on your team's role model and the tools you're connecting. Most teams find that three to four tiers cover the majority of cases.

Start with a baseline role that covers every team member: read-only access to shared tools like your project management, documentation, and communication servers. On top of that, create additional roles for anyone who needs write access or access to sensitive systems: a role for engineers who can push to GitHub, a separate role for team members who need to query your data warehouse, and a wildcard admin role for account owners. Keep those roles narrow and explicit until you have confidence in what each server actually exposes.

The key question when designing each grant: what's the blast radius if this role is compromised or misused? A role with read access to Notion and query access to Snowflake is a relatively contained risk. A role with write access to GitHub and full access to your data warehouse is a much larger one. Use that framing to decide where to use explicit tool lists versus wildcards, and which roles get access to which servers at all.

For teams handling sensitive data, the most important grant decision is usually keeping data access roles separate from general access roles, not as a matter of trust, but so that the set of identities (human and agent) that can reach sensitive data stays as small as possible. The example in the code above shows one way to do this: data_access is a separate role that can be added to specific users without affecting anyone else's grants.

Keeping your grant configuration current

When a server adds new tools. If the server uses an explicit grant, new tools won't appear for any role until you add them to a grant, which is the behavior you want. If it uses a wildcard grant, new tools appear immediately. Set up a process to review new tool additions for servers on wildcard grants, particularly if those servers touch sensitive data.

Auditing which roles have access to what. Your grant configuration is the source of truth for what each role can reach. Treat it like infrastructure: version it, review changes, and audit it periodically. An access review that can answer "which roles can call snowflake_query?" is significantly more useful than one that can only say "who has Snowflake access."

Onboarding new servers. When you add a new MCP server, the default should be no grants. Define which roles should have access before the server becomes available to the team. This is especially important for servers with shell access or credential access, which are tier-0 supply chain risk.

FAQs

Why enforce at the proxy layer rather than just filtering tools from Claude's list?

Filtering the tool list is advisory: a capable model can still attempt to call a tool that isn't presented, and a direct API call to the gateway bypasses client-side filtering entirely. Proxy-layer enforcement rejects unauthorized calls before they reach the upstream server, regardless of how the request was made. Both are useful (filtering improves the user experience) but only proxy enforcement provides a security guarantee.

Can I safely use wildcard grants?

For administrator roles where trust is established and tool sets are stable, wildcards are practical. For any role that touches sensitive data, has access to production systems, or includes tools with destructive potential, explicit grants are safer. The risk with wildcards is silent access expansion: when a server ships a new tool, every wildcard grantee gets it immediately without review.

How often should I audit my grant configuration?

Periodic audits without clear triggers tend to miss the changes that matter. A better approach is treating grants as infrastructure: version-controlled, reviewed on change, and validated against your role structure whenever your team or tool set changes significantly. Key triggers: an engineer changing roles, a server updating its tool list, a new server being onboarded.

What happens if an MCP server changes its tool list?

With explicit grants, new tools are inaccessible to all roles until you deliberately add them, which is the safe default. With wildcard grants, new tools become immediately accessible to all grantees without any review step. For servers with sensitive data or destructive tools, this is why explicit grants are recommended.

Can agents have different tool grants than the humans on my team?

Yes, and they should. Agents provisioned as robot users are assigned to roles with their own grant sets, separate from any human user's access. This is covered in detail in Agent identity and robot users.

Next steps

If you need to log what gets called against your servers: Audit logging for MCP tool calls: immutable records with encrypted arguments, queryable by user, server, and tool

If you're managing credentials across shared and personal servers: MCP authentication: bearer tokens, OAuth, shared vs. personal credentials, and token lifecycle

If you're giving agents their own permission set: Agent identity and robot users: separate grants and identity for non-human principals

If you're ready to roll this out across your team: Deploying MCP for your whole team: from individual setup to org-wide deployment with MDM

Aptible MCP Gateway gives engineering teams tool-level access control, audit logging, and centralized credential management for MCP without building the proxy infrastructure yourself. Deployed alongside Aptible AI Gateway, it covers both LLM and tool call governance in one place. Join the MCP Gateway waitlist →