-
-
็ฎๅฝ
-
+
-
-ไฝ ๆฏ Doris ็็จๆทใ็ฎก็ๅๅทฒ็ปไธบไฝ ้จ็ฝฒๅฅฝไบ MCP Server๏ผไฝ ๅช้่ฆ็ฅ้๏ผ
-1. ๅฆไฝๆๆๅกๆฅๅ
ฅ AI Agent๏ผClaude Code / Codex๏ผ
-2. ๅฆไฝไฝฟ็จ CLI ็ฎก็่ฏญไนๆจกๅ
-3. ๅฆไฝ้่ฟ AI ๅฏน่ฏๆฅ่ฏขๆฐๆฎ
-
-
-
1. ๆฅๅ
ฅ AI Agent๏ผClaude Code / Cursor / Codex๏ผ
+
1. Connect an AI Agent
+
Ask your administrator for the MCP Server URL, your Doris username/password, and the workspace name that contains semantic models. The default workspace for the bundled sample is example.
-
-
๐ ๆฅๅ
ฅๅๅๅค โ ไฝ ้่ฆไป็ฎก็ๅ้ฃ้่ทๅ๏ผ
-
- - MCP Server ๅฐๅ๏ผ
http://<IP>:<็ซฏๅฃ>๏ผ้ป่ฎค็ซฏๅฃ 3000๏ผ
- - Doris ่ดฆๅทๅฏ็ ๏ผ็จไบ่ฎค่ฏ๏ผๆ ผๅผ
็จๆทๅ:ๅฏ็ ๏ผๅฆ admin:admin๏ผ
- - Workspace ๅ็งฐ๏ผ่ฏญไนๆจกๅๆๅจ็ๅทฅไฝๅบ๏ผ้ป่ฎค
example๏ผ
-
-
-
-
1.1 ๆฅๅ
ฅ Claude Code
-
ๆญฅ้ชค 1๏ผๆทปๅ MCP Server
-
claude mcp add --transport http doris http://<MCP-Server-ๅฐๅ>:<็ซฏๅฃ>/mcp \
- --header "Authorization: Bearer admin:admin"
-
-
ๆญฅ้ชค 2๏ผ้ช่ฏ่ฟๆฅ
-
ๅจ Claude Code ๅฏน่ฏไธญ่พๅ
ฅ๏ผ
-
-ๆฃๆฅๆๅกๅฅๅบท็ถๆ
-
-
AI ไผ่ชๅจ่ฐ็จ check_service_health๏ผ่ฟๅ Doris ่ฟๆฅ็ถๆๅๆๆ workspace ็ถๆใๅฆๆ็ๅฐ "doris": "connected" ่ฏดๆๆฅๅ
ฅๆๅใ
+
Claude Desktop / Claude Code
+
claude mcp add --transport http doris http://<host>:3000/mcp \
+ --header "Authorization: Bearer <user>:<password>"
+
-
ๆญฅ้ชค 3๏ผๅผๅงๆฅ่ฏข
-
-ๅๅบๆๆๅฏ็จๆๆ
-
-
AI ไผ่ชๅจๆไธๆญฅๆต็จๆไฝ๏ผlist_metrics โ list_dimensions_for_metric โ query_metricใ
-
-
1.2 ๆฅๅ
ฅ Cursor / VS Code
-
ๅจ้กน็ฎๆ น็ฎๅฝๅๅปบ .cursor/mcp.json ๆ VS Code ็ MCP ้
็ฝฎ๏ผ
+
Cursor / VS Code
{
"mcpServers": {
"doris": {
- "url": "http://<MCP-Server-ๅฐๅ>:<็ซฏๅฃ>/mcp",
- "transport": "streamable-http",
+ "type": "http",
+ "url": "http://<host>:3000/mcp",
"headers": {
- "Authorization": "Bearer admin:admin"
+ "Authorization": "Bearer <user>:<password>"
}
}
}
-}
-
-
1.3 ๅฏ็จ Tool ไธ่ง๏ผๅ
ฑ 10 ไธช๏ผ
-
-
๐ข ๅฏๅจ & ๅฅๅบทๆฃๆฅ๏ผAgent ๅฏน่ฏๅผๅงๆถๅ
่ฐ่ฟ้๏ผ
-
-| Tool | ๅๆฐ | ็จ้ |
-
-
- get_query_guide |
- โ |
- ็ฌฌไธๆญฅๅฟ
่ฐใ่ฟๅๅฎๆดๅทฅไฝๆตๆๅผ๏ผไฝๆถ็จ่ฏญไนๅฑ vs ่ฃธ SQLใๅทฅๅ
ท่ฐ็จ้กบๅบใๆฅ่ฏข่ฏญๆณใไธ่ฐๆญคๅทฅๅ
ท Agent ไผ่ตฐๅผฏ่ทฏใ |
-
-
- check_service_health |
- detail(ๅฏ้) |
- ่ฟๅ Doris ่ฟ้ๆง + ๆฏไธช workspace ็็ถๆ๏ผhealthy / no_models / not_ready๏ผๅๆๆ ๆฐ้ใ้ฆๆฌกๅฏน่ฏๅฟ
่ฐใ |
-
-
-
-
-
๐ ่ฏญไนๆฅ่ฏข๏ผๆจ่ไผๅ
ไฝฟ็จ๏ผ่ชๅจ็ๆๆญฃ็กฎ SQL๏ผ
-
-| Tool | ๅๆฐ | ็จ้ |
-
-
- list_metrics |
- workspace(ๅฟ
ๅกซ), page_size, page_token |
- ๅๅบๆๆๅฏ็จๆๆ ๏ผname + description๏ผใAgent ๆ นๆฎ็จๆทๆๅพๅน้
ๆๆ ใ |
-
-
- list_dimensions_for_metric |
- workspace(ๅฟ
ๅกซ), metric_name(ๅฟ
ๅกซ) |
- ่ฟๅๆๆ ็ๅฏ็จ group_by ็ปดๅบฆ๏ผname + type + description๏ผใ็กฎไฟ group_by ไธๆฅ้ใ |
-
-
- query_metric |
- workspace(ๅฟ
ๅกซ), metrics(ๅฟ
ๅกซ), group_by, where, order_by, limit, having, database, max_rows |
- ๆ ธๅฟๆฅ่ฏขๅทฅๅ
ทใ็ผ่ฏ + ๆง่ก่ฏญไนๆฅ่ฏขใๆถๅ่ฎกๆฐ/ๆฑๅ/ๆฏ็/ๆๅ/่ถๅฟ้ฝๅฟ
้กป่ตฐ่ฟ้ใๆฏๆ HAVING ่ฟๆปค่ๅ็ปๆใdatabase ๆๅฎ็ฎๆ ๅบ๏ผmax_rows ็กฌ้ๅถ่ฟๅ่กๆฐ๏ผ0=้ป่ฎค10000๏ผใ |
-
-
- reload_semantic_layer |
- workspace(ๅฟ
ๅกซ) |
- ๆๅจ่งฆๅ่ฏญไนๅฑ้่ฝฝใ็ฎก็ๅไฟฎๆน YAML ๆจกๅๅนถ commit ๅๅฆ้็ซๅณ็ๆๅฏ่ฐ็จ๏ผๅฆๅ 60s ่ชๅจ่ฝฎ่ฏขใ |
-
-
-
-
-
๐ ๅบๅฑๅ
ๆฐๆฎ & ่ฃธ SQL๏ผไป
ๅจไปฅไธไธค็งๅบๆฏไฝฟ็จ๏ผ
-
-
-
โ ๏ธ ไธค็ง่ฃธ SQL ๅบๆฏ๏ผ
-
- - ๅบๆฏ A โ ่ฏญไนๅฑไธๅฏ็จ๏ผ
check_service_health ่ฟๅๆๆ workspace ้ฝไธๆฏ healthy๏ผno_models / not_ready๏ผใๆญคๆถๆๆๆฅ่ฏขๅช่ฝ่ตฐ list_databases โ list_tables โ describe_table โ execute_queryใ
- - ๅบๆฏ B โ ๆ ๅน้
ๆๆ ๏ผ่ฏญไนๅฑ healthy๏ผไฝ
list_metrics ไธญๆฒกๆไปปไฝๆๆ ่ฝๅน้
็จๆทๆๅพใๆญคๆถๅๆ ทๅ้ๅฐ่ฃธ SQL ่ทฏๅพใ
-
-
-
-
-| Tool | ๅๆฐ | ็จ้ |
-
-
- list_databases |
- page_size, page_token |
- ๅๅบๆๆๆฐๆฎๅบใๆข็ดข้ถๆฎตๅฏ็จ๏ผไฝไธ่ฝๆฟไปฃ list_metricsใ |
-
-
- list_tables |
- database(ๅฟ
ๅกซ), like, page_size, page_token |
- ๅๅบๅบไธญ็่กจๅใไป
่ฟๅ่กจๅ๏ผไธๅซๅไฟกๆฏใๆณ็ๅ็จ describe_tableใ |
-
-
- describe_table |
- database(ๅฟ
ๅกซ), table(ๅฟ
ๅกซ), detail_level |
- ๆฅ็็ฉ็่กจ็ปๆ๏ผๅๅใ็ฑปๅ๏ผใdetail_level: names/summary/fullใ |
-
-
- execute_query |
- sql(ๅฟ
ๅกซ), database, max_rows |
- ่ฃธ SQL ๅ
ๅบใไป
ๅ
่ฎธๅช่ฏป๏ผSELECT/SHOW/DESCRIBE/EXPLAIN๏ผใๅฆๆ่ฏญไนๅฑๆๅน้
ๆๆ ๏ผไผๅ
็จ query_metricใ |
-
-
-
-
-
1.4 Agent ๆจ่ๅทฅไฝๆต
-
-
-
๐ ๆฏๆฌกๅฏน่ฏ็ๆ ๅๆต็จ๏ผ
-
- get_query_guide() โ ่ทๅๅฎๆดๆๅผ๏ผAgent ็ฅ้ไฝๆถ็จๅชไธชๅทฅๅ
ท๏ผ
- check_service_health() โ ็กฎ่ฎค Doris ่ฟ้๏ผๆฅ็ๅฏ็จ workspace
- -
- ่ทฏ็ฑๅคๆญ๏ผ
-
- - ็จๆท้ฎ"ๆๅคๅฐ"ใ"่ฎก็ฎ"ใ"่ถๅฟ"ใ"ๆฏ็" โ ่ตฐ่ฏญไนๅฑ 4-5-6
- - ็จๆท้ฎ"็่กจ็ปๆ"ใ"ๆๅชไบๅบ" โ ่ตฐๅ
ๆฐๆฎๅทฅๅ
ท
- - ่ฏญไนๅฑไธๅฏ็จ๏ผๆๆ workspace ้ healthy๏ผโ ่ทณ่ฟ 4-6๏ผ็ดๆฅ่ตฐ 7
-
-
- list_metrics(workspace) โ ๅๅบๆๆ ๏ผๅน้
็จๆทๆๅพ
- list_dimensions_for_metric(workspace, metric_name) โ ่ทๅๅฏ็จ็ปดๅบฆ
- query_metric(workspace, metrics, group_by, ...) โ ๆง่กๆฅ่ฏข
- - ๅ
ๅบ๏ผๅฆๆๆญฅ้ชค 4 ๆฒกๆๅน้
็ๆๆ ๏ผ็จ
list_databases โ list_tables โ describe_table โ execute_query ่ตฐ่ฃธ SQL ่ทฏๅพ
-
-
-
-
-
๐ซ ๅ
ณ้ฎ็ฆๅบ๏ผ
-
- - ่ฏญไนๅฑ healthy + ๆๅน้
ๆๆ โ ็ปไธๅ
่ฎธ็ปๅผ
query_metric ็ดๆฅ็จ execute_query ๅ SQLใ
- - ่ฏญไนๅฑ healthy + ๆ ๅน้
ๆๆ โ ๅ
ๅ็ฅ็จๆท"่ฏญไนๅฑๆฒกๆ่ฆ็ๆญคๆฅ่ฏข"๏ผๅ็จ
execute_query๏ผๅนถๅจ็ปๆไธญ้่ญฆๅใ
-
-
-
-
1.5 ๅฏน่ฏ็คบไพ
-
็จๆท๏ผๆ่ฟไธไธชๆๅๆธ ้็่ฎขๅๆป้ขๆฏๅคๅฐ๏ผ
-
-AI ๆ่๏ผ
- 1. get_query_guide โ ่ทๅๆๅผ โ
- 2. check_service_health โ workspace "example" healthy, 5 metrics โ
- 3. ๆถๅ"ๆป้ข"ใ"ๆธ ้" โ ่ตฐ่ฏญไนๅฑ
- 4. list_metrics(example) โ total_amount๏ผ่ฎขๅๆป้้ข๏ผโ
- 5. list_dimensions_for_metric(example, total_amount) โ channel โ
- 6. query_metric(example, metrics=[total_amount], group_by=[channel],
- where="order_date >= '2026-04-30'")
-
-AI ๅๅค๏ผ
- ๆธ ้ ่ฎขๅๆป้ข
- WEB 1,096.00
- APP 2,396.00
- MINI 298.00
-
-
2. CLI ๅฝไปค่กๅทฅๅ
ท
-
doris-mcp-client ๆฏ Doris MCP Server ็ๅฝไปค่กๅฎขๆท็ซฏ๏ผ็จไบ่ฐ็จ MCP Tool ๅ็ฎก็่ฏญไนๆจกๅใ
-
-
2.1 ้
็ฝฎ่ฟๆฅ
-
CLI ้่ฟ็ฏๅขๅ้ๆ้
็ฝฎๆไปถ่ทๅ่ฟๆฅไฟกๆฏ๏ผไบ้ไธ๏ผ๏ผ
-
ๆนๅผไธ๏ผ็ฏๅขๅ้
-
export DORIS_MCP_SERVER=http://<MCP-Server-ๅฐๅ>:<็ซฏๅฃ>
-export DORIS_MCP_TOKEN=admin:admin
-
ๆนๅผไบ๏ผ้
็ฝฎๆไปถ doris-mcp-client.toml
-
[server]
-DORIS_MCP_SERVER = "http://<MCP-Server-ๅฐๅ>:<็ซฏๅฃ>"
-DORIS_MCP_TOKEN = "admin:admin"
-
ไนๅฏ็จ -c ๆๅฎ้
็ฝฎๆไปถ่ทฏๅพ๏ผ
-
doris-mcp-client -c /path/to/doris-mcp-client.toml tool list
-
-
2.2 ่ฐ็จ MCP Tool
-
# ๅๅบๆๆๅฏ็จ Tool
-doris-mcp-client tool list
-
-# ๆฅ็ Tool ๅๆฐ
-doris-mcp-client tool describe execute_query
-
-# ่ฐ็จ Tool๏ผ็ฎๅๅๆฐ
-doris-mcp-client tool call list_tables --arg database=analytics
-
-# ่ฐ็จ Tool๏ผJSON ๅๆฐ
-doris-mcp-client tool call query_metric --json \
- '{"workspace":"example","metrics":["total_amount"],"group_by":["channel"]}'
-
-# ๆฅ็ๆๅกๅฅๅบท็ถๆ
-doris-mcp-client tool call check_service_health
-
-# Verbose ๆจกๅผ๏ผๆพ็คบ่ฏฆ็ป้่ฏฏ๏ผ
-doris-mcp-client -v tool call execute_query --arg sql="SELECT ..."
-
-
2.3 ่ฏญไนๆจกๅ็ฎก็
-
# ๅๅบไธไธช workspace ็ๆๆ่ฏญไนๆไปถ
-doris-mcp-client semantic list --workspace example
-
-# ๆฅ็ๆไธชๆไปถๅ
ๅฎน
-doris-mcp-client semantic view orders.yaml --workspace example
-
-# ไธไผ ๅนถๆจ้่ฏญไนๆจกๅ
-doris-mcp-client semantic push ./models --workspace example
-
-# ๆฅ่ฏขๆจ้็ปๆ๏ผๅผๆญฅ๏ผ
-doris-mcp-client semantic result <request_id>
-
-# ็ผ่พ่ฏญไนๆไปถ๏ผ่ฐ่ตท $EDITOR๏ผ
-doris-mcp-client semantic edit orders.yaml --workspace example
-
-# ๆ็ดๆฅไผ ๅ
ๅฎน
-doris-mcp-client semantic edit orders.yaml --content "..." --workspace example
-
-# Stage ๅ ้คๆไปถ
-doris-mcp-client semantic delete old_model.yaml --workspace example
-
-# ่งฆๅ่ฏญไนๅฑ้่ฝฝ
-doris-mcp-client semantic reload --workspace example
-
-# ๆฅ็่ฏญไนๅฑ็ถๆ
-doris-mcp-client semantic status
-
-# ๆๅๅฝๅ็ๆ็่ฏญไนๆจกๅๅฐๆฌๅฐ
-doris-mcp-client semantic pull --output ./remote-models --workspace example
-
-
2.4 ๅธธ็จๅฝไปค้ๆฅ
-
-| ๅฝไปค | ่ฏดๆ |
-
-doris-mcp-client tool list | ๅๅบๆๆ MCP Tool |
-doris-mcp-client tool describe <name> | ๆฅ็ Tool ๅๆฐ |
-doris-mcp-client tool call <name> --arg k=v | ่ฐ็จ Tool๏ผ้ฎๅผๅๆฐ๏ผ |
-doris-mcp-client tool call <name> --json '{...}' | ่ฐ็จ Tool๏ผJSON ๅๆฐ๏ผ |
-doris-mcp-client semantic list -w <ws> | ๅๅบ่ฏญไนๆไปถ |
-doris-mcp-client semantic push <dir> -w <ws> | ๆจ้่ฏญไนๆจกๅ |
-doris-mcp-client semantic result <id> | ๆฅ่ฏขๆจ้็ปๆ |
-doris-mcp-client semantic edit <file> -w <ws> | ็ผ่พ่ฏญไนๆไปถ |
-doris-mcp-client semantic pull -o <dir> -w <ws> | ๆๅ่ฏญไนๆจกๅ |
-doris-mcp-client semantic status | ่ฏญไนๅฑ็ถๆ |
-doris-mcp-client semantic reload -w <ws> | ่งฆๅ้่ฝฝ |
-
-
-
-
-
3. ๅฏ็จ็ๆฅ่ฏข่ฝๅ
-
ๆฅๅ
ฅๅ๏ผไฝ ๅจ Claude Code ๅฏน่ฏไธญ็ดๆฅ็จ่ช็ถ่ฏญ่จๆ้ฎ๏ผAI ไผ่ชๅจ่ฐ็จๅฏนๅบ็ MCP Toolใ
-
-
3.1 ๅ
ๆฐๆฎๅ็ฐ
-
-| ไฝ ๆณๅไปไน | ็ดๆฅ่ฏด | ่ฐ็จไบๅชไธช Tool |
-
-| ็ๆๅชไบๅบ | "ๅๅบๆๆๆฐๆฎๅบ" | list_databases |
-| ็ๅบไธๆๅชไบ่กจ | "ๅๅบ dw ๅบ็่กจ" | list_tables |
-| ็่กจ็ปๆ | "ๆฅ็ orders ่กจๆๅชไบๅญๆฎต" | describe_table |
-
-
-
-
3.2 SQL ๆฅ่ฏข
-
-| ไฝ ๆณๅไปไน | ็ดๆฅ่ฏด | ่ฐ็จไบๅชไธช Tool |
-
-| ๆฅๆฐๆฎ | "ๆฅ่ฏข top 10 ่ฎขๅ" | execute_query |
-| ็ป่ฎกๅๆ | "ๆ็ถๆ็ป่ฎก่ฎขๅๆฐ" | execute_query |
-
-
-
execute_query ๅชๅ
่ฎธๅช่ฏป SQL๏ผSELECT / SHOW / DESCRIBE / EXPLAIN๏ผใ
-
-
3.3 ่ฏญไนๅฑๆฅ่ฏข๏ผ้่ฆ็ฎก็ๅ้
็ฝฎไบ่ฏญไนๆจกๅ๏ผ
-
ๅฆๆไฝ ็ Doris ้็พค้
็ฝฎไบ่ฏญไนๅฑ๏ผAgent ไธๆญฅๅณๅฏๅฎๆๆฅ่ฏข๏ผ
-
- list_metrics โ ๅๅบๆๆๅฏ็จๆๆ ๏ผname + description๏ผ
- list_dimensions_for_metric โ ๆฅ็ๆๆ ็ๅฏ็จ็ปดๅบฆ๏ผname + type + description๏ผ
- query_metric โ ๆง่ก่ฏญไนๆฅ่ฏข๏ผๆฏๆ having ่ฟๆปค่ๅ็ปๆ๏ผ
-
-
-| ไฝ ๆณๅไปไน | ็ดๆฅ่ฏด | ่ฐ็จไบๅชไธช Tool |
-
-| ็ๆๅชไบๆๆ | "ๅๅบๆๆๆๆ " | list_metrics |
-| ๆฅ่ฅๆถ่ถๅฟ | "ๆฅ่ฏข่ฟๅป 7 ๅคฉ็่ฅๆถ๏ผๆๅคฉๅ็ป" | query_metric |
-| ็ๅฏ็จ็ปดๅบฆ | "total_amount ๅฏไปฅๆๅชไบ็ปดๅบฆๅๆ" | list_dimensions_for_metric |
-| ๆฃๆฅๆๅก็ถๆ | "ๆฃๆฅๆๅกๅฅๅบท็ถๆ" | check_service_health |
-
-
-
ๆๆ่ฏญไนๅทฅๅ
ทๅฟ
ไผ workspace ๅๆฐ๏ผ้ป่ฎคๅฏ็จ "example"๏ผใ
-
-
-
4. ๆฅ่ฏข็คบไพ
-
-
็คบไพ 1๏ผๆข็ดขๆฐๆฎ็ปๆ
-
ไฝ ๏ผๅๅบๆๆๆฐๆฎๅบ
-AI๏ผ[่ฐ็จ list_databases]
- dw
-
-ไฝ ๏ผ็็ dw ๅบ้ๆๅชไบ่กจ
-AI๏ผ[่ฐ็จ list_tables]
- orders, users, products, dim_date
-
-ไฝ ๏ผ็ไธไธ orders ่กจ็็ปๆ
-AI๏ผ[่ฐ็จ describe_table]
- ๅๅ ็ฑปๅ ๆณจ้
- order_id BIGINT ่ฎขๅID
- user_id BIGINT ็จๆทID
- amount DECIMAL ้้ข
- channel VARCHAR ๆธ ้
- status VARCHAR ็ถๆ
- order_date DATE ไธๅๆฅๆ
-
-
็คบไพ 2๏ผๆฐๆฎๆฅ่ฏข
-
ไฝ ๏ผๆฅไธไธๆ่ฟไธๅจๅ็ถๆ็่ฎขๅๆฐๅๆป้้ข
-AI๏ผ[่ฐ็จ execute_query]
- status count total_amount
- completed 1234 567890.00
- pending 567 89012.00
- cancelled 89 12345.00
-
-
็คบไพ 3๏ผ่ฏญไนๆๆ
-
ไฝ ๏ผๅๅบๆๆๅฏ็จ็ๆๆ
-AI๏ผ[่ฐ็จ list_metrics]
- total_amount ่ฎขๅๆป้้ข
- order_count ่ฎขๅๆฐ
- avg_amount ๅนณๅๅฎขๅไปท
- unique_users ไธๅ็จๆทๆฐ
- user_count ็จๆทๆฐ
-
-ไฝ ๏ผๆฅ่ฏขๆ่ฟ 7 ๅคฉๅๆธ ้็่ฎขๅๆป้้ข
-AI๏ผ[่ฐ็จ list_dimensions_for_metric] โ channel
- [่ฐ็จ query_metric]
- ๆธ ้ ๆป้้ข
- ็บฟไธ 12345.00
- ็บฟไธ 15678.00
- ...
-
-
-
5. ๅธธ่ง้ฎ้ข
-
-
Q: ่ฟไธไธ๏ผๆฅ 401
-
็กฎ่ฎค Token ๆ ผๅผๆญฃ็กฎ๏ผ็จๆทๅ:ๅฏ็ ๏ผ่ฑๆๅๅทๅ้๏ผใ
+}
+
-
Q: ่ฏญไนๆฅ่ฏขๆฅ้ "workspace not found"
-
็กฎ่ฎค workspace ๅ็งฐๆญฃ็กฎ๏ผ้ป่ฎคๅฏ็จ "example"ใ็ปๅฝ WebUI ๆฅ็ๅฏ็จ workspaceใ
+
Verify the connection by asking the agent to check service health. The agent should call check_service_health and report "doris": "connected".
-
Q: ่ฝๅฆๆง่กๅๆไฝ๏ผ
-
ไธ่ฝใexecute_query ไป
ๅ
่ฎธๅช่ฏป SQL๏ผSELECT / SHOW / DESCRIBE / EXPLAIN๏ผใ
+
+
+ | Tool | Purpose |
+ get_query_guide | Mandatory first step. Returns workflow rules, semantic layer routing, raw SQL fallback rules, and query syntax. |
+ check_service_health | Returns Doris connectivity and each workspace status: healthy, no_models, or not_ready. |
+ list_metrics | Lists available metrics in a workspace. |
+ list_dimensions_for_metric | Lists valid group_by dimensions for one metric. |
+ query_metric | Primary tool for counts, sums, rates, averages, rankings, and trends. Compiles and executes semantic SQL. |
+ reload_semantic_layer | Manually reloads a workspace after model changes. |
+ list_databases | Lists Doris databases for raw SQL fallback or metadata discovery. |
+ list_tables | Lists tables in a database. |
+ describe_table | Returns physical table schema at names, summary, or full detail. |
+ execute_query | Raw SQL fallback. Only SELECT, SHOW, DESCRIBE, and EXPLAIN are allowed. |
+
-
Q: ๆฅ่ฏขๅพๆ
ข๏ผ
-
ๆฐๆฎ้ๅคงๆถ่ฎฉ AI ๅ LIMIT ๆ็ผฉๅฐๆถ้ด่ๅดใ
+
3. Recommended Agent Workflow
+
get_query_guide()
+check_service_health()
+if a healthy workspace exists:
+ list_metrics(workspace)
+ list_dimensions_for_metric(workspace, metric_name)
+ query_metric(workspace, metrics, group_by, where, order_by, limit, having)
+else:
+ list_databases()
+ list_tables(database)
+ describe_table(database, table)
+ execute_query(sql)
+
+
+ If the semantic layer is healthy and a matching metric exists, use query_metric. Do not bypass it with raw SQL.
+
-
Q: ่ฏญไนๅฑ่ฝ็จๅชไบๆๆ ๏ผ
-
็จ CLI ๅฟซ้ๆฅ็๏ผ
-
doris-mcp-client tool call list_metrics --json '{"workspace":"example"}'
-doris-mcp-client tool call check_service_health
+
4. CLI Client
+
The doris-mcp-client command line client calls MCP tools and manages semantic model files through the same Bearer authentication.
+
export DORIS_MCP_SERVER=http://<host>:3000
+export DORIS_MCP_TOKEN=<user>:<password>
+
+# List tools and inspect arguments
+./mcp-client.sh tool list
+./mcp-client.sh tool describe query_metric
+
+# Call tools
+./mcp-client.sh tool call check_service_health
+./mcp-client.sh tool call list_metrics --json '{"workspace":"example"}'
+./mcp-client.sh tool call query_metric --json '{"workspace":"example","metrics":["total_amount"],"group_by":["channel"]}'
+
+# Manage semantic model files
+./mcp-client.sh semantic list -w example
+./mcp-client.sh semantic push ./models -w example
+./mcp-client.sh semantic pull -o ./backup -w example
+./mcp-client.sh semantic reload -w example
+./mcp-client.sh semantic status
+
+
+
5. Query Examples
+
Explore metadata
+
User: List all databases.
+AI: calls list_databases
+
+User: List tables in the dw database.
+AI: calls list_tables
+
+User: Show the schema for dw.orders.
+AI: calls describe_table
+
+
+
Query semantic metrics
+
User: What is the total order amount by channel for the last 7 days?
+AI:
+ 1. calls get_query_guide
+ 2. calls check_service_health
+ 3. calls list_metrics and matches total_amount
+ 4. calls list_dimensions_for_metric and verifies channel
+ 5. calls query_metric with metrics=["total_amount"], group_by=["channel"]
+
+
+
Raw SQL fallback
+
User: Show the top 10 rows from dw.orders.
+AI: calls execute_query with a read-only SELECT statement
+
+
+
6. FAQ
+
+ | Question | Answer |
+ | Connection returns 401 | Confirm the token format is username:password, separated with an ASCII colon. |
+ | Semantic query says workspace not found | Confirm the workspace name. The bundled sample uses example. |
+ | Can write SQL be executed? | No. execute_query allows read-only SQL only. |
+ | Queries are slow | Add LIMIT, reduce the time range, or use semantic dimensions to narrow the query. |
+ | Which metrics are available? | Call list_metrics or run ./mcp-client.sh tool call list_metrics --json '{"workspace":"example"}'. |
+
+
+
diff --git a/mcp-server.toml b/mcp-server.toml
index 0d51dfd..2d9d038 100644
--- a/mcp-server.toml
+++ b/mcp-server.toml
@@ -1,35 +1,35 @@
# =============================================================================
-# mcp-server ้
็ฝฎๆไปถ (TOML)
+# mcp-server configuration file (TOML)
# =============================================================================
-# ไผ ่พๆจกๅผ๏ผไป
streamable-httpใ
-# ้
็ฝฎๅผๆฏๆ ${VAR_NAME} ็ฏๅขๅ้ๆๅผใ
+# Transport mode: streamable-http only.
+# Values support ${VAR_NAME} environment-variable interpolation.
# ---------------------------------------------------------------------------
-# ๆๅก็ซฏ
+# Server
# ---------------------------------------------------------------------------
[server]
-mcp_name = "doris-new-mcp" # MCP ๆๅกๅ็งฐ
-mcp_host = "0.0.0.0" # HTTP ็ๅฌๅฐๅ
-mcp_port = 3000 # HTTP ็ๅฌ็ซฏๅฃ
-fe_port = 9030 # Doris FE MySQL ็ซฏๅฃ
-fe_host = "127.0.0.1" # Doris FE ๅฐๅ๏ผ่ฟ็จ้จ็ฝฒๅกซๅ FE ็ง็ฝ IP ๆๅๅ
+mcp_name = "doris-new-mcp" # MCP service name
+mcp_host = "0.0.0.0" # HTTP listen address
+mcp_port = 3000 # HTTP listen port
+fe_port = 9030 # Doris FE MySQL port
+fe_host = "127.0.0.1" # Doris FE address; use the private FE IP or hostname for remote deployment
# ---------------------------------------------------------------------------
-# ๆฅๅฟ
+# Logging
# ---------------------------------------------------------------------------
[logging]
level = "info" # debug / info / warning / error
-audit_log = "./logs/audit.log" # ๅฎก่ฎกๆฅๅฟ่ทฏๅพ
-rotation_when = "midnight" # ่ฝฎ่ฝฌ็ญ็ฅ: midnight / H / D / W0~W6
-rotation_backup_count = 30 # ไฟ็็่ฝฎ่ฝฌๆไปถๆฐ
+audit_log = "./logs/audit.log" # Audit log path
+rotation_when = "midnight" # Rotation policy: midnight / H / D / W0~W6
+rotation_backup_count = 30 # Number of rotated files to keep
# ---------------------------------------------------------------------------
-# ๆฅ่ฏข
+# Query
# ---------------------------------------------------------------------------
[query]
-pool_min_size = 0 # ่ฟๆฅๆฑ ๆๅฐ่ฟๆฅๆฐ๏ผadmin pool + per-user pool๏ผ
-pool_max_size = 10 # ่ฟๆฅๆฑ ๆๅคง่ฟๆฅๆฐ
-pool_idle_timeout_seconds = 300 # ็ฉบ้ฒ่ฟๆฅ่ถ
ๆถๅๆถ๏ผ็ง๏ผ๏ผไฝไธบ aiomysql pool_recycle๏ผๅบๅฐไบ Doris wait_timeout
-query_timeout_seconds = 600 # SQL ๆฅ่ฏข่ถ
ๆถ๏ผ็ง๏ผ
-query_max_rows = 10000 # ๅๆฌกๆฅ่ฏขๆๅคง่ฟๅ่กๆฐ
-db_whitelist = [] # ๆฐๆฎๅบ็ฝๅๅ๏ผ็ฉบๅ่กจ่กจ็คบไธ้ๅถ
+pool_min_size = 0 # Minimum connection count (admin pool + per-user pool)
+pool_max_size = 10 # Maximum connection count
+pool_idle_timeout_seconds = 300 # Idle connection recycle timeout in seconds; as aiomysql pool_recycle, should be lower than Doris wait_timeout
+query_timeout_seconds = 600 # SQL query timeout in seconds
+query_max_rows = 10000 # Maximum rows returned by one query
+db_whitelist = [] # Database allow-list; empty means unrestricted
diff --git a/src/skills/doris-mcp-skill.md b/src/skills/doris-mcp-skill.md
index 495ebc2..ee8dc62 100644
--- a/src/skills/doris-mcp-skill.md
+++ b/src/skills/doris-mcp-skill.md
@@ -55,22 +55,22 @@ Returns:
// Response
{
"data": [
- {"name": "total_amount", "description": "่ฎขๅๆป้้ข"},
- {"name": "order_count", "description": "่ฎขๅๆฐ"},
- {"name": "avg_amount", "description": "ๅนณๅๅฎขๅไปท"},
- {"name": "unique_users", "description": "ไธๅ็จๆทๆฐ"},
- {"name": "user_count", "description": "็จๆทๆฐ"}
+ {"name": "total_amount", "description": "Total order amount"},
+ {"name": "order_count", "description": "Order count"},
+ {"name": "avg_amount", "description": "Average order value"},
+ {"name": "unique_users", "description": "Ordering users"},
+ {"name": "user_count", "description": "User count"}
],
"meta": {"total_count": 5}
}
```
**How to match user intent to a metric:**
-- "้ๅฎ้ข / ่ฅๆถ / GMV" โ `total_amount`
-- "่ฎขๅ้ / ๆไบค้" โ `order_count`
-- "ๅฎขๅไปท" โ `avg_amount`
-- "ไธๅ็จๆทๆฐ" โ `unique_users`
-- "็จๆทๆฐ" โ `user_count`
+- "sales / revenue / GMV" โ `total_amount`
+- "orders / transactions" โ `order_count`
+- "average order value / AOV" โ `avg_amount`
+- "ordering users / purchasing users" โ `unique_users`
+- "users / customer count" โ `user_count`
If the user's question doesn't clearly match any metric, call `list_metrics` and scan all descriptions. If nothing matches, fall back to raw SQL.
@@ -85,14 +85,14 @@ If the user's question doesn't clearly match any metric, call `list_metrics` and
// Response
{
"data": [
- {"name": "order_date", "type": "time", "description": "ไธๅๆฅๆ๏ผๆๅคฉ๏ผ"},
- {"name": "channel", "type": "categorical", "description": "ไธๅๆธ ้"},
- {"name": "status", "type": "categorical", "description": "่ฎขๅ็ถๆ"},
- {"name": "city", "type": "categorical", "description": "ๅๅธ"},
- {"name": "level", "type": "categorical", "description": "็ญ็บง"},
- {"name": "register_date", "type": "time", "description": "ๆณจๅๆฅๆ"},
- {"name": "category", "type": "categorical", "description": "ๅ็ฑป"},
- {"name": "brand", "type": "categorical", "description": "ๅ็"}
+ {"name": "order_date", "type": "time", "description": "Order date (day grain)"},
+ {"name": "channel", "type": "categorical", "description": "Order channel"},
+ {"name": "status", "type": "categorical", "description": "Order status"},
+ {"name": "city", "type": "categorical", "description": "City"},
+ {"name": "level", "type": "categorical", "description": "Customer level"},
+ {"name": "register_date", "type": "time", "description": "Registration date"},
+ {"name": "category", "type": "categorical", "description": "Product category"},
+ {"name": "brand", "type": "categorical", "description": "Brand"}
],
"meta": {"metric": "total_amount", "count": 8}
}
@@ -180,23 +180,23 @@ order_by=["-total_amount", "channel"] # multi-column
### Full Examples
```json
-// "ๅๆธ ้้ๅฎ้ข"
+// "Sales by channel"
{"workspace": "example", "metrics": ["total_amount"], "group_by": ["channel"]}
-// "2ๆไปฝๆฏๆฅ่ฎขๅ้่ถๅฟ"
+// "Daily order trend in February"
{"workspace": "example", "metrics": ["order_count"], "group_by": ["order_date"],
"where": "order_date >= '2026-02-01' AND order_date <= '2026-02-28'",
"order_by": ["order_date"]}
-// "้ๅฎ้ข Top 3 ๆธ ้"
+// "Top 3 channels by sales"
{"workspace": "example", "metrics": ["total_amount"], "group_by": ["channel"],
"order_by": ["-total_amount"], "limit": 3}
-// "ๅทฒๅฎๆ่ฎขๅ็ๆธ ้ๅๅธ"
+// "Channel distribution for completed orders"
{"workspace": "example", "metrics": ["total_amount", "order_count"],
"group_by": ["channel"], "where": "status = 'completed'"}
-// "ๅๅ็้ๅฎ้ข๏ผๅช็ๅๅพๅฅฝ็"
+// "Sales by brand, showing only strong sellers"
{"workspace": "example", "metrics": ["total_amount"], "group_by": ["brand"],
"order_by": ["-total_amount"], "having": "total_amount > 500"}
```
diff --git a/src/store/seed.py b/src/store/seed.py
index ce3292c..b4311f6 100644
--- a/src/store/seed.py
+++ b/src/store/seed.py
@@ -113,19 +113,19 @@ def set_doris_port(port: int) -> None:
]
_USERS_DATA = [
- (1, "ๅผ ไธ", "ๅไบฌ", "VIP", "2025-06-01"),
- (2, "ๆๅ", "ไธๆตท", "ๆฎ้", "2025-08-15"),
- (3, "็ไบ", "ๆทฑๅณ", "VIP", "2025-10-01"),
- (4, "่ตตๅ
ญ", "ๆญๅท", "ๆฎ้", "2026-01-10"),
- (5, "ๅญไธ", "ๅนฟๅท", "ๆฎ้", "2026-02-01"),
+ (1, "Alex Chen", "Beijing", "VIP", "2025-06-01"),
+ (2, "Bella Li", "Shanghai", "Standard", "2025-08-15"),
+ (3, "Chris Wang", "Shenzhen", "VIP", "2025-10-01"),
+ (4, "Dana Zhao", "Hangzhou", "Standard", "2026-01-10"),
+ (5, "Evan Sun", "Guangzhou", "Standard", "2026-02-01"),
]
_PRODUCTS_DATA = [
- (1, "ๆ ็บฟ่ณๆบ", "็ตๅญ", "็ดขๅฐผ", 199.00),
- (2, "ๆบๆขฐ้ฎ็", "็ตๅญ", "็ฝๆ", 599.00),
- (3, "่ฟๅจ้", "ๆ่ฃ
", "่ๅ
", 299.00),
- (4, "่ๅ
", "้
้ฅฐ", "ๆฐ็งไธฝ", 199.00),
- (5, "ๆบ่ฝๆ่กจ", "็ตๅญ", "ๅไธบ", 899.00),
+ (1, "Wireless Earbuds", "Electronics", "Sony", 199.00),
+ (2, "Mechanical Keyboard", "Electronics", "Logitech", 599.00),
+ (3, "Running Shoes", "Apparel", "Nike", 299.00),
+ (4, "Backpack", "Accessories", "Samsonite", 199.00),
+ (5, "Smartwatch", "Electronics", "Huawei", 899.00),
]
# ---------------------------------------------------------------------------
@@ -135,8 +135,8 @@ def set_doris_port(port: int) -> None:
_ORDERS_YAML = """---
semantic_model:
name: orders
- description: ่ฎขๅ่กจ
- label: ่ฎขๅ่กจ
+ description: Orders table
+ label: Orders table
db_table: dw.orders
@@ -147,29 +147,29 @@ def set_doris_port(port: int) -> None:
- name: order
type: primary
expr: order_id
- label: ่ฎขๅ
+ label: Order
- name: user
type: foreign
expr: user_id
- label: ็จๆท
+ label: User
measures:
- name: total_amount
expr: amount
agg: sum
- description: ่ฎขๅๆป้้ข
+ description: Total order amount
- name: order_count
expr: order_id
agg: count_distinct
- description: ่ฎขๅๆฐ
+ description: Order count
- name: avg_amount
expr: amount
agg: average
- description: ๅนณๅๅฎขๅไปท
+ description: Average order value
- name: unique_users
expr: user_id
agg: count_distinct
- description: ไธๅ็จๆทๆฐ
+ description: Ordering users
dimensions:
- name: order_date
@@ -177,20 +177,20 @@ def set_doris_port(port: int) -> None:
type_params:
time_granularity: day
expr: order_date
- label: ไธๅๆฅๆ
+ label: Order date
- name: channel
type: categorical
- label: ๆธ ้
+ label: Channel
- name: status
type: categorical
- label: ็ถๆ
+ label: Status
"""
_USERS_YAML = """---
semantic_model:
name: users
- description: ็จๆท่กจ
- label: ็จๆท่กจ
+ description: Users table
+ label: Users table
db_table: dw.users
@@ -201,34 +201,34 @@ def set_doris_port(port: int) -> None:
- name: user
type: primary
expr: user_id
- label: ็จๆท
+ label: User
measures:
- name: user_count
expr: user_id
agg: count_distinct
- description: ็จๆทๆฐ
+ description: User count
dimensions:
- name: city
type: categorical
- label: ๅๅธ
+ label: City
- name: level
type: categorical
- label: ็ญ็บง
+ label: Customer level
- name: register_date
type: time
type_params:
time_granularity: day
expr: register_date
- label: ๆณจๅๆฅๆ
+ label: Registration date
"""
_PRODUCTS_YAML = """---
semantic_model:
name: products
- description: ๅๅ่กจ
- label: ๅๅ่กจ
+ description: Products table
+ label: Products table
db_table: dw.products
@@ -236,22 +236,22 @@ def set_doris_port(port: int) -> None:
- name: product
type: primary
expr: product_id
- label: ๅๅ
+ label: Product
measures:
- name: product_count
expr: product_id
agg: count_distinct
- description: ๅๅๆฐ
+ description: Product count
create_metric: false
dimensions:
- name: category
type: categorical
- label: ๅ็ฑป
+ label: Category
- name: brand
type: categorical
- label: ๅ็
+ label: Brand
"""
_PROJECT_YAML = """---
diff --git a/start-mcp-server.sh b/start-mcp-server.sh
index d1e51b9..a0d3995 100755
--- a/start-mcp-server.sh
+++ b/start-mcp-server.sh
@@ -1,13 +1,13 @@
#!/usr/bin/env bash
# =============================================================================
-# start-mcp-server.sh โ ไธ้ฎๅฏๅจ doris-mcp-server
+# start-mcp-server.sh โ start doris-mcp-server
#
-# ๆๆ้
็ฝฎไปๅ็ฎๅฝ mcp-server.toml ่ฏปๅ๏ผๆ ้ไผ ๅใ
+# All configuration is read from mcp-server.toml in the same directory.
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-# ไผๅ
ไฝฟ็จ DORIS_MCP_PYTHON ็ฏๅขๅ้๏ผๅฆๅ็จ่ชๅธฆ็ Python
+# Prefer DORIS_MCP_PYTHON; otherwise use the bundled Python.
PYTHON="${DORIS_MCP_PYTHON:-$SCRIPT_DIR/python/bin/python3}"
CONFIG="$SCRIPT_DIR/mcp-server.toml"
diff --git a/test/README.md b/test/README.md
index 513b136..6e52b75 100644
--- a/test/README.md
+++ b/test/README.md
@@ -1,139 +1,139 @@
# Test Cases โ doris-new-mcp
-ๅบไบ [DESIGN.md](../DESIGN.md)ใ[INSTALL.html](../INSTALL.html)ใ[doris-mcp-docs.html](../doris-mcp-docs.html) ็ๆใ
+Generated from [DESIGN.md](../DESIGN.md), [INSTALL.html](../INSTALL.html), and [doris-mcp-docs.html](../doris-mcp-docs.html).
-## ๆไปถ่ฏดๆ
+## File Overview
-### ็ฆป็บฟๅๅ
ๆต่ฏ๏ผๆ ้ MCP Server / Doris๏ผ
+### Offline Unit Tests (No MCP Server / Doris Required)
-| ๆไปถ | ๅ
ๅฎน |
+| File | Coverage |
|------|------|
-| `test_sql_validator.py` | `core.sql_validator.validate_readonly` ๅช่ฏป SQL ๆ ก้ช๏ผๆพ่ก/ๆฆๆช/ๅค่ฏญๅฅ/ๆณจ้็ป่ฟ/ๅทฒ็ฅๅ็ผ่กไธบ๏ผ |
-| `test_sensitive_mask.py` | `core.sensitive_mask` ๅฏ็ /token ่ฑๆ |
-| `test_pagination.py` | `core.pagination` ๅ้กตไธ token TTL ่กไธบ |
-| `test_private_ip_config.py` | ่ฏทๆฑ่็น IP ่ทๅไธๅฏๅจ่ฃ
้
|
-| `test_deps.py` | ่ฟ่กๆถไพ่ตๅฎๅซ๏ผimport ็ๅฎๆจกๅ๏ผ |
-| `test_cross_file_deps.py` | ๅ ้คๅ่ทจๆไปถไพ่ตๆฃๆต |
-| `test_credential_pass.py` | ่ฏทๆฑ็บงๅญๆฎ้ไผ ๅฐ store ๅฑ |
-| `test_watcher.py` | `MultiWorkspaceWatcher.ensure_fresh` ๅทๅด/้่ฝฝ/้็บง |
-| `test_web_session_cookie.py` | Web ไผ่ฏ Cookie |
-| `test_session_affinity_proxy_routing.py` | ไผ่ฏไบฒๅไปฃ็่ทฏ็ฑ |
-| `test_session_affinity_proxy_streaming.py` | ไผ่ฏไบฒๅไปฃ็ๆตๅผ่ฝฌๅ |
-| `test_session_affinity_proxy_relogin.py` | ไผ่ฏไบฒๅไปฃ็้็ปๅฝ |
-| `test_session_affinity_proxy_force_target.py` | ไผ่ฏไบฒๅไปฃ็่ฏทๆฑๅฐๅ่ทฏ็ฑ |
-
-### ๅจ็บฟๆต่ฏ๏ผ้่ฟ่กไธญ็ MCP Server + Doris๏ผ
-
-| ๆไปถ | ๅ
ๅฎน |
+| `test_sql_validator.py` | `core.sql_validator.validate_readonly` read-only SQL validation (allow/block/multiple statements/comment bypass/known prefixes) |
+| `test_sensitive_mask.py` | `core.sensitive_mask` password/token masking |
+| `test_pagination.py` | `core.pagination` pagination and token TTL behavior |
+| `test_private_ip_config.py` | Request node IP detection and startup wiring |
+| `test_deps.py` | Runtime dependency guard (imports real modules) |
+| `test_cross_file_deps.py` | Cross-file dependency detection before delete |
+| `test_credential_pass.py` | Request-scoped credential pass-through to the store layer |
+| `test_watcher.py` | `MultiWorkspaceWatcher.ensure_fresh` cooldown/reload/degradation |
+| `test_web_session_cookie.py` | Web session cookie |
+| `test_session_affinity_proxy_routing.py` | Session affinity proxy routing |
+| `test_session_affinity_proxy_streaming.py` | Session affinity proxy streaming |
+| `test_session_affinity_proxy_relogin.py` | Session affinity proxy relogin |
+| `test_session_affinity_proxy_force_target.py` | Session affinity proxy request-address routing |
+
+### Online Tests (Require Running MCP Server + Doris)
+
+| File | Coverage |
|------|------|
-| `test_mcp_tools.py` | ๅ
จ้จ 10 ไธช MCP Tool + ่ฎค่ฏ + E2E๏ผ30 ็จไพ๏ผ |
-| `test_web_api.py` | Web UI + REST API + ๅทฅไฝๅบ็ฎก็๏ผ12 ็จไพ๏ผ |
+| `test_mcp_tools.py` | All 10 MCP tools + auth + E2E (30 cases) |
+| `test_web_api.py` | Web UI + REST API + workspace management (12 cases) |
-### ๅ
ฅๅฃ่ๆฌ
+### Entry Script
-| ๆไปถ | ๅ
ๅฎน |
+| File | Coverage |
|------|------|
-| `run_all_tests.sh` | ไธ้ฎ่ฟ่ก่ๆฌ๏ผๆฏๆ `--offline` / `--tools` / `--web` / `--smoke` |
+| `run_all_tests.sh` | One-command runner supporting `--offline` / `--tools` / `--web` / `--smoke` |
-## ็ฏๅข่ฆๆฑ
+## Requirements
-| ๆกไปถ | ่ฏดๆ |
+| Requirement | Description |
|------|------|
-| MCP Server | ่ฟ่กๅจ `localhost:3000`๏ผไป
ๅจ็บฟๆต่ฏ้่ฆ๏ผ |
-| Doris FE | ่ฟ่กๅจ `127.0.0.1:9030`๏ผไป
ๅจ็บฟๆต่ฏ้่ฆ๏ผ |
-| ่ฎค่ฏ | `admin:admin` |
-| Python | 3.10+๏ผ็ฆป็บฟๆต่ฏไฝฟ็จ้กน็ฎ `.venv`๏ผ`PYTHONPATH=src`๏ผ |
+| MCP Server | Running at `localhost:3000` (online tests only) |
+| Doris FE | Running at `127.0.0.1:9030` (online tests only) |
+| Auth | `admin:admin` |
+| Python | 3.10+; offline tests use the project `.venv` (`PYTHONPATH=src`) |
-ๅฏ้่ฟ็ฏๅขๅ้่ฆ็้ป่ฎค้
็ฝฎ:
+Override defaults with environment variables:
```bash
export MCP_URL=http://192.168.1.100:3000/mcp
export MCP_BASE_URL=http://192.168.1.100:3000
export MCP_TOKEN=admin:admin
export MCP_WORKSPACE=example
-export DORIS_USER=admin # test_web_api.py ็ปๅฝๅญๆฎ
+export DORIS_USER=admin # test_web_api.py login credentials
export DORIS_PASS=admin
-export DORIS_MCP_TEST_DESTRUCTIVE=1 # ๅ
่ฎธ็ ดๅๆง็จไพ๏ผ่งไธ๏ผ
+export DORIS_MCP_TEST_DESTRUCTIVE=1 # Enable destructive cases (see below)
```
-## ่ฟ่กๆนๅผ
+## How to Run
```bash
-# ไป
็ฆป็บฟๅๅ
ๆต่ฏ๏ผๆ ้ๅฏๅจไปปไฝๆๅก๏ผ
+# Offline unit tests only (no services required)
bash test/run_all_tests.sh --offline
-# ๆ็จ unittest discover ่ทๅ
จ้จ็ฆป็บฟ็จไพ๏ผๅจ็บฟๆไปถๆ ๅฏๆถ้็จไพ๏ผไธๅฝฑๅ๏ผ
+# Or use unittest discover for all offline cases; online files collect no unittest cases
PYTHONPATH=src .venv/bin/python -m unittest discover -s test -p 'test_*.py'
-# ๅไธช็ฆป็บฟๆไปถไนๅฏ็ดๆฅ่ฟ่ก๏ผๅทฒๅ
็ฝฎ sys.path ่ชไธพ๏ผ
+# Individual offline files can also run directly (sys.path bootstrap is built in)
.venv/bin/python test/test_watcher.py
-# ๅ
จ้จๆต่ฏ๏ผ้ MCP Server ๅจ็บฟ๏ผ
+# Full suite (requires MCP Server online)
bash test/run_all_tests.sh
-# ไป
ๅ็ๆต่ฏ (ๅฟซ้, ~5็ง)
+# Smoke test only (fast, about 5 seconds)
bash test/run_all_tests.sh --smoke
-# ไป
MCP Tool ๆต่ฏ
+# MCP Tool tests only
bash test/run_all_tests.sh --tools
-# ไป
Web/API ๆต่ฏ
+# Web/API tests only
bash test/run_all_tests.sh --web
-# ๆ็ดๆฅ่ฟ่ก Python๏ผๆๅกๅจไธๅฏ่พพๆถๆดไฝ่ทณ่ฟ๏ผ้ๅบ็ 0๏ผ
+# Or run Python directly; unreachable server skips everything with exit code 0
python test/test_mcp_tools.py
python test/test_web_api.py
```
-## ็ ดๅๆง็จไพ
+## Destructive Cases
-`test_web_api.py` ไธญไปฅไธ็จไพไผๅฝฑๅๅ
ฑไบซๆๅกๅจ็ถๆ๏ผ**้ป่ฎค่ทณ่ฟ**๏ผ
-้ๆพๅผ่ฎพ็ฝฎ `DORIS_MCP_TEST_DESTRUCTIVE=1` ๆๆง่ก๏ผ
+The following cases in `test_web_api.py` affect shared server state and are
+**skipped by default**. Set `DORIS_MCP_TEST_DESTRUCTIVE=1` explicitly to run them:
-- `test_api_staging_discard` โ ไผไธขๅผ็ๅฎ็จๆท็ๆๅญๅๆด
-- `test_api_workspace_create_and_delete` โ ไผๅๅปบ/ๅ ้ค็ๅฎๅทฅไฝๅบ
+- `test_api_staging_discard` โ discards real user staging changes
+- `test_api_workspace_create_and_delete` โ creates/deletes a real workspace
-## ๆต่ฏ่ฆ็็ฉ้ต
+## Coverage Matrix
-### MCP Tool ๆต่ฏ (10 ไธช Tool)
+### MCP Tool Tests (10 Tools)
-| Tool | ๆต่ฏๅบๆฏ | ้ช่ฏ็น |
+| Tool | Scenario | Checks |
|------|----------|--------|
-| `get_query_guide` | ่ทๅๅทฅไฝๆตๆๅผ | ่ฟๅๆๆฌ >100ๅญ๏ผๅซๅ
ณ้ฎ่ฏ |
-| `check_service_health` | ๅบ็ก/่ฏฆ็ป | Doris=connected, workspaces ๅญๅจ |
-| `list_metrics` | ๅ่กจ/ๅ้กต | data ๆฐ็ป, meta.total_count |
-| `list_dimensions_for_metric` | ๆๆๆ ๆฅ็ปดๅบฆ | data ๅ
ๅซ็ปดๅบฆ |
-| `query_metric` | ๅบ็ก/group_by/where/order+limit | 4 ็งๆฅ่ฏขๆจกๅผ |
-| `list_databases` | ๅ่กจ/ๅ้กต | dw,mysql,system_mcp,information_schema |
-| `list_tables` | mysqlๅบ/dw็งๅญไธ่กจ/likeๆจก็ณ | 4 ๅผ ็งๅญ่กจ้ช่ฏ |
-| `describe_table` | summary/full/names | 3 ็บง่ฏฆ็ป็จๅบฆ |
-| `execute_query` | SELECT/VERSION/SHOW/EXPLAIN/max_rows/ๅๆฆๆช | 7 ็งๅบๆฏ |
-| `reload_semantic_layer` | ๆๅจ้่ฝฝ | ่ฟๅ็ปๆๅ JSON๏ผๅซ success ๅญๆฎต |
-
-ๆณจ๏ผ่ฏญไนๅฑ็ธๅ
ณ็จไพ๏ผ`list_dimensions_for_metric`ใ`query_metric` ็ณปๅ๏ผ
-ไป
ๅจ MCP Server ไธๅฏ่พพ๏ผ่ฟๆฅ็ฑปๅผๅธธ๏ผๆถ่ทณ่ฟ๏ผ่ฏญไนๅฑๆชๅฐฑ็ปช็ญๆญ่จๅคฑ่ดฅ
-ไผๅฆๅฎ่ฎกไธบ FAIL๏ผไธๅ้้ป่ทณ่ฟใ
-
-### Web UI & API ๆต่ฏ
-
-| ๅ็ฑป | ๆต่ฏ็น |
+| `get_query_guide` | Get workflow guide | Returned text >100 chars and contains keywords |
+| `check_service_health` | Basic/detail | Doris=connected, workspaces exist |
+| `list_metrics` | List/pagination | data array, meta.total_count |
+| `list_dimensions_for_metric` | Dimensions by metric | data contains dimensions |
+| `query_metric` | Basic/group_by/where/order+limit | 4 query modes |
+| `list_databases` | List/pagination | dw,mysql,system_mcp,information_schema |
+| `list_tables` | mysql DB / dw seed tables / LIKE matching | 4 seed tables verified |
+| `describe_table` | summary/full/names | 3 detail levels |
+| `execute_query` | SELECT/VERSION/SHOW/EXPLAIN/max_rows/write blocking | 7 scenarios |
+| `reload_semantic_layer` | Manual reload | Returns structured JSON with success field |
+
+Note: semantic-layer cases (`list_dimensions_for_metric` and the `query_metric`
+series) skip only when the MCP Server is unreachable due to connection errors.
+Semantic-layer readiness assertion failures are counted as FAIL.
+
+### Web UI & API Tests
+
+| Category | Checks |
|------|--------|
-| **Web UI** | ็ปๅฝ้กต้ขใ็ปๅฝๆไบคใๆช่ฎค่ฏๆฆๆชใๆจกๅ็ฎก็้กต |
-| **REST API** | ่ฏญไนๆไปถๅ่กจใpull ไธ่ฝฝใreload ้่ฝฝใstaging validate/discard |
-| **ๅทฅไฝๅบ** | ๅๅปบ โ ้ช่ฏๅญๅจ โ ๅ ้ค (ๅฎๆด็ๅฝๅจๆ๏ผ็ ดๅๆง๏ผ้ป่ฎค่ทณ่ฟ) |
-| **่ฎค่ฏ** | admin ๆ้ๆงๅถใBearer token ๆ ผๅผ้ช่ฏ |
+| **Web UI** | Login page, login submit, unauthenticated guard, model management page |
+| **REST API** | Semantic file list, pull download, reload, staging validate/discard |
+| **Workspace** | Create โ verify existence โ delete (full lifecycle, destructive, skipped by default) |
+| **Auth** | Admin permission control, Bearer token format validation |
-### ่พน็ & ้่ฏฏๆต่ฏ
+### Boundary & Error Tests
-| ๅบๆฏ | ้ขๆ |
+| Scenario | Expected |
|------|------|
-| ๆ Authorization | 401/403 |
-| ๆ ๆ Token | 401/403 |
-| ๅๆไฝ SQL (INSERT) | ่ขซๆฆๆช |
-| SQL ่ฏญๆณ้่ฏฏ | ๅๅฅฝ้่ฏฏไฟกๆฏ |
-| ้ admin ๅๅปบๅทฅไฝๅบ | 403 |
-| ไธๅญๅจ็ๆๆ | ๅๅฅฝ้่ฏฏ |
-
-### ็ซฏๅฐ็ซฏๆต่ฏ (Agent ๅทฅไฝๆต)
+| No Authorization | 401/403 |
+| Invalid token | 401/403 |
+| Write SQL (INSERT) | Blocked |
+| SQL syntax error | Friendly error message |
+| Non-admin workspace creation | 403 |
+| Missing metric | Friendly error |
+
+### End-to-End Test (Agent Workflow)
```
get_query_guide โ check_service_health โ list_databases
diff --git a/test/run_all_tests.sh b/test/run_all_tests.sh
index 78fe01e..4d164d9 100755
--- a/test/run_all_tests.sh
+++ b/test/run_all_tests.sh
@@ -1,13 +1,13 @@
#!/bin/bash
# =============================================================================
-# test/run_all_tests.sh โ ไธ้ฎ่ฟ่กๆๆๆต่ฏ
+# test/run_all_tests.sh โ run all tests
#
-# ็จๆณ:
-# bash test/run_all_tests.sh # ่ฟ่กๅ
จ้จๆต่ฏ๏ผ้ MCP Server๏ผ
-# bash test/run_all_tests.sh --offline # ไป
็ฆป็บฟๅๅ
ๆต่ฏ๏ผๆ ้ MCP Server๏ผ
-# bash test/run_all_tests.sh --tools # ็ฆป็บฟๆต่ฏ + ไป
Tool ๅจ็บฟๆต่ฏ
-# bash test/run_all_tests.sh --web # ็ฆป็บฟๆต่ฏ + ไป
Web/API ๅจ็บฟๆต่ฏ
-# bash test/run_all_tests.sh --smoke # ็ฆป็บฟๆต่ฏ + ๅ็ๆต่ฏ (ๅฟซ้)
+# Usage:
+# bash test/run_all_tests.sh # run all tests (requires MCP Server)
+# bash test/run_all_tests.sh --offline # offline unit tests only (no MCP Server)
+# bash test/run_all_tests.sh --tools # offline tests + Tool online tests only
+# bash test/run_all_tests.sh --web # offline tests + Web/API online tests only
+# bash test/run_all_tests.sh --smoke # offline tests + smoke test only (fast)
# =============================================================================
set -euo pipefail
@@ -16,7 +16,7 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
MCP_URL="${MCP_URL:-http://localhost:3000/mcp}"
MCP_BASE_URL="${MCP_BASE_URL:-http://localhost:3000}"
-# ไผๅ
ไฝฟ็จ้กน็ฎ venv ็่งฃ้ๅจ๏ผ็ฆป็บฟๆต่ฏไพ่ตๅ
ถไธญ็็ฌฌไธๆนๅ
๏ผ
+# Prefer the project venv interpreter; offline tests depend on its packages.
PYTHON_BIN="${PYTHON_BIN:-$PROJECT_DIR/.venv/bin/python}"
if [ ! -x "$PYTHON_BIN" ]; then
PYTHON_BIN="python3"
@@ -27,18 +27,18 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
-# โโ ๆฃๆฅ MCP Server ๆฏๅฆ่ฟ่ก โโโโโโโโโโโโโโโโโโโโ
+# โโ Check whether MCP Server is running โโโโโโโโโโโโโโโโโโโโ
check_server() {
if curl -s -o /dev/null -w "%{http_code}" "$MCP_BASE_URL/mcp/web/login" > /dev/null 2>&1; then
- echo -e "${GREEN}โ
MCP Server ่ฟ่กไธญ${NC}"
+ echo -e "${GREEN}โ
MCP Server is running${NC}"
else
- echo -e "${RED}โ MCP Server ๆช่ฟ่กๅจ $MCP_BASE_URL${NC}"
- echo " ่ฏทๅ
ๅฏๅจ: cd $PROJECT_DIR && ./start-mcp-server.sh"
+ echo -e "${RED}โ MCP Server is not running at $MCP_BASE_URL${NC}"
+ echo " Start it first: cd $PROJECT_DIR && ./start-mcp-server.sh"
exit 1
fi
}
-# โโ ่ฟ่ก Python ๆต่ฏ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# โโ Run Python tests โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
run_python_test() {
local test_file="$1"
local label="$2"
@@ -47,18 +47,18 @@ run_python_test() {
echo -e "${YELLOW} $label${NC}"
echo -e "${YELLOW}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"
if "$PYTHON_BIN" "$test_file"; then
- echo -e "${GREEN}โ
$label ้่ฟ${NC}"
+ echo -e "${GREEN}โ
$label passed${NC}"
return 0
else
- echo -e "${RED}โ $label ๅคฑ่ดฅ${NC}"
+ echo -e "${RED}โ $label failed${NC}"
return 1
fi
}
-# โโ ๅ็ๆต่ฏ (ๅฟซ้) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# โโ Smoke test (fast) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
smoke_test() {
echo -e "${YELLOW}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"
- echo -e "${YELLOW} ๅ็ๆต่ฏ${NC}"
+ echo -e "${YELLOW} Smoke test${NC}"
echo -e "${YELLOW}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"
# 1. Health check
@@ -114,28 +114,28 @@ smoke_test() {
return 1
fi
- echo -e "${GREEN}โ
ๅ็ๆต่ฏๅ
จ้จ้่ฟ${NC}"
+ echo -e "${GREEN}โ
Smoke test passed${NC}"
return 0
}
-# โโ ็ฆป็บฟๅๅ
ๆต่ฏ๏ผๆ ้ๅฏๅจ MCP Server๏ผ โโโโโโโโโ
+# โโ Offline unit tests (no MCP Server required) โโโโโโโโโ
run_offline_unit_tests() {
- run_python_test "$SCRIPT_DIR/test_sql_validator.py" "SQL ๅช่ฏปๆ ก้ช็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_sensitive_mask.py" "ๆๆไฟกๆฏ่ฑๆ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_pagination.py" "ๅ้กต็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_private_ip_config.py" "่ฏทๆฑ่็น IP ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_deps.py" "่ฟ่กๆถไพ่ตๅฎๅซ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_cross_file_deps.py" "่ทจๆไปถไพ่ตๆฃๆต็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_semantic_grant.py" "่ฏญไน่กจๆๆ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_credential_pass.py" "ๅญๆฎ้ไผ ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_watcher.py" "ๅทฅไฝๅบ Watcher ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_manifest_dims.py" "ๆๆ ็ปดๅบฆๆๅ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_compiler_where.py" "WHERE ๅญ้ข้ๅค็็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_web_session_cookie.py" "Web ไผ่ฏ Cookie ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_session_affinity_proxy_routing.py" "ไผ่ฏไบฒๅไปฃ็่ทฏ็ฑ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_session_affinity_proxy_streaming.py" "ไผ่ฏไบฒๅไปฃ็ๆตๅผ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_session_affinity_proxy_relogin.py" "ไผ่ฏไบฒๅไปฃ็้็ปๅฝ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_session_affinity_proxy_force_target.py" "ไผ่ฏไบฒๅไปฃ็่ฏทๆฑๅฐๅ็ฆป็บฟๅๅ
ๆต่ฏ" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_sql_validator.py" "SQL read-only validation offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_sensitive_mask.py" "Sensitive data masking offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_pagination.py" "Pagination offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_private_ip_config.py" "Request node IP offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_deps.py" "Runtime dependency guard offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_cross_file_deps.py" "Cross-file dependency detection offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_semantic_grant.py" "Semantic table grant offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_credential_pass.py" "Credential pass-through offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_watcher.py" "Workspace watcher offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_manifest_dims.py" "Metric dimension extraction offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_compiler_where.py" "WHERE literal handling offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_web_session_cookie.py" "Web session cookie offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_session_affinity_proxy_routing.py" "Session affinity proxy routing offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_session_affinity_proxy_streaming.py" "Session affinity proxy streaming offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_session_affinity_proxy_relogin.py" "Session affinity proxy relogin offline unit test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_session_affinity_proxy_force_target.py" "Session affinity proxy request target offline unit test" || ((FAIL_COUNT += 1))
}
# โโ Main โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -146,7 +146,7 @@ MODE="${1:-}"
run_offline_unit_tests
if [ "$MODE" = "--offline" ]; then
- : # ็ฆป็บฟๆจกๅผ๏ผไธๆฃๆฅๆๅกๅจใไธ่ทๅจ็บฟ้ถๆฎต
+ : # Offline mode: do not check the server or run online stages.
else
check_server
fi
@@ -155,27 +155,27 @@ case "$MODE" in
--offline)
;;
--tools)
- run_python_test "$SCRIPT_DIR/test_mcp_tools.py" "MCP Tools ๆต่ฏ" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_mcp_tools.py" "MCP Tools test" || ((FAIL_COUNT += 1))
;;
--web)
- run_python_test "$SCRIPT_DIR/test_web_api.py" "Web UI & API ๆต่ฏ" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_web_api.py" "Web UI & API test" || ((FAIL_COUNT += 1))
;;
--smoke)
smoke_test || ((FAIL_COUNT += 1))
;;
"")
- # ๅ
จ้จ่ฟ่ก
+ # Run everything.
smoke_test || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_mcp_tools.py" "MCP Tools ๆต่ฏ" || ((FAIL_COUNT += 1))
- run_python_test "$SCRIPT_DIR/test_web_api.py" "Web UI & API ๆต่ฏ" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_mcp_tools.py" "MCP Tools test" || ((FAIL_COUNT += 1))
+ run_python_test "$SCRIPT_DIR/test_web_api.py" "Web UI & API test" || ((FAIL_COUNT += 1))
;;
*)
echo "Usage: $0 [--offline|--tools|--web|--smoke]"
- echo " (no args) ่ฟ่กๅ
จ้จๆต่ฏ๏ผ้ MCP Server๏ผ"
- echo " --offline ไป
็ฆป็บฟๅๅ
ๆต่ฏ๏ผๆ ้ MCP Server๏ผ"
- echo " --tools ไป
MCP Tool ๆต่ฏ"
- echo " --web ไป
Web UI & API ๆต่ฏ"
- echo " --smoke ไป
ๅ็ๆต่ฏ (ๅฟซ้)"
+ echo " (no args) Run all tests (requires MCP Server)"
+ echo " --offline Offline unit tests only (no MCP Server)"
+ echo " --tools MCP Tool tests only"
+ echo " --web Web UI & API tests only"
+ echo " --smoke Smoke test only (fast)"
exit 1
;;
esac
@@ -183,11 +183,11 @@ esac
echo ""
if [ "$FAIL_COUNT" -eq 0 ]; then
echo -e "${GREEN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"
- echo -e "${GREEN} ๐ ๅ
จ้จๆต่ฏ้่ฟ!${NC}"
+ echo -e "${GREEN} ๐ All tests passed!${NC}"
echo -e "${GREEN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"
else
echo -e "${RED}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"
- echo -e "${RED} โ $FAIL_COUNT ไธชๆต่ฏๅคฑ่ดฅ${NC}"
+ echo -e "${RED} โ $FAIL_COUNT test(s) failed${NC}"
echo -e "${RED}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ${NC}"
exit 1
fi
diff --git a/test/test_cross_file_deps.py b/test/test_cross_file_deps.py
index 9a86881..1530743 100644
--- a/test/test_cross_file_deps.py
+++ b/test/test_cross_file_deps.py
@@ -82,7 +82,7 @@
metrics:
- name: revenue_per_user
type: derived
- description: ไบบๅๆถ่ดน
+ description: Spend per user
expr: total_amount / user_count
"""
@@ -99,7 +99,7 @@
metrics:
- name: order_completion_rate
type: ratio
- description: ่ฎขๅๅฎๆ็
+ description: Order completion rate
numerator: order_count
denominator: total_amount
"""
@@ -120,7 +120,7 @@
metrics:
- name: visit_to_order
type: conversion
- description: ่ฎฟ้ฎๅฐไธๅ่ฝฌๅ
+ description: Visit-to-order conversion
entity: user
"""
diff --git a/test/test_manifest_dims.py b/test/test_manifest_dims.py
index b86ec39..1a6720f 100644
--- a/test/test_manifest_dims.py
+++ b/test/test_manifest_dims.py
@@ -27,22 +27,22 @@
"name": "orders",
"measures": [{"name": "total_amount"}, {"name": "order_count"}],
"dimensions": [
- {"name": "channel", "type": "categorical", "description": "ๆธ ้"},
- {"name": "order_date", "type": "time", "description": "ไธๅๆฅๆ"},
+ {"name": "channel", "type": "categorical", "description": "Channel"},
+ {"name": "order_date", "type": "time", "description": "Order date"},
],
},
{
"name": "users",
"measures": [{"name": "user_count"}],
"dimensions": [
- {"name": "city", "type": "categorical", "description": "ๅๅธ"},
+ {"name": "city", "type": "categorical", "description": "City"},
],
},
{
"name": "visits",
"measures": [{"name": "visit_count"}, {"name": "signup_count"}],
"dimensions": [
- {"name": "source", "type": "categorical", "description": "ๆฅๆบ"},
+ {"name": "source", "type": "categorical", "description": "Source"},
],
},
]
diff --git a/test/test_mcp_tools.py b/test/test_mcp_tools.py
index c959c63..36de5f7 100644
--- a/test/test_mcp_tools.py
+++ b/test/test_mcp_tools.py
@@ -1,15 +1,15 @@
#!/usr/bin/env python3
"""
-MCP Tool ๆต่ฏ็จไพ โ ่ฆ็ๅ
จ้จ 10 ไธช Tool
+MCP Tool test cases โ covers all 10 tools
-่ฟ่ก็ฏๅข:
- - Doris FE 127.0.0.1:9030๏ผadmin:admin
+Runtime:
+ - Doris FE 127.0.0.1:9030, admin:admin
- MCP Server http://localhost:3000/mcp
- Python 3.10+
-็จๆณ:
+Usage:
python -m pytest test/test_mcp_tools.py -v
- ๆ็ดๆฅ: python test/test_mcp_tools.py
+ Or run directly: python test/test_mcp_tools.py
"""
import json
@@ -19,7 +19,7 @@
import urllib.request
import urllib.error
-# โโ ้
็ฝฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# โโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
MCP_URL = os.environ.get("MCP_URL", "http://localhost:3000/mcp")
AUTH_TOKEN = os.environ.get("MCP_TOKEN", "admin:admin")
WORKSPACE = os.environ.get("MCP_WORKSPACE", "example")
@@ -30,12 +30,12 @@
"Authorization": f"Bearer {AUTH_TOKEN}",
}
-# ไป
่ฟๆฅ็ฑปๅผๅธธๅ
่ฎธ่ทณ่ฟๆต่ฏ๏ผAssertionError ๅฟ
้กปๅๅบๆฅ
+# Only connection-type errors may skip tests; AssertionError must surface.
_CONN_ERRORS = (urllib.error.URLError, ConnectionError, TimeoutError)
def _call_tool(name: str, arguments: dict) -> dict:
- """่ฐ็จ MCP Tool๏ผ่งฃๆ SSE ๅๅบ"""
+ """Call an MCP tool and parse the SSE response."""
payload = {
"jsonrpc": "2.0",
"id": 1,
@@ -51,7 +51,7 @@ def _call_tool(name: str, arguments: dict) -> dict:
except urllib.error.HTTPError as e:
return {"error": str(e.code), "body": e.read().decode()}
- # ่งฃๆ SSE: "event: message\ndata:
\n\n"
+ # Parse SSE: "event: message\ndata: \n\n"
data_json = body
if body.startswith("event: message"):
data_line = [l for l in body.split("\n") if l.startswith("data:")]
@@ -64,7 +64,7 @@ def _call_tool(name: str, arguments: dict) -> dict:
def _assert_success(result: dict):
- """ๆญ่จ่ฐ็จๆๅ"""
+ """Assert that the tool call succeeded."""
assert "result" in result, f"Expected 'result' key: {json.dumps(result, ensure_ascii=False)[:500]}"
assert not result.get("isError"), f"Tool returned error: {json.dumps(result, ensure_ascii=False)[:500]}"
content = result["result"]["content"][0]["text"]
@@ -78,15 +78,15 @@ def _assert_success(result: dict):
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_get_query_guide():
- """Tool #1: ่ทๅๅทฅไฝๆตๆๅผ โ ็ฌฌไธๆญฅๅฟ
่ฐ"""
+ """Tool #1: get workflow guide โ mandatory first step."""
result = _call_tool("get_query_guide", {})
assert "result" in result
text = result["result"]["content"][0]["text"]
assert len(text) > 100, f"Guide too short: {len(text)} chars"
- # ๅบๅ
ๅซๅ
ณ้ฎๆๅผๅ
ๅฎน๏ผๅฆ่ฏญไนๅฑไฝฟ็จ่ฏดๆใๅทฅๅ
ท่ฐ็จ้กบๅบ็ญ
- assert any(kw in text.lower() for kw in ["ๆๆ ", "่ฏญไน", "metric", "query", "workspace"]), \
+ # Must contain key guide content, such as metric layer usage and tool order.
+ assert any(kw in text.lower() for kw in ["metric", "semantic", "query", "workspace"]), \
f"Guide missing key content: {text[:200]}"
- print(" โ
get_query_guide ่ฟๅๆๆๆๅผ")
+ print(" โ
get_query_guide returned a valid guide")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -94,18 +94,18 @@ def test_get_query_guide():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_check_service_health_basic():
- """Tool #2: ๅบ็กๅฅๅบทๆฃๆฅ"""
+ """Tool #2: basic health check."""
data = _assert_success(_call_tool("check_service_health", {}))
assert data["data"]["doris"] == "connected", f"Doris not connected: {data}"
assert "workspaces" in data["data"], "Missing workspaces"
- print(f" โ
Doris ่ฟๆฅๆญฃๅธธ, workspaces: {list(data['data']['workspaces'].keys())}")
+ print(f" โ
Doris connected, workspaces: {list(data['data']['workspaces'].keys())}")
def test_check_service_health_detail():
- """Tool #2: ่ฏฆ็ปๅฅๅบทๆฃๆฅ"""
+ """Tool #2: detailed health check."""
data = _assert_success(_call_tool("check_service_health", {"detail": True}))
assert data["data"]["doris"] == "connected"
- print(f" โ
่ฏฆ็ปๅฅๅบทๆฃๆฅ้่ฟ")
+ print(" โ
Detailed health check passed")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -113,21 +113,21 @@ def test_check_service_health_detail():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_list_metrics():
- """Tool #3: ๅๅบๅทฅไฝๅบๆๆ """
+ """Tool #3: list workspace metrics."""
data = _assert_success(
_call_tool("list_metrics", {"workspace": WORKSPACE})
)
assert "data" in data
- print(f" โ
ๆๆ ๅ่กจ: {data['data']}")
+ print(f" โ
Metric list: {data['data']}")
def test_list_metrics_pagination():
- """Tool #3: ๅ้กตๆต่ฏ"""
+ """Tool #3: pagination test."""
data = _assert_success(
_call_tool("list_metrics", {"workspace": WORKSPACE, "page_size": 2})
)
assert "meta" in data, "Missing pagination meta"
- print(f" โ
ๅ้กตๆญฃๅธธ, total_count={data.get('meta', {}).get('total_count', 'N/A')}")
+ print(f" โ
Pagination OK, total_count={data.get('meta', {}).get('total_count', 'N/A')}")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -135,8 +135,8 @@ def test_list_metrics_pagination():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_list_dimensions_for_metric():
- """Tool #4: ๅๅบๆๆ ็ปดๅบฆ"""
- # ้่ฆ่ฏญไนๅฑๅฐฑ็ปช๏ผไป
ๅจ MCP Server ไธๅฏ่พพ๏ผ่ฟๆฅ็ฑปๅผๅธธ๏ผๆถ่ทณ่ฟ
+ """Tool #4: list metric dimensions."""
+ # Requires a ready semantic layer; skip only on MCP Server connection errors.
try:
data = _assert_success(
_call_tool("list_dimensions_for_metric", {
@@ -145,10 +145,10 @@ def test_list_dimensions_for_metric():
})
)
assert "data" in data
- print(f" โ
total_amount ็ปดๅบฆ: {data['data']}")
+ print(f" โ
total_amount dimensions: {data['data']}")
except _CONN_ERRORS as e:
- # ๅชๆ่ฟๆฅ็ฑปๅผๅธธๆ่ทณ่ฟ๏ผ่ฏญไนๅฑๆชๅฐฑ็ปช็ญๆญ่จๅคฑ่ดฅๅฟ
้กปๅๅบๆฅ
- raise unittest.SkipTest(f"่ทณ่ฟ๏ผMCP Server ไธๅฏ่พพ: {e}")
+ # Only connection errors may skip; semantic readiness assertions must surface.
+ raise unittest.SkipTest(f"Skipped: MCP Server is unreachable: {e}")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -156,7 +156,7 @@ def test_list_dimensions_for_metric():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_query_metric_basic():
- """Tool #5: ๆ ธๅฟ โ ่ฏญไนๆฅ่ฏขๅไธชๆๆ """
+ """Tool #5: core semantic query for one metric."""
try:
data = _assert_success(
_call_tool("query_metric", {
@@ -165,14 +165,14 @@ def test_query_metric_basic():
})
)
assert "data" in data
- print(f" โ
query_metric ็ปๆ: {data['data']}")
+ print(f" โ
query_metric result: {data['data']}")
except _CONN_ERRORS as e:
- # ๅชๆ่ฟๆฅ็ฑปๅผๅธธๆ่ทณ่ฟ๏ผ่ฏญไนๅฑๆชๅฐฑ็ปช็ญๆญ่จๅคฑ่ดฅๅฟ
้กปๅๅบๆฅ
- raise unittest.SkipTest(f"่ทณ่ฟ๏ผMCP Server ไธๅฏ่พพ: {e}")
+ # Only connection errors may skip; semantic readiness assertions must surface.
+ raise unittest.SkipTest(f"Skipped: MCP Server is unreachable: {e}")
def test_query_metric_with_group_by():
- """Tool #5: ๅธฆ group_by ็่ฏญไนๆฅ่ฏข"""
+ """Tool #5: semantic query with group_by."""
try:
data = _assert_success(
_call_tool("query_metric", {
@@ -182,14 +182,14 @@ def test_query_metric_with_group_by():
})
)
assert "data" in data
- print(f" โ
ๅคๆๆ +group_by ๆฅ่ฏขๆๅ")
+ print(" โ
Multi-metric group_by query succeeded")
except _CONN_ERRORS as e:
- # ๅชๆ่ฟๆฅ็ฑปๅผๅธธๆ่ทณ่ฟ๏ผ่ฏญไนๅฑๆชๅฐฑ็ปช็ญๆญ่จๅคฑ่ดฅๅฟ
้กปๅๅบๆฅ
- raise unittest.SkipTest(f"่ทณ่ฟ๏ผMCP Server ไธๅฏ่พพ: {e}")
+ # Only connection errors may skip; semantic readiness assertions must surface.
+ raise unittest.SkipTest(f"Skipped: MCP Server is unreachable: {e}")
def test_query_metric_with_where():
- """Tool #5: ๅธฆ where ๆกไปถ็่ฏญไนๆฅ่ฏข"""
+ """Tool #5: semantic query with where."""
try:
data = _assert_success(
_call_tool("query_metric", {
@@ -198,14 +198,14 @@ def test_query_metric_with_where():
"where": '{{ Dimension("user__city") }} = \'Beijing\'',
})
)
- print(f" โ
ๅธฆ where ๆกไปถ็ๆฅ่ฏขๆๅ")
+ print(" โ
Query with where succeeded")
except _CONN_ERRORS as e:
- # ๅชๆ่ฟๆฅ็ฑปๅผๅธธๆ่ทณ่ฟ๏ผ่ฏญไนๅฑๆชๅฐฑ็ปช็ญๆญ่จๅคฑ่ดฅๅฟ
้กปๅๅบๆฅ
- raise unittest.SkipTest(f"่ทณ่ฟ๏ผMCP Server ไธๅฏ่พพ: {e}")
+ # Only connection errors may skip; semantic readiness assertions must surface.
+ raise unittest.SkipTest(f"Skipped: MCP Server is unreachable: {e}")
def test_query_metric_with_order_and_limit():
- """Tool #5: ๆๅบ+ๅ้กต"""
+ """Tool #5: ordering and pagination."""
try:
data = _assert_success(
_call_tool("query_metric", {
@@ -216,10 +216,10 @@ def test_query_metric_with_order_and_limit():
"limit": 3,
})
)
- print(f" โ
ๆๅบ+ๅ้กตๆฅ่ฏขๆๅ")
+ print(" โ
Order and pagination query succeeded")
except _CONN_ERRORS as e:
- # ๅชๆ่ฟๆฅ็ฑปๅผๅธธๆ่ทณ่ฟ๏ผ่ฏญไนๅฑๆชๅฐฑ็ปช็ญๆญ่จๅคฑ่ดฅๅฟ
้กปๅๅบๆฅ
- raise unittest.SkipTest(f"่ทณ่ฟ๏ผMCP Server ไธๅฏ่พพ: {e}")
+ # Only connection errors may skip; semantic readiness assertions must surface.
+ raise unittest.SkipTest(f"Skipped: MCP Server is unreachable: {e}")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -227,7 +227,7 @@ def test_query_metric_with_order_and_limit():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_list_databases():
- """Tool #6: ๅๅบๆๆๆฐๆฎๅบ"""
+ """Tool #6: list all databases."""
data = _assert_success(_call_tool("list_databases", {}))
databases = data["data"]
assert isinstance(databases, list)
@@ -235,17 +235,17 @@ def test_list_databases():
expected = {"dw", "mysql", "information_schema", "system_mcp"}
found = set(databases) & expected
assert len(found) >= 2, f"Missing expected databases: {expected - found}"
- print(f" โ
ๆฐๆฎๅบๅ่กจ: {databases}")
+ print(f" โ
Database list: {databases}")
def test_list_databases_pagination():
- """Tool #6: ๅ้กต"""
+ """Tool #6: pagination."""
data = _assert_success(
_call_tool("list_databases", {"page_size": 2})
)
assert "meta" in data
assert data["meta"]["total_count"] >= 2
- print(f" โ
ๅ้กต: total={data['meta']['total_count']}")
+ print(f" โ
Pagination: total={data['meta']['total_count']}")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -253,17 +253,17 @@ def test_list_databases_pagination():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_list_tables_mysql():
- """Tool #7: ๅๅบ mysql ๅบ็่กจ"""
+ """Tool #7: list tables in mysql."""
data = _assert_success(
_call_tool("list_tables", {"database": "mysql"})
)
assert "data" in data
assert "user" in data["data"], f"Expected 'user' table: {data['data']}"
- print(f" โ
mysql ่กจ: {data['data']}")
+ print(f" โ
mysql tables: {data['data']}")
def test_list_tables_dw():
- """Tool #7: ๅๅบ dw ๅบ็่กจ"""
+ """Tool #7: list tables in dw."""
data = _assert_success(
_call_tool("list_tables", {"database": "dw"})
)
@@ -271,16 +271,16 @@ def test_list_tables_dw():
expected = {"orders", "users", "products", "dim_date"}
assert expected.issubset(set(dw_tables)), \
f"Missing seed tables: {expected - set(dw_tables)}"
- print(f" โ
dw ็งๅญ่กจ: {dw_tables}")
+ print(f" โ
dw seed tables: {dw_tables}")
def test_list_tables_with_like():
- """Tool #7: ๆจก็ณๅน้
"""
+ """Tool #7: LIKE matching."""
data = _assert_success(
_call_tool("list_tables", {"database": "mysql", "like": "user%"})
)
assert "user" in data["data"]
- print(f" โ
like ๅน้
ๆญฃๅธธ")
+ print(" โ
LIKE matching OK")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -288,7 +288,7 @@ def test_list_tables_with_like():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_describe_table_summary():
- """Tool #8: ่กจ็ปๆ โ summary ็บงๅซ"""
+ """Tool #8: table schema โ summary level."""
data = _assert_success(
_call_tool("describe_table", {
"database": "dw",
@@ -297,11 +297,11 @@ def test_describe_table_summary():
})
)
assert "data" in data
- print(f" โ
dw.orders ็ปๆ: {data['data']}")
+ print(f" โ
dw.orders schema: {data['data']}")
def test_describe_table_full():
- """Tool #8: ่กจ็ปๆ โ full ็บงๅซ"""
+ """Tool #8: table schema โ full level."""
data = _assert_success(
_call_tool("describe_table", {
"database": "dw",
@@ -310,11 +310,11 @@ def test_describe_table_full():
})
)
assert "data" in data
- print(f" โ
dw.orders ๅฎๆด็ปๆ")
+ print(" โ
dw.orders full schema")
def test_describe_table_names():
- """Tool #8: ่กจ็ปๆ โ names ็บงๅซ๏ผไป
ๅๅ๏ผ"""
+ """Tool #8: table schema โ names level (column names only)."""
data = _assert_success(
_call_tool("describe_table", {
"database": "dw",
@@ -323,7 +323,7 @@ def test_describe_table_names():
})
)
assert "data" in data
- print(f" โ
dw.orders ๅๅ")
+ print(" โ
dw.orders column names")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -331,24 +331,24 @@ def test_describe_table_names():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_execute_query_select():
- """Tool #9: ่ฃธ SQL โ ๅบๆฌ SELECT"""
+ """Tool #9: raw SQL โ basic SELECT."""
data = _assert_success(
_call_tool("execute_query", {"sql": "SELECT 1 AS n"})
)
assert data["data"]["rows"][0]["n"] == 1
- print(f" โ
SELECT 1 ่ฟๅๆญฃ็กฎ")
+ print(" โ
SELECT 1 returned the expected value")
def test_execute_query_version():
- """Tool #9: ่ฃธ SQL โ Doris ็ๆฌ"""
+ """Tool #9: raw SQL โ Doris version."""
data = _assert_success(
_call_tool("execute_query", {"sql": "SELECT VERSION()"})
)
- print(f" โ
Doris ็ๆฌ: {data['data']['rows'][0]}")
+ print(f" โ
Doris version: {data['data']['rows'][0]}")
def test_execute_query_with_database():
- """Tool #9: ่ฃธ SQL โ ๆๅฎๆฐๆฎๅบ"""
+ """Tool #9: raw SQL โ explicit database."""
data = _assert_success(
_call_tool("execute_query", {
"sql": "SELECT count(*) AS cnt FROM orders",
@@ -356,11 +356,11 @@ def test_execute_query_with_database():
})
)
assert data["data"]["rows"][0]["cnt"] == 12
- print(f" โ
dw.orders ๆ 12 ๆกๆฐๆฎ")
+ print(" โ
dw.orders has 12 rows")
def test_execute_query_with_max_rows():
- """Tool #9: ่ฃธ SQL โ ้ๅถ่ฟๅ่กๆฐ"""
+ """Tool #9: raw SQL โ limit returned rows."""
data = _assert_success(
_call_tool("execute_query", {
"sql": "SELECT * FROM dw.dim_date",
@@ -368,31 +368,31 @@ def test_execute_query_with_max_rows():
})
)
assert data["meta"]["row_count"] <= 5
- print(f" โ
max_rows ้ๅถ็ๆ, ่ฟๅ {data['meta']['row_count']} ่ก")
+ print(f" โ
max_rows enforced, returned {data['meta']['row_count']} rows")
def test_execute_query_show():
- """Tool #9: ่ฃธ SQL โ SHOW ่ฏญๅฅ"""
+ """Tool #9: raw SQL โ SHOW statement."""
data = _assert_success(
_call_tool("execute_query", {"sql": "SHOW DATABASES"})
)
assert len(data["data"]["rows"]) >= 2
- print(f" โ
SHOW DATABASES ๆๅ")
+ print(" โ
SHOW DATABASES succeeded")
def test_execute_query_explain():
- """Tool #9: ่ฃธ SQL โ EXPLAIN ่ฏญๅฅ"""
+ """Tool #9: raw SQL โ EXPLAIN statement."""
data = _assert_success(
_call_tool("execute_query", {
"sql": "EXPLAIN SELECT count(*) FROM dw.orders"
})
)
assert "data" in data
- print(f" โ
EXPLAIN ๆๅ")
+ print(" โ
EXPLAIN succeeded")
def test_execute_query_blocked_write():
- """Tool #9: ๅช่ฏปๆ ก้ช โ ๆ็ป INSERT"""
+ """Tool #9: read-only validation โ reject INSERT."""
result = _call_tool("execute_query", {
"sql": "INSERT INTO dw.orders VALUES (99,1,1,100,'online','done','2024-01-01')"
})
@@ -400,7 +400,7 @@ def test_execute_query_blocked_write():
"reject" in str(result).lower() or "not allowed" in str(result).lower() or \
"forbidden" in str(result).lower() or "only" in str(result).lower(), \
f"Should block write SQL: {result}"
- print(f" โ
ๅๆไฝ่ขซๆญฃ็กฎๆฆๆช")
+ print(" โ
Write operation was blocked correctly")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@@ -408,23 +408,23 @@ def test_execute_query_blocked_write():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_reload_semantic_layer():
- """Tool #10: ๆๅจ้่ฝฝ่ฏญไนๅฑ"""
+ """Tool #10: manually reload semantic layer."""
result = _call_tool("reload_semantic_layer", {"workspace": WORKSPACE})
- # ๅฟ
้กป่ฟๅๅๆณ็ JSON-RPC result๏ผcontent ไธบๅฏ่งฃๆ็็ปๆๅ JSON
- # ๏ผsuccess_response / error_response ้ฝๅธฆ success ๅญๆฎต๏ผ
+ # Must return a valid JSON-RPC result with parseable structured JSON content.
+ # success_response / error_response both include the success field.
assert "result" in result, f"Expected JSON-RPC result: {json.dumps(result, ensure_ascii=False)[:500]}"
content = result["result"]["content"][0]["text"]
data = json.loads(content)
assert "success" in data, f"Payload missing 'success' field: {data}"
- print(f" โ
reload ่ฐ็จๆๅ (success={data['success']})")
+ print(f" โ
reload call succeeded (success={data['success']})")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-# ่ฎค่ฏ/้่ฏฏๅค็ๆต่ฏ
+# Auth/error handling tests
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_auth_required():
- """ๆ Authorization header ๅบๆ็ป"""
+ """No Authorization header should be rejected."""
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "tools/call",
@@ -442,11 +442,11 @@ def test_auth_required():
assert False, "Should have returned error for unauthenticated request"
except urllib.error.HTTPError as e:
assert e.code in (401, 403), f"Expected 401/403, got {e.code}"
- print(f" โ
ๆช่ฎค่ฏ่ฏทๆฑ่ฟๅ {e.code}")
+ print(f" โ
Unauthenticated request returned {e.code}")
def test_invalid_token():
- """ๆ ๆ Token ๅบ่ขซๆ็ป"""
+ """Invalid token should be rejected."""
bad_headers = {**HEADERS, "Authorization": "Bearer fake:fake"}
payload = {
"jsonrpc": "2.0", "id": 1,
@@ -461,28 +461,28 @@ def test_invalid_token():
assert False, "Should have rejected invalid credentials"
except urllib.error.HTTPError as e:
assert e.code in (401, 403), f"Expected 401/403, got {e.code}"
- print(f" โ
ๆ ๆ Token ่ฟๅ {e.code}")
+ print(f" โ
Invalid token returned {e.code}")
def test_execute_query_syntax_error():
- """SQL ่ฏญๆณ้่ฏฏๅบ่ฟๅๅๅฅฝ้่ฏฏ"""
+ """SQL syntax errors should return a friendly error."""
result = _call_tool("execute_query", {"sql": "SELECTT 1"})
- # ๅฏ่ฝๆฏ error response ๆ success=false
+ # May be an error response or success=false.
is_error = (
result.get("isError") or
("error" in str(result).lower()) or
("syntax" in str(result).lower())
)
assert is_error, f"Should report syntax error: {result}"
- print(f" โ
SQL ่ฏญๆณ้่ฏฏๆญฃ็กฎๅค็")
+ print(" โ
SQL syntax error handled correctly")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-# ็ซฏๅฐ็ซฏๅทฅไฝๆตๆต่ฏ
+# End-to-end workflow test
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_agent_workflow():
- """ๆจกๆ AI Agent ็ๆ ๅๅทฅไฝๆต: guide โ health โ databases โ tables โ query"""
+ """Simulate the standard AI Agent workflow: guide โ health โ databases โ tables โ query."""
# Step 1: guide
r1 = _call_tool("get_query_guide", {})
assert "result" in r1
@@ -519,13 +519,13 @@ def test_agent_workflow():
_assert_success(r6)
print(" Step 6: execute_query โ
")
- print(" ๐ ๅฎๆด Agent ๅทฅไฝๆต้่ฟ!")
+ print(" ๐ Complete Agent workflow passed!")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if __name__ == "__main__":
print("=" * 60)
- print("MCP Tool ๆต่ฏๅผๅง")
+ print("MCP Tool tests starting")
print(f" URL: {MCP_URL}")
print(f" Workspace: {WORKSPACE}")
print("=" * 60)
@@ -567,10 +567,10 @@ def test_agent_workflow():
("execute_query (block write)", test_execute_query_blocked_write),
# Tool #10
("reload_semantic_layer", test_reload_semantic_layer),
- # ่ฎค่ฏ
+ # Auth
("auth required", test_auth_required),
("invalid token", test_invalid_token),
- # ้่ฏฏๅค็
+ # Error handling
("SQL syntax error", test_execute_query_syntax_error),
# E2E
("Agent workflow", test_agent_workflow),
@@ -589,7 +589,7 @@ def test_agent_workflow():
print(f" โ FAIL: {e}")
failed += 1
except Exception as e:
- if "่ทณ่ฟ" in str(e) or "not_ready" in str(e):
+ if "Skipped" in str(e) or "not_ready" in str(e):
print(f" โ ๏ธ SKIP: {e}")
skipped += 1
else:
@@ -597,7 +597,7 @@ def test_agent_workflow():
failed += 1
print(f"\n{'='*60}")
- print(f"็ปๆ: {passed} passed, {failed} failed, {skipped} skipped")
+ print(f"Result: {passed} passed, {failed} failed, {skipped} skipped")
print(f"{'='*60}")
if failed > 0:
diff --git a/test/test_session_affinity_proxy_relogin.py b/test/test_session_affinity_proxy_relogin.py
index 647ee94..7f96f9a 100644
--- a/test/test_session_affinity_proxy_relogin.py
+++ b/test/test_session_affinity_proxy_relogin.py
@@ -1,8 +1,8 @@
-"""็ฆป็บฟๆต่ฏ๏ผไธๆธธ่็นไธๅฏ่พพๆถๆธ
้ค Session Cookie ๅนถๅผๅฏผ้ๆฐ็ปๅฝใ
+"""Offline test: clear the session cookie and redirect to login when upstream is unreachable.
-็ฎๆ ่็นๅฎๆบๅไปฃ็ๅฟ
้กป๏ผ
-1. ๆธ
้คๆต่งๅจไธญ็ ``doris_mcp_session`` Cookie๏ผSet-Cookie: max-age=0๏ผ
-2. ่ฟๅ 303 ้ๅฎๅๅฐ ``/mcp/web/login``๏ผ่ฎฉ็จๆทๅจๅฅๅบท่็น้ๆฐ็ปๅฝ
+After the target node goes down, the proxy must:
+1. Clear the browser's ``doris_mcp_session`` cookie (Set-Cookie: max-age=0)
+2. Return a 303 redirect to ``/mcp/web/login`` so the user can log in on a healthy node
"""
from __future__ import annotations
@@ -57,7 +57,7 @@ async def send(message: dict[str, Any]) -> None:
class UpstreamUnreachableReloginTests(unittest.IsolatedAsyncioTestCase):
- """ไธๆธธ่็นไธๅฏ่พพๆถๅฟ
้กปๆธ
้ค Cookie ๅนถๅผๅฏผ้็ปๅฝใ"""
+ """Unreachable upstream nodes must clear cookies and force relogin."""
def _make_proxy(
self,
@@ -77,7 +77,7 @@ async def local_app(scope: Any, receive: Any, send: Any) -> None:
)
return proxy, client
- # โโ ่ฟๆฅๅคฑ่ดฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ # โโ Connection failures โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def test_connect_error_clears_cookie_and_redirects_to_login(self) -> None:
def handler(request: httpx.Request) -> httpx.Response:
@@ -113,7 +113,7 @@ def handler(request: httpx.Request) -> httpx.Response:
self._assert_clears_cookie_and_redirects_to_login(sent)
- # โโ ่ถ
ๆถ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ # โโ Timeouts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def test_read_timeout_clears_cookie_and_redirects_to_login(self) -> None:
def handler(request: httpx.Request) -> httpx.Response:
@@ -166,7 +166,7 @@ def handler(request: httpx.Request) -> httpx.Response:
self._assert_clears_cookie_and_redirects_to_login(sent)
- # โโ logout ไธๅฏ่พพไน่ฆๆธ
้ค Cookie โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ # โโ Unreachable logout must still clear cookies โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def test_logout_upstream_unreachable_still_clears_local_cookie(self) -> None:
def handler(request: httpx.Request) -> httpx.Response:
@@ -185,7 +185,7 @@ def handler(request: httpx.Request) -> httpx.Response:
self._assert_clears_cookie_and_redirects_to_login(sent)
- # โโ ๆญฃๅธธไปฃ็ไธ่ฟๅบฆๆธ
็ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ # โโ Successful proxying must not over-clear โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def test_successful_upstream_does_not_clear_cookie(self) -> None:
def handler(request: httpx.Request) -> httpx.Response:
@@ -203,7 +203,7 @@ def handler(request: httpx.Request) -> httpx.Response:
await client.aclose()
self.assertEqual(sent[0]["status"], 200)
- # ไธๆธธ็ Set-Cookie ้ไผ ๏ผไธ้ขๅคๆณจๅ
ฅๆธ
Cookie
+ # Upstream Set-Cookie passes through without injecting an extra clearing cookie.
set_cookie_values = [
v for n, v in sent[0]["headers"] if n.lower() == b"set-cookie"
]
@@ -231,7 +231,7 @@ def handler(request: httpx.Request) -> httpx.Response:
{n.lower() for n, _ in sent[0]["headers"]},
)
- # โโ ไธๅ
่ฎธๆณ้ฒ็ฎๆ IP โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ # โโ Target IP must not leak โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async def test_unreachable_response_does_not_leak_target_ip(self) -> None:
def handler(request: httpx.Request) -> httpx.Response:
@@ -253,7 +253,7 @@ def handler(request: httpx.Request) -> httpx.Response:
header_bytes = b"".join(n + b":" + v for n, v in sent[0]["headers"])
self.assertNotIn(REMOTE_IP.encode(), header_bytes)
- # โโ ๆญ่จ่พ
ๅฉ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ # โโ Assertion helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def _assert_clears_cookie_and_redirects_to_login(
self, sent: list[dict[str, Any]]
@@ -262,17 +262,17 @@ def _assert_clears_cookie_and_redirects_to_login(
self.assertEqual(start["type"], "http.response.start")
self.assertIn(start["status"], (302, 303))
- # Location ๆๅ็ปๅฝ้กต
+ # Location points to the login page.
location = [
v for n, v in start["headers"] if n.lower() == b"location"
]
self.assertEqual(location, [b"/mcp/web/login"])
- # Set-Cookie ๆธ
้ค doris_mcp_session
+ # Set-Cookie clears doris_mcp_session.
set_cookie_headers = [
v for n, v in start["headers"] if n.lower() == b"set-cookie"
]
- # ่ณๅฐๆไธไธชๆธ
Cookie ็ Set-Cookie
+ # At least one Set-Cookie header must clear the cookie.
clearing = [
v for v in set_cookie_headers
if _WEBUI_SESSION_COOKIE in v and (b"Max-Age=0" in v or b"Expires=" in v)
diff --git a/test/test_web_api.py b/test/test_web_api.py
index 447d873..9b980b0 100644
--- a/test/test_web_api.py
+++ b/test/test_web_api.py
@@ -1,22 +1,22 @@
#!/usr/bin/env python3
"""
-Web UI & REST API ๆต่ฏ็จไพ
+Web UI & REST API test cases
-่ฆ็:
- - Web UI ้กต้ข่ฎฟ้ฎ (login, models, file editor)
+Coverage:
+ - Web UI page access (login, models, file editor)
- REST API (semantic push/pull/validate/commit)
- - ๅทฅไฝๅบ็ฎก็ API (create/delete)
- - ่ฎค่ฏ (login, logout, session)
+ - Workspace management API (create/delete)
+ - Auth (login, logout, session)
-็จๆณ:
+Usage:
python test/test_web_api.py
-็ฏๅขๅ้:
- MCP_BASE_URL ๆๅกๅจๅฐๅ (้ป่ฎค http://localhost:3000)
- MCP_TOKEN Bearer token (้ป่ฎค admin:admin)
- DORIS_USER / DORIS_PASS Doris ็ปๅฝๅญๆฎ (้ป่ฎค admin / admin)
- DORIS_MCP_TEST_DESTRUCTIVE=1 ๆๆง่ก็ ดๅๆง็จไพ (discard ๆๅญๅๆดใ
- ๅๅปบ/ๅ ้คๅทฅไฝๅบ)๏ผ้ป่ฎค่ทณ่ฟ
+Environment variables:
+ MCP_BASE_URL Server URL (default http://localhost:3000)
+ MCP_TOKEN Bearer token (default admin:admin)
+ DORIS_USER / DORIS_PASS Doris login credentials (default admin / admin)
+ DORIS_MCP_TEST_DESTRUCTIVE=1 Enable destructive cases (discard staging
+ changes and create/delete workspaces).
"""
import json
@@ -28,14 +28,14 @@
import http.cookiejar
from urllib.parse import urlencode
-# โโ ้
็ฝฎ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+# โโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
BASE_URL = os.environ.get("MCP_BASE_URL", "http://localhost:3000")
AUTH_TOKEN = os.environ.get("MCP_TOKEN", "admin:admin")
WORKSPACE = os.environ.get("MCP_WORKSPACE", "example")
TEST_DORIS_USER = os.environ.get("DORIS_USER", "admin")
TEST_DORIS_PASS = os.environ.get("DORIS_PASS", "admin")
-# ็ ดๅๆง็จไพ๏ผไผ discard ๅ
ฑไบซๆๅกๅจไธ็็ๅฎๆๅญๅๆดใๅๅปบ/ๅ ้คๅทฅไฝๅบ๏ผ
-# ้ป่ฎค่ทณ่ฟ๏ผๆพๅผ่ฎพ็ฝฎ DORIS_MCP_TEST_DESTRUCTIVE=1 ๆๆง่ก
+# Destructive cases can discard real staging changes and create/delete
+# workspaces on a shared server. They are skipped unless explicitly enabled.
DESTRUCTIVE = os.environ.get("DORIS_MCP_TEST_DESTRUCTIVE") == "1"
HEADERS = {
@@ -48,18 +48,18 @@
def _require_destructive():
- """็ ดๅๆง็จไพๅฎๅซ๏ผๆช่ฎพ็ฝฎ DORIS_MCP_TEST_DESTRUCTIVE=1 ๆถ่ทณ่ฟใ"""
+ """Skip destructive cases unless DORIS_MCP_TEST_DESTRUCTIVE=1 is set."""
if not DESTRUCTIVE:
raise unittest.SkipTest(
- "่ทณ่ฟ๏ผ็ ดๅๆง็จไพ้ DORIS_MCP_TEST_DESTRUCTIVE=1 ๆๆง่ก"
+ "Skipped: destructive cases require DORIS_MCP_TEST_DESTRUCTIVE=1"
)
def _server_reachable() -> bool:
- """ๆขๆต MCP Server ๆฏๅฆๅฏ่พพ๏ผ็ปๅฝ้กตๅบ่ฟๅ 200๏ผใ
+ """Probe whether the MCP Server is reachable (login page should return 200).
- ๅช่ฎค 200๏ผ่ฟๆฅๆ็ปๆฏๆชๅฏๅจ๏ผ502/404 ็ญ่ฏดๆ็ซฏๅฃ่ขซๅ
ถไปๆๅกๅ ็จ๏ผ
- ๅๆ ท่งไธบไธๅฏ่พพ๏ผ้ฟๅ
ๅฏน้่ฏฏ็็ฎๆ ่ทๅฎๆดๅฅ็จไพใ
+ Only 200 is accepted: connection refused means the server is not running,
+ while 502/404 suggests another service is bound to the port.
"""
req = urllib.request.Request(f"{BASE_URL}/mcp/web/login", headers={})
try:
@@ -70,7 +70,7 @@ def _server_reachable() -> bool:
def _get(path: str, headers: dict = None, expect_code: int = 200) -> dict:
- """GET ่ฏทๆฑ๏ผ่ฟๅ (status_code, body_dict)"""
+ """GET request, returning (status_code, body_dict)."""
req = urllib.request.Request(f"{BASE_URL}{path}", headers=headers or HEADERS)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
@@ -87,7 +87,7 @@ def _get(path: str, headers: dict = None, expect_code: int = 200) -> dict:
def _post(path: str, data: dict = None, headers: dict = None,
expect_code: int = 200) -> tuple:
- """POST ่ฏทๆฑ"""
+ """POST request."""
req = urllib.request.Request(
f"{BASE_URL}{path}",
data=json.dumps(data).encode() if data else None,
@@ -107,19 +107,19 @@ def _post(path: str, data: dict = None, headers: dict = None,
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-# Web UI ๆต่ฏ
+# Web UI tests
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_webui_login_page():
- """GET /mcp/web/login โ ๅบ่ฟๅ็ปๅฝ้กต้ข HTML"""
+ """GET /mcp/web/login โ should return the login page HTML."""
status, body = _get("/mcp/web/login", headers={}, expect_code=200)
assert "html" in str(body).lower() or "login" in str(body).lower(), \
f"Should return login page: {str(body)[:200]}"
- print(" โ
็ปๅฝ้กต้ขๅฏ่ฎฟ้ฎ")
+ print(" โ
Login page is accessible")
def test_webui_login_post():
- """POST /mcp/web/login โ ไฝฟ็จ Doris ๅญๆฎ็ปๅฝ"""
+ """POST /mcp/web/login โ log in with Doris credentials."""
import urllib.parse
form_data = urlencode({
"username": TEST_DORIS_USER,
@@ -135,45 +135,45 @@ def test_webui_login_post():
cookies = resp.headers.get_all("Set-Cookie")
assert resp.status in (200, 302, 303, 400), f"Unexpected status: {resp.status}"
# 400 may occur if already logged in or missing CSRF, still means endpoint works
- print(f" โ
็ปๅฝๆฅๅฃๅๅบ status={resp.status}")
+ print(f" โ
Login endpoint responded with status={resp.status}")
except urllib.error.HTTPError as e:
if e.code in (302, 303):
- print(f" โ
็ปๅฝๆๅ (้ๅฎๅ {e.code})")
+ print(f" โ
Login succeeded (redirect {e.code})")
elif e.code == 400:
- print(f" โ
็ปๅฝๆฅๅฃๅฏ่ฎฟ้ฎ (400, ๅฏ่ฝ็ผบๅฐ CSRF token)")
+ print(" โ
Login endpoint is accessible (400, possibly missing CSRF token)")
else:
raise
def test_webui_requires_auth():
- """GET /mcp/web โ ๆช็ปๅฝๅบ่ฟๅ็ปๅฝ้กตๆ 401"""
+ """GET /mcp/web โ unauthenticated access should return the login page or 401."""
status, body = _get("/mcp/web", headers={}, expect_code=200)
- # ๆช็ปๅฝๆถๅฏ่ฝ่ฟๅ 401 ๆ้ๅฎๅๅฐ็ปๅฝ้กต
+ # Unauthenticated requests may return 401 or redirect to the login page.
assert status in (200, 401, 302), f"Unexpected status: {status}"
- print(f" โ
ๆช็ปๅฝ่ฎฟ้ฎ่ฟๅ {status}")
+ print(f" โ
Unauthenticated access returned {status}")
def test_webui_models_page():
- """GET /mcp/web/models โ ๅทฒ่ฎค่ฏ่ฎฟ้ฎ"""
+ """GET /mcp/web/models โ authenticated access."""
status, body = _get("/mcp/web/models")
assert status == 200, f"Expected 200, got {status}: {str(body)[:200]}"
- print(" โ
ๆจกๅ็ฎก็้กต้ขๅฏ่ฎฟ้ฎ")
+ print(" โ
Model management page is accessible")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-# REST API ๆต่ฏ โ ่ฏญไนๆจกๅ็ฎก็
+# REST API tests โ semantic model management
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_api_semantic_files_list():
- """GET /mcp/web/semantic/files โ ๅๅบๅทฒ็ๆๆไปถ"""
+ """GET /mcp/web/semantic/files โ list active files."""
status, body = _get("/mcp/web/semantic/files")
assert status == 200
assert body.get("success") is not False, f"Failed: {body}"
- print(f" โ
่ฏญไนๆไปถๅ่กจ: {json.dumps(body, ensure_ascii=False)[:200]}")
+ print(f" โ
Semantic file list: {json.dumps(body, ensure_ascii=False)[:200]}")
def test_api_semantic_pull():
- """GET /mcp/web/semantic/pull โ ไธ่ฝฝๅทฒ็ๆ YAML๏ผ.tar.gz ไบ่ฟๅถ๏ผ"""
+ """GET /mcp/web/semantic/pull โ download active YAML (.tar.gz binary)."""
req = urllib.request.Request(f"{BASE_URL}/mcp/web/semantic/pull", headers=HEADERS)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
@@ -185,20 +185,20 @@ def test_api_semantic_pull():
print(f" โ
pull: status={resp.status}, size={size}B, gzip={is_gzip}")
except urllib.error.HTTPError as e:
if e.code == 404:
- print(f" โ
pull: 404 (ๆ ๆไปถ)")
+ print(" โ
pull: 404 (no files)")
else:
raise
def test_api_semantic_reload():
- """POST /mcp/web/semantic/reload โ ้่ฝฝๅทฅไฝๅบ"""
+ """POST /mcp/web/semantic/reload โ reload workspace."""
status, body = _post("/mcp/web/semantic/reload", {"workspace": WORKSPACE})
assert status == 200
print(f" โ
reload: {json.dumps(body, ensure_ascii=False)[:200]}")
def test_api_staging_validate():
- """POST /mcp/web/staging/validate โ ้ช่ฏๆๅญๅๆด"""
+ """POST /mcp/web/staging/validate โ validate staged changes."""
status, body = _post("/mcp/web/staging/validate", {"workspace": WORKSPACE})
assert status == 200
assert "success" in body, f"Missing success field: {body}"
@@ -206,9 +206,9 @@ def test_api_staging_validate():
def test_api_staging_discard():
- """POST /mcp/web/staging/discard โ ไธขๅผๆๅญๅๆด
+ """POST /mcp/web/staging/discard โ discard staged changes.
- ็ ดๅๆง๏ผไผไธขๅผๅ
ฑไบซๆๅกๅจไธ็ๅฎ็จๆท็ๆๅญๅๆด๏ผ้ป่ฎค่ทณ่ฟใ
+ Destructive: can discard real staging changes on a shared server; skipped by default.
"""
_require_destructive()
status, body = _post("/mcp/web/staging/discard", {"workspace": WORKSPACE})
@@ -217,29 +217,29 @@ def test_api_staging_discard():
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-# ๅทฅไฝๅบ็ฎก็ API ๆต่ฏ
+# Workspace management API tests
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_api_workspace_create_and_delete():
- """ๅๅปบ โ ้ช่ฏ โ ๅ ้ค ๅทฅไฝๅบ
+ """Create โ validate โ delete a workspace.
- ็ ดๅๆง๏ผไผๅจๆๅกๅจไธๅๅปบ/ๅ ้ค็ๅฎๅทฅไฝๅบ๏ผ้ป่ฎค่ทณ่ฟใ
+ Destructive: creates/deletes a real workspace on the server; skipped by default.
"""
_require_destructive()
test_ws = "test_workspace_tmp"
- # ๆธ
็ๆฎ็
+ # Clean up leftovers.
_post("/mcp/web/workspace/delete", {"workspace": test_ws}, expect_code=200)
- # ๅๅปบ
+ # Create.
status, body = _post("/mcp/web/workspace/create", {
"name": test_ws,
}, expect_code=200)
assert status == 200 or body.get("success") is not False, \
f"Create failed: status={status} body={json.dumps(body, ensure_ascii=False)[:200]}"
- print(f" โ
ๅๅปบๅทฅไฝๅบ '{test_ws}': {json.dumps(body, ensure_ascii=False)[:150]}")
+ print(f" โ
Created workspace '{test_ws}': {json.dumps(body, ensure_ascii=False)[:150]}")
- # ้ช่ฏๅญๅจ โ ้่ฟ health check
+ # Verify existence through health check.
import urllib.request as ur
mcp_payload = {
"jsonrpc": "2.0", "id": 99,
@@ -259,21 +259,21 @@ def test_api_workspace_create_and_delete():
with ur.urlopen(req, timeout=15) as resp:
raw = resp.read().decode()
assert test_ws in raw, f"New workspace not found in health: {raw[:500]}"
- print(f" โ
ๅทฅไฝๅบ '{test_ws}' ๅจ health check ไธญๅฏ่ง")
+ print(f" โ
Workspace '{test_ws}' is visible in health check")
- # ๅ ้ค
+ # Delete.
status, body = _post("/mcp/web/workspace/delete", {"workspace": test_ws})
assert status == 200
- print(f" โ
ๅ ้คๅทฅไฝๅบ '{test_ws}'")
+ print(f" โ
Deleted workspace '{test_ws}'")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-# ่ฎค่ฏๆต่ฏ
+# Auth tests
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def test_api_requires_admin_for_create():
- """้ admin ็จๆทไธ่ฝๅๅปบๅทฅไฝๅบ"""
- # ไฝฟ็จ test ็จๆท token
+ """Non-admin users cannot create workspaces."""
+ # Use test user token.
test_headers = {
"Content-Type": "application/json",
"Authorization": "Bearer test:test",
@@ -285,12 +285,12 @@ def test_api_requires_admin_for_create():
expect_code=403,
)
assert status in (401, 403), f"Expected 401/403, got {status}: {body}"
- print(f" โ
test ็จๆทๆ ๆณๅๅปบๅทฅไฝๅบ ({status})")
+ print(f" โ
test user cannot create workspace ({status})")
def test_bearer_token_format():
- """้ช่ฏ Bearer token ๆ ผๅผ: username:password"""
- # ๆญฃ็กฎๆ ผๅผ
+ """Verify Bearer token format: username:password."""
+ # Correct format.
ok_headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {TEST_DORIS_USER}:{TEST_DORIS_PASS}",
@@ -309,19 +309,19 @@ def test_bearer_token_format():
with urllib.request.urlopen(req, timeout=15) as resp:
body = resp.read().decode()
assert "connected" in body.lower(), f"Auth failed with valid token: {body[:300]}"
- print(" โ
ๆญฃ็กฎ Bearer token ๆ ผๅผ้ช่ฏ้่ฟ")
+ print(" โ
Correct Bearer token format passed")
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if __name__ == "__main__":
print("=" * 60)
- print("Web UI & REST API ๆต่ฏๅผๅง")
+ print("Web UI & REST API tests starting")
print(f" URL: {BASE_URL}")
print(f" Workspace: {WORKSPACE}")
print("=" * 60)
if not _server_reachable():
- print(f"\nโ ๏ธ MCP Server ไธๅฏ่พพ ({BASE_URL})๏ผๆดไฝ่ทณ่ฟ๏ผไธ่งไธบๅคฑ่ดฅ")
+ print(f"\nโ ๏ธ MCP Server is unreachable ({BASE_URL}); skipping all tests without failure")
sys.exit(0)
tests = [
@@ -364,7 +364,7 @@ def test_bearer_token_format():
traceback.print_exc()
print(f"\n{'='*60}")
- print(f"็ปๆ: {passed} passed, {failed} failed, {skipped} skipped")
+ print(f"Result: {passed} passed, {failed} failed, {skipped} skipped")
print(f"{'='*60}")
if failed > 0:
sys.exit(1)