Skip to main content
Glama
README.md
# move-api-mcp

A local [MCP](https://modelcontextprotocol.io) server for the **Move AI UGC GraphQL API**
(`https://api.move.ai/ugc/graphql`). It gives Claude Code, Claude Desktop, Cursor and any
other MCP client typed tools for the markerless mocap pipeline — upload footage, create
takes, run single- and multi-camera jobs, poll progress and pull the output files down.

- Transport: **stdio** (runs locally, nothing is exposed on the network)
- Auth: **API key** (`Authorization: mv_key_…`) or **JWT bearer** (`Authorization: Bearer …`)
- Mutations are **refused unless writes are explicitly enabled** — mocap jobs are billable
- Schema reference: <https://move-ai.github.io/move-ugc-api/> ·
  docs: <https://developers.move.ai/docs/api-reference>

---

## Install

```bash
git clone <this repo> && cd move-api-mcp

# with a venv (no extra tooling needed)
python -m venv .venv
.venv/bin/pip install -e '.[dev]'
.venv/bin/move-api-mcp --version

# or with uv, if you have it
uv sync
uv run move-api-mcp --version
```

Requires Python 3.11+. Whichever you pick, note the absolute path of the launcher —
`.venv/bin/move-api-mcp` or `uv` — because MCP clients need it below.

## Configure

Copy `.env.example` and set **one** credential:

| Variable | Default | Purpose |
|---|---|---|
| `MOVE_API_KEY` | — | API key from [dev.move.ai](https://dev.move.ai). Sent verbatim: `Authorization: mv_key_…` |
| `MOVE_API_JWT` | — | JWT / OAuth access token. Sent as `Authorization: Bearer …` |
| `MOVE_API_TOKEN` | — | Alias for either; the scheme is auto-detected from the token shape |
| `MOVE_API_ENDPOINT` | `https://api.move.ai/ugc/graphql` | Use `https://api-test.move.ai/ugc/graphql` for the test stack |
| `MOVE_API_AUTH_SCHEME` | `auto` | `auto` \| `bearer` \| `raw` — force the `Authorization` format |
| `MOVE_API_ALLOW_WRITES` | `false` | Must be `true` before any mutating tool will run |
| `MOVE_API_TIMEOUT` | `30` | HTTP timeout in seconds |
| `MOVE_API_MAX_UPLOAD_BYTES` | `5368709120` (5 GiB) | Upload size ceiling |
| `MOVE_API_DOWNLOAD_DIR` | unset | When set, downloads may only be written inside this directory |

Precedence for the credential is `MOVE_API_JWT` → `MOVE_API_TOKEN` → `MOVE_API_KEY`.
`auto` sends `Bearer` for JWT-shaped tokens (three dot-separated segments) and the raw
value for Move API keys, which the API expects **without** a scheme prefix.

CLI flags override the environment: `--endpoint`, `--auth-scheme`, `--allow-writes`,
`--read-only`.

## Register with an MCP client

### Claude Code

Substitute your own checkout path for `/path/to/move-api-mcp` — `claude mcp add` will
happily register the placeholder, and the server then fails to start.

```bash
# venv install
claude mcp add move-api \
  --env MOVE_API_KEY=mv_key_… \
  -- /path/to/move-api-mcp/.venv/bin/move-api-mcp

# uv install
claude mcp add move-api \
  --env MOVE_API_KEY=mv_key_… \
  -- uv --directory /path/to/move-api-mcp run move-api-mcp
```

Re-registering under a name that already exists is refused, so change it with
`claude mcp remove move-api` followed by a fresh `add`. `claude mcp get move-api`
shows what is currently registered.

### Claude Desktop / Cursor (`claude_desktop_config.json`, `.cursor/mcp.json`)

```json
{
  "mcpServers": {
    "move-api": {
      "command": "/path/to/move-api-mcp/.venv/bin/move-api-mcp",
      "env": {
        "MOVE_API_KEY": "mv_key_…",
        "MOVE_API_ALLOW_WRITES": "false"
      }
    }
  }
}
```

Enable writes only when you intend to spend processing credits:

```json
"env": { "MOVE_API_KEY": "mv_key_…", "MOVE_API_ALLOW_WRITES": "true" }
```

Check it works by asking the client to call `move_client_info` — it returns the account
the credential belongs to, the endpoint, the auth scheme in use and whether writes are on.

## Tools

Read-only (always available):

| Tool | What it does |
|---|---|
| `move_client_info` | Current account + effective server config |
| `move_get_file` | File details and a fresh presigned URL |
| `move_download_file` | Stream a file to local disk |
| `move_get_take` / `move_list_takes` | Takes, with `expand` for sources, volume, files |
| `move_get_job` / `move_list_jobs` | Jobs, state and `progress.percentageComplete` |
| `move_download_job_outputs` | Pull every (or selected) output of a finished job |
| `move_get_volume` / `move_list_volumes` | Multicam calibration volumes |
| `move_list_rigs` | Available skeleton rigs |
| `move_list_camera_settings` | Supported camera lens ids |

Mutating (need `MOVE_API_ALLOW_WRITES=true`):

| Tool | What it does |
|---|---|
| `move_create_file` | Reserve a file and get a presigned PUT URL |
| `move_upload_file` | Create **and** upload a local video in one step |
| `move_update_file` / `move_update_take` / `move_update_job` | Rename / set metadata |
| `move_create_single_cam_take` / `move_create_multi_cam_take` | Group footage into a take |
| `move_create_single_cam_job` / `move_create_multi_cam_job` | Start mocap processing (billable) |
| `move_create_volume_with_human` | Calibrate a multicam rig from footage of a person (billable) |
| `move_generate_share_code` | Shareable link for an output file |
| `move_update_client` | Set account metadata |
| `move_upsert_webhook_endpoint` | Create/update a webhook subscription |

Escape hatch: `move_graphql` runs an arbitrary document for fields the typed tools do not
cover. Mutations detected in it are gated the same way.

`expand` arguments mirror `move-ugc-python`'s: pass `["outputs"]`, `["take", "inputs"]`,
`["sources", "volume"]` and so on to pull nested objects in a single round trip.

## Workflows

**Single camera**

1. `move_upload_file` — the `.mp4` (plus the matching `.move` file for Move One footage;
   both share a `device_label`)
2. `move_create_single_cam_take` with those file ids
3. `move_create_single_cam_job` with the take id
4. Poll `move_get_job` until `state` is `FINISHED` (`NOT_STARTED → STARTED → RUNNING →
   FINISHED` / `FAILED`)
5. `move_download_job_outputs`

**Multi camera**

1. `move_list_camera_settings` — find each camera's lens id
2. `move_upload_file` per calibration clip → `move_create_volume_with_human`
   (needs the performer's height in metres)
3. Poll `move_get_volume` until `FINISHED`
4. `move_upload_file` per action clip → `move_create_multi_cam_take` with the volume id,
   re-using the same `device_label`s and lenses
5. `move_create_multi_cam_job` → poll → download

## Design notes

- **`AWSJSON`** — the API's `metadata` fields carry JSON *as a string*. Tools take a normal
  JSON object and encode/decode it for you; `move_graphql` does not (pass strings there).
- **Pagination** — list tools return `{items, count, next_cursor}`; feed `next_cursor`
  back as `after`.
- **Presigned URLs** — short-lived, and the same field is an upload target on input files
  and a download link on outputs. Fetch them at the point of use.
- **Uploads** stream in 8 MiB chunks with an explicit `Content-Length`, so large takes
  don't sit in memory and S3 doesn't reject chunked encoding.
- **Errors** — GraphQL errors are surfaced with the Move error code (`MV_010_010_0001`)
  and any suggestions attached; a 401/403 says which header format was sent.
- **Downloads** are confined to `MOVE_API_DOWNLOAD_DIR` when it is set, and never
  overwrite without `overwrite=true`.

## Development

```bash
pip install -e '.[dev]'
pre-commit install          # mandatory before committing

pytest                      # unit tests
pytest --cov                # with coverage (fails under 90%)
ruff check . && ruff format --check .
mypy src

# stdio handshake against the real MCP client SDK — no Move API call is made
MOVE_API_KEY=mv_key_dummy python scripts/smoke_stdio.py
```

The transport is exercised through `httpx.MockTransport`; no test touches the real API.

```
src/move_api_mcp/
  config.py     env -> Settings, credential + auth-scheme resolution
  client.py     GraphQL/HTTP transport, AWSJSON encoding, upload + download
  errors.py     Move error codes and suggestions
  graphql.py    documents and expandable selection sets
  models.py     typed tool inputs (Source, SyncMethod, JobOptions, ClipWindow)
  server.py     MCPServer assembly, instructions, lifespan
  tools/        one module per resource: catalog, files, takes, jobs, volumes,
                webhooks, raw
```

Built against the MCP Python SDK 2.x (`MCPServer`).

## Security

- The credential is read from the environment only — never committed, never logged
  (`describe()` redacts it, and status output goes to stderr, not the MCP stdout channel).
- Writes are off by default so an agent cannot start billable jobs by accident.
- `secret` on `move_upsert_webhook_endpoint` is a credential: treat tool output as sensitive.
- Presigned URLs in responses grant temporary access to the underlying object — avoid
  pasting them into shared transcripts.
- However you register the server, the key is stored in plaintext in the client's config
  (`~/.claude.json` for Claude Code), so treat that file as a secret. Passing it as
  `-e MOVE_API_KEY="$MOVE_API_KEY"` from an exported variable keeps it out of your shell
  history, which an inline literal does not. Rotate any key that has been pasted into a
  terminal or a chat.

TDQS

B3.4/5.0

Scored across 26 tools

Disambiguation4/5

Tools are mostly organized by resource and action, making takes, jobs, volumes, and files easy to distinguish. The only real confusable pairs are create_file/upload_file and get_file/download_file, though their descriptions clarify the intended use.

Naming Consistency4/5

Nearly every tool follows a clear move_<verb>_<noun> pattern with consistent snake_case. Minor deviations like move_client_info and move_graphql break the pattern slightly, but the overall style is predictable.

Tool Count3/5

With 26 tools, the server is on the heavy side, and the many file/job variants make the surface feel crowded. However, the tools map to a genuinely broad API spanning clients, files, takes, jobs, volumes, and webhooks, so the count is not an extreme mismatch.

Completeness3/5

The core file-to-take-to-job-to-output workflow is well covered, including multicam volume calibration and webhook setup. Notable gaps remain: there is no list-files tool, no delete/cancel operations for takes or jobs, and volume update/delete coverage is absent.

Maintenance

ActivityMaintained
ResponsivenessNo issues