I registered a mock MCP server in Unity Catalog, exposed only its read tool, attached a deny policy, and then called the delete tool anyway. What decided the outcome was access to the underlying connection, and it came from grants on the schema, not on the service.

WHAT THE EXPERIMENT SHOWED
- Tool selection and service policies only govern calls through the MCP Service. An identity with USE CONNECTION called the blocked delete tool through the connection proxy.
- On a schema-level connection, ALL PRIVILEGES on the schema or USE CONNECTION on the catalog also opened the proxy. A metastore-level connection opened only for a grant on the connection itself.
- Proxied calls are audited as ucHttpConnectionProxiedRequest, with the caller but no tool name.
An MCP Service looks like the natural security boundary for an agent tool. You pick which tools it exposes, attach service policies, grant EXECUTE, and Unity Gateway logs each call. All of that applies to calls that go through the service. The external server also sits behind a Unity Catalog HTTP connection, and the connection has its own entry point.
Databricks says so plainly in the registration guide:
"Invoking an MCP Service requires no privilege on the underlying connectionāEXECUTE on the MCP Service is sufficient. Don't grant USE CONNECTION to end users: it lets them call the external server directly through the connection, or register their own MCP Service on it, bypassing the tool selection, service policies, and auditing of your MCP Service. Reserve connection access for service authors and administrators."
ā Register an external MCP server
I wanted to see what each part of that warning looks like in practice.

Figure 1. Two paths to one server. POST /ai-gateway/mcp-services/<name> runs the service's tool selection, policies and mcpCall audit. POST /api/2.0/unity-catalog/connections/<name>/proxy checks connection privileges and forwards the request; none of the service controls run.
The setup
- A small FastMCP server with two tools: get_inventory (read) and delete_inventory_record, a mock write that returns "deleted": true and deletes nothing. Bearer-token protected, published through a temporary Cloudflare tunnel.
- A schema-level HTTP connection, workspace.mcp_boundary.inventory_conn, which is the placement the docs recommend.
- An MCP Service, inventory_readonly, with include_tool_selectors: ["get_*"].
- A custom service policy (Beta) that returns DENY when the tool is delete_inventory_record.
- Three service principals calling with OAuth machine-to-machine tokens:
- A has USE CATALOG, USE SCHEMA and EXECUTE on the service.
- B has the same, plus USE CONNECTION.
- C has USE CATALOG and ALL PRIVILEGES on the schema, and nothing granted on the service or the connection.
Each identity sent tools/list, a read and a delete down both paths.

Figure 2. The service as its consumers see it: one tool, get_inventory. The server behind it has two.
What each identity could reach
| Identity | Service: tools/list | Service: delete | Proxy: tools/list | Proxy: delete |
| A (EXECUTE only) | get_inventory | -32003 blocked | 403 | 403 |
| B (+ USE CONNECTION) | get_inventory | -32003 blocked | both tools | deleted: true |
| C (ALL PRIVILEGES on schema) | get_inventory | -32003 blocked | both tools | deleted: true |
Through the service, all three saw one tool, and the delete came back with the documented error, -32003 "Tool not allowed by MCP service configuration." On the proxy they split. A got 403 PERMISSION_DENIED. B and C listed both tools and ran the delete:
=== User B: EXECUTE + USE CONNECTION ===
MCP Service (Unity Gateway) tools/list HTTP 200 tools=['get_inventory']
MCP Service (Unity Gateway) call delete_inventory_record HTTP 200 {"code": -32003, "message": "Tool not allowed by MCP service configuration."}
Connection proxy (direct) tools/list HTTP 200 tools=['get_inventory', 'delete_inventory_record']
Connection proxy (direct) call delete_inventory_record HTTP 200 result={"sku":"SKU-001","deleted":true,"note":"MOCK - nothing was actually deleted"}The policy never sees the proxy
For a second run I widened the tool selection to every tool, so the deny policy was the only guard left. Through the service, both A and B got the policy's reason back as a JSON-RPC error: -32003 "Deleting inventory records is not permitted through this service." B then sent the same delete to the proxy and got "deleted": true. The policy ran on the path it is attached to. Nothing consulted it on the other path. (The policy docs describe a block as an HTTP 200 response carrying a databricks_service_policy object; through this MCP Service it arrived as the JSON-RPC error above.)

Figure 3. The deny policy is attached to the service and applies to all account users. It does not apply to the connection.
USE CONNECTION can come from the schema
User C mattered more than I expected. Nobody granted C USE CONNECTION. C held ALL PRIVILEGES on the schema that contains the connection, and its proxy calls went through exactly as B's did. When I revoked that grant and left C with USE SCHEMA only, the proxy returned 403 and the service returned -32007 "Not authorized to invoke MCP service."
This follows from the docs' own advice. They recommend creating the connection at the schema level "so it's governed alongside the MCP Service." In this workspace the schema-level connection picked up schema grants, so ALL PRIVILEGES or USE CONNECTION on the schema reached the connection. The schema was also the only place I could grant it: GRANT ⦠ON CONNECTION with the three-part name did not parse, and the connection's Permissions tab returned an error. Here, a review that only read the connection's own permission list would have missed both grants.

