Skip to main content
Glama
README.md
# AFSIM MCP

本项目是一个本地 MCP(Model Context Protocol)服务器,让大模型能真正跑起 AFSIM
(Advanced Framework for Simulation, Integration, and Modeling)仿真并读懂结果。

**只有 4 个工具。** 不是一个功能齐全的想定编辑器,而是一层薄薄的、专门补上
模型自身做不到的那几件事的垫片。

## 边界:工具归工具,知识库归知识库

这个 MCP 只做两件事,两件事都不含判断:

**一、AFSIM 知识库(`core/knowledge/`)** — 只读 AFSIM 安装目录,只报告里面有什么。
不含任何作者编写的规则,AFSIM 升级后可以整体重新生成。

**二、工具(`core/runtime|validation|analysis`)** — 执行能力。薄,不含知识。

### 分层与依赖方向

```
core/knowledge/     事实:从安装目录提取           不含判断
  ├ source.py         归一化读取(CRLF 会静默吞声明)
  ├ type_index.py     类型命名空间
  └ mission_grammar.py  mission 输出的语法

core/policy.py      判断:哪些事实算失败         全部具名、可覆盖

core/runtime.py     执行器:起进程
core/validation.py  执行器:跑 + 解析
core/analysis.py    执行器:evt → 指标
core/results.py     执行器:运行记录

core/server.py      配置解析与 JSON-RPC 分发
tools/              MCP 接口:模型答不了的问题
transport/stdio.py  传输
```

**依赖严格单向,下层不导入上层**(`tests/test_layers.py` 会检查)。
一个事实不能悄悄变成判断,一个底层模块不能悄悄依赖上层。

### 什么进知识库,什么不进

| 进知识库 | 不进知识库 |
|---|---|
| 这台机器上有哪些 `platform_type` | 什么样的防空部署是合理的 |
| mission 的报错长什么样 | 这个想定该怎么设计 |
| `.evt` 怎么分类 | Pk 意味着什么、要不要追求 |

判据:**能从 AFSIM 安装里提取出来的进,靠经验猜的不进。**

这不是理论。这个仓库里两件"嵌知识"的事成功、两件失败:

- **成功**:类型目录、错误语法。都是从 mission 真正读取的字节里提取的。
  装上之后模型不再编造 `AUTO_BLUE_FIGHTER_AIR`。
- **失败**:关键词推断层、想定模板。都是作者根据经验猜的。结果是提示词写
  "不要任何飞机",照样生成空中战斗机,平台类型只有 `icon` + `mover`。

### 4 个工具,每个对应一个模型答不了的问题

| 工具 | 模型为什么答不了 |
|---|---|
| `afsim_types` | `platform_type` 命名空间是封闭的,编错 mission 直接拒绝。 |
| `run_mission` | 编译、实例化、运行在 AFSIM 里是一次 `mission.exe` 调用,报错格式必须从真实输出解析。 |
| `analyze_run` | KPI 和时间线来自数 MB 的 `.evt`,模型数不准。 |
| `launch_afsim_tool` | 启动 GUI 工具。 |

每个工具都要能回答"模型为什么不能自己问这个问题"。
按这条规则 `list_demos` 站不住(68 个目录,`ls` 一下就行),已经删掉。

### 判断是显式的

`core/policy.py` 里的每条规则都具名、可从参数覆盖:

```jsonc
// 一份"跑了但什么都没发生"的仿真,默认判失败
{ "ok": false, "policy": { "inert_is_failure": true, ... },
  "problems": ["simulation ran but produced no activity: ..."] }

// 不认同这个判断的调用方可以覆盖
run_mission { scenario_file: "...", inert_is_failure: false }
// → problems 变 caveats,ok 变 true
```

之前这些判断埋在 `validation.py` 里,读起来像是 mission 自己说的。

## 前置条件

- Windows
- Python 3.10+
- 已安装 AFSIM(`mission.exe` 等)。类型目录从 AFSIM 自带的 demos 索引而来。

