mcpField
G
geonames-mcp-server

geonames-mcp-server

远程服务
github.com/cyanheads地图与出行11 周前更新
代码仓库

关于 geonames-mcp-server

geonames-mcp-server 为开发者和地理位置研究提供 GeoNames 地名及地理参考数据。工具涵盖地点搜索、行政层级、反向地理编码、邮政编码和国家信息。

主要功能

  1. 01按地理条件搜索地点
  2. 02获取地点详细记录
  3. 03浏览上级与下级层级
  4. 04对坐标进行反向地理编码
  5. 05查找邮政编码和国家信息

适用场景

  1. 01查找某国或边界框内的地点
  2. 02识别坐标对应的行政区域
  3. 03探索地点的上级或下级区域
  4. 04查询邮政编码或国家详情

MCP 相关

通过 STDIO 或 Streamable HTTP 向 MCP 客户端提供八种 GeoNames 工具,也提供公开托管的 MCP 服务。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://geonames.caseyjhand.com/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

安装包
npm@cyanheads/geonames-mcp-server0.1.3

工具

8
参数类型必填说明
limitinteger否要返回的最大条目数,范围为 1 到 100。默认值为 10。
matchenum否查询的匹配方式。name_required(默认值)要求地点名称中至少包含一个查询词,其他词可匹配国家/地区或行政区名称;any_field 允许每个词匹配这些字段中的任意字段;exact_name 要求完整名称完全匹配,也包括别名和历史名称;name_prefix 匹配以查询词开头的名称。必须提供 query。不区分大小写。name_required | any_field | exact_name | name_prefix
querystring否要搜索的地点名称,例如 Springfield 或 "Berlin, Germany",最多 200 个字符。match 设置比较方式。省略此项可仅按过滤条件搜索,此时 countries、featureClasses、featureCodes 或 boundingBox 中至少需要提供一项(仅提供 cities 不够)。
citiesenum否仅保留人口至少为 1,000(cities1000)、5,000(cities5000)或 15,000(cities15000)的居住地(类别 P),以及行政区驻地:即 GeoNames 的城市级别。与 featureClasses 或 featureCodes 同时使用时,只列出类别 P 或类别 P 的代码(例如 PPLC)。不区分大小写。cities1000 | cities5000 | cities15000
offsetinteger否分页时,在首个返回条目之前跳过的条目数,范围为 0 到 5000。默认值为 0。
orderByenum否结果排序方式:relevance(GeoNames 默认值)或 population(按人口从高到低)。不区分大小写。relevance | population
countriesarray否ISO 3166-1 国家/地区代码,最多 10 个,可用列表或逗号分隔的字符串表示:alpha-2(US、GB、DE)、alpha-3(USA、GBR、DEU)或三位数字代码(840、826、276);每个代码都会以 alpha-2 形式发送给 GeoNames。不区分大小写;接受 UK 作为 GB。
boundingBoxobject否仅保留此边界框内的地点。边界框不能跨越 180° 经线;此类区域需拆分为两次搜索。
boundingBox.eastnumber是东侧经度边界,单位为十进制度,范围为 -180 到 180。
boundingBox.westnumber是西側经度边界,单位为十进制度,范围为 -180 到 180;小于 east。
boundingBox.northnumber是北纬边界,十进制度,范围为 -90 至 90。
boundingBox.southnumber是南纬边界,十进制度,范围为 -90 至 90;低于北纬边界。
featureCodesarray否GeoNames 要素代码(PPLC 首都、ADM1 州、MT 山、AIRP 机场),最多 20 个,可用列表或逗号分隔的字符串。区分大小写;类前缀(P.PPLC)会被移除。每个代码都隐含其类别;同时指定 featureClasses 时,两份列表取交集,因此 featureClasses 必须恰好列出这些代码对应的类别。geonames_list_reference 的 topic feature_codes 列出所有代码。
featureClassesarray否GeoNames 要素类别:A 行政区划、H 水域、L 区域、P 居民地、R 道路、S 地点和建筑物、T 地形、U 海底、V 植被。可用列表或逗号分隔的字符串;不区分大小写。同时指定 featureCodes 时,两份列表取交集:请准确列出这些代码对应的类别,或仅传入 featureCodes。
geonamesUsernamestring否你自己的 GeoNames 用户名,这样调用会消耗该账户的免费额度,而不是服务器的额度。省略此参数则使用服务器账户。该账户必须在 geonames.org 账户页面启用免费 Web 服务。

