Skip to content

Commit fcd160e

Browse files
committed
feat: Upgrade FastMCP to 3.2.0
1 parent 4b18790 commit fcd160e

File tree

8 files changed

+101
-385
lines changed

8 files changed

+101
-385
lines changed

mcp_proxy_for_aws/middleware/tool_filter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import logging
1616
from collections.abc import Awaitable, Callable
1717
from fastmcp.server.middleware import Middleware, MiddlewareContext
18-
from fastmcp.tools.tool import Tool
18+
from fastmcp.tools import Tool
1919
from typing import Sequence
2020

2121

mcp_proxy_for_aws/proxy.py

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -16,84 +16,20 @@
1616
import logging
1717
from fastmcp import Client
1818
from fastmcp.client.transports import ClientTransport
19-
from fastmcp.exceptions import NotFoundError
20-
from fastmcp.server.proxy import ClientFactoryT
21-
from fastmcp.server.proxy import FastMCPProxy as _FastMCPProxy
22-
from fastmcp.server.proxy import ProxyClient as _ProxyClient
23-
from fastmcp.server.proxy import ProxyToolManager as _ProxyToolManager
24-
from fastmcp.tools import Tool
19+
from fastmcp.server.providers.proxy import ProxyClient as _ProxyClient
2520
from mcp import McpError
2621
from mcp.types import InitializeRequest, JSONRPCError, JSONRPCMessage
27-
from typing import Any
2822
from typing_extensions import override
2923

3024

3125
logger = logging.getLogger(__name__)
3226

3327

34-
class AWSProxyToolManager(_ProxyToolManager):
35-
"""Customized proxy tool manager that better suites our needs."""
36-
37-
def __init__(self, client_factory: ClientFactoryT, **kwargs: Any):
38-
"""Initialize a proxy tool manager.
39-
40-
Cached tools are set to None.
41-
"""
42-
super().__init__(client_factory, **kwargs)
43-
self._cached_tools: dict[str, Tool] | None = None
44-
45-
@override
46-
async def get_tool(self, key: str) -> Tool:
47-
"""Return the tool from cached tools.
48-
49-
This method is invoked when the client tries to call a tool.
50-
51-
tool = self.get_tool(key)
52-
tool.invoke(...)
53-
54-
The parent class implementation always make a mcp call to list the tools.
55-
Since the client already knows the name of the tools, list_tool is not necessary.
56-
We are wasting a network call just to get the tools which were already listed.
57-
58-
In case the server supports notifications/tools/listChanged, the `get_tools` method
59-
will be called explicity , hence, we are not missing the change to the tool list.
60-
"""
61-
if self._cached_tools is None:
62-
logger.debug('cached_tools not found, calling get_tools')
63-
self._cached_tools = await self.get_tools()
64-
if key in self._cached_tools:
65-
return self._cached_tools[key]
66-
raise NotFoundError(f'Tool {key!r} not found')
67-
68-
@override
69-
async def get_tools(self) -> dict[str, Tool]:
70-
"""Return list tools."""
71-
self._cached_tools = await super(AWSProxyToolManager, self).get_tools()
72-
return self._cached_tools
73-
74-
75-
class AWSMCPProxy(_FastMCPProxy):
76-
"""Customized MCP Proxy to better suite our needs."""
77-
78-
def __init__(
79-
self,
80-
*,
81-
client_factory: ClientFactoryT,
82-
**kwargs,
83-
):
84-
"""Initialize a client."""
85-
super().__init__(client_factory=client_factory, **kwargs)
86-
self._tool_manager = AWSProxyToolManager(
87-
client_factory=self.client_factory,
88-
transformations=self._tool_manager.transformations,
89-
)
90-
91-
9228
class AWSMCPProxyClient(_ProxyClient):
9329
"""Proxy client that handles HTTP errors when connection fails."""
9430

9531
def __init__(self, transport: ClientTransport, max_connect_retry=3, **kwargs):
96-
"""Constructor of AutoRefreshProxyCilent."""
32+
"""Constructor of AWSMCPProxyClient."""
9733
super().__init__(transport, **kwargs)
9834
self._max_connect_retry = max_connect_retry
9935

