MCP 实战——MCP 核心组件

150 阅读12分钟

在前两章里,你已经取得了巨大的进步:第 2 章里,你从一片空白起步,构建了第一个 MCP 服务器,掌握了本地与远程通信;第 3 章里,你把自定义工具与 Anthropic、Google、OpenAI 的强大 LLM 接了起来。

本质上,你已经学会了如何给 AI 赋予一项新技能。但目前这些技能还是简单的“一次性函数” :LLM 调用工具,工具完成工作并返回一个结果。很强大,但这只是开始。

如果你的工具需要提供背景信息,让 LLM 在不执行函数的前提下就能读取呢?如果它提供的数据需要根据用户 ID 或文件名而变化呢?如果你的工具执行到一半才发现缺少关键信息该怎么办?

本章将深入 Model Context Protocol 的核心,超越简单工具,探索能够实现更丰富、更动态、更具交互性的工作流的核心组件。我们要把你的服务器,从一个“命令—响应”的简单系统,升级成 LLM 的上下文感知型伙伴

在本章结束时,你将能够:

  • 通过静态资源(Resources)暴露只读数据

  • 使用资源模板(Resource Templates)创建按需动态数据

  • 用**可复用、可参数化的提示(Prompts)**指导 LLM;

  • 掌握强大的 Context 对象,以便:

    • 向客户端发送实时日志与进度
    • 在缺信息时通过 Elicitation(引导提问) 向用户请求补充。

你已经学会给 AI 一件工具;现在,你将学会给它一整间工具工坊

资源与提示:构建上下文的地基

在进入更具交互性的组件之前,我们先看看除工具之外,MCP 服务器的另外两块基石:ResourcesPrompts

  • Resources只读文件,向 LLM 提供数据——可把它们视为 Web API 里的 GET 请求。
  • Prompts可复用的模板,为 LLM 提供专家级指令,并可包含用于填充细节的占位符。

我们先搭一个展示这些组件的服务器,后续章节都将以此为基础。

fastmcp 方式

创建新文件 mcp_server.py

图 39. mcp_server.py(fastmcp)

from fastmcp import FastMCP

mcp = FastMCP("My MCP Server")

# 1. A static Resource
@mcp.resource("resource://welcome_message")
def welcome() -> str:
    return "Welcome to my MCP server! Don't be naughty here."

# 2. A Resource Template
@mcp.resource("users://{user_id}/tokens")
def get_user_tokens(user_id: int) -> int:
    if user_id < 10:
        return 100
    else:
        return 8

# 3. A Prompt
@mcp.prompt
def get_employee_profile(employee_id: int) -> str:
    return f"Using SQL, get the employee profile from the id: {employee_id}"

if __name__ == "__main__":
    mcp.run(transport="http", port=9000)

解析:

  • 静态资源@mcp.resourcewelcome 的返回值暴露出去。固定的 URI resource://welcome_message 表示:任何客户端读取该资源都将得到同一条欢迎信息。
  • 资源模板:看起来像资源,但 URI users://{user_id}/tokens 带有占位符。这告诉 fastmcp:这是模板{user_id} 会作为参数传入 get_user_tokens,从而返回动态数据
  • 提示@mcp.prompt 创建可复用的指令集。函数名 get_employee_profile 成为提示的标识。客户端可以按名称获取该提示、填入 employee_id,然后把生成的指令发送给 LLM。

mcp 库方式

mcp 版本几乎一模一样,体现了 fastmcp 先行的高层 API 设计。

图 40. mcp_server.py(mcp)

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My MCP Server", host="127.0.0.1", port=9000)

@mcp.resource("resource://welcome_message")
def welcome() -> str:
    return "Welcome to my MCP server! Don't be naughty here."

@mcp.resource("users://{user_id}/tokens")
def get_user_tokens(user_id: int) -> int:
    if user_id < 10:
        return 100
    else:
        return 8

@mcp.prompt()
def get_employee_profile(employee_id: int) -> str:
    return f"Using SQL, get the employee profile from the id: {employee_id}"

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

主要差异在导入与构造/运行配置(第 2 章已见)。@mcp.resource@mcp.prompt 的核心概念保持一致。

从客户端进行交互

接下来看看客户端如何与这些新组件交互。

fastmcp 客户端

图 41. mcp_client.py(fastmcp)

import asyncio
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport

client = Client(transport=StreamableHttpTransport("http://localhost:9000/mcp"))

