---
canonical_url: "https://docs.composio.dev/examples/harness-integration"
markdown_url: "https://docs.composio.dev/examples/harness-integration.md"
product: "platform"
version: null
last_updated: null
---

# Integrate Composio into an existing harness (/examples/harness-integration)

Most Composio examples hand the agent a session and let Composio's [meta tools](/docs/how-composio-works#meta-tools) find, authenticate, and run tools. That default works well until you already have your own planner, retrieval index, dispatcher, and permission layer. Then you need the 1000+ apps and managed auth, not another agent inside yours.

Composio sits underneath your harness in four calls:

1. **List toolkits.** [`composio.toolkits.get()`](/reference/sdk-reference/typescript/toolkits) returns the catalog independent of any user. That's your integrations directory.
2. **Connect what the user picks.** [`session.authorize(slug)`](/docs/authentication) starts the OAuth flow for this user. Bring your own OAuth credentials via [auth configs](/docs/authentication/programmatic-auth-configs) for a white-label flow, or [import existing tokens](/docs/authentication/importing-existing-connections) to migrate from another store.
3. **Fetch tools.** Load raw JSON Schema with [`composio.tools.getRawComposioTools({ toolkits })`](/docs/configuring-sessions#browsing-the-catalog), or serve the same tools over [MCP](/docs/sessions-via-mcp) and let your harness call them through the protocol.
4. **Execute by slug.** [`session.execute(slug, args)`](/docs/how-composio-works#executing-session-tools) runs one tool as the connected user and hands back data.

Nothing here asks a model to make a decision. Every call is one your code makes, at a moment you choose, because your planner decided.

```mermaid
`flowchart LR
subgraph yours["Your harness"]
  planner[Planner]
  index[Tool index]
  mcpClient[MCP client]
  dispatch[Dispatcher]
end
subgraph composio["Composio"]
  toolkits["composio.toolkits.get()"]
  authorize["session.authorize()"]
  schemas["tools.getRawComposioTools()"]
  mcp["session.mcp"]
  execute["session.execute()"]
end
ui([Your settings UI]) --> toolkits
toolkits --> authorize
toolkits --> schemas
toolkits --> mcp
schemas --> index
index --> planner
mcp --> mcpClient
mcpClient --> planner
planner --> dispatch
dispatch --> execute
execute --> planner
`
```

> **Is this the right example for you?**: Read this one if you already own the loop: your own planner, your own tool search or retrieval, your own execution and approval path. You get the app catalog, managed auth, and a single execute call, and Composio stays out of the reasoning.

Read [What is a session?](/docs/how-composio-works) instead if you'd rather Composio did the discovery. Runtime search returns guidance and recommended steps alongside schemas, keeps a shared context across calls, and costs you far less context than loading hundreds of schemas upfront. That's a real advantage to give up knowingly.

## Setup [#setup]

You need a [Composio API key](https://dashboard.composio.dev/~/project/settings/api-keys?utm_source=docs\&utm_medium=content\&utm_campaign=examples-agent-harness) to list the catalog and drive sessions. Listing apps does not need a user id; creating a session and connecting accounts does. There's no model provider in this example: your harness already has one.

**Python:**

**uv:**

```bash
uv add composio
```

**pip:**

```bash
pip install composio
```

**TypeScript:**

**npm:**

```bash
npm install @composio/core
```

**pnpm:**

```bash
pnpm add @composio/core
```

**bun:**

```bash
bun add @composio/core
```

**yarn:**

```bash
yarn add @composio/core
```

## Create a session your code drives [#create-a-session-your-code-drives]

A [session](/docs/how-composio-works) is the per-user context: which toolkits are in play, which accounts are connected, what may run. You still want one. What changes is that nothing on it ever reaches a model. Your harness never calls `session.tools()`, so the search and execute meta tools sit on the session unused.

Two settings are still worth passing at creation. `manageConnections: false` drops the connection meta-tools, because your settings page is doing that job. [Disabling the sandbox](/docs/configuring-sessions#disabling-the-sandbox) drops the code-execution tools, because your harness runs its own code. Leave the toolkit filter open and the session can execute any tool your planner names.

If you want a white-label flow, pass `authConfigs` at creation to pin the session to your own OAuth app. `session.authorize()` then sends the user through your consent screen instead of Composio's managed app. You can update those configs later, so migrating from a default app to your own app is a session patch, not a rebuild.

**Python:**

```python
from composio import Composio

composio = Composio()

# user_id is your own identifier, stable for the life of the account
session = composio.sessions.create(
    user_id="user_123",
    manage_connections=False,
    sandbox={"enable": False},
)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';

const composio = new Composio();

// userId is your own identifier, stable for the life of the account
const session = await composio.create('user_123', {
  manageConnections: false,
  sandbox: { enable: false },
});
```

Create one session per user and reuse it. Sessions persist on the server and don't expire, so store the session id on the user record and pick it back up with `composio.use(sessionId)` on later requests. Calling `create()` again just makes another session.

## 1. List the toolkits [#1-list-the-toolkits]

The catalog does not belong to a user. `composio.toolkits.list()` in Python or `composio.toolkits.get()` in TypeScript returns the app list independent of any session, so your integrations page can render before anyone signs in.

**Python:**

```python
page = composio.toolkits.list(limit=50)

for toolkit in page.items:
    print(f"{toolkit.slug:<20} {toolkit.name}")

# page.next_cursor -> pass back as next_cursor= for the next page
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio();
const toolkits = await composio.toolkits.get({ limit: 50 });

for (const toolkit of toolkits) {
  console.log(`${toolkit.slug.padEnd(20)} ${toolkit.name}`);
}

// pass a cursor as { cursor } to request the next page
```

Filter by category or sort alphabetically. If you also need to know whether this user has a live connection, call `session.toolkits()` with the same toolkit list; it merges the global catalog with per-user connection state.

**Python:**

```python
developer = composio.toolkits.list(category="developer-tools", limit=50)
connected = session.toolkits(is_connected=True, limit=100)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio();
const session = await composio.create('user_123');
const developer = await composio.toolkits.get({ category: 'developer-tools', limit: 50 });
const connected = await session.toolkits({ isConnected: true, limit: 100 });
```

> **Toolkits with no auth**: Toolkits where `isNoAuth` is true (search, scrapers, and similar) need no connection. They show up in the global catalog and, if you use `session.toolkits()`, come back with no `connection` object. Treat them as always available.

## 2. Connect what the user picks [#2-connect-what-the-user-picks]

`session.authorize(slug)` starts the flow and returns a connection request with a redirect URL. Send the user there. Composio holds the credentials and refreshes them; no token ever lands in your code.

**Python:**

```python
request = session.authorize(
    "github",
    callback_url="https://your-app.com/composio/callback",
)

# Redirect the user here
print(request.redirect_url)

# Scripts and CLIs can block; a web server should not
account = request.wait_for_connection()
print(f"Connected account: {account.id}")
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio();
const session = await composio.create('user_123');
const request = await session.authorize('github', {
  callbackUrl: 'https://your-app.com/composio/callback',
});

// Redirect the user here
console.log(request.redirectUrl);

// Scripts and CLIs can block; a web server should not
const account = await request.waitForConnection();
console.log(`Connected account: ${account.id}`);
```

In a web app, don't hold a request open on `waitForConnection`. Point `callbackUrl` at your own route and, when the user lands back on it, confirm with `session.toolkits({ toolkits: ['github'], isConnected: true })`. That check is authoritative and costs one call.

> **White-label OAuth and migrating apps**: By default, `session.authorize()` uses Composio's managed OAuth app. To brand the consent screen or control the OAuth client, create an auth config with your own credentials and pass it at session creation.

**Python:**

```python
auth_config = composio.auth_configs.create(
    toolkit="github",
    options={
        "type": "use_custom_auth",
        "auth_scheme": "OAUTH2",
        "name": "My GitHub App",
        "credentials": {
            "client_id": os.environ["GITHUB_CLIENT_ID"],
            "client_secret": os.environ["GITHUB_CLIENT_SECRET"],
            "oauth_redirect_uri": "https://backend.composio.dev/api/v1/auth-apps/add",
        },
    },
)

session = composio.sessions.create(
    user_id="user_123",
    toolkits=["github"],
    auth_configs={"github": auth_config.id},
    manage_connections=False,
    sandbox={"enable": False},
)
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio();
const authConfig = await composio.authConfigs.create('github', {
  type: 'use_custom_auth',
  authScheme: 'OAUTH2',
  name: 'My GitHub App',
  credentials: {
    client_id: process.env.GITHUB_CLIENT_ID!,
    client_secret: process.env.GITHUB_CLIENT_SECRET!,
    oauth_redirect_uri: 'https://backend.composio.dev/api/v1/auth-apps/add',
  },
});

const session = await composio.sessions.create('user_123', {
  toolkits: ['github'],
  authConfigs: { github: authConfig.id },
  manageConnections: false,
  sandbox: { enable: false },
});
```

To migrate users from an existing token store, create a connected account directly with `composio.connectedAccounts.initiate()` (TypeScript) or `composio.connected_accounts.initiate()` (Python). You can update `authConfigs` on the session later, so moving from the default app to your own app is a patch rather than a rebuild.

## 3. Fetch tools [#3-fetch-tools]

Your harness can consume tools in two shapes. Load raw JSON Schema and call them yourself, or expose them through an MCP server and let your harness call them over the protocol.

### Option A: Raw JSON Schema [#option-a-raw-json-schema]

`getRawComposioTools` returns tool definitions with no user context and no provider wrapping: a slug, a name, a description, and input parameters as plain JSON Schema. Take the toolkits you want to expose, pull their tools, and write them into whatever corpus your planner already searches.

**Python:**

```python
connected = session.toolkits(is_connected=True, limit=100)
slugs = [toolkit.slug for toolkit in connected.items]

tools = composio.tools.get_raw_composio_tools(toolkits=slugs)

for tool in tools:
    index.add(
        name=tool.slug,                    # "GITHUB_CREATE_AN_ISSUE"
        description=tool.description,
        parameters=tool.input_parameters,  # JSON Schema
    )
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio();
const session = await composio.create('user_123');
declare const index: {
  add(entry: { name: string; description?: string; parameters?: unknown }): void;
};
const connected = await session.toolkits({ isConnected: true, limit: 100 });
const slugs = connected.items.map(toolkit => toolkit.slug);

const tools = await composio.tools.getRawComposioTools({
  toolkits: slugs,
  important: false,
});

for (const tool of tools) {
  index.add({
    name: tool.slug,                  // "GITHUB_CREATE_AN_ISSUE"
    description: tool.description,
    parameters: tool.inputParameters, // JSON Schema
  });
}
```

> **Pass `important: false` to fetch every tool, not a curated subset**: In TypeScript, `getRawComposioTools({ toolkits })` applies Composio's `important` flag on your behalf and returns a hand-picked subset of each toolkit rather than all of it. Nothing errors; the list is just shorter than the toolkit, which is a bad surprise to find in an index weeks later. The auto-apply is suppressed by `important: false` and by passing any of `tools`, `tags`, `search`, or `limit`, so a call that already sets a large `limit` is getting the full list. The Python method has no `important` parameter and always returns the unfiltered list.

Which you want depends on your index. Curated is a reasonable default for semantic search over a few toolkits. Full is what you want when your planner needs to name an exact tool.

Fetch per toolkit rather than in one shot once a user has a lot connected. Thirty toolkits at full breadth is several thousand schemas, and you almost certainly want them cached, keyed by toolkit and refreshed on your own schedule, rather than pulled on every turn.

### Option B: MCP server [#option-b-mcp-server]

Create a session with `mcp: true` and hand `session.mcp.url` (and `session.mcp.headers` if required) to any MCP client. The server exposes the same tools and executes them under the session's connected accounts, so your harness talks the protocol instead of calling `session.execute()` directly.

**Python:**

```python
from composio import Composio, SESSION_PRESET_DIRECT_TOOLS

composio = Composio()

session = composio.sessions.create(
    user_id="user_123",
    toolkits=["gmail"],
    tools={
        "gmail": {
            "enable": ["GMAIL_FETCH_EMAILS", "GMAIL_CREATE_EMAIL_DRAFT"],
        },
    },
    session_preset=SESSION_PRESET_DIRECT_TOOLS,
    mcp=True,
)

print(session.mcp.url)
print(session.mcp.headers)
```

**TypeScript:**

```typescript
import { Composio, SessionPreset } from '@composio/core';

const composio = new Composio();

const session = await composio.sessions.create('user_123', {
  toolkits: ['gmail'],
  tools: {
    gmail: {
      enable: ['GMAIL_FETCH_EMAILS', 'GMAIL_CREATE_EMAIL_DRAFT'],
    },
  },
  sessionPreset: SessionPreset.DIRECT_TOOLS,
  mcp: true,
});

console.log(session.mcp.url);
console.log(session.mcp.headers);
```

Wire the URL into any MCP client. Examples for OpenAI Agents, Claude Agent SDK, and Vercel AI SDK are in the [sessions via MCP guide](/docs/sessions-via-mcp).

**Alternative: let the session decide which tools exist**

`getRawComposioTools` reads the catalog and ignores your session's filters. If you'd rather have one source of truth, scope the session and read its tool list back instead: set `preload: { tools: 'all' }` alongside a toolkit filter, then fetch with `composio.tools.getRawToolRouterSessionTools(session.sessionId)` in TypeScript or `composio.tools.get_raw_tool_router_meta_tools(session.session_id)` in Python. You get exactly the tools the session allows, so the set you index and the set that can execute can't drift apart.

The trade-off is that every change to the exposed set becomes a session update rather than a local filter, and the returned list includes any meta tools the session still has enabled.

### Keep the session in step with what you indexed [#keep-the-session-in-step-with-what-you-indexed]

If you scoped the session to specific toolkits, that filter is what gates execution later. When a user connects something new, widen the session rather than creating another one. `session.update()` is a patch: fields you omit are left alone.

**Python:**

```python
session.update(toolkits={"enable": [*slugs, "linear"]})
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio();
const session = await composio.create('user_123');
declare const slugs: string[];
await session.update({ toolkits: { enable: [...slugs, 'linear'] } });
```

A session created with no toolkit filter, as in the setup above, already allows everything and needs no update.

## 4. Execute by slug [#4-execute-by-slug]

Your planner picked a tool and produced arguments. `session.execute` runs it as this user's connected account and returns the result. No model, no retry loop, no interpretation.

**Python:**

```python
result = session.execute(
    "GITHUB_CREATE_AN_ISSUE",
    arguments={
        "owner": "ComposioHQ",
        "repo": "composio",
        "title": "Tool schemas drift between staging and prod",
        "body": "Filed by the harness.",
    },
)

if result.error:
    raise RuntimeError(result.error)

print(result.data)
print(result.log_id)  # look this up in the dashboard
```

**TypeScript:**

```typescript
import { Composio } from '@composio/core';
const composio = new Composio();
const session = await composio.create('user_123');
const result = await session.execute('GITHUB_CREATE_AN_ISSUE', {
  owner: 'ComposioHQ',
  repo: 'composio',
  title: 'Tool schemas drift between staging and prod',
  body: 'Filed by the harness.',
});

if (result.error) {
  throw new Error(result.error);
}

console.log(result.data);
console.log(result.logId); // look this up in the dashboard
```

Arguments go through untouched: neither SDK validates them locally, so a wrong field reaches the upstream API and comes back as a populated `error` your planner can read and correct on the next turn, not as an exception. Exceptions are for transport failures and cancellation. `logId` ties the call to its entry in the dashboard, which is the fastest way to see what Composio actually sent upstream when a response surprises you.

When the thing you need isn't wrapped as a tool, [`session.proxyExecute()`](/docs/extending-sessions/proxy-execute) calls the raw API endpoint under the same connected account.

## What maps to what [#what-maps-to-what]

| Your harness                   | Composio                                                             |
| ------------------------------ | -------------------------------------------------------------------- |
| Integrations directory         | `composio.toolkits.get()` / `composio.toolkits.list()`               |
| Per-user connection state      | `session.toolkits()`                                                 |
| "Connect" button               | `session.authorize(slug)`                                            |
| White-label OAuth app          | `authConfigs` at session creation                                    |
| Tool index or retrieval corpus | `composio.tools.getRawComposioTools({ toolkits, important: false })` |
| MCP tool server                | `session.mcp`                                                        |
| Tool dispatch                  | `session.execute(slug, args)`                                        |
| Per-user isolation             | one session per user id                                              |
| Audit trail                    | `result.logId`                                                       |

Your planner, context window, approval gate, and traces stay the same. Composio takes over the global app catalog, OAuth registration, token refresh, schema maintenance, and the execute path, whether you call `session.execute()` directly or through the MCP server.

> **If the tool set is fixed**: A harness with a known, small tool set doesn't need step three at all. Create the session with the [direct tools preset](/docs/configuring-sessions#direct-tools-preset) and an explicit `tools` filter, and `session.tools()` hands back exactly those tools with search, multi-execute, connection management, and the sandbox all off. That's `sessionPreset: SessionPreset.DIRECT_TOOLS` in TypeScript and `session_preset=SESSION_PRESET_DIRECT_TOOLS` in Python. Worth it when the list is short and stable; the four calls above are for when the list is whatever the user connected this morning.

- [Configuring sessions](/docs/configuring-sessions): Every filter a session takes: toolkits, tools, tags, auth configs, connected accounts

- [What is a session?](/docs/how-composio-works): The runtime context behind all four calls, and what the meta tools do when you leave them on

- [Authentication](/docs/authentication): Managed auth, your own OAuth credentials, and pre-connecting accounts

- [Proxy execute](/docs/extending-sessions/proxy-execute): Call an API endpoint Composio doesn't wrap, as the connected account

---

📚 **More documentation:** [View all docs](https://docs.composio.dev/llms.txt) | [Changelog](https://docs.composio.dev/docs/changelog.md) | [Glossary](https://docs.composio.dev/llms.mdx/reference/glossary) | [Examples](https://docs.composio.dev/llms.mdx/examples) | [API Reference](https://docs.composio.dev/llms.mdx/reference)

---

# Composio SDK — Notes for AI Code Generators

**Purpose:** Reference for generating current (v3) [Composio](https://composio.dev/) integration code.
**Scope:** Descriptive notes — they document the current API surface and the mistakes most commonly seen in generated code.

---

## 1. Recommended Integration: Sessions

Composio supports two integration modes: **Native Tools** (with a provider package) and **MCP** (no provider package needed).

### Native Tools

```python
# ✅ CORRECT — Python (defaults to OpenAI)
from composio import Composio

composio = Composio()
session = composio.create(user_id="user_123")
tools = session.tools()
# Pass tools to your agent/LLM framework
```

```typescript
// ✅ CORRECT — TypeScript (defaults to OpenAI)
import { Composio } from "@composio/core";

const composio = new Composio();
const session = await composio.create("user_123");
const tools = await session.tools();
// Pass tools to your agent/LLM framework
```

For other providers, pass the provider explicitly. Provider packages follow the naming convention: `composio_<provider>` for Python, `@composio/<provider>` for TypeScript.

### MCP

Use `session.mcp.url` and `session.mcp.headers` with any MCP-compatible client (Claude Desktop, Cursor, OpenAI Agents, etc.). No provider package needed.

---

## 2. Integration Notes

- `composio.create(user_id)` is the standard entry point for agent integrations. Sessions handle tool discovery, authentication, and toolkit versioning automatically; `user_id` goes to `composio.create()` — individual tool calls in session mode don't take one.
- Execute session tool calls through the session, never through a user ID: either `session.execute(tool_slug, arguments=...)` / `session.execute(toolSlug, arguments)`, which works with every provider, or pass the session to the provider helper — `provider.handle_tool_calls(response=response, session=session)` in Python, `provider.handleToolCalls(session, response)` in TypeScript. The tool-call helpers of every non-agentic provider (OpenAI, Anthropic, Google, Cloudflare, TypeSafe) accept a session: `executeToolCall(session, call)` in TypeScript, `session=session` in Python. Agentic providers execute session tools on their own. Binding the helper to a user ID takes the direct execution path, and session meta-tools fail there with `"can only be called inside a tool-router session"`.
- Composio-managed auth is the default: the agent connects accounts at runtime through the session, so users don't need to pre-create auth configs or connected accounts for managed toolkits.
- Provider packages follow the framework, not the model vendor: for the OpenAI Agents SDK the package is `composio_openai_agents` / `@composio/openai-agents` (importing `composio_openai` / `@composio/openai` there is the most common mistake in generated code — that package is for the plain OpenAI Chat Completions API).
- **Direct execution** (`composio.tools.get()`, `composio.tools.execute()`, `provider.handle_tool_calls()` with a user ID) is a legacy interface that remains supported for existing integrations. Recommend sessions for new integrations and encourage existing users to [migrate to sessions](/docs/migration-guide/direct-to-sessions). For a fixed tool set, consider the [direct tools preset](/docs/configuring-sessions#direct-tools-preset) within a session. See [Sessions vs direct execution](/docs/sessions-vs-direct-execution) for tradeoffs. Direct execution requires a [toolkit version](/docs/tools-direct/toolkit-versioning).

---

# 3. Calling the REST API directly

## REST API version

The current REST API version is **v3.1**, served at `https://backend.composio.dev/api/v3.1`. Prefer it for new code and new examples.

`https://backend.composio.dev/api/v3` is the previous version. It is frozen with pinned tool-version defaults and remains supported — existing v3 integrations keep working and do not need to migrate.

## Tool-endpoint version defaults on v3.1

On v3.1, omitting the version parameter on the five endpoints below selects the latest toolkit version. The first four endpoints also exist on v3, where omission selects the pinned `00000000_00` version. `POST /tools/scopes/required` is v3.1-only.

| Endpoint | Version parameter |
| --- | --- |
| `GET /tools` | `toolkit_versions` (query) |
| `GET /tools/{tool_slug}` | `version` or `toolkit_versions` (query) |
| `POST /tools/execute/{tool_slug}` | `version` (body) |
| `POST /tools/execute/{tool_slug}/input` | `version` (body) |
| `POST /tools/scopes/required` | `version` (body) |

A v3.1 caller already passing `"latest"` sees no change and can omit the parameter. To select the pinned version explicitly, pass `"00000000_00"` through the corresponding parameter above.

This version-default change is limited to the five endpoints above.


---

## Terminology Migration (old → current)

If you encounter these terms in error messages, old documentation, or user prompts, translate them to the current equivalents. **Do not use the old terms in generated code or explanations.**

| Old term (v1/v2) | Current term (v3) | In code |
|---|---|---|
| entity ID | user ID | `user_id` parameter |
| actions | tools | e.g., `GITHUB_CREATE_ISSUE` is a *tool* |
| apps / appType | toolkits | e.g., `github` is a *toolkit* |
| integration / integration ID | auth config / auth config ID | `auth_config_id` parameter |
| connection | connected account | `connected_accounts` namespace |
| ComposioToolSet / OpenAIToolSet | `Composio` class with a provider | `Composio(provider=...)` |
| toolset | provider | e.g., `OpenAIProvider` |

If a user says "entity ID", they mean `user_id`. If they say "integration", they mean "auth config". Always respond using the current terminology.

