Skip to main content
Glama
kevics1

GeoOntology-MCP

by kevics1
README.md
# GeoOntology-MCP

地理本体层 MCP 服务器——让 LLM 用地理结构推理,而非裸 SQL。

## 为什么需要这个项目

现有 PostGIS MCP 服务器只暴露原始 SQL 查询能力。LLM 拿到的是行和列,不是"南京属于江苏"或"苏州与上海相邻"这样的地理结构知识。

GeoOntology-MCP 暴露的是**地理本体层**——层级关系、拓扑邻接、语义消歧、空间运算、属性对比,以及核心创新:**地理约束校验**。LLM 生成地理断言后,MCP 可以校验真伪并返回结构化纠错,驱动 LLM 自我修正。

```
LLM: "苏州与上海不相邻"
  → check_adjacency("320500", "310000")
  ← { valid: false, correction: "苏州市 与 上海市 相邻,共享边界 42.3km",
       evidence: { shared_boundary_length_km: 42.3 } }
LLM: "我修正之前的说法,苏州与上海实际上是相邻的……"
```

## 架构

```
LLM Client ──MCP Protocol──► GeoOntology-MCP (TypeScript)
                                    │
                          ┌─────────┴─────────┐
                          │  Ontology Engine    │
                          │  - 层级遍历 (KG)    │
                          │  - 拓扑查询 (PostGIS)│
                          │  - 约束校验 (两层)  │
                          │  - 空间运算 (PostGIS)│
                          │  - 路径搜索 (BFS)   │
                          └─────────┬─────────┘
                                    │
                      ┌─────────────┴─────────────┐
                      │                           │
              Knowledge Graph (内存)        PostGIS (持久化)
              - 46,413 节点               - boundary geometries
              - 44,956 BELONGS_TO 边      - admin_adjacency (预计算)
              - 地名/拼音/英文索引         - admin_containment (预计算)
                                          - admin_attributes (属性数据)
```

**数据策略**:知识图谱 JSON 在启动时加载到内存,处理所有层级关系和地名消歧(纯图遍历,O(depth));邻接关系通过 PostGIS `ST_Touches` 预计算后存入 `admin_adjacency` 表,查询时 O(1)。

## 工具一览(10 个)

### 查询类

| 工具 | 参数 | 说明 |
|------|------|------|
| `resolve_place` | `name, context?` | 地名消歧,支持中文、拼音、英文。返回候选列表(ID、层级位置、坐标),按相关性排序取 top-5 |
| `get_ancestors` | `id` | 向上遍历层级链(县→市→省),返回从直接父到顶级的有序数组 |
| `get_neighbors` | `id` | 查询同层级邻接区域,含共享边界长度 |
| `describe_region` | `id, include_geometry?, geometry_tolerance_km?` | 区域全景:层级位置、邻居、面积、类型、子区数量,可选返回简化 GeoJSON |

### 校验类(核心差异化)

| 工具 | 参数 | 说明 |
|------|------|------|
| `check_containment` | `child_id, parent_id` | 验证包含关系,KG 层级检查 + PostGIS 空间回退 |
| `check_adjacency` | `id_a, id_b` | 验证邻接关系,预计算表 + 实时空间回退 |

### 空间运算类

| 工具 | 参数 | 说明 |
|------|------|------|
| `find_path` | `id_a, id_b, max_hops?` | BFS 搜索两区域间的邻接路径(如 江苏→浙江) |
| `measure_distance` | `id_a, id_b` | 两区域质心间距离 (km) |
| `find_within` | `id, radius_km` | 查找半径内的同层级区域 |

### 属性对比类

| 工具 | 参数 | 说明 |
|------|------|------|
| `compare_regions` | `ids[], metric` | 多区域属性排名对比(人口/GDP/面积) |

### 校验返回格式

校验通过:

```json
{
  "valid": true,
  "claim": "南京市 属于 江苏省",
  "explanation": "层级关系确认: 南京市(city) → 江苏省(province)"
}
```

