Skip to content

Commit 94d1314

Browse files
committed
Fixing CI issues and adding tests for the list of tools
1 parent aab091e commit 94d1314

File tree

7 files changed

+388
-22
lines changed

7 files changed

+388
-22
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Redis MCP Server
2-
[![Integration](https://github.com/redis/mcp-redis/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/redis/lettuce/actions/workflows/integration.yml)
2+
[![Integration](https://github.com/redis/mcp-redis/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/redis/mcp-redis/actions/workflows/ci.yml)
33
[![Python Version](https://img.shields.io/badge/python-3.13%2B-blue)](https://www.python.org/downloads/)
44
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE.txt)
55
[![smithery badge](https://smithery.ai/badge/@redis/mcp-redis)](https://smithery.ai/server/@redis/mcp-redis)
66
[![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/70102150-efe0-4705-9f7d-87980109a279)
7-
[![codecov](https://codecov.io/gh/redis/mcp-redis/branch/master/graph/badge.svg?token=yenl5fzxxr)](https://codecov.io/gh/redis/mcp-redis)
7+
![Docker Image Version](https://img.shields.io/docker/v/mcp/redis?sort=semver&logo=docker&label=Docker)
88

99

1010
[![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/redis)

src/common/server.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
import pkgutil
33
from mcp.server.fastmcp import FastMCP
44

5+
56
def load_tools():
67
import src.tools as tools_pkg
8+
79
for _, module_name, _ in pkgutil.iter_modules(tools_pkg.__path__):
810
importlib.import_module(f"src.tools.{module_name}")
911

12+
1013
# Initialize FastMCP server
1114
mcp = FastMCP("Redis MCP Server", dependencies=["redis", "dotenv", "numpy"])
1215

src/tools/string.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99

1010

1111
@mcp.tool()
12-
async def set(key: str, value: Union[str, bytes, int, float, dict], expiration: int = None) -> str:
12+
async def set(
13+
key: str, value: Union[str, bytes, int, float, dict], expiration: int = None
14+
) -> str:
1315
"""Set a Redis string value with an optional expiration time.
1416
1517
Args:

tests/test_integration.py

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
"""
2+
Integration tests for Redis MCP Server.
3+
4+
These tests actually start the MCP server process and verify it can handle real requests.
5+
"""
6+
7+
import json
8+
import subprocess
9+
import sys
10+
import time
11+
import os
12+
from pathlib import Path
13+
14+
import pytest
15+
16+
17+
def _redis_available():
18+
"""Check if Redis is available for testing."""
19+
try:
20+
import redis
21+
22+
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
23+
r.ping()
24+
return True
25+
except Exception:
26+
return False
27+
28+
29+
@pytest.mark.integration
30+
class TestMCPServerIntegration:
31+
"""Integration tests that start the actual MCP server."""
32+
33+
@pytest.fixture
34+
def server_process(self):
35+
"""Start the MCP server process for testing."""
36+
# Get the project root directory
37+
project_root = Path(__file__).parent.parent
38+
39+
# Start the server process
40+
process = subprocess.Popen(
41+
[sys.executable, "-m", "src.main"],
42+
cwd=project_root,
43+
stdin=subprocess.PIPE,
44+
stdout=subprocess.PIPE,
45+
stderr=subprocess.PIPE,
46+
text=True,
47+
env={"REDIS_HOST": "localhost", "REDIS_PORT": "6379", **dict(os.environ)},
48+
)
49+
50+
# Give the server a moment to start
51+
time.sleep(1)
52+
53+
yield process
54+
55+
# Clean up
56+
process.terminate()
57+
try:
58+
process.wait(timeout=5)
59+
except subprocess.TimeoutExpired:
60+
process.kill()
61+
process.wait()
62+
63+
def test_server_starts_successfully(self, server_process):
64+
"""Test that the MCP server starts without crashing."""
65+
# Check if process is still running
66+
assert server_process.poll() is None, "Server process should be running"
67+
68+
# Check for startup message in stderr
69+
# Note: MCP servers typically output startup info to stderr
70+
time.sleep(0.5) # Give time for startup message
71+
72+
# The server should still be running
73+
assert server_process.poll() is None
74+
75+
def test_server_responds_to_initialize_request(self, server_process):
76+
"""Test that the server responds to MCP initialize request."""
77+
# MCP initialize request
78+
initialize_request = {
79+
"jsonrpc": "2.0",
80+
"id": 1,
81+
"method": "initialize",
82+
"params": {
83+
"protocolVersion": "2024-11-05",
84+
"capabilities": {},
85+
"clientInfo": {"name": "test-client", "version": "1.0.0"},
86+
},
87+
}
88+
89+
# Send the request
90+
request_json = json.dumps(initialize_request) + "\n"
91+
server_process.stdin.write(request_json)
92+
server_process.stdin.flush()
93+
94+
# Read the response
95+
response_line = server_process.stdout.readline()
96+
assert response_line.strip(), "Server should respond to initialize request"
97+
98+
# Parse the response
99+
try:
100+
response = json.loads(response_line)
101+
assert response.get("jsonrpc") == "2.0"
102+
assert response.get("id") == 1
103+
assert "result" in response
104+
except json.JSONDecodeError:
105+
pytest.fail(f"Invalid JSON response: {response_line}")
106+
107+
def test_server_lists_tools(self, server_process):
108+
"""Test that the server can list available tools."""
109+
# First initialize
110+
initialize_request = {
111+
"jsonrpc": "2.0",
112+
"id": 1,
113+
"method": "initialize",
114+
"params": {
115+
"protocolVersion": "2024-11-05",
116+
"capabilities": {},
117+
"clientInfo": {"name": "test-client", "version": "1.0.0"},
118+
},
119+
}
120+
121+
server_process.stdin.write(json.dumps(initialize_request) + "\n")
122+
server_process.stdin.flush()
123+
server_process.stdout.readline() # Read initialize response
124+
125+
# Send initialized notification
126+
initialized_notification = {
127+
"jsonrpc": "2.0",
128+
"method": "notifications/initialized",
129+
}
130+
server_process.stdin.write(json.dumps(initialized_notification) + "\n")
131+
server_process.stdin.flush()
132+
133+
# Request tools list
134+
tools_request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
135+
136+
server_process.stdin.write(json.dumps(tools_request) + "\n")
137+
server_process.stdin.flush()
138+
139+
# Read the response
140+
response_line = server_process.stdout.readline()
141+
response = json.loads(response_line)
142+
143+
assert response.get("jsonrpc") == "2.0"
144+
assert response.get("id") == 2
145+
assert "result" in response
146+
assert "tools" in response["result"]
147+
148+
# Verify we have some Redis tools
149+
tools = response["result"]["tools"]
150+
tool_names = [tool["name"] for tool in tools]
151+
152+
# Should have basic Redis operations
153+
expected_tools = [
154+
"hset",
155+
"hget",
156+
"hdel",
157+
"hgetall",
158+
"hexists",
159+
"set_vector_in_hash",
160+
"get_vector_from_hash",
161+
"json_set",
162+
"json_get",
163+
"json_del",
164+
"lpush",
165+
"rpush",
166+
"lpop",
167+
"rpop",
168+
"lrange",
169+
"llen",
170+
"delete",
171+
"type",
172+
"expire",
173+
"rename",
174+
"scan_keys",
175+
"scan_all_keys",
176+
"publish",
177+
"subscribe",
178+
"unsubscribe",
179+
"get_indexes",
180+
"get_index_info",
181+
"get_indexed_keys_number",
182+
"create_vector_index_hash",
183+
"vector_search_hash",
184+
"dbsize",
185+
"info",
186+
"client_list",
187+
"sadd",
188+
"srem",
189+
"smembers",
190+
"zadd",
191+
"zrange",
192+
"zrem",
193+
"xadd",
194+
"xrange",
195+
"xdel",
196+
"set",
197+
"get",
198+
]
199+
for tool in tool_names:
200+
assert tool in expected_tools, (
201+
f"Expected tool '{tool}' not found in {tool_names}"
202+
)
203+
204+
def test_server_tool_count_and_names(self, server_process):
205+
"""Test that the server registers the correct number of tools with expected names."""
206+
# Initialize the server
207+
self._initialize_server(server_process)
208+
209+
# Request tools list
210+
tools_request = {"jsonrpc": "2.0", "id": 3, "method": "tools/list"}
211+
212+
server_process.stdin.write(json.dumps(tools_request) + "\n")
213+
server_process.stdin.flush()
214+
215+
# Read the response
216+
response_line = server_process.stdout.readline()
217+
response = json.loads(response_line)
218+
219+
assert response.get("jsonrpc") == "2.0"
220+
assert response.get("id") == 3
221+
assert "result" in response
222+
assert "tools" in response["result"]
223+
224+
tools = response["result"]["tools"]
225+
tool_names = [tool["name"] for tool in tools]
226+
227+
# Expected tool count (based on @mcp.tool() decorators in codebase)
228+
expected_tool_count = 44
229+
assert len(tools) == expected_tool_count, (
230+
f"Expected {expected_tool_count} tools, but got {len(tools)}"
231+
)
232+
233+
# Expected tool names (alphabetically sorted for easier verification)
234+
expected_tools = [
235+
"client_list",
236+
"create_vector_index_hash",
237+
"dbsize",
238+
"delete",
239+
"expire",
240+
"get",
241+
"get_index_info",
242+
"get_indexed_keys_number",
243+
"get_indexes",
244+
"get_vector_from_hash",
245+
"hdel",
246+
"hexists",
247+
"hget",
248+
"hgetall",
249+
"hset",
250+
"info",
251+
"json_del",
252+
"json_get",
253+
"json_set",
254+
"llen",
255+
"lpop",
256+
"lpush",
257+
"lrange",
258+
"publish",
259+
"rename",
260+
"rpop",
261+
"rpush",
262+
"sadd",
263+
"scan_all_keys",
264+
"scan_keys",
265+
"set",
266+
"set_vector_in_hash",
267+
"smembers",
268+
"srem",
269+
"subscribe",
270+
"type",
271+
"unsubscribe",
272+
"vector_search_hash",
273+
"xadd",
274+
"xdel",
275+
"xrange",
276+
"zadd",
277+
"zrange",
278+
"zrem",
279+
]
280+
281+
# Verify all expected tools are present
282+
missing_tools = set(expected_tools) - set(tool_names)
283+
extra_tools = set(tool_names) - set(expected_tools)
284+
285+
assert not missing_tools, f"Missing expected tools: {sorted(missing_tools)}"
286+
assert not extra_tools, f"Found unexpected tools: {sorted(extra_tools)}"
287+
288+
# Verify tool categories are represented
289+
tool_categories = {
290+
"string": ["get", "set"],
291+
"hash": ["hget", "hset", "hgetall", "hdel", "hexists"],
292+
"list": ["lpush", "rpush", "lpop", "rpop", "lrange", "llen"],
293+
"set": ["sadd", "srem", "smembers"],
294+
"sorted_set": ["zadd", "zrem", "zrange"],
295+
"stream": ["xadd", "xdel", "xrange"],
296+
"json": ["json_get", "json_set", "json_del"],
297+
"pub_sub": ["publish", "subscribe", "unsubscribe"],
298+
"server_mgmt": ["dbsize", "info", "client_list"],
299+
"misc": [
300+
"delete",
301+
"expire",
302+
"rename",
303+
"type",
304+
"scan_keys",
305+
"scan_all_keys",
306+
],
307+
"vector_search": [
308+
"create_vector_index_hash",
309+
"vector_search_hash",
310+
"get_indexes",
311+
"get_index_info",
312+
"set_vector_in_hash",
313+
"get_vector_from_hash",
314+
"get_indexed_keys_number",
315+
],
316+
}
317+
318+
for category, category_tools in tool_categories.items():
319+
for tool in category_tools:
320+
assert tool in tool_names, (
321+
f"Tool '{tool}' from category '{category}' not found in registered tools"
322+
)
323+
324+
def _initialize_server(self, server_process):
325+
"""Helper to initialize the MCP server."""
326+
# Send initialize request
327+
initialize_request = {
328+
"jsonrpc": "2.0",
329+
"id": 1,
330+
"method": "initialize",
331+
"params": {
332+
"protocolVersion": "2024-11-05",
333+
"capabilities": {},
334+
"clientInfo": {"name": "test-client", "version": "1.0.0"},
335+
},
336+
}
337+
338+
server_process.stdin.write(json.dumps(initialize_request) + "\n")
339+
server_process.stdin.flush()
340+
server_process.stdout.readline() # Read response
341+
342+
# Send initialized notification
343+
initialized_notification = {
344+
"jsonrpc": "2.0",
345+
"method": "notifications/initialized",
346+
}
347+
server_process.stdin.write(json.dumps(initialized_notification) + "\n")
348+
server_process.stdin.flush()

0 commit comments

Comments
 (0)