[MCP在LangChain中的应用-03]在Session构建的上下文中与MCP Server交互

在MCP架构中,Session是客户端与服务器之间建立的逻辑连接上下文。如果把传输层(Stdio, SSE, WebSocket)比作电话线 ,那么Session就是在这条线上进行的一次完整通话。它的核心意义体现在以下四个方面:

  • 状态管理:虽然底层传输(尤其是HTTP/SSE)可能是无状态的,但Session建立了状态;
  • 双向能力协商:Session允许双方确认彼此支持的功能;
  • 多路复用与隔离:在一个Session内部,可以同时发起多个请求(例如同时读取两个资源),Session负责通过JSON-RPC ID匹配对应的请求和响应。每个Session通常拥有独立的上下文,相互隔离,互不影响;
  • 反向通信:服务端可用反向请求和通知客户端,比如变更通知、日志回传、进度报告、LLM采样和用户征询等,需要Session的支持;

1. ClientSession

MultiServerMCPClient利用如下所示的session方法返回一个针对ClientSession的异步生成器(迭代器),标注在它上面的@asynccontextmanager装饰器使用我们我们可以使用async with来控制其生命周期。

python 复制代码
class MultiServerMCPClient:
    @asynccontextmanager
    async def session(
        self,
        server_name: str,
        *,
        auto_initialize: bool = True,
    ) -> AsyncIterator[ClientSession]

ClientSessionmcp库提供的类型,MCP规范定义的所有从客户端发起的操作都可以从ClientSession类型中找到对应的方法。换句话说,MultiServerMCPClient实现的所有操作都是通过ClientSession完成的。ClientSession继承自BaseSession,后者定义了send_requestsend_notification方法用于向服务端发送请求和通知。

python 复制代码
class BaseSession(
    Generic[
        SendRequestT,
        SendNotificationT,
        SendResultT,
        ReceiveRequestT,
        ReceiveNotificationT,
    ],
):
    async def send_request(
        self,
        request: SendRequestT,
        result_type: type[ReceiveResultT],
        request_read_timeout_seconds: timedelta | None = None,
        metadata: MessageMetadata = None,
        progress_callback: ProgressFnT | None = None,
    ) -> ReceiveResultT

    async def send_notification(
        self,
        notification: SendNotificationT,
        related_request_id: RequestId | None = None,
    ) -> None

对于MCP最核心的七个操作(读取工具列表、读取静态资源列表、读取动态资源模板列表、读取提示词(模板)列表、调用工具、读取指定资源、渲染指定提示词),都可以在ClientSession中找到对应的方法(list_toolslist_resourceslist_resource_templateslist_promptscall_toolread_resourceget_prompt)。

python 复制代码
class ClientSession(
    BaseSession[
        types.ClientRequest,
        types.ClientNotification,
        types.ClientResult,
        types.ServerRequest,
        types.ServerNotification,
    ]
):
    async def list_tools(self, cursor: str | None) -> types.ListToolsResult: ...
    @overload
    async def list_tools(self, *, params: types.PaginatedRequestParams | None) -> types.ListToolsResult: ...
    @overload
    async def list_tools(self) -> types.ListToolsResult: ...
    async def list_tools(
        self,
        cursor: str | None = None,
        *,
        params: types.PaginatedRequestParams | None = None,
    ) -> types.ListToolsResult

    @overload
    async def list_resources(self, *, params: types.PaginatedRequestParams | None) -> types.ListResourcesResult: ...
    @overload
    async def list_resources(self) -> types.ListResourcesResult: ...
    async def list_resources(
        self,
        cursor: str | None = None,
        *,
        params: types.PaginatedRequestParams | None = None,
    ) -> types.ListResourcesResult

    @overload
    async def list_resource_templates(
        self, *, params: types.PaginatedRequestParams | None
    ) -> types.ListResourceTemplatesResult: ...
    @overload
    async def list_resource_templates(self) -> types.ListResourceTemplatesResult: ...
    async def list_resource_templates(
        self,
        cursor: str | None = None,
        *,
        params: types.PaginatedRequestParams | None = None,
    ) -> types.ListResourceTemplatesResult

    @overload
    async def list_prompts(self, *, params: types.PaginatedRequestParams | None) -> types.ListPromptsResult: ...
    @overload
    async def list_prompts(self) -> types.ListPromptsResult: ...
    async def list_prompts(
        self,
        cursor: str | None = None,
        *,
        params: types.PaginatedRequestParams | None = None,
    ) -> types.ListPromptsResult
    
    async def call_tool(
        self,
        name: str,
        arguments: dict[str, Any] | None = None,
        read_timeout_seconds: timedelta | None = None,
        progress_callback: ProgressFnT | None = None,
        *,
        meta: dict[str, Any] | None = None,
    ) -> types.CallToolResult
    async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult 
    async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult

除此之外,ClientSession还定义了如下这些方法。

python 复制代码
class ClientSession(
    BaseSession[
        types.ClientRequest,
        types.ClientNotification,
        types.ClientResult,
        types.ServerRequest,
        types.ServerNotification,
    ]
):
    async def send_ping(self) -> types.EmptyResult
    async def send_progress_notification(
        self,
        progress_token: str | int,
        progress: float,
        total: float | None = None,
        message: str | None = None,
    ) -> None
    async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResult
    async def subscribe_resource(self, uri: AnyUrl) -> types.EmptyResult
    async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult
    async def send_roots_list_changed(self) -> None

上述方法都是针对MCP客户端规范的实现,都会涉及一次从客户端到服务器的请求,MCP规范为它们定义了标准的JSON-RPC方法:

  • send_ping:向服务端发送ping请求确认服务端是否存活,JSON-RPC方法为ping;
  • send_progress_notification:向服务端发送某项任务的进度变化的通知,JSON-RPC方法为notifications/progress
  • set_logging_level: 向服务端发送请求设置日志等级,JSON-RPC方法为logging/setLevel
  • subscribe_resource: 订阅指定资源的变换,当资源发生变更的时候,服务端会发送对应的通知,JSON-RPC方法为resources/subscribe
  • unsubscribe_resource: 接触资源变更订阅,JSON-RPC方法为resources/unsubscribe
  • send_roots_list_changed:向服务端发送Roots(客户端为服务器设置的资源操作边界的)变化的通知,JSON-RPC方法为notifications/roots/list_changed

MultiServerMCPClient只提供了如下三个基本的操作,并分别由对应的三个全局函数来完成。

  • load_mcp_tools: 提取工具列表;
  • load_mcp_resources:读取指定资源:
  • get_prompt:渲染指定提示符模板(我们沿用FastMCP的说法,将利用指定参数填充提示词模板的操作称为渲染)

这三个函数均以ClientSession作为其参数,所以这些函数执行的操作仅仅针对于单一的服务器。

2. 获取工具列表(load_mcp_tools)

用于读取工具列表的load_mcp_tools函数对应ClientSessionlist_tools方法。如果传入的session参数为None,会利用connection创建一个ClientSession对象,并在函数结束之前关闭Session。sessionconnection不允许同时为None。由于tools/list操作采用分页的方式获取工具,所以如果工具集太大,会涉及多次调用。ClientSessionlist_tools方法返回的工具是一个mcp.types.Tool对象,它会被转换成BaseTool对象,具体是一个StructuredTool对象。

python 复制代码
async def load_mcp_tools(
    session: ClientSession | None,
    *,
    connection: Connection | None = None,
    callbacks: Callbacks | None = None,
    tool_interceptors: list[ToolCallInterceptor] | None = None,
    server_name: str | None = None,
    tool_name_prefix: bool = False,
) -> list[BaseTool]

BaseTool是一个继承自Runnable的可执行对象。当get_tools方法在将mcp.types.Tool对象转换成StructuredTool对象时,会创建一个异步函数作为它的"执行体"。这个异步函数通过调用ClientSessioncall_tool方法完成目标工具的执行。

我们已经知道了load_mcp_tools得到的就是"适配"好的BaseTool对象,当我们调用此BaseTool对象的时候会自动请求MCP服务器来执行工具。现在我们关注另一个问题:ClientSessioncall_tool方法返回的是一个mcp.types.CallToolResult对象,但是LangChain的工具返回的应该是一个ToolMessage或者Command,这两者之间又是如何适配的?为此我们创建一个演示实例,如下这个是我们构建的MCP服务器,它提供了一个根据指定用户ID返回个人信息的工具get_profile。我们采用传输协议streamable-http启动它。