服务只依赖 Python 标准库,**不需要安装 `mcp` 包**:`transport/` 内置了完整的
JSON-RPC / MCP stdio 实现。

## 快速开始

```bash
python configure_mcp.py
```

脚本会询问 AFSIM 根目录、工程目录、demos 目录和 bin 目录,写入
`%USERPROFILE%\.afsim_mcp\config.json`,并输出客户端配置片段。

| 环境变量 | 作用 |
|---|---|
| `AFSIM_ROOT` | AFSIM 安装根目录 |
| `AFSIM_PROJECT_ROOT` | 你的工程目录 |
| `AFSIM_DEMOS_DIR` | demos 目录(类型目录的数据源) |
| `AFSIM_BIN` | `mission.exe` 等可执行文件所在目录 |
| `AFSIM_MCP_CONFIG_DIR` | 配置文件所在目录,客户端只需传这一个 |
| `AFSIM_MCP_STATE_DIR` | 状态目录(类型目录缓存、运行记录) |

客户端配置形如:

```json
{
  "mcpServers": {
    "afsim": {
      "command": "python",
      "args": ["<仓库路径>/transport/stdio.py"],
      "env": { "AFSIM_MCP_CONFIG_DIR": "C:\\Users\\你\\.afsim_mcp" }
    }
  }
}
```

## 工作流

```
1. afsim_types          查真实的 platform_type(模型必须选,不能编)
2. (模型自己写 AFSIM 文本到工程目录)
3. run_mission          跑 + 校验,拿到 file:line:column 的真实报错
4. 按报错改文本,再跑
5. analyze_run          出战斗报告与 KPI
```

### 第 1 步:类型目录

AFSIM 有**两个封闭的类型命名空间**,两个都被索引了(2.9.0 实测):

| 层级 | 数量 | 说明 |
|---|---|---|
| `platform_type` | 657(224 个含传感器或武器) | 完整的平台类型,含运动器/传感器/武器/处理器清单 |
| 组件类型 | 535 | `sensor` / `weapon` / `mover` / `processor` / `comm` / `filter` |
| AFSIM 内置库类 | 88 | `WSF_*`,编译进 DLL,从不在 demos 里声明,但确实存在 |

区分方法是缩进:**顶格**的 `sensor aesa WSF_RADAR_SENSOR` 是类型定义,
**缩进**的 `sensor rdr1 aesa` 是平台内的实例(实例 `rdr1`,类型 `aesa`)。

```
afsim_types(domain="air")
  → AIR_MOVER_FIGHTER, DCA, ESCORT, HVAA, LTE_FIGHTER, SIX_DOF_FIGHTER, SOJ ...

afsim_types(name="LTE_FIGHTER")
  → sensor:    aesa, esm
    weapon:    MEDIUM_RANGE_RADAR_MISSILE, SHORT_RANGE_IR_MISSILE
    mover:     WSF_BRAWLER_MOVER
    processor: radar_track_cueing, weapon_datalink_manager
    include_path: platforms/lte_fighter.txt

afsim_types(kind="sensor", keyword="radar")   # 列传感器类型
afsim_types(name="aesa", kind="sensor")
  → base:       WSF_RADAR_SENSOR
    parameters: ignore_domain, initial_mode, selection_mode, slew_mode,
                azimuth_slew_limits, elevation_slew_limits, ...
    used_by:    AIR_MOVER_FIGHTER, DCA, ESCORT, LTE_FIGHTER
```

**默认只列含传感器或武器的平台类型是有意的**:一个既不能探测也不能开火的平台,
出现在交战想定里没有意义。每个组件类型还记录了它**接受哪些参数**和**被哪些平台
使用**,所以写组件时不用猜关键字。目录首次构建约 9 秒,之后走缓存。

**脚本处理器的接口**:这是"加了战术处理器却不开火"这个最常见故障的根因。目录会
把处理器声明的 `script_variables` 抽出来,标出**默认为空因而必须赋值**的变量,并
给出 demo 里的可用配置:

```
afsim_types(name="AA_TACTICS", kind="processor")
  script_interface:
    must_set = ['SENSOR_NAME', 'WEAPON_NAME']
  working_configuration:
    platform_type = BLUE_NAVAL_SAM_BATTERY
    on_initialize = [{WEAPON_NAME: "blue_naval_sam"}, {MAX_ENGAGEMENTS: "20"}]
```

赋值必须写在 `on_initialize` 块里;在 `script_variables` 里重声明会被 mission
拒绝——子 Agent 实测正是在这里卡住过。535 个组件类型中有 75 个带这样的脚本接口。

### 第 3 步:校验

`run_mission` 真正跑 mission.exe 并解析它的真实输出,而不是只看退出码:

```jsonc
{
  "ok": false,
  "returncode": 0,            // mission 自己认为成功
  "simulation_complete": true,
  "inert_simulation": true,   // 但只有 PLATFORM_INITIALIZED,什么都没发生
  "evt_events": 4,
  "evt_categories": { "PLATFORM_INITIALIZED": 4 },
  "problems": ["simulation ran but produced no activity: ..."]
}
```

判定失败的条件:Mission 报错、退出码非 0、超时、没跑到 `Simulation complete`、
**一个输出文件都没写**(`no_evidence`),或者**跑完了却只有 `PLATFORM_INITIALIZED`
事件**(`inert_simulation`)。

另有一类是"通过了但没法分析"(`not_analyzable`):仿真确实跑了、`.aer` 也有内容,
但一条 `.evt` 事件都没记录——通常是想定里 `event_output` 的 `enable` 全被注释掉了。
这种情况返回 `ok: true` 并附 `caveat` 说明,而不是误判成失败。

报错带精确定位:

```
ERROR Unknown command: bogus_keyword
     at C:\proj\s.txt:2:22
     source: "bogus_keyword 5"
```

**关于内置库类**:`WSF_AIR_MOVER`、`WSF_RADAR_SENSOR` 这类是编译进 AFSIM 的,
demos 里不会声明,但 mission 认。目录把这类名字也收进来了(从别的类型的 `base`
字段和平台组件引用中反推),查不到时会明确区分"不在 demos 里"和"不是已知类型",
不会像之前那样误导你说 `WSF_AIR_MOVER` 不存在。

**include 解析**:demo 里的类型文件用相对路径 include 兄弟文件,只有当入口的
`file_path` 也指向那个 demo 时才能解析。`run_mission` 遇到
`Cannot open file: X` 时会直接在诊断里给出可执行的修复行:

```jsonc
"include_resolution": {
  "found": true, "match": "basename",
  "candidates": [{
    "path": "air_to_air/platforms/lte_fighter.txt",
    "add_to_entry_file": "file_path C:\SoftCode\Sim\AFSim\demos\air_to_air"
  }]
}
```

**调试行为**:`include_stdout=true` 会带回 Mission 的原始 stdout。AFSIM 把
`show_state_transitions`、`show_task_messages` 这类行为诊断放在 stdout,
查"为什么这个战术处理器不开火"基本只能靠它。

### 第 5 步:分析

`analyze_run` 除了 KPI 和交战统计,还会给出 `diagnostics`,说明报告为什么是空的
——这两种情况完全不同:

```jsonc
"diagnostics": [
  { "issue": "empty_evt", "detail": "output/scn.evt is 0 bytes.",
    "likely_cause": "The scenario's event_output block enables no events..." },
  { "issue": "no_events", ... }
]
```

对比之下,一次健康的交战跑出来的 `diagnostics` 是 `[]`——**有诊断信息本身就是
信号**:它在告诉你结果可信还是不可信。

分析还给出 `first_event_sec` / `last_event_sec`(从全部解析记录算,不是从截断的
时间线),以及 `kill_attribution` 说明:AFSIM 的事件输出不记录击毁方的射手,
所以 `first_kill` 是按"损失方归给对面"推断的,油耗耗尽、坠毁或同归于尽都会被
算到对方头上——要下结论前请对照 `weapon_statistics`。

