[FastMCP设计、原理与应用-11]Transform——数据炼金术,跨协议边界的无缝适配与格式转换

Transform核心作用是在MCP服务器处理请求的生命周期中,允许我们拦截、修改或过滤工具、资源(包括静态资源和动态资源模板)和提示词(模板)的读取。

python 复制代码
class Transform:
    async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
    async def get_tool(
        self, name: str, call_next: GetToolNext, *, version: VersionSpec | None = None
    ) -> Tool | None

    async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
    async def get_resource(
        self,
        uri: str,
        call_next: GetResourceNext,
        *,
        version: VersionSpec | None = None,
    ) -> Resource | None

    async def list_resource_templates(
        self, templates: Sequence[ResourceTemplate]
    ) -> Sequence[ResourceTemplate]
    async def get_resource_template(
        self,
        uri: str,
        call_next: GetResourceTemplateNext,
        *,
        version: VersionSpec | None = None,
    ) -> ResourceTemplate | None

    async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
    async def get_prompt(
        self, name: str, call_next: GetPromptNext, *, version: VersionSpec | None = None
    ) -> Prompt | None

这个类主要处理两类行为:

  • 读取组件列表(list_toolslist_resourceslist_resource_templates和list_prompts):Transform链将Provider提供组件列表做相应的转换(比如过滤、添加或者修改);
  • 获取指定组件(get_toolget_resourceget_resource_templateget_prompt):提供call_next参数将请求交付给后续流程进行处理,在此之前和之后注入前置和后置操作;

1. Namespace

接下来我们介绍几个系统预定义的Transform类型。Namespace这个Transform旨在解决命名冲突而指定了一个命名空间。由于组件来自与不同的Provider,它们之间难免产生命名冲突,这个问题可以通过为某个Provider下的组件指定一个命名空间来解决。

