File size: 7,493 Bytes
09d8104
 
 
8dc263e
09d8104
ecb6207
 
09d8104
 
 
 
a853a78
09d8104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85e9cfd
 
 
09d8104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a9d761
 
09d8104
 
 
 
 
 
 
 
 
 
 
 
3a9d761
 
7021309
 
09d8104
 
 
 
 
 
 
a853a78
 
 
09d8104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
from typing import Any

import pytest
from mcp.types import TextContent

from fastmcp import FastMCP
from fastmcp.contrib.bulk_tool_caller.bulk_tool_caller import (
    BulkToolCaller,
    CallToolRequest,
    CallToolRequestResult,
)
from fastmcp.tools.tool import Tool


class ToolException(Exception):
    """Custom exception for tool errors."""

    pass


async def error_tool(arg1: str) -> dict[str, Any]:
    """A tool that raises an error for testing purposes."""
    raise ToolException(f"Error in tool with arg1: {arg1}")


def error_tool_result_factory(arg1: str) -> CallToolRequestResult:
    """Generates the expected error result for error_tool."""
    # Mimic the error message format generated by BulkToolCaller when catching ToolException
    formatted_error_text = (
        "Error calling tool 'error_tool': Error in tool with arg1: " + arg1
    )
    return CallToolRequestResult(
        isError=True,
        content=[TextContent(text=formatted_error_text, type="text")],
        tool="error_tool",
        arguments={"arg1": arg1},
    )


async def echo_tool(arg1: str) -> str:
    """A simple tool that echoes arguments or raises an error."""
    return arg1


def echo_tool_result_factory(arg1: str) -> CallToolRequestResult:
    """A tool that returns a result based on the input arguments."""
    return CallToolRequestResult(
        isError=False,
        content=[TextContent(text=f"{arg1}", type="text")],
        tool="echo_tool",
        arguments={"arg1": arg1},
    )


async def no_return_tool(arg1: str) -> None:
    """A simple tool that echoes arguments or raises an error."""


def no_return_tool_result_factory(arg1: str) -> CallToolRequestResult:
    """A tool that returns a result based on the input arguments."""
    return CallToolRequestResult(
        isError=False,
        content=[],
        tool="no_return_tool",
        arguments={"arg1": arg1},
    )


@pytest.fixture(scope="module")
def live_server_with_tool() -> FastMCP:
    """Fixture to create a FastMCP server instance with the echo_tool registered."""
    server = FastMCP()
    server.add_tool(Tool.from_function(echo_tool))
    server.add_tool(Tool.from_function(error_tool))
    server.add_tool(Tool.from_function(no_return_tool))
    return server


@pytest.fixture
def bulk_caller_live(live_server_with_tool: FastMCP) -> BulkToolCaller:
    """Fixture to create a BulkToolCaller instance connected to the live server."""
    bulk_tool_caller = BulkToolCaller()
    bulk_tool_caller.register_tools(live_server_with_tool)
    return bulk_tool_caller


ECHO_TOOL_NAME = "echo_tool"
ERROR_TOOL_NAME = "error_tool"
NO_RETURN_TOOL_NAME = "no_return_tool"


async def test_call_tool_bulk_single_success(bulk_caller_live: BulkToolCaller):
    """Test single successful call via call_tool_bulk using echo_tool."""
    tool_arguments = [{"arg1": "value1"}]
    expected_result = echo_tool_result_factory(**tool_arguments[0])

    results = await bulk_caller_live.call_tool_bulk(ECHO_TOOL_NAME, tool_arguments)

    assert len(results) == 1
    result = results[0]
    assert result == expected_result


async def test_call_tool_bulk_multiple_success(bulk_caller_live: BulkToolCaller):
    """Test multiple successful calls via call_tool_bulk using echo_tool."""
    tool_arguments = [{"arg1": "value1"}, {"arg1": "value2"}]
    expected_results = [echo_tool_result_factory(**args) for args in tool_arguments]

    results = await bulk_caller_live.call_tool_bulk(ECHO_TOOL_NAME, tool_arguments)

    assert len(results) == 2
    assert results == expected_results


async def test_call_tool_bulk_error_stops(bulk_caller_live: BulkToolCaller):
    """Test call_tool_bulk stops on first error using error_tool."""
    tool_arguments = [{"arg1": "error_value"}, {"arg1": "value2"}]
    expected_result = error_tool_result_factory(**tool_arguments[0])

    results = await bulk_caller_live.call_tool_bulk(
        ERROR_TOOL_NAME, tool_arguments, continue_on_error=False
    )

    assert len(results) == 1
    result = results[0]
    assert result == expected_result


async def test_call_tool_bulk_error_continues(bulk_caller_live: BulkToolCaller):
    """Test call_tool_bulk continues on error using error_tool and echo_tool."""
    tool_arguments = [{"arg1": "error_value"}, {"arg1": "success_value"}]
    expected_error_result = error_tool_result_factory(**tool_arguments[0])
    expected_success_result = echo_tool_result_factory(**tool_arguments[1])

    tool_calls = [
        CallToolRequest(tool=ERROR_TOOL_NAME, arguments=tool_arguments[0]),
        CallToolRequest(tool=ECHO_TOOL_NAME, arguments=tool_arguments[1]),
    ]

    results = await bulk_caller_live.call_tools_bulk(tool_calls, continue_on_error=True)

    assert len(results) == 2

    error_result = results[0]
    assert error_result == expected_error_result

    success_result = results[1]
    assert success_result == expected_success_result


async def test_call_tools_bulk_single_success(bulk_caller_live: BulkToolCaller):
    """Test single successful call via call_tools_bulk using echo_tool."""
    tool_calls = [CallToolRequest(tool=ECHO_TOOL_NAME, arguments={"arg1": "value1"})]
    expected_result = echo_tool_result_factory(**tool_calls[0].arguments)

    results = await bulk_caller_live.call_tools_bulk(tool_calls)

    assert len(results) == 1
    result = results[0]
    assert result == expected_result


async def test_call_tools_bulk_multiple_success(bulk_caller_live: BulkToolCaller):
    """Test multiple successful calls via call_tools_bulk with different tools."""
    tool_calls = [
        CallToolRequest(tool=ECHO_TOOL_NAME, arguments={"arg1": "echo_value"}),
        CallToolRequest(
            tool=NO_RETURN_TOOL_NAME, arguments={"arg1": "no_return_value"}
        ),
    ]
    expected_results = [
        echo_tool_result_factory(**tool_calls[0].arguments),
        no_return_tool_result_factory(**tool_calls[1].arguments),
    ]

    results = await bulk_caller_live.call_tools_bulk(tool_calls)

    assert len(results) == 2
    assert results == expected_results


async def test_call_tools_bulk_error_stops(bulk_caller_live: BulkToolCaller):
    """Test call_tools_bulk stops on first error using error_tool."""
    tool_calls = [
        CallToolRequest(tool=ERROR_TOOL_NAME, arguments={"arg1": "error_value"}),
        CallToolRequest(tool=ECHO_TOOL_NAME, arguments={"arg1": "skipped_value"}),
    ]
    expected_result = error_tool_result_factory(**tool_calls[0].arguments)

    results = await bulk_caller_live.call_tools_bulk(
        tool_calls, continue_on_error=False
    )

    assert len(results) == 1
    result = results[0]
    assert result == expected_result


async def test_call_tools_bulk_error_continues(bulk_caller_live: BulkToolCaller):
    """Test call_tools_bulk continues on error using error_tool and echo_tool."""
    tool_calls = [
        CallToolRequest(tool=ERROR_TOOL_NAME, arguments={"arg1": "error_value"}),
        CallToolRequest(tool=ECHO_TOOL_NAME, arguments={"arg1": "success_value"}),
    ]
    expected_error_result = error_tool_result_factory(**tool_calls[0].arguments)
    expected_success_result = echo_tool_result_factory(**tool_calls[1].arguments)

    results = await bulk_caller_live.call_tools_bulk(tool_calls, continue_on_error=True)

    assert len(results) == 2

    error_result = results[0]
    assert error_result == expected_error_result

    success_result = results[1]
    assert success_result == expected_success_result