详细介绍

@cyanheads/geonames-mcp-server

通过 MCP 搜索 GeoNames 地点、遍历行政层级、反向地理编码、获取邮政编码和国家信息。支持 STDIO 或 Streamable HTTP。

8 个工具

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework

公共托管服务: https://geonames.caseyjhand.com/mcp


概述

GeoNames 地名数据库收录了全球 1,300 多万个地点,并将它们按行政层级从大洲到街区关联成树形结构。可搜索地点、读取完整记录、遍历行政层级、对坐标进行反向地理编码、查询邮政编码并读取国家信息。

工具
工具描述
geonames_search_places按名称、国家、行政区划代码、要素类别或代码、人口等级和边界框搜索地点
geonames_get_place获取单个 geonameId 的完整记录:行政区划链、时区、海拔、别名、邮政编码和外部标识符
geonames_get_hierarchy获取从地球和大洲到该地理要素的父级层级链
geonames_get_children获取该地理要素在行政、旅游或属地树中的直接子级
geonames_reverse_geocode获取坐标所在的国家和行政区划(或海洋),以及最近的地点或地理要素,并可选择获取时区
geonames_find_postal_codes按邮政编码、地点名称或坐标附近查找邮政编码
geonames_get_countries获取国家信息:ISO 和 FIPS 代码、geonameId、首都、人口、面积、语言、货币和邮政编码格式
geonames_list_reference获取要素类别、要素代码以及提供邮政编码数据的国家

功能参考

geonames_search_places 工具
  • query(最多 200 个字符)按 match 进行匹配:name_required(默认)、any_field、exact_name 或 name_prefix;筛选条件包括 countries(最多 10 个,使用 ISO alpha-2、alpha-3 或数字代码)、adminCode1–adminCode4、featureClasses、featureCodes(最多 20 个)、cities(cities1000 / cities5000 / cities15000)和 boundingBox。调用时必须提供 query 或 countries、featureClasses、featureCodes、boundingBox 之一
  • 列出的国家也会返回 GeoNames 文件中归属于该国的领地(GB 还会返回 IM、GG、JE 和 GS),每个地点的 countryCode 都是其自身的代码;如果页面上有 countries 未列出的国家,通知中会注明
  • adminCode1–adminCode4 按 GeoNames 行政区划代码搜索行政区,geonames_get_place 和 geonames_reverse_geocode 会在 adminLevels 中返回该代码(countries: ["US"], adminCode1: "TX", featureCodes: ["AIRP"] 可列出德克萨斯州机场)。代码不区分大小写,前导零也计入代码(02,而非 2);它们不是 ISO 3166-2 代码或邮政编码(巴伐利亚的代码是 02,而非 BY)。每次搜索必须指定一个国家,级别 2–4 还必须提供 adminCode1
  • limit 为 1–1,000(默认 10),每次调用最多返回 100 条;有更多结果时会发出通知;offset 为 0–5000,orderBy 为 relevance 或 population(省略时使用 GeoNames 自身的顺序);返回 totalCount、effectiveQuery 和 nextOffset
  • featureClasses、featureCodes 和 cities(仅限类别 P)会相互取交集:同时设置两个列表时,每个代码的类别必须在列表中,且每个列出的类别都必须有对应代码;与 cities 同时使用时,仅可指定类别 P 及其代码。其他组合会以 feature_filter_mismatch 失败
  • 在发出任何请求前,若出现 query_or_filter_required、query_required、unknown_country_code、unknown_feature_code、admin_code_unscoped、feature_filter_mismatch 或 invalid_bounding_box,调用会失败