校验失败 + 纠错:

```json
{
  "valid": false,
  "claim": "南京市 属于 浙江省",
  "explanation": "南京市 不属于 浙江省,实际属于 江苏省(province)",
  "correction": "实际归属: 南京市 属于 江苏省(province)",
  "evidence": {
    "actual_parent_id": "320000",
    "actual_parent_name": "江苏省",
    "actual_parent_level": "province"
  }
}
```

这种结构化返回使 LLM 能理解错误原因并自我修正——这就是**地理约束驱动的推理精炼环**。

## 地名消歧(多语言)

`resolve_place` 支持三级匹配:

1. **中文精确/模糊匹配** — "南京"、"南京市"、"江宁"
2. **拼音匹配** — "nanjing"、"jss"(首字母)、"jiangsusheng"
3. **英文别名匹配** — "Nanjing"、"Jiangsu"、"Canton"

拼音索引使用 `pinyin-pro` 库在启动时构建,英文别名覆盖所有省和主要地级市(~60 个)。

## Skills

### GeoRiddle(地理谜题推理)

`skills/geo-riddle/SKILL.md` 定义地理谜题推理工作流。LLM 解谜时自动调用 MCP 工具逐步缩小候选范围:

```
谜题: "我是江苏省面积最小的与安徽接壤的地级市"
→ get_children("320000")        → 获取江苏所有地级市
→ describe_region(每个市)        → 获取面积
→ check_adjacency(每个市, "340000") → 筛选与安徽相邻的
→ 综合约束得出答案: 南京市
```

5 道示例谜题(1-5 分难度),每道附带完整 solution trace。

### GeoFactCheck(地理断言校验)

`skills/geo-fact-check/SKILL.md` 定义 LLM 输出中的地理断言自动扫描与校验:

1. **扫描** — 识别包含/邻接/否定断言模式
2. **提取** — 解析为 (subject, relation, object) 三元组
3. **校验** — 调用 `check_containment` / `check_adjacency`
4. **报告** — verified / falsified / unverifiable 三类

支持中英文断言模式:

| 模式 | 示例 | 三元组 |
|------|------|--------|
| X属于Y | "南京属于江苏" | (南京, BELONGS_TO, 江苏) |
| X与Y相邻 | "江苏与浙江相邻" | (江苏, ADJACENT_TO, 浙江) |
| X不属于Y | "南京不属于浙江" | (南京, NOT_BELONGS_TO, 浙江) |

## MCP Resources

`mapping_knowledge/mapping_resources/` 目录下的 JSON 文件自动注册为 MCP Resource,LLM 可按需读取制图知识:

- `projection-advice.json` — 中国地图投影选择指南(Albers / Lambert / Web Mercator)
- `admin-level-guide.json` — 行政区划层级体系及编码规则

添加新 JSON 文件即可扩展,无需代码改动。

## 校验机制(双层)

1. **层级检查**(KG 内存遍历):遍历 `parentOf` 链,O(depth) ≈ 最多 4 次查找。无需数据库查询。
2. **空间回退**(PostGIS `ST_Contains` / `ST_Touches`):当 ID 不在 KG 中或需要空间验证时使用。单次查询约 16-23ms。

## 前置条件

- Node.js 18+
- PostgreSQL + PostGIS 扩展
- 本地 PostGIS 数据库包含中国行政区划表(`province`, `city`, `county`, `village`)
- 知识图谱 JSON 文件(`admin_knowledge_graph.json`)

### 数据库表结构

项目连接的 PostGIS 数据表:

| 表名 | 名称列 | 区划码列 | 几何列 |
|------|--------|----------|--------|
| `province` | `省` | `省级码` | `geom` |
| `city` | `地名` | `区划码` | `geom` |
| `county` | `地名` | `区划码` | `geom` |
| `village` | `name` | `area_code` | `geom` |

预计算表(`npm run materialize` 创建):

