"""
MCP Sampling Support - Client-side LLM formatting
This module provides reusable sampling functionality that allows the client's LLM
to format/restructure tool responses based on user preferences.
Key Benefits:
- User controls output format (tables, JSON, bullets, etc.)
- Reusable across all tools (DRY principle)
- Offloads formatting to client (reduces server load)
- Consistent with user's preferred style
Usage:
from src.core.sampling import create_sampling_request
# In your tool:
return create_sampling_request(
data=vendor_data,
prompt="Format this vendor list as a table with columns: Name, Location, Manager",
model_preferences={"temperature": 0.0}
)
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from dataclasses import dataclass, asdict
@dataclass
class SamplingMessage:
"""
A message in the sampling conversation.
This follows the standard LLM message format with role and content.
"""
role: str # "user", "assistant", or "system"
content: str
@dataclass
class ModelPreferences:
"""
Preferences for how the client LLM should generate the response.
These are hints to the client about how to format the output.
"""
max_tokens: Optional[int] = 1000
temperature: Optional[float] = 0.0 # 0.0 = deterministic, 1.0 = creative
stop_sequences: Optional[List[str]] = None
top_p: Optional[float] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary, excluding None values."""
return {k: v for k, v in asdict(self).items() if v is not None}
@dataclass
class SamplingRequest:
"""
A request for the client to sample (generate) formatted content.
This is what gets returned to the MCP client when you want the
client's LLM to format your data.
"""
messages: List[SamplingMessage]
model_preferences: Optional[ModelPreferences] = None
system_prompt: Optional[str] = None
include_context: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to MCP-compatible dictionary format."""
result = {
"messages": [
{"role": msg.role, "content": msg.content}
for msg in self.messages
]
}
if self.model_preferences:
result["modelPreferences"] = self.model_preferences.to_dict()
if self.system_prompt:
result["systemPrompt"] = self.system_prompt
if self.include_context:
result["includeContext"] = self.include_context
return result
def create_sampling_request(
data: Any,
prompt: str,
system_prompt: Optional[str] = None,
temperature: float = 0.0,
max_tokens: int = 2000,
include_raw_data: bool = True
) -> Dict[str, Any]:
"""
Create a sampling request for client-side formatting.
This is the main function you'll use in your tools to request
client-side formatting of your data.
Args:
data: The raw data to be formatted (will be JSON-serialized)
prompt: Instructions for how to format the data
system_prompt: Optional system-level instructions
temperature: 0.0 = deterministic, 1.0 = creative
max_tokens: Maximum tokens for the response
include_raw_data: Whether to include raw data in the prompt
Returns:
Dictionary in MCP sampling format
Example:
```python
# In your tool:
vendors = fetch_vendors()
return create_sampling_request(
data=vendors,
prompt="Format this vendor list as a markdown table with columns: Name, Location, Manager, Status",
temperature=0.0
)
```
"""
import json
# Build the user message
if include_raw_data:
# Include the raw data so the LLM can format it
data_str = json.dumps(data, indent=2)
user_message = f"{prompt}\n\nData to format:\n```json\n{data_str}\n```"
else:
user_message = prompt
# Create messages
messages = [
SamplingMessage(
role="user",
content=user_message
)
]
# Create model preferences
model_prefs = ModelPreferences(
temperature=temperature,
max_tokens=max_tokens
)
# Create sampling request
sampling_request = SamplingRequest(
messages=messages,
model_preferences=model_prefs,
system_prompt=system_prompt
)
return {
"_meta": {
"sampling": True,
"description": "Client-side formatting requested"
},
"sampling_request": sampling_request.to_dict(),
"raw_data": data # Include raw data for fallback
}
def create_format_options_sampling(
data: Any,
available_formats: List[str],
default_format: str = "markdown"
) -> Dict[str, Any]:
"""
Create a sampling request that lets users choose output format.
This is useful when you want to give users multiple format options
(table, JSON, bullets, etc.) and let them choose.
Args:
data: The raw data to be formatted
available_formats: List of supported formats
default_format: Default format if user doesn't specify
Returns:
Dictionary in MCP sampling format
Example:
```python
return create_format_options_sampling(
data=vendors,
available_formats=["table", "json", "bullets", "csv"],
default_format="table"
)
```
"""
import json
data_str = json.dumps(data, indent=2)
formats_list = "\n".join([f"- {fmt}" for fmt in available_formats])
prompt = f"""Format the following data in {default_format} format.
Available formats:
{formats_list}
If the user requests a different format, use that instead.
Data to format:
```json
{data_str}
```
Please format this data clearly and professionally."""
return create_sampling_request(
data=data,
prompt=prompt,
temperature=0.0,
include_raw_data=False # Already included in prompt
)
def create_smart_formatting_sampling(
data: Any,
data_type: str,
user_preference: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a sampling request with smart formatting based on data type.
This automatically suggests the best format based on the data type
and respects user preferences if provided.
Args:
data: The raw data to be formatted
data_type: Type of data (e.g., "vendor_list", "vendor_details", "contracts")
user_preference: User's preferred format (if any)
Returns:
Dictionary in MCP sampling format
Example:
```python
return create_smart_formatting_sampling(
data=vendors,
data_type="vendor_list",
user_preference="table" # From user's previous requests
)
```
"""
import json
# Smart format suggestions based on data type
format_suggestions = {
"vendor_list": "markdown table with columns: Name, Location, Manager, Status",
"vendor_details": "structured format with sections for: Basic Info, Contacts, Contracts, Documents",
"contracts": "table with columns: Contract Name, Status, Amount, Start Date, End Date",
"organizations": "hierarchical list showing organization structure",
"summary": "bullet points with key metrics highlighted",
}
suggested_format = format_suggestions.get(data_type, "clear and organized format")
# Override with user preference if provided
if user_preference:
format_instruction = f"Format as {user_preference}"
else:
format_instruction = f"Format as {suggested_format}"
data_str = json.dumps(data, indent=2)
prompt = f"""{format_instruction}
Data to format:
```json
{data_str}
```
Make it clear, professional, and easy to read."""
system_prompt = f"""You are a data formatting assistant. Your job is to take structured data and present it in the format requested by the user.
Guidelines:
- Use markdown formatting for tables and lists
- Highlight important information
- Keep it concise and scannable
- Use appropriate emojis sparingly for visual appeal
- Ensure all data is accurately represented"""
return create_sampling_request(
data=data,
prompt=prompt,
system_prompt=system_prompt,
temperature=0.0,
include_raw_data=False
)
# Convenience functions for common formats
def format_as_table(data: Any, columns: List[str]) -> Dict[str, Any]:
"""Request table formatting with specific columns."""
columns_str = ", ".join(columns)
return create_sampling_request(
data=data,
prompt=f"Format this data as a markdown table with columns: {columns_str}",
temperature=0.0
)
def format_as_json(data: Any, pretty: bool = True) -> Dict[str, Any]:
"""Request JSON formatting."""
style = "pretty-printed with indentation" if pretty else "compact"
return create_sampling_request(
data=data,
prompt=f"Format this data as {style} JSON",
temperature=0.0
)
def format_as_bullets(data: Any, hierarchical: bool = False) -> Dict[str, Any]:
"""Request bullet point formatting."""
style = "hierarchical nested" if hierarchical else "flat"
return create_sampling_request(
data=data,
prompt=f"Format this data as {style} bullet points",
temperature=0.0
)
def format_as_csv(data: Any) -> Dict[str, Any]:
"""Request CSV formatting."""
return create_sampling_request(
data=data,
prompt="Format this data as CSV (comma-separated values)",
temperature=0.0
)