geonames_get_place 工具
  • 提供一个 geonameId;未知 ID 会返回 found: false 和 guidance
  • 返回 1–5 级 adminLevels(代码、名称、geonameId)、时区(1 月 1 日和 7 月 1 日的 UTC 偏移量)、边界框、记录海拔和 DEM 海拔、人口、Wikipedia URL、alternateNames、postalCodes、links 和 identifiers(IATA、ICAO、FAA、Transport Canada、UN/LOCODE、Wikidata)
  • nameLanguages(最多 20 个标签;zh 也会匹配 zh-CN)仅筛选 alternateNames

geonames_get_hierarchy 工具
  • 提供一个 geonameId;chain 从地球及其大洲开始,经过国家和行政区划,直至该地理要素,并跳过其不隶属的级别
  • 每个级别都包含 geonameId、要素类别和代码、国家及一级行政区代码、坐标和人口;未知 ID 会返回 found: false

geonames_get_children 工具
  • hierarchy:administrative(默认)、tourism(岛屿、海岸及其市镇;几乎都在西班牙)或 dependency(一国的属地)。子级大多为行政区划(类别 A)和人口聚居地(类别 P);大洲和海岸属于类别 L,岛屿属于类别 T
  • 如果该地理要素没有 tourism 或 dependency 树,GeoNames 会针对这两种请求返回行政子级。工具会比较两个列表;如果相同,则设置 sameAsAdministrative 并发出通知;即使是实际的树结构,两者也可能相同,因此仍会保留这些行
  • 每个父级最多获取 1,000 个子级,并缓存结果,因此 nameContains、limit(1–1,000,默认 100,每次调用最多 500 条;有更多结果时会发出通知)和 offset 不会额外消耗额度;GeoNames 列出的结果更多时会发出通知
  • 未知 ID 会返回 found: false;叶节点会返回空的 children 列表,并附带通知

geonames_reverse_geocode 工具
  • lat / lng 可解析为 country 和 adminLevels(最多到 ADM5,每级均包含 geonameId,以及存在时的 ISO 3166-2 行政区划代码);若位于近海,则解析为 ocean
  • coastalBufferKm(0–50,默认值为 0)会在点位不属于任何国家时,匹配该距离内最近的国家,适用于港口、码头和海岸线点位;缓冲匹配会携带 country.distanceInKm,而附近地点和时区查询仍使用精确点位
  • nearby 会列出最近的有人居住地点(nearbyLimit 为 0–1,000,默认值为 5;每次调用最多返回 50 个,半径内还有更多地点时会显示提示;radiusKm 最大为 300,默认值为 20;可选的 cities 等级);若设置了 featureClasses / featureCodes,则会列出该类型的最近地理要素;nearbyKind 会说明类型。featureClasses 和 featureCodes 的筛选条件取交集
  • 发起任何请求前,若存在筛选条件冲突则会失败,并返回 conflicting_filters(cities 与地理要素筛选条件同时使用)、unknown_feature_code 或 feature_filter_mismatch(代码所属类别未列出,或列出的类别没有对应代码)
  • includeTimezone 会添加 IANA 标识符、UTC 时差、本地时间、日出和日落(仅近海位置会报告时差;两项日期均按标准时差报告)

geonames_find_postal_codes 工具
  • mode:code(需要 postalCode)、place_name(需要 placeName)或 nearby(需要 lat 和 lng;radiusKm 最大为 30,默认值为 10);缺少必填字段,或在 nearby 模式下设置 countries 或大于 0 的 offset,都会以 mode_fields_mismatch 失败
  • countries 筛选条件适用于 code 和 place_name,可使用 ISO alpha-2、alpha-3 或数字代码(共 250 个国家代码,另含已废弃的 AN、CS 和 YU;其他代码,例如 EL 或 ZZZ,会以 unknown_country_code 失败)
  • 列出的国家还会返回 GeoNames 归属该国的领土文件(GB 还会返回 IM、JE 和 GG),每个邮政编码均使用其自身的 countryCode;提示会列出页面上未包含在 countries 中的国家
  • limit 为 1–1,000(默认值为 10),每次调用最多返回 100 条;若仍有更多结果,会显示提示;code 和 place_name 支持分页,offset 为 0–5000,若 5000 以内仍有更多行,则返回 nextOffset;GeoNames 不提供总数,因此 truncated 表示是否还有更多行,而不是其数量
  • 覆盖 122 个国家;爱尔兰仅返回 Eircode 路由键,马耳他仅返回字母前缀