### 损失是怎么算出来的

`red_blue_losses` 里最容易被误读的是 `platforms_lost`。一发打出去的导弹在 AFSIM
里也是一个平台,命中后同样会产生 `PLATFORM_BROKEN`,所以"损失"这个词本身有歧义。
本项目不靠平台名字判断,判据全部取自事件流自身:

- `WEAPON_FIRED` 的第三个 token 就是这发弹的实例名,它随后会原样出现在
  `PLATFORM_BROKEN` 里。名字对上的记为 `weapons_expended`,不计入战斗损失。
- `PLATFORM_INITIALIZED` 列出想定开场就摆上场的平台。名字在其中的才算真实损失。
- 两者都覆盖不到时(`.evt` 被截断,或 `event_output` 没开这两类事件),该条
  损失进 `loss_accounting.unverified_losses`,照常计入 `platforms_lost`,但明确
  标注依据不足。

```jsonc
"loss_accounting": {
  "basis": "AFSIM event stream (WEAPON_FIRED / PLATFORM_INITIALIZED), not name spelling",
  "launched_weapon_platforms": 14,
  "initialized_platforms": 9,
  "unverified_loss_count": 0
}
```

早期版本用名字关键词判定,把 `BLUE_SAM_SITE` 因为含 "sam" 当成了一枚导弹,
于是一份零战斗损失的仿真报出 9 架飞机损失。宁可报"依据不足",也不报一个看起来
精确、方向却错了的数字。

### 平台类型的域

`afsim_types` 的 `domain` 取自平台**自己声明的 mover**,并沿 `base` 链向上查找。
链上任何一环都没有声明 mover 的类型返回 `undetermined`——这是真实答案,不是失败。
早期版本会在类型名和定义文件路径里搜 "air""space" 这类词,结果把 GPS 归到
space、把 HVAA 和 SSM_MISSILE 归到 air。

## 仓库结构

```
core/knowledge/     AFSIM 知识库:只从安装目录提取事实
  source.py           归一化读取
  type_index.py       类型命名空间(platform_type / 组件类型 / 内置库类)
  mission_grammar.py  mission 输出的语法
core/policy.py      判断层:哪些事实算失败,具名可覆盖
core/runtime.py     执行器:进程
core/validation.py  执行器:跑 + 解析
core/analysis.py    执行器:evt → 指标
core/results.py     执行器:运行记录
core/server.py      配置解析与 JSON-RPC 分发
tools/              4 个工具的定义与路由
transport/stdio.py  stdio 传输入口
tests/
  test_layers.py     层级纪律:下层不得导入上层
  test_knowledge.py  知识层提取规则
  test_type_index.py 类型索引提取规则
  test_validation.py mission 输出解析
  test_protocol.py   MCP 协议
```

## 测试

```bash
python -m pytest tests/ -v
```

## 许可

MIT,见 `LICENSE`。

### 一个平台类型能不能开火

`platform_type` 继承是**叠加**的:派生类型只覆盖一个 mover,也会保留基类声明的
全部传感器、武器和处理器。所以只看类型自己那一段会把每个派生类型都读成裸的——
`SAM-MR_RB6` 会报"无传感器、无武器",而它其实继承了 `SAM-MR` 的导引头和四个
处理器。`afsim_types` 因此同时给出两份:

- `components` —— 这个类型自己声明的
- `effective_components` —— 沿 `base` 链解析后的实际配置

还有一个坑:老版本里 `armed` 的定义是"声明了传感器**或**武器",于是只有雷达、
根本没有导弹的阵地也被标成 armed。一个 Agent 挑中这样的类型建了防空阵地,工程
干净运行、返回 0、然后全场一弹未发,而 MCP 一直在说它是武装的。

现在分开报三个事实:

| 字段 | 含义 |
|---|---|
| `has_sensors` | 有效配置里有传感器 |
| `has_weapons` | 有效配置里有武器 |
| `has_processors` | 有效配置里有处理器 |
| `can_engage` | 同时有武器和处理器 |

