mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-26 07:24:18 +02:00
Add Approval and Choice providers (#3686)
This commit is contained in:
parent
8c10bb8881
commit
beb35a4ed8
11 changed files with 625 additions and 0 deletions
BIN
docs/apps/images/app-approval.png
Normal file
BIN
docs/apps/images/app-approval.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 571 KiB |
BIN
docs/apps/images/app-choice.png
Normal file
BIN
docs/apps/images/app-choice.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 536 KiB |
80
docs/apps/providers/approval.mdx
Normal file
80
docs/apps/providers/approval.mdx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
title: Approval
|
||||
sidebarTitle: Approval
|
||||
description: Human-in-the-loop approval gates for agent actions
|
||||
icon: shield-check
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
`Approval` adds a human-in-the-loop confirmation step to any server. The LLM presents what it's about to do, the user approves or rejects via buttons, and the decision flows back into the conversation as a message.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-approval.png" alt="The Approval provider shown in Goose, with a payment confirmation card and Approve/Cancel buttons" />
|
||||
</Frame>
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.approval import Approval
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(Approval())
|
||||
```
|
||||
|
||||
This registers a single tool:
|
||||
|
||||
| Tool | Visibility | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `request_approval` | Model | Shows an approval card, sends the user's decision back as a message |
|
||||
|
||||
The LLM calls `request_approval` with a summary (and optional details) whenever it's about to take a significant action. The user sees a card with Approve and Reject buttons. Clicking either sends a message back into the conversation via `SendMessage`, which triggers the LLM's next turn.
|
||||
|
||||
The message looks like it came from the user:
|
||||
|
||||
```
|
||||
"Deploy v3.2 to production" — I selected: Approve
|
||||
```
|
||||
|
||||
<Note>
|
||||
Approval is an advisory gate, not an enforcement mechanism. The conversation isn't blocked while the card is open — the user can keep typing, and a determined LLM could proceed without waiting. Think of it as a strong UX signal that encourages confirmation, not a security boundary. For hard enforcement, implement approval logic server-side in your tool implementations.
|
||||
</Note>
|
||||
|
||||
## Configuration
|
||||
|
||||
The constructor sets defaults; the LLM can override all of these per-call via tool arguments.
|
||||
|
||||
```python
|
||||
Approval(
|
||||
name="Approval", # App name
|
||||
title="Approval Required", # Card heading
|
||||
approve_text="Approve", # Approve button label
|
||||
reject_text="Reject", # Reject button label
|
||||
approve_variant="default", # "default", "destructive", "success", "info"
|
||||
reject_variant="outline", # same options plus "outline"
|
||||
)
|
||||
```
|
||||
|
||||
The LLM can customize each invocation:
|
||||
|
||||
```python
|
||||
request_approval(
|
||||
summary="Delete 47 files from /tmp",
|
||||
details="This cannot be undone.",
|
||||
title="Destructive Action",
|
||||
approve_text="Delete",
|
||||
approve_variant="destructive",
|
||||
reject_text="Keep files",
|
||||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
When the user clicks a button, two things happen:
|
||||
|
||||
1. `SendMessage` pushes the decision into the conversation as a user message
|
||||
2. `SetState("decided", True)` replaces the buttons with "Response sent."
|
||||
|
||||
The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding. If approved, it continues. If rejected, it acknowledges and asks how to proceed.
|
||||
72
docs/apps/providers/choice.mdx
Normal file
72
docs/apps/providers/choice.mdx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
title: Choice
|
||||
sidebarTitle: Choice
|
||||
description: Present clickable options instead of free-text responses
|
||||
icon: list-check
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
`Choice` lets the LLM present a set of options as clickable buttons instead of asking the user to type a response. The selection flows back into the conversation as a message, giving the LLM clean structured input.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-choice.png" alt="The Choice provider shown in Goose, with four lunch options as clickable buttons" />
|
||||
</Frame>
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.choice import Choice
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(Choice())
|
||||
```
|
||||
|
||||
This registers a single tool:
|
||||
|
||||
| Tool | Visibility | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `choose` | Model | Shows a card with clickable options, sends the selection back as a message |
|
||||
|
||||
The LLM calls `choose` with a prompt and a list of options. The user sees a card with one button per option. Clicking one sends a message back into the conversation:
|
||||
|
||||
```
|
||||
"Which deployment strategy?" — I selected: Blue-green
|
||||
```
|
||||
|
||||
<Note>
|
||||
Like [Approval](/apps/providers/approval), this is an advisory interaction — the conversation isn't blocked while the card is open. The tool description instructs the LLM to wait for the "I selected:" response before proceeding.
|
||||
</Note>
|
||||
|
||||
## Configuration
|
||||
|
||||
The constructor sets defaults; the LLM can override `title` per-call.
|
||||
|
||||
```python
|
||||
Choice(
|
||||
name="Choice", # App name
|
||||
title="Choose an Option", # Default card heading
|
||||
variant="outline", # Button style for all options
|
||||
)
|
||||
```
|
||||
|
||||
The LLM provides the options per-call:
|
||||
|
||||
```python
|
||||
choose(
|
||||
prompt="What should we have for lunch?",
|
||||
options=["Pizza", "Tacos", "Ramen", "Salad"],
|
||||
title="The Important Questions",
|
||||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
Each option renders as a full-width button in a vertical stack. When the user clicks one:
|
||||
|
||||
1. `SendMessage` pushes the selection into the conversation as a user message
|
||||
2. `SetState("decided", True)` replaces the buttons with "Response sent."
|
||||
|
||||
The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding with whatever the user chose.
|
||||
|
|
@ -213,6 +213,8 @@
|
|||
"group": "Providers",
|
||||
"icon": "layer-group",
|
||||
"pages": [
|
||||
"apps/providers/approval",
|
||||
"apps/providers/choice",
|
||||
"apps/providers/file-upload",
|
||||
"apps/providers/generative"
|
||||
],
|
||||
|
|
|
|||
13
examples/apps/approval/approval_server.py
Normal file
13
examples/apps/approval/approval_server.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""Approval gate — require human sign-off before the agent acts.
|
||||
|
||||
Usage:
|
||||
uv run python approval_server.py
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.approval import Approval
|
||||
|
||||
mcp = FastMCP("Approval Demo", providers=[Approval()])
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
13
examples/apps/choice/choice_server.py
Normal file
13
examples/apps/choice/choice_server.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""Multiple choice — let the user pick from options instead of typing.
|
||||
|
||||
Usage:
|
||||
uv run python choice_server.py
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.choice import Choice
|
||||
|
||||
mcp = FastMCP("Choice Demo", providers=[Choice()])
|
||||
|
||||
if __name__ == "__main__":
|
||||
mcp.run()
|
||||
198
src/fastmcp/apps/approval.py
Normal file
198
src/fastmcp/apps/approval.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
"""Approval — a Provider that adds human-in-the-loop approval to any server.
|
||||
|
||||
The LLM presents a summary of what it's about to do, and the user
|
||||
approves or rejects via buttons. The result is sent back into the
|
||||
conversation as a message, prompting the LLM's next turn.
|
||||
|
||||
Requires ``fastmcp[apps]`` (prefab-ui).
|
||||
|
||||
Usage::
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.approval import Approval
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(Approval())
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
try:
|
||||
from prefab_ui.actions import SetState
|
||||
from prefab_ui.actions.mcp import SendMessage
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
H3,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
Column,
|
||||
Muted,
|
||||
Row,
|
||||
Text,
|
||||
)
|
||||
from prefab_ui.components.control_flow import If
|
||||
from prefab_ui.rx import STATE
|
||||
except ImportError as _exc:
|
||||
raise ImportError(
|
||||
"Approval requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
|
||||
) from _exc
|
||||
|
||||
|
||||
from fastmcp.apps.app import FastMCPApp
|
||||
|
||||
|
||||
class Approval(FastMCPApp):
|
||||
"""A Provider that adds human-in-the-loop approval to a server.
|
||||
|
||||
The LLM calls the ``request_approval`` tool with a summary and
|
||||
optional details. The user sees an approval card with Approve and
|
||||
Reject buttons. Clicking either sends a message back into the
|
||||
conversation (via ``SendMessage``), triggering the LLM's next turn.
|
||||
|
||||
The message appears as if the user sent it, so the LLM sees
|
||||
something like ``'"Deploy v3.2 to production" is APPROVED'``.
|
||||
|
||||
Example::
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.approval import Approval
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(Approval())
|
||||
|
||||
Customized::
|
||||
|
||||
Approval(
|
||||
title="Deploy Gate",
|
||||
approve_text="Ship it",
|
||||
approve_variant="default",
|
||||
reject_text="Abort",
|
||||
reject_variant="destructive",
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Approval",
|
||||
*,
|
||||
title: str = "Approval Required",
|
||||
approve_text: str = "Approve",
|
||||
reject_text: str = "Reject",
|
||||
approve_variant: Literal[
|
||||
"default", "destructive", "success", "info"
|
||||
] = "default",
|
||||
reject_variant: Literal[
|
||||
"default", "outline", "destructive", "success", "info"
|
||||
] = "outline",
|
||||
) -> None:
|
||||
super().__init__(name)
|
||||
self._title = title
|
||||
self._approve_text = approve_text
|
||||
self._reject_text = reject_text
|
||||
self._approve_variant = approve_variant
|
||||
self._reject_variant = reject_variant
|
||||
self._register_tools()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Approval({self.name!r})"
|
||||
|
||||
def _register_tools(self) -> None:
|
||||
provider = self
|
||||
|
||||
@self.ui()
|
||||
def request_approval(
|
||||
summary: str,
|
||||
details: str | None = None,
|
||||
title: str | None = None,
|
||||
approve_text: str | None = None,
|
||||
reject_text: str | None = None,
|
||||
approve_variant: str | None = None,
|
||||
reject_variant: str | None = None,
|
||||
) -> PrefabApp:
|
||||
"""Request human approval before proceeding with an action.
|
||||
|
||||
Call this tool proactively whenever you are about to take a
|
||||
significant or irreversible action and want the user to
|
||||
confirm first. Do NOT wait for the user to ask you to seek
|
||||
approval — use your judgment about when confirmation is
|
||||
appropriate.
|
||||
|
||||
The user will see an approval card with the summary, optional
|
||||
details, and Approve/Reject buttons. When they click a button,
|
||||
their decision appears as a message in the conversation (as if
|
||||
the user typed it), like:
|
||||
|
||||
"Deploy v3.2 to production" — I selected: Approve
|
||||
|
||||
or:
|
||||
|
||||
"Deploy v3.2 to production" — I selected: Reject
|
||||
|
||||
IMPORTANT: After calling this tool, you MUST stop and wait
|
||||
for the user's response. Do not continue, do not take any
|
||||
other actions, do not generate further output until you see
|
||||
the "I selected:" message. If approved, continue with the
|
||||
action. If rejected, acknowledge and ask how to proceed.
|
||||
|
||||
Args:
|
||||
summary: Brief description of the action requiring approval
|
||||
(shown prominently to the user).
|
||||
details: Optional longer explanation, context, or
|
||||
consequences of the action.
|
||||
title: Heading for the approval card (default: "Approval Required").
|
||||
approve_text: Label for the approve button (default: "Approve").
|
||||
reject_text: Label for the reject button (default: "Reject").
|
||||
approve_variant: Button style — "default", "destructive",
|
||||
"success", or "info".
|
||||
reject_variant: Button style for the reject button
|
||||
(same options plus "outline").
|
||||
"""
|
||||
_title = title or provider._title
|
||||
_approve = approve_text or provider._approve_text
|
||||
_reject = reject_text or provider._reject_text
|
||||
_approve_v = approve_variant or provider._approve_variant
|
||||
_reject_v = reject_variant or provider._reject_variant
|
||||
|
||||
approve_msg = f'"{summary}" — I selected: {_approve}'
|
||||
reject_msg = f'"{summary}" — I selected: {_reject}'
|
||||
|
||||
with Card(css_class="max-w-lg mx-auto") as view:
|
||||
with CardHeader():
|
||||
H3(_title)
|
||||
|
||||
with CardContent(), Column(gap=3):
|
||||
Text(summary, css_class="font-medium")
|
||||
if details:
|
||||
Muted(details)
|
||||
|
||||
with CardFooter():
|
||||
with If(STATE.decided):
|
||||
Muted("Response sent.")
|
||||
with If(~STATE.decided): # noqa: SIM117
|
||||
with Row(gap=2, css_class="w-full justify-end"):
|
||||
Button(
|
||||
_reject,
|
||||
variant=_reject_v,
|
||||
on_click=[
|
||||
SendMessage(reject_msg),
|
||||
SetState("decided", True),
|
||||
],
|
||||
)
|
||||
Button(
|
||||
_approve,
|
||||
variant=_approve_v,
|
||||
on_click=[
|
||||
SendMessage(approve_msg),
|
||||
SetState("decided", True),
|
||||
],
|
||||
)
|
||||
|
||||
return PrefabApp(
|
||||
view=view,
|
||||
state={"decided": False},
|
||||
)
|
||||
141
src/fastmcp/apps/choice.py
Normal file
141
src/fastmcp/apps/choice.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
"""Choice — a Provider that lets the user pick from a set of options.
|
||||
|
||||
The LLM presents options, the user clicks one, and the selection
|
||||
flows back into the conversation as a message.
|
||||
|
||||
Requires ``fastmcp[apps]`` (prefab-ui).
|
||||
|
||||
Usage::
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.choice import Choice
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(Choice())
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
try:
|
||||
from prefab_ui.actions import SetState
|
||||
from prefab_ui.actions.mcp import SendMessage
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
H3,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
Column,
|
||||
Muted,
|
||||
Text,
|
||||
)
|
||||
from prefab_ui.components.control_flow import If
|
||||
from prefab_ui.rx import STATE
|
||||
except ImportError as _exc:
|
||||
raise ImportError(
|
||||
"Choice requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
|
||||
) from _exc
|
||||
|
||||
from fastmcp.apps.app import FastMCPApp
|
||||
|
||||
|
||||
class Choice(FastMCPApp):
|
||||
"""A Provider that lets the user choose from a set of options.
|
||||
|
||||
The LLM calls ``choose`` with a prompt and a list of options.
|
||||
The user sees a card with one button per option. Clicking a button
|
||||
sends the selection back into the conversation via ``SendMessage``,
|
||||
triggering the LLM's next turn.
|
||||
|
||||
Example::
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.choice import Choice
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(Choice())
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "Choice",
|
||||
*,
|
||||
title: str = "Choose an Option",
|
||||
variant: Literal[
|
||||
"default", "outline", "destructive", "success", "info"
|
||||
] = "outline",
|
||||
) -> None:
|
||||
super().__init__(name)
|
||||
self._title = title
|
||||
self._variant = variant
|
||||
self._register_tools()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Choice({self.name!r})"
|
||||
|
||||
def _register_tools(self) -> None:
|
||||
provider = self
|
||||
|
||||
@self.ui()
|
||||
def choose(
|
||||
prompt: str,
|
||||
options: list[str],
|
||||
title: str | None = None,
|
||||
) -> PrefabApp:
|
||||
"""Present the user with a set of options to choose from.
|
||||
|
||||
Call this tool when you need the user to make a decision
|
||||
between discrete alternatives. Use it proactively — don't
|
||||
ask the user to type their choice in chat when you can
|
||||
present clean, clickable options instead.
|
||||
|
||||
The user will see a card with one button per option. When
|
||||
they click one, their choice appears as a message in the
|
||||
conversation (as if the user typed it), like:
|
||||
|
||||
"Which deployment strategy?" — I selected: Blue-green
|
||||
|
||||
IMPORTANT: After calling this tool, you MUST stop and wait
|
||||
for the user's response. Do not continue or take any other
|
||||
actions until you see the "I selected:" message.
|
||||
|
||||
Args:
|
||||
prompt: The question or decision to present to the user.
|
||||
options: List of options the user can choose from.
|
||||
title: Optional heading for the card.
|
||||
"""
|
||||
_title = title or provider._title
|
||||
|
||||
with Card(css_class="max-w-lg mx-auto") as view:
|
||||
with CardHeader():
|
||||
H3(_title)
|
||||
|
||||
with CardContent():
|
||||
Text(prompt, css_class="font-medium")
|
||||
|
||||
with CardFooter():
|
||||
with If(STATE.decided):
|
||||
Muted("Response sent.")
|
||||
with If(~STATE.decided): # noqa: SIM117
|
||||
with Column(gap=2, css_class="w-full"):
|
||||
for option in options:
|
||||
Button(
|
||||
option,
|
||||
variant=provider._variant,
|
||||
css_class="w-full justify-start",
|
||||
on_click=[
|
||||
SendMessage(
|
||||
f'"{prompt}" — I selected: {option}'
|
||||
),
|
||||
SetState("decided", True),
|
||||
],
|
||||
)
|
||||
|
||||
return PrefabApp(
|
||||
view=view,
|
||||
state={"decided": False},
|
||||
)
|
||||
56
tests/apps/test_approval.py
Normal file
56
tests/apps/test_approval.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""Tests for the Approval provider."""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.approval import Approval
|
||||
|
||||
|
||||
class TestApprovalProvider:
|
||||
async def test_request_approval_returns_structured_content(self):
|
||||
server = FastMCP("test", providers=[Approval()])
|
||||
|
||||
result = await server.call_tool(
|
||||
"request_approval",
|
||||
{"summary": "Delete 47 files"},
|
||||
)
|
||||
assert result.structured_content is not None
|
||||
|
||||
async def test_request_approval_with_details(self):
|
||||
server = FastMCP("test", providers=[Approval()])
|
||||
|
||||
result = await server.call_tool(
|
||||
"request_approval",
|
||||
{"summary": "Deploy to prod", "details": "Version 3.2.0"},
|
||||
)
|
||||
assert result.structured_content is not None
|
||||
|
||||
async def test_tool_visible_to_model(self):
|
||||
server = FastMCP("test", providers=[Approval()])
|
||||
|
||||
tools = await server.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
assert "request_approval" in tool_names
|
||||
|
||||
async def test_custom_name(self):
|
||||
server = FastMCP("test", providers=[Approval(name="Gate")])
|
||||
|
||||
tools = await server.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
assert "request_approval" in tool_names
|
||||
|
||||
async def test_custom_button_text(self):
|
||||
server = FastMCP(
|
||||
"test",
|
||||
providers=[
|
||||
Approval(
|
||||
approve_text="Ship it",
|
||||
reject_text="Nope",
|
||||
title="Deploy Gate",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = await server.call_tool(
|
||||
"request_approval",
|
||||
{"summary": "Deploy v3.2"},
|
||||
)
|
||||
assert result.structured_content is not None
|
||||
50
tests/apps/test_choice.py
Normal file
50
tests/apps/test_choice.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""Tests for the Choice provider."""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.choice import Choice
|
||||
|
||||
|
||||
class TestChoiceProvider:
|
||||
async def test_choose_returns_structured_content(self):
|
||||
server = FastMCP("test", providers=[Choice()])
|
||||
|
||||
result = await server.call_tool(
|
||||
"choose",
|
||||
{"prompt": "Pick one", "options": ["A", "B", "C"]},
|
||||
)
|
||||
assert result.structured_content is not None
|
||||
|
||||
async def test_tool_visible_to_model(self):
|
||||
server = FastMCP("test", providers=[Choice()])
|
||||
|
||||
tools = await server.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
assert "choose" in tool_names
|
||||
|
||||
async def test_custom_name(self):
|
||||
server = FastMCP("test", providers=[Choice(name="Picker")])
|
||||
|
||||
tools = await server.list_tools()
|
||||
tool_names = [t.name for t in tools]
|
||||
assert "choose" in tool_names
|
||||
|
||||
async def test_custom_title(self):
|
||||
server = FastMCP("test", providers=[Choice(title="Select Strategy")])
|
||||
|
||||
result = await server.call_tool(
|
||||
"choose",
|
||||
{"prompt": "How?", "options": ["Fast", "Slow"]},
|
||||
)
|
||||
assert result.structured_content is not None
|
||||
|
||||
async def test_many_options(self):
|
||||
server = FastMCP("test", providers=[Choice()])
|
||||
|
||||
result = await server.call_tool(
|
||||
"choose",
|
||||
{
|
||||
"prompt": "Pick a color",
|
||||
"options": ["Red", "Blue", "Green", "Yellow", "Purple"],
|
||||
},
|
||||
)
|
||||
assert result.structured_content is not None
|
||||
Loading…
Add table
Add a link
Reference in a new issue