> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cycls.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connectors and MCP

> A user connects an account once, and the tools that need it get a live credential. OAuth, pasted keys, private endpoints and remote MCP servers.

A connector is a grant a person makes, for themselves or for a workspace, plus
the policy over the tools that use it. You declare it once, the directory renders
it, and the credential is attached server-side when a tool runs. Your code never
holds a token and neither does the browser.

```python catalog.py theme={null}
import cycls

notion = cycls.OAuth2(
    "notion",
    authorize="https://api.notion.com/v1/oauth/authorize",
    token="https://api.notion.com/v1/oauth/token",
    client_id=cycls.env("NOTION_CLIENT_ID"),
    secret=cycls.env("NOTION_CLIENT_SECRET"),
    scopes=["read_content", "update_content"],
    api="https://api.notion.com",
    api_headers={"Notion-Version": "2022-06-28"},
    scope="either",
)

web = cycls.Web().auth(cycls.Clerk()).connectors(notion)
llm = cycls.LLM().model("anthropic/claude-sonnet-4-6").connectors(notion)
```

Declare the same objects on both builders. `Web` serves the directory and the
connect routes, `LLM` decides whose tools the loop may offer.

## Three kinds of grant

<Tabs>
  <Tab title="OAuth2">
    Authorization code with PKCE against an app you registered, or against an MCP
    server that publishes its own authorization server and registers clients
    dynamically.

    ```python theme={null}
    github = cycls.OAuth2(
        "github",
        authorize="https://github.com/login/oauth/authorize",
        token="https://github.com/login/oauth/access_token",
        client_id=cycls.env("GITHUB_CLIENT_ID"),
        secret=cycls.env("GITHUB_CLIENT_SECRET"),
        scopes=["repo", "read:org"],
        extra={"prompt": "consent"},
        scope="user",
    )
    ```

    Tokens refresh when stale. The state that rides through the provider is signed
    and names the user and workspace, so the callback carries no JWT.
  </Tab>

  <Tab title="Key">
    For services where the user pastes a key.

    ```python theme={null}
    posthog = cycls.Key(
        "posthog",
        hint="phx_...",
        api="https://app.posthog.com",
        auth="bearer",
        scope="workspace",
    )
    ```

    `hint` is the field placeholder. Override `validate(value)` on a subclass to
    reject a malformed key before it is stored.
  </Tab>

  <Tab title="Endpoint">
    For services where the secret is the address, such as a private MCP link a
    merchant copies from their dashboard.

    ```python theme={null}
    zid = cycls.Endpoint("zid", host=".zid.sa", scope="workspace")
    ```

    `host` bounds what counts as a valid link, so a typo or an internal address is
    refused rather than dialled.
  </Tab>
</Tabs>

## Where a grant lives

```python theme={null}
cycls.OAuth2("gmail", ..., scope="user")        # personal, follows the person
cycls.Key("posthog", ..., scope="workspace")    # shared with the team
cycls.OAuth2("drive", ..., scope="either")      # the person chooses
```

A mailbox is a person, so `user`. A team analytics key is shared, so `workspace`.
`either` lets an admin connect one account for everyone while individuals can
still link their own.

## Calling a REST API

Give a connector an `api` base and the loop offers one `{name}_request` tool. The
model builds a path, the platform attaches the credential, and the host is bounded
to that base.

```python theme={null}
linear = cycls.Key("linear", api="https://api.linear.app", auth="bearer")
```

The same relay backs `cycls.connector(name)` inside a generated app, so a
dashboard an agent builds can read live data without ever holding a key.

| Argument      | Meaning                                        |
| ------------- | ---------------------------------------------- |
| `api`         | https base URL the relay is bounded to         |
| `api_headers` | fixed headers the API requires on every call   |
| `auth`        | `bearer`, `basic`, `header` or `query`         |
| `auth_name`   | header or query parameter name when not bearer |

## MCP servers

`cycls.MCP` connects to a remote MCP server. The harness speaks Streamable HTTP
itself, so every provider gets MCP, not just Anthropic. Tools are discovered once
and cached, and a session opens only when a tool is actually called.

```python theme={null}
server = (
    cycls.MCP("https://mcp.example.com/mcp")
    .name("example")
    .token(cycls.env("EXAMPLE_TOKEN"))
    .allow("search", "create_issue")
)

llm = cycls.LLM().mcp(server)
```

| Method              | Meaning                                                                   |
| ------------------- | ------------------------------------------------------------------------- |
| `.name(alias)`      | the name shown in the UI and used in the tool index                       |
| `.token(bearer)`    | a fixed bearer token                                                      |
| `.connector(oauth)` | act with a user's grant instead of a fixed token                          |
| `.allow(*names)`    | offer only these tools                                                    |
| `.guidance(text)`   | extra prompt guidance for this server                                     |
| `.writes(fn)`       | classify which calls change something, for approvals                      |
| `.server_side()`    | hand the server to the Anthropic connector instead, Anthropic models only |

## Loading tools on demand

A directory of connectors can hold hundreds of tools, and their schemas would not
fit in a system prompt. Each connector contributes one index line of about
fifteen tokens instead, and the model calls `find_tools` to load the schemas it
needs. Loaded tools stay for the rest of the conversation.

## Approvals

Every tool call is classified before it runs.

| Risk        | Behavior                           |
| ----------- | ---------------------------------- |
| read        | always runs                        |
| write       | follows the composer's Auto switch |
| destructive | asks in both modes                 |

When a call needs approval, the model gets a stop and the user gets a confirm
card naming the tool and the arguments. Approving binds to those exact arguments,
so a changed call asks again. Each person can also set allow, ask or never per
tool, and an organization admin can switch a connector off for everyone.

Deletes through bash are not destructive by default, because the sandbox shims
`rm` into a thirty day trash. Commands with nothing behind them, such as `shred`,
`mkfs` or `git push --force`, always ask.

## Directory copy

What the directory shows can come from your code or from a CMS, field by field,
with code winning.

```python theme={null}
web = cycls.Web().cms(
    brand="https://cms.example.com/agents/my-agent",
    connectors="https://cms.example.com/connectors",
    token=os.environ["CMS_TOKEN"],
)
```

This keeps behavior in Python, where it belongs, and copy in a CMS where a writer
can change it without a redeploy.

<Warning>
  Connector objects are built on your machine and pickled into the deployment. Keep
  the declaration file free of functions and module-level values a callable would
  close over: a `writes` classifier or a tool handler must live in the file you
  deploy, not in an imported module.
</Warning>

## Next

<Card title="Sandbox and trust model" icon="shield-halved" href="/agents/sandbox">
  What isolates the bash tool, and what it does not cover.
</Card>
