noita-data
# mcp-noita-data
[](https://github.com/deepseek-ai/deepseek-harness)
把 Noita 解包后的 `data/` 目录以 **只读** MCP 工具暴露给模型。
**只做数据获取,不做计算**:不执行 Lua、不推导法术属性、不解析图片内容,
只负责把磁盘上的 XML / Lua / 文本原样读出来。
## 工具
| 工具 | 作用 |
|------------------------------------------------|-----------------------------------------------------------------------------------|
| `list_dir(path="")` | 列出目录直接子项(名称、路径、类型、大小、扩展名) |
| `read_text(path, offset=1, limit=400)` | 按行读取文本文件,返回总行数以便翻页 |
| `read_xml(path, max_nodes=200)` | 解析 XML 为嵌套字典(属性平铺、同名子节点转数组) |
| `search(query, glob="**/*", regex=False, ...)` | 在文本内容中检索,返回文件 + 行号 + 行内容;有译文时支持中文查询 |
| `translate(text, limit=20)` | 在译文表里互查:`$key` → 译文,译文 → `$key` |
| `spell(spell_id, max_tables=10)` | 把一个法术在 `scripts/gun/*.lua` 各版本表里的条目并列返回 |
| `spell_bundle(spell_id)` | 一次汇总:卡牌字段 + 全部组件(合并 `<Base>`)+ 粒子颜色 + 图集帧 + 音效 + 换算值 |
| `entity(path)` | 读取任意实体,合并一层 `<Base>`,返回**未筛选**的全部组件 |
路径统一使用游戏内写法,`data/` 前缀可省略:
```text
data/scripts/gun/gun_actions.lua
scripts/gun/gun_actions.lua # 等价
```
## 安装与运行
```bash
uv sync
uv run noita-data-mcp --data-dir ./data
```
默认按以下顺序查找数据目录:
1. 环境变量 `NOITA_DATA_DIR`
2. 当前工作目录下的 `data/`
3. 仓库根目录下的 `data/`
## 客户端配置
```json
{
"mcpServers": {
"noita-data": {
"command": "uv",
"args": [
"run", "--directory", "C:/Users/dynas/Documents/Code/mcp-noita-data",
"noita-data-mcp", "--data-dir", "C:/Users/dynas/Documents/Code/mcp-noita-data/data"
]
}
}
}
```
## 设计说明
- **路径隔离**:所有路径先 `resolve()` 再校验是否位于数据根之下,`..` 与绝对路径一律拒绝。
- **只读**:没有任何写盘操作;文件系统权限交给操作系统。
- **不预扫描**:4,000+ XML、1,000+ Lua 全部按需读取,避免启动时全量索引。
- **返回有上限**:`read_text` 按行分页,`read_xml` 按节点数截断并标记 `_truncated`,
`search` 默认最多 50 条,避免单次结果撑爆上下文。
- **二进制识别**:按扩展名 + NUL 字节双重判断,图片/音频不会被当作文本读出。
- **UTF-8**:启动时把 stdin/stdout 强制为 UTF-8,避免 Windows 控制台 GBK 破坏 JSON-RPC。
## 返回示例
```
read_text("scripts/gun/gun_actions.lua", offset=1, limit=2)
```
```json
{
"path": "data/scripts/gun/gun_actions.lua",
"total_lines": 11126,
"start_line": 1,
"end_line": 2,
"lines": ["dofile_once(\"data/scripts/gun/procedural/gun_action_utils.lua\")",
"dofile_once(\"data/scripts/lib/utilities.lua\")"],
"truncated": true
}
```
## 结构
```text
src/noita_data/
├─ paths.py # data/xxx -> 绝对路径,越界与盘符一律拒绝
├─ text.py # 编码嗅探、二进制判定、按行分页
├─ xmljson.py # XML -> 嵌套字典(多根兜底、节点预算)
├─ search.py # glob + 正则/子串检索
└─ server.py # MCP 工具与入口
```
## 本地化(translations)
译文来自 Noita 原生的 `data/translations/*.csv`(形如 `"$action_light_bullet","火花弹"`)。
放进去即生效,也可以用环境变量指定:
```bash
set NOITA_TRANSLATIONS=D:\path\to\common.csv # 或一个目录
```
有译文表时:
- `search("火花弹")` 会先反查成 `$action_light_bullet` 再搜,命中行附带 `translations`;
- `translate("火花弹")` 返回对应 key,`translate("$action_light_bullet")` 返回译文;
- 没有译文表时两者安全降级,只是不能按中文查。
当前这份 dump **不含** `data/translations/`,所以开箱只能用 id 搜索。
## 开发
```bash
uv run pytest # 10 个用例,覆盖路径、分页、XML、检索
```
## 示例:复刻一个法术需要什么
`spell_bundle("POLLEN")` 一次返回整条链(节选):
```json
{
"card": {"mana": "10", "price": "110", "spawn_level": "0,1,3,4",
"action_effects": ["c.fire_rate_wait = c.fire_rate_wait + 2",
"c.spread_degrees = c.spread_degrees + 20"]},
"projectiles": [{
"body": {"damage": "0.2", "lifetime": "750", "speed_min": "150", "speed_max": "250"},
"velocity": {"gravity_y": "10", "air_friction": "2.8", "mass": "0.35"},
"homing": {"homing_targeting_coeff": "10.0", "homing_velocity_multiplier": "0.89"},
"particles": [{"material": "spark_white", "color": "7fFEFEFF", "glow": "90"}],
"sprite": {"animation": {"frame_count": "4", "frame_width": "11", "frame_wait": "0.09"}},
"audio": {"event_root": "player_projectiles/bullet_arrow"}
}],
"not_available": ["贴图像素", "音频采样", "引擎侧公式(追踪/碰撞/伤害换算)"]
}
```
数值来自 `gun_actions.lua` → `related_projectiles` → 实体 XML(合并一层 `<Base>`)→
`materials.xml` 的材质颜色 → `projectiles_gfx/*.xml` 的帧定义。全部只读。
## 同一法术的多份定义
`scripts/gun/` 下有多张法术表(普通 / 有限法术 / 无限 / petri),字段并不相同。
`spell("LIGHT_BULLET")` 会把这些表里的条目并列列出(含行号区间):
```text
_gun_actions_unlimited.lua L1052-1067 mana=5 price=- max_uses=- name="Spark bolt"
gun_actions.lua L43-63 mana=5 price=100 max_uses=- name="$action_light_bullet"
gun_actions_limited.lua L1146-1162 mana=5 price=- max_uses=100 name="Spark bolt"
gun_actions_petri.lua L1146-1162 mana=5 price=- max_uses=- name="Spark bolt"
gun_actions_unlimited.lua L1052-1067 mana=5 price=- max_uses=- name="Spark bolt"
```
它按源码文本解析,不执行 Lua;要读函数体就用返回的 `source` 区间再 `read_text`。
## 范围
明确**不做**的事情:执行 Lua、计算法术派生数值、切分图集、渲染图片。
需要这类派生数据时应当在调用方完成,或另建一个构建期脚本,而不是塞进 MCP 工具。
## 致谢
Design inspired by [Noita_Lib](https://github.com/Caramel-Diode/Noita_Lib) (GPL); no code copied.TDQS
Scored across 8 tools
Most tools have clearly distinct purposes (directory listing, raw text read, XML parse, search, translation, entity read). There is mild overlap between spell / spell_bundle (both about spells, though one returns aggregated numbers and the other Lua source entries) and between entity / read_xml (both read entity XML), but the descriptions clarify the boundaries well enough.
A partial verb_noun pattern exists (list_dir, read_text, read_xml), but it is broken by bare nouns (entity, spell) and noun compounds (spell_bundle), plus a bare verb (search, translate). Mixed conventions but each name is still readable and unambiguous.
Eight tools is well-scoped for a read-only data-inspection server, and each tool covers a distinct layer (listing, raw IO, parsing, search, localization, domain-specific aggregation). No redundant or filler tools.
The surface covers the core read-only lifecycle: locate files, read raw text, parse XML, search content, resolve translations, and domain-specific entity/spell inspection. Minor gaps like a spell/entity index or bulk listing exist but search plus list_dir let agents work around them.