<< All versions

Skill v1.0.3

currentAutomated scan100/100
worldcoin/developer-portal/mcp

19 files

──Details
PublishedOctober 31, 2026 at 07:11 PM
Content Hashsha256:871d2d7c405dd8ce...
Git SHAab8adb84c12c
Bump Typepatch
Compare with v1.0.2
──Files
Files (1 file, 18.8 KB)
SKILL.md18.8 KBactive
SKILL.md · 175 lines · 18.8 KB

name: world-id-mcp description: 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. metadata: version: "1.0.3"


World ID developer portal MCP

This MCP authenticates with a developer-portal team API key and exposes 12 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

ToolPurposeKey inputs
get_team_contextList the team's apps + status(none)
get_app_configSnapshot of an app: World ID config, store metadata, mini-app settingsapp_id
create_appCreate a new production appname; optional app_mode (external \mini-app), verification (cloud \on-chain), category, integration_url
configure_world_idCreate a managed World ID 4.0 RP for the app: KMS manager key, on-chain registration, signer walletapp_id; optional signer_private_key (else the server generates one and returns it once)
get_world_id_signing_keyRead the signer address for an app. Private key is never returned here.app_id; optional rotate_if_unavailable
rotate_world_id_signing_keyGenerate a new World ID signing key. Returns the private key once.app_id; optional signer_private_key
get_world_id_registration_statusSync the on-chain registry status for an RPapp_id
retry_world_id_registrationRetry managed registration using the saved RP ID, manager key and signer; checks the contract firstapp_id, environment (production or staging)
create_world_id_actionCreate / update a v4 action (the thing you verify against)app_id, action; optional description, environment
configure_mini_appUpdate mini-app store metadata + Advanced/Permissions configapp_id; many optional fields, see below
upload_app_imageUpload an app image (logo, hero, content_card, meta_tag, showcase_1/2/3) and patch the matching *_url fieldapp_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_reviewSubmit 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.
  • A new Portal registration creates a new RP with a random ID. Existing on-chain RPs are not imported. Switching an existing integration to this ID changes users' nullifiers; do not treat it as continuing an existing on-chain registration.
  • `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` to retry registration or get a new key. It returns the saved registration without resubmitting or rotating. For pending, poll status_endpoint. For a failed managed registration, use retry_world_id_registration { app_id, environment }; it keeps the saved ID and keys. Use rotate_world_id_signing_key only when registration is complete and the user 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. If the tool is unavailable, fall back to the simulator browser flow 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 the call fails after retaining registration state, save error.data.signing_key.private_key instead. 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 (retry a failed managed registration with retry_world_id_registration { app_id, environment: "production" } or environment: "staging" for a failed staging mirror).
  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.
← v1.0.2All versions