mcp_proxy_for_aws/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@
2727
import logging
2828
from fastmcp.server.middleware.error_handling import RetryMiddleware
2929
from fastmcp.server.middleware.logging import LoggingMiddleware
30+
from fastmcp.server.providers.proxy import FastMCPProxy
3031
from fastmcp.server.server import FastMCP
3132
from mcp_proxy_for_aws import __version__
3233
from mcp_proxy_for_aws.cli import parse_args
3334
from mcp_proxy_for_aws.logging_config import configure_logging
3435
from mcp_proxy_for_aws.middleware.initialize_middleware import InitializeMiddleware
3536
from mcp_proxy_for_aws.middleware.tool_filter import ToolFilteringMiddleware
36-
from mcp_proxy_for_aws.proxy import AWSMCPProxy, AWSMCPProxyClientFactory
37+
from mcp_proxy_for_aws.proxy import AWSMCPProxyClientFactory
3738
from mcp_proxy_for_aws.utils import (
3839
create_transport_with_sigv4,
3940
determine_aws_region,
@@ -87,7 +88,7 @@ async def run_proxy(args) -> None:
8788
client_factory = AWSMCPProxyClientFactory(transport)
8889

8990
try:
90-
proxy = AWSMCPProxy(
91+
proxy = FastMCPProxy(
9192
client_factory=client_factory,
9293
name='MCP Proxy for AWS',
9394
version=__version__,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ description = "MCP Proxy for AWS"
1616
readme = "README.md"
1717
requires-python = ">=3.10,<3.15"
1818
dependencies = [
19-
"fastmcp~=2.14.1",
19+
"fastmcp (>=3.2.0,<4)",
2020
"boto3>=1.41.0",
2121
"botocore[crt]>=1.41.0",
2222
]

tests/integ/mcp/simple_mcp_server/mcp_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@ def greet(name: str):
3838

3939

4040
@mcp.tool
41-
def add_tool_multiply(ctx: Context):
41+
async def add_tool_multiply(ctx: Context):
4242
"""MCP Tool used for testing dynamic tool behavior through the proxy."""
43-
if not ctx.get_state('multiply_registered'):
43+
if not await ctx.get_state('multiply_registered'):
4444

4545
@mcp.tool
4646
def multiply(x: int, y: int):
4747
"""Multiply two numbers."""
4848
return x * y
4949

50-
ctx.set_state('multiply_registered', True)
50+
await ctx.set_state('multiply_registered', True)
5151
return 'Tool "multiply" added successfully'
5252
return 'Tool "multiply" already exists'
5353

tests/unit/test_proxy.py

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,75 +17,15 @@
1717
import httpx
1818
import pytest
1919
from fastmcp.client.transports import ClientTransport
20-
from fastmcp.exceptions import NotFoundError
21-
from fastmcp.tools import Tool
2220
from mcp import McpError
2321
from mcp.types import ErrorData, InitializeRequest, JSONRPCError
2422
from mcp_proxy_for_aws.proxy import (
25-
AWSMCPProxy,
2623
AWSMCPProxyClient,
2724
AWSMCPProxyClientFactory,
28-
AWSProxyToolManager,
2925
)
3026
from unittest.mock import AsyncMock, Mock, patch
3127

3228

33-
@pytest.mark.asyncio
34-
async def test_tool_manager_get_tool_with_cache():
35-
"""Test get_tool returns from cache when available."""
36-
mock_factory = Mock()
37-
manager = AWSProxyToolManager(mock_factory)
38-
mock_tool = Mock(spec=Tool)
39-
manager._cached_tools = {'test_tool': mock_tool}
40-
41-
result = await manager.get_tool('test_tool')
42-
assert result == mock_tool
43-
44-
45-
@pytest.mark.asyncio
46-
async def test_tool_manager_get_tool_without_cache():
47-
"""Test get_tool fetches tools when cache is empty."""
48-
mock_factory = Mock()
49-
manager = AWSProxyToolManager(mock_factory)
50-
mock_tool = Mock(spec=Tool)
51-
52-
with patch.object(manager, 'get_tools', return_value={'test_tool': mock_tool}):
53-
result = await manager.get_tool('test_tool')
54-
assert result == mock_tool
55-
assert manager._cached_tools == {'test_tool': mock_tool}
56-
57-
58-
@pytest.mark.asyncio
59-
async def test_tool_manager_get_tool_not_found():
60-
"""Test get_tool raises NotFoundError when tool doesn't exist."""
61-
mock_factory = Mock()
62-
manager = AWSProxyToolManager(mock_factory)
63-
manager._cached_tools = {}
64-
65-
with pytest.raises(NotFoundError, match="Tool 'missing_tool' not found"):
66-
await manager.get_tool('missing_tool')
67-
68-
69-
@pytest.mark.asyncio
70-
async def test_tool_manager_get_tools_updates_cache():
71-
"""Test get_tools updates the cache."""
72-
mock_factory = Mock()
73-
manager = AWSProxyToolManager(mock_factory)
74-
mock_tools = {'tool1': Mock(spec=Tool), 'tool2': Mock(spec=Tool)}
75-
76-
with patch('mcp_proxy_for_aws.proxy._ProxyToolManager.get_tools', return_value=mock_tools):
77-
result = await manager.get_tools()
78-
assert result == mock_tools
79-
assert manager._cached_tools == mock_tools
80-
81-
82-
def test_proxy_initialization():
83-
"""Test AWSMCPProxy initializes with custom tool manager."""
84-
mock_factory = Mock()
85-
proxy = AWSMCPProxy(client_factory=mock_factory, name='test')
86-
assert isinstance(proxy._tool_manager, AWSProxyToolManager)
87-
88-
8929
@pytest.mark.asyncio
9030
async def test_proxy_client_connect_success():
9131
"""Test successful connection."""

tests/unit/test_server.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class TestServer:
3232

3333
@patch('mcp_proxy_for_aws.server.AWSMCPProxyClientFactory')
3434
@patch('mcp_proxy_for_aws.server.create_transport_with_sigv4')
35-
@patch('mcp_proxy_for_aws.server.AWSMCPProxy')
35+
@patch('mcp_proxy_for_aws.server.FastMCPProxy')
3636
@patch('mcp_proxy_for_aws.server.determine_aws_region')
3737
@patch('mcp_proxy_for_aws.server.determine_service_name')
3838
@patch('mcp_proxy_for_aws.server.add_tool_filtering_middleware')
@@ -43,7 +43,7 @@ async def test_setup_mcp_mode(
4343
mock_add_filtering,
4444
mock_determine_service,
4545
mock_determine_region,
46-
mock_aws_proxy,
46+
mock_fastmcp_proxy,
4747
mock_create_transport,
4848
mock_client_factory_class,
4949
):
@@ -79,7 +79,7 @@ async def test_setup_mcp_mode(
7979
mock_proxy = Mock()
8080
mock_proxy.run_async = AsyncMock()
8181
mock_proxy.add_middleware = Mock()
82-
mock_aws_proxy.return_value = mock_proxy
82+
mock_fastmcp_proxy.return_value = mock_proxy
8383

8484
# Act
8585
await run_proxy(mock_args)
@@ -97,7 +97,7 @@ async def test_setup_mcp_mode(
9797
# call_args[0][4] is the Timeout object
9898
assert call_args[0][5] is None # profile
9999
mock_client_factory_class.assert_called_once_with(mock_transport)
100-
mock_aws_proxy.assert_called_once()
100+
mock_fastmcp_proxy.assert_called_once()
101101
mock_add_filtering.assert_called_once_with(mock_proxy, True)
102102
mock_add_retry.assert_called_once_with(mock_proxy, 1)
103103
mock_proxy.run_async.assert_called_once_with(
@@ -106,7 +106,7 @@ async def test_setup_mcp_mode(
106106

107107
@patch('mcp_proxy_for_aws.server.AWSMCPProxyClientFactory')
108108
@patch('mcp_proxy_for_aws.server.create_transport_with_sigv4')
109-
@patch('mcp_proxy_for_aws.server.AWSMCPProxy')
109+
@patch('mcp_proxy_for_aws.server.FastMCPProxy')
110110
@patch('mcp_proxy_for_aws.server.determine_aws_region')
111111
@patch('mcp_proxy_for_aws.server.determine_service_name')
112112
@patch('mcp_proxy_for_aws.server.add_tool_filtering_middleware')
@@ -115,7 +115,7 @@ async def test_setup_mcp_mode_no_retries(
115115
mock_add_filtering,
116116
mock_determine_service,
117117
mock_determine_region,
118-
mock_aws_proxy,
118+
mock_fastmcp_proxy,
119119
mock_create_transport,
120120
mock_client_factory_class,
121121
):
@@ -151,7 +151,7 @@ async def test_setup_mcp_mode_no_retries(
151151
mock_proxy = Mock()
152152
mock_proxy.run_async = AsyncMock()
153153
mock_proxy.add_middleware = Mock()
154-
mock_aws_proxy.return_value = mock_proxy
154+
mock_fastmcp_proxy.return_value = mock_proxy
155155

156156
# Act
157157
await run_proxy(mock_args)
@@ -172,15 +172,15 @@ async def test_setup_mcp_mode_no_retries(
172172
# call_args[0][4] is the Timeout object
173173
assert call_args[0][5] == 'test-profile' # profile
174174
mock_client_factory_class.assert_called_once_with(mock_transport)
175-
mock_aws_proxy.assert_called_once()
175+
mock_fastmcp_proxy.assert_called_once()
176176
mock_add_filtering.assert_called_once_with(mock_proxy, False)
177177
mock_proxy.run_async.assert_called_once_with(
178178
transport='stdio', show_banner=False, log_level='INFO'
179179
)
180180

181181
@patch('mcp_proxy_for_aws.server.AWSMCPProxyClientFactory')
182182
@patch('mcp_proxy_for_aws.server.create_transport_with_sigv4')
183-
@patch('mcp_proxy_for_aws.server.AWSMCPProxy')
183+
@patch('mcp_proxy_for_aws.server.FastMCPProxy')
184184
@patch('mcp_proxy_for_aws.server.determine_aws_region')
185185
@patch('mcp_proxy_for_aws.server.determine_service_name')
186186
@patch('mcp_proxy_for_aws.server.add_tool_filtering_middleware')
@@ -189,7 +189,7 @@ async def test_setup_mcp_mode_no_metadata_injects_aws_region(
189189
mock_add_filtering,
190190
mock_determine_service,
191191
mock_determine_region,
192-
mock_aws_proxy,
192+
mock_fastmcp_proxy,
193193
mock_create_transport,
194194
mock_client_factory_class,
195195
):
@@ -222,7 +222,7 @@ async def test_setup_mcp_mode_no_metadata_injects_aws_region(
222222
mock_proxy = Mock()
223223
mock_proxy.run_async = AsyncMock()
224224
mock_proxy.add_middleware = Mock()
225-
mock_aws_proxy.return_value = mock_proxy
225+
mock_fastmcp_proxy.return_value = mock_proxy
226226

227227
# Act
228228
await run_proxy(mock_args)
@@ -235,7 +235,7 @@ async def test_setup_mcp_mode_no_metadata_injects_aws_region(
235235

236236
@patch('mcp_proxy_for_aws.server.AWSMCPProxyClientFactory')
237237
@patch('mcp_proxy_for_aws.server.create_transport_with_sigv4')
238-
@patch('mcp_proxy_for_aws.server.AWSMCPProxy')
238+
@patch('mcp_proxy_for_aws.server.FastMCPProxy')
239239
@patch('mcp_proxy_for_aws.server.determine_aws_region')
240240
@patch('mcp_proxy_for_aws.server.determine_service_name')
241241
@patch('mcp_proxy_for_aws.server.add_tool_filtering_middleware')
@@ -244,7 +244,7 @@ async def test_setup_mcp_mode_metadata_without_aws_region_injects_it(
244244
mock_add_filtering,
245245
mock_determine_service,
246246
mock_determine_region,
247-
mock_aws_proxy,
247+
mock_fastmcp_proxy,
248248
mock_create_transport,
249249
mock_client_factory_class,
250250
):
@@ -277,7 +277,7 @@ async def test_setup_mcp_mode_metadata_without_aws_region_injects_it(
277277
mock_proxy = Mock()
278278
mock_proxy.run_async = AsyncMock()
279279
mock_proxy.add_middleware = Mock()
280-
mock_aws_proxy.return_value = mock_proxy
280+
mock_fastmcp_proxy.return_value = mock_proxy
281281

282282
# Act
283283
await run_proxy(mock_args)

0 commit comments

Comments
 (0)