Skip to main content
Glama

curseforge-ark-mcp

一个用于 CurseForge 模组管理、发现和更新监控的只读 MCP 服务器,专为《方舟:生存飞升》设计。

这是 v0 版本。此处所有内容均未经过真实响应验证。

目前尚无 CurseForge API 密钥。该密钥并非自助获取——需向 Overwolf 提交申请才能获得——因此,本仓库中从未有人进行过任何经过身份验证的调用。 每个测试夹具和每个工具输出中的每个字段路径都是根据已发布的模式解读出的假设

这并非谦虚。兄弟仓库 nitrado-ark-mcp 以同样谨慎的方式根据文档构建了其测试夹具,而提交 5481c04 修正了三个字段路径,这些路径在根据真实响应检查之前一直是错误的。 假设本仓库也有三个类似的错误等待发现。

版本号为 0.1.0,这是对验证状态的声明。本 README 中没有“已根据真实账户验证”部分,其缺失是准确的,而非遗漏。

今天已验证的是本仓库自身的行为:端点允许列表、主机锁定、路径规范化、分页边界、信封处理以及三态(缺失/空/未知)规范。所有这些都通过注入的虚假 fetch 进行了测试,无需密钥,无需网络。截至撰写本文时,146 个测试,0 个失败。

设计记录:docs/adr/ADR-002-endpoint-allow-list.md(状态:已提议)。以下每个章节引用(§1、§4.3、§14.3 …)均指向该文档。


它能做什么,以及它故意不能做什么

七个工具,全部只读:

工具

回答

search_mods

“哪些 ASA 模组匹配此搜索词?”

get_mod

“项目 777001 是什么?”

list_mod_files

“此模组发布了哪些文件?”

get_mod_file

“这个特定文件是什么?”

get_latest_file

“对于此模组,是否有比我当前运行的版本更新的文件?”

resolve_mod_dependencies

“此模组引入了哪些依赖?”(批量处理,每个树级别一个请求)

get_api_diagnostics

“是我、密钥还是 CurseForge 的问题?”——以及“此构建有多诚实?”

不能

  • 下载或安装任何内容。 GET /v1/mods/{modId}/files/{fileId}/download-url 是一个已记录的只读端点,位于锁定主机上,但被拒绝——因为它不在端点允许列表中(DEC-002 §11.3)。Nitrado 自行安装模组。

  • 在任何地方写入任何内容。 没有允许列表条目命名了可变端点。CurseForge 确实在另一个主机上运行了一个可变上传 API(§14.2);主机锁定出于另一个独立原因再次拒绝它。

  • 发布或创作模组。 直接拒绝(DEC-002 裁决 2)。由启动断言强制执行,而非承诺:注册任何声明非第一层级的工具会导致进程拒绝启动

  • 接触 Nitrado。 本仓库的配置界面中不存在 NITRADO_* 变量,其缺失是一种控制。此服务器不持有任何 Nitrado 令牌,也不读取任何 Nitrado 配置。

  • 定时唤醒并更新你的服务器。 没有调度器,没有轮询循环,没有持久化的“上次看到的版本”状态(§10)。监控意味着模型可能观察到新版本。它不能采取行动。


关键点:端点允许列表,而非方法检查

这是唯一一个在接触代码前值得阅读的设计决策。

CurseForge 使用 POST 进行读取POST /v1/modsPOST /v1/mods/files 是批量检索,正是它们使得 resolve_mod_dependencies 每个依赖层级只需一个请求,而不是每个节点一个。因此,兄弟仓库的 method !== "GET" → refuse 在这里会以最昂贵的方式失败:它会工作。 它会拒绝一些东西,通过自己的测试,然后悄悄地使服务器无法胜任其工作。

而显而易见的修复比错误本身更糟糕:

allowed = { GET }          → the batch reads are refused (broken, loudly)
allowed = { GET, POST }    → every request this client can construct is allowed

已记录的目录 API 仅包含 GETPOST。一个同时允许两者的门会允许一切——同时看起来仍然正常。

因此,每个出站请求必须匹配一个封闭的 {method, path} 对列表中的显式条目。七个条目,位于 src/allowlist.ts

#

方法

路径

服务于

E1

GET

/v1/games

游戏 ID 解析,get_api_diagnostics

E2

GET

/v1/mods/search

search_mods

E3

GET

/v1/mods/{modId}

get_modget_latest_file

E4

GET

/v1/mods/{modId}/files

list_mod_filesget_latest_file

E5

GET

/v1/mods/{modId}/files/{fileId}

get_mod_file

E6

POST

/v1/mods

resolve_mod_dependencies(批量读取)

E7

POST

/v1/mods/files

resolve_mod_dependencies(批量读取)

