Metadata-Version: 2.3
Name: gauss-mcp
Version: 0.1.3
Summary: MCP server for Gauss automatic import workflows
Requires-Dist: httpx>=0.28.1
Requires-Dist: mcp>=1.12.4
Requires-Dist: pydantic>=2.11.0
Requires-Dist: pydantic-settings>=2.10.1
Requires-Dist: tenacity>=9.1.2
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# gauss-mcp

一个基于 FastMCP 的 Python MCP server，用来把公开图片 URL 驱动到 Gauss 自动导入链路；配合 `gauss-import` skill 时，也支持先把本地图片上传到 OUS，再继续导入。

当前 v1 聚焦这条主链路：

1. `ImageType`
2. `Img2Pano`（仅普通图需要）
3. `PanoClear`（按需）
4. `Pano2PointCloud`
5. `SpatialTune`
6. `GaussImport`
7. `GaussInfo`（轮询 `v2/gauss/info` 直到展示信息就绪）
8. 返回 `design_id` / `plan_id` / `level_id` / `result_url` / `gauss_info_status` / `gauss_splat_url` / `gauss_drc_url`

不在本轮范围内：渲染发布。

## 能力概览

- 运行状态持久化到 SQLite + artifact 文件
- 支持中断恢复，不依赖会话上下文
- 支持从中间步骤派生 variant run
- 支持导出单步调试信息和完整 run report
- `clear_furniture`、`floor_height_m` 缺失时显式停下来等待用户输入
- `ImageType` 失败时，skill 可以向用户确认是否为全景图，再通过人工覆盖继续推进
- `GaussImport` 成功后会继续轮询 `v2/gauss/info`，直到展示信息可读
- 如果本地等待窗口结束但展示信息仍在生成，run 会保持 `running`，后续继续 `gauss_resume_run` 即可

## 工作流 UML

下面这张状态图表达 run 从创建到展示信息就绪的主链路，以及 3 个显式人机 gate。

```mermaid
stateDiagram-v2
    [*] --> Created: gauss_create_run

    state "等待人工确认是否全景图" as ManualPanoramaConfirm
    state "全景图已就绪" as PanoramaReady
    state "等待 clear_furniture" as WaitClearFurniture
    state "等待 floor_height_m" as WaitFloorHeight
    state "展示信息仍在生成" as DisplayGenerating
    state imageTypeResult <<choice>>
    state clearDecision <<choice>>
    state floorDecision <<choice>>

    Created --> ImageType: gauss_run_image_type / gauss_resume_run
    ImageType --> imageTypeResult
    imageTypeResult --> PanoramaReady: is_pano=true\npanorama_url=source_url
    imageTypeResult --> Img2Pano: is_pano=false
    ImageType --> ManualPanoramaConfirm: 失败 / 疑似误判
    Img2Pano --> PanoramaReady: panorama_url ready
    Img2Pano --> ManualPanoramaConfirm: 异常且怀疑误判
    ManualPanoramaConfirm --> PanoramaReady: gauss_set_run_options(is_pano=true)
    ManualPanoramaConfirm --> Img2Pano: gauss_set_run_options(is_pano=false)

    PanoramaReady --> clearDecision
    clearDecision --> WaitClearFurniture: clear_furniture 缺失
    clearDecision --> PanoClear: clear_furniture=true
    clearDecision --> Pano2PointCloud: clear_furniture=false
    WaitClearFurniture --> clearDecision: gauss_set_run_options(clear_furniture)

    PanoClear --> Pano2PointCloud: cleared_panorama_url ready
    Pano2PointCloud --> SpatialTune: point cloud ready

    SpatialTune --> floorDecision
    floorDecision --> WaitFloorHeight: floor_height_m 缺失
    floorDecision --> GaussImport: floor_height_m ready
    WaitFloorHeight --> GaussImport: gauss_set_run_options(floor_height_m)

    GaussImport --> GaussInfo: result_url / ids ready
    GaussInfo --> DisplayGenerating: gauss_info_status=generating
    DisplayGenerating --> GaussInfo: gauss_resume_run
    GaussInfo --> Completed: gauss_info_status=ready\n返回 result_url / splat / drc
    Completed --> [*]
```