| 表名 | 说明 |
|------|------|
| `admin_adjacency` | 邻接关系(70 + 963 + 8134 对) |
| `admin_containment` | 包含关系闭包(预留) |
| `admin_attributes` | 区域属性(人口/GDP/面积) |

## 安装与运行

```bash
# 安装依赖
npm install

# 首次运行:预计算邻接表(约 30 秒)
npm run materialize

# 导入属性数据(人口/GDP/面积)
npx tsx scripts/materialize-attributes.ts

# 启动 MCP 服务器
npm run dev
```

启动后输出:

```
Loading knowledge graph...
Loaded 46413 nodes, 44956 edges
Connecting to PostGIS...
PostGIS connected
Registered 2 mapping resources
GeoOntology-MCP server running
```

## 配置

### 环境变量

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `KG_PATH` | (required) | 知识图谱 JSON 路径 |
| `MAPPING_RESOURCES_DIR` | `mapping_knowledge/mapping_resources` | 制图知识资源目录 |
| `PGHOST` | `127.0.0.1` | PostgreSQL 主机 |
| `PGPORT` | `5432` | PostgreSQL 端口 |
| `PGDATABASE` | (required) | 数据库名 |
| `PGUSER` | (required) | 数据库用户 |
| `PGPASSWORD` | (required) | 数据库密码 |

### Claude Desktop / Claude Code 配置

```json
{
  "mcpServers": {
    "geo-ontology": {
      "command": "npx",
      "args": ["tsx", "/path/to/GeoOntology-MCP/src/index.ts"],
      "env": {
        "KG_PATH": "/path/to/admin_knowledge_graph.json",
        "PGHOST": "127.0.0.1",
        "PGPORT": "5432",
        "PGDATABASE": "your_database",
        "PGUSER": "your_user",
        "PGPASSWORD": "your_password"
      }
    }
  }
}
```

## 使用示例

### 典型工作流:先消歧,再查询,最后校验

```
→ resolve_place(name="南京")
← [{ id: "320100", name: "南京市", level: "city", parentName: "江苏省", score: 104 }]

→ get_ancestors(id="320100")
← [{ id: "320000", name: "江苏省", level: "province", type: "省" }]

→ check_containment(child_id="320102", parent_id="320000")
← { valid: true, claim: "玄武区 属于 江苏省",
     explanation: "层级关系确认: 玄武区(county) → 南京市(city) → 江苏省(province)" }
```

### 拼音/英文搜索

```
→ resolve_place(name="Nanjing")
← [{ id: "320100", name: "南京市", level: "city", score: 90 }]

→ resolve_place(name="jss")
← [{ id: "320000", name: "江苏省", level: "province", score: 40 }]
```

### 路径搜索

```
→ find_path(id_a="320000", id_b="330000")
← { found: true, path: ["320000", "330000"], pathNames: ["江苏省", "浙江省"],
     hops: 1, totalBoundaryKm: 152.7 }
```

### 距离与范围查询

```
→ measure_distance(id_a="320100", id_b="330100")
← { idA: "320100", nameA: "南京市", idB: "330100", nameB: "杭州市",
     distanceKm: 267.4, method: "centroid-to-centroid" }

→ find_within(id="320100", radius_km=200)
← [{ id: "330100", name: "杭州市", distanceKm: 267.4, ... }, ...]
```

### 属性对比

```
→ compare_regions(ids=["110000", "310000", "320000"], metric="population")
← { metric: "population", regions: [
     { id: "320000", name: "江苏省", value: 85054000, rank: 1 },
     { id: "110000", name: "北京市", value: 21893095, rank: 2 },
     { id: "310000", name: "上海市", value: 24870895, rank: 3 }
   ] }
```

### 几何数据

```
→ describe_region(id="110000", include_geometry=true, geometry_tolerance_km=0.5)
← { id: "110000", name: "北京市", ..., geometry: { type: "Polygon", coordinates: [...] },
     geometryToleranceKm: 0.5 }
```

## 项目结构

