> For the complete documentation index, see [llms.txt](https://docs.drizz.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.drizz.dev/twins/using-twins/connect-your-agent.md).

# Connect your agent

Give your agent a world's MCP address and token, from Claude Code, Cursor, Codex, a stdio-only client, Scalekit or Python.

Give your agent a world's MCP address and token. Nothing in your agent needs to know it is a twin.

|               |                                                                                                |
| ------------- | ---------------------------------------------------------------------------------------------- |
| **Twins**     | Google Calendar · Gmail                                                                        |
| **You need**  | a world, from `twin start`, `twin world create` or `twin world run`                            |
| **Clients**   | Claude Code, Cursor, Codex, stdio-only agents such as goose, Scalekit, your own code or Python |
| **Watch out** | the token lets anyone act in the world. Keep it secret                                         |

## Prerequisites

* An agent that speaks MCP.
* A sign-in, to make a world: run `twin login`, or set `TWINS_API_KEY`. See [Keys and limits](/twins/reference/keys-and-limits.md).

## Copy this

Make a world, then add it to Claude Code as an HTTP MCP server with the address and token `twin start` prints:

```bash
twin start calendar
claude mcp add --transport http calendar-twin <mcp address> --header "Authorization: Bearer <token>"
```

Then ask Claude Code "what's on my calendar?" It is reading the twin.

## How it works

A world is two things: an **MCP address** and a **token**. Your agent connects to that address and sends the token, exactly as it would connect to the real Google Calendar or Gmail MCP server.

```mermaid
flowchart LR
    A["Your agent"] -->|"MCP + Authorization: Bearer token"| W["A world<br/>(calendar, mailbox)"]
    W -->|"records every call"| R["Record of calls"]
```

## Get the address and token

There are three ways, depending on how you made the world.

{% tabs %}
{% tab title="twin start" %}
`twin start` makes a fresh world and prints everything you need to connect:

```bash
twin start calendar
```

```
calendar world <id>, yours until <time> (it holds made-up data only)
  mcp:    <mcp address>
  token:  <token>

Connect an agent -- Claude Code, Cursor, Codex and others, detected for you:
  npx add-mcp <mcp address> --name calendar-twin --header "Authorization: Bearer <token>"
Or Claude Code alone:
  claude mcp add --transport http calendar-twin <mcp address> --header "Authorization: Bearer <token>"
```

Use `twin start gmail` for a mailbox.
{% endtab %}

{% tab title="twin world create" %}
`twin world create` makes a world from your [seed file](/twins/using-twins/describe-a-world.md). It prints the MCP address of each app, and keeps the tokens in the folder:

```bash
twin world create seed.md --folder world
```

```
folder:   world/
calendar: <calendar mcp address>
gmail:    <gmail mcp address>
          tokens in world/handle.json, private to you
lives:    until <time> unless deleted: twin world delete world
next:     twin world run world -- <your agent> "<task>"
```

To see the tokens, print the world's handle. Its `env` holds `TWIN_MCP`, `TWIN_TOKEN`, and the Gmail pair when the world has a mailbox:

```bash
twin world handle world
```

{% endtab %}

{% tab title="twin world run" %}
`twin world run` starts your agent with the address and token already in its environment. Nothing to copy:

```bash
twin world run world -- python my_agent.py "What's on my calendar this week?"
```

See [Run your agent](/twins/using-twins/run-your-agent.md).
{% endtab %}
{% endtabs %}

{% hint style="warning" %}
The token lets anyone act in the world. Keep it secret: don't commit it, and don't put it on a command line in a shared script. The world's handle holds its tokens too.
{% endhint %}

## What your agent gets in its environment

When you run your agent with `twin world run`, it gets these variables:

| Variable                             | What it is                                                                   |
| ------------------------------------ | ---------------------------------------------------------------------------- |
| `TWIN_MCP`                           | the calendar's MCP address in this world                                     |
| `TWIN_TOKEN`                         | the token to send it: `Authorization: Bearer $TWIN_TOKEN`                    |
| `TWIN_GMAIL_MCP`, `TWIN_GMAIL_TOKEN` | the mailbox's address and token, when the world has one                      |
| `TWIN_NOW`                           | the world's "now" (from `now:`), so your agent's "today" matches the world's |

Your agent never gets your sign-in key. That stays with `twin`.

## Connect a client

{% tabs %}
{% tab title="Claude Code" %}
Add the world as an HTTP MCP server:

```bash
claude mcp add --transport http calendar-twin <mcp address> --header "Authorization: Bearer <token>"
```

Then ask Claude Code "what's on my calendar?" It is reading the twin.

To hand Claude Code a world for **one run only**, without saving anything in your Claude settings, write a short script and start it with `twin world run`. It builds an MCP config from `TWIN_MCP` and `TWIN_TOKEN` and passes it with `--mcp-config` and `--strict-mcp-config`:

```bash
#!/bin/sh
# claude_agent.sh -- run as: twin world run world -- sh claude_agent.sh "<task>"
: "${TWIN_MCP:?run this inside a world: twin world run <folder> -- sh claude_agent.sh \"<task>\"}"
config=$(mktemp) || exit 1
trap 'rm -f "$config"' EXIT
chmod 600 "$config"
python3 -c '
import json, os, sys
def server(url, token):
    return {"type": "http", "url": url, "headers": {"Authorization": "Bearer " + token}}
servers = {"calendar": server(os.environ["TWIN_MCP"], os.environ["TWIN_TOKEN"])}
if os.environ.get("TWIN_GMAIL_MCP"):
    servers["gmail"] = server(os.environ["TWIN_GMAIL_MCP"], os.environ.get("TWIN_GMAIL_TOKEN", ""))
json.dump({"mcpServers": servers}, open(sys.argv[1], "w"))
' "$config" || exit 1
allowed="mcp__calendar"
[ -n "$TWIN_GMAIL_MCP" ] && allowed="$allowed mcp__gmail"
task=${TWIN_NOW:+"(Today is $TWIN_NOW.) "}$1
claude -p "$task" --mcp-config "$config" --strict-mcp-config --allowedTools $allowed < /dev/null
```

The token goes into a file only you can read, which is removed when the script ends. It never appears on a command line.
{% endtab %}

{% tab title="Cursor, Codex and others" %}
`add-mcp` detects the MCP clients on your machine (Claude Code, Cursor, Codex and others) and adds the world to them:

```bash
npx add-mcp <mcp address> --name calendar-twin --header "Authorization: Bearer <token>"
```

`twin start` prints this line for you, filled in.
{% endtab %}

{% tab title="Your own code" %}
Read the address and token from the environment instead of hard-coding them. Then the same code reaches the twin under `twin world run`, and the real service in production:

```python
import os

mcp_url = os.environ["TWIN_MCP"]                                   # where the calendar is
headers = {"Authorization": f"Bearer {os.environ['TWIN_TOKEN']}"}  # who you are in it
# hand mcp_url and headers to your MCP client, as you would for the real server
```

For a mailbox, connect a second MCP server with `TWIN_GMAIL_MCP` and `TWIN_GMAIL_TOKEN`.
{% endtab %}
{% endtabs %}

## Agents that need stdio

Some agents only start MCP servers as a command (goose, some desktop apps). Others take a URL but cannot add a header. For these, use the bridge that ships with twin-harness. It reads the world's address and token from the environment, relays each message to the twin with the token attached, and sends the answer back on stdout:

```
agent ──stdio──► python3 -m twin_harness.integrations.mcp_stdio ──https + token──► twin
```

Run your agent under `twin world run`, so the bridge finds `TWIN_MCP` and `TWIN_TOKEN`. For example, goose:

```bash
twin world run world -- goose run --no-session \
  --with-extension "python3 -m twin_harness.integrations.mcp_stdio" \
  -t "What's on my calendar this week?"
```

* `--app gmail` serves the mailbox instead of the calendar (`TWIN_GMAIL_MCP` and `TWIN_GMAIL_TOKEN`). Add one bridge per app.
* The bridge never takes the token on the command line, where other users of the machine could see it.
* The bridge is a Python module in twin-harness. Run it with a Python that can import `twin_harness`.

{% hint style="info" %}
**Tool names with dots.** The twin names its tools as Google does: `events.list`, `message.get`. OpenAI and Anthropic refuse a tool name with a dot, so an agent that passes MCP tool names straight to its model fails on its first request. The bridge lists the tools as `events_list` and maps each call back to the twin's own name.
{% endhint %}

## Scalekit agents

If your agent reaches Google Calendar through Scalekit, point it at a twin by adding the twin as a **custom connector**. The swap is configuration, not code.

The calendar twin answers on two MCP endpoints. Same calendar, same data, different tool names. Pick the one that matches the code you have:

| Endpoint         | Address               | Tools look like                                                        |
| ---------------- | --------------------- | ---------------------------------------------------------------------- |
| Scalekit shape   | `<twin>/scalekit/mcp` | `googlecalendar_create_event`, `start_datetime`, `event_duration_hour` |
| Google API shape | `<twin>/mcp`          | `events.create`, `start`, `end`                                        |

If your agent calls Scalekit's built-in Google Calendar connector today, use the Scalekit shape. The argument shapes are the same, so the call your code already makes is the call the twin takes:

```python
googlecalendar_create_event(
    summary="Q4 planning sync",
    start_datetime="2026-10-05T14:00:00",
    event_duration_hour=1,
    calendar_id="primary",
    timezone="UTC",
)
```

{% stepper %}
{% step %}

### Add the twin as a custom connector

In your Scalekit account, add a custom connector:

| Field      | Value                                                                      |
| ---------- | -------------------------------------------------------------------------- |
| Server URL | the world's MCP address, with `/scalekit/mcp` in place of the final `/mcp` |
| Auth Type  | Bearer Token                                                               |
| Token      | the world's token                                                          |

Create a connection on it, and note the connection name.
{% endstep %}

{% step %}

### Make the connection a setting

Read the connection name from the environment, with no default. If the variable is unset, the run stops instead of quietly reaching production:

```python
import os

CALENDAR_CONNECTION = os.environ["CALENDAR_CONNECTION"]   # no default, on purpose

scalekit.actions.execute_tool(
    tool_name=TOOLS[CALENDAR_CONNECTION]["create"],
    connection_name=CALENDAR_CONNECTION,
    identifier=user_id,
    tool_input={...},
)
```

Don't switch on a flag such as `IS_TEST` instead. A missing flag reaches production; a missing connection name stops the run.
{% endstep %}

{% step %}

### Map the tool names

Scalekit renames the tools it imports from a custom connector:

* every name gains a `c-<connector>_` prefix. A connector called "Calendar Twin Scalekit" serves `c-calendartwinscalekit_googlecalendar_create_event`;
* every name is lowercased (`events.quickAdd` arrives as `events.quickadd`).

Ask Scalekit for the exact spellings rather than guessing:

```python
tools = client.tools.list_scoped_tools(
    identifier=user_id, filter={"connection_names": ["calendar-twin"]})
print([t.tool.definition["name"] for t in tools.tools])
```

Keep the mapping in one place, keyed by connection. Make a missing entry raise an error, never fall back to the production name.
{% endstep %}

{% step %}

### Run it, then switch back

```bash
CALENDAR_CONNECTION=calendar-twin python main.py     # against the twin
CALENDAR_CONNECTION=googlecalendar python main.py    # against production
```

Nothing else changes.
{% endstep %}
{% endstepper %}

{% hint style="warning" %}
**A new world or a new token means reconnecting in Scalekit.** The connector keeps its own copy of the address and token you gave it. Until you update it and reconnect, every call fails with `the credential presented is not one this twin issued` (401). The twin is not down: Scalekit is sending a token the twin no longer knows.
{% endhint %}

<details>

<summary>Two Scalekit limits worth knowing</summary>

**Virtual MCP rejects custom connectors** ("Custom connectors are not supported yet"). A twin added as a custom connector is reached through the SDK (`execute_tool`), not through a Virtual MCP server URL.

**Imported schemas can arrive broken.** Some carry a `$ref` to `#/$defs/...` with no `$defs` block. Strict providers (OpenAI, Groq) then refuse the whole tool list with "json-pointer not found". If that happens, replace the dangling `$ref` with `{}` before you hand the schema to the model.

</details>

## Python

If you run your agent from your own Python code, use `Twin` from the twin-harness package. It makes the world, gives you its MCP servers, and deletes the world when the `with` block ends:

```python
from twin_harness import Twin

with Twin.from_test("seed.md") as world:   # a fresh world, seeded from your seed file
    world.mcp          # {"calendar": {"url", "token"}, "gmail": {"url", "token"}}
    my_agent.run("What's on my calendar this week?", mcp=world.mcp)
    for call in world.calls():              # every call the twin recorded
        print(call["tool"], call["status"])
# the world is deleted here
```

| Use                                                      | What it gives you                                                                                     |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `world.mcp`                                              | each app's MCP address and token                                                                      |
| `world.agent_env()`                                      | the `TWIN_*` variables `twin world run` gives an agent (for the stdio bridge). Never your sign-in key |
| `world.calls()`                                          | the twin's record of every call made in the world                                                     |
| `world.api("events.list", maxResults=5)`                 | one tool call, as your agent would make it                                                            |
| `Twin.from_test("seed.md", lease=7200, label="ci-1234")` | a world that lives 2 hours if nobody deletes it, listed under a label                                 |
| `Twin.from_snapshot("snap.json")`                        | a world made again from a snapshot                                                                    |

From asyncio, `AsyncTwin` is the same world with every call awaited:

```python
from twin_harness import AsyncTwin

async with AsyncTwin("seed.md", label="ci-1234") as world:
    await my_agent.run("What's on my calendar this week?", mcp=world.mcp)
```

A case that is cancelled (a timeout, Ctrl-C) still deletes its world on the way out. A twin that is busy (429 or 503) is asked again after its `Retry-After`.

{% hint style="info" %}
Making a world needs a sign-in. Run `twin login` first, or set `TWINS_API_KEY`. See [Keys and limits](/twins/reference/keys-and-limits.md).
{% endhint %}

## Common mistakes

| What you write                                                                        | What happens                                                                                                                         |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| The twin's tool names (`events.list`) passed straight to an OpenAI or Anthropic model | The model refuses a tool name with a dot, and your agent fails on its first request. Use the [stdio bridge](#agents-that-need-stdio) |
| A new world or token, with the old Scalekit connector                                 | Every call fails with 401, `the credential presented is not one this twin issued`. Update the connector and reconnect                |
| A flag such as `IS_TEST` to switch between twin and production                        | A missing flag reaches production. Read a connection name with no default instead                                                    |
| An imported Scalekit schema with a dangling `$ref`, handed to OpenAI or Groq          | The whole tool list is refused with "json-pointer not found". Replace the `$ref` with `{}`                                           |

## Next

* [Run your agent](/twins/using-twins/run-your-agent.md) in the world.
* [See what your agent did](/twins/using-twins/see-what-your-agent-did.md).
* [Troubleshooting](/twins/reference/troubleshooting.md), if a call fails.

***

*Last updated: 27 October 2026*
