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:
2026-04-26 15:34:26 -04:00
parent 388b37e471
commit 2df22941d2
30 changed files with 1180 additions and 126 deletions

View File

View 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)

View 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"])

View 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).",
)

View 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)