机制上:

  • 基于 {method, path} 联合匹配。 E3 不授权 DELETE /v1/mods/123。E6 不授权 POST /v1/mods/123

  • 主机被锁定https://api.curseforge.com,该锁定是允许一个来源,而非拒绝任何命名的其他来源。

  • ID 段绑定到 [0-9]+,而非 [^/]+ 这是关键:宽松的 {modId} 会使 E3 吞掉 /v1/mods/search。数字绑定使得这种歧义在结构上不可能,而非依赖于匹配顺序——并且有一个测试反转了整个列表以证明顺序并非其保障。

  • 一次规范化,在检查之前,URL 由其输出构建。百分号解码一次;拒绝任何存留的 %;折叠反斜杠;拒绝 ... 和空段。

  • 只有 E6/E7 可以携带请求体,在分发前进行形状检查。GET 条目上的请求体被拒绝,而非丢弃。

  • 只有 resolve_mod_dependencies 可以访问 POST 条目(§8),在传输层强制执行。

失败模式是“未匹配的请求被拒绝”,而非“未识别的请求被发送。” 添加功能是一个可审查的单行差异,其审查问题——“此端点是只读的吗?”——是人类能够回答的问题。

证明它是允许列表的测试

GET /v1/mods/{modId}/files/{fileId}/download-url拒绝。它是一个已记录的只读端点,GET,位于锁定主机上,具有格式良好的数字 ID。它被拒绝纯粹是因为它不在列表中。如果该测试因其他原因通过——主机锁定拒绝、路径拒绝——则该属性未实现,因此测试断言拒绝的代码和详细信息,而不仅仅是抛出了异常。

每个拒绝测试还断言虚假 fetch调用次数,因为“在请求构建之前拒绝”是实际的规定,而分发后抛出的错误会满足较弱的断言。并且拒绝套件之前有一个原像测试,证明所有七个条目确实进行了分发——一个在无法发送任何内容的客户端上运行的拒绝套件会完美通过,但证明不了任何东西。


仍未验证

下面的每一行都是假设。 这些是 ADR-002 的 §14.3,完整转载。字段路径来自已发布的模式,而这正是导致兄弟仓库出现三个错误路径的那类工件。

#

主张

依据

为何重要

U1

ASA gameId

没有密钥无法发现(§5)

错误的值会返回干净、空、错误的搜索结果

U2

ASA 对已授予的密钥是否完全可见

没有密钥无法发现

可能完全阻塞 v1

U3

Mod 字段:id, gameId, name, slug, latestFiles, latestFilesIndexes, dateModified, links, categories, allowModDistribution

已发布的模式

每个工具的输出

U4

File 字段:id, modId, displayName, fileName, fileDate, gameVersions, sortableGameVersions, dependencies, releaseType, isAvailable

已发布的模式

get_latest_file, list_mod_files

U5

FileDependency = { modId, relationType }

已发布的模式

resolve_mod_dependencies 遍历

U6

FileRelationType 数字枚举映射

未解决。 对文档进行了三次尝试;该页面将 relationType 显示为裸整数,且没有发布的值表。不要从记忆、博客或本仓库中获取映射。

决定一条边是必需的、可选的、工具还是不兼容 — 即是否会被遍历。resolve_mod_dependencies 在此处被阻塞。

U7

FileReleaseType 数字枚举(release/beta/alpha)

未从文档页面解决。仅得到部分佐证: Upload API 使用名称 alpha, beta, release — 这支持该集合,而不是 读取 API 中的数字映射。

get_latest_file 过滤;将 alpha 当作 release 会产生错误的更新建议

U8

pagination 是否存在于每个分页端点

有文档记载的形状;但从未观察到

此客户端会报错,而不是假设只有一页

U9

ASA 模组是否真的填充 dependencies, sortableGameVersions, latestFilesIndexes

模式说明它们可以;但是 ASA 特定行为未知

一个总是为空的字段是能力缺口,而不是 bug — 并且三态规则要求区分它们

U10

POST /v1/mods / POST /v1/mods/files 请求体上的任何 id 数量上限

没有文档记录。 此客户端中的 200 个 id 上限是我们的,而不是供应商的

分块策略

U11

CurseForge 速率限制

未记录。 未找到已发布的数字

get_api_diagnostics 报告观察到的头或 null,从不猜测

U12

index 超过 0 时的真实分页行为,以及在 10000 上限时的行为

仅文档记录的约束

§4.3 中的截断披露

U13

基础 URL https://api.curseforge.com

源自文档

主机固定依赖于它

你将在工具输出中看到的两个后果

relationTypereleaseType 以原始整数形式呈现,且从不进行映射。 不映射为 required/optional,也不映射为 release/beta/alpha。CurseForge 对这两者都没有发布值表,错误的标签会产生一份依赖列表 — 或更新建议 — 其错误方式无人会去核查。因此,resolve_mod_dependencies 会遍历每一条边并明确说明:它会过度收集,其输出也直白地说明了这一点。一张大网至少是显而易见的宽。

