Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/environmentd/tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5412,8 +5412,22 @@ fn run_mcp_datadriven_inner(
other => panic!("unknown directive: {}", other),
};

let json: serde_json::Value = serde_json::from_str(&tc.input).unwrap();
let res = Client::new().post(url).json(&json).send().unwrap();
// Directive args: `get` sends a GET request instead of POST (the
// input is ignored), and `origin` attaches a non-allowlisted
// Origin header to exercise the DNS-rebinding defense. (Datadriven
// arg values cannot contain `:` or `/`, so the origin value is a
// fixed constant here rather than a directive parameter.)
let client = Client::new();
let mut req = if tc.args.contains_key("get") {
client.get(url)
} else {
let json: serde_json::Value = serde_json::from_str(&tc.input).unwrap();
client.post(url).json(&json)
};
if tc.args.contains_key("origin") {
req = req.header("origin", "https://evil.example.com");
}
let res = req.send().unwrap();

let status = res.status();
let body = res.text().unwrap();
Expand Down Expand Up @@ -5504,6 +5518,18 @@ fn test_mcp_developer_disabled() {
run_mcp_datadriven("tests/testdata/mcp/developer_disabled", harness);
}

/// Tests that MCP tool responses larger than `mcp_max_response_size` are
/// rejected with an error telling the agent to narrow the query, instead of
/// returning an unbounded payload.
#[mz_ore::test]
fn test_mcp_developer_response_size_limit() {
let harness = test_util::TestHarness::default()
.with_mcp_routes(false, true)
.with_system_parameter_default("enable_mcp_developer".to_string(), "true".to_string())
.with_system_parameter_default("mcp_max_response_size".to_string(), "1024".to_string());
run_mcp_datadriven("tests/testdata/mcp/developer_response_limit", harness);
}

/// Regression test for database-issues#11320.
///
/// The developer endpoint validator allows unqualified `mz_*` table names as
Expand Down
33 changes: 33 additions & 0 deletions src/environmentd/tests/testdata/mcp/agent
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,36 @@ mcp-agent
{"jsonrpc":"2.0","id":null,"method":"tools/list"}
----
200 OK

# =============================================================================
# HTTP method enforcement
# =============================================================================

# MCP spec 2025-11-25: GET on the POST-only endpoint returns 405.
mcp-agent get
----
405 Method Not Allowed

# =============================================================================
# Origin validation (DNS rebinding defense)
# =============================================================================

# Browser-style requests with an Origin header are rejected unless the origin
# is on the CORS allowlist (empty in this test harness). Requests without an
# Origin header (every other test in this file) are allowed.
mcp-agent origin
{"jsonrpc":"2.0","id":40,"method":"tools/list"}
----
403 Forbidden

# =============================================================================
# read_data_product - name validation
# =============================================================================

# A data product name that does not parse as a single object name (e.g. an
# SQL injection attempt) is rejected by validation, not interpolated into SQL.
mcp-agent
{"jsonrpc":"2.0","id":41,"method":"tools/call","params":{"name":"read_data_product","arguments":{"name":"users\"; DROP TABLE users; --"}}}
----
200 OK
{"jsonrpc":"2.0","id":41,"error":{"code":-32602,"message":"Query validation failed: Invalid data product name: users\"; DROP TABLE users; --. Expected a valid object name, e.g. '\"database\".\"schema\".\"name\"' or 'my_view'","data":{"error_type":"ValidationError"}}}
40 changes: 40 additions & 0 deletions src/environmentd/tests/testdata/mcp/developer
Original file line number Diff line number Diff line change
Expand Up @@ -233,3 +233,43 @@ mcp-developer
----
200 OK
{"jsonrpc":"2.0","id":54,"error":{"code":-32602,"message":"Query validation failed: Query references non-system tables: public.user_data. Only system catalog tables (mz_*, pg_catalog, information_schema) are allowed.","data":{"error_type":"ValidationError"}}}

# =============================================================================
# HTTP method enforcement and Origin validation
# =============================================================================

# MCP spec 2025-11-25: GET on the POST-only endpoint returns 405.
mcp-developer get
----
405 Method Not Allowed

# Browser-style requests with an Origin header are rejected unless the origin
# is on the CORS allowlist (empty in this test harness). Requests without an
# Origin header (every other test in this file) are allowed.
mcp-developer origin
{"jsonrpc":"2.0","id":60,"method":"tools/list"}
----
403 Forbidden