补充说明：

- MCP 主链路当前仍消费公开图片 URL；`gauss-import` skill 已支持通过 `gauss_get_ous_upload_token` + `/ous-upload` 先上传本地图片，再继续导入。
- `clear_furniture`、`floor_height_m`、`is_pano` 人工覆盖都是显式 gate，不会在 MCP 内部静默填默认值。
- `rotation_z` 未显式提供时，会默认复用 `SpatialTune.rotate`。

## 环境要求

- Python 3.11+
- `uv`

## 安装

### 本地开发

```bash
uv sync
```

### 已发布包

如果已经发布到 PyPI 或内网 Python 仓库，其他人可以直接运行：

```bash
uvx gauss-mcp
```

## 配置

### 必填鉴权配置

本地开发可以创建 `.env`，变量名必须使用 `GAUSS_*` 前缀：

```dotenv
GAUSS_APPUID=your_appuid
GAUSS_APPKEY=your_appkey
GAUSS_APPSECRET=your_appsecret
GAUSS_API_BASE_URL=https://api-beta.kujiale.com/p/openapi/
GAUSS_WEB_BASE_URL=https://www.kujiale.com
```

### 可选运行时配置

```dotenv
GAUSS_STATE_DIR=/absolute/path/to/gauss-mcp/state
GAUSS_RUNS_DIR=/absolute/path/to/gauss-mcp/runs
GAUSS_TIMEOUT_S=300
GAUSS_POLL_INTERVAL_S=2
GAUSS_LOG_LEVEL=INFO
GAUSS_TRANSPORT=stdio
GAUSS_HOST=127.0.0.1
GAUSS_PORT=8000
```

### 全量参数说明

- `GAUSS_APPUID`：OpenAPI 鉴权所需的 appuid。默认 `None`，调用真实接口时通常必填。
- `GAUSS_APPKEY`：OpenAPI 鉴权所需的 appkey。默认 `None`。
- `GAUSS_APPSECRET`：OpenAPI 鉴权所需的 appsecret。默认 `None`。
- `GAUSS_API_BASE_URL`：OpenAPI 基础地址。默认 `https://api-beta.kujiale.com/p/openapi/`。
- `GAUSS_WEB_BASE_URL`：结果页基础地址，用于拼接 `result_url`。默认 `https://www.kujiale.com`。
- `GAUSS_STATE_DIR`：SQLite 状态目录。默认行为：
  - 从源码仓库直接运行时，使用 `<repo>/data/state`
  - 从已安装包或 `uvx gauss-mcp` 运行时，使用 `~/.gauss-mcp/state`
- `GAUSS_RUNS_DIR`：run artifact 目录。默认行为：
  - 从源码仓库直接运行时，使用 `<repo>/data/runs`
  - 从已安装包或 `uvx gauss-mcp` 运行时，使用 `~/.gauss-mcp/runs`
- `GAUSS_TIMEOUT_S`：单步远端轮询等待窗口，单位秒。默认 `300`。
- `GAUSS_POLL_INTERVAL_S`：轮询间隔，单位秒。默认 `2`。
- `GAUSS_LOG_LEVEL`：日志级别。默认 `INFO`。
- `GAUSS_TRANSPORT`：MCP 传输方式。默认 `stdio`。可选值：`stdio`、`sse`、`streamable-http`。
- `GAUSS_HOST`：HTTP 类传输绑定地址。默认 `127.0.0.1`；仅在 `GAUSS_TRANSPORT=sse` 或 `streamable-http` 时生效。
- `GAUSS_PORT`：HTTP 类传输监听端口。默认 `8000`；仅在 `GAUSS_TRANSPORT=sse` 或 `streamable-http` 时生效。

补充说明：

