Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:34:21 +02:00
commit 7ca726caf2
659 files changed
+98171

No files matched your search

+376
View File
@@ -0,0 +1,376 @@
"""Fixtures for the MCP contract tests.
Mirrors the same fixture-construction pattern as `tests/contract/api/**`
and `tests/contract/sdk/python/**` (real `RetrievalAuthorizationService`,
a fixture executor/generator standing in only for the retrieval
backend/model provider) -- these tests exist to prove the MCP adapter's
own auth/mapping/rate-limit layer, not to re-prove authorization/
retrieval business logic already covered there.
"""
from __future__ import annotations
import contextlib
from collections.abc import AsyncIterator
from dataclasses import dataclass, replace
from datetime import UTC, datetime
from typing import Any
from uuid import UUID, uuid4
import httpx2
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamable_http_client
from mcp.server.mcpserver import MCPServer
from ragcore.api.v1.answers.service import AnswerService
from ragcore.api.v1.context.service import (
ContextExecutionRequest,
ContextExecutionResult,
ContextService,
)
from ragcore.api.v1.search.service import (
AuthorizationInputs,
SearchHit,
SearchService,
)
from ragcore.application.identity.credentials import ServiceAccountCredentialService
from ragcore.application.retrieval.authorization import RetrievalAuthorizationService
from ragcore.domain.authorization import Principal
from ragcore.domain.citations import Citation
from ragcore.domain.control import Application, Role, Scope, ServiceAccount, Workspace
from ragcore.infrastructure.database.identity import InMemoryCredentialRepository
from ragcore.mcp.server import MCPDependencies, build_mcp_server
from ragcore.retrieval.context import ContextCandidate
from ragcore.retrieval.ranking import FusedCandidate
from ragcore.retrieval.search.models import Candidate, CandidateBackendKind, CandidateProvenance
EVIDENCE_EXCERPT = "The refund window is thirty days from the original purchase date."
CREDENTIAL_PEPPER = "test-credential-pepper-that-is-long-enough"
class InMemoryDocumentRepository:
def __init__(self) -> None:
self._documents: dict[UUID, Any] = {}
self._versions: dict[UUID, Any] = {}
def add_document(self, document: Any) -> None:
self._documents[document.id] = document
def add_version(self, version: Any) -> None:
self._versions[version.id] = version
async def get_document(self, document_id: UUID) -> Any:
return self._documents.get(document_id)
async def get_version(self, version_id: UUID) -> Any:
return self._versions.get(version_id)
async def list_searchable_versions(self, document_id: UUID) -> tuple[Any, ...]:
from ragcore.application.documents.models import DocumentVersionState
return tuple(
v
for v in self._versions.values()
if v.document_id == document_id and v.state is DocumentVersionState.PUBLISHED
)
class InMemoryCitationRepository:
def __init__(self) -> None:
self._citations: dict[UUID, Citation] = {}
def add(self, citation: Citation) -> None:
self._citations[citation.id] = citation
async def get_authorized(
self, citation_id: UUID, *, workspace_id: UUID, effective_space_ids: frozenset[UUID]
) -> Citation | None:
citation = self._citations.get(citation_id)
if citation is None or citation.workspace_id != workspace_id:
return None
if citation.space_id not in effective_space_ids:
return None
return citation
class _FixtureInputs:
"""Loads the one space plus a real workspace-level `Grant` tying it to
`application` -- `EffectiveSpaceResolver` requires an actual `Grant`
row (not just `Application.default_knowledge_space_ids`, which is only
consulted when the caller's own `requested_space_ids` is empty) to
intersect an *explicit* request against for a SERVICE_ACCOUNT
principal, exactly like a real workspace-level grant REST would use.
"""
def __init__(self, workspace: Workspace, space: Any, application: Application) -> None:
self.workspace = workspace
self.space = space
self.application = application
self.grant = workspace.grant(
id=uuid4(), application=application, knowledge_space=space, role=Role.EDITOR
)
async def load(self, principal: Principal) -> Any:
return type(
"Inputs",
(),
{
"spaces": (self.space,),
"grants": (self.grant,),
"user_grants": (),
"application": self.application,
},
)()
@dataclass
class McpFixture:
server: MCPServer
deps: MCPDependencies
workspace: Workspace
application: Application
space_id: UUID
token: str
document_repository: InMemoryDocumentRepository
citation_repository: InMemoryCitationRepository
def _citation(workspace_id: UUID, space_id: UUID) -> Citation:
return Citation.create(
workspace_id=workspace_id,
space_id=space_id,
document_id=uuid4(),
document_version_id=uuid4(),
chunk_id=uuid4(),
parent_chunk_id=None,
location={"page_start": 4, "section": "Refunds"},
excerpt=EVIDENCE_EXCERPT,
source_uri="https://example.test/policy.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
retrieval_run_id=uuid4(),
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
async def build_mcp_fixture(*, grant_scopes: set[Scope] | None = None) -> McpFixture:
workspace = Workspace(uuid4(), "MCP workspace", "mcp-workspace")
space_id = uuid4()
space = workspace.create_knowledge_space(id=space_id, name="MCP space", slug="mcp-space")
application = workspace.create_application(
id=uuid4(),
name="MCP client",
slug="mcp-client",
scopes=frozenset(
grant_scopes
or {
Scope.SEARCH,
Scope.CONTEXT,
Scope.ANSWER,
Scope.DOCUMENTS_READ,
Scope.CITATIONS_READ,
}
),
default_knowledge_space_ids=frozenset({space_id}),
)
service_account = ServiceAccount(
id=uuid4(),
workspace_id=workspace.id,
application_id=application.id,
name="MCP service account",
scopes=application.scopes,
)
async def load_service_account(account_id: UUID) -> ServiceAccount | None:
return service_account if account_id == service_account.id else None
credential_service = ServiceAccountCredentialService(
InMemoryCredentialRepository(),
CREDENTIAL_PEPPER,
service_account_loader=load_service_account,
)
issued = await credential_service.issue(service_account, scopes=service_account.scopes)
citation = _citation(workspace.id, space_id)
candidate = Candidate(
point_id=uuid4(),
score=0.87,
rank=1,
backend=CandidateBackendKind.DENSE,
provenance=CandidateProvenance(
workspace_id=workspace.id,
space_id=space_id,
document_id=citation.document_id,
document_version_id=citation.document_version_id,
chunk_id=citation.chunk_id,
source_id=uuid4(),
parent_chunk_id=None,
status="published",
content_type="text/markdown",
language="en",
trust_level="trusted_internal",
),
)
hit = SearchHit(
fused=FusedCandidate(candidate, 1 / 61, 1, None),
text=EVIDENCE_EXCERPT,
title="Refund policy",
citation=citation,
dense_score=0.87,
)
class _SearchExecutor:
async def execute(self, request: Any) -> Any:
refreshed = replace(
hit,
citation=Citation.create(
workspace_id=hit.citation.workspace_id,
space_id=hit.citation.space_id,
document_id=hit.citation.document_id,
document_version_id=hit.citation.document_version_id,
chunk_id=hit.citation.chunk_id,
parent_chunk_id=hit.citation.parent_chunk_id,
location=hit.citation.location,
excerpt=hit.citation.excerpt,
source_uri=hit.citation.source_uri,
acquired_at=hit.citation.acquired_at,
retrieval_run_id=request.retrieval_run_id,
index_alias=hit.citation.index_alias,
profile_version=hit.citation.profile_version,
),
)
return type(
"Result",
(),
{
"hits": (refreshed,),
"degraded": False,
"degradation_codes": (),
"timings_ms": {},
},
)()
inputs = _FixtureInputs(workspace, space, application)
search_service = SearchService(
authorization=RetrievalAuthorizationService(),
inputs=inputs,
executor=_SearchExecutor(),
)
context_candidate = ContextCandidate(citation, EVIDENCE_EXCERPT, 1, 1.0)
class _ContextExecutor:
async def execute(self, request: ContextExecutionRequest) -> ContextExecutionResult:
refreshed = Citation.create(
workspace_id=citation.workspace_id,
space_id=citation.space_id,
document_id=citation.document_id,
document_version_id=citation.document_version_id,
chunk_id=citation.chunk_id,
parent_chunk_id=citation.parent_chunk_id,
location=citation.location,
excerpt=citation.excerpt,
source_uri=citation.source_uri,
acquired_at=citation.acquired_at,
retrieval_run_id=request.retrieval_run_id,
index_alias=citation.index_alias,
profile_version=citation.profile_version,
)
return ContextExecutionResult(
candidates=(replace(context_candidate, citation=refreshed),)
)
context_service = ContextService(
authorization=RetrievalAuthorizationService(), inputs=inputs, executor=_ContextExecutor()
)
import json
import re
citation_id_pattern = re.compile(r"citation_ids=([0-9a-fA-F-]{36})")
class _Generator:
async def generate(self, *, prompt: str, generation_profile: str, spaces: Any) -> str:
match = citation_id_pattern.search(prompt)
assert match is not None
return json.dumps(
{
"status": "answerable",
"answer": "The refund window is thirty days from purchase.",
"citations": [match.group(1)],
}
)
answer_service = AnswerService(
authorization=RetrievalAuthorizationService(),
inputs=inputs,
context_executor=_ContextExecutor(),
generator=_Generator(),
)
document_repository = InMemoryDocumentRepository()
citation_repository = InMemoryCitationRepository()
citation_repository.add(citation)
class _AuthorizationInputsForMcp:
async def load(self, principal: Principal) -> AuthorizationInputs:
loaded = await inputs.load(principal)
return AuthorizationInputs(
spaces=loaded.spaces,
grants=loaded.grants,
user_grants=loaded.user_grants,
application=loaded.application,
)
deps = MCPDependencies(
search_application=search_service,
context_application=context_service,
answer_application=answer_service,
authorization_inputs=_AuthorizationInputsForMcp(),
credential_service=credential_service,
citation_repository=citation_repository,
document_repository=document_repository,
)
server = build_mcp_server(deps)
return McpFixture(
server=server,
deps=deps,
workspace=workspace,
application=application,
space_id=space_id,
token=issued.token,
document_repository=document_repository,
citation_repository=citation_repository,
)
_MCP_TEST_URL = "http://127.0.0.1:8765"
@contextlib.asynccontextmanager
async def mcp_session(
server: MCPServer, *, token: str | None = None
) -> AsyncIterator[ClientSession]:
"""A real streamable-HTTP MCP session over `httpx2.ASGITransport` -- genuine
Starlette `Request`/header handling, no real TCP socket needed.
`ASGITransport` never runs the ASGI app's own lifespan protocol (that's
normally what starts the session manager's task group when a real
server like uvicorn owns the app), so `session_manager.run()` is
entered explicitly here instead, mirroring what a real deployment's
lifespan would do.
"""
app = server.streamable_http_app(stateless_http=True)
headers = {"authorization": f"Bearer {token}"} if token else {}
async with (
server.session_manager.run(),
httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url=_MCP_TEST_URL, headers=headers
) as http_client,
streamable_http_client(f"{_MCP_TEST_URL}/mcp", http_client=http_client) as (read, write),
ClientSession(read, write) as session,
):
await session.initialize()
yield session
+225
View File
@@ -0,0 +1,225 @@
from __future__ import annotations
from dataclasses import replace
from mcp_fixtures import build_mcp_fixture, mcp_session
from ragcore.application.audit.service import AuditEventService
from ragcore.application.limits.models import RateLimitRule
from ragcore.application.limits.rate_limiter import RateLimiter, RateLimitRegistry
from ragcore.domain.control import Scope
from ragcore.mcp.server import build_mcp_server
EXPECTED_TOOLS = {
"rag_search",
"rag_get_context",
"rag_answer",
"rag_list_spaces",
"rag_get_document",
"rag_get_citation",
}
async def test_tool_surface_is_exactly_the_agreed_read_only_tools() -> None:
fixture = await build_mcp_fixture()
async with mcp_session(fixture.server, token=fixture.token) as session:
listed = await session.list_tools()
names = {tool.name for tool in listed.tools}
assert names == EXPECTED_TOOLS
for tool in listed.tools:
assert tool.annotations is not None
assert tool.annotations.read_only_hint is True
assert tool.annotations.destructive_hint is False
async def test_rag_search_returns_typed_results() -> None:
fixture = await build_mcp_fixture()
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool(
"rag_search", {"query": "refund", "spaces": [str(fixture.space_id)]}
)
assert result.is_error is not True
body = result.structured_content
assert body["effective_space_ids"] == [str(fixture.space_id)]
assert body["results"][0]["citation"]["excerpt_sha256"]
async def test_rag_get_context_returns_typed_blocks() -> None:
fixture = await build_mcp_fixture()
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool(
"rag_get_context", {"query": "refund", "spaces": [str(fixture.space_id)]}
)
assert result.is_error is not True
body = result.structured_content
assert body["citations"]
assert body["blocks"][0]["citation_ids"]
async def test_rag_answer_returns_grounded_answer() -> None:
fixture = await build_mcp_fixture()
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool(
"rag_answer", {"query": "refund window", "spaces": [str(fixture.space_id)]}
)
assert result.is_error is not True
body = result.structured_content
assert body["answer"]
assert body["citations"]
assert all(claim["citation_ids"] for claim in body["claims"])
async def test_rag_list_spaces_lists_only_granted_spaces() -> None:
fixture = await build_mcp_fixture()
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool("rag_list_spaces", {})
assert result.is_error is not True
spaces = result.structured_content["result"]
assert [s["id"] for s in spaces] == [str(fixture.space_id)]
async def test_rag_get_document_returns_the_published_version() -> None:
from datetime import UTC, datetime
from uuid import uuid4
from ragcore.application.documents.models import (
DocumentRecord,
DocumentVersionRecord,
DocumentVersionState,
)
fixture = await build_mcp_fixture()
document = DocumentRecord(
id=uuid4(),
workspace_id=fixture.workspace.id,
space_id=fixture.space_id,
source_key="policy.md",
external_id="policy",
title="Refund policy",
)
version = DocumentVersionRecord(
id=uuid4(),
document_id=document.id,
source_fingerprint="fp",
content_hash="hash",
parser_version="v1",
chunk_profile_version="v1",
state=DocumentVersionState.PUBLISHED,
created_at=datetime(2026, 8, 1, tzinfo=UTC),
updated_at=datetime(2026, 8, 1, tzinfo=UTC),
)
fixture.document_repository.add_document(document)
fixture.document_repository.add_version(version)
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool("rag_get_document", {"document_id": str(document.id)})
assert result.is_error is not True
body = result.structured_content
assert body["id"] == str(document.id)
assert body["version"]["id"] == str(version.id)
async def test_rag_get_document_hides_a_document_outside_granted_spaces() -> None:
from uuid import uuid4
from ragcore.application.documents.models import DocumentRecord
fixture = await build_mcp_fixture()
other_space_document = DocumentRecord(
id=uuid4(),
workspace_id=fixture.workspace.id,
space_id=uuid4(),
source_key="secret.md",
external_id="secret",
title="Not granted",
)
fixture.document_repository.add_document(other_space_document)
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool(
"rag_get_document", {"document_id": str(other_space_document.id)}
)
assert result.is_error is True
async def test_rag_get_citation_returns_a_granted_citation() -> None:
fixture = await build_mcp_fixture()
(citation,) = fixture.citation_repository._citations.values()
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool("rag_get_citation", {"citation_id": str(citation.id)})
assert result.is_error is not True
assert result.structured_content["id"] == str(citation.id)
async def test_rag_get_citation_hides_an_out_of_scope_citation() -> None:
from uuid import uuid4
fixture = await build_mcp_fixture()
outside = fixture.citation_repository
from mcp_fixtures import _citation
stray = _citation(fixture.workspace.id, uuid4())
outside.add(stray)
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool("rag_get_citation", {"citation_id": str(stray.id)})
assert result.is_error is True
async def test_no_token_is_rejected() -> None:
fixture = await build_mcp_fixture()
async with mcp_session(fixture.server, token=None) as session:
result = await session.call_tool("rag_search", {"query": "refund"})
assert result.is_error is True
async def test_wrong_scope_token_is_rejected() -> None:
fixture = await build_mcp_fixture(grant_scopes={Scope.SEARCH})
async with mcp_session(fixture.server, token=fixture.token) as session:
result = await session.call_tool("rag_answer", {"query": "refund"})
assert result.is_error is True
async def test_malformed_token_is_rejected() -> None:
fixture = await build_mcp_fixture()
async with mcp_session(fixture.server, token="not-a-real-token") as session:
result = await session.call_tool("rag_search", {"query": "refund"})
assert result.is_error is True
async def test_rate_limit_is_enforced_via_the_shared_rate_limiter() -> None:
fixture = await build_mcp_fixture()
# A default rule (keyed by operation only, no subject) applies to any
# caller -- the same `RateLimitRegistry` primitive M6-02 built for REST,
# reused unchanged here. `AuditEventService`'s repository is never
# touched: only `set_override` (not used by this test) calls `.record()`.
registry = RateLimitRegistry(
default_rules={"mcp:rag_search": RateLimitRule(max_requests=1, window_seconds=60.0)},
audit=AuditEventService(repository=None), # type: ignore[arg-type]
)
limiter = RateLimiter(registry=registry)
limited_server = build_mcp_server(replace(fixture.deps, rate_limiter=limiter))
async with mcp_session(limited_server, token=fixture.token) as session:
first = await session.call_tool(
"rag_search", {"query": "refund", "spaces": [str(fixture.space_id)]}
)
second = await session.call_tool(
"rag_search", {"query": "refund", "spaces": [str(fixture.space_id)]}
)
assert first.is_error is not True
assert second.is_error is True
@@ -0,0 +1,125 @@
"""One real MCP smoke flow: ragcore.mcp.main's own composition root, real
Postgres and real Qdrant -- not just the fixture-backed protocol tests in
test_mcp.py (which deliberately stay on fake repositories to prove the MCP
adapter's own auth/mapping/rate-limit layer, not re-prove infrastructure
wiring). This proves the actual production dependency graph
(``build_dependencies``/``build_mcp_server``) authenticates a real bearer
credential against real Postgres and serves a real tool call end to end.
"""
from __future__ import annotations
import asyncio
import sys
from collections.abc import AsyncIterator
from uuid import uuid4
import httpx
import pytest
from mcp_fixtures import mcp_session
from qdrant_client import AsyncQdrantClient
from sqlalchemy.ext.asyncio import AsyncEngine
from ragcore.application.identity.credentials import ServiceAccountCredentialService
from ragcore.config import Settings
from ragcore.domain.control import Scope, ServiceAccount, Workspace
from ragcore.infrastructure.database import create_database_engine, create_session_factory
from ragcore.infrastructure.database.control import ControlPlaneRepository
from ragcore.infrastructure.database.identity.repository import PostgresCredentialRepository
from ragcore.mcp.main import build_dependencies
from ragcore.mcp.server import build_mcp_server
from ragcore.platform.telemetry import configure_telemetry
pytestmark = pytest.mark.integration
_TEST_PEPPER = "test-credential-pepper-that-is-long-enough-for-real"
@pytest.fixture(scope="session")
def event_loop_policy() -> asyncio.AbstractEventLoopPolicy:
if sys.platform == "win32":
return asyncio.WindowsSelectorEventLoopPolicy()
return asyncio.get_event_loop_policy()
@pytest.fixture
async def db_engine() -> AsyncIterator[AsyncEngine]:
engine = create_database_engine(Settings(env="test"))
try:
yield engine
finally:
await engine.dispose()
async def test_real_composition_root_authenticates_and_serves_a_real_tool_call(
db_engine: AsyncEngine,
) -> None:
session_factory = create_session_factory(db_engine)
suffix = uuid4().hex[:8]
async with session_factory() as session, session.begin():
control = ControlPlaneRepository(session)
workspace = await control.create_workspace(
Workspace(uuid4(), "MCP Real Root", f"mcp-real-root-{suffix}")
)
application = await control.create_application(
workspace.create_application(
id=uuid4(),
name="MCP Real Root Client",
slug=f"mcp-real-root-client-{suffix}",
scopes=frozenset({Scope.SEARCH, Scope.CONTEXT, Scope.ANSWER}),
)
)
service_account = await control.create_service_account(
ServiceAccount(
id=uuid4(),
workspace_id=workspace.id,
application_id=application.id,
name="MCP Real Root Service Account",
scopes=application.scopes,
)
)
credential_service = ServiceAccountCredentialService(
PostgresCredentialRepository(session),
_TEST_PEPPER,
control.get_service_account_by_id,
)
issued = await credential_service.issue(service_account, scopes=service_account.scopes)
# Same pepper the credential above was issued with, so
# build_dependencies' own session-scoped credential service (which reads
# settings.api_token_pepper) can verify it for real.
settings = Settings(env="test", api_token_pepper=_TEST_PEPPER)
qdrant_client = AsyncQdrantClient(
url=settings.qdrant_url, api_key=settings.qdrant_api_key.get_secret_value(), timeout=30
)
http_client = httpx.AsyncClient(follow_redirects=False)
telemetry = configure_telemetry(settings, service_name="test-mcp")
try:
deps = build_dependencies(
settings,
engine=db_engine,
qdrant_client=qdrant_client,
http_client=http_client,
telemetry=telemetry,
)
server = build_mcp_server(deps)
async with mcp_session(server, token=issued.token) as mcp_client_session:
listed = await mcp_client_session.list_tools()
result = await mcp_client_session.call_tool("rag_list_spaces", {})
finally:
await http_client.aclose()
await qdrant_client.close()
telemetry.shutdown()
assert {tool.name for tool in listed.tools} == {
"rag_search",
"rag_get_context",
"rag_answer",
"rag_list_spaces",
"rag_get_document",
"rag_get_citation",
}
assert result.is_error is not True