# =============================================================================
# query_system_catalog - mz_unsafe exclusion
# =============================================================================

# mz_unsafe is not on the system catalog allowlist even though it is a system
# schema, so references to it are rejected at validation.
mcp-developer
{"jsonrpc":"2.0","id":61,"method":"tools/call","params":{"name":"query_system_catalog","arguments":{"sql_query":"SELECT * FROM mz_unsafe.mz_sleep_forever"}}}
----
200 OK
{"jsonrpc":"2.0","id":61,"error":{"code":-32602,"message":"Query validation failed: Query references non-system tables: mz_unsafe.mz_sleep_forever. Only system catalog tables (mz_*, pg_catalog, information_schema) are allowed.","data":{"error_type":"ValidationError"}}}

# =============================================================================
# Null id notification
# =============================================================================

# Null id is treated as a notification by serde (Option<Value> deserializes
# null as None): 200 with no response body.
mcp-developer
{"jsonrpc":"2.0","id":null,"method":"tools/list"}
----
200 OK
26 changes: 26 additions & 0 deletions src/environmentd/tests/testdata/mcp/developer_query_tool
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,29 @@ mcp-developer
----
200 OK
{"jsonrpc":"2.0","id":50,"result":{"content":[{"type":"text","text":"[\n [\n \"materialize\"\n ]\n]"}],"isError":false}}

# =============================================================================
# query - cluster_replica validation
# =============================================================================

# Empty cluster_replica is rejected by validation before reaching the engine.
mcp-developer
{"jsonrpc":"2.0","id":60,"method":"tools/call","params":{"name":"query","arguments":{"cluster":"quickstart","cluster_replica":"","sql_query":"SELECT 1"}}}
----
200 OK
{"jsonrpc":"2.0","id":60,"error":{"code":-32602,"message":"Query validation failed: cluster_replica must not be empty or whitespace-only","data":{"error_type":"ValidationError"}}}

# Whitespace-only cluster_replica is rejected too.
mcp-developer
{"jsonrpc":"2.0","id":61,"method":"tools/call","params":{"name":"query","arguments":{"cluster":"quickstart","cluster_replica":" ","sql_query":"SELECT 1"}}}
----
200 OK
{"jsonrpc":"2.0","id":61,"error":{"code":-32602,"message":"Query validation failed: cluster_replica must not be empty or whitespace-only","data":{"error_type":"ValidationError"}}}

# A replica that does not exist on the target cluster fails at execution with
# an error naming the replica, so agents can self-correct.
mcp-developer
{"jsonrpc":"2.0","id":62,"method":"tools/call","params":{"name":"query","arguments":{"cluster":"quickstart","cluster_replica":"r99","sql_query":"SELECT 1"}}}
----
200 OK
{"jsonrpc":"2.0","id":62,"error":{"code":-32603,"message":"Query execution failed: cluster replica 'quickstart.r99' does not exist","data":{"error_type":"ExecutionError"}}}
28 changes: 28 additions & 0 deletions src/environmentd/tests/testdata/mcp/developer_response_limit
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright Materialize, Inc. and contributors. All rights reserved.
#
# Use of this software is governed by the Business Source License
# included in the LICENSE file at the root of this repository.
#
# As of the Change Date specified in that file, in accordance with
# the Business Source License, use of this software will be governed
# by the Apache License, Version 2.0.


# =============================================================================
# Response size limit (mcp_max_response_size = 1024 for this test)
# =============================================================================

# Small responses pass through untouched.
mcp-developer
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"query_system_catalog","arguments":{"sql_query":"SELECT 1"}}}
----
200 OK
{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Query validation failed: Query must reference at least one system catalog table","data":{"error_type":"ValidationError"}}}

# Responses over the limit are rejected with an error telling the agent to
# narrow its query. repeat() gives a deterministic response size.
mcp-developer
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"query_system_catalog","arguments":{"sql_query":"SELECT repeat('x', 5000) FROM mz_catalog.mz_databases WHERE name = 'materialize'"}}}
----
200 OK
{"jsonrpc":"2.0","id":2,"error":{"code":-32603,"message":"Query execution failed: Response size (5018 bytes) exceeds the 1024 byte limit. Use LIMIT or WHERE to narrow your query.","data":{"error_type":"ExecutionError"}}}
Loading