get_latest_file 要求你说明“最新”的含义。fileDate 最新、匹配某个游戏版本的最新、以及具有给定 releaseType 的最新会给出不同的答案,而基于错误答案做出的模组更新决策,正是本仓库所针对的那类“自信地给出错误答案”的情形。selection 没有默认值

selection

还要求

含义

newest_by_file_date

所有候选文件中按 fileDate 最新

newest_matching_game_version

game_version

声明该游戏版本的最新文件

newest_with_release_type

release_type(原始整数

携带该 release-type 整数的最新文件

没有名为 release/beta/alpha 的过滤器,因为 U7 尚未解决,此服务器不会自行发明映射。你传入你想要的整数。

这个定义是一个开放的产品问题。 ADR-002 的开放问题 2 将其标记为一个创始人决策,在构建本仓库时尚未做出,因此该工具被参数化而非固执己见:当答案到来时,它会变成一个默认值,或者减少一个变体 — 这是一个小改动,而不是重写。每个答案都会重申它使用的排序、它过滤了什么、考虑了多少候选者,以及候选者来自哪里。


设置

Node 20+(在 22 上开发)。无需配置构建步骤;npm test 会先构建。

npm install
npm test          # builds, then runs the suite — no key, no network
npm run typecheck
npm run smoke     # refuses cleanly until a key exists, naming what it would probe

然后,一旦你有了密钥:

cp .env.example .env
# set CURSEFORGE_API_KEY, then:
npm run smoke

MCP 客户端配置(stdio):

{
  "mcpServers": {
    "curseforge-ark": {
      "command": "node",
      "args": ["C:/path/to/curseforge-ark-mcp/dist/src/server.js"],
      "env": { "CURSEFORGE_API_KEY": "your-key" }
    }
  }
}

没有密钥,服务器拒绝启动,它会说明它搜索过的两个位置、确切的变量,以及密钥不是自助服务的事实。一个能够干净启动、然后在全部七个工具上都抛错的 stdio MCP 服务器,调试起来非常痛苦。

关于密钥

API 密钥以 x-api-key 请求头形式发送。它不是 Authorization: Bearer 令牌 — 那是姊妹 Nitrado 服务器的方案,本仓库刻意不同时支持两者,因为同时支持意味着这段代码可能以 CurseForge 从未记录的形式传输凭据。

密钥是通过向 Overwolf 申请授予的,且不可转让。 实际的后果,也是这一段存在的唯一理由:泄露意味着撤销并重新申请,而重新申请是一个排队过程,不是自助重置。 你不能在喝杯咖啡的工夫重新生成它,也不能借用别人的。请相应地对待它 — .env 已被 gitignore 忽略,.env.example 包含变量名和一个空值,任何已提交的文件中都不会出现密钥值。

本仓库中没有作用域矩阵,这并非疏忽: CurseForge 没有发布只读作用域,也没有作用域选择,因此没有东西可以做成矩阵。此服务器的只读属性来自它自身的端点允许列表,而不是来自更窄的凭据。也没有 token 泄露运行手册 — 泄露的密钥授予对公共目录的读取权限以及配额消耗,这是真实存在的,并且与姊妹仓库的 Nitrado token(文档中描述为等同于对游戏服务器的完全控制)不属于同一类别。这种合理定位在 ADR-002 §12 中有论证,它基于那里陈述的一个可以被证伪的主张:CurseForge 目录数据在构造上就是公开的。

编辑,全部内容

一条规则:永远不要泄露 API 密钥。 一个函数 src/scrub.ts,应用于错误消息和任何上游正文片段。请求头永远不会出现在错误中——无论是密钥、编辑后的密钥还是头名称列表。get_api_diagnostics 报告密钥是否已配置,并且永远不会返回其值、前缀或长度


在阅读输出前需要了解的行为

  • 空不是未知。 data: [] 表示 CurseForge 回答了“无”——这是一个真实的答案,并回显了查询,以便您看到返回了什么为空。缺失的字段是 null绝不是 0""[]。未完成的请求或形状错误的响应是错误——绝不是值。

  • 缺失 data 键是错误,而不是空结果。 将其强制转换为 [] 会把损坏的集成变成“未找到结果”。

  • 分页端点上的缺失 pagination 也是错误。 假设只有一页,工具就会报告 50 个模组中的 900 个,好像它们就是全部(U8 正是这个悬而未决的问题)。

  • 拒绝 pageSize > 50,而不是截断,同样拒绝 index + pageSize > 10000 ——并在消息中提及该索引处允许的最大页面大小。要求 200 但悄悄得到 50 的模型将把一页当作一个集合来推理。

  • totalCount 超过 10000 时,工具输出会说明尾部 UNREACHABLE,使用这些词,并建议缩小筛选条件而不是翻页。

  • ASA gameId 在运行时发现,通过 GET /v1/games 并在进程生命周期内缓存;从不硬编码,从不猜测。如果无法解析,服务器会大声失败,说明它搜索的内容以及密钥可以看到多少个游戏——因为 gameId必需的搜索筛选条件,错误的 gameId 会返回干净、空、完全错误的结果,而不是错误。如果内置的候选值不正确,请设置 CURSEFORGE_GAME_SLUG

  • resolve_mod_dependencies 有界限,深度 4 和 400 个节点,并有已访问集用于循环。当达到界限时,结果会报告为 已截断,使用该词,并列出未探索的前沿。


仓库布局

src/
  allowlist.ts    THE CHOKEPOINT — seven entries, host pin, normalization, bounds, body checks
  client.ts       the single transport; the ONLY place x-api-key is attached; envelope unwrap
  config.ts       refuse-to-start; no NITRADO_*, no mode switch, no settable base URL
  coerce.ts       empty / absent / unknown, kept apart
  errors.ts       the error taxonomy
  game.ts         runtime gameId resolution (injected, process-lifetime cache)
  registry.ts     ToolDef + tier, and the boot assertion that refuses a non-tier-1 tool
  scrub.ts        never echo the key. That is the whole module.
  probe-plan.ts   one probe per unverified row, asserted complete by a test
  server.ts       stdio entry point
  smoke.ts        the key-arrival command
  tools/          the seven tools
test/             146 tests; fixtures are synthetic in content, structural in shape
scripts/          buildinfo generator, test enumerator

src/buildinfo.ts生成的并被 gitignore,在每个 tsc 运行之前标记提交和 dirty 标志,并由 get_api_diagnostics 显示。dist/ 被 gitignore,服务器作为长期运行的进程从中运行,因此“哪个代码产生了那个答案?”在运行时无法从 git 回答——它必须随工件一起传输。

与兄弟仓库的偏差,明确说明

ADR-002 的未解决问题 7 和 8 要求在这些偏差发生的地方命名它们:

  • 故意相同的基线。 Node ≥20、TypeScript 5.9.3、@modelcontextprotocol/sdk 1.30.0、zod 4.4.3、node:test 通过相同的 scripts/run-tests.mjs 枚举器。相同的审查者,相同的惯用法,降低阅读两者的成本。

  • 此处没有 @cfworker/json-schema 依赖。 它支持兄弟仓库的 cron 表达式验证,并且这里没有写入路径需要验证。

  • registry.ts 在结构上移植,保留 tier,但去掉了模式/启用列表机制——它没有任何可筛选的内容,因为每个工具都是 tier 1,每个端点都是读取。一个没有实际内容的模式变量会宣传一个不存在的控制。一个五行引导断言取代了子系统。

  • redact.ts 没有移植(§12.1)。参见上面的“编辑,全部内容”。

  • 没有 UNKNOWN_OUTCOME 错误码。 兄弟仓库需要它,因为丢失的 PUT 响应可能仍然改变了世界。此客户端可以发出的每个请求都是读取,因此超时确实意味着“它没有发生”,重试是安全的。

  • npm run smoke 在因缺少密钥而拒绝时退出 0。 拒绝是今天运行它的预期结果,横幅上写着无法忽略的 SMOKE NOT RUN。如果您希望流水线在缺少密钥时失败,请将流水线门控放在密钥上,而不是这个退出码上。


相关记录

在兄弟仓库 nitrado-ark-mcp 中,从这里只读——该仓库中没有被此仓库修改的内容:

  • docs/decisions/EXECUTIVE-BOARD-2026-08-16-curseforge-mods.md —— 此仓库执行的董事会会议纪要(DEC-002)。其主席裁决具有约束力。

  • docs/decisions/decision-log.md —— DEC-002,以及 DEC-001 用于第 10 节所依赖的范围拆分。

  • docs/adr/ADR-001-write-path-enforcement.md —— ADR-002 移植的形状,以及规范化规则、引导检查推理和拒绝启动推理的来源。

两个服务器保持独立。nitrado-ark-mcp 回答 “这些项目 ID 在 active-mods 中”;此仓库回答 “项目 X 的最新文件是 v2.1”模型持有两者。没有服务器调用另一个,也从未持有另一个的凭证。

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Related MCP Connectors

  • A MCP server built for developers enabling Git based project management with project and personal…

  • MCP server for doc2mcp documentation, generated by doc2mcp.

  • Official MCP server for Lovable, the AI-powered full-stack app builder.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JShort-bufr/curseforge-ark-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server