# World Id MCP

> Build, configure, and submit World ID 4.0 apps and Mini Apps via the World developer portal. Use whenever the user wants to ship something to the World app store, set up sign-in-with-World-ID, or manage signing keys / actions for an existing app.

- Skill: `worldcoin/world-id-mcp` (Agent Skill, multi-file: 17 files)
- Install (CLI): `npx skillmds@latest add worldcoin/world-id-mcp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/worldcoin/world-id-mcp/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: worldcoin (https://skillmd.com/u/worldcoin)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/worldcoin/world-id-mcp

---


# World ID developer portal MCP

This MCP authenticates with a developer-portal team API key and exposes 11 tools for the full app lifecycle. Always use `get_team_context` first if the user hasn't given you an `app_id` — it returns the team and any existing apps so you can pick or confirm.

## Tool reference

| Tool                               | Purpose                                                                                                       | Key inputs                                                                                                                                               |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_team_context`                 | List the team's apps + status                                                                                 | (none)                                                                                                                                                   |
| `get_app_config`                   | Snapshot of an app: World ID config, store metadata, mini-app settings                                        | `app_id`                                                                                                                                                 |
| `create_app`                       | Create a new production app                                                                                   | `name`; optional `app_mode` (`external` \| `mini-app`), `verification` (`cloud` \| `on-chain`), `category`, `integration_url`                            |
| `configure_world_id`               | Create a managed World ID 4.0 RP for the app: KMS manager key, on-chain registration, signer wallet           | `app_id`; optional `signer_private_key` (else the server generates one and returns it once)                                                              |
| `get_world_id_signing_key`         | Read the signer address for an app. Private key is never returned here.                                       | `app_id`; optional `rotate_if_unavailable`                                                                                                               |
| `rotate_world_id_signing_key`      | Generate a new World ID signing key. Returns the private key once.                                            | `app_id`; optional `signer_private_key`                                                                                                                  |
| `get_world_id_registration_status` | Sync the on-chain registry status for an RP                                                                   | `app_id`                                                                                                                                                 |
| `create_world_id_action`           | Create / update a v4 action (the thing you `verify` against)                                                  | `app_id`, `action`; optional `description`, `environment`                                                                                                |
| `configure_mini_app`               | Update mini-app store metadata + Advanced/Permissions config                                                  | `app_id`; many optional fields, see below                                                                                                                |
| `upload_app_image`                 | Upload an app image (logo, hero, content_card, meta_tag, showcase_1/2/3) and patch the matching `*_url` field | `app_id`, `image_type`, one of `source_url` (https only, public addr, no redirects) / `image_base64`. Format detected from magic bytes. PNG/JPEG ≤500KB. |
| `submit_app_for_review`            | Submit an unverified app for review. Requires `confirm_submission: true`.                                     | `app_id`, `confirm_submission`; optional `changelog`, `is_developer_allow_listing`                                                                       |

## Canonical flows

### A. Build an external (non-mini) World ID app end-to-end

```
1. create_app             { name, app_mode: "external", verification: "cloud" }
2. configure_world_id     { app_id, generate_signing_key: true }      ← capture private_key, it's one-time
3. create_world_id_action { app_id, action: "verify-account" }
4. upload_app_image       { app_id, image_type: "logo",       source_url }   ← required: logo_img_url
5. upload_app_image       { app_id, image_type: "showcase_1", source_url }   ← required: ≥1 showcase per locale
6. configure_mini_app     {
                            app_id,
                            app_website_url:     "https://your-app.example",
                            description_overview: "What the app does, in one paragraph.",
                            supported_countries: ["us"],
                            supported_languages: ["en"],
                            is_android_only:     false,
                            is_for_humans_only:  false,
                          }
7. submit_app_for_review  { app_id, confirm_submission: true }
```

After step 2, store the returned `private_key` in the developer's app environment as `WORLD_ID_PRIVATE_KEY` (or whatever their app expects). The portal does not retain it.

### B. Build a Mini App with full Advanced settings

```
1. create_app             { name, app_mode: "mini-app" }
2. configure_world_id     { app_id, generate_signing_key: true }      ← only if the mini app verifies proofs itself
3. upload_app_image       { app_id, image_type: "logo",         source_url }   ← required
4. upload_app_image       { app_id, image_type: "content_card", source_url }   ← required for Mini Apps
5. upload_app_image       { app_id, image_type: "showcase_1",   source_url }   ← required: ≥1 showcase per locale
6. configure_mini_app     {
                            app_id,
                            short_name:           "MyApp",
                            category:             "Other",
                            world_app_description: "One-line tag line",
                            description_overview:  "What the mini app does, in one paragraph.",
                            app_website_url:      "https://your-app.example",
                            support_link:         "mailto:support@your-app.example",  // or "https://your-app.example/support"
                            supported_countries:  ["us"],
                            supported_languages:  ["en"],
                            is_android_only:      false,
                            is_for_humans_only:   false,
                            // optional Advanced fields:
                            contracts:            ["0x..."],
                            permit2_tokens:       ["0x..."],
                            max_notifications_per_day: "2",
                          }
7. submit_app_for_review  { app_id, confirm_submission: true }
```

