mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 14:07:55 +00:00
196 lines
6.4 KiB
Python
196 lines
6.4 KiB
Python
"""Tests for the MCP server."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from openjarvis.mcp.protocol import (
|
|
INVALID_PARAMS,
|
|
METHOD_NOT_FOUND,
|
|
MCPRequest,
|
|
)
|
|
from openjarvis.mcp.server import MCPServer
|
|
from openjarvis.tools.calculator import CalculatorTool
|
|
from openjarvis.tools.think import ThinkTool
|
|
|
|
|
|
@pytest.fixture
|
|
def server():
|
|
"""Create an MCP server with calculator and think tools."""
|
|
return MCPServer([CalculatorTool(), ThinkTool()])
|
|
|
|
|
|
class TestMCPServer:
|
|
def test_initialize(self, server):
|
|
req = MCPRequest(method="initialize", id=1)
|
|
resp = server.handle(req)
|
|
assert resp.error is None
|
|
assert resp.id == 1
|
|
result = resp.result
|
|
assert "protocolVersion" in result
|
|
assert "capabilities" in result
|
|
assert "serverInfo" in result
|
|
assert result["serverInfo"]["name"] == "openjarvis"
|
|
|
|
def test_initialize_capabilities(self, server):
|
|
req = MCPRequest(method="initialize", id=1)
|
|
resp = server.handle(req)
|
|
caps = resp.result["capabilities"]
|
|
assert "tools" in caps
|
|
|
|
def test_tools_list_all(self, server):
|
|
req = MCPRequest(method="tools/list", id=2)
|
|
resp = server.handle(req)
|
|
assert resp.error is None
|
|
tools = resp.result["tools"]
|
|
assert len(tools) == 2
|
|
names = {t["name"] for t in tools}
|
|
assert "calculator" in names
|
|
assert "think" in names
|
|
|
|
def test_tools_list_includes_spec(self, server):
|
|
req = MCPRequest(method="tools/list", id=2)
|
|
resp = server.handle(req)
|
|
tools = resp.result["tools"]
|
|
for tool in tools:
|
|
assert "name" in tool
|
|
assert "description" in tool
|
|
assert "inputSchema" in tool
|
|
|
|
def test_tools_list_input_schema_has_properties(self, server):
|
|
req = MCPRequest(method="tools/list", id=2)
|
|
resp = server.handle(req)
|
|
tools = resp.result["tools"]
|
|
for tool in tools:
|
|
schema = tool["inputSchema"]
|
|
assert "properties" in schema
|
|
|
|
def test_tools_call_calculator(self, server):
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={"name": "calculator", "arguments": {"expression": "2+2"}},
|
|
id=3,
|
|
)
|
|
resp = server.handle(req)
|
|
assert resp.error is None
|
|
assert resp.result["isError"] is False
|
|
content = resp.result["content"]
|
|
assert len(content) == 1
|
|
assert content[0]["type"] == "text"
|
|
assert "4" in content[0]["text"]
|
|
|
|
def test_tools_call_calculator_complex(self, server):
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={"name": "calculator", "arguments": {"expression": "3 * (4 + 5)"}},
|
|
id=4,
|
|
)
|
|
resp = server.handle(req)
|
|
assert resp.error is None
|
|
assert "27" in resp.result["content"][0]["text"]
|
|
|
|
def test_tools_call_think(self, server):
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={
|
|
"name": "think",
|
|
"arguments": {"thought": "Step 1: analyze the problem."},
|
|
},
|
|
id=5,
|
|
)
|
|
resp = server.handle(req)
|
|
assert resp.error is None
|
|
assert resp.result["isError"] is False
|
|
assert "Step 1" in resp.result["content"][0]["text"]
|
|
|
|
def test_tools_call_unknown_tool(self, server):
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={"name": "nonexistent", "arguments": {}},
|
|
id=6,
|
|
)
|
|
resp = server.handle(req)
|
|
assert resp.error is not None
|
|
assert resp.error["code"] == INVALID_PARAMS
|
|
assert "Unknown tool" in resp.error["message"]
|
|
|
|
def test_tools_call_missing_name(self, server):
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={"arguments": {}},
|
|
id=7,
|
|
)
|
|
resp = server.handle(req)
|
|
assert resp.error is not None
|
|
assert resp.error["code"] == INVALID_PARAMS
|
|
assert "name" in resp.error["message"]
|
|
|
|
def test_tools_call_invalid_arguments(self, server):
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={"name": "calculator", "arguments": {}},
|
|
id=8,
|
|
)
|
|
resp = server.handle(req)
|
|
# Calculator returns failure but not a protocol error
|
|
assert resp.error is None
|
|
assert resp.result["isError"] is True
|
|
|
|
def test_result_format_mcp_compliant(self, server):
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={"name": "think", "arguments": {"thought": "hello"}},
|
|
id=9,
|
|
)
|
|
resp = server.handle(req)
|
|
result = resp.result
|
|
assert "content" in result
|
|
assert "isError" in result
|
|
assert isinstance(result["content"], list)
|
|
assert result["content"][0]["type"] == "text"
|
|
|
|
def test_unknown_method(self, server):
|
|
req = MCPRequest(method="resources/list", id=10)
|
|
resp = server.handle(req)
|
|
assert resp.error is not None
|
|
assert resp.error["code"] == METHOD_NOT_FOUND
|
|
assert "Unknown method" in resp.error["message"]
|
|
|
|
def test_response_preserves_request_id(self, server):
|
|
req = MCPRequest(method="tools/list", id=42)
|
|
resp = server.handle(req)
|
|
assert resp.id == 42
|
|
|
|
def test_response_preserves_string_id(self, server):
|
|
req = MCPRequest(method="tools/list", id="req-abc")
|
|
resp = server.handle(req)
|
|
assert resp.id == "req-abc"
|
|
|
|
def test_empty_tools_server(self):
|
|
empty_server = MCPServer([])
|
|
req = MCPRequest(method="tools/list", id=1)
|
|
resp = empty_server.handle(req)
|
|
assert resp.result["tools"] == []
|
|
|
|
def test_tools_call_with_no_arguments_key(self, server):
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={"name": "calculator"},
|
|
id=11,
|
|
)
|
|
resp = server.handle(req)
|
|
# Should still execute (with empty arguments)
|
|
assert resp.error is None
|
|
|
|
def test_calculator_division_by_zero_returns_inf(self, server):
|
|
# Rust calculator (meval) returns inf for 1/0 rather than an error
|
|
req = MCPRequest(
|
|
method="tools/call",
|
|
params={"name": "calculator", "arguments": {"expression": "1/0"}},
|
|
id=12,
|
|
)
|
|
resp = server.handle(req)
|
|
assert resp.error is None
|
|
assert resp.result["isError"] is False
|
|
assert "inf" in resp.result["content"][0]["text"]
|