This is an automated email from the ASF dual-hosted git repository.

FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git


The following commit(s) were added to refs/heads/master by this push:
     new 779e558  fix: type prompt retrieval failures (#96)
779e558 is described below

commit 779e558e4f49cdcd09ef3ebfe44f1e3d25f4c160
Author: Yijia Su <[email protected]>
AuthorDate: Wed Jul 29 17:43:53 2026 +0800

    fix: type prompt retrieval failures (#96)
    
    Co-authored-by: FreeOnePlus <[email protected]>
---
 MCP-2026-07-28-DEVELOPMENT-LEDGER.md      |  50 +++++++--
 doris_mcp_server/protocol.py              |  46 +++++++-
 doris_mcp_server/tools/prompts_manager.py |  96 +++++++++++++----
 test/protocol/stdio_capability_server.py  |  52 ++++++++-
 test/protocol/test_mcp_v2_protocol.py     | 168 ++++++++++++++++++++++++++++++
 test/tools/test_prompts_manager_errors.py | 110 +++++++++++++++++++
 6 files changed, 491 insertions(+), 31 deletions(-)

diff --git a/MCP-2026-07-28-DEVELOPMENT-LEDGER.md 
b/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
index d53cc3c..4b79774 100644
--- a/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
+++ b/MCP-2026-07-28-DEVELOPMENT-LEDGER.md
@@ -132,7 +132,7 @@
 | `CORE-001` | P1 | list 异常不再返回空列表 | PROTO-002 | DB/权限/内部错误与真实空列表可区分 | 
`BACKLOG` |
 | `CORE-002` | P1 | Tool 错误使用 `isError=true` | PROTO-002 | 
可恢复业务错误对模型可见;内部异常为稳定协议错误 | `DONE` |
 | `CORE-003` | P1 | Resource not found 使用 `-32602` | PROTO-002 | 不存在 URI 返回 
Invalid Params,不返回错误正文成功 | `DONE` |
-| `CORE-004` | P1 | Prompt 错误类型化 | PROTO-002 | 缺参数、未知 prompt、DB 上下文失败语义不同 | 
`READY` |
+| `CORE-004` | P1 | Prompt 错误类型化 | PROTO-002 | 缺参数、未知 prompt、DB 上下文失败语义不同 | 
`DONE` |
 | `CORE-005` | P1 | 单一 Tool Definition Registry | PROTO-002 | 
schema、policy、handler、审计和文档同源 | `BACKLOG` |
 | `CORE-006` | P1 | `/live` 与 `/ready` 分离 | 无 | Doris 不可用时 live 可真、ready 
必假;探针有短超时 | `BACKLOG` |
 | `CORE-007` | P1 | 显式跨调用 handle | PROTO-003, SEC-015 | 状态不依赖协议 session;handle 
绑定 principal/expiry | `BACKLOG` |
@@ -409,17 +409,53 @@ missing resource: doris://table/__core_003_missing__
 
 连接通过 SSH key 和临时本地隧道完成;凭据未写入仓库或台账,探针完成后服务与隧道均已关闭。
 
+提交与评审回执:
+
+- commit:`5e57a2c fix: return invalid params for missing resources`
+- Draft 
PR:[apache/doris-mcp-server#95](https://github.com/apache/doris-mcp-server/pull/95)
+
+### CORE-004
+
+Prompt 的三类失败现在拥有独立、稳定的协议语义:
+
+| 场景 | JSON-RPC code | `promptErrorCode` |
+|---|---:|---|
+| 未知 Prompt | `-32602` | `UNKNOWN_PROMPT` |
+| 缺少必填参数 | `-32602` | `MISSING_REQUIRED_ARGUMENT` |
+| 数据库上下文不可用 | `-32603` | `DATABASE_CONTEXT_UNAVAILABLE` |
+
+缺少必填参数时 `data.argument` 指明参数名。数据库异常详情只写服务端日志,协议正文不回传连接或堆栈细节。该语义同时适用于现代和 legacy 
协议,旧客户端不再把 Prompt 故障误当成成功内容。
+
+数据库上下文查询增加独立 10 秒预算,避免底层连接池恢复时间超过客户端请求预算;连接管理器的 context API 与旧式 get/release 
API 都保证在成功、异常和取消时归还连接。
+
+自动化验证:
+
+- Streamable HTTP:三类错误契约、错误后成功恢复;
+- 真实子进程 STDIO:三类错误契约、错误后成功恢复;
+- STDIO legacy:未知 Prompt 返回 `-32602`;
+- manager:未知 Prompt、缺参数、DB 异常、超时和两类连接归还路径;
+- 完整 pytest:`343 passed / 57 skipped / 0 failed / 247 warnings`;
+- `uv lock --check`、作用域 Ruff、`compileall`、`uv build` 全部通过。
+
+真实 Doris 与故障注入验证:
+
+- HTTP modern/legacy:未知 Prompt、缺参数后均可继续从 `hhm_dt_sim` 生成真实数据库上下文;
+- STDIO modern/legacy:未知 Prompt、缺参数后均可继续从 `hhm_dt_sim` 生成真实数据库上下文;
+- HTTP 不可达端口故障注入:10 秒内返回 `-32603`,随后 `prompts/list` 成功;
+- STDIO 子进程 manager 故障注入:返回 `-32603`,随后有效 Prompt 成功。
+
+完全不可达 Doris 的真实 STDIO 进程会在协议协商前退出,无法进入 Prompt handler;这是启动/就绪与故障恢复边界,继续由 
`CORE-006` / `TEST-005` 跟踪,未计作本项通过证据。
+
 ## 11. 下一开发批次
 
 批次:`BATCH-02-CONFORMANCE-AND-ERROR-SEMANTICS`
 
 按以下顺序推进:
 
-1. `CORE-004`:Prompt 错误类型化;
-2. `CORE-010` / `CORE-011` / `COMPAT-001`:修复真实 Doris 已复现缺陷;
-3. `TEST-003`:运行官方 `server-stateless` Conformance;
-4. `TEST-005` / `TEST-012`:补权限不足、超时、故障恢复和工具错误路径;
-5. `PROTO-018` / `DOC-001` / `DOC-002`:版本单一来源和迁移文档;
-6. `SEC-003`~`SEC-005`:进入下一安全批,完成非 loopback fail-closed。
+1. `CORE-010` / `CORE-011` / `COMPAT-001`:修复真实 Doris 已复现缺陷;
+2. `TEST-003`:运行官方 `server-stateless` Conformance;
+3. `TEST-005` / `TEST-012`:补权限不足、超时、故障恢复和工具错误路径;
+4. `PROTO-018` / `DOC-001` / `DOC-002`:版本单一来源和迁移文档;
+5. `SEC-003`~`SEC-005`:进入下一安全批,完成非 loopback fail-closed。
 
 `REL-001` 已达成。`REL-002` 仍由官方 Conformance、完整真实 Doris 矩阵、P0/P1 安全项、Compose 
和发布门阻塞。
diff --git a/doris_mcp_server/protocol.py b/doris_mcp_server/protocol.py
index 3047598..6d77f09 100644
--- a/doris_mcp_server/protocol.py
+++ b/doris_mcp_server/protocol.py
@@ -29,6 +29,7 @@ from mcp.server.context import CallNext
 from mcp.server.transport_security import TransportSecuritySettings
 from mcp.shared.exceptions import MCPError
 from mcp.types import (
+    INTERNAL_ERROR,
     INVALID_PARAMS,
     LATEST_PROTOCOL_VERSION,
     MISSING_REQUIRED_CLIENT_CAPABILITY,
@@ -113,6 +114,12 @@ _RESOURCE_INVALID_PARAMS_MESSAGES = {
     "RESOURCE_NOT_FOUND": "Resource not found",
 }
 
+_PROMPT_INVALID_PARAMS_MESSAGES = {
+    "UNKNOWN_PROMPT": "Prompt not found",
+    "MISSING_REQUIRED_ARGUMENT": "Missing required prompt argument",
+}
+_PROMPT_DATABASE_CONTEXT_ERROR = "DATABASE_CONTEXT_UNAVAILABLE"
+
 
 def _decode_resource_request_error(payload: str) -> tuple[str, str] | None:
     """Decode only manager errors that are safe to classify as client input."""
@@ -222,10 +229,41 @@ def create_doris_mcp_server(
     ) -> GetPromptResult:
         del ctx
         authorize_operation(get_current_auth_context(), "get_prompt")
-        return await prompts_manager.get_prompt(
-            params.name,
-            dict(params.arguments or {}),
-        )
+        try:
+            return await prompts_manager.get_prompt(
+                params.name,
+                dict(params.arguments or {}),
+            )
+        except Exception as exc:
+            prompt_error_code = getattr(exc, "error_code", None)
+            message = _PROMPT_INVALID_PARAMS_MESSAGES.get(prompt_error_code)
+            if message is not None:
+                data = {
+                    "name": params.name,
+                    "promptErrorCode": prompt_error_code,
+                }
+                argument = getattr(exc, "argument", None)
+                if isinstance(argument, str):
+                    data["argument"] = argument
+                raise MCPError(
+                    code=INVALID_PARAMS,
+                    message=message,
+                    data=data,
+                ) from exc
+            if prompt_error_code == _PROMPT_DATABASE_CONTEXT_ERROR:
+                logger.exception(
+                    "Database context failed while rendering prompt %s",
+                    params.name,
+                )
+                raise MCPError(
+                    code=INTERNAL_ERROR,
+                    message="Database context unavailable",
+                    data={
+                        "name": params.name,
+                        "promptErrorCode": prompt_error_code,
+                    },
+                ) from exc
+            raise
 
     private_no_cache = CacheHint(ttl_ms=0, scope="private")
     server = Server(
diff --git a/doris_mcp_server/tools/prompts_manager.py 
b/doris_mcp_server/tools/prompts_manager.py
index e034e1e..313a0ab 100644
--- a/doris_mcp_server/tools/prompts_manager.py
+++ b/doris_mcp_server/tools/prompts_manager.py
@@ -19,6 +19,8 @@ Apache Doris MCP Prompts Manager
 Provides standardized management of query templates and intelligent prompts
 """
 
+import asyncio
+from contextlib import asynccontextmanager
 from datetime import datetime
 from typing import Any
 
@@ -61,13 +63,60 @@ class PromptTemplate:
         return content
 
 
+class UnknownPromptError(ValueError):
+    """The requested prompt name is not registered."""
+
+    error_code = "UNKNOWN_PROMPT"
+
+
+class MissingPromptArgumentError(ValueError):
+    """A registered prompt is missing one required template argument."""
+
+    error_code = "MISSING_REQUIRED_ARGUMENT"
+
+    def __init__(self, argument: str):
+        super().__init__(f"Missing required parameter: {argument}")
+        self.argument = argument
+
+
+class PromptDatabaseContextError(RuntimeError):
+    """The database context needed to render a prompt is unavailable."""
+
+    error_code = "DATABASE_CONTEXT_UNAVAILABLE"
+
+
 class DorisPromptsManager:
     """Apache Doris Prompts Manager"""
 
-    def __init__(self, connection_manager: DorisConnectionManager):
+    def __init__(
+        self,
+        connection_manager: DorisConnectionManager,
+        database_context_timeout_seconds: float = 10.0,
+    ):
         self.connection_manager = connection_manager
+        self.database_context_timeout_seconds = 
database_context_timeout_seconds
         self.templates = self._init_prompt_templates()
 
+    @asynccontextmanager
+    async def _connection_context(self, session_id: str = "system"):
+        manager_context = getattr(
+            self.connection_manager,
+            "get_connection_context",
+            None,
+        )
+        if manager_context:
+            async with manager_context(session_id) as connection:
+                yield connection
+            return
+
+        connection = await self.connection_manager.get_connection(session_id)
+        try:
+            yield connection
+        finally:
+            release = getattr(self.connection_manager, "release_connection", 
None)
+            if release:
+                await release(session_id, connection)
+
     def _init_prompt_templates(self) -> dict[str, PromptTemplate]:
         """Initialize prompt templates"""
         templates = {}
@@ -344,7 +393,7 @@ Please provide complete monitoring solution and 
implementation recommendations."
     async def get_prompt(self, name: str, arguments: dict[str, Any]) -> 
GetPromptResult:
         """Get content of specific prompt template"""
         if name not in self.templates:
-            raise ValueError(f"Prompt template named '{name}' not found")
+            raise UnknownPromptError(f"Prompt template named '{name}' not 
found")
 
         template = self.templates[name]
 
@@ -383,7 +432,7 @@ Please generate accurate and efficient SQL queries based on 
the above requiremen
             if arg.name in arguments:
                 processed[arg.name] = arguments[arg.name]
             elif arg.required:
-                raise ValueError(f"Missing required parameter: {arg.name}")
+                raise MissingPromptArgumentError(arg.name)
             else:
                 # Provide default handling for optional parameters
                 processed[arg.name] = self._get_default_argument_text(arg.name)
@@ -411,9 +460,18 @@ Please generate accurate and efficient SQL queries based 
on the above requiremen
     async def _get_database_context(self) -> str:
         """Get database context information"""
         try:
-            connection = await self.connection_manager.get_connection("system")
+            return await asyncio.wait_for(
+                self._query_database_context(),
+                timeout=self.database_context_timeout_seconds,
+            )
+        except Exception as e:
+            raise PromptDatabaseContextError(
+                "Database context unavailable"
+            ) from e
 
-            # Get basic database information
+    async def _query_database_context(self) -> str:
+        """Query the prompt context within the caller's bounded deadline."""
+        async with self._connection_context("system") as connection:
             db_info_sql = """
             SELECT
                 COUNT(*) as table_count,
@@ -424,10 +482,12 @@ Please generate accurate and efficient SQL queries based 
on the above requiremen
             """
 
             auth_context = get_auth_context()
-            db_result = await connection.execute(db_info_sql, 
auth_context=auth_context)
+            db_result = await connection.execute(
+                db_info_sql,
+                auth_context=auth_context,
+            )
             db_info = db_result.data[0] if db_result.data else {}
 
-            # Get main table list
             tables_sql = """
             SELECT
                 table_name,
@@ -440,24 +500,24 @@ Please generate accurate and efficient SQL queries based 
on the above requiremen
             LIMIT 10
             """
 
-            tables_result = await connection.execute(tables_sql, 
auth_context=auth_context)
+            tables_result = await connection.execute(
+                tables_sql,
+                auth_context=auth_context,
+            )
 
-            context = f"""Current database statistics:
+        context = f"""Current database statistics:
 - Total number of tables: {db_info.get("table_count", 0)}
 - Total data rows: {db_info.get("total_rows", 0):,}
 
 Main data tables:"""
 
-            for table in tables_result.data:
-                context += f"\n- {table['table_name']}"
-                if table.get("table_comment"):
-                    context += f": {table['table_comment']}"
-                context += f" ({table.get('table_rows', 0):,} rows)"
+        for table in tables_result.data:
+            context += f"\n- {table['table_name']}"
+            if table.get("table_comment"):
+                context += f": {table['table_comment']}"
+            context += f" ({table.get('table_rows', 0):,} rows)"
 
-            return context
-
-        except Exception as e:
-            return f"Unable to get database context information: {str(e)}"
+        return context
 
     def get_templates_by_category(self, category: str) -> list[PromptTemplate]:
         """Get templates by category"""
diff --git a/test/protocol/stdio_capability_server.py 
b/test/protocol/stdio_capability_server.py
index 5de86bd..fecac41 100644
--- a/test/protocol/stdio_capability_server.py
+++ b/test/protocol/stdio_capability_server.py
@@ -21,7 +21,15 @@ import json
 import logging
 
 from mcp.server.stdio import stdio_server
-from mcp.types import ClientCapabilities, GetPromptResult, Prompt, Resource, 
Tool
+from mcp.types import (
+    ClientCapabilities,
+    GetPromptResult,
+    Prompt,
+    PromptMessage,
+    Resource,
+    TextContent,
+    Tool,
+)
 
 from doris_mcp_server.protocol import create_doris_mcp_server
 
@@ -58,6 +66,19 @@ class OneToolManager:
         return "{}"
 
 
+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 EmptyPromptsManager:
     async def list_prompts(self) -> list[Prompt]:
         return []
@@ -67,7 +88,34 @@ class EmptyPromptsManager:
         name: str,
         arguments: dict,
     ) -> GetPromptResult:
-        raise ValueError(f"Unknown prompt: {name}")
+        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', '')}",
+                    ),
+                )
+            ],
+        )
 
 
 async def main() -> None:
diff --git a/test/protocol/test_mcp_v2_protocol.py 
b/test/protocol/test_mcp_v2_protocol.py
index c32045f..e8736cf 100644
--- a/test/protocol/test_mcp_v2_protocol.py
+++ b/test/protocol/test_mcp_v2_protocol.py
@@ -90,11 +90,40 @@ class StubToolsManager:
         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=[
@@ -225,6 +254,28 @@ def modern_resource_headers(uri: str) -> dict[str, str]:
     }
 
 
+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,
+    }
+
+
 @pytest.mark.asyncio
 async def 
test_http_discover_is_stateless_and_unknown_method_does_not_kill_server():
     app = create_test_server().streamable_http_app(
@@ -473,6 +524,80 @@ async def 
test_http_resource_not_found_is_invalid_params_and_server_recovers():
         assert recovered.json()["result"]["contents"][0]["uri"] == valid_uri
 
 
[email protected]
+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_stdio_validates_capabilities_versions_and_process_survival():
     server_script = Path(__file__).with_name("stdio_capability_server.py")
@@ -527,3 +652,46 @@ async def 
test_stdio_validates_capabilities_versions_and_process_survival():
         assert [tool.name for tool in (await legacy.list_tools()).tools] == 
["echo"]
         legacy_error = await legacy.read_resource("doris://table/missing")
         assert json.loads(legacy_error.contents[0].text)["error_code"] == 
"RESOURCE_NOT_FOUND"
+
+
[email protected]
+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
diff --git a/test/tools/test_prompts_manager_errors.py 
b/test/tools/test_prompts_manager_errors.py
new file mode 100644
index 0000000..dc2178b
--- /dev/null
+++ b/test/tools/test_prompts_manager_errors.py
@@ -0,0 +1,110 @@
+import asyncio
+from contextlib import asynccontextmanager
+
+import pytest
+
+from doris_mcp_server.tools.prompts_manager import DorisPromptsManager
+
+
+class FailingConnection:
+    async def execute(self, sql, params=None, auth_context=None):
+        raise RuntimeError("database context backend failed")
+
+
+class FailingConnectionManager:
+    def __init__(self):
+        self.acquires = 0
+        self.releases = 0
+        self.connection = FailingConnection()
+
+    async def get_connection(self, session_id):
+        self.acquires += 1
+        return self.connection
+
+    async def release_connection(self, session_id, connection):
+        assert connection is self.connection
+        self.releases += 1
+
+
+class ContextFailingConnectionManager:
+    def __init__(self):
+        self.acquires = 0
+        self.releases = 0
+        self.connection = FailingConnection()
+
+    @asynccontextmanager
+    async def get_connection_context(self, session_id):
+        self.acquires += 1
+        try:
+            yield self.connection
+        finally:
+            self.releases += 1
+
+
+class HangingConnectionManager:
+    async def get_connection(self, session_id):
+        await asyncio.Event().wait()
+
+
[email protected]
+async def test_unknown_prompt_has_stable_request_error_code():
+    manager = DorisPromptsManager(FailingConnectionManager())
+
+    with pytest.raises(ValueError) as error:
+        await manager.get_prompt("missing", {})
+
+    assert error.value.error_code == "UNKNOWN_PROMPT"
+
+
[email protected]
+async def test_missing_required_argument_names_the_argument():
+    manager = DorisPromptsManager(FailingConnectionManager())
+
+    with pytest.raises(ValueError) as error:
+        await manager.get_prompt("sales_analysis", {})
+
+    assert error.value.error_code == "MISSING_REQUIRED_ARGUMENT"
+    assert error.value.argument == "date_range"
+
+
[email protected]
[email protected](
+    "connection_manager",
+    [
+        FailingConnectionManager,
+        ContextFailingConnectionManager,
+    ],
+)
+async def test_database_context_failure_is_typed_and_releases_connection(
+    connection_manager,
+):
+    connections = connection_manager()
+    manager = DorisPromptsManager(connections)
+
+    with pytest.raises(RuntimeError) as error:
+        await manager.get_prompt(
+            "sales_analysis",
+            {"date_range": "last 30 days"},
+        )
+
+    assert error.value.error_code == "DATABASE_CONTEXT_UNAVAILABLE"
+    assert isinstance(error.value.__cause__, RuntimeError)
+    assert connections.acquires == 1
+    assert connections.releases == 1
+
+
[email protected]
+async def test_database_context_timeout_becomes_typed_failure():
+    manager = DorisPromptsManager(
+        HangingConnectionManager(),
+        database_context_timeout_seconds=0.01,
+    )
+
+    with pytest.raises(RuntimeError) as error:
+        await manager.get_prompt(
+            "sales_analysis",
+            {"date_range": "last 30 days"},
+        )
+
+    assert error.value.error_code == "DATABASE_CONTEXT_UNAVAILABLE"
+    assert isinstance(error.value.__cause__, TimeoutError)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to