feat: replace server-side RAG with MCP retrieval primitives
- Remove Phase 4 RAG pipeline in favor of retrieval-only architecture - Add FastMCP server exposing search, get_chunk, list_libraries tools - Mount MCP endpoints (streamable HTTP + SSE) via Starlette in ASGI config - Update README to clarify Mnemosyne is a retrieval engine, not RAG - Let calling LLMs drive synthesis and iterative retrieval themselves
This commit is contained in:
0
mnemosyne/mcp_server/tests/__init__.py
Normal file
0
mnemosyne/mcp_server/tests/__init__.py
Normal file
53
mnemosyne/mcp_server/tests/test_auth.py
Normal file
53
mnemosyne/mcp_server/tests/test_auth.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Tests for resolve_mcp_user."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from mcp_server.auth import MCPAuthError, resolve_mcp_user
|
||||
from mcp_server.models import MCPToken
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ResolveMCPUserTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="bob", email="bob@example.com", password="pw"
|
||||
)
|
||||
self.token = MCPToken.objects.create(user=self.user, name="t")
|
||||
|
||||
def test_resolves_valid_token(self):
|
||||
user, token = resolve_mcp_user(self.token.token)
|
||||
self.assertEqual(user.pk, self.user.pk)
|
||||
self.assertEqual(token.pk, self.token.pk)
|
||||
|
||||
def test_records_usage(self):
|
||||
self.assertIsNone(self.token.last_used_at)
|
||||
resolve_mcp_user(self.token.token)
|
||||
self.token.refresh_from_db()
|
||||
self.assertIsNotNone(self.token.last_used_at)
|
||||
|
||||
def test_invalid_token_raises(self):
|
||||
with self.assertRaises(MCPAuthError):
|
||||
resolve_mcp_user("not-a-real-token")
|
||||
|
||||
def test_inactive_token_raises(self):
|
||||
self.token.is_active = False
|
||||
self.token.save()
|
||||
with self.assertRaises(MCPAuthError):
|
||||
resolve_mcp_user(self.token.token)
|
||||
|
||||
def test_expired_token_raises(self):
|
||||
self.token.expires_at = timezone.now() - timedelta(hours=1)
|
||||
self.token.save()
|
||||
with self.assertRaises(MCPAuthError):
|
||||
resolve_mcp_user(self.token.token)
|
||||
|
||||
def test_disabled_user_raises(self):
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
with self.assertRaises(MCPAuthError):
|
||||
resolve_mcp_user(self.token.token)
|
||||
52
mnemosyne/mcp_server/tests/test_command.py
Normal file
52
mnemosyne/mcp_server/tests/test_command.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Tests for the create_mcp_token management command."""
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.test import TestCase
|
||||
|
||||
from mcp_server.models import MCPToken
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CreateMCPTokenCommandTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="carol", email="carol@example.com", password="pw"
|
||||
)
|
||||
|
||||
def test_create_basic_token(self):
|
||||
out = StringIO()
|
||||
call_command("create_mcp_token", user="carol@example.com", name="CLI", stdout=out)
|
||||
self.assertEqual(MCPToken.objects.count(), 1)
|
||||
self.assertIn("CLI", out.getvalue())
|
||||
|
||||
def test_lookup_by_username(self):
|
||||
out = StringIO()
|
||||
call_command("create_mcp_token", user="carol", name="CLI2", stdout=out)
|
||||
self.assertEqual(MCPToken.objects.count(), 1)
|
||||
|
||||
def test_unknown_user_raises(self):
|
||||
with self.assertRaises(CommandError):
|
||||
call_command("create_mcp_token", user="nobody@x.com", name="x")
|
||||
|
||||
def test_inactive_user_raises(self):
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
with self.assertRaises(CommandError):
|
||||
call_command("create_mcp_token", user="carol", name="x")
|
||||
|
||||
def test_tool_whitelist_parsed(self):
|
||||
out = StringIO()
|
||||
call_command(
|
||||
"create_mcp_token",
|
||||
user="carol",
|
||||
name="Restricted",
|
||||
tools="search,get_chunk",
|
||||
stdout=out,
|
||||
)
|
||||
token = MCPToken.objects.get(name="Restricted")
|
||||
self.assertEqual(token.allowed_tools, ["search", "get_chunk"])
|
||||
25
mnemosyne/mcp_server/tests/test_server.py
Normal file
25
mnemosyne/mcp_server/tests/test_server.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Tests that the FastMCP server registers the expected tools."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from mcp_server.server import mcp
|
||||
|
||||
|
||||
EXPECTED_TOOLS = {"search", "get_chunk", "list_libraries", "list_collections", "list_items"}
|
||||
|
||||
|
||||
class ServerRegistrationTest(TestCase):
|
||||
def test_expected_tools_registered(self):
|
||||
tools = asyncio.run(mcp.get_tools())
|
||||
self.assertEqual(EXPECTED_TOOLS, set(tools.keys()))
|
||||
|
||||
def test_tool_descriptions_within_limit(self):
|
||||
tools = asyncio.run(mcp.get_tools())
|
||||
for name, tool in tools.items():
|
||||
description = tool.description or ""
|
||||
self.assertLessEqual(
|
||||
len(description), 1024,
|
||||
f"Tool '{name}' description exceeds 1024 chars (MCP spec limit).",
|
||||
)
|
||||
63
mnemosyne/mcp_server/tests/test_token.py
Normal file
63
mnemosyne/mcp_server/tests/test_token.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Tests for the MCPToken model."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from mcp_server.models import MCPToken
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class MCPTokenModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="alice", email="alice@example.com", password="pw"
|
||||
)
|
||||
|
||||
def test_token_auto_generated(self):
|
||||
token = MCPToken.objects.create(user=self.user, name="t")
|
||||
self.assertTrue(token.token)
|
||||
self.assertGreater(len(token.token), 20)
|
||||
|
||||
def test_active_token_is_valid(self):
|
||||
token = MCPToken.objects.create(user=self.user, name="t")
|
||||
self.assertTrue(token.is_valid)
|
||||
|
||||
def test_inactive_token_not_valid(self):
|
||||
token = MCPToken.objects.create(user=self.user, name="t", is_active=False)
|
||||
self.assertFalse(token.is_valid)
|
||||
|
||||
def test_expired_token_not_valid(self):
|
||||
token = MCPToken.objects.create(
|
||||
user=self.user,
|
||||
name="t",
|
||||
expires_at=timezone.now() - timedelta(hours=1),
|
||||
)
|
||||
self.assertFalse(token.is_valid)
|
||||
|
||||
def test_unrestricted_permits_all(self):
|
||||
token = MCPToken.objects.create(user=self.user, name="t")
|
||||
self.assertTrue(token.can_use_tool("anything"))
|
||||
|
||||
def test_tool_whitelist(self):
|
||||
token = MCPToken.objects.create(
|
||||
user=self.user, name="t", allowed_tools=["search"]
|
||||
)
|
||||
self.assertTrue(token.can_use_tool("search"))
|
||||
self.assertFalse(token.can_use_tool("get_chunk"))
|
||||
|
||||
def test_record_usage(self):
|
||||
token = MCPToken.objects.create(user=self.user, name="t")
|
||||
self.assertIsNone(token.last_used_at)
|
||||
token.record_usage()
|
||||
token.refresh_from_db()
|
||||
self.assertIsNotNone(token.last_used_at)
|
||||
|
||||
def test_masked_token(self):
|
||||
token = MCPToken.objects.create(user=self.user, name="t")
|
||||
masked = token.get_masked_token()
|
||||
self.assertTrue(masked.endswith(token.token[-8:]))
|
||||
self.assertIn("*", masked)
|
||||
Reference in New Issue
Block a user