Merge branch 'feat/project-write-upload'
# Conflicts: # README.md # nocobase_crm_mcp/server.py # scripts/manage_mcp_service.py # tests/test_mcp_service.py # tests/test_nb_client.py # tests/test_server.py
This commit is contained in:
+84
-1
@@ -2,9 +2,21 @@ import asyncio
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
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
|
||||
from nocobase_crm_mcp.server import (
|
||||
DEFAULT_HTTP_HOST,
|
||||
DEFAULT_HTTP_PATH,
|
||||
DEFAULT_HTTP_PORT,
|
||||
BearerTokenAuthMiddleware,
|
||||
FastMCP,
|
||||
create_http_app,
|
||||
create_server,
|
||||
run_http_server,
|
||||
)
|
||||
|
||||
|
||||
async def _ok_app(scope, receive, send):
|
||||
@@ -71,6 +83,19 @@ class MissingNbCrmService(FakeCrmService):
|
||||
raise FileNotFoundError(2, "No such file or directory", "nb")
|
||||
|
||||
|
||||
class FakeProjectService:
|
||||
def create_project(self, **kwargs):
|
||||
return {"project_id": 1}
|
||||
|
||||
def update_project(self, project_id, **kwargs):
|
||||
return {"project_id": project_id}
|
||||
|
||||
|
||||
class FakeUploadEndpoint:
|
||||
async def __call__(self, request):
|
||||
return JSONResponse({"uploaded": True})
|
||||
|
||||
|
||||
class ServerTests(unittest.TestCase):
|
||||
def test_bearer_token_middleware_rejects_missing_or_wrong_token(self) -> None:
|
||||
app = BearerTokenAuthMiddleware(_ok_app, "secret")
|
||||
@@ -115,6 +140,21 @@ class ServerTests(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def test_write_server_exposes_fixed_project_tools(self) -> None:
|
||||
if FastMCP is None:
|
||||
self.skipTest("MCP SDK is not installed")
|
||||
|
||||
server = create_server(
|
||||
crm_service=FakeCrmService(),
|
||||
project_service=FakeProjectService(),
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
names = {tool.name for tool in asyncio.run(server.list_tools())}
|
||||
|
||||
self.assertTrue({"create_project", "update_project"}.issubset(names))
|
||||
self.assertNotIn("destroy_project", names)
|
||||
|
||||
def test_tool_schemas_expose_raw_opt_in_parameters(self) -> None:
|
||||
if FastMCP is None:
|
||||
self.skipTest("MCP SDK is not installed")
|
||||
@@ -162,6 +202,49 @@ class ServerTests(unittest.TestCase):
|
||||
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_upload_route_requires_the_same_bearer_token(self) -> None:
|
||||
if FastMCP is None:
|
||||
self.skipTest("MCP SDK is not installed")
|
||||
|
||||
server = create_server(crm_service=FakeCrmService(), readonly=True)
|
||||
app = create_http_app(server, "secret", upload_endpoint=FakeUploadEndpoint())
|
||||
|
||||
self.assertEqual(run_asgi_request(app, path="/uploads")[0]["status"], 401)
|
||||
|
||||
def test_upload_route_accepts_the_matching_bearer_token(self) -> None:
|
||||
if FastMCP is None:
|
||||
self.skipTest("MCP SDK is not installed")
|
||||
|
||||
server = create_server(crm_service=FakeCrmService(), readonly=True)
|
||||
|
||||
async def upload_endpoint(request):
|
||||
return JSONResponse({"uploaded": True})
|
||||
|
||||
app = create_http_app(server, "secret", upload_endpoint=upload_endpoint)
|
||||
|
||||
async def request_upload() -> httpx.Response:
|
||||
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as client:
|
||||
return await client.post("/uploads", headers={"Authorization": "Bearer secret"})
|
||||
|
||||
response = asyncio.run(request_upload())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), {"uploaded": True})
|
||||
|
||||
def test_http_write_mode_requires_upload_api_base_url(self) -> None:
|
||||
with patch.dict("os.environ", {"NOCOBASE_MCP_TOKEN": "secret"}, clear=True):
|
||||
with patch("uvicorn.run") as uvicorn_run:
|
||||
with self.assertRaisesRegex(RuntimeError, "NOCOBASE_MCP_UPLOAD_API_BASE_URL"):
|
||||
run_http_server(
|
||||
config_path=None,
|
||||
host=DEFAULT_HTTP_HOST,
|
||||
port=DEFAULT_HTTP_PORT,
|
||||
token_env="NOCOBASE_MCP_TOKEN",
|
||||
streamable_http_path=DEFAULT_HTTP_PATH,
|
||||
allowed_hosts=None,
|
||||
allowed_origins=None,
|
||||
)
|
||||
uvicorn_run.assert_not_called()
|
||||
|
||||
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