Skip to main content
Glama
README.md
# mcp-noita-data

[![powered by dsh](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](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

A3.7/5.0

Scored across 8 tools

Disambiguation4/5

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.

Naming Consistency3/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues