在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]
ClientSession是mcp库提供的类型,MCP规范定义的所有从客户端发起的操作都可以从ClientSession类型中找到对应的方法。换句话说,MultiServerMCPClient实现的所有操作都是通过ClientSession完成的。ClientSession继承自BaseSession,后者定义了send_request和send_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_tools、list_resources、list_resource_templates、list_prompts、call_tool、read_resource和get_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函数对应ClientSession的list_tools方法。如果传入的session参数为None,会利用connection创建一个ClientSession对象,并在函数结束之前关闭Session。session和connection不允许同时为None。由于tools/list操作采用分页的方式获取工具,所以如果工具集太大,会涉及多次调用。ClientSession的list_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对象时,会创建一个异步函数作为它的"执行体"。这个异步函数通过调用ClientSession的call_tool方法完成目标工具的执行。
我们已经知道了load_mcp_tools得到的就是"适配"好的BaseTool对象,当我们调用此BaseTool对象的时候会自动请求MCP服务器来执行工具。现在我们关注另一个问题:ClientSession的call_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:一种是调用ClientSession的call_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())
ClientSession的call_tool方法返回一个mcp.types.CallToolResult对象,我们将它们的content和structured_content字段打印出来。直接调用工具对象会返回个ToolMessage对象,我们输出它的content和artifact字段。从如下所示的输出结果可以看出,ToolMessage的content和artifact分别是根据CallToolResult的content和structured_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.'}}
上面介绍了正常情况下从CallToolResult到ToolMessage的转换规则。如果工具调用出现异常,会反映在CallToolResult的isError字段上,此时会抛出一个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函数执行的逻辑如下:
- 如果没有指定资源地址,函数对调用
ClientSession的list_resources方法得到一组mcp.types.Resource对象的列表。如果资源数量超出单页容纳的限制,这里会涉及多次调用。这里的Resource并不包含内容,仅提供描述资源的元数据,这里只提取标识资源的URI; - 针对每个URI,调用
ClientSession的read_resource方法读取资源内容,具体得到的分别是代表文本内容和二进制内容的TextResourceContents或者BlobResourceContents对象; - 这些
TextResourceContents和BlobResourceContents对象被统一转换成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函数调用的是ClientSession的get_prompt方法,该方法返回一组mcp.types.PromptMessage对象。PromptMessage与LangChain的消息具有类似的定义,比如它们都绑定某种角色(user或者assistant),都可以携带多媒体内容,所以可以直接转换成对应的HumanMessage或者AIMessage类型(分别对应角色user和assistant)。
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方法
定义在MultiServerMCPClient的get_tools、get_resources和get_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_tools和get_resources方法,如果没有指定服务器名称,方法针对每个服务器执行上面的操作,然后将得到结果进行合并返回;