async def main():
    async with client:
        # List available resources
        resources = await client.list_resources()
        print(resources)
        
        # Read the static resource
        output = await client.read_resource("resource://welcome_message")
        print(f"Resource 'welcome_message': {output[0].text}")

        # Read the resource template with a specific user ID
        user_id = 12
        output = await client.read_resource(f"users://{user_id}/tokens")
        print(f"Resource of user tokens: {output[0].text}")

        # Get the prompt with a specific employee ID
        output = await client.get_prompt("get_employee_profile", {"employee_id": 7})
        print(f"Prompt of getting employee profile: {output.messages[0].content.text}")

asyncio.run(main())

mcp 库客户端

图 42. mcp_client.py(mcp)

import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

MCP_SERVER_URL = "http://127.0.0.1:9000/mcp/"

async def run():
    async with streamablehttp_client(MCP_SERVER_URL) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()

            resources = await session.list_resources()
            print(resources)
            
            output = await session.read_resource("resource://welcome_message")
            print(f"Resource 'welcome_message': {output.contents[0].text}")

            user_id = 12
            output = await session.read_resource(f"users://{user_id}/tokens")
            print(f"Resource of user tokens: {output.contents[0].text}")

            output = await session.get_prompt("get_employee_profile", {"employee_id": "7"})
            print(output.messages[0].content.text)

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

运行代码:
先运行你选择的服务器脚本:> python mcp_server.py
再在新终端中运行对应的客户端脚本:> python mcp_client.py

你会看到类似输出:

图 43. 客户端输出

[Resource(name='welcome', ...), Resource(name='get_user_tokens', ...)]
Resource 'welcome_message': Welcome to my MCP server! Don't be naughty here.
Resource of user tokens: 8
Prompt of getting employee profile: Using SQL, get the employee profile from the id: 7

可以看到,客户端通过专门的方法(read_resourceget_prompt)与这些组件交互:既能直接读取静态资源,也能给资源模板与提示**“填空”**,获得定制的数据与指令。

关于类型的小提示: 你可能注意到,在 get_prompt 上,官方 MCP 规范通常期望参数值为字符串fastmcp 更“宽松”,常会帮你做类型转换;而 mcp 库可能要求你更显式。这正是高层与低层库在哲学上的常见差异。

Context 对象:给工具装上“超能力”

ResourcesPrompts 很强,但相对静态。MCP 最动态、最具交互性的能力由 Context 对象解锁。

当工具在运行时,它经常需要在执行期间与客户端通信:比如汇报“已完成 50%”、记录一个警告,或暂停并向用户要更多信息。**Context 对象(常简写为 ctx)**就是实现这些能力的桥梁。

你可以在工具函数签名中添加一个类型标注为 Context 的参数,或调用专门的 get_context() 函数来获取它。

日志与进度上报

想象一个耗时很长的工具:处理大数据集,或调用很慢的外部 API。如果工具静默运行,用户只能盯着空白界面,怀疑是否卡死。

MCP 通过日志进度机制解决这一问题:你的工具可以在运行时实时回传消息。

服务器端实现

我们来写一个模拟长任务的工具,它会回传进度与日志信息。

fastmcp 服务器

图 44. logging_mcp_server.py(fastmcp)

from fastmcp import FastMCP
from fastmcp.server.dependencies import get_context

mcp = FastMCP("My MCP Server")

@mcp.resource("data://database")
def get_data() -> str:
    return "Data from database"

@mcp.tool
async def call_api_somewhere() -> str:
    # 1. Get the context object
    ctx = get_context()

    # 2. Use the context to read another resource
    resources = await ctx.read_resource("data://database")

    # 3. Use the context to report progress
    await ctx.report_progress(progress=80, total=100, message=f"{resources[0].content} pipeline")

    # 4. Use the context to send an info log
    await ctx.info(f"Processing 80% of the total data")

    return "Done!"

if __name__ == "__main__":
    mcp.run(transport="http", port=9000)

mcp 库服务器

图 45. logging_mcp_server.py(mcp)

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("My MCP Server", host="127.0.0.1", port=9000)

@mcp.resource("data://database")
def get_data() -> str:
    return "Data from database"

@mcp.tool()
async def call_api_somewhere() -> str:
    # Get the context from the server instance
    ctx = mcp.get_context()

    resources = await ctx.read_resource("data://database")

    await ctx.report_progress(progress=80, total=100, message=f"{resources[0].content} pipeline")

    await ctx.info(f"Processing data")

    return "Done!"

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

两段服务器代码非常相似。工具 call_api_somewhere 获取 ctx,并调用其方法:

  • ctx.read_resource():甚至可以在同一台 MCP 服务器内部读取其他资源!
  • ctx.report_progress():发送进度更新(非常适合驱动客户端的进度条)。
  • ctx.info():发送 info 级别日志(还支持 ctx.debug()ctx.warning()ctx.error())。

使用 Context 的函数需要是异步的;在同步函数里无法使用 Context。

客户端实现

要接收这些消息,客户端需要提供回调处理函数——当日志或进度消息到达时会被自动调用。

fastmcp 客户端

图 46. logging_mcp_client.py(fastmcp)

import asyncio
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.client.logging import LogMessage

# 1. Define a handler for log messages
async def log_handler(message: LogMessage):
    print(f"[{message.level}]: {message.data}")

# 2. Define a handler for progress reports
async def progress_handler(progress: float, total: float | None, message: str | None):
    if total is not None:
        percentage = (progress / total) * 100
        print(f"Progress: {percentage:.1f}% - {message or ''}")
    else:
        print(f"Progress: {progress} - {message or ''}")

# 3. Pass the handlers to the Client constructor
client = Client(transport=StreamableHttpTransport("http://localhost:9000/mcp"),
                log_handler=log_handler)

async def main():
    async with client:
        await client.call_tool("call_api_somewhere",
                               progress_handler=progress_handler)

asyncio.run(main())

mcp 库客户端

图 47. logging_mcp_client.py(mcp)

import asyncio
from mcp import ClientSession, types
from mcp.client.streamable_http import streamablehttp_client

# Define the log handler
async def log_handler(log_message: types.LoggingMessageNotificationParams):
    print(f"[{log_message.level}]: {log_message.data}")

# Define the progress handler
async def progress_handler(progress: float, total: float | None, message: str | None):
    if total is not None:
        percentage = (progress / total) * 100
        print(f"Progress: {percentage:.1f}% - {message or ''}")
    else:
        print(f"Progress: {progress} - {message or ''}")

MCP_SERVER_URL = "http://127.0.0.1:9000/mcp/"

async def run():
    async with streamablehttp_client(MCP_SERVER_URL) as (read, write, _):
        # Pass handlers to the session constructor
        async with ClientSession(
            read, write, logging_callback=log_handler
        ) as session:
            await session.initialize()
            # Pass progress handler to the tool call
            await session.call_tool("call_api_somewhere",
                                    progress_callback=progress_handler)

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

当你先运行服务器、再运行客户端时,你会实时看到从服务器返回的消息被打印出来:

图 48. 日志与进度输出

1 Progress: 80.0% - Data from database pipeline
2 [info]: Processing data

这比“黑盒沉默”的工具强太多了:你的客户端应用现在可以提供丰富而信息量十足的用户体验。

Elicitation:将独白变为对话

这或许是 Context 对象最强大的特性。Elicitation(引导提问)指的是:工具在执行过程中暂停,向用户索取更多信息的机制。

想想一个 save_file 工具:如果用户没有指定云存储提供商,工具该怎么办?猜测?失败?有了 elicitation,它可以直接发问:“你想把数据保存到哪里?Dropbox 还是 Google Drive?”

服务器端实现

我们来构建一个会向用户询问“保存到哪里”的工具。

fastmcp 服务器:
图 49. elicitation_mcp_server.py(fastmcp)

from fastmcp import FastMCP
from fastmcp.server.dependencies import get_context
from dataclasses import dataclass

mcp = FastMCP("My MCP Server")

# 1. Define the structure of the expected answer
@dataclass
class CloudInfo:
    name: str

@mcp.tool
async def save_data_to_cloud() -> str:
    ctx = get_context()

    # 2. Elicit information from the user
    result = await ctx.elicit(
        message="Where do you want to save your data?",
        response_type=CloudInfo
    )

    # 3. Handle the user's response
    if result.action == "accept":
        cloud = result.data
        if cloud.name == "Dropbox":
            return "Saving data to Dropbox"
        elif cloud.name == "Google Drive":
            return "Saving data to Google Drive"
    elif result.action == "decline":
        return "Information not provided"
    else:
        return "Operation cancelled"

if __name__ == "__main__":
    mcp.run(transport="http", port=9000)

mcp 库服务器:
图 50. elicitation_mcp_server.py(mcp)

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("My MCP Server", host="127.0.0.1", port=9000)

class CloudInfo(BaseModel):
    name: str = Field(description="The name of the cloud")