geonames_get_countries 工具
  • 可按 ISO alpha-2、alpha-3 或数字代码筛选,最多指定 250 个 countries;也可按 continent、nameContains 筛选,或不设筛选条件以获取全部 250 个国家;limit 为 1–1,000(默认值为 50),每次调用最多返回 250 条,并支持 offset
  • 每行包含 ISO 和 FIPS 代码、geonameId(可作为 geonames_get_children 的起始点)、首都、人口、面积、大洲、语言、货币、邮政编码格式及本土边界框;未知代码会列入 notFound

geonames_list_reference 工具
  • topic:feature_classes(9 项)、feature_codes(684 项,可按 featureClass 筛选)或 postal_countries(122 项,包含各国的邮政编码范围和数量);将 featureClass 与其他主题搭配使用会以 filter_not_applicable 失败
  • nameContains、limit 为 1–1,000(默认值为 100,每次调用最多返回 700 条,足以涵盖所有主题),以及 offset;地理要素类别和代码已打包,不消耗额度

功能

基于 @cyanheads/mcp-ts-core 构建:支持 stdio 和 Streamable HTTP 传输、可插拔身份验证(none / jwt / oauth)、可切换存储(in-memory、filesystem、Supabase、Cloudflare KV/R2/D1)、结构化日志记录,以及可选的 OpenTelemetry 跟踪。

GeoNames 专属功能:

  • 数据来自 GeoNames,采用 CC BY 4.0 许可:传递结果时必须注明 GeoNames。此服务独立于 GeoNames。
  • 每个账户每小时限制 1,000 次请求;超出配额后会进入冷却期,且仅影响该账户
  • 响应在不同调用方之间共享缓存:搜索缓存 1 小时,时区不缓存,其他所有查询缓存 24 小时。命中缓存不会消耗配额
  • 宽松的输入格式:支持 alpha-3 和数字国家代码、用 UK 表示 GB,以及用 FL 表示 LI。服务还接受其公开的 schema 未列出的格式(以逗号连接的列表字符串、枚举值和代码的其他大小写形式、P.PPLC 类别前缀、GeoNames.org URL 或表示 geonameId 的 JSON 整数、邮政编码中的多余空格、用 username 表示 geonamesUsername),但工具描述只提供规范格式,因为会验证参数的客户端会拒绝其他格式

适合 Agent 的输出:

  • 类型化失败原因会区分调用方账户(caller_account_rejected)和运营方账户(server_account_rejected)、已耗尽的配额(quota_exhausted,其 data.window 为 hour、day、week 或 local),以及被 GeoNames 拒绝的值(upstream_rejected_parameter)
  • 未知 ID 会返回 found: false 和 guidance,而不是错误;空白或不完整的分页结果会附带 notice,指出下一个 offset 或应放宽的筛选条件
  • 对于 GeoNames 用来表示“无”的占位值(population: 0、空管理区名称、geonameId: 0),会直接丢弃,不会当作事实报告