`can_engage` 是必要条件而非充分条件:脚本处理器在变量没配之前仍然是惰性的,而
mission 不会在加载时拒绝一个没接线的平台——照样跑、照样返回 0、照样不开火。

### `<length-value>` 到底是什么

命令索引里每个带数值的参数都写成一个占位符——`altitude <length-value>`、
`end_time <time-value>`——而占位符正是语法变得不可校验的地方。`<length-value>`
是**两个 token**,第二个是单位,所以 `altitude 5000m msl` 会被拒,而索引无法
表达这个理由;`end_time 00:00:30` 也会被拒,因为 `<time-value>` 是一个数加一个
单位,不是时钟。这些全是一个 Agent 反复运行 mission 直到它不再报错试出来的。

AFSIM 在 `documentation/html/docs/argumenttypes.html` 里正式定义了这套产生式,
本项目直接读那份定义:

```
altitude <length-value>
  ->  altitude <real> <length-units>
      length-units 36 个可接受拼写: meters meter m kilometers km feet ft kft miles mi nm nmi cm mm ...
```

命令索引里 2267 个 `<*-value>` 占位符,现在 2068 个(91%)能被展开。剩下的是文档
本身没定义的(裸 `<value>`、`dbratio-value`、坐标类的 `X-value`),保持未定义。

拼写表里的每个词都来自 AFSIM 自己,不是谁试出来的。

### include 是怎么找文件的

`include` 找不到文件时的提示以前指向 demo 的顶层目录,于是给出
`file_path demos/suppressor`,而文件其实在 `demos/suppressor/platforms` 下面——
加了这行没有任何变化。

AFSIM 是把 `file_path` 条目拼到它被要求的名字上来找文件的,所以正确的条目不是文件
所在目录,而是**把请求的名字从文件路径里去掉之后剩下的目录**:

| mission 找不到 | 正确的 file_path |
|---|---|
| `sam_radar.txt` | `demos/suppressor/sensors/radar` |
| `sensors/radar/sam_radar.txt` | `demos/suppressor/platforms` |

搜索顺序取自 AFSIM File Commands 页的说明(它说这套算法和 C/C++ 预处理器一致):
先做 `$(name)` / `${name}` 路径变量替换,然后绝对路径直接用,然后拼到**当前正在
处理的那个文件**所在目录,最后依次拼到每个 `file_path` 条目。`declared_duration`
也走同一套遍历,所以 `end_time` 写在被 include 的文件里一样算数——以前只读入口
文件,于是一份跑了 7200 秒的仿真一边报"用了 AFSIM 的 60 秒默认值"、一边建议加一个
已经存在且生效的 `end_time`。

### 仿真事件名

`event_output` 的 `enable` / `disable` 接受事件名,mission 对不认识的名字报
`Invalid event name for event_output`。这些名字既不在命令索引里(索引收的是命令,
不是命令的参数),也不可靠地在 HTML 文档里:`documentation/html/docs/event/`
有 80 页,而 AFSIM 自带 demo 里实际出现的 21 个事件有 17 个不在其中,包括
`PLATFORM_BROKEN` 和全部 `SENSOR_TRACK_*`、`LOCAL_TRACK_*`。

它们的声明在 SDK 头文件里。每个事件结果类给 `cNAME` 字符串一个字面量,那就是
`event_output` 接受的全集。这些类不集中在一个头文件里:核心的在
`WsfEventResults.hpp`,每个扩展自带一份——`WsfMilEventResults.hpp` 携带整个
`WEAPON_*` 事件族(`WEAPON_FIRED`、`WEAPON_HIT`、`WEAPON_MISSED`……)。
只读核心头文件会静默丢掉 55 个名字(早期版本就丢过整个武器事件族),所以索引
对 SDK include 树里所有 `*EventResults*.hpp` 取并集:**153 个**。GUI 工具的
偏好设置类也用 `cNAME`,但没有一个叫这个名字的文件,按文件名过滤即可精确排除。