@mcp.tool()
async def save_data_to_cloud() -> str:
    ctx = mcp.get_context()

    result = await ctx.elicit(
        message="Where do you want to save your data?",
        schema=CloudInfo
    )

    if result.action == "accept":
        cloud = result.data
        if cloud.name == "Dropbox":
            return "Saving data to Dropbox"
        elif cloud.name == "Google Drive":
            return "Saving data to Google Drive"
    elif result.action == "decline":
        return "Information not provided"
    else:
        return "Operation cancelled"

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

关键步骤:

  • 定义模式(Schema) :定义你期望用户返回的数据结构。fastmcp 使用 Python dataclass,mcp 库使用 Pydantic BaseModel,效果一致。
  • 调用 ctx.elicit() :该方法把问题发给客户端,并暂停工具执行,等待用户回应。
  • 处理结果:返回的 result 告知用户的动作:"accept""decline""cancel"。若为 "accept"data 属性中包含已校验的用户响应,并被解析为你的 dataclass 或 Pydantic 模型。

客户端实现

与日志类似,客户端需要一个 elicitation 处理器:这个函数负责实际向用户提问并把他们的答案回传给服务器。

fastmcp 客户端:
图 51. elicitation_mcp_client.py(fastmcp)

import asyncio
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.client.elicitation import ElicitResult

# 1. The elicitation handler
async def elicitation_handler(message: str, response_type: type, params, context):
    user_input = input(f"{message}: ")
    response = {"name": user_input}
    # 2. Return the user's accepted response
    return ElicitResult(action="accept", content=response)

client = Client(transport=StreamableHttpTransport("http://localhost:9000/mcp"),
                elicitation_handler=elicitation_handler)

async def main():
    async with client:
        result = await client.call_tool("save_data_to_cloud")
        print(result)

asyncio.run(main())

mcp 库客户端:
图 52. elicitation_mcp_client.py(mcp)

import asyncio
from mcp import ClientSession, types
from mcp.client.streamable_http import streamablehttp_client

async def elicitation_handler(context, params):
    user_input = input(f"{params.message}: ")
    response = {"name": user_input}
    return types.ElicitResult(action="accept", content=response)

MCP_SERVER_URL = "http://127.0.0.1:9000/mcp/"

async def run():
    async with streamablehttp_client(MCP_SERVER_URL) as (read, write, _):
        async with ClientSession(
            read, write, elicitation_callback=elicitation_handler
        ) as session:
            await session.initialize()
            result = await session.call_tool("save_data_to_cloud")
            print(result)

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

交互流程非常有趣:

  1. 客户端发送 call_tool 请求;
  2. 服务器端工具运行并触发 ctx.elicit()
  3. 服务器向客户端发回elicitation 请求
  4. 客户端的 elicitation_handler 被触发,打印提示并等待 input()
  5. 你输入 “Google Drive” 并回车;
  6. 处理器把答案封装为 ElicitResult 回传给服务器;
  7. 服务器恢复执行,result.action == "accept"result.data.name == "Google Drive"
  8. 工具返回最终字符串,客户端打印结果。

输出示例:

1 Where do you want to save your data?: Google Drive
2 CallToolResult(content=[TextContent(type='text', text='Saving data to Google Drive', annotations=None, meta=None)], structured_content={'result': 'Saving data to Google Drive'}, data='Saving data to Google Drive', is_error=False)

若使用 mcp 库,输出如下:
图 53. Elicitation 输出

1 meta=None content=[TextContent(type='text', text='Saving data to Google Drive', annotations=None, meta=None)] structuredContent={'result': 'Saving data to Google Drive'} isError=False

你刚刚创建了一个交互式、对话式的工具。

关键要点(Key Takeaways)

  • Resources vs. Tools:你已经理解两者的区别。Resources 用于提供只读数据(类似 GET),Tools 用于执行动作(类似 POST)。

  • 静态 vs. 动态数据:用静态资源暴露固定数据;用资源模板按需、参数化地生成数据。

  • 引导 LLMPrompts 让你把专家级指令直接嵌入服务器,形成可复用且有效的交互模式。

  • Context 的力量ctx 是高级特性的钥匙:

    • 日志与进度:在长流程中向用户提供实时反馈
    • Elicitation(引导提问) :在缺少信息时,把一次工具调用变成互动对话

你已经掌握了构建复杂、交互式 MCP 服务器的能力。但它们仍运行在本地,通过临时的 ngrok 隧道暴露到公网。这适合开发,却不是生产方案。

下一章,我们将把这些强大的服务器稳健地部署到云端:学习如何打包部署、在 Google Cloud Run 等平台运行,并通过正确的身份验证保障安全,把你的开发项目升级为生产级 AI 服务