python 复制代码
from fastmcp import FastMCP
mcp = FastMCP("Server")

@mcp.tool()
async def get_profile(user_id:str) -> str:
    """Get user profile information."""
    print(f"Received request for user_id: {user_id}")
    if user_id:
        return (
            f"My name is John Doe (user id is {user_id}), and I am a software engineer with 5 years of experience in web development."
            "I enjoy working with Python and JavaScript, and I have a passion for learning new technologies."
            "In my free time, I like to travel and explore new places."
        )
    else:
        raise ValueError("User ID is required to get profile information.")
    
mcp.run(transport="streamable-http",host="0.0.0.0")

在如下所示的客户端程序中,我们连接上面这个MCP服务器创建了MultiServerMCPClient对象。我们采用两种方式调用工具get_profile:一种是调用ClientSessioncall_tool方法,另一种则是直接调用工具对象的ainvoke方法。

python 复制代码
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_core.messages import ToolMessage,ToolCall
import asyncio

async def main(): 
    client = MultiServerMCPClient(
       connections= {
            "server": {
                "transport": "streamable_http",
                "url": "http://localhost:8000/mcp"
            }
        }
    )

    async with client.session("server") as session:
        result = await session.call_tool(
            name="get_profile",
            arguments={"user_id": "123"},
        )
        print(f"""\
session.call_tool:
    content:{result.content}
    structured_content:{result.structuredContent}""")      
        
        tool = (await load_mcp_tools(session=session))[0]
        assert tool.response_format == "content_and_artifact"
        tool_call:ToolCall ={
            "id": "t001",
            "name": tool.name,
            "args": {"user_id": "123"},
            "type": "tool_call"
        }
        tool_messsage:ToolMessage = await tool.ainvoke(input=tool_call)
        print(f"""
tool.ainvoke:
    content:{tool_messsage.content}
    artifact:{tool_messsage.artifact}""")

asyncio.run(main())

ClientSessioncall_tool方法返回一个mcp.types.CallToolResult对象,我们将它们的contentstructured_content字段打印出来。直接调用工具对象会返回个ToolMessage对象,我们输出它的contentartifact字段。从如下所示的输出结果可以看出,ToolMessagecontentartifact分别是根据CallToolResultcontentstructured_content生成的。

复制代码
session.call_tool:
    content:[TextContent(type='text', text='My name is John Doe (user id is 123), and I am a software engineer with 5 years of experience in web development.I enjoy working with Python and JavaScript, and I have a passion for learning new technologies.In my free time, I like to travel and explore new places.', annotations=None, meta=None)]
    structured_content:{'result': 'My name is John Doe (user id is 123), and I am a software engineer with 5 years of experience in web development.I enjoy working with Python and JavaScript, and I have a passion for learning new technologies.In my free time, I like to travel and explore new places.'}

tool.ainvoke:
    content:[{'type': 'text', 'text': 'My name is John Doe (user id is 123), and I am a software engineer with 5 years of experience in web development.I enjoy working with Python and JavaScript, and I have a passion for learning new technologies.In my free time, I like to travel and explore new places.', 'id': 'lc_9d4a73e2-469a-480e-b4e7-04a8234fb969'}]
    artifact:{'structured_content': {'result': 'My name is John Doe (user id is 123), and I am a software engineer with 5 years of experience in web development.I enjoy working with Python and JavaScript, and I have a passion for learning new technologies.In my free time, I like to travel and explore new places.'}}

上面介绍了正常情况下从CallToolResultToolMessage的转换规则。如果工具调用出现异常,会反映在CallToolResultisError字段上,此时会抛出一个ToolInvocationError异常。

3. 读取资源(load_mcp_resources)

load_mcp_resources函数用于"加载"资源的内容。除了将ClientSession作为其参数之外,我们还可以利用uris指定目标资源的地址。读取资源内容被转换成Blob对象。

python 复制代码
async def load_mcp_resources(
    session: ClientSession,
    *,
    uris: str | list[str] | None = None,
) -> list[Blob]

class Blob(BaseMedia):
    data: bytes | str | None = None
    mimetype: str | None = None
    encoding: str = "utf-8"
    path: PathLike | None = None
    model_config = ConfigDict(
        arbitrary_types_allowed=True,
        frozen=True,
    )

