Skip to content

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_limit works like max_rows in the REST API.
  • sample_table takes the fields of POST /v1/sample. The sample request fields list the rules.
  • table is a name of the form catalog.schema.table. Sluice does not add a missing catalog or schema. A policy that names them does not match a shorter name.
  • limit is 500 by default and at most 1000. Pass next_cursor as cursor to get the next page. A cursor that is not valid gives ERR_SYNTAX.
  • explain_access runs no query.
  • The approval tools read the state of a held execute_sql or sample_table call. An unknown id gives ERR_APPROVAL_EXPIRED. Without approval.publicBaseUrl both tools give ERR_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:
  enabled: true
  transport: "streamable_http"
  listen: ":8081"

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