关于 mcportal
mcportal 将韩国 data.go.kr 公共 API 规范转换为 OpenAPI 3.1 和 MCP 服务,适合需要自托管并控制 API 用量的用户。预设支持回放响应,可在开发时免密使用。
主要功能
- 01将 API 规范规范化为 OpenAPI 3.1
- 02把规范编译为 MCP 服务
- 03通过退避和缓存限制每日调用预算
- 04无需 API 密钥回放已记录响应
- 05通过命令行查看预设和配额状态
适用场景
- 01通过 MCP stdio 提供预设 API 服务
- 02无需凭据复现 API 响应和测试
- 03按每日预算限制 data.go.kr 实时调用
- 04将 API 规范转换为 MCP 服务
MCP 相关
可选的 MCP 层通过 stdio 向 MCP 客户端提供编译后的 data.go.kr API 服务包。支持免密回放模式,也可在预算保护下进行实时调用。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
MCPortal
连接 data.go.kr 与全球 MCP 生态系统的缺失桥梁。 MCPortal 将韩国公共 API 规范标准化为 OpenAPI 3.1,再将其编译为 MCP 服务,并对所有出站调用 施加严格的每日预算限制,同时提供退避和缓存机制。完全开源,完全可自托管。
한국어 문서: README.ko.md — 本英文 README 是规范文档,韩文文件
是其译文。
准确性免责声明
data.go.kr 不提供“剩余配额”接口。因此,MCPortal 的使用记录仅统计经由 MCPortal 发出的调用,所以只是尽力而为的估算。同一
serviceKey在 MCPortal 之外消耗的调用 ——例如其他脚本、门户控制台或其他工具发出的调用——都不会计入记录,因此记录中的数值 始终是实际用量的下限估算。可信赖的并非该估算,而是硬性预算上限(CALL_BUDGET)。 即使记录有误,硬性保护机制也会在超过每日上限时实际阻止调用,因此防止因配额问题导致 账户受处罚的安全保障始终来自CALL_BUDGET。
无密钥边界
| 无需任何 API 密钥即可使用 | 需要 data.go.kr 服务密钥 |
|---|---|
| 重放 record/replay 磁带 | 调用实时 API |
完整测试套件(pytest),包括在合成传输上运行的 record 模式测试 | 采样实时响应 |
根据已提交的规范和磁带启动 MCP 服务(mcportal serve <id> --replay)——可从仓库检出目录启动,也可将 PyPI 安装指向该目录启动(注 1) | 临时转换尚无磁带的 API |
重新生成编译演示(examples/compile_demo.py) | 录制新磁带 |
重新生成并列出预设包(mcportal compile / mcportal presets)——自 0.2.0 起随 wheel 一同发布(注 1) | 采样仍未解析的响应架构(mcportal sample) |
查看配额状态(mcportal quota status) | 依赖密钥的基准测试项目 K1–K3 |
运行基准测试工具包(benchmarks/PROTOCOL.md 中的五个免密项目) | — |
MCPortal 中的所有演示、开发和 CI 流程都无需密钥。record/replay 层会重放先前录制的磁带,因此无需提供 serviceKey,就能复现相同的响应流程并通过整个测试套件。规范到 MCP 的转换已在构建时完成——编译后的构件已提交到 specs/ 下——因此克隆仓库,甚至启动 MCP 服务并响应工具调用,都无需密钥:mcportal serve 15000115 --replay 可通过 stdio 提供八个工具,且环境中无需出现 serviceKey。
这里特意写了克隆。重放需要磁带,而磁带是上游响应的录制文件,只保留在仓库中,不会随 wheel 一同发布(注 1);因此,单纯执行 pip install mcportal 后没有可供重放的内容,除非通过 --presets-root <path> 或 MCPORTAL_PRESETS=<path> 将其指向一个检出目录。四个预设包中有三个存在磁带——仓库中也没有 15081808 的磁带,因为它被有意排除在采样之外(见下文预设),所以该预设包只能实时使用。
需要密钥的场景范围很窄:向 data.go.kr 发送实时请求,以及采样或转换尚无磁带的 API。
注 1——wheel 包含和不包含的内容。 自 0.2.0 起,发布的 wheel 包含四个预设包(16 个包文件以及
presets/下的两个文档,以构建后的构件为准),因此,直接执行pip install mcportal后,无需检出目录即可运行mcportal presets。 自 0.2.2 起,wheel 还包含三个sampled_schemas.json文件——15000115、15101612和15102108;15081808没有此文件,因为它被有意排除在采样之外。这些文件随包发布是因为mcportal compile --check需要它们:--check不会对文件进行差异比较,而是根据源文件、整理层和采样层重新合成openapi.json;因此,如果没有采样层,已安装的副本就无法重现其随包发布的openapi.json。在发布的 0.2.1 版本中,从普通安装运行compile --check会报告 4 个中有 3 个存在偏差,并以状态码 3 退出(测量日期:2026-08-15);在该版本中,请传入--presets-root <checkout>/presets。0.2.2 则恢复为 4 个全部匹配,状态码为 0,仅凭安装包即可完成。 这三个文件仅包含从响应中推断出的字段名和类型——不含任何响应值——而这些结构已随openapi.json一同发布,因此不会暴露 wheel 原本未包含的信息。 wheel 仍不包含的是录制的上游流量本身:cassettes/和samples/仅保留在仓库中,因此mcportal serve --replay只能通过检出目录运行。若要使用其他预设包集合,或为 PyPI 安装提供磁带,请通过--presets-root <path>或环境变量MCPORTAL_PRESETS=<path>将 MCPortal 指向一个检出目录。 如果mcportal presets未找到任何预设包,它会打印搜索过的路径。
无密钥复现指南
# 1) Compile a spec from synthetic fixtures (zero network, zero credentials).
python examples/compile_demo.py
# -> specs/demo/openapi.json + specs/demo/samples/*.json
# Re-running produces byte-identical output; the determinism check is
# built into the script.
# 2) Stand up an MCP server over stdio from a committed bundle (no key).
# Needs the [mcp] extra. --replay reads presets/<id>/cassettes/<id>.json,
# which exists for 15000115, 15101612 and 15102108; 15081808 has none.
mcportal serve 15000115 --replay
# -> 8 tools over stdio, zero credentials. On a PyPI install add
# --presets-root <checkout>/presets (note 1).
#
# The same thing without the CLI, against any spec plus any cassette:
python -c "from mcportal.mcp import build_server; \
build_server('specs/demo/openapi.json', mode='replay', \
cassette_path='<cassette path>')"
# specs/demo/ commits the spec and the samples, not a cassette, so
# <cassette path> must point at one you recorded yourself. That a server
# really stands up and answers a tool call without a key is proven by the
# last case in tests/test_mcp_wiring.py, which builds a synthetic cassette
# in tmp_path. presets/<id>/openapi.json fits the same slot.
# 3) Regenerate the preset bundles from real published specs (no key).
# Works from a checkout, and — since 0.2.2 started carrying the sampled
# layer — from a plain pip install too (note 1). On 0.2.1 as published from
# PyPI, or to point at a different bundle set, pass --presets-root <path>
# or set MCPORTAL_PRESETS=<path>.
mcportal presets # list
mcportal compile --check # byte-compare committed vs regenerated (exit 3 on drift)
# 4) The whole test suite (no live network, no key, no real data).
pytest -q
安装
pip install mcportal # core runtime (single dependency: httpx)
pip install "mcportal[mcp]" # + the MCP conversion layer (fastmcp)
依赖策略:核心运行时只依赖 httpx。 规范化编译器(mcportal.compiler)仅使用标准库和 httpx。fastmcp 仅由 mcportal.mcp 需要,并作为可选的 [mcp] 额外依赖提供——不安装它,import mcportal 和整个测试套件仍可正常运行;调用 MCP 层时则会抛出一条韩语 ImportError,说明如何安装。[mcp] 额外依赖还声明了 anyio,因为同步转异步桥接会直接导入 anyio.to_thread;虽然 httpx 会间接引入 anyio,但直接导入的依赖应直接声明,以便固定版本和锁文件对其进行约束。
import mcportal 不会导入 mcportal.mcp——这正是未安装 fastmcp 时仍能正常导入的原因。MCP 符号会通过模块 __getattr__
(PEP 562)在首次访问属性时惰性解析,因此 from mcportal.mcp import build_server 和 mcportal.build_server 指向同一个对象。两种写法都可以。
单密钥原则(不轮换多个密钥)
MCPortal 的 data.go.kr 配置文件不支持轮换多个密钥。 data.go.kr 会为每个开发账户签发一个密钥,并据此统计每日调用限额;轮换多个密钥以绕过限额,可能违反服务的运营政策并招致账户处罚。MCPortal 尊重这一机制,接受单个密钥。当限额过低时,支持的做法是遵循 data.go.kr 自身的流程——注册用例并申请运营级别,而不是使用更多密钥。
CLI
CLI 仅使用标准库 argparse。本项目的硬性规定是不得新增运行时依赖,因此连终端表格也都是手工排版(韩文按双倍宽度计算,使用 ASCII 边框,在 Windows cp949 控制台上安全)。
mcportal quota status [--ledger PATH] [--budget N] [--day YYYY-MM-DD]
[--key-fp FP | --key-env VAR] [--json]
mcportal compile [PRESET_ID ...] [--presets-root PATH] [--check] [--json]
mcportal presets [--presets-root PATH] [--json] [--verbose]
mcportal sample PRESET_ID ... --key-env VAR [--budget N] [--count N]
[--ledger PATH] [--presets-root PATH] [--json]
mcportal serve PRESET_ID [--replay | --key-env VAR] [--presets-root PATH]
[--name TEXT]
| 子命令 | 功能 |
|---|---|
quota status | 显示每个密钥指纹今日(KST)的用量、预算、剩余量和状态。**账本以只读方式(mode=ro)打开,绝不会创建账本。**预算解析顺序为 --budget > CALL_BUDGET > 配置文件默认值,输出会说明采用了哪种方式 |
compile | 重新生成预设包。内容未更改的文件不会重写。--check 不写入任何内容,只进行字节比较 |
presets | 以表格形式列出预设包;--verbose 会展开整理说明 |
sample | 实时采样,补全仍标记为未解析的响应架构,并写入推断出的架构、响应正文和可重放的录制文件。这是唯一需要密钥的子命令,密钥只能从 --key-env VAR 指定的环境变量中读取,绝不会作为字面参数传入 |
serve | 通过 stdio 将一个预设包作为 MCP 服务启动。默认的 --replay 不需要密钥;--key-env VAR 会通过同一配额保护机制进行实时调用。需要 [mcp] extra。文档基于仓库检出版本编写:--replay 会读取 presets/<id>/cassettes/<id>.json,而 wheel 不包含该文件(注 1),因此从 PyPI 安装时,请传入 --presets-root <checkout>/presets 或设置 MCPORTAL_PRESETS=<checkout>/presets。四个预设包中有三个带有录制文件——仓库中也没有 15081808 的录制文件,因此它只能进行实时调用。stdout 仅用于协议传输;所有便于人阅读的内容(包括横幅)都会发送到 stderr |
退出码:0 成功(包括“无账本”“无预设包”和“没有内容更改”——空状态不代表失败)/ 1 执行失败 / 2 用法错误 / 3 compile --check 检测到差异 / 130 用户中断。
--json 只在 stdout 上输出 JSON(不混入任何人类可读文本,可安全用于管道),并将所有错误发送到 stderr。任何情况下都不会打印原始服务密钥——即使使用 --key-env,也只会读取环境变量并在本地计算指纹。账本不会存储原始密钥,因此 CLI 能显示的只有指纹。
预设 — 三项服务,四个数据集
presets/ 中保存的是基于 data.go.kr 上真实发布的规范构建的捆绑包。它们旨在通过数据而非代码,展示如何修复朴素规范转换的失败模式:生成的每个工具最终都被描述成“列表查询”。
| ID | 服务 | 领域 | 来源类型 | 操作数 | 已发布的数据许可 |
|---|---|---|---|---|---|
15000115 | 法制处 — 国家法律信息共享服务 | 法律 | rest_doc_manual | 8 | KOGL Type 1(署名) |
15081808 | 国税厅 — 营业执照有效性和状态查询 | 营业执照 | odcloud_swagger | 2 | 未注明限制 |
15101612 | 关税厅 — 按国家统计的贸易数据 | 海关 | gw_swagger | 1 | 未注明限制 |
15102108 | 关税厅 — 进出口汇总 | 海关 | gw_swagger | 1 | 未注明限制 |
许可措辞以各捆绑包记录的获取日期当天 data.go.kr 上显示的内容为准。按领域统计有三项服务,而海关有两个数据集,因此每份文档都写作**“三项服务(四个数据集)”**。
十二个响应架构中有十个尚未确定;2026-08-09 的实时采样确定了全部十个(15000115 八个、15101612 一个、15102108 一个——每项操作调用一次,共调用十次)。推断出的架构保存在各捆绑包的 sampled_schemas.json 中,响应正文保存在 samples/ 中,请求/响应对保存在 cassettes/ 中,因此无需密钥即可离线重放结果。已采样的捆绑包会在 info.x-mcportal 中报告 generation_mode: "sampled"。
其余两项操作(15081808)从未处于未确定状态——该来源声明了响应架构。我们特意未对该捆绑包进行采样,因为其请求正文包含营业执照号码;因此,它的架构已声明,但未经测量。MCPortal 不会隐藏这两种状态:实时计数会写入生成文档中的 info.x-mcportal.schema_inference.unresolved,而各捆绑包仍有哪些未知内容,则列在各自的 presets/<id>/README.md 中。将未经验证的内容写成已验证,与本项目的规则相悖。
一个捆绑包由四个已提交文件组成,有采样证据时还会附带采样证据:
presets/<id>/
├─ source.json <- the spec document plus source URL, acquisition date, sha256
├─ curation.json <- human-checked descriptions, examples, hints
├─ openapi.json <- the merge of both layers (the committed artifact)
├─ README.md <- provenance and open questions for this dataset
├─ sampled_schemas.json <- schemas inferred from live samples (sampled bundles only)
├─ samples/ <- scrubbed response bodies from those calls
└─ cassettes/ <- request/response pairs for offline replay
顶部的四个文件会随 wheel 一同发布;从 0.2.2 起,存在时 sampled_schemas.json 也会一同发布——四个捆绑包中有三个包含该文件——因为没有它,compile --check 就无法重新合成 openapi.json(注 1)。记录的流量数据 samples/ 和 cassettes/ 仅保存在仓库中。
- 底层不包含任何领域知识。
mcportal.compiler.curation是一个通用引擎,用于读取、验证和合并整理数据;代码中不会出现任何机构或数据集名称,并且有测试通过扫描源字符串来确保这一点。 - **整理不会更改规范中的事实。**它会添加描述、示例、标签和提示。参数类型、位置和必填状态,以及操作路径和方法,仍以来源规范的声明为准。只有两种渠道可以更正事实,而且都要求提供书面
reason:将响应架构降级为未确定,以及移除参数。 - 对于相同输入,
openapi.json会逐字节一致地重新生成。使用mcportal compile --check作为 CI 门禁(出现偏差时退出代码为 3)。有一个例外:info.x-mcportal.tool_version会记录软件包版本,因此升级版本会更改全部四个产物,而在此情况下重新生成是惯例。
约定和待处理事项由
presets/README.md 管理;规范元数据的来源和使用条款由
presets/NOTICE-DATA.md 管理。
架构
编译时分为两层,运行时则是一条带防护的调用链。
COMPILE TIME (offline: no key, no network)
spec documents +--------------------------------+
- odcloud OAS (JSON) | lower layer: the compiler |
- gateway Swagger 2.0 / 3.x ---> | zero domain knowledge |
- hand-mapped usage guide | sources -> SourceSpec -> IR |
+---------------+----------------+
|
curation.json |
(human-checked descriptions, ---------------> merge
examples, hints) upper layer: data, not code
|
v
openapi.json (committed;
byte-identical on rebuild)
RUN TIME (one MCP tool call)
MCP client
| tool call
v
FastMCP server <- built by FastMCP.from_openapi() from openapi.json
|
v
sync/async bridge -> MCPortalTransport
|-- quota guard token bucket + SQLite ledger
| + CALL_BUDGET hard cap + backoff
|-- key injection the key never enters the spec
|-- TTL cache
|-- record / replay cassettes, scrubbed on write
|-- normalization XML -> JSON, EUC-KR, error codes
v
data.go.kr (or the cassette, in replay mode)
这种架构有两个值得明确说明的结果。
- 工具定义不是手动生成的。
FastMCP.from_openapi()负责将规范转换为工具;MCPortal 提供它之前的阶段(规范规范化)和之后的阶段(配额与清理)。不同版本系列之间的差异通过运行时签名内省来处理:如果类公开了FastMCP.from_openapi,就使用它;否则回退到构建FastMCP(providers=[OpenAPIProvider(...)])。这是能力检查,而不是版本检查——from_openapi并非仅限于 2.x 的入口,它在 3.x 系列中也存在。依赖固定为fastmcp==2.14.7,因为在录制回放测试中实际验证过这一组合;已知的硬性边界是 4.0,它将 HTTP 堆栈迁移到httpx2>=2.5,因此会破坏本项目所依赖的httpx.AsyncBaseTransport桥接(4.0 还会再次拆分发行包,改为fastmcp-slim)。固定该版本的理由记录在pyproject.toml中,紧挨着版本声明。 - 服务密钥无处泄漏。 编译器不会生成
security或securitySchemes,并会从源文件中移除密钥参数,因此密钥永远不会成为 MCP 工具参数,不会出现在规范文件中,也不会出现在提示词日志中。仅保留密钥由传输层注入这一事实,即info.x-mcportal.key_injection: "transport"。
每条默认路径都会接入防护。预算的解析顺序为 create_client(budget=...) > CALL_BUDGET 环境变量 > 配置文件默认值;即使省略该参数,也仍会接入防护——既然 README 声明硬性上限是信任的依据,就不能让防护悄然消失。在 before_call 时进行中的请求会预留预算,并在 after_call 时释放,因此即使 MCP 服务同时发起多个工具调用,上限也依然有效。
基准测试
测量计划已预先注册。
benchmarks/PROTOCOL.md 在测试框架创建之前就确定了测试项、重复次数、统计定义和局限性,测试框架不会测量该文档中未列出的内容。结果文件中嵌入了协议指纹,因此事后仍可核查某个数字对应的协议版本。
python benchmarks/harness.py --label <label>
测量 5 个无需密钥的测试项(回放往返、脱敏、编译与确定性、配额保护开销、FastMCP 构建)。网络调用数为 0;输入要么完全合成,要么使用已提交的预设。不会剔除异常值——原始样本随结果文件一同提供,任何人都可以重新计算。
核心数据:配额保护开销的中位数为每次调用 +0.90 ms (+901,050 ns;保护后的中位数为 1.04 ms,裸调用的中位数为 0.14 ms;预热 20 轮后测量 N = 200;测量日期为 2026-08-09,环境为 Windows 10、CPython 3.11.9、httpx 0.28.1、SQLite 3.45.1)。
这一核心数据取决于环境,环境也是该结论的一部分。 2026-08-15 在一台全新配置的机器上重新运行同一测试框架后,保护后的中位数为 738.9 us,裸调用的中位数为 91.0 us,增量为 +0.65 ms。这两个数字都没有错:绝对开销取决于主机的 SQLite 写入延迟,因此第三方复现的数字会是他们自己的结果。引用核心数据时,请附上测量环境,或者自行重新测量。
请将该数据理解为绝对增量,并同时考虑以下两点。该数字包含 SQLite 日志写入(WAL 日志模式),因为这项写入属于实际开销。作为减数的基线是通过内存中的 httpx.MockTransport 运行的裸 httpx.Client——没有套接字,也没有 I/O——因此用百分比表示同一测量结果时,数值天然会很大,单独看没有意义。与真实的 data.go.kr 往返相比,结果会有所不同;MCPortal 在此不对这种对比作出结论,因为尚未进行测量。
这些数字衡量的是 MCPortal 自身增加的这一层的开销,并非与其他库的对比。 用于判断两层设计是否值得采用的测试项(K2——朴素生成与精心整理的工具调用成功率)仅在协议中作了定义,尚未运行。提前写下定义是有意为之:这样可以避免在结果出来后挑选有利标准。0.2.0 中使用的有效密钥仅用于响应模式采样;依赖密钥的基准测试项 K1–K3 不在此版本的范围内。
机器可读的预设根目录
mcportal presets --json 会在 root 旁报告 root_source 键,标明所采用的预设根目录来源:argument(显式指定 --presets-root)、env:MCPORTAL_PRESETS、discovered(通过默认搜索找到)或 none(完全没有根目录)——这样脚本就能区分有意指定的根目录和意外采用的根目录。
许可
Apache-2.0。请参阅 LICENSE 和 NOTICE。
数据衍生文件(测试固件、响应样本、编译器产物)的来源和使用条款单独记录在
NOTICE-DATA.md 中。
- 没有提交任何通过调用 API 获得的响应载荷。 测试固件、录制文件和演示产物均为合成内容,且无需密钥的复现流程完全基于这些合成固件。
- 公共 API 的规范元数据(Swagger 文档、请求/响应表)存放在
presets/下,并记录其来源 URL 和获取日期。获取过程完全未经身份验证:未使用任何服务密钥,也未调用任何网关数据接口。规范文档包含门户为文档目的编写的示例值,这些值均为占位符;各文件的来源、条款和示例值清单由presets/NOTICE-DATA.md规定。 - 不包含任何个人身份信息(真实姓名、真实企业注册号、个人电话号码、个人电子邮件地址)。此声明适用于数据包文件,即
source.json、curation.json、openapi.json和每个README.md。presets/_raw/下的门户页面快照仍保留了运营机构的公共帮助台电子邮件地址([email protected])、主电话号码(1566-0025)以及各数据集管理部门的代表号码,因为这些信息出现在原始页面中;它们是机构联系信息,而非个人信息。相关证据和完整清单由presets/NOTICE-DATA.md§2-1 规定。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。