```bash
afsim_command kind=event keyword=SENSOR_TRACK   # 列出
afsim_command name=PLATFORM_BROKEN              # 查一个
afsim_command name=WEAPON_FIRED                 # 武器事件族(扩展头文件声明)
afsim_command keyword=PLATFORM_BROKEN           # 查不到命令时自动回落到事件名
```

每个事件还带一个 `seen_in_demos` 标记,表示它是否真的在 AFSIM 自带场景的输出里
出现过——153 个里有 21 个。

### 实例接线:类型没写在那里的那一半

`afsim_types(name=...)` 返回的类型事实是"这个类型是什么",但 BRAWLER_TEST 这类
战术类型族还有另一半事实藏在 demo 的入口想定里:**平台实例怎么接线**。指挥链
(`commander` / `command_chain`)、战术处理器读的 `script_variables`
(`MISSION_TYPE`、`ROUTE.Append`、`WINCHESTER`……)、`edit processor` 改配置——
这些都写在平台实例上,不写类型就不生效;入口文件自己还得带一个顶层
`script_variables` 全局块(`log`、`iout` 等),否则脚本上下文根本初始化不了。
省掉它们的表现完全静默:仿真照样跑、照样退出 0,只是跟踪了十分钟、一弹未发。

索引因此在构建时把 demo 入口里实例化每种类型的块解析出来(每型最多 3 份、按
完整度排序),连同入口文件的 include、命名航路和全局脚本变量一起返回。force
layout 常被入口委托给被 include 的 `scenarios/<名字>.txt`(往往经
`${SCNRIO}` 路径变量),而序言在父入口里——索引沿 include 链把父入口的
`$define`、序言逐行合并下来,并按"序言前 / 序言后"拆分父入口的 include:
消耗这些全局变量的 observer 必须在序言之后 include,放错位置脚本上下文就
初始化失败:

```
afsim_types(name="BLUE_FIGHTER")
  → demo_instantiations:
      file: air_to_air/scenarios/2v2.txt
      wiring:   commander SELF, command_chain IFLITE SELF, ...
                script_variables: START_TYPE = "route"; MISSION_TYPE = "SWEEP"; ...
      entry:    includes [platforms/lte_fighter.txt, observer.txt, ...]
                global_script_variables [closest_hostile, log_print, iout, ...]
```

照抄一份就是"这个类型被使用的方式"。配套地,`analyze_run` 把"跟踪很多、零开火"
单列为一个诊断项(`tracking_without_engagement`),并把原因指向实例接线而不是
让报告看起来只是运气不好。

### 脚本 API

战术处理器就是脚本,而脚本是写在对象上的——`PLATFORM`、`SENSOR`、`WEAPON`、
`TRACK`、`PROCESSOR`——以及它们的那些方法。这些此前在这个 server 上无处可查:
命令索引收的是输入文件命令,事件索引收的是事件名,所以"WsfTrack 有哪些方法"
在哪儿都没有答案。把一个处理器接起来,只能猜标识符,然后反复运行 mission 直到
它不再说 `Unknown identifier`。

产品自己文档化了:`documentation/html/docs/script/` 330 页,295 页是 WSF 类,
每页列出方法、返回类型、参数和说明;另外 208 个模型页在文档根目录也带脚本方法。
本项目把两处一起读,得到 **329 个类、4551 个方法**:

```bash
afsim_command kind=script name=WsfTrack        # 列出方法
afsim_command kind=script name=TrackQuality    # 按方法名反查是哪些类的
afsim_command kind=script keyword="track quality"  # 搜参数和说明正文
afsim_command kind=script                       # 列全部类
```

`WsfAssetManager` 的方法里有一组是 `GetEWRequired` / `GetTARRequired` /
`GetTTRRequired` / `GetWeaponRequired`——这就是"传感器没指定分类,资产永远不合格,
于是永远不开火"那条规则的接口面。现在它可查了。

`WsfTrack.TrackQuality()` 会连文档说明一起返回:*"Return the track's quality as
a value in the range [0..1]"*。

