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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user