feat: improve CRM MCP reliability

This commit is contained in:
nac
2026-07-13 02:56:36 +00:00
parent 3be6dee45e
commit dd129a90a4
9 changed files with 666 additions and 62 deletions
+59
View File
@@ -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")