File size: 10,698 Bytes
3d30e82
 
 
15f96d2
7826473
 
81e8514
8cc3f59
81e8514
8cc3f59
81e8514
 
 
 
 
7826473
3acddf3
2c75133
3becd4d
48f7143
7826473
 
fa364d7
7826473
8dc263e
7826473
3d30e82
 
 
7826473
 
 
 
3d30e82
7826473
 
 
 
3d30e82
7826473
 
 
 
 
 
 
 
3d30e82
 
1534a61
8dc263e
c4ac623
 
7826473
7e50621
 
 
c4ac623
7826473
2c75133
c4ac623
7826473
 
 
cba81f4
 
 
3d30e82
7826473
 
 
 
 
 
3d30e82
 
7826473
 
 
 
 
 
 
 
1534a61
7826473
 
 
 
 
c4ac623
 
 
 
7826473
2c75133
7826473
 
 
3d30e82
7826473
 
 
 
 
3d30e82
 
7826473
 
 
 
 
 
 
 
3d30e82
 
 
 
1534a61
3d30e82
1ac845d
15f96d2
 
 
 
 
7826473
15f96d2
7826473
c4ac623
 
 
 
7826473
2c75133
7826473
 
 
15f96d2
7826473
 
c4ac623
7826473
 
 
 
 
873479e
 
3d30e82
7826473
 
 
 
3d30e82
7826473
 
 
 
 
 
 
1534a61
7826473
 
3becd4d
7826473
 
 
3d30e82
7826473
81e8514
 
db65850
 
 
 
 
81e8514
 
d32f68a
fd9bcfe
d32f68a
 
 
dccb245
 
d32f68a
ed6ec93
 
 
db65850
81e8514
db65850
81e8514
db65850
 
 
 
 
 
 
81e8514
 
d32f68a
 
 
 
 
dccb245
 
d32f68a
ed6ec93
db65850
ed6ec93
 
 
db65850
81e8514
db65850
81e8514
e2f3152
 
 
db65850
 
e2f3152
 
 
db65850
81e8514
 
d32f68a
 
 
 
 
dccb245
 
d32f68a
ed6ec93
 
 
 
 
 
81e8514
db65850
81e8514
e2f3152
 
 
 
 
81e8514
 
d32f68a
 
 
 
 
dccb245
 
d32f68a
ed6ec93
 
 
 
 
db65850
81e8514
8dc263e
81e8514
a277f25
81e8514
 
 
 
 
 
a277f25
81e8514
a277f25
81e8514
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a277f25
81e8514
 
 
a277f25
81e8514
 
 
 
3becd4d
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
from __future__ import annotations

from typing import TYPE_CHECKING, Any, cast
from urllib.parse import quote

import mcp.types
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.shared.exceptions import McpError
from mcp.types import (
    METHOD_NOT_FOUND,
    BlobResourceContents,
    GetPromptResult,
    TextResourceContents,
)
from pydantic.networks import AnyUrl

from fastmcp.client import Client
from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
from fastmcp.prompts import Prompt, PromptMessage
from fastmcp.resources import Resource, ResourceTemplate
from fastmcp.server.context import Context
from fastmcp.server.server import FastMCP
from fastmcp.tools.tool import Tool
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import MCPContent

if TYPE_CHECKING:
    from fastmcp.server import Context

logger = get_logger(__name__)


class ProxyTool(Tool):
    def __init__(self, client: Client, **kwargs):
        super().__init__(**kwargs)
        self._client = client

    @classmethod
    async def from_client(cls, client: Client, tool: mcp.types.Tool) -> ProxyTool:
        return cls(
            client=client,
            name=tool.name,
            description=tool.description,
            parameters=tool.inputSchema,
        )

    async def run(
        self,
        arguments: dict[str, Any],
        context: Context | None = None,
    ) -> list[MCPContent]:
        # the client context manager will swallow any exceptions inside a TaskGroup
        # so we return the raw result and raise an exception ourselves
        async with self._client:
            result = await self._client.call_tool_mcp(
                name=self.name,
                arguments=arguments,
            )
        if result.isError:
            raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
        return result.content


class ProxyResource(Resource):
    _client: Client
    _value: str | bytes | None = None

    def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
        super().__init__(**kwargs)
        self._client = client
        self._value = _value

    @classmethod
    async def from_client(
        cls, client: Client, resource: mcp.types.Resource
    ) -> ProxyResource:
        return cls(
            client=client,
            uri=resource.uri,
            name=resource.name,
            description=resource.description,
            mime_type=resource.mimeType,
        )

    async def read(self) -> str | bytes:
        if self._value is not None:
            return self._value

        async with self._client:
            result = await self._client.read_resource(self.uri)
        if isinstance(result[0], TextResourceContents):
            return result[0].text
        elif isinstance(result[0], BlobResourceContents):
            return result[0].blob
        else:
            raise ResourceError(f"Unsupported content type: {type(result[0])}")