下面这个程序沿用了上面定义的ResourceProvider。作为MCP服务器的FastMCP由两个ResourceProvider构建而成,但是它们提供的资源具有相同的URI(file://foobar)进而产生冲突。为此我们为它们注册了一个Namespace,并将它们的命名空间设置为provider1provider2,那么两个资源的URI将变成file://provider1/foobar和file://provider2/foobar。

python 复制代码
from fastmcp import FastMCP
from pydantic.networks import AnyUrl
from fastmcp.resources import Resource,TextResource
from fastmcp.utilities.versions import VersionSpec
from fastmcp.server.providers import Provider
from fastmcp.server.transforms import Namespace
from fastmcp.client import Client
    
class ResourceProvider(Provider):
    def __init__(self, name: str, resources: list[Resource]):
        super().__init__()
        self.name = name
        self._resources = {str(resource.uri): resource for resource in resources}
    async def _list_resources(self) -> list[Resource]:
        return list(self._resources.values())
    async def _get_resource(
        self, uri: str, version: VersionSpec | None = None
    ) -> Resource | None:
        return self._resources.get(uri)
    
async def main():
    provider1 = ResourceProvider("Provider1", [TextResource(uri=AnyUrl.build(scheme="file", host="foobar"), text="this is resource from provider 1")])
    provider2 = ResourceProvider("Provider2", [TextResource(uri=AnyUrl.build(scheme="file", host="foobar"), text="this is resource from provider 2")])

    provider1.add_transform(Namespace("provider1"))
    provider2.add_transform(Namespace("provider2"))

    fastmcp = FastMCP("Server",providers=[provider1, provider2])
    async with Client(fastmcp) as client:
        resources = await client.read_resource("file://provider1/foobar/")
        assert resources[0].text == "this is resource from provider 1" # type: ignore

        resources = await client.read_resource("file://provider2/foobar/")
        assert resources[0].text == "this is resource from provider 2" # type: ignore

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

当我们创建一个Namespace对象的时候,只需要指定作为命名空间前缀的prefix就可以了

python 复制代码
class Namespace(Transform):
    def __init__(self, prefix: str) -> None

Namespace的实现原理很简单:

  • 重写list_方法,为名称或者URI添加命名空间前缀。对于工具和提示词,原来的名称从{name}变成{namespace}_{name};对于资源(或者资源模板),原来的URI从{protocol}://{path} 变成 {protocol}://{namespace}/{path};
  • 重写get_方法,剔除命名空间前缀,还原成原来的名称或者URI;

2. Visibility

Visibility这个Transform利用定义的规则控制组件的可见性。比如在如下这个例子中,我们注册了三个工具,其中第一个工具foo上面利用@mcp.tool装饰器附上了一个obsolete标签。我们通过调用FastMCP对象的disable方法添加了Visibility。指定的参数tags={"obsolete"}表示禁用(不可见)具有obsolete标签的所有组件。

python 复制代码
from fastmcp import FastMCP
from fastmcp.client import Client

mcp = FastMCP("Server").disable(tags={"obsolete"})
@mcp.tool(tags={"obsolete"})
def foo():
    ...

@mcp.tool()
def bar():
    ...

@mcp.tool
def baz():
    ...    

async def main():
    async with Client(mcp) as client:
        tools = await client.list_tools()
        tool_names = set(tool.name for tool in tools)
        assert tool_names == {"bar", "baz"}       
            
if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Visibility不仅仅可以利用组件的标签来控制可见性,可见性规则可以根据名称、Key、版本、组件类型来制定,这可以从其构造函数的参数定义看出来。参数enabled表示制定的规则是控制可见还是不可见,如果将match_all设置为True,表示忽略前面所有的规则(即规则与所有组件匹配)

python 复制代码
class Visibility(Transform):
    def __init__(
        self,
        enabled: bool,
        *,
        names: set[str] | None = None,
        keys: set[str] | None = None,
        version: VersionSpec | None = None,
        tags: set[str] | None = None,
        components: set[Literal["tool", "resource", "template", "prompt"]]
        | None = None,
        match_all: bool = False,
    ) -> None:

演示实例调用FastMCP的disable方法其实是继承自Provider,它还有另一个与之相对的enable方法,两个方法定义如下。

python 复制代码
class Provider:
    def enable(
        self,
        *,
        names: set[str] | None = None,
        keys: set[str] | None = None,
        version: VersionSpec | None = None,
        tags: set[str] | None = None,
        components: set[Literal["tool", "resource", "template", "prompt"]]
        | None = None,
        only: bool = False,
    ) -> Self

    def disable(
        self,
        *,
        names: set[str] | None = None,
        keys: set[str] | None = None,
        version: VersionSpec | None = None,
        tags: set[str] | None = None,
        components: set[Literal["tool", "resource", "template", "prompt"]]
        | None = None,
    ) -> Self:
        self._transforms.append(
            Visibility(
                False,
                names=names,
                keys=keys,
                version=version,
                components=set(components) if components else None,
                tags=set(tags) if tags else None,
            )
        )
        return self

3. RegexSearchTransform

当服务器公开成百上千种工具时,将完整的工具目录发送给LLM会浪费令牌并降低工具选择的准确性。搜索转换通过将工具列表替换为搜索界面来解决这个问题------LLM可以按需发现工具,而不是预先接收所有工具,这也服务"渐进式披露"原则。 RegexSearchTransform会创建一个工具,并利用它实现基于正则表达式工具检索。它没有额外开销,也不需要构建索引。

下面的程序演示了如何利用RegexSearchTransform来进行基于正则表达式的工具搜索。我们创建了一个此对象并将它创建的工具命名为search_tools(这是默认值,也可以不用显式指定),然后利用它创建了FastMCP对象,并注册了两个工具send_emailsend_sms

python 复制代码
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.transforms.search import RegexSearchTransform
import json

mcp = FastMCP("Server",transforms=[RegexSearchTransform(search_tool_name="search_tools")])
@mcp.tool()
def send_email(to:str, message:str):
    """Send an email"""
    print(f"Sending email to {to} with message '{message}'")

@mcp.tool()
def send_sms(to:str, message:str):
    """Send an SMS"""
    print(f"Sending SMS to {to} with message '{message}'")

async def main():
    async with Client(mcp) as client:
        result = await client.call_tool("search_tools", arguments={"pattern": ".*mail.*"})
        print(json.dumps(result.structured_content["result"], indent=2)) # type: ignore

        result = await client.call_tool("search_tools", arguments={"pattern": ".*send.*"})
        print(json.dumps(result.structured_content["result"], indent=2)) # type: ignore
            
if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

在根据FastMCP对象将Client创建出来后,我们两次调用了由RegexSearchTransform创建的工具search_tools,指定的参数pattern作为搜索条件的正则表达式。如下所示的是两次得到的工具集:

json 复制代码
[
  {
    "name": "send_email",
    "description": "Send an email",
    "inputSchema": {
      "additionalProperties": false,
      "properties": {
        "to": {
          "type": "string"
        },
        "message": {
          "type": "string"
        }
      },
      "required": [
        "to",
        "message"
      ],
      "type": "object"
    },
    "meta": {
      "fastmcp": {
        "tags": []
      }
    }
  }
]
json 复制代码
[
  {
    "name": "send_email",
    "description": "Send an email",
    "inputSchema": {
      "additionalProperties": false,
      "properties": {
        "to": {
          "type": "string"
        },
        "message": {
          "type": "string"
        }
      },
      "required": [
        "to",
        "message"
      ],
      "type": "object"
    },
    "meta": {
      "fastmcp": {
        "tags": []
      }
    }
  },
  {
    "name": "send_sms",
    "description": "Send an SMS",
    "inputSchema": {
      "additionalProperties": false,
      "properties": {
        "to": {
          "type": "string"
        },
        "message": {
          "type": "string"
        }
      },
      "required": [
        "to",
        "message"
      ],
      "type": "object"
    },
    "meta": {
      "fastmcp": {
        "tags": []
      }
    }
  }
]

BaseSearchTransformRegexSearchTransform的基类,后者继承了基类的构造函数:

python 复制代码
class BaseSearchTransform(CatalogTransform):
    def __init__(
        self,
        *,
        max_results: int = 5,
        always_visible: list[str] | None = None,
        search_tool_name: str = "search_tools",
        call_tool_name: str = "call_tool",
        search_result_serializer: SearchResultSerializer | None = None,
    ) -> None

构造函数相关参数说明如下:

  • max_results:限制单次搜索返回的工具数量。防止搜索结果过多再次撑爆LLM的上下文窗口;
  • always_visible:指定一组"常驻"工具名,它们不会被隐藏。用于放置那些最常用、最核心的工具,让LLM无需搜索就能直接看到并调用;
  • search_tool_name:自定义生成的那个"搜索工具"的名字。如果你觉得默认名不够直观,或者想将其改名为find_apis以更契合你的业务语境;
  • call_tool_name: 自定义生成的那个"代理执行工具"的名字。LLM搜索到隐藏工具后,必须通过调用这个名字的工具来间接执行目标逻辑;
  • search_result_serializer: 定义搜索结果如何变成字符串展示给LLM。默认是转成JSON。如果你希望返回更精简的Markdown列表或者特定格式,可以通过这个参数自定义序列化逻辑,从而节省Token或引导LLM更好地理解结果;

4. BM25SearchTransform

BM25会根据所有工具的可搜索文本构建内存索引。该索引在首次搜索时延迟创建,并在工具目录发生更改时自动重建。过期检查基于所有可搜索文本的哈希值,因此即使工具名称保持不变,也能检测到描述的更改。当LLM擅长构建目标模式,并且需要确定性、可预测的结果时,使用正则表达式。正则表达式也更容易调试。当LLM倾向于使用自然语言描述其需求,或者工具目录包含细致的描述,并且相关性排名能够带来价值时,应该使用BM25。BM25能更好地处理部分匹配和同义词,因为它基于单个词条进行评分,而不是要求匹配单个模式。

上面的实例演示了RegexSearchTransform基于正则表达式这种确定性的工具搜索,现在我们按照如下的方式将其替换成BM25SearchTransform实现基于自然语言的工具检索。

python 复制代码
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.transforms.search import BM25SearchTransform
import json

mcp = FastMCP("Server",transforms=[BM25SearchTransform(search_tool_name="search_tools")])
@mcp.tool()
def send_email(to:str, message:str):
    """Send an email"""
    print(f"Sending email to {to} with message '{message}'")

@mcp.tool()
def send_sms(to:str, message:str):
    """Send an SMS"""
    print(f"Sending SMS to {to} with message '{message}'")


async def main():
    async with Client(mcp) as client:
        result = await client.call_tool("search_tools", arguments={"query": "SMS"})
        print(json.dumps(result.structured_content["result"], indent=2)) # type: ignore
            
if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

输出结果:

json 复制代码
[
  {
    "name": "send_sms",
    "description": "Send an SMS",
    "inputSchema": {
      "additionalProperties": false,
      "properties": {
        "to": {
          "type": "string"
        },
        "message": {
          "type": "string"
        }
      },
      "required": [
        "to",
        "message"
      ],
      "type": "object"
    },
    "meta": {
      "fastmcp": {
        "tags": []
      }
    }
  }
]

BM25SearchTransform也是BaseSearchTransform,它的构造函数具有与基类一致的参数定义。

python 复制代码
class BM25SearchTransform(BaseSearchTransform):
    def __init__(
        self,
        *,
        max_results: int = 5,
        always_visible: list[str] | None = None,
        search_tool_name: str = "search_tools",
        call_tool_name: str = "call_tool",
        search_result_serializer: SearchResultSerializer | None = None,
    ) -> None:

5. ToolTransform

工具转换功能允许修改工具架构,包括重命名工具、更改描述、调整标签以及重塑参数架构。FastMCP提供了如下两种类似的机制,它们共享相同的配置选项,但执行时机不同:

  • 使用ToolTransform进行延迟转换,会在工具流经转换链时应用修改。此功能适用于来自已挂载服务器、代理或其他您无法直接控制源的工具提供程序的工具;
  • 使用Tool的类方法from_tool进行立即转换,会立即创建一个修改后的工具对象。此功能适用于您可以直接访问工具并希望在注册前对其进行转换的情况。

在前面介绍工具组件时,我们已经介绍过Tool的类方法from_tool,并且知道该方法会创建一个TransformedTool对象,实际上经过ToolTransform转换生成的也是这么一个对象。在如下的演示程序中,我们注册了一个名为create_contact的工具,并利用注册的ToolTransform对它做了相应的转换,其中包括工具名称(add_contact)、描述和每个参数的名称(name、email和gender别改成full_name、e-mail和sex)。

python 复制代码
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.tools.tool_transform import ArgTransformConfig
from fastmcp.server.transforms.tool_transform import ToolTransform, ToolTransformConfig
from typing import Literal

mcp = FastMCP("Server",transforms=[ToolTransform(transforms={
    "create_contact": ToolTransformConfig(
        name="add_contact",
        description="Add a new contact based on the provided name, email and gender",
        arguments={
            "name": ArgTransformConfig(name = "full_name",description="The contact's name"),
            "email": ArgTransformConfig(name="e-mail", description="The contact's email address"),
            "gender": ArgTransformConfig(name="sex", description="The contact's gender")
        }
    )
})])
@mcp.tool()
def create_contact(name:str, email:str, gender:Literal["male", "female"]):
    """Create a contact"""    

async def main():
    async with Client(mcp) as client:
        result = await client.list_tools()
        tool = result[0]
        print(f"""
Tool name: {tool.name}
Description: {tool.description}
Arguments:""")        
        for name, arg in tool.inputSchema["properties"].items(): # type: ignore
            print(f"\t{name}:{arg["description"]}") # type: ignore
            
if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

输出:

复制代码
Tool name: add_contact
Description: Add a new contact based on the provided name, email and gender
Arguments:
    full_name:The contact's name
    e-mail:The contact's email address
    sex:The contact's gender

ToolTransform的构造函数如下所示,唯一的参数transforms以字典的形式定义针对一个或者多个基础工具的转换规则。字典的Key表示被转换的原始工具名称,转换规则通过一个ToolTransformConfig表示。利用ToolTransformConfig可以设置新工具的名称、版本、标题、描述、标签、可见性和参数列表。它定义的apply方法会根据这些字段将指定的原始工具转化成返回的TransformedTool对象。

python 复制代码
class ToolTransform(Transform):
    def __init__(self, transforms: dict[str, ToolTransformConfig]) -> None

class ToolTransformConfig(FastMCPBaseModel):
    name: str | None
    version: str | None
    title: str | None 
    description: str | None 
    tags: set[str]
    meta: dict[str, Any] | None 
    enabled: bool 
    arguments: dict[str, ArgTransformConfig] 
    def apply(self, tool: Tool) -> TransformedTool

ArgTransformConfig定义了针对单个参数的转换规则,我们可以指定参数的名称、描述、默认值、可见性等,还可以指定有一个或者多个说明性的实例。它的to_arg_transform方法会将这些转换成一个ArgTransform,后者用来提供TransformedTool的参数列表。

python 复制代码
class ArgTransformConfig(FastMCPBaseModel):
    name: str | None
    description: str | None 
    default: str | int | float | bool | None 
    hide: bool 
    required: Literal[True] | None 
    examples: Any | None 

    def to_arg_transform(self) -> ArgTransform

6. ResourcesAsTools & PromptsAsTools

某些MCP客户端仅支持工具,无法直接列出或读取资源和提示词。ResourcesAsTools/PromptsAsTools通过生成可访问服务器资源/提示词的工具来弥补这一缺陷。这两个Transform并不对单个资源和提示词进行转换,而是在只当的Provider范围内对提供的所有资源和提示词进行批量转换。

在如下的演示程序中,我们为创建的FastMCP分别注册了一个静态资源、一个动态资源模板和一个提示词模板。然后我们基于这个FastMCP创建了ResourcesAsToolsPromptsAsTools对象,并注册到新创建的FastMCP上。通过客户端读取的工具列表,我们发现这两个Transform为我们创建了四个工具(list_resourceslist_promptsread_resourceget_prompt

python 复制代码
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.transforms import ResourcesAsTools, PromptsAsTools

mcp = FastMCP("Server")

@mcp.resource(uri="resources://greet/everyone")
def greet_everyone():
    """Greet everyone"""
    return "Hello,everyone!"

@mcp.resource(uri="resources://greet/{name}")
def greet_for_name(name: str):
    """Greet a specific person"""
    return f"Hello,{name}!"

@mcp.prompt()
async def greet_prompt(name: str) -> str:
    """Get a greeting message for the given name"""
    return f"Hi, {name}!"


async def main():
    new_mcp = FastMCP("Server", transforms=[ResourcesAsTools(mcp), PromptsAsTools(mcp)])
    async with Client(new_mcp) as client:
        result = await client.list_tools()
        tool_names = set(tool.name for tool in result)
        assert tool_names == {'list_prompts', 'read_resource', 'get_prompt', 'list_resources'}       
            
if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

ResourcesAsToolsPromptsAsTools类型的构造函数定义如下,我们只需指定用于提供待转换资源和提示词的Provider即可。

python 复制代码
class ResourcesAsTools(Transform)
    def __init__(self, provider: Provider) -> None

class PromptsAsTools(Transform):
    def __init__(self, provider: Provider) -> None
相关推荐
a9511416422 小时前
CSS定位如何实现模态框垂直居中_使用负边距或transform
jvm·数据库·python
2301_775148152 小时前
c++怎么抛出文件读写异常_exceptions()方法开启流异常【详解】
jvm·数据库·python
码农很忙2 小时前
从零到英雄:使用 Playwright 实现高效网页数据爬取与自动化测试
爬虫·python
2401_883600252 小时前
如何用 super 绑定机制在子类构造函数中调用父类
jvm·数据库·python
yuanpan2 小时前
Python 连接 SQLite 数据库:从建表到增删改查的完整演示项目
数据库·python·sqlite
2401_871696522 小时前
HTML怎么构建开发者仪表盘_HTML关键指标卡片汇总【教程】
jvm·数据库·python
2301_796588502 小时前
c++ aot编程 c++如何使用oneapi进行跨平台并行编程
jvm·数据库·python
八代臻2 小时前
design-md
ai编程
草木红2 小时前
Python 中使用 Docker Compose
开发语言·python·docker·flask