MCP agents¶
An AI agent queries Sluice over MCP. Policies apply to MCP calls as they do to REST calls.
Example¶
Start the server with examples/mcp-agent/serve.sh. MCP listens on port 8081.
# MCP over HTTP on its own port. Every request must carry the agent's API key.
# `sluice mcp` also reads this file, but takes the transport from its own flag.
mcp:
enabled: true
transport: "streamable_http"
listen: ":8081"
sessionIdleMax: 30m
The agent's API key maps to the group agents. The group may read products, and every query gets a row limit.
# Agents may read the products table. Everything else is denied by default,
# also the customers table in the same catalog.
apiVersion: sluice.bino.bi/v1alpha1
kind: SqlAccessPolicy
metadata:
name: allow-agents
priority: 100
spec:
effect: allow
match:
any:
- subjects:
groups: ["agents"]
resources:
catalogs: ["shop"]
schemas: ["main"]
tables: ["products"]
# Row limit for agents. A query without LIMIT gets one, a larger LIMIT is cut.
# The value is small so the demo data shows the effect.
apiVersion: sluice.bino.bi/v1alpha1
kind: QueryRewritePolicy
metadata:
name: limit-agents
priority: 100
spec:
match:
any:
- subjects:
groups: ["agents"]
rewrite:
limit:
max: 3
Open a session. The step stores the session id in data/mcp-session. It needs curl 7.84 or later.
curl -s http://localhost:8081/ \
-H "X-Api-Key: mcp.supersecret" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "curl", "version": "1.0"}}}' \
-o /dev/null -w '%header{mcp-session-id}' > data/mcp-session
Read the columns of products.
curl -s http://localhost:8081/ \
-H "X-Api-Key: mcp.supersecret" \
-H "Mcp-Session-Id: $(cat data/mcp-session)" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {
"name": "describe_table",
"arguments": {"table": "shop.main.products"}}}'
List the tables that the agent may read.
curl -s http://localhost:8081/ \
-H "X-Api-Key: mcp.supersecret" \
-H "Mcp-Session-Id: $(cat data/mcp-session)" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {
"name": "list_accessible_tables",
"arguments": {}}}'
event: message
data: {
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "1 accessible table(s)"
}
],
"structuredContent": {
"tables": ["shop.main.products"]
}
}
}
Run a query.
curl -s http://localhost:8081/ \
-H "X-Api-Key: mcp.supersecret" \
-H "Mcp-Session-Id: $(cat data/mcp-session)" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": {
"name": "execute_sql",
"arguments": {"sql": "SELECT name, price_cents FROM shop.main.products ORDER BY price_cents DESC LIMIT 2"}}}'
event: message
data: {
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "name | price_cents\n----------------\nBlack T-Shirt (M) | 2499\nCeramic Mug | 1299\nrow_count=2 truncated=false query_id=<id>\n"
}
],
"structuredContent": {
"columns": ["name", "price_cents"],
"query_id": "<query-id>",
"row_count": 2,
"rows": [
["Black T-Shirt (M)", 2499],
["Ceramic Mug", 1299]
],
"truncated": false
}
}
}
What happened: Sluice checked the key on each request and applied the policies of the group. The query keeps its LIMIT 2 because it is below the limit.
More examples¶
A query without LIMIT returns three of the five rows, and truncated is true.
curl -s http://localhost:8081/ \
-H "X-Api-Key: mcp.supersecret" \
-H "Mcp-Session-Id: $(cat data/mcp-session)" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 6, "method": "tools/call", "params": {
"name": "execute_sql",
"arguments": {"sql": "SELECT id, name FROM shop.main.products ORDER BY id"}}}'
event: message
data: {
"jsonrpc": "2.0",
"id": 6,
"result": {
"content": [
{
"type": "text",
"text": "id | name\n----------------\n1 | Ceramic Mug\n2 | Black T-Shirt (M)\n3 | A5 Notebook\nrow_count=3 truncated=true query_id=<id>\n"
}
],
"structuredContent": {
"columns": ["id", "name"],
"query_id": "<query-id>",
"row_count": 3,
"rows": [
[1, "Ceramic Mug"],
[2, "Black T-Shirt (M)"],
[3, "A5 Notebook"]
],
"truncated": true
}
}
}
A query on customers is denied. The agent gets ACL_DENIED as a tool error.
curl -s http://localhost:8081/ \
-H "X-Api-Key: mcp.supersecret" \
-H "Mcp-Session-Id: $(cat data/mcp-session)" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": {
"name": "execute_sql",
"arguments": {"sql": "SELECT name, email FROM shop.main.customers"}}}'
event: message
data: {
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "ACL_DENIED: no SqlAccessPolicy matched (default-deny) (query_id=<id>)"
}
],
"structuredContent": {
"columns": null,
"query_id": "",
"row_count": 0,
"rows": null,
"truncated": false
},
"isError": true
}
}
Claude Desktop¶
Claude Desktop starts sluice mcp over stdio with this config. sluice mcp checks the key once at start. Replace the path. Run serve.sh once to create the data, then stop it. make docs-check does not run this file.
{
"mcpServers": {
"sluice": {
"command": "sh",
"args": [
"-c",
"cd /ABSOLUTE/PATH/TO/sluice/examples/mcp-agent && exec ../../bin/sluice mcp --config server.yaml --api-key mcp.supersecret"
],
"env": {
"SLUICE_APIKEY_PEPPER": "mcp-agent-demo-pepper",
"SLUICE_AUDIT_GENESIS": "mcp-agent-demo-genesis",
"SLUICE_APIKEY_MCP_HASH": "341081ca3a9ec503c1ee5ab902f514de90c9978a103d67ee43ed836247da40bb"
}
}
}
}
Limits¶
- Sluice writes an audit record for
execute_sql,sample_tableandexplain_access, not for the other tools.