class ProxyTemplate(ResourceTemplate):
    def __init__(self, client: Client, **kwargs):
        super().__init__(**kwargs)
        self._client = client

    @classmethod
    async def from_client(
        cls, client: Client, template: mcp.types.ResourceTemplate
    ) -> ProxyTemplate:
        return cls(
            client=client,
            uri_template=template.uriTemplate,
            name=template.name,
            description=template.description,
            parameters={},
        )

    async def create_resource(
        self,
        uri: str,
        params: dict[str, Any],
        context: Context | None = None,
    ) -> ProxyResource:
        # don't use the provided uri, because it may not be the same as the
        # uri_template on the remote server.
        # quote params to ensure they are valid for the uri_template
        parameterized_uri = self.uri_template.format(
            **{k: quote(v, safe="") for k, v in params.items()}
        )
        async with self._client:
            result = await self._client.read_resource(parameterized_uri)

        if isinstance(result[0], TextResourceContents):
            value = result[0].text
        elif isinstance(result[0], BlobResourceContents):
            value = result[0].blob
        else:
            raise ResourceError(f"Unsupported content type: {type(result[0])}")

        return ProxyResource(
            client=self._client,
            uri=parameterized_uri,
            name=self.name,
            description=self.description,
            mime_type=result[0].mimeType,
            _value=value,
        )


class ProxyPrompt(Prompt):
    _client: Client

    def __init__(self, client: Client, **kwargs):
        super().__init__(**kwargs)
        self._client = client

    @classmethod
    async def from_client(cls, client: Client, prompt: mcp.types.Prompt) -> ProxyPrompt:
        return cls(
            client=client,
            name=prompt.name,
            description=prompt.description,
            arguments=[a.model_dump() for a in prompt.arguments or []],
        )

    async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
        async with self._client:
            result = await self._client.get_prompt(self.name, arguments)
        return result.messages


class FastMCPProxy(FastMCP):
    def __init__(self, client: Client, **kwargs):
        super().__init__(**kwargs)
        self.client = client

    async def _list_tools(self, apply_middleware: bool = True) -> list[Tool]:
        tools = {
            tool.name: tool
            for tool in await super()._list_tools(apply_middleware=apply_middleware)
        }

        async with self.client:
            try:
                client_tools = await self.client.list_tools()
            except McpError as e:
                if e.error.code == METHOD_NOT_FOUND:
                    client_tools = []
                else:
                    raise e
            for tool in client_tools:
                # don't overwrite tools defined in the server
                if tool.name not in tools:
                    tool_proxy = await ProxyTool.from_client(self.client, tool)
                    tools[tool_proxy.key] = tool_proxy

        return list(tools.values())

    async def _list_resources(self, apply_middleware: bool = True) -> list[Resource]:
        resources = {
            resource.uri: resource
            for resource in await super()._list_resources(
                apply_middleware=apply_middleware
            )
        }

        async with self.client:
            try:
                client_resources = await self.client.list_resources()
            except McpError as e:
                if e.error.code == METHOD_NOT_FOUND:
                    client_resources = []
                else:
                    raise e
            for resource in client_resources:
                # don't overwrite resources defined in the server
                if resource.uri not in resources:
                    resource_proxy = await ProxyResource.from_client(
                        self.client, resource
                    )
                    resources[resource_proxy.uri] = resource_proxy

        return list(resources.values())

    async def _list_resource_templates(
        self, apply_middleware: bool = True
    ) -> list[ResourceTemplate]:
        templates = {
            template.uri_template: template
            for template in await super()._list_resource_templates(
                apply_middleware=apply_middleware
            )
        }

        async with self.client:
            try:
                client_templates = await self.client.list_resource_templates()
            except McpError as e:
                if e.error.code == METHOD_NOT_FOUND:
                    client_templates = []
                else:
                    raise e
            for template in client_templates:
                # don't overwrite templates defined in the server
                if template.uriTemplate not in templates:
                    template_proxy = await ProxyTemplate.from_client(
                        self.client, template
                    )
                    templates[template_proxy.uri_template] = template_proxy

        return list(templates.values())

    async def _list_prompts(self, apply_middleware: bool = True) -> list[Prompt]:
        prompts = {
            prompt.name: prompt
            for prompt in await super()._list_prompts(apply_middleware=apply_middleware)
        }

        async with self.client:
            try:
                client_prompts = await self.client.list_prompts()
            except McpError as e:
                if e.error.code == METHOD_NOT_FOUND:
                    client_prompts = []
                else:
                    raise e
            for prompt in client_prompts:
                # don't overwrite prompts defined in the server
                if prompt.name not in prompts:
                    prompt_proxy = await ProxyPrompt.from_client(self.client, prompt)
                    prompts[prompt_proxy.name] = prompt_proxy

        return list(prompts.values())

    async def _call_tool(self, key: str, arguments: dict[str, Any]) -> list[MCPContent]:
        try:
            result = await super()._call_tool(key, arguments)
            return result
        except NotFoundError:
            async with self.client:
                result = await self.client.call_tool(key, arguments)
            return result

    async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
        try:
            result = await super()._read_resource(uri)
            return result
        except NotFoundError:
            async with self.client:
                resource = await self.client.read_resource(uri)
                if isinstance(resource[0], TextResourceContents):
                    content = resource[0].text
                elif isinstance(resource[0], BlobResourceContents):
                    content = resource[0].blob
                else:
                    raise ValueError(f"Unsupported content type: {type(resource[0])}")

            return [
                ReadResourceContents(content=content, mime_type=resource[0].mimeType)
            ]

    async def _get_prompt(
        self, name: str, arguments: dict[str, Any] | None = None
    ) -> GetPromptResult:
        try:
            result = await super()._get_prompt(name, arguments)
            return result
        except NotFoundError:
            async with self.client:
                result = await self.client.get_prompt(name, arguments)
            return result