### 一个处理器要配成什么样才会开火

处理器出厂时 `SENSOR_NAME` / `WEAPON_NAME` 是空的,而 mission 拒绝在
`script_variables` 里重声明它们——只能在处理器内部的 `on_initialize` 块里赋值。

以前只返回第一条匹配,结果目录里 18 个需要外部配置的处理器,只有 1 个有示例,
其余 17 个被告知"要从脚本源码推断"——而答案就在旁边一行的派生类型里:

```
processor SR_SAM_TELAR_TASK_MGR TELAR_TASK_MGR
   on_initialize
      ACQ_SENSOR_NAME = "sr_sam_acq_radar_sensor";
      SENSOR_NAME     = "sr_sam_ttr_radar_sensor";
      WEAPON_NAME     = "sam";
      ...
```

现在按完整度排序返回**全部**示例,优先给出类型自己定义里那段(哪怕它在一个派生
类上),并逐条标明覆盖了哪些变量、还缺哪些。`TELAR_TASK_MGR`、`AA_TACTICS`、
`SAM_BATTERY_TASK_MGR` 都能直接拿到一份完整可抄的配置。

`on_initialize` 的存储也从"扁平赋值列表"改成"保留块边界"——实际使用时是整块照抄,
而需要 `SENSOR_NAME` 和 `WEAPON_NAME` 的处理器只有两者在**同一个块**里才工作。

### 交战包线

`zone` 是平台声明自己能在哪里活动、交战的地方,而 zone 的名字正是战术处理器里
`ENVELOPE = "..."` 要填的把手。两者必须一起给:只给范围不知道叫什么,只给名字
不知道边界。

```jsonc
"zones": [
  { "name": "full_kinematic", "shape": "circular",
    "extent": { "maximum_radius": "6 nm", "maximum_altitude": "20000 ft" } }
]
```

`SR_SAM_TELAR` 此前两个数字都看不见——`maximum_radius` 根本不在关键词表里,而且
`_performance` 只对组件调用、平台类型从来没调用过。一个 Agent 想知道这个阵地够
不着 9 km 高 60 km 外的目标,只能靠二分二十几个几何。现在一眼可见。

zones 和性能限值都沿 `base` 链继承,因为交战包线通常写在基类里。657 个平台类型
里 10 个声明了 zone,314 个带性能限值。

### 同一个关键字在不同块里参数不同

命令索引里 5646 条命令带着 `context` 字段,但埋得太深。`hits_to_establish_track`
要两个整数,而且只在传感器的 mode 块里合法——只读语法的人写
`hits_to_establish_track 3` 被拒,然后断定命令写错了,而不是自己放错了块。

现在每条 form 都带 `legal_in`(`_` 解释成 "top level")和一句说明。`end_time` 就有
四种形式,其中三种限定在别的块内。

TDQS

A3.8/5.0

Scored across 37 tools

Disambiguation5/5

Each tool targets a distinct action or resource: scenario, platform, component, simulation, results, or configuration. Even closely related tools like run_simulation, run_warlock, run_mission are clearly differentiated by their specific AFSIM binary and purpose. No two tools overlap in functionality.

Naming Consistency5/5

All tools follow a verb_noun naming pattern (e.g., create_scenario, list_platforms, run_simulation). Verbs are descriptive and consistent (create, delete, list, get, set, run, query, etc.). Even longer names like export_results_to_json maintain the pattern with additional qualifiers.

Tool Count3/5

With 37 tools, the server is above the typical range but still appropriate for the complex AFSIM domain. Each tool serves a specific need across scenario creation, platform management, simulation execution, and result analysis. The count is high but not excessive given the breadth of functionality.

Completeness5/5

The tool set covers the full lifecycle: scenario CRUD, platform and component management, simulation execution (multiple binaries), result querying/export, and configuration. There are no critical gaps; all major operations for managing AFSIM simulations are represented.

Maintenance

ActivityMaintained
ResponsivenessNo issues