mcpField
F
Flywizz

Flywizz

本地服务
github.com/victorlane地图与出行08 周前更新

关于 Flywizz

Flywizz 是用于获取 Wizz Air 航班数据的非官方开源 Python 封装,并附带 MCP 服务,可用自然语言搜索航班。它涵盖时刻表、票价、航线和航班状态,同时说明部分余票接口受机器人防护限制。

主要功能

  1. 01查询运营日期和时刻表
  2. 02按日期比较票价
  3. 03探索航线和目的地
  4. 04获取航班状态
  5. 05提供异步客户端方法

适用场景

  1. 01查找指定航线和日期范围的航班
  2. 02比较不同日期的票价
  3. 03探索从某机场出发的目的地
  4. 04查询航班状态

MCP 相关

内置 MCP 服务允许 MCP 客户端通过自然语言搜索 Wizz Air 航班。README 列出了 Claude Desktop、Claude Code 和 Cursor。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"victorlane-flywizz": {
"command": "uvx",
"args": [
"Flywizz"
]
}
}
}
启动命令uvx Flywizz

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypiFlywizz0.1.0

工具

5

收录的工具列表 · 更新于 2 周前

参数类型必填说明
adultsinteger否
originstring是
date_tostring是
infantsinteger否
childreninteger否
date_fromstring是
destinationstring是
wizz_discount_clubboolean否

详细介绍

Flywizz SDK

PyPI version Python versions CI CodeQL PyPI downloads License

一个开源的非官方 API 封装,用于获取 Wizz Air 航班数据。

提示

内置面向 AI Agent 的 MCP 服务。 将 Flywizz 接入 Claude Desktop、 Claude Code 或 Cursor,即可用自然语言搜索 Wizz Air 航班。 跳转至 MCP 快速入门。

目录

安装

pip install Flywizz

或使用 uv:

uv add Flywizz

快速入门

from datetime import datetime, timedelta
from flywizz import WizzAir, TimetableSearch

# Initialize the client
client = WizzAir()

# Set up search parameters
search = TimetableSearch(
    origin="BUD",       # Budapest
    destination="LTN",  # London Luton
    date_from=datetime.now() + timedelta(days=30),
    date_to=datetime.now() + timedelta(days=60),
)

# One call gets the schedule and the prices
for day in client.get_timetable(search):
    if day.price is None:
        continue
    print(f"{day.departure_date.date()}: {day.price.amount} {day.price.currency}")
    print(f"  departures: {', '.join(d.departure.strftime('%H:%M') for d in day.departures)}")

每个条目对应一个运营日:当天最便宜的票价,以及当天所有的起飞时间,因此一次调用即可同时回答“何时有航班”和“价格是多少”。

首先需要了解的三件事

价格使用出发站的货币

Wizz Air 不支持在服务器端覆盖货币。BUD -> LTN 报价使用 HUF, LTN -> BUD 报价使用 GBP,WAW -> LTN 报价使用 PLN。名为 currency 的请求体字段、查询参数、Cookie 和请求头都会被忽略。

如果你需要在搜索前了解将使用哪种货币,请从 get_network() 中读取 Station.currency_code,并在客户端进行换算。

并非每个运营日都有报价

timetableV2 和 farechart 会为每一天附加一个 price 对象,但对于 不会直接提供报价的日期,amount 为 0,而 price_type 为 checkPrice(存在票价,但只能通过 search/search 查询)或 noData。Flywizz 会将两者都映射为 price=None,因此无报价的日期绝不会被误认为免费。时刻表条目仍会通过 original_price 提供参考票价:

for day in client.get_timetable(search):
    if day.price is not None:
        print(day.departure_date.date(), day.price.amount, day.price.currency)
    elif day.original_price is not None:
        print(day.departure_date.date(), "~", day.original_price.amount, "(check price)")
搜索时间范围有限制,且上限各不相同

提供和不提供价格的接口对可查询的未来时间范围有不同限制。超出范围时,两者都会返回 400 InvalidTimeDateRange,且不提供进一步说明,因此 Flywizz 会自行执行各自的限制,并在提示消息中注明上限。

from flywizz.misc import (
    MAX_TIMETABLE_WINDOW_DAYS,      # 42, for timetableV2 and farechart
    MAX_FLIGHT_DATES_WINDOW_DAYS,   # 62, for the schedule endpoints
)

cheapest_weekend() 会在内部拆分时间范围,因为其默认值 months_ahead=3 远超任一上限。

search/search 受机器人防护机制限制

四个端点(search/search、booking/seatmap、booking/ancillaries、 booking/passengers)受 Kasada 防护,来自任何非浏览器客户端的请求都会返回空响应体和 429。Flywizz 会抛出 BotGateError,而不会尝试破解验证。

其他所有接口,包括可提供价格的 timetableV2 和 farechart,均开放访问。这足以用于价格跟踪、航线探索和日历搜索。如果你需要票价套餐和销售密钥,请通过真实浏览器会话发送请求,并传入会话的请求头:

from flywizz import WizzAir, WizzairTransport

client = WizzAir(WizzairTransport(kasada_headers={
    "x-kpsdk-ct": "...",
    "x-kpsdk-v": "...",
    "x-kpsdk-h": "...",
    "x-kpsdk-cd": "...",
}))

有关防护机制、会话握手以及完整航线表的详细信息,请参阅 docs/internal-api-spec.md。

API 参考

WizzAir 类
构造函数
WizzAir(transport: Optional[Transport] = None)

创建一个新的 Wizz Air 客户端实例。

参数:

  • transport(Transport,可选):注入自定义传输,例如包装默认传输的 CachingTransport、带有 kasada_headers 的 WizzairTransport,或用于测试的固定数据传输。默认使用全新的 WizzairTransport。

示例:

# Defaults
client = WizzAir()

# With caching for the 650 KB network metadata
from flywizz import CachingTransport, WizzairTransport
client = WizzAir(CachingTransport(WizzairTransport(), ttl=3600))
方法
方法端点提供的内容
get_network(language="en-gb")asset/map所有站点及其坐标、货币和连接信息
get_destinations(origin, direct_only=True)派生从 origin 可到达的站点
explore_by_country(origin)派生按国家/地区代码分组的目的地
validate_route(origin, destination)派生Wizz Air 是否运营这条直飞航线
get_flight_dates(origin, destination, date_from, date_to)search/flightDates运营日期,不含价格,成本很低
get_flight_dates_multi(origin, destinations, date_from, date_to)search/FlightDatesMultiArrival每次请求最多查询 5 个目的地的运营日期,分批处理
get_connecting_flight_dates(origin, destination, date_from, date_to)search/dohopFlightDates包含一次中转的运营日期
get_timetable(params)search/timetableV2每天的最低票价及所有出发航班
get_return_timetable(params)search/timetableV2一次调用查询去程和回程
get_fare_chart(params)asset/farechart目标日期前后的价格区间
get_availability(params)search/search票价套餐和销售密钥。受机器人防护限制
cheapest_in_month(origin, destination, month)派生一个日历月内最便宜的日期
cheapest_weekend(origin, destination, months_ahead=3)派生最便宜的周五至周日或周五至周一往返行程
explore_with_fares(origin, date_from, date_to, limit=None)派生所有目的地及其最低票价
get_flight_status(carrier_code, flight_number, date=None)asset/flightinformation单个航班号的实时状态
get_currencies()asset/currencies支持的 ISO 4217 代码
get_countries()asset/country含欧盟 / 申根标记的国家/地区
get_cultures()asset/cultures网站语言及其货币
get_service_fees(currencies=None)asset/serviceFees已公布的行李、座位和改签费用
get_wdc_prices()asset/wdcPrice折扣俱乐部等级和最低折扣

AsyncWizzAir 上也提供所有方法,且签名相同。

TimetableSearch 类

时刻表搜索的参数。

TimetableSearch(
    origin: str,
    destination: str,
    date_from: datetime,
    date_to: datetime,
    return_date_from: Optional[datetime] = None,
    return_date_to: Optional[datetime] = None,
    adults: int = 1,
    children: int = 0,
    infants: int = 0,
    price_type: str = "regular",
)

