# Connect your AI

> Connect an AI assistant to your browser with MCP, then ask it to read pages or explore a site with you.

Connect your AI assistant to Apigami and ask it to work with the pages you already have open.
You choose which sites it can access.

## Add Apigami

Add this server URL to your assistant. Claude and ChatGPT call it a connector, the others an MCP
server:

```text
https://api.eu.apigami.com/mcp
```

Your assistant signs in through your browser, with OAuth, the first time it uses the server. In
Claude and ChatGPT that sign-in opens in the browser you use them from. A coding assistant needs a
browser on the machine where it runs; without one, in a terminal on a server or in CI, use the
[agent sign-in](#agent-sign-in) instead.

**Claude, on claude.ai, in the desktop app or in Cowork.** A connector added once shows up in all
three, and in Claude Code signed in with the same account.

1. Open **Customize > Connectors** ([claude.ai/customize/connectors](https://claude.ai/customize/connectors);
   in the desktop app, **Customize** in the sidebar).
2. Choose **Add custom connector** and paste the server URL in **MCP server URL**. Leave the OAuth
   settings as they are.
3. Choose **Add**, then sign in on the Apigami page that opens. The connector shows as **Connected**.
4. In a conversation, choose **+**, then **Connectors**, and turn Apigami on. The first time a tool
   runs, choose **Always allow** or **Allow once**.

Cowork loads connectors when a task starts: start a new task once the connector is added. The Free
plan allows one custom connector. On a Team or Enterprise plan, an Owner adds it first, in
**Organization settings > Connectors**: **Add**, **Custom**, then **Web** if asked. Each member then
finds it under **Customize > Connectors**, with the **Custom** label, and chooses **Connect**.

The connector runs from Claude's side, not in the sandbox where Claude runs code: it works even when
that sandbox's network does not reach Apigami.

**ChatGPT**, on the web, with a paid plan. On a Business, Enterprise or Edu workspace, an admin may
have to allow custom MCP servers first.

1. In **Settings > Security and login**, turn on **Developer mode**.
2. Open [chatgpt.com/plugins](https://chatgpt.com/plugins), choose **+**, then **Add custom MCP
   server**.
3. Name it Apigami, paste the server URL in **Server URL** under **Connection**, and set
   **Authentication** to **OAuth**.
4. Choose **I understand and want to continue**, then **Create as a plugin**.
5. In your [personal plugins](https://chatgpt.com/plugins?view=personal), open Apigami and choose
   **+** to install it. Sign in on the Apigami page when it opens.
6. Start a new chat in the **Work** tab, type `@` and choose Apigami.

**Claude Code:**

```sh
claude mcp add --transport http --scope user apigami https://api.eu.apigami.com/mcp
```

Then open `/mcp`, choose **apigami** and **Authenticate**, or run `claude mcp login apigami` from the
shell. Signed in with a Claude account, Claude Code also lists the connectors added on claude.ai.

**Cursor:** add the server to `~/.cursor/mcp.json`, then choose **Connect** next to it on the
**Customize** page.

```json title="~/.cursor/mcp.json"
{
  "mcpServers": {
    "apigami": { "url": "https://api.eu.apigami.com/mcp" }
  }
}
```

**Codex CLI:**

```sh
codex mcp add apigami --url https://api.eu.apigami.com/mcp
codex mcp login apigami
```

**VS Code:** run **MCP: Add Server** from the Command Palette, choose **HTTP**, paste the server URL,
name it `apigami`, and add it for your user rather than the workspace.

Other MCP clients that support HTTP and OAuth use the same server URL.

## Connect your browser

1. Sign in on the page that opens with your email. No account yet? One is created on the spot;
   there's no password, you sign in with a code sent to that address.
2. Authorize your AI client. If Apigami is installed in the browser you're signing in from and
   that browser isn't attached to your account yet, an Apigami window opens in it, showing your
   account's email: choose **Allow** to attach the browser. Closing the window attaches nothing.
   An older version of Apigami, before 0.15.0, is attached without a window.
3. Return to the conversation.

The MCP server works once your email address is confirmed. Until then, your assistant tells you a
confirmation email has just been sent: open its link, then ask again. An account created from a
team invitation is already confirmed.

To use another browser, ask your assistant: `apigami_status` gives a link to open in that Chrome.
Sign in if the page asks, then choose **Continue**. From Apigami 0.15.0, an Apigami window opens in
that browser, showing your account's email: choose **Allow** to attach it. Closing the window
attaches nothing. The page then confirms the browser is attached to your account.

## Agent sign-in

Connect an assistant that has no browser of its own, from a network that reaches
`api.eu.apigami.com`: a terminal on a server, or CI. The assistant asks Apigami for a link and gives
it to you; you open it on any device, your phone included, sign in, and approve. The assistant then
receives a token for your account and adds the server to its MCP settings. When a browser runs on the
same machine as the assistant, the OAuth sign-in above is shorter. In Claude, in ChatGPT, or in a
sandbox whose network blocks Apigami, add the [connector](#add-apigami) instead.

1. The assistant asks for a link, and names itself `<client> on <machine or project>`, such as
   `Claude Code on ada-laptop`, so that you recognise it in your account later.
2. You open the link, check that the page shows the code the assistant gave you, accept the terms the
   first time, and choose **Authorize**. Only authorize a link you just asked your own assistant for:
   whoever sent it would act on your account.
3. The assistant collects its token, adds Apigami to its MCP settings for your user, never for a
   project, and asks you to restart it so that it loads the server. Until then, it can already call
   the `/v1/apps` routes with the same token.

The token opens what the OAuth sign-in opens: the MCP tools, and the routes that create, write,
deploy and publish your team's applications. It never manages your team, its API keys or its
webhooks, never deletes an application and never creates another access. It lasts 90 days, and
`apigami_status` warns two weeks before. It appears in your
[account](https://app.apigami.com/account) under **Your agents' access**, where **Remove access** ends
it at once; the applications it created stay.

### Ask for a link

The assistant runs this, with its own name. It keeps the request in
`~/.config/apigami/agent-sign-in.json` and prints only the link and the code to give you:

```sh
python3 - "Claude Code on ada-laptop" <<'PY'
import json, os, sys, urllib.error, urllib.request
request = urllib.request.Request(
    'https://api.eu.apigami.com/v1/device/code',
    data=json.dumps({'agent_name': sys.argv[1]}).encode(),
    headers={'Content-Type': 'application/json'},
)
try:
    answer = json.load(urllib.request.urlopen(request, timeout=30))
except urllib.error.HTTPError as error:
    sys.exit(json.load(error).get('error_description'))
path = os.path.expanduser('~/.config/apigami/agent-sign-in.json')
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
with open(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), 'w') as file:
    json.dump({'device_code': answer['device_code'], 'interval': answer['interval']}, file)
print(f"Open {answer['verification_uri_complete']} and check that the page shows {answer['user_code']}.")
PY
```

The name takes letters, digits, spaces and `. _ ( ) -`, 60 characters at most, and names neither a
domain nor Apigami. The link expires after 15 minutes.

### Wait for the answer

This loop asks whether you decided, slows down when Apigami asks it to, waits and asks again when
Apigami or the network does not answer, and stops on a refusal or an expired link. It writes the
token to `~/.config/apigami/agent-token`, readable by your user only, and never prints it. It gives
up after seven and a half minutes, within the time an assistant's command may run: run it again as
it is while the link is still valid.

```sh
python3 - <<'PY'
import json, os, sys, time, urllib.error, urllib.request
state_path = os.path.expanduser('~/.config/apigami/agent-sign-in.json')
token_path = os.path.expanduser('~/.config/apigami/agent-token')
state = json.load(open(state_path))
interval, deadline = state['interval'], time.monotonic() + 450
while time.monotonic() < deadline:
    request = urllib.request.Request(
        'https://api.eu.apigami.com/v1/device/token',
        data=json.dumps({'device_code': state['device_code']}).encode(),
        headers={'Content-Type': 'application/json'},
    )
    try:
        answer = json.load(urllib.request.urlopen(request, timeout=60))
    except urllib.error.HTTPError as error:
        try:
            answer = json.load(error) if error.code < 500 else {}
        except ValueError:
            answer = {}
    except (urllib.error.URLError, TimeoutError, ValueError):
        answer = {}
    if 'access_token' in answer:
        with open(os.open(token_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), 'w') as file:
            file.write(answer['access_token'])
        os.remove(state_path)
        print(f"Approved for {answer['email']}. The token is in {token_path}, not shown.")
        sys.exit()
    if not answer or answer.get('error') in ('authorization_pending', 'slow_down'):
        interval = answer.get('interval', interval)
        time.sleep(interval)
        continue
    sys.exit(f"{answer.get('error')}: {answer.get('error_description')}")
sys.exit('No answer yet. Run this again while the link is valid.')
PY
```

It ends on what to do next: after `access_denied`, you refused, so the assistant does not start again
unless you ask it to; after `expired_token`, it asks for a new link; after `invalid_grant`, the token
was already collected.

### Add the server to your client

Each snippet reads the token from its file, so it never appears in the conversation. Always add the
server for your user: a project file such as `.mcp.json` or `.vscode/mcp.json` ends up committed,
token included.

**Claude Code.** Remove any earlier `apigami` server first, or the new one is refused, or the old
OAuth one keeps answering:

```sh
claude mcp remove apigami --scope local 2>/dev/null
claude mcp remove apigami --scope user 2>/dev/null
claude mcp add --transport http --scope user apigami https://api.eu.apigami.com/mcp \
  --header "Authorization: Bearer $(cat ~/.config/apigami/agent-token)"
```

**Cursor**, in `~/.cursor/mcp.json`:

```sh
python3 - <<'PY'
import json, os
token = open(os.path.expanduser('~/.config/apigami/agent-token')).read().strip()
path = os.path.expanduser('~/.cursor/mcp.json')
config = json.load(open(path)) if os.path.exists(path) else {}
config.setdefault('mcpServers', {})['apigami'] = {
    'url': 'https://api.eu.apigami.com/mcp',
    'headers': {'Authorization': f'Bearer {token}'},
}
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), 'w') as file:
    json.dump(config, file, indent=2)
os.chmod(path, 0o600)
PY
```

**Codex CLI**, in its `config.toml`:

```sh
codex mcp remove apigami >/dev/null 2>&1
python3 - <<'PY'
import os
token = open(os.path.expanduser('~/.config/apigami/agent-token')).read().strip()
path = os.path.join(os.path.expanduser(os.environ.get('CODEX_HOME', '~/.codex')), 'config.toml')
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600), 'a') as file:
    os.chmod(path, 0o600)
    file.write('\n[mcp_servers.apigami]\nurl = "https://api.eu.apigami.com/mcp"\n'
               f'http_headers = {{ Authorization = "Bearer {token}" }}\n')
PY
```

Where the environment already holds its secrets, in CI for example, keep the token in a variable
instead: `codex mcp add apigami --url https://api.eu.apigami.com/mcp --bearer-token-env-var
APIGAMI_AGENT_TOKEN`, or `"Authorization": "Bearer ${env:APIGAMI_AGENT_TOKEN}"` in Cursor's
`headers`.

**VS Code**, in the `mcp.json` of your user profile (**MCP: Open User Configuration**), never in a
workspace's `.vscode/mcp.json`:

```json
{
  "servers": {
    "apigami": {
      "type": "http",
      "url": "https://api.eu.apigami.com/mcp",
      "headers": { "Authorization": "Bearer <the token in ~/.config/apigami/agent-token>" }
    }
  }
}
```

**Without an MCP client**, the server answers JSON-RPC over HTTP, each request standing alone:

```sh
curl -s https://api.eu.apigami.com/mcp \
  -H "Authorization: Bearer $(cat ~/.config/apigami/agent-token)" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"apigami_status","arguments":{}}}'
```

A client usually loads a server added during a session only at the next one: the assistant asks you
to restart it. Meanwhile the same token works as `Authorization: Bearer` on the routes of
[Push a version](/docs/app-publish/#push-a-version).

**When the token expires or is removed**, Claude Code, Cursor and Codex show the `apigami` server as
failed or needing authentication, without saying why. A new agent sign-in repairs it: ask for a new
link, then run the snippet of your client again.

## Try a first prompt

Open [example.com](https://example.com) in the connected browser, then ask:

```text
Show my connected browsers, then read the text of my example.com tab.
```

The first use of the site opens an approval window. Allow access, then let the assistant continue.
A refusal is remembered until you change it from Apigami's menu in your browser.

## What your assistant can do

It can list tabs, read page content and send requests using your existing sign-in.
It only reaches the browsers attached to your own account, never a teammate's, and the sites you
allow it are yours alone: an approval given to your assistant doesn't carry over to an app you
build, and the other way round.

Behind the scenes, Apigami gives your account its own private MCP app, shared by every AI client
you connect: allowing a site once in Claude Code allows it in your other clients too. It doesn't
appear in the dashboard, and there's no default app shared by your team.

Your cookies stay in the browser by default. Requests and cookie-based anti-CSRF headers are handled
there, and `cookie_get` returns a cookie's name, scope, length and flags, not its value. Only
`cookie_get` with `redact: false` returns the raw value, which then reaches the conversation.

<details>
<summary>Available tools</summary>

| Tool | Does |
|---|---|
| `apigami_status` | Who your AI acts for, the browsers attached to your account, the sites you already allow. Call it first |
| `request_fetch` | Run a request from your browser, with your session for that site — the `request.fetch` operation of `POST /call`, with `as_client_call` to get the payload for your own code |
| `tabs_list` | The tabs open on a site |
| `page_read` | Read a tab you already have open: `outline` maps the page and hands back reusable selectors, `markdown` its readable content, `text` its innerText, `html` the cleaned fragment to write an extractor against |
| `page_storage` | The `localStorage` and `sessionStorage` keys of a tab, with the type and a preview of each value — a key whose name looks like a credential is measured, never returned |
| `apigami_docs` | This documentation, whole or by keyword, without leaving the conversation |

</details>

<details>
<summary>Network and console tools for developers</summary>

Apigami Dev Assistant is a separate install for exploring a site's network requests and console.
Capture starts on the first call and stops after 30 minutes without one.

| Tool | Does |
|---|---|
| `network_endpoints` | The requests of a tab, grouped by endpoint — `/api/contacts/{id}`, GraphQL operations by name — with counts, statuses and sizes. A page that made 1 400 requests becomes 30 lines |
| `network_requests`, `network_get` | One request at a time: headers, body, the **shape** of a JSON body, a JSONPath into it |
| `network_requests` | Where a string appears — url, headers, bodies — with the requests that carry it |
| `network_mark` | Name a moment ("clicked Load more") and read only what came after |
| `console_list` | Console messages and exceptions of the tab |

Cookie, `Set-Cookie` and `Authorization` values are dropped **in the browser** before anything is
sent; the AI sees their length, never their value.

</details>

## Turn a request into app code

Once you've explored a site, ask your assistant to use `request_fetch` with `as_client_call`.
It returns the request body and HTTP headers to use with [`POST /call`](/docs/operations/) and
your app's API key, with your `user_ref` and `install_ref` for your team's apps. That call goes
through once you've [connected to your app](/docs/connect/) and allowed the site there: what you
allowed your assistant is for the MCP server alone. The export requests gzip: `params.responseType: "binary"`, outer
`Accept: application/octet-stream` and `Accept-Encoding: gzip`. Send all three, even for
text or JSON from the site, and enable gzip decoding in your HTTP client, such as `curl --compressed`.

Read the raw response body, `X-Apigami-Status` for the site's status and `X-Apigami-Headers`
for its JSON-encoded headers. Apigami errors still return non-2xx JSON. See
[gzip and binary responses](/docs/operations/#gzip-and-binary-responses) for a complete example.
The MCP tool itself keeps returning text or base64; its `headers` input targets the site,
so putting these Apigami transport headers there does not enable gzip.

There is no store of ready-made applications yet: `request_fetch` runs any request with the user's session, and `network_endpoints` shows what a site's own pages call.
To get Apigami Dev Assistant while its listing is private, [contact us](mailto:contact@apigami.com).