# 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.

| | |
|---|---|
| **MCP endpoint** | `https://visa-mcp.naytive.com/mcp` |
| **Docs & playground** | `https://visa-mcp.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.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.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.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.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 tools, grouped by what they touch. 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, per‑application debits, refunds — each with running balance. |
| `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.

---

## 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*