参数:

  • origin(str):出发站的 IATA 代码(例如 "BUD")
  • destination(str):到达站的 IATA 代码(例如 "LTN")
  • date_from(datetime):去程出发日期范围的起始时间
  • date_to(datetime):去程出发日期范围的结束时间
  • return_date_from / return_date_to(datetime,可选):回程日期范围。 get_return_timetable() 必须同时提供两者
  • adults / children / infants(int):乘客人数。至少需要一名成人
  • price_type(str):"regular" 或 "wdc",用于指定 Wizz Discount Club 价格
FareChartSearch 类

价格区间的参数。

FareChartSearch(
    origin: str,
    destination: str,
    date: datetime,
    day_interval: int = 3,
    adults: int = 1,
    children: int = 0,
    infants: int = 0,
    price_type: str = "regular",
)

day_interval 是 date 前后的半区间,且必须至少为 3,因此默认会返回七天的数据。较小的值会被上游拒绝,并返回 DayIntervalMustBeGreaterOrEqualTo3。

AvailabilitySearch 类

受机器人防护限制的可用性调用参数。

AvailabilitySearch(
    origin: str,
    destination: str,
    departure_date: datetime,
    return_date: Optional[datetime] = None,
    wdc: bool = True,
    is_flight_change: bool = False,
    adults: int = 1,
    children: int = 0,
    infants: int = 0,
)

数据模型

Price

表示 Wizz Air 报告的金额。

属性:

  • amount(float):金额
  • currency(str):ISO 4217 代码,始终为出发站的货币
  • exchanged_amount(Optional[float]):SPA 的客户端转换钩子。 匿名会话中保持为 None
  • exchanged_currency(Optional[str]):exchanged_amount 的货币
TimetableEntry

航线的一个运营日及其最低票价。

属性:

  • departure_station(str)、arrival_station(str):IATA 代码
  • departure_date(datetime):运营日
  • price(Optional[Price]):当天的最低票价;售罄时为 None
  • original_price(Optional[Price]):折扣前价格
  • departures(list[Departure]):当天的所有航班
  • price_type(Optional[str]):有可售座位时为 "price"
  • has_mac_flight(bool):该航线包含都市区备选机场
  • applied_coupon_code(Optional[str])
Departure

属性:

  • departure(datetime):出发时间
  • is_cheapest_of_the_day(bool):该航班的 price 为当天最低价
FlightDate

get_connecting_flight_dates() 返回的一个运营日。

属性:

  • date(datetime):运营日
  • stops(int):直飞为 0,中转为 1 或更高
  • is_direct(bool):属性;当 stops 为 0 时为 true
FareChartEntry

价格条中的一天。

属性:

  • departure_station(str)、arrival_station(str):IATA 代码
  • day(datetime):日期
  • price(Optional[Price]):当天的最低价格
  • class_of_service(Optional[str]):该报价所属的预订舱位
  • price_type(Optional[str])、has_mac_flight(bool)
Station

Wizz Air 实时航线网络中的机场。由探索方法返回。