`configure_mini_app` accepts store metadata, Advanced/Permissions config, and the App Store description in a single call:

If an app already has an approved version and no current draft, `configure_mini_app` and `upload_app_image` first create an `unverified` draft copied from the approved metadata, then apply the change. Successful responses include `draft_created: true` when that happens.

- `description_overview: string` — required for review; the human-readable overview shown to users in the app store. Pass it as a plain string here; the server JSON-encodes it (with `description_how_it_works` and `description_connect`) into the underlying `app_metadata.description` column for you. Don't pre-construct the JSON yourself.
- `description_how_it_works`, `description_connect` — optional companion sections (also stored inside the encoded description JSON).
- `contracts: string[]` — Worldchain contract addresses the mini app calls
- `permit2_tokens: string[]` — token addresses approved for Permit2 signing
- `whitelisted_addresses: string[]` — wallets allowed to interact (pass `[]` to disable)
- `associated_domains: string[]` — universal-link domains
- `can_import_all_contacts`, `can_use_attestation` — capability toggles
- `max_notifications_per_day`: `"0" | "1" | "2" | "unlimited"` — notification cap (string-only enum so MCP clients with strict JSON schema validators accept all values; `"unlimited"` automatically sets `is_allowed_unlimited_notifications: true`)

All address arrays are validated as `0x` + 40 hex chars; URLs as `https://...`; reject any element with an embedded comma.

### C. Upload images (logo / hero / content_card / meta_tag / showcase_N)

`upload_app_image` accepts the bytes in two ways:

- **`source_url`** — public HTTPS URL the server fetches (best for URLs the user already gave you, or staging asset CDNs)
- **`image_base64`** — base64-encoded bytes (use for local files)

When the user gives you a **local file path**, base64-encode it client-side via Bash and pass through:

```
# in Bash:
base64 -i ./logo.png
# pipe / capture the output, then call upload_app_image:
upload_app_image { app_id, image_type: "logo", image_base64: "<that string>" }
```

You don't need to specify the format — the server inspects the magic bytes and stores the file as PNG or JPEG accordingly. Anything that isn't a real PNG/JPEG (or anything >500KB) is rejected with `-32602`.

`source_url` is fetched server-side over HTTPS only, must resolve to a public address (loopback / private / link-local rejected), and **redirects are not followed** — if the user gives you a URL that redirects, resolve the final URL client-side first.

The server uploads the bytes to S3 and patches `app_metadata.{logo_img_url|hero_image_url|content_card_image_url|meta_tag_image_url|showcase_img_urls[N]}` in one call. No follow-up `configure_mini_app` is needed for images. Successful responses include `committed: true`.

If the metadata patch fails after the S3 upload (rare — typically a transient Hasura error), the server best-effort deletes the S3 object and returns `-32603` with `data: { committed: false }`. Retries are idempotent, so just call the same tool again.

`upload_app_image` is rate-limited per API key: **60 uploads/minute, 500/day**. Hitting the cap returns `-32029` with `data.retry_after_seconds`.

Image bytes flow through your context as base64 — fine for typical app icons (<100KB), wasteful for very large screenshots. Prefer `source_url` whenever the image is already on the web.

### D. Rotate a leaked signing key

```
1. rotate_world_id_signing_key { app_id }
```

Returns a fresh `private_key` once. Submits the on-chain `updateRp` transaction (primary registry, plus best-effort staging on production deployments) and updates `app_metadata.signer_address`. The status flips to `pending` until the on-chain TX confirms; poll `status_endpoint` to watch the transition back to `registered`. Self-managed RPs (e.g. legacy registrations not created via MCP) return `-32004` with `data.reason: "self_managed_mode"` — those have to be rotated by the developer themselves.

### E. Read-only inspection

```
get_team_context                    ← what apps exist, their status
get_app_config         { app_id }   ← world ID config, store metadata, actions
get_world_id_signing_key { app_id } ← signer address (no private key)
get_world_id_registration_status { app_id }  ← on-chain registry sync
```

## Pitfalls and constraints