- `database_path` 由 `GAUSS_STATE_DIR` 派生，固定为 `<state_dir>/gauss_mcp.sqlite3`，不单独暴露环境变量。
- 分发给其他人时，推荐显式配置 `GAUSS_STATE_DIR`、`GAUSS_RUNS_DIR`，避免状态写入默认目录后不易排查。
- 不要把真实密钥提交到仓库。

## 启动 MCP Server

默认使用 `stdio` 传输。

如果设置 `GAUSS_TRANSPORT=sse` 或 `GAUSS_TRANSPORT=streamable-http`，进程会改为监听 `GAUSS_HOST:GAUSS_PORT`。

### 从源码仓库启动

```bash
uv run gauss-mcp
```

或：

```bash
uv run python -m gauss_mcp
```

### 从已发布包启动

```bash
uvx gauss-mcp
```

## 打包与发布

当前工程已经具备 Python 包分发能力：

- `pyproject.toml` 已声明 `uv_build` 构建后端
- 已提供 console script：`gauss-mcp`
- 可以构建 `sdist` / `wheel` 后上传到 PyPI 或内网仓库

典型发布流程：

```bash
uv build
uv run twine check dist/*
uv run twine upload dist/*
```

说明：

- 发布前记得更新 `pyproject.toml` 中的 `project.version`
- 如果使用内网仓库，可按仓库要求补充 `twine upload` 参数
- 发布完成后，其他人可直接通过 `uvx gauss-mcp` 使用

## Claude Code / MCP 配置

推荐优先使用“已发布包 + `uvx`”的方式分发给其他人，不要求对方 clone 仓库。

### 方式一：用 CLI 添加到 Claude Code

添加到当前项目：

```bash
claude mcp add --scope project \
  --env GAUSS_APPUID=your_appuid \
  --env GAUSS_APPKEY=your_appkey \
  --env GAUSS_APPSECRET=your_appsecret \
  --env GAUSS_API_BASE_URL=https://api-beta.kujiale.com/p/openapi/ \
  --env GAUSS_WEB_BASE_URL=https://www.kujiale.com \
  gauss-mcp -- uvx gauss-mcp
```

如果想给自己全局使用，把 `--scope project` 改成 `--scope user`。

### 方式二：在项目根目录提供 `.mcp.json`

```json
{
  "mcpServers": {
    "gauss-mcp": {
      "command": "uvx",
      "args": ["gauss-mcp"],
      "env": {
        "GAUSS_APPUID": "your_appuid",
        "GAUSS_APPKEY": "your_appkey",
        "GAUSS_APPSECRET": "your_appsecret",
        "GAUSS_API_BASE_URL": "https://api-beta.kujiale.com/p/openapi/",
        "GAUSS_WEB_BASE_URL": "https://www.kujiale.com"
      }
    }
  }
}
```

配置建议：

- 本地开发继续用 `.env` + `uv run gauss-mcp` 即可
- 分发给其他人时，更推荐用 Claude Code 的 `env` 配置显式注入参数，不要假设对方工作目录里一定有 `.env`
- 如果团队要共享 `.mcp.json`，不要把真实密钥直接提交到仓库

## 主要工具

下面这张 UML 结构图按职责分层展示 MCP 暴露的 tool / resource 面。

```mermaid
classDiagram
    class RunManagement {
      +gauss_create_run()
      +gauss_get_run()
      +gauss_list_runs()
      +gauss_set_run_options()
      +gauss_resume_run()
      +gauss_create_variant()
    }

    class UploadSupport {
      +gauss_get_ous_upload_token()
    }

    class StepExecution {
      +gauss_run_image_type()
      +gauss_run_img2pano()
      +gauss_run_pano_clear()
      +gauss_run_pano2pointcloud()
      +gauss_run_spatial_tune()
      +gauss_run_gauss_import()
      +gauss_run_gauss_info()
    }

    class DebugExport {
      +gauss_get_step()
      +gauss_retry_step()
      +gauss_export_run_report()
    }

    class ResourceView {
      +run_summary()
      +step_list()
      +step_detail()
      +run_report()
    }

    class WorkflowRun {
      +run_id
      +status
      +current_step
      +state
    }

    class StepSnapshot {
      +step_key
      +status
      +remote_task_id
      +request_path
      +submit_response_path
      +last_poll_response_path
      +output_path
      +error_path
    }

    RunManagement --> WorkflowRun : 创建 / 查询 / 推进
    StepExecution --> WorkflowRun : 推进状态机
    StepExecution --> StepSnapshot : 持久化单步快照
    DebugExport --> StepSnapshot : 调试 / 重试 / 导出
    ResourceView --> WorkflowRun : 只读访问
    ResourceView --> StepSnapshot : 只读访问
```