```
src/
  index.ts                        # MCP 服务器入口
  data-source/
    interface.ts                  # GeoDataSource 接口 + 所有类型定义
    postgis-adapter.ts            # PostGIS 实现(核心适配器)
    column-map.ts                 # 表名/列名映射 + inferLevel()
  knowledge-graph/
    types.ts                      # KG 节点/边类型(含拼音/别名字段)
    loader.ts                     # KG JSON 加载 + 拼音索引 + 别名合并
    english-aliases.ts            # 省市英文名映射 (~60个)
  ontology/
    hierarchy.ts                  # 层级遍历(getAncestors, getChildren)
    resolver.ts                   # 地名消歧(中文/拼音/英文四级匹配)
    verifier.ts                   # 包含关系校验(KG 层级检查)
    pathfinder.ts                 # BFS 路径搜索(纯函数)
  tools/
    registration.ts               # 10 个工具注册到 McpServer
  resources/
    registration.ts               # MCP Resource 注册(mapping_knowledge)
scripts/
  materialize-adjacency.ts        # 邻接预计算脚本
  materialize-attributes.ts       # 属性数据导入脚本
skills/
  geo-riddle/SKILL.md             # GeoRiddle 谜题推理 skill
  geo-fact-check/SKILL.md         # GeoFactCheck 断言校验 skill
mapping_knowledge/
  mapping_resources/              # MCP Resource JSON 文件
    projection-advice.json        # 投影选择指南
    admin-level-guide.json        # 行政区划编码规则
data/
  admin-attributes.csv            # 省级属性数据(人口/GDP/面积)
tests/
  knowledge-graph.test.ts         # KG 加载器测试
  hierarchy.test.ts               # 层级遍历测试
  resolver.test.ts                # 地名消歧测试
  verifier.test.ts                # 校验逻辑测试
  column-map.test.ts              # 列映射测试
  pinyin-resolver.test.ts         # 拼音消歧测试
  path.test.ts                    # BFS 路径搜索测试
  fixtures/
    sample-kg.json                # 测试用样本 KG
```

## 可插拔数据源

所有工具通过 `GeoDataSource` 接口访问数据,PostGIS 只是默认实现:

```typescript
interface GeoDataSource {
  connect(): Promise<void>;
  close(): Promise<void>;
  getRegionInfo(id: string): Promise<RegionInfo | null>;
  getAncestors(id: string): Promise<HierarchyNode[]>;
  getChildren(id: string): Promise<HierarchyNode[]>;
  getNeighbors(id: string): Promise<AdjacencyResult[]>;
  checkContainment(childId: string, parentId: string): Promise<VerificationResult>;
  checkAdjacency(idA: string, idB: string): Promise<VerificationResult>;
  resolvePlace(name: string, context?: string): Promise<PlaceCandidate[]>;
  findPath(idA: string, idB: string, maxHops?: number): Promise<PathResult>;
  getRegionGeometry(id: string, toleranceKm?: number): Promise<RegionGeometry | null>;
  measureDistance(idA: string, idB: string): Promise<DistanceResult | null>;
  findWithin(id: string, radiusKm: number): Promise<WithinResult[]>;
  getAttributes(id: string): Promise<RegionAttributes | null>;
  compareRegions(ids: string[], metric: 'population' | 'gdp' | 'areaSqKm'): Promise<CompareResult>;
}
```

## 测试

```bash
npm test
```

50 个单元测试覆盖:KG 加载器、层级遍历、地名消歧(中文+拼音)、校验逻辑、列映射、BFS 路径搜索。测试使用 `tests/fixtures/sample-kg.json` 样本数据,无需连接数据库。

## 技术栈

- **TypeScript** + ESM (`@modelcontextprotocol/sdk`)
- **pg** — PostgreSQL/PostGIS 客户端
- **pinyin-pro** — 拼音转换
- **zod** — 工具参数校验
- **Vitest** — 单元测试

## License

MIT

Maintenance

ActivityInactive
ResponsivenessNo issues