- **`configure_world_id` is asynchronous.** The on-chain registration is submitted to the bundler and returns immediately with `status: "pending"` and an `operation_hash`. Watch `status_endpoint` until it returns `production_status: "registered"` before relying on the RP for verifications.
- **`configure_world_id` is available by default.** There is no team-level World ID 4.0 feature flag gate. Staging apps return `-32004` with `data.reason: "staging_not_supported"` — use a production app instead. If the app already has an RP registration, the tool returns the existing record (use `rotate_world_id_signing_key` to change keys).
- **Private keys are returned once.** If the user loses the value from `configure_world_id` / `rotate_world_id_signing_key`, they must rotate again. The portal does not store private keys.
- **Submission preconditions are strict.** `submit_app_for_review` runs the same Yup completeness check as the dashboard. The full required set:
  - **Always required**: `name`, `logo_img_url`, `app_website_url`, `is_android_only`, `is_for_humans_only`, `supported_countries` (≥1), `supported_languages` (must include `"en"`), `description_overview` (encoded into the description JSON), and at least one entry in `showcase_img_urls` for the English locale.
  - **Mini-app additional**: `short_name`, `category`, `world_app_description` (the tag line), `content_card_image_url`, and either `support_link` (HTTPS URL) or `support_link` set to a `mailto:` URL.
  - On failure the server returns a `-32602` with the exact field path; surface that to the user verbatim.
- **Use `upload_app_image`, not `configure_mini_app`, for image fields.** Image fields store filenames (`logo_img.png`), not full URLs — the dashboard reconstructs the CDN URL at view time. `upload_app_image` handles the S3 upload and stores the right filename automatically. Passing `logo_img_url`, `hero_image_url`, `meta_tag_image_url`, `showcase_img_urls`, or `content_card_image_url` to `configure_mini_app` is rejected with `-32602`.
- **Draft creation is edit-driven.** `configure_mini_app` and `upload_app_image` can create a draft from an approved app when no draft exists. `submit_app_for_review` does not create a draft by itself; make at least one metadata or image change first.
- **MCP-created apps are production apps.** The MCP no longer exposes staging app creation; use the dashboard or existing internal tooling for legacy staging-only sandbox flows.
- **`is_developer_allow_listing` is optional on submit.** If you omit it, the existing value on `app_metadata` is preserved — the MCP will not silently un-list a previously listed app.
- **Don't re-run `configure_world_id` on an already-configured app.** It returns the existing registration without rotating. Use `rotate_world_id_signing_key` if the user actually wants a new key.

## Test native World ID 4.0 with the simulator

The Portal MCP configures the app; the separate Simulator MCP completes its real staging proof request. The simulator's `complete_test_request` tool is not a Portal tool: connect it separately using the [simulator setup guide](https://github.com/worldcoin/simulator/blob/main/docs/mcp.md). If the tool is unavailable, fall back to the [simulator browser flow](https://simulator.worldcoin.org/) with the application's connector URL; do not invent a Portal test-payload endpoint.

1. Use an existing production app, or create one first (canonical flow A: `create_app` → `configure_world_id`). Capture `result.signing_key.private_key` from `configure_world_id` immediately — it is returned only once. If it is lost, wait until registration completes, then `rotate_world_id_signing_key` on that app only.
2. Poll `get_world_id_registration_status` until both `production_status` and `staging_status` are `registered`. While either is `pending`, wait and poll again; do not start the simulator request. Treat `failed` as a configuration failure (a failed staging mirror can be retried from the app's World ID configuration).
3. Start the application's actual signed IDKit request: `environment: "staging"`, `allow_legacy_proofs: false`, RP-signed on the application's backend, and enforce `min_protocol_version: "4.0"` at verification. A minimal end-to-end example lives in the simulator setup guide. Actions need no pre-registration — the v4 verify endpoint lazy-creates them on first verification; use `create_world_id_action` only to configure one deliberately.
4. Pass the request's connector URI to the Simulator MCP's `complete_test_request { connect_url }`. The first version supports a single native v4 Proof of Human uniqueness request; the signed request supplies the action and proof context.
5. Wait for the application's own IDKit polling/callback to receive the proof, then check its real backend verification response and business effects. `proof_delivered` from the simulator is delivery acknowledgment, not application acceptance.
6. On failure, follow the simulator's outcome guidance: `not_completed` — inspect the reported stage and the IDKit result; `unknown` — inspect the original request before another attempt, never retry blindly; `simulator_busy` — nothing was started, retry after a short wait. Repair the application and start a fresh request as appropriate.

Never log or repeat connector URLs: they contain a bridge encryption key. The simulator needs neither the RP private signing key nor the Portal team API key. These tests use real proofs from staging test credentials; they do not establish production-phone behavior, credential issuance, or session-proof support. Portal v4 currently accepts nullifier reuse — a repeat verification succeeds with a message noting the reuse — so application-level duplicate-operation rules must be tested separately.

## When in doubt

- `get_team_context` first to see what's already there. Reusing an existing app is almost always cheaper than creating a duplicate.
- Echo the `app_id` and `signer_address` you're operating on to the user before destructive actions (rotate, submit).
- Treat any `-32602` (invalid params) error as user-fixable input; surface the validation errors verbatim. Treat `-32603` (internal) as something to retry once and then escalate.

