davenpi commited on
Commit
a5dd87c
·
1 Parent(s): f8721ad

Expose model preferences in ctx.sample

Browse files
docs/servers/context.mdx CHANGED
@@ -228,8 +228,8 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
228
  # Create a sampling prompt asking for sentiment analysis
229
  prompt = f"Analyze the sentiment of the following text as positive, negative, or neutral. Just output a single word - 'positive', 'negative', or 'neutral'. Text to analyze: {text}"
230
 
231
- # Send the sampling request to the client's LLM
232
- response = await ctx.sample(prompt)
233
 
234
  # Process the LLM's response
235
  sentiment = response.text.strip().lower()
@@ -247,11 +247,12 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict:
247
 
248
  **Method signature:**
249
 
250
- - **`ctx.sample(messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None) -> TextContent | ImageContent`**
251
  - `messages`: A string or list of strings/message objects to send to the LLM
252
  - `system_prompt`: Optional system prompt to guide the LLM's behavior
253
  - `temperature`: Optional sampling temperature (controls randomness)
254
  - `max_tokens`: Optional maximum number of tokens to generate (defaults to 512)
 
255
  - Returns the LLM's response as TextContent or ImageContent
256
 
257
  When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles.
 
228
  # Create a sampling prompt asking for sentiment analysis
229
  prompt = f"Analyze the sentiment of the following text as positive, negative, or neutral. Just output a single word - 'positive', 'negative', or 'neutral'. Text to analyze: {text}"
230
 
231
+ # Send the sampling request to the clients LLM (provide a hint for the model you want to use)
232
+ response = await ctx.sample(prompt, model_preferences="claude-3-sonnet")
233
 
234
  # Process the LLM's response
235
  sentiment = response.text.strip().lower()
 
247
 
248
  **Method signature:**
249
 
250
+ - **`ctx.sample(messages: str | list[str | SamplingMessage], system_prompt: str | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None) -> TextContent | ImageContent`**
251
  - `messages`: A string or list of strings/message objects to send to the LLM
252
  - `system_prompt`: Optional system prompt to guide the LLM's behavior
253
  - `temperature`: Optional sampling temperature (controls randomness)
254
  - `max_tokens`: Optional maximum number of tokens to generate (defaults to 512)
255
+ - `model_preferences`: Optional model selection preferences (e.g., a model hint string, list of hints, or a ModelPreferences object)
256
  - Returns the LLM's response as TextContent or ImageContent
257
 
258
  When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles.
src/fastmcp/server/context.py CHANGED
@@ -12,6 +12,8 @@ from mcp.shared.context import RequestContext
12
  from mcp.types import (
13
  CreateMessageResult,
14
  ImageContent,
 
 
15
  Root,
16
  SamplingMessage,
17
  TextContent,
@@ -200,6 +202,7 @@ class Context:
200
  system_prompt: str | None = None,
201
  temperature: float | None = None,
202
  max_tokens: int | None = None,
 
203
  ) -> TextContent | ImageContent:
204
  """
205
  Send a sampling request to the client and await the response.
@@ -231,6 +234,7 @@ class Context:
231
  system_prompt=system_prompt,
232
  temperature=temperature,
233
  max_tokens=max_tokens,
 
234
  )
235
 
236
  return result.content
@@ -248,3 +252,42 @@ class Context:
248
  )
249
 
250
  return fastmcp.server.dependencies.get_http_request()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  from mcp.types import (
13
  CreateMessageResult,
14
  ImageContent,
15
+ ModelHint,
16
+ ModelPreferences,
17
  Root,
18
  SamplingMessage,
19
  TextContent,
 
202
  system_prompt: str | None = None,
203
  temperature: float | None = None,
204
  max_tokens: int | None = None,
205
+ model_preferences: ModelPreferences | str | list[str] | None = None,
206
  ) -> TextContent | ImageContent:
207
  """
208
  Send a sampling request to the client and await the response.
 
