blob: f46cd5183503e6ccd972f5fdbedc2b87170692ec [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import json
import logging
import os
import sys
from pathlib import Path
import httpx2
import pytest
from mcp import Client, ClientSession, MCPError, StdioServerParameters
from mcp.client import advertise
from mcp.client.stdio import stdio_client
from mcp.types import (
ClientCapabilities,
GetPromptResult,
Prompt,
PromptMessage,
Resource,
SamplingCapability,
SubscriptionFilter,
SubscriptionsListenRequest,
SubscriptionsListenRequestParams,
SubscriptionsListenResult,
TextContent,
Tool,
)
from doris_mcp_server import __version__
from doris_mcp_server.http_transport import (
LEGACY_MCP_PATH,
DorisMCPHTTPTransport,
)
from doris_mcp_server.protocol import (
create_doris_mcp_server,
create_transport_security,
)
from doris_mcp_server.tools.doris_feature_matrix import (
EXPECTED_DOMAIN_CHILDREN,
)
from test.protocol.schema_validation_server import create_schema_validation_server
from test.protocol.stdio_capability_server import OneToolManager as ProfileToolManager
from test.protocol.tool_registry_server import create_registry_test_server
REQUIRED_EXTENSION = "io.apache.doris/read"
PROFILE_TOOL_NAMES = sorted(
[
"echo",
"get_sql_profile",
"monitor_data_freshness",
"analyze_data_access_patterns",
"analyze_columns",
"get_monitoring_metrics",
]
)
def test_transport_security_accepts_explicit_deployment_allowlists():
settings = create_transport_security(
"0.0.0.0",
allowed_hosts=["mcp.example.test", "mcp.example.test:*"],
allowed_origins=["https://client.example.test"],
)
assert settings.allowed_hosts == [
"mcp.example.test",
"mcp.example.test:*",
]
assert settings.allowed_origins == ["https://client.example.test"]
def test_transport_security_rejects_allow_all_host():
with pytest.raises(ValueError, match="must list deployment hosts explicitly"):
create_transport_security("0.0.0.0", allowed_hosts=["*"])
async def _unused_sampling_callback(context, params):
del context, params
raise AssertionError("The capability fixture must not issue sampling requests")
class StubResourcesManager:
async def list_resources(self) -> list[Resource]:
return [
Resource(
uri="doris://table/orders",
name="orders",
mime_type="application/json",
)
]
async def read_resource(self, uri: str) -> str:
if uri == "doris://table/missing":
return json.dumps(
{
"error": "Failed to read resource: Table missing does not exist",
"error_code": "RESOURCE_NOT_FOUND",
"uri": uri,
}
)
return json.dumps({"uri": uri, "columns": 3})
class StubToolsManager:
async def list_tools(self) -> list[Tool]:
return [
Tool(
name="echo",
description="Echo structured input.",
input_schema={
"type": "object",
"properties": {"value": {"type": "string"}},
"required": ["value"],
},
),
Tool(
name="fail",
description="Return a model-readable tool error.",
input_schema={"type": "object", "properties": {}},
),
]
async def call_tool(self, name: str, arguments: dict) -> str:
if name == "fail":
if arguments:
return json.dumps(
{
"error": (
f"query failed: password={arguments['password']}; "
f"token={arguments['token']}; {arguments['sql']}"
),
"arguments": arguments,
"token": arguments["token"],
}
)
return json.dumps({"error": "expected failure"})
return json.dumps({"name": name, "arguments": arguments})
class PromptFixtureError(Exception):
def __init__(
self,
message: str,
*,
error_code: str,
argument: str | None = None,
):
super().__init__(message)
self.error_code = error_code
self.argument = argument
class StubPromptsManager:
async def list_prompts(self) -> list[Prompt]:
return [Prompt(name="explain", description="Explain a query.")]
async def get_prompt(self, name: str, arguments: dict) -> GetPromptResult:
if name == "missing":
raise PromptFixtureError(
"Prompt not found",
error_code="UNKNOWN_PROMPT",
)
if name == "needs_argument" and "required" not in arguments:
raise PromptFixtureError(
"Missing required argument",
error_code="MISSING_REQUIRED_ARGUMENT",
argument="required",
)
if name == "database_failure":
raise PromptFixtureError(
"Database context failed",
error_code="DATABASE_CONTEXT_UNAVAILABLE",
)
return GetPromptResult(
description=name,
messages=[
PromptMessage(
role="user",
content=TextContent(
type="text",
text=f"Explain {arguments.get('sql', '')}",
),
)
],
)
def create_test_server(
required_client_capabilities: dict[str, ClientCapabilities] | None = None,
required_tool_capabilities: dict[str, ClientCapabilities] | None = None,
tools_manager=None,
):
return create_doris_mcp_server(
resources_manager=StubResourcesManager(),
tools_manager=tools_manager or StubToolsManager(),
prompts_manager=StubPromptsManager(),
name="doris-mcp-server",
version=__version__,
logger=logging.getLogger(__name__),
required_client_capabilities=required_client_capabilities,
required_tool_capabilities=required_tool_capabilities,
)
@pytest.mark.asyncio
async def test_modern_and_legacy_clients_share_the_v2_protocol_core():
server = create_test_server()
async with Client(server) as modern:
assert modern.protocol_version == "2026-07-28"
assert modern.server_info is not None
assert modern.server_info.name == "doris-mcp-server"
assert modern.server_info.version == __version__
assert modern.session.discover_result is not None
assert modern.session.discover_result.result_type == "complete"
tools_result = await modern.list_tools(cache_mode="bypass")
assert [tool.name for tool in tools_result.tools] == ["echo", "fail"]
assert tools_result.result_type == "complete"
assert tools_result.ttl_ms == 0
assert tools_result.cache_scope == "private"
resources_result = await modern.list_resources(cache_mode="bypass")
assert [resource.name for resource in resources_result.resources] == ["orders"]
assert resources_result.result_type == "complete"
assert resources_result.ttl_ms == 0
assert resources_result.cache_scope == "private"
prompts_result = await modern.list_prompts(cache_mode="bypass")
assert [prompt.name for prompt in prompts_result.prompts] == ["explain"]
assert prompts_result.result_type == "complete"
assert prompts_result.ttl_ms == 0
assert prompts_result.cache_scope == "private"
tool_result = await modern.call_tool("echo", {"value": "hello"})
assert tool_result.result_type == "complete"
assert tool_result.is_error is False
assert tool_result.structured_content == {
"name": "echo",
"arguments": {"value": "hello"},
}
error_result = await modern.call_tool("fail", {})
assert error_result.is_error is True
assert error_result.structured_content == {"error": "expected failure"}
resource_result = await modern.read_resource(
"doris://table/orders",
cache_mode="bypass",
)
assert resource_result.result_type == "complete"
assert resource_result.cache_scope == "private"
assert resource_result.contents[0].uri == "doris://table/orders"
prompt_result = await modern.get_prompt("explain", {"sql": "SELECT 1"})
assert prompt_result.result_type == "complete"
assert prompt_result.messages[0].content.text == "Explain SELECT 1"
async with Client(server, mode="legacy") as legacy:
assert legacy.protocol_version == "2025-11-25"
assert legacy.server_info is not None
assert legacy.server_info.name == "doris-mcp-server"
assert legacy.server_info.version == __version__
assert [tool.name for tool in (await legacy.list_tools()).tools] == [
"echo",
"fail",
]
def modern_request(request_id: int, method: str) -> dict:
return {
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {
"name": "doris-mcp-test",
"version": "1.0.0",
},
}
},
}
def modern_headers(method: str) -> dict[str, str]:
return {
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
"Mcp-Protocol-Version": "2026-07-28",
"Mcp-Method": method,
}
def modern_resource_request(request_id: int, uri: str) -> dict:
request = modern_request(request_id, "resources/read")
request["params"]["uri"] = uri
return request
def modern_resource_headers(uri: str) -> dict[str, str]:
return {
**modern_headers("resources/read"),
"Mcp-Name": uri,
}
def modern_prompt_request(
request_id: int,
name: str,
arguments: dict | None = None,
) -> dict:
request = modern_request(request_id, "prompts/get")
request["params"].update(
{
"name": name,
"arguments": arguments or {},
}
)
return request
def modern_prompt_headers(name: str) -> dict[str, str]:
return {
**modern_headers("prompts/get"),
"Mcp-Name": name,
}
def modern_tool_request(
request_id: int,
name: str,
arguments: dict | None = None,
) -> dict:
request = modern_request(request_id, "tools/call")
request["params"].update(
{
"name": name,
"arguments": arguments or {},
}
)
return request
def modern_tool_headers(name: str) -> dict[str, str]:
return {
**modern_headers("tools/call"),
"Mcp-Name": name,
}
@pytest.mark.asyncio
async def test_http_uses_production_domain_catalog_for_discovery_validation():
server = create_registry_test_server()
app = server.streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
listed = await client.post(
"/mcp",
json=modern_request(1, "tools/list"),
headers=modern_headers("tools/list"),
)
assert listed.status_code == 200
tools = {
tool["name"]: tool
for tool in listed.json()["result"]["tools"]
}
assert set(tools) == set(EXPECTED_DOMAIN_CHILDREN)
assert set(tools["doris_catalog"]["inputSchema"]["properties"]) == {
"child_tool",
"arguments",
"manifest_version",
}
assert "get_monitoring_metrics_info" not in tools
invalid = await client.post(
"/mcp",
json=modern_tool_request(
2,
"doris_catalog",
{"unexpected": True},
),
headers=modern_tool_headers("doris_catalog"),
)
assert invalid.status_code == 400
assert invalid.json()["error"]["code"] == -32602
called = await client.post(
"/mcp",
json=modern_tool_request(
3,
"doris_catalog",
{},
),
headers=modern_tool_headers("doris_catalog"),
)
assert called.status_code == 200
structured = called.json()["result"]["structuredContent"]
assert structured["mode"] == "manifest"
assert structured["domain"] == "doris_catalog"
assert len(structured["children"]) == 5
@pytest.mark.asyncio
async def test_http_discover_is_stateless_and_unknown_method_does_not_kill_server():
app = create_test_server().streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
first = await client.post(
"/mcp",
json=modern_request(1, "server/discover"),
headers=modern_headers("server/discover"),
)
assert first.status_code == 200
assert "mcp-session-id" not in first.headers
assert first.json()["result"]["supportedVersions"] == ["2026-07-28"]
discover_server_info = first.json()["result"]["_meta"][
"io.modelcontextprotocol/serverInfo"
]
assert discover_server_info["name"] == "doris-mcp-server"
assert discover_server_info["version"] == __version__
unknown = await client.post(
"/mcp",
json=modern_request(2, "vendor/unknown"),
headers=modern_headers("vendor/unknown"),
)
assert unknown.status_code == 404
assert unknown.json()["error"]["code"] == -32601
malformed = await client.post(
"/mcp",
content=b"{",
headers=modern_headers("server/discover"),
)
assert malformed.status_code == 400
assert malformed.json()["error"]["code"] == -32700
mismatched_header = await client.post(
"/mcp",
json=modern_request(3, "server/discover"),
headers=modern_headers("tools/list"),
)
assert mismatched_header.status_code == 400
assert mismatched_header.json()["error"]["code"] == -32020
unsupported_request = modern_request(4, "server/discover")
unsupported_request["params"]["_meta"][
"io.modelcontextprotocol/protocolVersion"
] = "2099-01-01"
unsupported_headers = modern_headers("server/discover")
unsupported_headers["Mcp-Protocol-Version"] = "2099-01-01"
unsupported_version = await client.post(
"/mcp",
json=unsupported_request,
headers=unsupported_headers,
)
assert unsupported_version.status_code == 400
assert unsupported_version.json()["error"]["code"] == -32022
second = await client.post(
"/mcp",
json=modern_request(5, "server/discover"),
headers=modern_headers("server/discover"),
)
assert second.status_code == 200
assert second.json()["result"]["resultType"] == "complete"
@pytest.mark.asyncio
async def test_http_does_not_advertise_or_serve_subscriptions_without_change_source():
server = create_test_server()
assert server.get_request_handler("subscriptions/listen") is None
app = server.streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
discovered = await client.post(
"/mcp",
json=modern_request(1, "server/discover"),
headers=modern_headers("server/discover"),
)
assert discovered.status_code == 200
capabilities = discovered.json()["result"]["capabilities"]
assert capabilities["tools"]["listChanged"] is False
assert capabilities["prompts"]["listChanged"] is False
assert capabilities["resources"] == {
"subscribe": False,
"listChanged": False,
}
listen_request = modern_request(2, "subscriptions/listen")
listen_request["params"]["notifications"] = {
"toolsListChanged": True,
"promptsListChanged": True,
"resourcesListChanged": True,
"resourceSubscriptions": ["doris://table/orders"],
}
rejected = await client.post(
"/mcp",
json=listen_request,
headers=modern_headers("subscriptions/listen"),
)
assert rejected.status_code == 404
assert rejected.json()["error"] == {
"code": -32601,
"message": "Method not found",
"data": "subscriptions/listen",
}
recovered = await client.post(
"/mcp",
json=modern_request(3, "server/discover"),
headers=modern_headers("server/discover"),
)
assert recovered.status_code == 200
assert recovered.json()["result"]["resultType"] == "complete"
@pytest.mark.asyncio
async def test_http_enforces_bounded_2020_12_tool_schemas_and_recovers():
server = create_schema_validation_server()
app = server.streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
listed = await client.post(
"/mcp",
json=modern_request(1, "tools/list"),
headers=modern_headers("tools/list"),
)
assert listed.status_code == 200
tools = {
tool["name"]: tool
for tool in listed.json()["result"]["tools"]
}
assert tools["schema_guard"]["inputSchema"]["$schema"].endswith(
"/2020-12/schema"
)
assert "oneOf" in tools["schema_guard"]["inputSchema"]["$defs"]["selector"]
secret = "must-not-echo-schema-secret"
rejected = await client.post(
"/mcp",
json=modern_tool_request(
2,
"schema_guard",
{"selector": {"id": secret}},
),
headers=modern_tool_headers("schema_guard"),
)
assert rejected.status_code == 400
assert rejected.json()["error"]["code"] == -32602
assert rejected.json()["error"]["message"] == (
"Tool arguments do not match input schema"
)
assert secret not in rejected.text
assert rejected.json()["error"]["data"]["violations"]
accepted = await client.post(
"/mcp",
json=modern_tool_request(
3,
"schema_guard",
{"selector": {"id": 7}},
),
headers=modern_tool_headers("schema_guard"),
)
assert accepted.status_code == 200
assert accepted.json()["result"]["structuredContent"] == {
"accepted": True
}
array_output = await client.post(
"/mcp",
json=modern_tool_request(4, "array_output", {}),
headers=modern_tool_headers("array_output"),
)
assert array_output.status_code == 200
assert array_output.json()["result"]["structuredContent"] == [1, 2, 3]
bad_output = await client.post(
"/mcp",
json=modern_tool_request(5, "bad_output", {}),
headers=modern_tool_headers("bad_output"),
)
assert bad_output.status_code == 200
assert bad_output.json()["error"] == {
"code": -32603,
"message": "Internal server error",
}
assert "not-a-boolean" not in bad_output.text
recovered = await client.post(
"/mcp",
json=modern_tool_request(6, "echo", {"value": "alive"}),
headers=modern_tool_headers("echo"),
)
assert recovered.status_code == 200
assert recovered.json()["result"]["structuredContent"] == {
"value": "alive"
}
@pytest.mark.asyncio
async def test_http_rejects_untrusted_origin_and_legacy_adapter_is_stateless():
app = DorisMCPHTTPTransport(
app=create_test_server(),
security_settings=create_transport_security("127.0.0.1"),
legacy_adapter_enabled=True,
)
legacy_initialize = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "legacy-test", "version": "1.0.0"},
},
}
legacy_headers = {
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
}
async with (
app.run(),
httpx2.ASGITransport(app.handle_request) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
rejected = await client.post(
"/mcp",
json=modern_request(1, "server/discover"),
headers={
**modern_headers("server/discover"),
"Origin": "https://evil.example",
},
)
assert rejected.status_code == 403
rejected_host = await client.post(
"/mcp",
json=modern_request(2, "server/discover"),
headers={
**modern_headers("server/discover"),
"Host": "evil.example",
},
)
assert rejected_host.status_code == 421
initialized = await client.post(
LEGACY_MCP_PATH,
json=legacy_initialize,
headers=legacy_headers,
)
assert initialized.status_code == 200
assert "mcp-session-id" not in initialized.headers
assert initialized.json()["result"]["protocolVersion"] == "2025-11-25"
legacy_server_info = initialized.json()["result"]["serverInfo"]
assert legacy_server_info["name"] == "doris-mcp-server"
assert legacy_server_info["version"] == __version__
@pytest.mark.asyncio
async def test_legacy_http_adapter_serves_2025_06_18_host_tools():
app = DorisMCPHTTPTransport(
app=create_test_server(),
security_settings=create_transport_security("127.0.0.1"),
legacy_adapter_enabled=True,
)
headers = {
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
}
initialize = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "dify", "version": "1.16.1"},
},
}
async with (
app.run(),
httpx2.ASGITransport(app.handle_request) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
initialized = await client.post(
LEGACY_MCP_PATH,
json=initialize,
headers=headers,
)
assert initialized.status_code == 200
assert initialized.json()["result"]["protocolVersion"] == "2025-06-18"
assert "mcp-session-id" not in initialized.headers
listed = await client.post(
LEGACY_MCP_PATH,
json={
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {},
},
headers={**headers, "MCP-Protocol-Version": "2025-06-18"},
)
assert listed.status_code == 200
assert [tool["name"] for tool in listed.json()["result"]["tools"]] == [
"echo",
"fail",
]
called = await client.post(
LEGACY_MCP_PATH,
json={
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {"value": "legacy"},
},
},
headers={**headers, "MCP-Protocol-Version": "2025-06-18"},
)
assert called.status_code == 200
assert called.json()["result"]["structuredContent"] == {
"name": "echo",
"arguments": {"value": "legacy"},
}
@pytest.mark.asyncio
async def test_http_validates_request_meta_and_required_client_capabilities():
server = create_test_server(
{
"tools/list": ClientCapabilities(
extensions={REQUIRED_EXTENSION: {}},
)
}
)
app = server.streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
missing_meta = modern_request(1, "tools/list")
del missing_meta["params"]["_meta"][
"io.modelcontextprotocol/clientCapabilities"
]
invalid = await client.post(
"/mcp",
json=missing_meta,
headers=modern_headers("tools/list"),
)
assert invalid.status_code == 400
assert invalid.json()["error"]["code"] == -32602
missing_capability = await client.post(
"/mcp",
json=modern_request(2, "tools/list"),
headers=modern_headers("tools/list"),
)
assert missing_capability.status_code == 400
assert missing_capability.json()["error"]["code"] == -32021
assert missing_capability.json()["error"]["data"] == {
"requiredCapabilities": {
"extensions": {
REQUIRED_EXTENSION: {},
}
}
}
capable_request = modern_request(3, "tools/list")
capable_request["params"]["_meta"][
"io.modelcontextprotocol/clientCapabilities"
] = {
"extensions": {
REQUIRED_EXTENSION: {},
}
}
capable = await client.post(
"/mcp",
json=capable_request,
headers=modern_headers("tools/list"),
)
assert capable.status_code == 200
assert [tool["name"] for tool in capable.json()["result"]["tools"]] == [
"echo",
"fail",
]
@pytest.mark.asyncio
async def test_http_enforces_tool_specific_client_capabilities_and_recovers():
server = create_test_server(
required_tool_capabilities={
"echo": ClientCapabilities(sampling=SamplingCapability()),
}
)
app = server.streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
missing = await client.post(
"/mcp",
json=modern_tool_request(1, "echo", {"value": "missing"}),
headers=modern_tool_headers("echo"),
)
assert missing.status_code == 400
assert missing.json()["error"] == {
"code": -32021,
"message": "Missing required client capability",
"data": {"requiredCapabilities": {"sampling": {}}},
}
capable_request = modern_tool_request(2, "echo", {"value": "capable"})
capable_request["params"]["_meta"][
"io.modelcontextprotocol/clientCapabilities"
] = {"sampling": {}}
capable = await client.post(
"/mcp",
json=capable_request,
headers=modern_tool_headers("echo"),
)
assert capable.status_code == 200
assert capable.json()["result"]["structuredContent"] == {
"name": "echo",
"arguments": {"value": "capable"},
}
secret = "http-secret-sec-016"
recovered = await client.post(
"/mcp",
json=modern_tool_request(
3,
"fail",
{
"password": secret,
"token": secret,
"sql": f"SELECT '{secret}'",
},
),
headers=modern_tool_headers("fail"),
)
assert recovered.status_code == 200
assert recovered.json()["result"]["isError"] is True
assert secret not in recovered.text
structured = recovered.json()["result"]["structuredContent"]
assert "arguments" not in structured
assert structured["token"] == "[REDACTED]"
@pytest.mark.asyncio
async def test_http_resource_not_found_is_invalid_params_and_server_recovers():
app = create_test_server().streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
missing_uri = "doris://table/missing"
valid_uri = "doris://table/orders"
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
missing = await client.post(
"/mcp",
json=modern_resource_request(1, missing_uri),
headers=modern_resource_headers(missing_uri),
)
assert missing.status_code == 400
assert missing.json()["error"] == {
"code": -32602,
"message": "Resource not found",
"data": {
"uri": missing_uri,
"resourceErrorCode": "RESOURCE_NOT_FOUND",
},
}
recovered = await client.post(
"/mcp",
json=modern_resource_request(2, valid_uri),
headers=modern_resource_headers(valid_uri),
)
assert recovered.status_code == 200
assert recovered.json()["result"]["contents"][0]["uri"] == valid_uri
@pytest.mark.asyncio
async def test_http_prompt_errors_are_typed_and_server_recovers():
app = create_test_server().streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
unknown = await client.post(
"/mcp",
json=modern_prompt_request(1, "missing"),
headers=modern_prompt_headers("missing"),
)
assert unknown.status_code == 400
assert unknown.json()["error"] == {
"code": -32602,
"message": "Prompt not found",
"data": {
"name": "missing",
"promptErrorCode": "UNKNOWN_PROMPT",
},
}
missing_argument = await client.post(
"/mcp",
json=modern_prompt_request(2, "needs_argument"),
headers=modern_prompt_headers("needs_argument"),
)
assert missing_argument.status_code == 400
assert missing_argument.json()["error"] == {
"code": -32602,
"message": "Missing required prompt argument",
"data": {
"name": "needs_argument",
"promptErrorCode": "MISSING_REQUIRED_ARGUMENT",
"argument": "required",
},
}
database_failure = await client.post(
"/mcp",
json=modern_prompt_request(3, "database_failure"),
headers=modern_prompt_headers("database_failure"),
)
assert database_failure.status_code == 200
assert database_failure.json()["error"] == {
"code": -32603,
"message": "Database context unavailable",
"data": {
"name": "database_failure",
"promptErrorCode": "DATABASE_CONTEXT_UNAVAILABLE",
},
}
recovered = await client.post(
"/mcp",
json=modern_prompt_request(4, "explain", {"sql": "SELECT 1"}),
headers=modern_prompt_headers("explain"),
)
assert recovered.status_code == 200
assert recovered.json()["result"]["messages"][0]["content"]["text"] == (
"Explain SELECT 1"
)
@pytest.mark.asyncio
async def test_http_sql_profile_without_catalog_uses_production_analyzer_path(
tmp_path,
):
app = create_test_server(
tools_manager=ProfileToolManager(str(tmp_path)),
).streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
result = await client.post(
"/mcp",
json=modern_tool_request(
1,
"get_sql_profile",
{"sql": "SELECT 1", "db_name": "hhm_dt_sim"},
),
headers=modern_tool_headers("get_sql_profile"),
)
assert result.status_code == 200
assert result.json()["result"]["isError"] is False
assert result.json()["result"]["structuredContent"]["success"] is True
assert result.json()["result"]["structuredContent"]["query_id"] == "query-1"
recovered = await client.post(
"/mcp",
json=modern_tool_request(2, "echo", {}),
headers=modern_tool_headers("echo"),
)
assert recovered.status_code == 200
assert recovered.json()["result"]["isError"] is False
@pytest.mark.asyncio
async def test_http_unknown_freshness_uses_default_threshold(tmp_path):
app = create_test_server(
tools_manager=ProfileToolManager(str(tmp_path)),
).streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
result = await client.post(
"/mcp",
json=modern_tool_request(
1,
"monitor_data_freshness",
{"table_names": ["org_tenant"]},
),
headers=modern_tool_headers("monitor_data_freshness"),
)
assert result.status_code == 200
payload = result.json()["result"]["structuredContent"]
assert result.json()["result"]["isError"] is False
assert "error" not in payload
assert payload["monitoring_scope"]["time_threshold_hours"] == 24
assert payload["table_freshness"]["org_tenant"]["status"] == "unknown"
@pytest.mark.asyncio
async def test_http_doris4_role_metadata_uses_public_grants_command(tmp_path):
app = create_test_server(
tools_manager=ProfileToolManager(str(tmp_path)),
).streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
result = await client.post(
"/mcp",
json=modern_tool_request(1, "analyze_data_access_patterns", {}),
headers=modern_tool_headers("analyze_data_access_patterns"),
)
assert result.status_code == 200
assert result.json()["result"]["isError"] is False
roles = result.json()["result"]["structuredContent"]["role_analysis"]
assert list(roles) == ["operator"]
assert roles["operator"]["users"] == ["root"]
@pytest.mark.asyncio
async def test_http_rejects_injected_sql_identifier_and_recovers(tmp_path):
app = create_test_server(
tools_manager=ProfileToolManager(str(tmp_path)),
).streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
rejected = await client.post(
"/mcp",
json=modern_tool_request(
1,
"analyze_columns",
{
"table_name": "orders; DROP TABLE orders",
"columns": ["id"],
"db_name": "hhm_dt_sim",
},
),
headers=modern_tool_headers("analyze_columns"),
)
assert rejected.status_code == 200
assert rejected.json()["result"]["isError"] is True
assert "Invalid table name" in (
rejected.json()["result"]["structuredContent"]["error"]
)
recovered = await client.post(
"/mcp",
json=modern_tool_request(2, "echo", {}),
headers=modern_tool_headers("echo"),
)
assert recovered.status_code == 200
assert recovered.json()["result"]["isError"] is False
@pytest.mark.asyncio
async def test_http_monitoring_rejects_metadata_endpoint_and_recovers(tmp_path):
app = create_test_server(
tools_manager=ProfileToolManager(str(tmp_path)),
).streamable_http_app(
json_response=True,
stateless_http=True,
host="127.0.0.1",
transport_security=create_transport_security("127.0.0.1"),
)
async with (
app.router.lifespan_context(app),
httpx2.ASGITransport(app) as transport,
httpx2.AsyncClient(
transport=transport,
base_url="http://127.0.0.1:3000",
) as client,
):
rejected = await client.post(
"/mcp",
json=modern_tool_request(
1,
"get_monitoring_metrics",
{"content_type": "data", "role": "fe"},
),
headers=modern_tool_headers("get_monitoring_metrics"),
)
assert rejected.status_code == 200
assert rejected.json()["result"]["isError"] is False
fe_result = rejected.json()["result"]["structuredContent"]["data"]["fe"]
assert fe_result["success"] is False
assert fe_result["error_type"] == "prohibited_endpoint"
assert fe_result["node_info"]["host"] == "169.254.169.254"
recovered = await client.post(
"/mcp",
json=modern_tool_request(2, "echo", {}),
headers=modern_tool_headers("echo"),
)
assert recovered.status_code == 200
assert recovered.json()["result"]["isError"] is False
@pytest.mark.asyncio
async def test_stdio_validates_capabilities_versions_and_process_survival():
server_script = Path(__file__).with_name("stdio_capability_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with stdio_client(server_params) as streams:
async with ClientSession(*streams) as raw_session:
with pytest.raises(MCPError) as unsupported:
await raw_session.send_discover("2099-01-01")
assert unsupported.value.code == -32022
assert unsupported.value.data == {
"supported": ["2026-07-28"],
"requested": "2099-01-01",
}
recovered = await raw_session.send_discover("2026-07-28")
assert recovered["resultType"] == "complete"
async with Client(stdio_client(server_params)) as missing:
with pytest.raises(MCPError) as missing_capability:
await missing.list_tools(cache_mode="bypass")
assert missing_capability.value.code == -32021
assert (await missing.list_resources(cache_mode="bypass")).resources == []
with pytest.raises(MCPError) as missing_resource:
await missing.read_resource(
"doris://table/missing",
cache_mode="bypass",
)
assert missing_resource.value.code == -32602
assert missing_resource.value.data == {
"uri": "doris://table/missing",
"resourceErrorCode": "RESOURCE_NOT_FOUND",
}
recovered_resource = await missing.read_resource(
"doris://table/orders",
cache_mode="bypass",
)
assert recovered_resource.contents[0].uri == "doris://table/orders"
async with Client(
stdio_client(server_params),
extensions=[advertise(REQUIRED_EXTENSION)],
) as capable:
assert [
tool.name for tool in (await capable.list_tools()).tools
] == PROFILE_TOOL_NAMES
secret = "stdio-secret-sec-016"
error_result = await capable.call_tool(
"echo",
{
"fail": True,
"password": secret,
"token": secret,
"sql": f"SELECT '{secret}'",
},
)
assert error_result.is_error is True
serialized = json.dumps(
error_result.model_dump(by_alias=True, mode="json"),
ensure_ascii=False,
)
assert secret not in serialized
assert "arguments" not in error_result.structured_content
assert error_result.structured_content["token"] == "[REDACTED]"
async with Client(stdio_client(server_params), mode="legacy") as legacy:
assert [
tool.name for tool in (await legacy.list_tools()).tools
] == PROFILE_TOOL_NAMES
legacy_error = await legacy.read_resource("doris://table/missing")
assert (
json.loads(legacy_error.contents[0].text)["error_code"]
== "RESOURCE_NOT_FOUND"
)
@pytest.mark.asyncio
async def test_true_subprocess_stdio_does_not_advertise_or_serve_subscriptions():
server_script = Path(__file__).with_name("stdio_capability_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(stdio_client(server_params)) as modern:
capabilities = modern.server_capabilities
assert capabilities is not None
assert capabilities.tools is not None
assert capabilities.tools.list_changed is False
assert capabilities.prompts is not None
assert capabilities.prompts.list_changed is False
assert capabilities.resources is not None
assert capabilities.resources.list_changed is False
assert capabilities.resources.subscribe is False
listen_request = SubscriptionsListenRequest(
params=SubscriptionsListenRequestParams(
notifications=SubscriptionFilter(
tools_list_changed=True,
prompts_list_changed=True,
resources_list_changed=True,
resource_subscriptions=["doris://table/orders"],
)
)
)
with pytest.raises(MCPError) as unsupported:
await modern.session.send_request(
listen_request,
SubscriptionsListenResult,
request_read_timeout_seconds=1,
)
assert unsupported.value.code == -32601
assert unsupported.value.message == "Method not found"
recovered = await modern.list_resources(cache_mode="bypass")
assert recovered.result_type == "complete"
@pytest.mark.asyncio
async def test_true_subprocess_stdio_uses_production_domain_catalog():
server_script = Path(__file__).with_name("tool_registry_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(stdio_client(server_params)) as modern:
tools = {
tool.name: tool
for tool in (await modern.list_tools(cache_mode="bypass")).tools
}
assert set(tools) == set(EXPECTED_DOMAIN_CHILDREN)
assert set(tools["doris_catalog"].input_schema["properties"]) == {
"child_tool",
"arguments",
"manifest_version",
}
assert "get_monitoring_metrics_info" not in tools
with pytest.raises(MCPError) as invalid:
await modern.call_tool(
"doris_catalog",
{"unexpected": True},
)
assert invalid.value.code == -32602
called = await modern.call_tool("doris_catalog", {})
assert called.structured_content["mode"] == "manifest"
assert called.structured_content["domain"] == "doris_catalog"
query_manifest = await modern.call_tool("doris_query", {})
executed = await modern.call_tool(
"doris_query",
{
"child_tool": "execute_query",
"arguments": {"sql": "SELECT 1"},
"manifest_version": query_manifest.structured_content[
"manifest_version"
],
},
)
assert executed.is_error is False
assert executed.structured_content["mode"] == "result"
assert executed.structured_content["data"]["data"]["rows"] == [
{
"registry_dispatch": True,
"sql_length": 8,
}
]
with pytest.raises(MCPError) as old_name:
await modern.call_tool("exec_query", {"sql": "SELECT 1"})
assert old_name.value.code == -32602
assert old_name.value.message == "Tool not found"
assert old_name.value.data == {
"name": "exec_query",
"toolErrorCode": "TOOL_NOT_FOUND",
}
recovered = await modern.call_tool("doris_query", {})
assert recovered.structured_content["mode"] == "manifest"
async with Client(stdio_client(server_params), mode="legacy") as legacy:
tools = {
tool.name: tool
for tool in (await legacy.list_tools()).tools
}
assert set(tools) == set(EXPECTED_DOMAIN_CHILDREN)
called = await legacy.call_tool("doris_catalog", {})
assert called.structured_content["mode"] == "manifest"
assert called.structured_content["domain"] == "doris_catalog"
@pytest.mark.asyncio
async def test_true_subprocess_stdio_formal_flat_mode_executes_and_recovers():
server_script = Path(__file__).with_name("tool_registry_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
env={
**os.environ,
"MCP_TOOL_EXPOSURE_MODE": "flat",
},
)
async with Client(stdio_client(server_params)) as modern:
tools = {
tool.name: tool
for tool in (await modern.list_tools(cache_mode="bypass")).tools
}
assert len(tools) == 55
assert "doris_query_execute_query" in tools
assert not set(EXPECTED_DOMAIN_CHILDREN).intersection(tools)
assert "exec_query" not in tools
executed = await modern.call_tool(
"doris_query_execute_query",
{"sql": "SELECT 1"},
)
assert executed.is_error is False
assert executed.structured_content["domain"] == "doris_query"
assert executed.structured_content["child_tool"] == "execute_query"
with pytest.raises(MCPError) as old_name:
await modern.call_tool("exec_query", {"sql": "SELECT 1"})
assert old_name.value.message == "Tool not found"
recovered = await modern.call_tool(
"doris_query_execute_query",
{"sql": "SELECT 2"},
)
assert recovered.is_error is False
@pytest.mark.asyncio
async def test_true_subprocess_stdio_enforces_tool_schemas_for_both_eras():
server_script = Path(__file__).with_name("schema_validation_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(stdio_client(server_params)) as modern:
tools = {
tool.name: tool
for tool in (await modern.list_tools(cache_mode="bypass")).tools
}
assert tools["schema_guard"].input_schema["$schema"].endswith(
"/2020-12/schema"
)
secret = "must-not-echo-stdio-schema-secret"
with pytest.raises(MCPError) as invalid:
await modern.call_tool(
"schema_guard",
{"selector": {"id": secret}},
)
assert invalid.value.code == -32602
assert invalid.value.message == "Tool arguments do not match input schema"
assert secret not in repr(invalid.value.data)
accepted = await modern.call_tool(
"schema_guard",
{"selector": {"name": "orders"}},
)
assert accepted.structured_content == {"accepted": True}
array_output = await modern.call_tool("array_output", {})
assert array_output.structured_content == [1, 2, 3]
with pytest.raises(MCPError) as bad_output:
await modern.call_tool("bad_output", {})
assert bad_output.value.code == -32603
assert bad_output.value.message == "Internal server error"
recovered = await modern.call_tool("echo", {"value": "alive"})
assert recovered.structured_content == {"value": "alive"}
async with Client(stdio_client(server_params), mode="legacy") as legacy:
legacy_tools = {
tool.name: tool
for tool in (await legacy.list_tools()).tools
}
assert legacy_tools["array_output"].output_schema is None
with pytest.raises(MCPError) as invalid:
await legacy.call_tool(
"schema_guard",
{"selector": {"id": "wrong-type"}},
)
assert invalid.value.code == -32602
accepted = await legacy.call_tool(
"schema_guard",
{"selector": {"id": 9}},
)
assert accepted.structured_content == {"accepted": True}
@pytest.mark.asyncio
async def test_stdio_enforces_tool_specific_client_capabilities_and_recovers():
server_script = Path(__file__).with_name("conformance_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script), "--transport", "stdio"],
)
async with Client(stdio_client(server_params)) as missing:
assert [tool.name for tool in (await missing.list_tools()).tools] == [
"test_missing_capability"
]
with pytest.raises(MCPError) as missing_capability:
await missing.call_tool("test_missing_capability", {})
assert missing_capability.value.code == -32021
assert missing_capability.value.data == {
"requiredCapabilities": {"sampling": {}}
}
assert [tool.name for tool in (await missing.list_tools()).tools] == [
"test_missing_capability"
]
async with Client(
stdio_client(server_params),
sampling_callback=_unused_sampling_callback,
sampling_capabilities=SamplingCapability(),
) as capable:
result = await capable.call_tool("test_missing_capability", {})
assert result.is_error is False
assert result.structured_content == {
"ok": True,
"tool": "test_missing_capability",
}
async with Client(stdio_client(server_params), mode="legacy") as legacy:
result = await legacy.call_tool("test_missing_capability", {})
assert json.loads(result.content[0].text) == {
"ok": True,
"tool": "test_missing_capability",
}
@pytest.mark.asyncio
async def test_stdio_sql_profile_without_catalog_uses_production_analyzer_path():
server_script = Path(__file__).with_name("stdio_capability_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(
stdio_client(server_params),
extensions=[advertise(REQUIRED_EXTENSION)],
) as modern:
result = await modern.call_tool(
"get_sql_profile",
{"sql": "SELECT 1", "db_name": "hhm_dt_sim"},
)
assert result.is_error is False
assert result.structured_content["success"] is True
assert result.structured_content["query_id"] == "query-1"
recovered = await modern.call_tool("echo", {})
assert recovered.is_error is False
async with Client(stdio_client(server_params), mode="legacy") as legacy:
result = await legacy.call_tool(
"get_sql_profile",
{"sql": "SELECT 1", "db_name": "hhm_dt_sim"},
)
assert json.loads(result.content[0].text)["success"] is True
@pytest.mark.asyncio
async def test_stdio_unknown_freshness_uses_default_threshold():
server_script = Path(__file__).with_name("stdio_capability_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(
stdio_client(server_params),
extensions=[advertise(REQUIRED_EXTENSION)],
) as modern:
result = await modern.call_tool(
"monitor_data_freshness",
{"table_names": ["org_tenant"]},
)
payload = result.structured_content
assert result.is_error is False
assert "error" not in payload
assert payload["monitoring_scope"]["time_threshold_hours"] == 24
async with Client(stdio_client(server_params), mode="legacy") as legacy:
result = await legacy.call_tool(
"monitor_data_freshness",
{"table_names": ["org_tenant"]},
)
payload = json.loads(result.content[0].text)
assert "error" not in payload
assert payload["monitoring_scope"]["time_threshold_hours"] == 24
@pytest.mark.asyncio
async def test_stdio_doris4_role_metadata_uses_public_grants_command():
server_script = Path(__file__).with_name("stdio_capability_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(
stdio_client(server_params),
extensions=[advertise(REQUIRED_EXTENSION)],
) as modern:
result = await modern.call_tool("analyze_data_access_patterns", {})
assert result.is_error is False
roles = result.structured_content["role_analysis"]
assert list(roles) == ["operator"]
async with Client(stdio_client(server_params), mode="legacy") as legacy:
result = await legacy.call_tool("analyze_data_access_patterns", {})
roles = json.loads(result.content[0].text)["role_analysis"]
assert list(roles) == ["operator"]
@pytest.mark.asyncio
async def test_stdio_rejects_injected_sql_identifier_and_recovers():
server_script = Path(__file__).with_name("stdio_capability_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(
stdio_client(server_params),
extensions=[advertise(REQUIRED_EXTENSION)],
) as modern:
rejected = await modern.call_tool(
"analyze_columns",
{
"table_name": "orders; DROP TABLE orders",
"columns": ["id"],
"db_name": "hhm_dt_sim",
},
)
assert rejected.is_error is True
assert "Invalid table name" in rejected.structured_content["error"]
recovered = await modern.call_tool("echo", {})
assert recovered.is_error is False
@pytest.mark.asyncio
async def test_stdio_monitoring_rejects_metadata_endpoint_and_recovers():
server_script = Path(__file__).with_name("stdio_capability_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(
stdio_client(server_params),
extensions=[advertise(REQUIRED_EXTENSION)],
) as modern:
rejected = await modern.call_tool(
"get_monitoring_metrics",
{"content_type": "data", "role": "fe"},
)
assert rejected.is_error is False
fe_result = rejected.structured_content["data"]["fe"]
assert fe_result["success"] is False
assert fe_result["error_type"] == "prohibited_endpoint"
assert fe_result["node_info"]["host"] == "169.254.169.254"
recovered = await modern.call_tool("echo", {})
assert recovered.is_error is False
@pytest.mark.asyncio
async def test_stdio_prompt_errors_are_typed_and_process_survives():
server_script = Path(__file__).with_name("stdio_capability_server.py")
server_params = StdioServerParameters(
command=sys.executable,
args=[str(server_script)],
)
async with Client(stdio_client(server_params)) as modern:
with pytest.raises(MCPError) as unknown:
await modern.get_prompt("missing", {})
assert unknown.value.code == -32602
assert unknown.value.data == {
"name": "missing",
"promptErrorCode": "UNKNOWN_PROMPT",
}
with pytest.raises(MCPError) as missing_argument:
await modern.get_prompt("needs_argument", {})
assert missing_argument.value.code == -32602
assert missing_argument.value.data == {
"name": "needs_argument",
"promptErrorCode": "MISSING_REQUIRED_ARGUMENT",
"argument": "required",
}
with pytest.raises(MCPError) as database_failure:
await modern.get_prompt("database_failure", {})
assert database_failure.value.code == -32603
assert database_failure.value.data == {
"name": "database_failure",
"promptErrorCode": "DATABASE_CONTEXT_UNAVAILABLE",
}
recovered = await modern.get_prompt("explain", {"sql": "SELECT 1"})
assert recovered.messages[0].content.text == "Explain SELECT 1"
async with Client(stdio_client(server_params), mode="legacy") as legacy:
with pytest.raises(MCPError) as legacy_unknown:
await legacy.get_prompt("missing", {})
assert legacy_unknown.value.code == -32602