`ResourceView` 对应的实际 URI：

- `gauss://runs/{run_id}`
- `gauss://runs/{run_id}/steps`
- `gauss://runs/{run_id}/steps/{step_key}`
- `gauss://runs/{run_id}/report`

### Run 管理

- `gauss_create_run`
- `gauss_get_run`
- `gauss_list_runs`
- `gauss_set_run_options`
- `gauss_resume_run`
- `gauss_create_variant`

### 上传辅助

- `gauss_get_ous_upload_token`

### Step 执行

- `gauss_run_image_type`
- `gauss_run_img2pano`
- `gauss_run_pano_clear`
- `gauss_run_pano2pointcloud`
- `gauss_run_spatial_tune`
- `gauss_run_gauss_import`
- `gauss_run_gauss_info`

### 调试与导出

- `gauss_get_step`
- `gauss_retry_step`
- `gauss_export_run_report`

### Resources

- `gauss://runs/{run_id}`
- `gauss://runs/{run_id}/steps`
- `gauss://runs/{run_id}/steps/{step_key}`
- `gauss://runs/{run_id}/report`

## Skill 交互约定

推荐配合 `.claude/skills/gauss-import/SKILL.md` 使用。

正常情况下，skill 会采用“逐步展示、逐步汇报”的策略，而不是一上来就用一次 `gauss_resume_run(wait=true)` 把多个步骤吞在一起。

### 本地图片输入时

如果用户给的是本地图片路径，skill 会先：

1. 调用 `gauss_get_ous_upload_token` 获取 `ousToken` / `globalDomain` / `blockSize`
2. 调用 `/ous-upload` skill 上传本地文件
3. 如果 `/ous-upload` skill 不可用，提示用户到 `https://coops.qunhequnhe.com/ai/skill#/detail?name=ous-upload&namespaceId=dev` 安装
4. 拿到上传后的公开 URL 后，再继续 `gauss_create_run`

#### OUS 上传前置条件与成功判定

本地图片链路依赖 `gauss_get_ous_upload_token` 和 `/ous-upload` skill 配合完成。`gauss_get_ous_upload_token` 返回的 `ousToken`、`globalDomain`、`blockSize` 需要原样传给 `/ous-upload` skill；其中 `ousToken` 用于上传鉴权，`blockSize` 决定走单文件上传还是分片上传，`bucketType` 如果存在可以忽略。

`/ous-upload` skill 会按下面 3 个阶段执行：

1. 准备阶段：确认本地文件存在且可读，计算文件大小和 MD5；当 `file_size <= blockSize` 时走单文件上传，否则按 `ceil(file_size / blockSize)` 拆分分片，分片序号从 `1` 开始。
2. 上传阶段：单文件走 `POST /ous/api/v2/single/upload`；分片上传先走 `POST /ous/api/v2/block/upload/init`，再按需调用 `POST /ous/api/v2/block/upload/part` 上传缺失分片。
3. 状态轮询阶段：只有在上传请求成功建立任务后，才调用 `GET /ous/api/v2/upload/status` 轮询结果。

成功与失败以终态 `status` 为准：

