feat: implement MCP sampling for semantic search RAG (ADR-008)

Add nc_notes_semantic_search_answer tool that combines semantic search
with MCP sampling to generate natural language answers from retrieved
Nextcloud Notes. This enables Retrieval-Augmented Generation (RAG)
patterns without requiring a server-side LLM.

Key features:
- Client-side LLM generation via ctx.session.create_message()
- Graceful fallback when sampling unavailable
- Proper source citations in generated answers
- No results optimization (skips sampling when no docs found)
- Comprehensive unit and integration tests

Implementation details:
- SamplingSearchResponse model with generated_answer and sources
- Fixed prompt template with document context and citation instructions
- Model preferences hint Claude Sonnet for balanced performance
- Falls back to returning documents without answer on sampling failure

Updates:
- Add ADR-008 documenting sampling architecture decision
- Add MCP sampling pattern guidance to CLAUDE.md
- Update README.md and docs/notes.md (7 → 9 tools)
- Add 4 unit tests and 6 integration tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2025-11-09 01:00:18 +01:00
co-authored by Claude
parent e32c8f4aec
commit bb5d4f464f
8 changed files with 1350 additions and 4 deletions
+76
View File
@@ -224,6 +224,82 @@ docker compose exec db mariadb -u root -ppassword nextcloud -e \
**Testing**: Extract `data["results"]` from MCP responses, not `data` directly.
## MCP Sampling for RAG (ADR-008)
**What is MCP Sampling?**
MCP sampling allows servers to request LLM completions from their clients. This enables Retrieval-Augmented Generation (RAG) patterns where the server retrieves context and the client's LLM generates answers.
**When to use sampling:**
- Generating natural language answers from retrieved documents
- Synthesizing information from multiple sources
- Creating summaries with citations
**Implementation Pattern** (see ADR-008 for details):
```python
from mcp.types import ModelHint, ModelPreferences, SamplingMessage, TextContent
@mcp.tool()
@require_scopes("notes:read")
async def nc_notes_semantic_search_answer(
query: str, ctx: Context, limit: int = 5, max_answer_tokens: int = 500
) -> SamplingSearchResponse:
# 1. Retrieve documents
search_response = await nc_notes_semantic_search(query, ctx, limit)
# 2. Check for no results (don't waste sampling call)
if not search_response.results:
return SamplingSearchResponse(
query=query,
generated_answer="No relevant documents found.",
sources=[], total_found=0, success=True
)
# 3. Construct prompt with retrieved context
prompt = f"{query}\n\nDocuments:\n{format_sources(search_response.results)}\n\nProvide answer with citations."
# 4. Request LLM completion via sampling
try:
result = await ctx.session.create_message(
messages=[SamplingMessage(role="user", content=TextContent(type="text", text=prompt))],
max_tokens=max_answer_tokens,
temperature=0.7,
model_preferences=ModelPreferences(
hints=[ModelHint(name="claude-3-5-sonnet")],
intelligencePriority=0.8,
speedPriority=0.5,
),
include_context="thisServer",
)
return SamplingSearchResponse(
query=query,
generated_answer=result.content.text,
sources=search_response.results,
model_used=result.model,
stop_reason=result.stopReason,
success=True
)
except Exception as e:
# Fallback: Return documents without generated answer
return SamplingSearchResponse(
query=query,
generated_answer=f"[Sampling unavailable: {e}]\n\nFound {len(search_response.results)} documents.",
sources=search_response.results,
search_method="semantic_sampling_fallback",
success=True
)
```
**Key Points**:
- **No server-side LLM**: Server has no API keys, client controls which model is used
- **Graceful degradation**: Tool always returns useful results even if sampling fails
- **User control**: MCP clients SHOULD prompt users to approve sampling requests
- **No results optimization**: Skip sampling call when no documents found
- **Fixed prompts**: Prompts are not user-configurable to avoid injection risks
**Reference**: See `nc_notes_semantic_search_answer` in `nextcloud_mcp_server/server/notes.py:517` and ADR-008 for complete implementation.
## Testing Best Practices (MANDATORY)
### Always Run Tests