# Visit India Visa MCP

**A remote MCP server that exposes Naytive's Visit India / Edge City visa flow** — search visas, upload a passport & photo, file the application, and get paid — as tools any MCP‑compatible AI agent can call on a user's behalf.

It also covers **Próspera ZEDE company formation** (Honduras, via DAZ): see [Próspera companies](#próspera-companies).

| | |
|---|---|
| **MCP endpoint** | `https://visa-mcp.staging.naytive.com/mcp` |
| **Docs & playground** | `https://visa-mcp.staging.naytive.com/docs` |
| **Transport** | Streamable HTTP (JSON‑RPC 2.0), one endpoint: `POST /mcp` |
| **Environment** | Staging — runs against Naytive staging + Stripe **test mode** |

---

## Overview

The server speaks the Model Context Protocol over **Streamable HTTP**. Point any MCP client at the endpoint above; the client discovers the tools and drives them. Every call acts on behalf of a signed‑in Naytive user, so the visa is filed against a real account with a real document vault.

Two things authenticate every request:

- **Partner API key** (`X-API-Key`) — identifies *your integration* on every call, and is what your prepaid wallet is billed against. Issued by Naytive (see below).
- **Traveller sign‑in (Privy)** — user‑scoped tools additionally need the traveller to sign in via OAuth 2.1 in the browser.

And two ways to collect payment, chosen per connection with the optional `X-Stripe-Key` header:

- **Your own Stripe** (`X-Stripe-Key` set) — the end‑user's checkout is created on **your** Stripe (they pay you). Naytive's per‑application fee is drawn from your **prepaid wallet**, and the visa is filed immediately.
- **Naytive collects** (default, no `X-Stripe-Key`) — the end‑user pays Naytive; no wallet is used.

---

## Get an API key

Every call carries a Naytive **partner API key** in the `X-API-Key` header, so all usage is attributed to your integration and billed to your wallet. Keys are **issued by Naytive** (not open registration).

1. **Request** — send your organisation + a contact email (via the "Request access" form on the docs page, or your Naytive contact). We review and approve.
2. **Receive your key** — on approval you get an `oc_live_…` / `oc_test_…` key, shown **once**. Store it as a secret.
3. **Top up & build** — add funds to your wallet, then point your MCP client at the endpoint with your key set.

---

## Connect a client

The endpoint is a standard remote MCP server, so anything that speaks MCP works. On first use the client opens a browser window for the traveller to sign in, then remembers the session. Set your partner key as the `X-API-Key` header.

### Claude Desktop

`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "naytive-visa": {
      "type": "http",
      "url": "https://visa-mcp.staging.naytive.com/mcp",
      "headers": {
        "X-API-Key": "oc_test_your_key_here",
        "X-Stripe-Key": "rk_test_your_restricted_key"  // optional — collect on your own Stripe
      }
    }
  }
}
```

> `X-Stripe-Key` is optional. Include it (a **restricted** Stripe key) to collect end‑user payments on your own Stripe account (see [Wallet & billing](#wallet--billing)); omit it to have Naytive collect.

### Claude Code (terminal)

```bash
claude mcp add --transport http naytive-visa \
  https://visa-mcp.staging.naytive.com/mcp \
  --header "X-API-Key: oc_test_your_key_here"
```

### Cursor

Add to `~/.cursor/mcp.json` (or the project's `.cursor/mcp.json`) with the same shape — `"url"` pointing at the endpoint and an `X-API-Key` header.

### MCP Inspector

```bash
npx @modelcontextprotocol/inspector@latest
```
In the UI: **Add Server** → Transport `Streamable HTTP` → URL the endpoint → toggle on → complete the Naytive sign‑in. (Set the `X-API-Key` header where the inspector allows custom headers.)

### Custom client (TypeScript SDK)

```ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("https://visa-mcp.staging.naytive.com/mcp"),
  { authProvider }  // your OAuthClientProvider — SDK runs the PKCE flow
);
const client = new Client({ name: "my-agent", version: "1.0.0" }, { capabilities: {} });
await client.connect(transport);

const { tools } = await client.listTools();
const res = await client.callTool({ name: "list_india_events", arguments: {} });
```

---

## Authentication

Two layers:

- **Partner API key** (`X-API-Key`) identifies your integration on **every** request and is what your wallet is billed against. A request with no key → `401`. It never expires unless revoked.
- **Traveller Privy sign‑in** — user‑scoped tools need it. A user tool with no sign‑in → `401` + `WWW-Authenticate`, which a compliant client uses to run the browser sign‑in automatically.

The OAuth flow (handled by the client SDK):

1. Set your Naytive‑issued key as the `X-API-Key` header.
2. For a user‑scoped tool, the client opens the authorize URL; the server redirects to Naytive's hosted sign‑in, where the traveller logs in with **email or wallet (Privy)**.
3. Naytive verifies the identity and issues a short‑lived access token bound to that user. The client sends it as `Authorization: Bearer …` alongside your `X-API-Key`.

> **Two identities, two jobs.** `X-API-Key` = your integration (billing + attribution); the Privy sign‑in = the traveller the visa is filed under, using that user's stored documents. Access tokens are short‑lived and refresh automatically.

OAuth metadata: `GET /.well-known/oauth-authorization-server`

```json
{
  "issuer": "https://visa-mcp.staging.naytive.com/",
  "authorization_endpoint": ".../authorize",
  "token_endpoint": ".../token",
  "registration_endpoint": ".../register",
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": ["authorization_code", "refresh_token"]
}
```

---

## Wallet & billing

Two ways to collect payment from your end‑user — you choose per connection with the optional `X-Stripe-Key` header (a **restricted** Stripe key):

### Your own Stripe (`X-Stripe-Key` set)
The end‑user's checkout is created on **your** Stripe — they pay **you**. Naytive's fee is drawn from your **prepaid wallet** and the visa is filed immediately. `submit_application` returns a `paymentUrl` for your end‑user. Optionally pass `retailAmountUsd` to set the price they pay you (defaults to the visa price).

- **Per‑application fee** — `fee × traveller count`, debited from the wallet. Your rate is set by Naytive (per partner); check it with `get_wallet_balance`.
- **Low balance** — the application is **not created**; you get a clear `402`. Top up with `create_wallet_topup_link` and retry.

### Naytive collects (default, no `X-Stripe-Key`)
The end‑user pays **Naytive**. `submit_application` creates a `pending_payment`; call `create_payment_link` for a Naytive checkout. **No wallet is used.**

> **Your key is never stored.** `X-Stripe-Key` travels as a request **header** (not a tool argument, so it never enters the model's context), is used just‑in‑time to create the checkout on your account, and is never logged or persisted. Use a restricted key.

> **Never oversells.** Wallet debits are atomic and idempotent: a retried submit is charged at most once, and the balance can never go negative.

---

## Tools

Twelve visa tools, grouped by what they touch (plus seven Próspera company tools, see [Próspera companies](#próspera-companies)). All tools require your `X-API-Key`; user‑scoped tools also need the traveller's sign‑in. Every tool returns a JSON payload as text content. You never hardcode identifiers — a `visaConfigId` comes from `list_india_events`; an `applicationId` / `applicationNumber` comes from `submit_application` / `list_applications`; document and photo ids come from the upload tools.

### Catalog (read)

| Tool | Args | Description |
|---|---|---|
| `list_india_events` | — | India builder events you can get a visa for (Edge City India, India Blockchain Week, Devcon Mumbai) + available India visa options. |
| `get_visa_details` | `visaConfigId` (req), `nationality?` | Full detail for one visa: validity, max stay, entries, processing time, nationality‑aware pricing, and required documents. |
| `get_required_documents` | `visaConfigId` (req), `nationality?` | Required documents merged with what the signed‑in user already has stored — so the agent only asks for what's missing. |

### Applications (read)

| Tool | Args | Description |
|---|---|---|
| `list_applications` | — | The signed‑in user's filed applications with current status, dates, amount. |
| `check_application_status` | `applicationNumber` (req) | Status + detail for one application by its number (e.g. `NV…`). |

### Documents (write)

| Tool | Args | Description |
|---|---|---|
| `upload_passport` | `fileBase64` (req, ≤6 MB), `fileName` (req), `fileMime` (req: `image/png` \| `image/jpeg` \| `application/pdf`) | Reads the passport, extracts identity fields, stores it in the traveller's vault. Returns a `documentId`. |
| `upload_photo` | `imageBase64` (req), `countryCode?` (default `IN`), `format?` | Uploads the passport‑size photo. Returns a `photoId`. |

### Submit (write)

**`submit_application`** — File the application. Every traveller must already have a passport `documentId` and a `photoId`, or the call is rejected. Payment behaviour depends on the `X-Stripe-Key` header:

- **With `X-Stripe-Key`** — Naytive's fee (`rate × travellers`) is debited from your wallet, the visa is filed immediately (`submitted`), and a `paymentUrl` (checkout on **your** Stripe) is returned for your end‑user. Low balance → nothing is created (`402`).
- **Without it** — a `pending_payment` application is created; the end‑user pays Naytive via `create_payment_link`.

| Field | Type | | Notes |
|---|---|---|---|
| `visaConfigId` | integer | required | From `list_india_events`. |
| `nationality` | string | optional | ISO code. |
| `arrivalDate` / `departureDate` | string | optional | `YYYY-MM-DD`. |
| `eventName` / `visitType` | string | optional | e.g. "Edge City India". |
| `documentIds` | integer[] | single | Passport doc id(s) for a solo applicant. |
| `processedPhotoId` | integer | single | Photo id for a solo applicant. |
| `travelers` | object[] | group | One per traveller: `{ fullName?, relationship?, documentIds, processedPhotoId }`. |
| `retailAmountUsd` | number | own‑Stripe | Price your end‑user pays you (defaults to the visa price). |
| `returnUrl` | string | own‑Stripe | Where to send your end‑user's browser after they pay. **Pass your own app URL** (e.g. your success page) — if omitted it defaults to `naytive.com`. |

**`create_payment_link`** *(alternate flow)* — Have Naytive collect from the traveller directly (Stripe/Helio) instead of your wallet. Not needed for the wallet model — a wallet‑billed application is already `submitted`. Args: `applicationId` (req), `paymentMode?` (`card`\|`crypto`), `returnUrl?` (**pass your own app URL** — after payment the browser goes here; if omitted it defaults to `naytive.com`).

### Wallet & billing

| Tool | Args | Description |
|---|---|---|
| `get_wallet_balance` | — | Prepaid wallet balance (USD cents), currency, and per‑application wholesale fee. |
| `list_wallet_transactions` | `limit?` (1–100, default 20) | Recent ledger entries — top‑ups, debits, refunds — each with running balance. Visa entries carry `ref_application_id`; Próspera company entries carry `ref_business_registration_id` (the `registrationId`). |
| `create_wallet_topup_link` | `amountUsd` (req, min $1), `returnUrl?` | A Stripe checkout URL to add funds. Balance updates once the charge settles. |

---

## Filing a visa, end to end

1. **Discover** — `list_india_events` → pick a visa and event. `get_visa_details` for pricing and the document checklist.
2. **Check the vault** — `get_required_documents` shows what the user already has vs what's still needed.
3. **Collect documents** — `upload_passport` and `upload_photo` for anything missing. Keep the returned `documentId` and `photoId`.
4. **Fund your wallet** — once, up front: `get_wallet_balance` to check funds; `create_wallet_topup_link` if you need more. Your end‑user pays you separately, through your own Stripe.
5. **File** — `submit_application` with the ids, dates, and event. Naytive's fee is debited and the application is filed — returns an `applicationNumber` already `submitted`.
6. **Track** — `list_applications` / `check_application_status` — the application moves through review.

---

## Próspera companies

Form a **Próspera ZEDE LLC** (Roatán, Honduras) for your user, end to end, through Naytive's DAZ partnership. Same auth as visas: your `X-API-Key` on every call, plus the user's sign‑in (Bearer) for anything that creates or reads their companies.

| | |
|---|---|
| **Fee** | Government fee **$500** + Naytive service fee **$200** = **$700 per company** (`70000` cents), debited from your prepaid wallet when the company is submitted. Charge your user any retail price. `get_prospera_pricing` returns the live figure. |
| **Refunds** | Automatic **only** if Próspera refuses the submission itself, or if Naytive cannot confirm Próspera received it (see *Reconciling* below). Once Próspera has accepted a company, a later rejection or archival is **not** refunded automatically — contact Naytive; refunds are a manual decision. |
| **Scope** | Companies are scoped to your partner key **and** the signed‑in user: you only ever see companies created through your own key. |
| **Signature** | `create_prospera_company` usually returns a `signatureUrl`. The **founder** must open it and sign digitally; nothing progresses until they do. |
| **Status** | Próspera has no push. Naytive polls every 15 minutes; `check_prospera_company_status` asks Próspera live. Review is typically ~14 days. |
| **Shareholders** | One founder per company, who is the sole shareholder (a DAZ v1 limit). |

> **No undo after submit.** `create_prospera_company` sends the company to Próspera immediately and charges your wallet, so confirm every detail with the user first. If Próspera refuses the submission itself, the full fee is refunded to your wallet automatically (a `refund` row appears in `list_wallet_transactions`).

> **Reconciling.** If Próspera does not answer the create call in time (timeout, 5xx, 408, 429), the company *may* already exist, so nothing is refunded on the spot. `create_prospera_company` returns `status: "reconciling"` — "Submitted; confirming with Próspera — check status shortly". **Do not call create again** (a duplicate is refused anyway). Naytive then looks the company up on Próspera: if found it continues as normal (usually `pending_signature`); if Próspera has no record after about 30 minutes, the registration is `cancelled` and the fee refunded automatically.

### Forming a company, end to end

1. **Discover** — `list_prospera_options`: jurisdiction, fee, activity codes, chains and `requiredFields`. `connected: false` means creation is not live yet.
2. **Collect** — ask the founder for: company legal name and trading name, main business activity, founder full name, email and role, and a principal office address (street, city, postal code, 2‑letter country code).
3. **Check funds** — `get_prospera_pricing` (fee, balance, `canAfford`); top up with `create_wallet_topup_link` if needed.
4. **Create** — `create_prospera_company`. The fee is debited and the company submitted; you get `registrationId`, `status` and usually `signatureUrl`.
5. **Sign** — the founder opens `signatureUrl` and signs. Próspera may then email them for KYC (`pending_kyb`).
6. **Track** — `check_prospera_company_status` until `active` (or `rejected`); `list_prospera_companies` lists them all.
7. **Documents** — once `active`: `list_prospera_documents`, then `download_prospera_document` for the certificate and the other corporate documents.

### Tools

| Tool | Auth | Args | Description |
|---|---|---|---|
| `list_prospera_options` | key | — | Jurisdictions + fees, activity codes, chains, `requiredFields`, steps, `connected`. |
| `get_prospera_pricing` | key | `jurisdictionId?` | Government + service fee (USD and cents), your wallet balance, `canAfford`. |
| `create_prospera_company` | key + user | see below | Submits to Próspera and debits the fee. Returns `registrationId`, `registrationNumber`, `status`, `signatureUrl`, `nextStep`, `billing`. |
| `list_prospera_companies` | key + user | — | The user's companies with last known status and next step. |
| `check_prospera_company_status` | key + user | `registrationId` | Live status, `prosperaApplicationStatus`, and `zoneRegistrationNumber` once active. |
| `list_prospera_documents` | key + user | `registrationId` | Document types (e.g. `certificate`, `terms`, `privacy_policy`, `background_check`, `civil_penalty`, `agreement_of_coexistence`); `downloadable` once active. |
| `download_prospera_document` | key + user | `registrationId`, `documentType` | The file (usually PDF) as an embedded resource (base64 `blob`) plus name, type, size. |

`create_prospera_company` arguments:

| Field | Type | | Notes |
|---|---|---|---|
| `legalName` | string | required | Full legal name, e.g. "Acme Corp LLC". |
| `tradingName` | string | required | Trading name, e.g. "Acme". |
| `mainActivity` | string | required | Activity code from `list_prospera_options`. |
| `secondaryActivities` | string[] | optional | Up to 10 more codes. |
| `chain` | string | optional | `POLYGON` (default) or `ETHEREUM`. |
| `founder` | object | required | `{ name, email, jobRole?, kycProviderId? }`; `jobRole` defaults to Director. Próspera emails the founder. |
| `principalOffice` | object | required | `{ line1, line2?, city, state?, postalCode, country }`; `country` is ISO‑2, e.g. `HN`. |
| `jurisdictionId` | integer | optional | Defaults to Próspera ZEDE. |

```json
{
  "legalName": "Acme Corp LLC",
  "tradingName": "Acme",
  "mainActivity": "SOFTWARE_DEVELOPMENT_AND_DIGITAL_PRODUCTS",
  "chain": "POLYGON",
  "founder": { "name": "Jane Doe", "email": "jane@acme.com", "jobRole": "CEO" },
  "principalOffice": { "line1": "1 Main St", "city": "Roatán", "postalCode": "34101", "country": "HN" }
}
```

### Statuses

| `status` | Meaning |
|---|---|
| `pending_signature` | The founder must sign at `signatureUrl`. |
| `processing` | Próspera is processing (includes Próspera's own payment step). |
| `pending_kyb` | KYC/KYB checks on the founder. |
| `pending_review` | Under review by Próspera (~14 days). |
| `waiting_activate` | Approved, awaiting activation. |
| `active` | Formed; documents are downloadable. |
| `reconciling` | Submitted, but Próspera did not answer in time; Naytive is confirming it. Check again in a few minutes; do not resubmit. |
| `rejected` | Próspera rejected the application after accepting it. **Not refunded automatically** — contact Naytive. |
| `cancelled` | Either Próspera refused the submission / never received it (`paymentStatus: "refunded"`, refunded automatically), or Próspera later archived/deleted the company (**not** refunded automatically — contact Naytive). `nextStep` says which. |

### Errors

Próspera tool errors are JSON in the text content (`isError: true`) with an `error` code to branch on:

| `error` | What happened |
|---|---|
| `prospera_not_connected` | Naytive's Próspera connection is not live yet. Nothing created or charged. Catalog and pricing still work. |
| `validation_failed` | Missing/invalid details; `errors` lists each `{ field, message }`. Ask the user and retry. Nothing charged. |
| `insufficient_wallet_balance` | Includes `feeCents` and `balanceCents`. Nothing created. Top up and retry. |
| `duplicate_registration` | The user already has this company (same legal name) in progress. Nothing charged; use the returned `registrationId`. |
| `prospera_rejected` | Próspera refused the submission (`providerMessage` says why). Fee refunded in full. |
| `prospera_auth_failed` | Próspera refused Naytive's credentials. Nothing was created; fee refunded in full. Contact Naytive. |
| `not_found` | No company with that `registrationId` for this user under your partner key. |
| `document_not_available` | The company is not active yet, or Próspera has no such document. |

---

## Errors & limits

Tool errors come back as text content with `isError: true` and a plain message.

| Situation | What you get |
|---|---|
| Missing / invalid API key | `401` — set your Naytive `X-API-Key` header. |
| No / expired user token | `401` + `WWW-Authenticate` — the client re‑runs sign‑in. |
| Wallet balance too low on submit | `402` `insufficient_wallet_balance` with the fee + current balance — **no application is created**. Top up and retry. |
| Missing passport or photo on submit | Error: "Passport and photo are required for every traveller before filing." |
| File over 6 MB | Error: file too large. |

- **Wallet debits are atomic & idempotent.** A submitted application is charged once; a retry is never double‑charged and the balance can't go negative. Top‑ups are credited by the Stripe webhook, not from the client claiming success.
- **Documents belong to the signed‑in user.** A tool can only reference documents and applications owned by the authenticated account.
- **Staging Stripe is in test mode** — use a test card at checkout.

---

*Visit India Visa MCP · Naytive · Staging reference*
