MCP tools¶
AI agents call Sluice through MCP, the Model Context Protocol. A tool call passes the same policies as a REST query.
Example¶
Open a session over HTTP and store its id:
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
Call the tool execute_sql:
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
}
}
}
The answer holds the rows as text and as structuredContent.
Tools¶
| Tool | Input | Returns |
|---|---|---|
execute_sql | sql (required), row_limit | query_id, columns, rows, row_count, truncated |
sample_table | table (required), columns, rows, rate, method, stratify_by, seed, where | The same as execute_sql |
list_catalogs | none | catalogs with name, type, healthy |
list_tables | catalog (required), schema, limit, cursor | tables, next_cursor |
list_accessible_tables | catalog, limit, cursor | tables, next_cursor |
describe_table | table (required) | table, columns with name, type, nullable |
explain_access | table or sql | subject, resource, decision, row_filters, column_masks, matched, rejected |
whoami | none | anonymous, subject, issuer, email, groups, auth_method |
check_approval | approval_id (required) | approval_id, state, expires_at |
await_approval | approval_id (required), timeout_seconds | The same, after a wait of at most 55 seconds |
row_limitworks likemax_rowsin the REST API.sample_tabletakes the fields ofPOST /v1/sample. The sample request fields list the rules.tableis a name of the formcatalog.schema.table. Sluice does not add a missing catalog or schema. A policy that names them does not match a shorter name.limitis 500 by default and at most 1000. Passnext_cursorascursorto get the next page. Acursorthat is not valid givesERR_SYNTAX.explain_accessruns no query.- The approval tools read the state of a held
execute_sqlorsample_tablecall. An unknown id givesERR_APPROVAL_EXPIRED. Withoutapproval.publicBaseUrlboth tools giveERR_INTERNAL. See Approvals. - A failed call has
isError: true. Its text starts with the error code and can name the policy and the query id.
Transports¶
| Transport | How to start | How the subject is set |
|---|---|---|
| stdio | sluice mcp --config server.yaml, see CLI | Once at start, from --api-key or --jwt. |
stdio in sluice serve | mcp.enabled: true | Once at start, from mcp.apiKeyRef or mcp.tokenRef. |
| Streamable HTTP | mcp.transport and mcp.listen, as below | Each request sends X-Api-Key or Authorization: Bearer. Without it the answer is 401. |
mcp.listen is required for this transport. Set mcp.tls to serve HTTPS, see Deployment. limits.globalRps and limits.perIpRps count REST queries and MCP requests together, before login. A request over the limit gets 429 ERR_RATE_LIMITED. The limits count HTTP requests: one request can carry several tool calls.
sluice mcp --transport streamable_http listens on mcp.listen too. sluice mcp does not start when an ApprovalPolicy is loaded, see Approvals. Stdio without a credential does not start. With --allow-anonymous or mcp.allowAnonymous, a client without a credential runs as an anonymous subject. Default deny still applies.
Over HTTP, Sluice keeps no session state. A tool call runs as the subject of its own request. The value of the Mcp-Session-Id header has no effect on a tool call. A GET that asks for an event stream gets 405. A notifications/cancelled message has no effect. Close the request to stop a running call.
Which tables an agent sees¶
list_tables and list_accessible_tables show every table that a policy allows the subject to read. The data source has a third table, customers, that no policy allows:
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": "list_tables",
"arguments": {"catalog": "shop"}}}'
event: message
data: {
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "2 table(s)"
}
],
"structuredContent": {
"tables": ["shop.main.orders", "shop.main.products"]
}
}
}
Sluice reads the tables of a data source on the first call, and again after a reload. A data source that is down is left out. Sluice keeps the columns of at most 10 000 tables. With more tables, the lists are not complete.
describe_table answers ACL_DENIED for a table that the subject cannot read. For other tables it returns every column, masked ones too. list_catalogs hides a catalog when the subject can read none of its tables. A catalog that is empty or down stays visible.
Audit records¶
execute_sql, sample_table and explain_access write audit records with origin: mcp. The other tools write none.
See also¶
- MCP agents
- CLI for the flags of
sluice mcp - Audit log