feat: improve CRM MCP reliability
This commit is contained in:
@@ -2,6 +2,8 @@ import asyncio
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from nocobase_crm_mcp import server as server_module
|
||||
from nocobase_crm_mcp.nb_client import NbCommandError
|
||||
from nocobase_crm_mcp.server import BearerTokenAuthMiddleware, FastMCP, create_http_app, create_server
|
||||
|
||||
|
||||
@@ -59,6 +61,16 @@ class FakeCrmService:
|
||||
return {"results": []}
|
||||
|
||||
|
||||
class ErrorCrmService(FakeCrmService):
|
||||
def list_customers(self, **kwargs):
|
||||
raise NbCommandError("nb command failed with exit code 1: denied")
|
||||
|
||||
|
||||
class MissingNbCrmService(FakeCrmService):
|
||||
def crm_health(self):
|
||||
raise FileNotFoundError(2, "No such file or directory", "nb")
|
||||
|
||||
|
||||
class ServerTests(unittest.TestCase):
|
||||
def test_bearer_token_middleware_rejects_missing_or_wrong_token(self) -> None:
|
||||
app = BearerTokenAuthMiddleware(_ok_app, "secret")
|
||||
@@ -103,6 +115,53 @@ class ServerTests(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_tool_schemas_expose_raw_opt_in_parameters(self) -> None:
|
||||
if FastMCP is None:
|
||||
self.skipTest("MCP SDK is not installed")
|
||||
|
||||
server = create_server(crm_service=FakeCrmService(), readonly=True)
|
||||
tools = {tool.name: tool for tool in asyncio.run(server.list_tools())}
|
||||
|
||||
self.assertIn("include_raw", tools["list_customers"].inputSchema["properties"])
|
||||
self.assertIn("include_raw", tools["list_contacts"].inputSchema["properties"])
|
||||
self.assertIn("include_raw", tools["list_followups"].inputSchema["properties"])
|
||||
self.assertIn("include_raw_followups", tools["get_customer"].inputSchema["properties"])
|
||||
|
||||
def test_tools_return_structured_error_for_nb_failures(self) -> None:
|
||||
if FastMCP is None:
|
||||
self.skipTest("MCP SDK is not installed")
|
||||
|
||||
server = create_server(crm_service=ErrorCrmService(), readonly=True)
|
||||
|
||||
_content, structured = asyncio.run(server.call_tool("list_customers", {}))
|
||||
|
||||
self.assertEqual(structured["error"]["code"], "nb_command_failed")
|
||||
self.assertIn("denied", structured["error"]["message"])
|
||||
self.assertFalse(structured["error"]["retryable"])
|
||||
|
||||
def test_tools_return_specific_error_when_nb_is_not_found(self) -> None:
|
||||
if FastMCP is None:
|
||||
self.skipTest("MCP SDK is not installed")
|
||||
|
||||
server = create_server(crm_service=MissingNbCrmService(), readonly=True)
|
||||
|
||||
_content, structured = asyncio.run(server.call_tool("crm_health", {}))
|
||||
|
||||
self.assertEqual(structured["error"]["code"], "nb_not_found")
|
||||
self.assertIn("NOCOBASE_MCP_NB_BIN", structured["error"]["hint"])
|
||||
self.assertFalse(structured["error"]["retryable"])
|
||||
|
||||
def test_make_service_uses_nb_bin_from_environment(self) -> None:
|
||||
with (
|
||||
patch.dict("os.environ", {"NOCOBASE_MCP_NB_BIN": "/opt/node/bin/nb"}),
|
||||
patch.object(server_module, "NbClient") as nb_client,
|
||||
patch.object(server_module, "CrmService", side_effect=lambda config, client: {"config": config, "client": client}),
|
||||
):
|
||||
service = server_module._make_service()
|
||||
|
||||
self.assertIs(service["client"], nb_client.return_value)
|
||||
nb_client.assert_called_once_with(env_name="nb-mcp", data_source="main", nb_bin="/opt/node/bin/nb")
|
||||
|
||||
def test_config_template_tool_is_opt_in(self) -> None:
|
||||
if FastMCP is None:
|
||||
self.skipTest("MCP SDK is not installed")
|
||||
|
||||
Reference in New Issue
Block a user