属性:

  • iata(str):IATA 机场代码
  • name(str):机场名称
  • country_code(str):大写 ISO2 国家代码(例如 "HU"、"GB")
  • country_name(str):国家名称
  • currency_code(str):当地货币。此处出发的票价以该货币计价
  • latitude(float)、longitude(float):坐标
  • mac(Optional[str]):都市区代码(例如 "LON")
  • aliases(list[str]):别名
  • categories(list[int]):Wizz Air 分配的营销类别
  • rank(Optional[int)、is_fake_station(bool)
  • connections(list[Connection]):该机场可飞往的所有地点

辅助方法:destinations(direct_only=True) 仅返回 IATA 代码。

Connection

属性:

  • iata(str):目的地机场代码
  • is_direct(bool):Wizz Air 直飞航班。通常需要关注此标记
  • is_connected(bool):自助中转,而非直飞航班
  • is_domestic(bool)、is_new(bool)
  • operation_start_date(Optional[datetime]):航线开通时间
FlightStatus

来自航班信息端点的一个运营航班。

属性:

  • flight_id(int)、carrier_code(str)、flight_number(int)
  • departure_airport(str)、arrival_airport(str)
  • original_departure_airport / original_arrival_airport(Optional[str]): 航班备降时,此处的机场与实际机场不同
  • operation_day(Optional[datetime])
  • scheduled_departure / scheduled_arrival(Optional[datetime])
  • op_suffix(Optional[str])
DestinationFare

由 explore_with_fares() 返回。将可到达的目的地与其最低抽样票价配对;如果价格探测返回了票价,则包含该票价。

属性:

  • station(Station):目的地
  • price(Optional[Price]):时间范围内的最低票价;如果该航线属于航线网络,但未返回有价座位,则为 None
  • departure_date(Optional[datetime]):该票价对应的日期

示例

一个月内价格最低的日期
from datetime import datetime
from flywizz import WizzAir

client = WizzAir()
cheapest = client.cheapest_in_month("BUD", "LTN", datetime(2026, 11, 1))

if cheapest:
    print(f"{cheapest.departure_date.date()}: "
          f"{cheapest.price.amount} {cheapest.price.currency}")
前后一天的票价是否更低?
from datetime import datetime
from flywizz import WizzAir, FareChartSearch

client = WizzAir()
strip = client.get_fare_chart(
    FareChartSearch(origin="BUD", destination="LTN",
                    date=datetime(2026, 11, 10), day_interval=3)
)

for day in strip:
    price = f"{day.price.amount:.0f} {day.price.currency}" if day.price else "-"
    print(f"{day.day.date()}  {price}")
未来三个月内票价最低的周末
from flywizz import WizzAir

client = WizzAir()
weekend = client.cheapest_weekend("BUD", "LTN", months_ahead=3)

if weekend:
    out, back = weekend
    total = out.price.amount + back.price.amount
    print(f"{out.departure_date.date()} -> {back.departure_date.date()}: "
          f"{total} {out.price.currency}")
Discount Club 票价
from datetime import datetime, timedelta
from flywizz import WizzAir, TimetableSearch

client = WizzAir()
wdc = client.get_timetable(
    TimetableSearch(
        origin="BUD", destination="LTN",
        date_from=datetime.now() + timedelta(days=30),
        date_to=datetime.now() + timedelta(days=45),
        price_type="wdc",
    )
)
实时航班状态
from flywizz import WizzAir

client = WizzAir()
for leg in client.get_flight_status("W6", "6201"):
    print(f"{leg.operation_day.date()} {leg.departure_airport} -> {leg.arrival_airport}")

承运人代码为 AOC 前缀:W6(匈牙利)、W4(马耳他)、W9(英国)。

错误处理
from flywizz import BotGateError, ValidationError, WizzairException

try:
    entries = client.get_timetable(search)
    if not entries:
        print("No flights found for the given criteria")
except ValidationError as e:
    print(f"Wizz Air rejected the request: {e.codes}")
except BotGateError:
    print("This endpoint needs a browser session")
except WizzairException as e:
    print(f"Wizz Air API error: {e}")

ValidationError.codes 包含 Wizz Air 自己的验证代码,用于标识其认为有问题的字段。空列表表示 API 未返回任何匹配结果;这绝不表示发生了故障。

一次检查多条航线

get_flight_dates_multi() 可在每次请求中查询多个目的地的航班时刻,比循环调用 get_flight_dates() 便宜五倍:

from datetime import datetime, timedelta
from flywizz import WizzAir, WizzairTransport, CachingTransport

client = WizzAir(CachingTransport(WizzairTransport()))
start = datetime.now() + timedelta(days=30)

destinations = [s.iata for s in client.get_destinations("BUD")]
dates = client.get_flight_dates_multi("BUD", destinations, start, start + timedelta(days=30))

for iata, days in dates.items():
    print(f"{iata}: {len(days)} operating days")

你查询的每个目的地都会出现在结果中。在指定时间范围内没有航班的航线会对应一个空列表,而不是直接缺失,因此无需担心忘记检查成员关系。

该端点每次请求最多查询五个目的地,若列表过长,会直接返回空结果而不是报错。Flywizz 会为你分批处理,因此传入列表没有实际数量限制。

票价则有所不同:目前没有批量查询票价的接口,因此 explore_with_fares() 仍需为每个目的地调用一次。

需要中转的航班

validate_route() 和 get_flight_dates() 仅覆盖直飞航班,因此任一方法返回 False,并不意味着你无法飞往该目的地。 get_connecting_flight_dates() 也会查询需要换乘飞机的航班:

from datetime import datetime, timedelta
from flywizz import WizzAir

client = WizzAir()
start = datetime.now() + timedelta(days=30)

client.validate_route("KTW", "LIS")   # False - no direct flight

for day in client.get_connecting_flight_dates("KTW", "LIS", start, start + timedelta(days=30)):
    print(day.date.date(), "direct" if day.is_direct else f"{day.stops} stop(s)")

如果一条航线只有部分日期有直飞航班,两种航班会交错返回,因此你可以查看哪些日期需要中转。只需要直飞航班时,可按 is_direct 进行筛选。

探索模式

探索模式回答“从这里实际可以飞到哪里?”这个问题。它会读取 Wizz Air 的实时航线网络元数据,并提供从任一机场可到达的目的地,还可按条件分组,或与指定日期范围内的最低票价合并。

以下所有方法均可用于 WizzAir 和 AsyncWizzAir。

列出所有目的地
for station in client.get_destinations("BUD"):
    print(f"{station.iata} {station.name} ({station.country_code})")

传入 direct_only=False 可包含自行中转的航线。

对目的地分组
by_country = client.explore_by_country("BUD")

print(f"BUD flies to {len(by_country)} countries")
for country, stations in sorted(by_country.items()):
    codes = ", ".join(s.iata for s in stations)
    print(f"  {country}: {codes}")

国家代码为大写 ISO2 代码。

检查单条航线
client.validate_route("BUD", "LTN")  # True
查询目的地及其最低票价

explore_with_fares() 会将航线网络中的目的地与时刻表查询结果合并,因此每个目的地都会返回其最低 Price(如果该航线在指定日期范围内没有返回可售座位,则为 None)。

Wizz Air 没有“任意目的地”搜索,因此每个目的地都需要调用一次。遍历时使用 limit,并将传输层封装在 CachingTransport 中。

from datetime import datetime, timedelta

start = datetime.now() + timedelta(days=30)
end = start + timedelta(days=14)

results = client.explore_with_fares("BUD", start, end, limit=20)

priced = [d for d in results if d.price is not None]
for d in sorted(priced, key=lambda d: d.price.amount)[:10]:
    print(f"{d.station.iata} {d.station.name}: "
          f"{d.price.amount} {d.price.currency}")

所有目的地的票价都以出发地的货币表示,因此可以直接比较。

异步用法

AsyncWizzAir 提供了所有探索方法的对应版本,且 explore_with_fares() 会并发发起请求:

import asyncio
from datetime import datetime, timedelta
from flywizz import AsyncWizzAir

async def main():
    async with AsyncWizzAir() as client:
        results = await client.explore_with_fares(
            "BUD",
            datetime.now() + timedelta(days=30),
            datetime.now() + timedelta(days=45),
            limit=20,
            concurrency=5,
        )
        print(f"{sum(1 for r in results if r.price)} priced destinations")

asyncio.run(main())

如果连续调用多个探索方法,请将传输层封装在 CachingTransport 中,以便只获取一次航线网络元数据并重复使用。

与 Claude、Cursor 及其他 MCP 客户端搭配使用

重要

只需运行两条命令即可完成:

uv tool install "Flywizz[mcp]"
claude mcp add flywizz flywizz-mcp

现在,你的 Agent 就能用自然语言搜索 Wizz Air 航班。无需 API 密钥,也无需账号。

Flywizz 提供可选的 Model Context Protocol 服务,让你的 Agent 能根据自然语言提示词搜索 Wizz Air 票价,例如 “11 月从布达佩斯飞往伦敦,哪天最便宜?” 或 “12 月第一周从布达佩斯可以飞到哪里?”。

快速入门

1. 安装带有 MCP 扩展的 Flywizz:

uv tool install "Flywizz[mcp]"

也可以使用 pip:

pipx install "Flywizz[mcp]"

这会在你的 PATH 中安装一个 flywizz-mcp 控制台脚本。

2. 将其添加到你的 Agent:

Claude Code(单行命令):

claude mcp add flywizz flywizz-mcp

Claude Desktop:在 macOS 上打开 ~/Library/Application Support/Claude/claude_desktop_config.json(或在 Windows 上打开 %APPDATA%\Claude\claude_desktop_config.json),并添加:

{
  "mcpServers": {
    "flywizz": {
      "command": "flywizz-mcp"
    }
  }
}

然后重启 Claude Desktop。

Cursor:前往 Settings → MCP → Add new server,名称填写 flywizz,命令填写 flywizz-mcp。

3. 试着使用。 向你的 Agent 提问:

“11 月从布达佩斯飞往伦敦卢顿,哪天最便宜?”

Agent 应调用 cheapest_day,并传入 origin="BUD"、 destination="LTN"、month="2026-11-01",然后报告日期和票价。

货币

没有货币设置,因为 Wizz Air 没有此类设置。每个工具都会以出发站的当地货币返回价格,并同时报告货币代码。请告诉你的 Agent 报价时使用返回的货币,而不要默认使用欧元。

暴露的工具

该服务提供五个精选工具,方便 Agent 可靠地选择:

  • find_fares,适用于“11 月从 BUD 飞往 LTN 多少钱”这类问题,提供完整的逐日明细和每个出发时间
  • cheapest_day,适用于“这个月从 X 飞往 Y,哪天最便宜”这类问题
  • price_around,适用于“10 日前后一天的票价是否更便宜”这类问题
  • explore_destinations,适用于“从 X 可以飞往哪些国家”这类问题
  • flight_status,适用于“W6 6201 什么时候运营”这类问题

无需 API 密钥、账户或速率限制设置。该服务会在多次调用间复用单个缓存的 WizzAir 客户端,因此每个进程只会获取一次网络元数据。

有机器人验证门槛的 search/search 接口不会对外开放:它无法在无头进程中运行,而一个总是失败的 Agent 工具还不如没有。

API 特性

上游 API 的这些特性决定了此 SDK 能提供和不能提供的功能。Flywizz 会为你处理这些情况;了解它们有助于解释某些调用的行为。

行为
国家代码大写的 ISO 3166-1 alpha-2 代码。小写代码会被拒绝。
身份验证匿名,但每个请求都需要进行会话握手,并使用会在调用之间轮换的 CSRF 令牌。
货币由出发站决定,无法协商。请在客户端进行转换。
低价机票搜索没有“任意目的地”接口。explore_with_fares() 会针对每个目的地分别发起一次调用。
可查询票价的接口timetableV2 和 farechart 是开放的;search/search 有访问门槛。
分页无。每个响应都包含完整结果,因此 asset/map 有 650 KB。
时间窗口限制时刻表窗口最多可跨越 42 天;班次计划窗口最多可跨越 62 天。
机器人防护Kasada 永久应用于四个端点。

缓存

asset/map 有 650 KB,且很少变更。如果你多次调用它,请对传输层进行包装:

from flywizz import WizzAir, WizzairTransport, CachingTransport

client = WizzAir(CachingTransport(WizzairTransport(), ttl=3600))

异步代码需要使用异步包装器。两者不可互换:混用时会引发 TypeError,而不是缓存协程。

from flywizz import AsyncWizzAir, AsyncWizzairTransport, AsyncCachingTransport

client = AsyncWizzAir(AsyncCachingTransport(AsyncWizzairTransport(), ttl=3600))

这两种包装器都不会缓存 POST 请求,因此票价始终是最新的。调用 invalidate() 可提前清除缓存。

速率限制

SDK 会通过指数退避机制重试网络错误和 5xx 响应,最多重试 4 次。它有意不会重试 429:在此 API 中,这表示 Kasada 机器人验证门槛,而不是服务过载;重试只会增加负载,仍然无法成功。

Wizz Air 的 API 无需身份验证,但它并不属于你。请文明使用:缓存网络元数据,适度控制 explore_with_fares() 的并行请求数量,不要在票价实际变化之前频繁查询。

贡献

这是一个开源项目。欢迎贡献 — 请参阅 CONTRIBUTING.md。面向 Agent 的架构说明见 AGENTS.md,关于上游 API 的所有已知信息均见 docs/internal-api-spec.md。

免责声明

这是一个非官方 API 封装,与 Wizz Air 没有关联。它只会向该航空公司官网使用的公开端点发送只读请求。使用风险自负,并请确保遵守 Wizz Air 的服务条款。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。