已知限制:

  • 共享部署会共享配额。 所有未使用自有用户名的调用方共享服务端账户每小时 1,000 次的配额。一阵反向地理编码请求(每次最多消耗 7 个配额)就可能耗尽配额,而且 GeoNames 不会说明配额何时重置。
  • 免费层级的搜索和邮政编码搜索最多可查到 offset 5000,每页最多 100 行,因此最多可访问结果集中的第 5,100 行。附近邮政编码查询不支持分页。
  • 每个父项最多返回 1,000 个子项。
  • 邮政编码数据涵盖 122 个国家。 爱尔兰和马耳他只返回代码前缀。美国的附近查询会将第一行放在查询点,而不是 ZIP 代码质心处。
  • 附近查询半径上限为 300 km(地点和地理要素)和 30 km(邮政编码),这是 GeoNames 免费层级的限制。
  • 边界框不能跨越 180° 经线。 此类区域需拆分成两次搜索。
  • 沿海点在未设置缓冲距离时可能会解析到海洋。 默认情况下,只有包含该点的国家才会匹配,因此,刚好位于海港或海岸线轮廓之外的点会返回海洋,而附近地点则位于陆地上。设置 coastalBufferKm(最高 50),即可匹配最近的国家;在海峡中,两岸任一侧都有可能匹配。
  • 微型国家和飞地可能会解析到周边国家。 位于梵蒂冈城内的点会返回意大利。
  • 最近的有人居住地点中可能包含城区和历史地点。 在人口稠密的城市中,最近的结果通常是 PPLX 城区或 PPLH 前行政区;每行的 featureCode 会标明类型,cities 则会将结果限制在超过指定人口级别的地点。
  • 近海时区只提供时差。 海上没有 IANA ID、本地时间、日出或日落信息,两个日期的时差也都是标准时差(公海不实行夏令时)。
  • exact_name 会匹配别名和历史名称,因此结果的 name 可能与查询内容不同:查询“Springfield”可能会返回密苏里州的 Plattsburg 或 Palmyra。
  • 数据由社区编辑,并按“原样”提供。 许多地理要素没有记录人口或海拔。

入门指南

公共托管实例

可使用公共实例:https://geonames.caseyjhand.com/mcp,无需安装。通过 Streamable HTTP 将任意 MCP 客户端指向该实例:

{
  "mcpServers": {
    "geonames-mcp-server": {
      "type": "streamable-http",
      "url": "https://geonames.caseyjhand.com/mcp"
    }
  }
}

如果调用未传入 geonamesUsername,则会使用托管实例的 GeoNames 账户,该账户每小时 1,000 个额度由所有此类调用者共享。传入你自己的 geonamesUsername,即可使用自己账户的额度。

自托管 / 本地

将以下内容添加到 MCP 客户端配置文件中,并用你的 GeoNames 用户名替换占位符。

{
  "mcpServers": {
    "geonames-mcp-server": {
      "type": "stdio",
      "command": "bunx",
      "args": ["@cyanheads/geonames-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info",
        "GEONAMES_USERNAME": "your_geonames_username"
      }
    }
  }
}

或者使用 npx(无需 Bun):

{
  "mcpServers": {
    "geonames-mcp-server": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@cyanheads/geonames-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info",
        "GEONAMES_USERNAME": "your_geonames_username"
      }
    }
  }
}

或者使用 Docker:

{
  "mcpServers": {
    "geonames-mcp-server": {
      "type": "stdio",
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "MCP_TRANSPORT_TYPE=stdio",
        "-e", "GEONAMES_USERNAME=your_geonames_username",
        "ghcr.io/cyanheads/geonames-mcp-server:latest"
      ]
    }
  }
}

如需使用 Streamable HTTP,请设置传输方式并启动服务:

MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 GEONAMES_USERNAME=your_geonames_username bun run start:http
# Server listens at http://localhost:3010/mcp
前提条件
  • Bun v1.4.0 或更高版本(或 Node.js v24+)。
  • 一个免费的 GeoNames 账户,并在其账户页面启用免费 Web 服务。在启用前,GeoNames 会拒绝该账户发出的调用。
安装
  1. 克隆仓库:
git clone https://github.com/cyanheads/geonames-mcp-server.git
  1. 进入目录:
cd geonames-mcp-server
  1. 安装依赖:
bun install
  1. 配置环境:
cp .env.example .env
# edit .env and set GEONAMES_USERNAME

配置

每次 GeoNames 调用都会消耗 GeoNames 账户的额度。GEONAMES_USERNAME 是服务端账户:一个免费 GeoNames 账户,且已在账户页面启用免费 Web 服务。每个工具也都接受 geonamesUsername,因此,共享部署的调用者可以使用自己的账户,而不是服务端账户。两者都未设置时,调用会以 username_required 失败;只有 geonames_list_reference 中内置的 feature_classes 和 feature_codes 主题无需账户即可使用。