234
  system_prompt=system_prompt,
235
  temperature=temperature,
236
  max_tokens=max_tokens,
237
+ model_preferences=self._parse_model_preferences(model_preferences),
238
  )
239
 
240
  return result.content
 
252
  )
253
 
254
  return fastmcp.server.dependencies.get_http_request()
255
+
256
+ def _parse_model_preferences(self, model_preferences) -> ModelPreferences | None:
257
+ """
258
+ Validates and converts user input for model_preferences into a ModelPreferences object.
259
+
260
+ Args:
261
+ model_preferences (ModelPreferences | str | list[str] | None):
262
+ The model preferences to use. Accepts:
263
+ - ModelPreferences (returns as-is)
264
+ - str (single model hint)
265
+ - list[str] (multiple model hints)
266
+ - None (no preferences)
267
+
268
+ Returns:
269
+ ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
270
+
271
+ Raises:
272
+ ValueError: If the input is not a supported type or contains invalid values.
273
+ """
274
+ if model_preferences is None:
275
+ return None
276
+ if isinstance(model_preferences, ModelPreferences):
277
+ return model_preferences
278
+ if isinstance(model_preferences, str):
279
+ # Single model hint
280
+ return ModelPreferences(hints=[ModelHint(name=model_preferences)])
281
+ if isinstance(model_preferences, list):
282
+ # List of model hints (strings)
283
+ if not all(isinstance(h, str) for h in model_preferences):
284
+ raise ValueError(
285
+ "All elements of model_preferences list must be"
286
+ " strings (model name hints)."
287
+ )
288
+ return ModelPreferences(
289
+ hints=[ModelHint(name=h) for h in model_preferences]
290
+ )
291
+ raise ValueError(
292
+ "model_preferences must be one of: ModelPreferences, str, list[str], or None."
293
+ )
tests/server/test_context.py CHANGED
@@ -2,9 +2,11 @@ import warnings
2
  from unittest.mock import MagicMock, patch
3
 
4
  import pytest
 
5
  from starlette.requests import Request
6
 
7
  from fastmcp.server.context import Context
 
8
 
9
 
10
  class TestContextDeprecations:
@@ -57,3 +59,30 @@ class TestContextDeprecations:
57
  assert "https://gofastmcp.com/patterns/http-requests" in str(
58
  warning.message
59
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from unittest.mock import MagicMock, patch
3
 
4
  import pytest
5
+ from mcp.types import ModelPreferences
6
  from starlette.requests import Request
7
 
8
  from fastmcp.server.context import Context
9
+ from fastmcp.server.server import FastMCP
10
 
11
 
12
  class TestContextDeprecations:
 
59
  assert "https://gofastmcp.com/patterns/http-requests" in str(
60
  warning.message
61
  )
62
+
63
+
64
+ @pytest.fixture
65
+ def context():
66
+ return Context(fastmcp=FastMCP())
67
+
68
+
69
+ class TestParseModelPreferences:
70
+ def test_parse_model_preferences_string(self, context):
71
+ mp = context._parse_model_preferences("claude-3-sonnet")
72
+ assert isinstance(mp, ModelPreferences)
73
+ assert mp.hints is not None
74
+ assert mp.hints[0].name == "claude-3-sonnet"
75
+
76
+ def test_parse_model_preferences_list(self, context):
77
+ mp = context._parse_model_preferences(["claude-3-sonnet", "claude"])
78
+ assert isinstance(mp, ModelPreferences)
79
+ assert mp.hints is not None
80
+ assert [h.name for h in mp.hints] == ["claude-3-sonnet", "claude"]
81
+
82
+ def test_parse_model_preferences_object(self, context):
83
+ obj = ModelPreferences(hints=[])
84
+ assert context._parse_model_preferences(obj) is obj
85
+
86
+ def test_parse_model_preferences_invalid_type(self, context):
87
+ with pytest.raises(ValueError):
88
+ context._parse_model_preferences(123)