Custom MCP
Custom MCP lets you add your own remote MCP server to Composio and use its tools in sessions, next to Composio's toolkits. A Custom MCP toolkit is either shared with every user in your project or private to one user, so your users can add their own MCP servers too.
Custom MCP is experimental. The APIs may change.
This is different from Custom Tools and Toolkits, which run inside your app. A Custom MCP server runs on its own public HTTPS URL.
How it works
- Get a remote MCP server at a public HTTPS URL: your own, or an existing one.
- Create a toolkit for it. Composio gives it a
CUSTOM_*slug. - Connect an account if the server needs an API key or DCR OAuth.
- Composio syncs the tools automatically.
- Use the toolkit in a session.
Use the SDK (composio.experimental.custom_toolkits in Python, composio.experimental.customToolkits in TypeScript) or the API. See the API reference for create, update, sync, and delete. upsert() is deprecated; use create() and update(). See Move from upsert.
Create a toolkit
Pass your MCP server's URL as mcp_url, how to authenticate, and user_id. Use "all" to share the toolkit with every user in your project:
result = composio.experimental.custom_toolkits.create(
name="Acme",
mcp_url="https://mcp.acme.com/mcp",
auth_schemes=[{"mode": "NO_AUTH"}],
user_id="all",
)
print(result.slug) # CUSTOM_ACMESave the returned slug: you need it to sync, update, or delete the toolkit.
mcp_url, auth_schemes, and user_id can't change later. To change them, delete the toolkit and create it again.
Use the final URL
Set mcp_url to the final MCP endpoint, with no redirects: a redirect can send credentials to another host. Don't put keys in the URL.
Choose how users authenticate
Set auth_schemes to one of these:
| Mode | Use it when | auth_schemes |
|---|---|---|
| No auth | The server needs no credentials. | [{"mode": "NO_AUTH"}] |
| API key | Each user brings an API key. | [{"mode": "API_KEY", "headers": {"Authorization": "Bearer {{generic_api_key}}"}}] |
| DCR OAuth | The server supports OAuth Dynamic Client Registration. | [{"mode": "DCR_OAUTH", "discovery_url": "https://mcp.acme.com/.well-known/oauth-authorization-server"}] |
- API key: put
{{generic_api_key}}in the header where the key goes, as in the table. Composio fills in each user's key when it calls your server. - DCR OAuth: set
discovery_urlto your OAuth server's metadata URL, usually ending in/.well-known/oauth-authorization-server. The server must support Dynamic Client Registration and the authorization-code flow.
Create a toolkit for one user
Set user_id to a user's ID to make the toolkit private: only that user's sessions and API calls can see or run its tools. Private toolkits need a Pro plan: on the free plan, create returns 403.
result = composio.experimental.custom_toolkits.create(
name="Acme",
mcp_url="https://mcp.acme.com/mcp",
auth_schemes=[
{
"mode": "DCR_OAUTH",
"discovery_url": "https://mcp.acme.com/.well-known/oauth-authorization-server",
}
],
user_id="user_123",
callback_url="https://app.example.com/connected",
)
print(result.slug) # CUSTOM_ACME_K7F2Q
if result.connect_link is not None:
print(result.connect_link.redirect_url) # send user_123 here{
"slug": "CUSTOM_ACME_K7F2Q",
"connect_link": {
"redirect_url": "https://connect.composio.dev/link/lk_...",
"expires_at": "2026-11-03T12:10:00.000Z",
"connected_account_id": "ca_..."
}
}If the server needs auth, send the user to connect_link.redirect_url. Once they connect, the tools sync. connect_link is null for shared and no-auth toolkits. To get a new link, call POST /api/v3.1/connected_accounts/link.
Creating the same MCP URL twice for one user, or twice as shared, returns 409. "default" and other spellings of "all" (such as "ALL") return 400.
Add a logo
Add logo_file to create or update: a square PNG or JPEG, 256 to 1024 pixels, at most 3MB, base64-encoded on one line. Composio hosts it. Without one, the toolkit shows the MCP logo.
"logo_file": {"content": "<base64>", "mime_type": "image/png"}Connect and sync
- No auth: tools sync when you create the toolkit.
- API key or DCR OAuth: tools sync when the first account connects.
Sync again only if that first sync fails or the server's tools change. Pass an active connected account, or leave it out for a no-auth server:
result = composio.experimental.custom_toolkits.sync(
"CUSTOM_ACME",
connected_account_id="ca_custom_acme",
)
print(result.version, result.synced_count)A toolkit can have at most 500 tools. If the server has more, the sync fails and the last good version stays.
Update a toolkit
Update changes name, logo_file, or the API key field's label (api_key_field). Fields you leave out stay the same. If your project has 2FA on, updating a private toolkit needs its user_id.
result = composio.experimental.custom_toolkits.update("CUSTOM_ACME", name="Acme (prod)")
print(result.success) # TrueDelete a toolkit
Delete removes the toolkit and its tools, auth configs, and connected accounts.
result = composio.experimental.custom_toolkits.delete("CUSTOM_ACME")
print(result.deleted, result.revoke_job_ids)By default, delete also starts jobs to revoke the accounts' credentials and returns their revoke_job_ids. Pass ?revoke_on_delete=false to skip them. DCR OAuth grants and API keys usually aren't revoked on the server's side, so revoke them there if you need to. Delete doesn't stop calls already in progress or remove past execution logs.
Use the toolkit in a session
Add the slug to a session. A session only sees the custom toolkits its user_id can use: naming another user's private toolkit returns 400, and running its tools returns 403.
from composio import Composio
composio = Composio(api_key="your_api_key")
session = composio.sessions.create(
user_id="user_123",
toolkits=["CUSTOM_ACME"],
)
tools = session.tools()Your agent finds the tools with COMPOSIO_SEARCH_TOOLS. Sessions pick the user's connected account automatically. If you created the auth config yourself, set is_enabled_for_tool_router: true on it. To use a specific account, pass it in the session:
from composio import Composio
composio = Composio(api_key="your_api_key")
session = composio.sessions.create(
user_id="user_123",
toolkits=["CUSTOM_ACME"],
connected_accounts={
"CUSTOM_ACME": ["ca_custom_acme"],
},
)List your toolkits
GET /api/v3.1/toolkits?type=custom lists your shared custom toolkits. Add user_id to include that user's private ones. Each toolkit's access is "all" (shared) or "user" (private).
Troubleshooting
- Tools missing from search: search can miss custom tools that synced fine. Check with toolkit-filtered tool listing or
COMPOSIO_GET_TOOL_SCHEMAS, then call the tool by slug withCOMPOSIO_MULTI_EXECUTE_TOOL. Resyncing doesn't help; send the slug, time (UTC), and log IDs to support. - Empty tool list on v3: the v3 tools API reads a pinned toolkit version that has no custom tools. Use v3.1, or ask for the latest version:
toolkit_versions=latestto list tools,version=latestto get one, or"version": "latest"in the body to execute. See Toolkit Versioning.
Move from upsert
upsert() and POST /api/v3.1/custom/toolkits/upsert still work but are deprecated. Toolkits you created with upsert keep working and keep their slugs.
- New toolkit: call create instead of upsert. Don't pass a slug: create returns one, so save it. Set
user_idto"all"to share the toolkit like upsert did, or to a user's ID to make it private. - Existing toolkit: call update with its slug to change the name, logo, or API key field label. Don't create it again: a second shared toolkit for the same URL returns
409. - Request shape: create takes the fields directly (
name,mcp_url,auth_schemes, ...), not insidetoolkit_config.
Limits
- Private toolkits need a Pro plan. Shared toolkits (
user_id: "all") work on every plan. - Remote servers only: local and STDIO servers aren't supported.
mcp_url,auth_schemes, anduser_idcan't change after creation.- At most 500 tools per toolkit.
- OAuth works only through Dynamic Client Registration (DCR). Servers that need a pre-registered OAuth client, or that only support Client ID Metadata Documents (CIMD), aren't supported.
- No automatic resync: sync again when the server's tools change.