- `status = 5`：上传成功，此时才返回最终 `url` 和 `uploadKey`
- `status = 6` 或 `status = 8`：上传失败
- 轮询间隔不要低于 `200ms`，总轮询时间不超过 `2 分钟`
- 即使响应里提前出现 `uploadKey`、`obsTaskId` 或其他中间态字段，也不能视为上传成功

如果 `/ous-upload` skill 不可用，本地图片导入链路不会自动降级为 MCP 直接上传；需要先安装该 skill，拿到公开 URL 后再继续后续 Gauss 导入步骤。

### 新建导入任务时

skill 会按下面的顺序执行，并在每一步结束后明确展示结果：

1. `gauss_create_run`
2. `gauss_run_image_type`
3. `gauss_run_img2pano`（仅 `is_pano=false` 时）
4. `gauss_run_pano_clear`（按需）
5. `gauss_run_pano2pointcloud`
6. `gauss_run_spatial_tune`
7. `gauss_run_gauss_import`
8. `gauss_run_gauss_info`

展示时会尽量明确：

- 当前 step 是否执行
- 关键结果是什么
- 哪些 step 被跳过
- 跳过原因是什么
- 下一步将做什么
- 当首次拿到可用全景图时，当前回复里同时展示 markdown 图片 `![全景图](...)` 和 URL 文本
- 如果 `PanoClear` 产出了新的 `cleared_panorama_url`，会再次展示清家具后的全景图和 URL

例如：

- `ImageType` 已执行，结果是全景图 → `Img2Pano` 会被明确标记为“已跳过，原因是当前图已可直接视为全景图”，并在当前回复里同时展示 `![全景图](<source_url>)` 和原始 URL
- `ImageType` 已执行，结果是普通图 → 下一步将显式执行 `Img2Pano`；一旦拿到 `panorama_url`，会立即展示全景图预览和 URL
- 如果用户选择清家具，且 `PanoClear` 返回了新的 `cleared_panorama_url` → 会再次展示清家具后的全景图预览和 URL

### 已有 run 时

如果用户给的是 `run_id`，skill 会优先复用当前 run，而不是重建：

1. 先调用 `gauss_get_run(include_steps=true)` 读取当前状态
2. 明确展示 run 的 `status`、`current_step` 和已完成步骤
3. 需要继续时再调用 `gauss_resume_run` 或对应单步工具
4. 失败时先读取失败 step 详情，再向用户解释原因和下一步建议

### 显式人机 gate

当前仍有 3 个显式的人机 gate：

1. `clear_furniture` 未提供
2. `floor_height_m` 未提供
3. `ImageType` / `Img2Pano` 异常后，怀疑图片类型误判，需要人工确认是否为全景图

第 3 个 gate 的典型处理方式：

- `ImageType` 失败 → 先读取 `image_type` step 详情，再询问用户这是不是全景图
- `Img2Pano` 异常，且前面存在 `is_pano=false` 的判断 → 明确告诉用户 `ImageType` 这一步已经跑过了，但当前怀疑可能误判，再询问用户这张图其实是不是全景图
- 用户回答“是全景图” → `gauss_set_run_options(is_pano=true)`，再继续 `gauss_resume_run(wait=true)`
- 用户回答“不是全景图” → `gauss_set_run_options(is_pano=false)`，再继续 `gauss_resume_run(wait=true)`

## 开发与验证

```bash
uv run ruff check .
uv run mypy src
uv run pytest
uv build
uv run twine check dist/*
```

## 真实环境 smoke test

真实 smoke test 会在上游创建实际任务。执行前请确认：

- `.env` 中已配置有效鉴权信息
- 输入的是公开可访问的图片 URL
- 你接受在测试环境创建真实导入任务

## 项目结构

```text
src/gauss_mcp/
  __main__.py
  client.py
  config.py
  models.py
  server.py
  workflow.py
  persistence/
tests/
```

## 当前状态

本地质量门禁已打通：

- `ruff`
- `mypy`
- `pytest`
- `uv build`
- `twine check`

如果下一步要做真实链路验证，建议直接围绕 `gauss-import` skill 跑一次完整 smoke。