Figure 4. Grants on the schema, not on the connection. Two of these rows open the proxy path.
Where the connection lives decides who inherits it
To check that placement was the cause, I created a second connection to the same server at the metastore level, with its own MCP Service limited to get_*. I also added User D, who has USE CONNECTION on the catalog and EXECUTE on both services. Then I ran every identity against both connections.

Figure 5. The same grants, two placements. Grants on the schema and catalog reached the connection inside them and stopped at the one outside.
| Identity | Grant that could open the proxy | Schema-level connection | Metastore-level connection |
| A | none | 403 | 403 |
| B | USE CONNECTION on the schema; on the metastore connection directly | both tools, deleted: true | both tools, deleted: true |
| C | ALL PRIVILEGES on the schema | both tools, deleted: true | 403 |
| D | USE CONNECTION on the catalog | both tools, deleted: true | 403 |
Through either MCP Service, all four saw only get_inventory, and the delete returned -32003. On the proxy, the schema-level connection accepted grants from its schema and its catalog. The metastore-level connection accepted only the grant made on the connection itself, and refused everyone else with User does not have any privileges on Connection 'mcp_boundary_ms_conn'. The docs' reason for recommending the schema level is that the connection is then governed alongside the MCP Service. The same placement lets connection access arrive from the schema and the catalog above it.

Figure 6. At the metastore level the connection has its own permission list, and it was the only grant that opened the proxy.
The second bypass: your own MCP Service
The warning also covers registering another MCP Service on the same connection. That needs CREATE SERVICE too. B's first attempt failed with User does not have CREATE SERVICE on Schema. After I added that grant, B created shadow_b with no tool selection; it listed both tools and ran the delete. As the schema owner, and not a metastore admin, my list of MCP services in the schema still returned only inventory_readonly. C also created a service, but calls through it failed with Insufficient privileges on connection ⦠to vend credentials. I can't explain that difference yet.
What the logs show
The mock server can't tell the callers apart. A's governed read and B's direct delete arrived with the same header set: User-Agent: Databricks-MCP-Proxy/1.0, the connection's one bearer token, and no header naming the caller or the path. Calls blocked by tool selection or the policy never reached it.
Databricks does record both paths in system.access.audit, at different levels of detail. A governed call is an mcpCall row with the service name and tool_name. B's blocked delete is an mcpCall row with status 403 and tool_name delete_inventory_record, even though the caller received HTTP 200 with a JSON-RPC error. A proxied call is a ucHttpConnectionProxiedRequest row with the caller and the connection, but no tool name. You can see that B used the connection. You can't see from that row that B ran a delete. Calls through shadow_b appear as mcpCall rows naming shadow_b. This query surfaces all three patterns. Replace the service name with your own:
SELECT event_time,
coalesce(user_identity.email, user_identity.subject_name) AS caller,
action_name,
response.status_code AS status,
request_params['connection_name'] AS connection_name,
coalesce(request_params['fully_qualified_name'], request_params['mcp_service_id']) AS mcp_service,
request_params['tool_name'] AS tool_name
FROM system.access.audit
WHERE event_date >= current_date() - INTERVAL 30 DAYS
AND (
action_name = 'ucHttpConnectionProxiedRequest'
OR action_name = 'createMcpService'
OR (action_name = 'mcpCall'
AND coalesce(request_params['fully_qualified_name'], '')
<> 'workspace.mcp_boundary.inventory_readonly')
)
ORDER BY event_time DESCIn my workspace it returned 30 rows. They included every proxied request (B's and C's succeeded, A's were denied with 403), the createMcpService rows for shadow_b and shadow_c, and the mcpCall rows through both. Audit rows appeared roughly 7 to 15 minutes after the calls.

Figure 7. system.access.audit for A and B during the first run, with identities relabelled. Service calls name the tool. Proxied calls name only the connection.

The permission model to use
- Consumers: EXECUTE on the MCP Service, plus USE CATALOG and USE SCHEMA. That was all A needed to use the read tool, and it reached nothing else.
- Service authors and admins: USE CONNECTION, and CREATE SERVICE where they register services.
- A schema-level connection: give consumer groups USE SCHEMA and USE CATALOG only. ALL PRIVILEGES or USE CONNECTION on that schema, or USE CONNECTION on its catalog, is connection access. Keep CREATE SERVICE for service authors.
- A metastore-level connection: it did not inherit the schema or catalog grants I tested, so its own permission list is the one to review.
- Reviews: for a schema-level connection, check grants on the connection, its schema and its catalog. Run the audit query above for proxied requests and for services you didn't create.
Conclusion
An MCP Service is a real control. In this experiment, tool selection hid the delete tool, the deny policy blocked it, and the audit log recorded the blocked attempt with the tool's name. None of that applied to the connection underneath. Any identity that could use the connection reached the server directly, and on a schema-level connection that included identities whose only relevant grant was on the schema or catalog above it.
So when you review who can reach an MCP server, start with the connection: where it lives, who holds USE CONNECTION on it, and which grants on the levels above it carry that privilege down. Give consumers EXECUTE on the service and nothing that adds up to USE CONNECTION.