load_mcp_resources函数执行的逻辑如下:

  • 如果没有指定资源地址,函数对调用ClientSessionlist_resources方法得到一组mcp.types.Resource对象的列表。如果资源数量超出单页容纳的限制,这里会涉及多次调用。这里的Resource并不包含内容,仅提供描述资源的元数据,这里只提取标识资源的URI;
  • 针对每个URI,调用ClientSessionread_resource方法读取资源内容,具体得到的分别是代表文本内容和二进制内容的TextResourceContents或者BlobResourceContents对象;
  • 这些TextResourceContentsBlobResourceContents对象被统一转换成Blob对象。
python 复制代码
class ResourceContents(BaseModel):
    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]
    mimeType: str | None = None
    meta: dict[str, Any] | None = Field(alias="_meta", default=None)
    model_config = ConfigDict(extra="allow")
class TextResourceContents(ResourceContents):
    text: str
class BlobResourceContents(ResourceContents):
    blob: str

4.获取提示词(load_mcp_prompt)

load_mcp_prompt函数会读取指定的提示词模板,并利用提供的参数填充模板占位符。它最终返回的是一个HumanMessage或者AIMessage的列表,可以直接作为Chat语言模型的输入。

python 复制代码
async def load_mcp_prompt(
    session: ClientSession,
    name: str,
    *,
    arguments: dict[str, Any] | None = None,
) -> list[HumanMessage | AIMessage]

load_mcp_prompt函数调用的是ClientSessionget_prompt方法,该方法返回一组mcp.types.PromptMessage对象。PromptMessage与LangChain的消息具有类似的定义,比如它们都绑定某种角色(user或者assistant),都可以携带多媒体内容,所以可以直接转换成对应的HumanMessage或者AIMessage类型(分别对应角色userassistant)。

python 复制代码
class PromptMessage(BaseModel):
    role: Role
    content: ContentBlock
    model_config = ConfigDict(extra="allow")

Role = Literal["user", "assistant"]
ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource

5. get_tools、get_prompt和get_resources方法

定义在MultiServerMCPClientget_toolsget_resourcesget_prompt方法的作用与上述三个函数是一致的。不仅如此,这三个函数最终是调用上述三个函数来实现的。

python 复制代码
class MultiServerMCPClient:
    async def get_tools(self, *, server_name: str | None = None) -> list[BaseTool]:
    async def get_prompt(
        self,
        server_name: str,
        prompt_name: str,
        *,
        arguments: dict[str, Any] | None = None,
    ) -> list[HumanMessage | AIMessage]    
    async def get_resources(
        self,
        server_name: str | None = None,
        *,
        uris: str | list[str] | None = None,
    ) -> list[Blob]

这三个方法具有类似的逻辑:

  • 如果指定了服务器名称(get_prompt方法必需指定),方法会创建连接指定服务器的ClientSession对象,并将其作为参数调用对应的函数得到返回的结果,然后关闭Session;
  • 对于get_toolsget_resources方法,如果没有指定服务器名称,方法针对每个服务器执行上面的操作,然后将得到结果进行合并返回;
相关推荐
X56615 小时前
SQL注入防御技术方案_基于正则表达式的输入清洗
jvm·数据库·python
涛声依旧-底层原理研究所5 小时前
Qwen2.5模型加载与推理实战
人工智能·python
SunnyDays10115 小时前
如何使用 Python 将 PDF 转换为 TIFF 或将 TIFF 转换为 PDF
人工智能·python·pdf
程序员鱼皮5 小时前
小米送了我 16 亿 tokens,给我测爽了!手把手教你领取 | 附 Claude Code + MiMo-V2.5 实战测评
计算机·ai·程序员·编程·ai编程
tianyuanwo5 小时前
CentOS 7 使用 CentOS 8 YUM 源报错 “Invalid version flag: if” 深度解析
python·centos·yum
秒云5 小时前
MIAOYUN | 每周AI新鲜事儿 260430
人工智能·ai·语言模型·aigc·ai编程
技术钱5 小时前
Flask-SQLAIchemy和Flask-Migrate扩展的配置与使用
数据库·python·flask
Li emily5 小时前
用Python批量调用外汇接口获取多货币汇率
人工智能·python·api·fastapi
财经资讯数据_灵砚智能5 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年4月30日
人工智能·python·信息可视化·自然语言处理·ai编程