免费账户每小时有 1,000 个额度,每天有 10,000 个额度。缓存查询不会消耗额度。

工具每次调用的额度
geonames_search_places、geonames_get_place、geonames_get_hierarchy1
geonames_get_children1;当父级的行政列表未缓存时,tourism 或 dependency 为 2
geonames_find_postal_codes1(code、place_name;若未命中且传入 countries,则除非邮政覆盖列表已缓存,否则额外消耗 1);2(nearby)
geonames_reverse_geocode包含性查询为 1(无论是否设置 coastalBufferKm);查找最近的有人居住地点额外消耗 3,查找最近的地理要素额外消耗 4;若没有国家包含该点或位于缓冲区内,则查询海洋额外消耗 1;查询时区额外消耗 1
geonames_get_countries每天 1;国家表会被缓存
geonames_list_referencefeature_classes 和 feature_codes 为 0;postal_countries 每天 1
变量描述默认值
GEONAMES_USERNAME当调用未传入 geonamesUsername 时使用的 GeoNames 账户。在 geonames.org 免费注册;在账户页面启用免费 Web 服务。无
MCP_TRANSPORT_TYPE传输方式:stdio 或 http。stdio
MCP_HTTP_PORTHTTP 服务端口。3010
MCP_SESSION_MODEHTTP 会话模式:stateless、stateful 或 auto。.env.example 和 Docker 镜像设为 stateless。auto
MCP_AUTH_MODE身份验证:none、jwt 或 oauth。none
MCP_LOG_LEVEL日志级别(debug、info、warning、error 等)。info
LOGS_DIR日志文件目录(仅限 Node.js)。<app-root>/logs
LOG_TOOL_FAILURE_PAYLOADS记录每次失败的工具调用参数和结果,并按键名进行脱敏:GeoNames 用户名在 geonamesUsername、username 或任何包含 user 或 account 字样的相似键下都会被移除。false
STORAGE_PROVIDER_TYPE存储后端:in-memory、filesystem、supabase、cloudflare-kv/r2/d1。in-memory
OTEL_ENABLED启用 OpenTelemetry。false

完整的可选覆盖项列表请参阅 .env.example。

运行服务

本地开发
  • 构建并运行生产版本:

    # One-time build
    bun run rebuild
    
    # Run the built server
    bun run start:http
    # or
    bun run start:stdio
    
  • 运行检查和测试:

    bun run devcheck  # Lints, formats, type-checks, and more
    bun run test      # Runs the test suite
    

项目结构

目录用途
src/index.tscreateApp() 入口:服务说明、工具注册、GeoNames 服务的设置与拆除。
src/config使用 Zod 解析和验证 GEONAMES_USERNAME。
src/mcp-server/tools八个工具定义(*.tool.ts)及其共用的输入(shared-inputs.ts)。
src/services/geonamesGeoNames 服务:获取边界、状态映射、重试、按账户控制请求间隔、响应缓存、解析器,以及打包的要素代码表和国家代码表。
src/utils用于清理 format() 输出及 enrichment 页脚内容中由 GeoNames 编写或回显调用方内容的内联文本。
tests/单元测试和集成测试,结构与 src/ 对应。
docs/design.md设计说明:工具接口、凭证模型、上游行为。

开发指南

开发指南和架构规则请参阅 CLAUDE.md。简而言之:

  • 处理函数抛出异常,框架负责捕获——工具逻辑中不使用 try/catch
  • 使用 ctx.log 记录日志,使用 ctx.state 存储数据
  • 在 src/mcp-server/tools/definitions/index.ts 中注册新工具
  • 封装外部 API 调用:验证原始数据 → 规范化为领域类型 → 返回输出架构;绝不捏造缺失字段

贡献

欢迎提交问题。提交前请运行检查和测试:

bun run devcheck
bun run test

许可证

本项目采用 Apache 2.0 许可证。详情请参阅 LICENSE 文件。

评论

支持 Markdown

第一次接触 MCP?

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

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