MCP

Connect Claude, ChatGPT or any MCP app to AssistLoop with one URL, then go further: your own MCP client, a client built from scratch, and the full tool, error and limit reference.

Connect AssistLoop to an AI app such as Claude or ChatGPT, and you can ask the app to work in AssistLoop for you: read conversations, reply to customers, handle tickets and train your agents. The connection uses MCP, an open standard for linking AI apps to other tools.

Server URL

https://api.assistloop.ai/mcp

It is the same URL the AssistLoop dashboard shows on the MCP page (organization sidebar → MCP → Server URL, with a copy button). That URL is all an AI app needs. You don't need an API key or a password: the app opens AssistLoop in your browser, and you approve it there.

Your organization must be on a premium plan. The app acts as you: it has your role and sees only the agents you see in the dashboard.

Connect in three steps

  1. Add the URL to your AI app. In Claude, open Customize → Connectors, click + Add → Add custom connector. For ChatGPT and other apps, see Connect an AI app.
  2. Sign in and approve. The app opens the AssistLoop sign-in page. Sign in, check what the app will be allowed to do, and click Authorize.
  3. Ask. Start a new chat and check that AssistLoop is turned on (in Claude: + → Connectors; in ChatGPT: type @ and pick AssistLoop). Then try a prompt below.

Prompts to try

List my AssistLoop agents.
Show the open conversations for my agent and summarize what customers are asking.
List the open high-priority tickets and draft a reply for the oldest one. Don't send anything yet.
Train my agent on this text: "We deliver within 2-3 business days."
Crawl https://example.com, show me the pages it found, and train my agent on the pricing page.

Most AI apps ask before they run a tool that changes something, such as sending a reply to a customer. Read what the app is about to do before you approve.

Pick your path

The sections below go from simple to technical. Most people need only the first one.

You haveRead
Claude, ChatGPT, Claude Code, VS Code, Cursor or another app that accepts an MCP server URLConnect an AI app
Your own program or agent built with an MCP SDK or an agent frameworkUse your own MCP client
Code that must sign in and call the server without an MCP SDKBuild your own client

Reference: Tools and Errors and limits.

Connect an AI app

Every app below needs only the server URL:

https://api.assistloop.ai/mcp

Copy it from the dashboard's MCP page or from here. Leave any OAuth client ID or client secret field empty: the app sets itself up with AssistLoop and opens the AssistLoop sign-in page in your browser.

Menu names change between app versions. If a menu below looks different, look for Connectors, MCP or Integrations in the app's settings.

What happens when you connect

  1. The app opens the AssistLoop sign-in page. Sign in if you aren't already.
  2. The page shows the app's name, the site it returns you to, and the access it asks for.
    • Claude and ChatGPT show a Verified: Claude or Verified: ChatGPT badge.
    • Other apps show Unverified app. That is normal for developer tools: check that you started the connection yourself, then continue.
    • Apps that return only to your own computer (localhost or 127.0.0.1), such as Claude Code and other command-line apps, also warn that any program on your computer could use the connection. Continue only if you just started it.
  3. Leave Allow changes ticked to let the app reply to customers, update tickets and train agents. Untick it for read-only access. If you don't see Allow changes, the app asked for read-only access.
  4. Click Authorize. The browser returns to the app, and the app shows AssistLoop as connected.

Claude (claude.ai and Claude Desktop)

Works on Claude's paid plans and the Free plan. The Free plan allows one custom connector.

  1. Open Customize → Connectors.
  2. Click + Add, then Add custom connector.
  3. Enter AssistLoop as the name and https://api.assistloop.ai/mcp as the URL, then click Continue. Keep the detected settings and click Continue again.
  4. For authentication, choose Sign in now. For the OAuth client, keep Use Claude's published identity (Recommended) or choose Register automatically. Don't choose Use your own OAuth client.
  5. Click Add, then Connect, and approve on the AssistLoop page.
  6. In a chat, click + → Connectors and turn on AssistLoop.

Some accounts still show an older one-screen dialog with Advanced settings. Leave those fields empty.

A connector added on claude.ai also works in Claude Desktop and the Claude mobile apps.

Claude Team and Enterprise: an Owner (or Primary Owner) adds the connector once in Organization settings → Connectors → Add → Custom → Web. Each member then opens Customize → Connectors, finds AssistLoop (marked Custom), clicks Connect and approves with their own AssistLoop account.

ChatGPT

ChatGPT adds custom MCP servers as plugins, on the web. On Business, Enterprise and Edu plans, the workspace must allow creating plugins with MCP servers.

  1. Go to chatgpt.com/plugins, click +, then Add custom MCP server.
  2. Enter AssistLoop as the name and, under Connection, https://api.assistloop.ai/mcp as the Server URL.
  3. Choose OAuth for authentication and leave the client ID and secret empty.
  4. Click I understand and want to continue, then Create as a plugin.
  5. Install the plugin and approve on the AssistLoop page. ChatGPT may ask you to sign in only when it first uses an AssistLoop tool.
  6. In a chat, type @ and pick AssistLoop.

Claude Code

claude mcp add --transport http assistloop https://api.assistloop.ai/mcp

Then start Claude Code, run /mcp, choose assistloop and follow the sign-in. Approve in the browser that opens.

To use AssistLoop in every project, add --scope user:

claude mcp add --transport http --scope user assistloop https://api.assistloop.ai/mcp

Use --scope project instead to share it with your team through .mcp.json.

VS Code (GitHub Copilot)

  1. Open the Command Palette and run MCP: Add Server.
  2. Choose HTTP (HTTP or Server-Sent Events) and paste https://api.assistloop.ai/mcp.
  3. Enter assistloop as the server ID.
  4. Choose where to save it (for example Global), and approve the sign-in in the browser.

Or add a .mcp.json file at the root of your workspace:

{
  "mcpServers": {
    "assistloop": {
      "type": "http",
      "url": "https://api.assistloop.ai/mcp"
    }
  }
}

Use the tools from an agent session in the Chat view.

Cursor

Add the server to ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project):

{
  "mcpServers": {
    "assistloop": {
      "url": "https://api.assistloop.ai/mcp"
    }
  }
}

Then open Cursor's Customize page (older versions: Cursor Settings → MCP) and sign in next to assistloop.

Older Cursor versions sign in through a cursor:// return address, which AssistLoop does not accept. Cursor then shows a registration error (invalid_redirect_uri) and no AssistLoop page opens. Update Cursor: current versions return to http://localhost:8787, which works.

Gemini CLI

gemini mcp add --transport http assistloop https://api.assistloop.ai/mcp

This saves it for the current project. Add --scope user to use it in every project. Then, inside Gemini CLI, run /mcp auth assistloop and approve in the browser.

OpenAI Codex CLI

codex mcp add assistloop --url https://api.assistloop.ai/mcp
codex mcp login assistloop

The second command opens the browser to approve.

Any other app

AssistLoop works with any app that can add a remote MCP server (also called Streamable HTTP) and sign in with OAuth. Give it the server URL and nothing else.

The app must return to an https address or to http://localhost, http://127.0.0.1 or http://[::1]. Apps that use a custom address such as myapp://, even alongside an https one, can't connect yet.

The dashboard's MCP page, under Connect an app → Other apps, also has a prompt you can paste into an AI coding agent (Cursor, Codex, Gemini CLI) so it adds the server itself.

If the app only accepts a fixed access token, it can't sign in by itself. See Build your own client to get a token in code.

If something goes wrong

What you seeWhat to do
The app says it can't reach the serverCheck the URL ends in /mcp and starts with https://.
The sign-in page says the app isn't recognizedRemove AssistLoop from the app and add it again.
The app reports invalid_redirect_uri, or never opens the AssistLoop pageThe app uses a custom return address (such as cursor://). Update the app, or use another app.
Every tool answers premium_requiredUpgrade the organization to a premium plan.
The app can read but not change anythingOn the dashboard's MCP page the app shows Read only. Click Disconnect, confirm, then connect again from the app and leave Allow changes ticked.
The app stopped working after a whileIt lost its sign-in. Connect it again from the app.

Use your own MCP client

Use this section when you write the program yourself and use an MCP SDK (Python, TypeScript and others) or an agent framework with MCP support. The SDK finds the sign-in endpoints, registers your app and handles PKCE (the code check that protects the sign-in). To connect an existing app such as Claude or Cursor instead, see Connect an AI app.

Before you start

RequirementDetails
PlanThe organization must be on a premium plan. list_organizations also shows free organizations (with premium: false), but their agents are not listed and every tool refuses them. A person with no premium organization gets premium_required from every tool.
AccountA person with an AssistLoop account signs in once, in a browser, and approves the connection. Your system then acts as that person, with that person's permissions.
MembershipThat person must be an owner, admin or member of the organization. A member sees only the agents assigned to them, unless their membership gives access to all agents.
Redirect addressYour system needs a web address the browser can return to after approval: an https address, or an http address on the machine itself (127.0.0.1, localhost or [::1]).

Server details

WhatValue
Server URLhttps://api.assistloop.ai/mcp
TransportStreamable HTTP: JSON-RPC 2.0 in POST requests. GET /mcp answers 405.
MCP protocol versions2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05
Sign-inOAuth 2.1 authorization code flow with PKCE (S256 only), as the MCP authorization specification describes
Scopesmcp:read (read tools), mcp:write (write tools; includes read)
Access key lifetime1 hour
Refresh key lifetime30 days. Every refresh returns a new refresh key and revokes the old one.
MCP featuresTools only. The server offers no MCP resources or prompts.

Pick a client

Your systemUse
A developer tool for testingMCP Inspector: npx @modelcontextprotocol/inspector, choose Streamable HTTP, enter the server URL and click Connect.
Your own program or AI agentAn MCP SDK or an agent framework with MCP support. See the Python example below.
An LLM API that accepts remote MCP serversPass the server URL and an access key you get yourself: see Build your own client.
A fully automated job with no person to approve itMCP is not a fit: there is no sign-in without a person, and AssistLoop API keys do not work on /mcp. Use the Public API with an API key instead.

Connect with the Python MCP SDK

This program signs in once through the browser and stores its keys in a file. Later runs reuse the stored keys and refresh them, so no browser opens again until the refresh key expires or someone disconnects the app.

It was tested with the official MCP Python SDK, mcp version 1.30.0.

pip install "mcp==1.30.0"
export ASSISTLOOP_MCP_URL="https://api.assistloop.ai/mcp"
python assistloop_mcp.py
"""Connect your own program to the AssistLoop MCP server."""

import asyncio
import json
import os
import threading
import time
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse

import httpx
from mcp import ClientSession
from mcp.client.auth import OAuthClientProvider, TokenStorage
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken

SERVER_URL = os.environ["ASSISTLOOP_MCP_URL"]
CALLBACK_PORT = 8765
REDIRECT_URI = f"http://127.0.0.1:{CALLBACK_PORT}/callback"
STATE_FILE = Path(os.environ.get("ASSISTLOOP_MCP_STATE", "assistloop-mcp.json"))
APPROVAL_TIMEOUT_SECONDS = 600  # how long to wait for the person to approve


class FileTokenStorage(TokenStorage):
    """Keeps the registered app and its keys in a file, so the browser step happens once."""

    def read(self) -> dict:
        return json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}

    def write(self, data: dict) -> None:
        STATE_FILE.write_text(json.dumps(data))
        STATE_FILE.chmod(0o600)

    async def get_tokens(self) -> OAuthToken | None:
        data = self.read()
        return OAuthToken.model_validate(data["tokens"]) if "tokens" in data else None

    async def set_tokens(self, tokens: OAuthToken) -> None:
        data = self.read()
        data["tokens"] = tokens.model_dump(mode="json", exclude_none=True)
        self.write(data)

    async def get_client_info(self) -> OAuthClientInformationFull | None:
        data = self.read()
        return OAuthClientInformationFull.model_validate(data["client"]) if "client" in data else None

    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
        data = self.read()
        data["client"] = client_info.model_dump(mode="json", exclude_none=True)
        self.write(data)


async def refresh_stored_keys(storage: FileTokenStorage) -> None:
    """Swap stored keys for fresh ones before connecting.

    The SDK refreshes only keys it received in the same process: with keys loaded from the file,
    an expired access key would send the person through the browser again. Refreshing on every
    start gives each session a full hour.
    """
    data = storage.read()
    if "tokens" not in data or "client" not in data:
        # No keys: the person approves again. Register again too, because AssistLoop deletes
        # registrations that hold no keys.
        data.pop("client", None)
        storage.write(data)
        return

    server = urlparse(SERVER_URL)
    async with httpx.AsyncClient(timeout=30) as http:
        resource = (await http.get(f"{server.scheme}://{server.netloc}/.well-known/oauth-protected-resource{server.path}")).json()
        issuer = resource["authorization_servers"][0]
        metadata = (await http.get(f"{issuer}/.well-known/oauth-authorization-server")).json()
        response = await http.post(metadata["token_endpoint"], data={
            "grant_type": "refresh_token",
            "client_id": data["client"]["client_id"],
            "refresh_token": data["tokens"]["refresh_token"],
            "resource": SERVER_URL,
        })

    if response.status_code == 200:
        await storage.set_tokens(OAuthToken.model_validate(response.json()))
    elif response.status_code in (400, 401):
        # The refresh key expired, or the app was disconnected: the next connect registers again
        # and asks the person to approve.
        data.pop("tokens")
        data.pop("client")
        storage.write(data)
    else:
        # Rate limited or a server error: keep the keys and try again later.
        response.raise_for_status()


callback: dict[str, str] = {}
received = threading.Event()


class CallbackHandler(BaseHTTPRequestHandler):
    """Receives the browser after the person approves or cancels on the consent page."""

    def do_GET(self) -> None:
        url = urlparse(self.path)
        if url.path == "/callback":
            callback.update({key: values[0] for key, values in parse_qs(url.query).items()})
            received.set()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write(b"You can close this tab and return to the app.")

    def log_message(self, *args) -> None:
        pass


async def open_consent_page(authorization_url: str) -> None:
    callback.clear()
    received.clear()
    server = HTTPServer(("127.0.0.1", CALLBACK_PORT), CallbackHandler)
    server.timeout = 1

    def serve() -> None:
        deadline = time.monotonic() + APPROVAL_TIMEOUT_SECONDS
        while not received.is_set() and time.monotonic() < deadline:
            server.handle_request()
        server.server_close()

    threading.Thread(target=serve, daemon=True).start()
    print("Approve the connection in your browser:", authorization_url)
    webbrowser.open(authorization_url)


async def wait_for_callback() -> tuple[str, str | None]:
    if not await asyncio.to_thread(received.wait, APPROVAL_TIMEOUT_SECONDS):
        raise TimeoutError("Nobody approved the connection in time.")
    if "code" not in callback:
        raise RuntimeError(f"The connection was not approved: {callback.get('error', 'no code')}")
    return callback["code"], callback.get("state")


async def main() -> None:
    storage = FileTokenStorage()
    await refresh_stored_keys(storage)

    auth = OAuthClientProvider(
        server_url=SERVER_URL,
        client_metadata=OAuthClientMetadata(
            client_name="Acme support automation",
            redirect_uris=[REDIRECT_URI],
            grant_types=["authorization_code", "refresh_token"],
            response_types=["code"],
            token_endpoint_auth_method="none",
            scope="mcp:read mcp:write",
        ),
        storage=storage,
        redirect_handler=open_consent_page,
        callback_handler=wait_for_callback,
    )

    async with httpx.AsyncClient(auth=auth, timeout=httpx.Timeout(30, read=300)) as http:
        async with streamable_http_client(SERVER_URL, http_client=http) as (read, write, _):
            async with ClientSession(read, write) as session:
                await session.initialize()

                tools = await session.list_tools()
                print("Tools:", ", ".join(tool.name for tool in tools.tools))

                result = await session.call_tool("list_organizations", {})
                if result.isError:
                    print(result.content[0].text)
                else:
                    print(json.dumps(result.structuredContent, indent=2))


if __name__ == "__main__":
    asyncio.run(main())

What happens on the first run:

  1. The SDK calls /mcp without a key and gets 401 with the address of the sign-in metadata.
  2. It reads the metadata, registers your app through POST /oauth/register, and opens the AssistLoop consent page in the browser.
  3. The person signs in to AssistLoop if needed, checks the app's name and return address, chooses whether to allow changes, and clicks Authorize.
  4. The browser returns to http://127.0.0.1:8765/callback, and the SDK exchanges the code for keys and stores them.

On later runs, refresh_stored_keys swaps the stored keys for fresh ones, then the SDK connects with them. No browser opens. If the refresh key was rejected (it expired, or the app was disconnected), the program drops the keys and the registration, registers again and asks the person to approve.

Things to know about this example:

  • An access key lasts an hour, and SDK 1.30.0 does not refresh keys it loaded from storage. Open a new session for each batch of work, as main() does, and don't keep one session open for more than an hour. A session that outlives its key opens the browser again.
  • Each refresh replaces the refresh key and revokes the old one. Don't run two copies of the program on the same state file at once: the second refresh would fail and drop the keys.
  • If the sign-in request is wrong, the consent page shows an error and stays open; the browser never comes back to your callback. The program gives up after 10 minutes.
  • On a read-only connection, calling a write tool makes the SDK open the browser again to ask for write access, instead of returning an error.
  • Keep the state file private: it holds keys that act as that person.

Next: Tools and Errors and limits.

Build your own client

Use these steps when your code gets the access key itself: an LLM API that takes a remote MCP server and a bearer token, or a client written without an MCP SDK. See the requirements and server details above.

1. Find the sign-in endpoints

A request to /mcp without a key answers 401 with this header:

WWW-Authenticate: Bearer realm="mcp", resource_metadata="https://api.assistloop.ai/.well-known/oauth-protected-resource/mcp", scope="mcp:read mcp:write"

GET https://api.assistloop.ai/.well-known/oauth-protected-resource/mcp returns:

{
  "resource": "https://api.assistloop.ai/mcp",
  "authorization_servers": ["https://api.assistloop.ai"],
  "scopes_supported": ["mcp:read", "mcp:write"],
  "bearer_methods_supported": ["header"]
}

GET https://api.assistloop.ai/.well-known/oauth-authorization-server returns:

{
  "issuer": "https://api.assistloop.ai",
  "authorization_endpoint": "https://assistloop.ai/oauth/authorize",
  "token_endpoint": "https://api.assistloop.ai/oauth/token",
  "client_id_metadata_document_supported": true,
  "registration_endpoint": "https://api.assistloop.ai/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["mcp:read", "mcp:write"]
}

The authorization endpoint is the consent page in the AssistLoop web app. Read every URL from this metadata instead of building it yourself.

2. Identify your app

Your app needs a client_id. There are two ways to get one.

Register your app (POST /oauth/register, RFC 7591):

POST /oauth/register HTTP/1.1
Host: api.assistloop.ai
Content-Type: application/json

{
  "client_name": "Acme support automation",
  "redirect_uris": ["https://acme.example/assistloop/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

The answer is 201:

{
  "client_id": "9f7c2a4e-...",
  "client_name": "Acme support automation",
  "redirect_uris": ["https://acme.example/assistloop/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "mcp:read mcp:write",
  "client_id_issued_at": 1793116800
}

Store the client_id and reuse it while your app holds keys. AssistLoop deletes a registration that is more than 24 hours old and holds no keys: one that nobody approved goes after about a day; one whose keys all expired or were revoked goes about 31 days after its last access key expired. If the consent page says your app is not recognized, or the token endpoint answers invalid_client, register again.

Registration rules:

  • client_name is required, at most 100 characters.
  • redirect_uris holds 1 to 10 addresses. Each is https, or http on 127.0.0.1, localhost or [::1] only. No fragment (#), no user name or password in the address, no comma, at most 2000 characters. Custom schemes such as myapp:// are refused.
  • Apps are public clients: there is no client secret. token_endpoint_auth_method may only be none.
  • grant_types may only list authorization_code and refresh_token; response_types only code.
  • Other metadata, such as logo_uri, is ignored.
  • Each IP address can register 10 apps a minute and 100 a day.

A refused registration answers 400: "error": "invalid_client_metadata" when a field is missing or has a wrong value, or "error": "invalid_redirect_uri" when a redirect address breaks the rules above. Past the registration limit, it answers 429.

Or publish a Client ID Metadata Document. Your client_id is then an https URL that serves a JSON document about your app:

{
  "client_id": "https://acme.example/assistloop/client.json",
  "client_name": "Acme support automation",
  "redirect_uris": ["https://acme.example/assistloop/callback"],
  "token_endpoint_auth_method": "none"
}
  • client_id in the document must equal its URL exactly.
  • The URL needs a path, and no query or fragment.
  • The host must be written in lower case.
  • AssistLoop fetches it from a public address only and follows no redirects. The answer must be status 200, with a JSON content type, not compressed, within 3 seconds and at most 64 KB.
  • token_endpoint_auth_method, if present, must be none.
  • client_name and redirect_uris follow the registration rules above.
  • AssistLoop caches the document for its Cache-Control max-age, between 5 minutes and 24 hours (1 hour without one). After a failed fetch, it waits 5 minutes before fetching that URL again, so a fix can take that long to show.

Create a PKCE pair: a random code_verifier of 43 to 128 characters (letters, digits, -, ., _, ~), and code_challenge = base64url(SHA-256(code_verifier)) without padding. Then open the authorization_endpoint in the person's browser with these query parameters:

ParameterValue
response_typecode
client_idYour client_id
redirect_uriOne of your registered redirect addresses
stateA random value. Required.
code_challengeYour PKCE challenge. Required.
code_challenge_methodS256. A missing method or plain is refused.
resourcehttps://api.assistloop.ai/mcp, exactly the resource from the metadata. Required.
scopeOptional: mcp:read, or mcp:read mcp:write. Without it, the app asks for both.

The consent page shows your app's name, the hosts of its redirect addresses, and the access it asks for. It marks an app as Verified only for Claude and ChatGPT. Your app shows as Unverified app, with a note that AssistLoop cannot confirm who made it. When every redirect address is on the local machine, the page also warns that any program on that computer could use the connection.

The person can turn off Allow changes to give read access only. An app that asks for mcp:read alone never gets write access.

After the person clicks Authorize, the browser goes to:

https://acme.example/assistloop/callback?code=...&state=...

Check that state is the value you sent. If the person clicks Cancel, the browser returns with error=access_denied and your state. If redirect_uri is not one of your registered addresses, Cancel stays in AssistLoop instead.

The consent page refuses a missing client_id, redirect_uri or state, or a response_type other than code, as soon as it opens. It checks everything else when the person clicks Authorize. If the request is wrong (for example a missing resource, a wrong code_challenge_method, or a redirect_uri that is not registered), the page shows "Failed to authorize the application" and stays open; the browser does not come back to your app. For an unknown client_id, the page says the app is not recognized. Give up waiting after a while.

The redirect_uri must match a registered address exactly. For http://127.0.0.1 and http://[::1] addresses, the port may differ; for localhost it may not.

4. Exchange the code for keys

The code works once, within 10 minutes.

POST /oauth/token HTTP/1.1
Host: api.assistloop.ai
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=9f7c2a4e-...
&redirect_uri=https%3A%2F%2Facme.example%2Fassistloop%2Fcallback
&code=...
&code_verifier=...
&resource=https%3A%2F%2Fapi.assistloop.ai%2Fmcp

resource is required here too. The answer:

{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "...",
  "refresh_token": "..."
}

5. Call the server

Send the access key in the Authorization header of every request:

POST /mcp HTTP/1.1
Host: api.assistloop.ai
Authorization: Bearer ACCESS_KEY
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "list_organizations", "arguments": {}}}

Start with initialize, then tools/list and tools/call, as the MCP specification describes. The server answers each request with one JSON response (application/json), not an event stream. tools/list returns all the connection's tools in one page. The key works only on /mcp, not on the dashboard API or the Public API.

6. Refresh before the access key expires

POST /oauth/token HTTP/1.1
Host: api.assistloop.ai
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=9f7c2a4e-...
&refresh_token=...
&resource=https%3A%2F%2Fapi.assistloop.ai%2Fmcp

resource is optional on a refresh; if you send it, it must be the /mcp URL. The answer has the same shape as in step 4. Store the new refresh_token at once: the old access and refresh keys stop working. If your system does not refresh within 30 days, the person has to approve the connection again.

A refresh key that expired, was already used, or belongs to a disconnected app answers HTTP 401 with "error": "invalid_request" (not invalid_grant). A deleted registration answers 401 with "error": "invalid_client". In both cases, send the person through the consent page again.

Read-only connections

On a read-only connection, tools/list shows only the 17 read tools. A call to a write tool answers HTTP 403:

WWW-Authenticate: Bearer realm="mcp", error="insufficient_scope", scope="mcp:read mcp:write", resource_metadata="https://api.assistloop.ai/.well-known/oauth-protected-resource/mcp"
{
  "error": "insufficient_scope",
  "error_description": "This connection is read-only. Reconnect and allow changes to use this tool."
}

To get write access, send the person through the consent page again (step 3) with scope=mcp:read mcp:write.

Next: Tools and Errors and limits.

Tools

Every id is a uuid that another tool returned. Start with list_organizations, then list_agents.

Read tools (scope mcp:read)

ToolWhat it doesInputs (required in bold)
list_organizationsYour organizations: id, name, slug, your role, plan name, and whether it is premium.none
list_agentsThe agents you can see in your premium organizations.organization_id, status (active, disabled)
get_agentOne agent: name, description, model, handoff settings, whether it is disabled and why, its organization, and whether ticketing is on.agent_id
list_ai_modelsThe AI models an agent can use, newest first, with the credits one reply costs.none
list_conversationsOne agent's conversations, most recently active first.agent_id, page, per_page
get_conversationOne conversation: status, response mode, channel, the customer and the last message.conversation_id
list_conversation_messagesOne conversation's messages, newest first. Cursor-paged.conversation_id, cursor, per_page
list_sourcesOne agent's training sources, oldest first, with their training state.agent_id, page, per_page
get_website_sourceOne website page source: its stored text (first 4000 characters) and content_hash.source_id
get_crawl_resultsA website crawl's status and, once completed, the pages it found.crawl_task_id
get_training_progressOne agent's running and recent website page fetches (last 24 hours, at most 20).agent_id
get_training_statusThe training state of one source.source_id
list_ticketsOne agent's tickets, newest first.agent_id, page, per_page, status, priority, assigned_to, q
get_ticketOne ticket, with its linked conversation.ticket_id
list_ticket_messagesA ticket's customer thread, oldest first.ticket_id, page, per_page
list_ticket_commentsA ticket's internal notes, newest first. The customer never sees them.ticket_id, page, per_page
list_ticket_assigneesThe teammates who can be assigned this agent's tickets.agent_id

Write tools (scope mcp:write)

ToolWhat it doesInputs (required in bold)
reply_to_conversationSends a reply to the customer as a teammate, on the channel the conversation came from (for example the chat widget or WhatsApp), and switches the conversation to human mode, so the AI stops answering. Plain text. Cannot be taken back.conversation_id, message (at most 10000 characters)
update_conversationOpens or closes a conversation, or switches who answers it. A closed conversation must be reopened before its mode can change.conversation_id, status (open, closed), response_mode (ai, human); at least one
update_agentChanges an agent's name, model, human_intervention or only_human.agent_id, name, ai_model_id, human_intervention, only_human; at least one
update_agent_promptReplaces an agent's system prompt. The old prompt cannot be restored through MCP.agent_id, prompt (at most 20000 characters)
create_ticketCreates a ticket. The customer is not contacted.agent_id, subject, description, priority, assigned_to, requester_name, requester_email, conversation_id
update_ticketChanges a ticket's status, priority, assignee or subject.ticket_id, status, priority, assigned_to, subject; at least one
add_ticket_commentAdds an internal note. The customer never sees it.ticket_id, body
reply_to_ticket_customerEmails the ticket's customer and stores the message on the ticket. The ticket needs a requester email. Cannot be taken back.ticket_id, body
add_text_sourceAdds a text training source. Training runs in the background. Returns a source_id.agent_id, text, title
add_qa_sourceAdds a question-and-answer training source. Returns a source_id.agent_id, questions (a list, at least one), answer
start_website_crawlStarts crawling a website to find its pages. Returns a crawl_task_id.agent_id, url
train_website_pagesFetches website pages and trains the agent on them. Returns a task_id.agent_id, urls (1 to 100), replace_existing
update_website_source_contentReplaces the stored text of a website page source. Only stores the text: run retrain_source afterwards. Content containing the [truncated] marker is refused.source_id, content, base_content_hash, title
retrain_sourceTrains one source again.source_id

Every write tool also takes an optional idempotency_key; see Retries.

Ticket text you send (description, body) is plain text or markdown. AssistLoop converts it to HTML, and the length limits (10000 for description, 20000 for body) apply to the HTML, so a text just under the limit can still be refused as invalid_input. Ticket messages and notes come back as HTML. reply_to_conversation takes plain text only.

tools/list returns each tool's full input schema and a description written for AI models. The tools also carry MCP annotations: read tools are readOnlyHint and idempotentHint; update_agent and update_conversation are also idempotentHint; update_agent_prompt and update_website_source_content are destructiveHint, and the two tools that reach customers (reply_to_conversation, reply_to_ticket_customer) are openWorldHint. If an AI model calls the tools, ask a person to confirm those before they run.

Extra permissions

Beyond seeing the agent, some tools need more:

ToolsNeeds
update_agent with name or ai_model_id, update_agent_promptOwner or admin of the organization
get_website_source, get_crawl_results, and all training write toolsPermission to manage the agent's training sources, as in the dashboard
All ticket toolsTicketing switched on for the agent

Results

A successful call returns the result as MCP structured content, and the same JSON as text for clients that read only text:

{
  "data": [
    { "id": "0fcd3a02-3010-4df3-b4f6-b5acacac514a", "name": "Acme", "role": "owner", "premium": true }
  ],
  "meta": { "total": 1, "has_more": false }
}

The example shows only some of the fields.

  • data holds one record or a list.
  • Every list has meta.has_more. Page-numbered lists add page, per_page and total; lists without pages add total. list_conversation_messages adds per_page and next_cursor: pass next_cursor back as cursor to get the next page.
  • per_page is at most 50 (default 20). page is at most 10000.
  • Ids are uuids. A record's own id is in id. Ids of other records keep their names, such as organization_id, and the background tools return theirs as source_id, crawl_task_id or task_id.
  • Text longer than 4000 characters is cut and ends with the [truncated] marker.

Work that runs in the background

Training and crawling don't finish during the call. The tool returns at once, and you check progress with a read tool:

  1. start_website_crawl returns a crawl_task_id. If the agent is already crawling the same page, it returns that crawl's id with status already_running.
  2. Call get_crawl_results until status is completed (or failed).
  3. Pass the page URLs you want to train_website_pages. Pages already saved are refused unless replace_existing is true.
  4. train_website_pages returns a task_id. Find it in get_training_progress to follow the fetch, then check the sources with list_sources or get_training_status. (get_crawl_results accepts only a crawl_task_id from start_website_crawl; it answers not_found for a fetch's task_id.)

add_text_source, add_qa_source and retrain_source return a source_id: follow it with get_training_status.

Errors and limits

Tool errors

A tool that fails returns an MCP result with isError: true and one line of text:

[not_found] Not found, or you do not have access to it. (request 6f1c0d7e-...)

The code in brackets is stable, so your system can match on it. Quote the request id when you contact support.

CodeMeaning
unauthenticatedThe connection expired. Reconnect. (A missing or expired key normally gets HTTP 401 before any tool runs.)
not_foundThe id does not exist, or you have no access to it. The two cases answer the same way.
forbiddenYour role does not allow this action, or ticketing is off for the agent.
scope_requiredThe connection is read-only. Reserved: a write call on a read-only connection gets HTTP 403 insufficient_scope before any tool runs.
premium_requiredThe organization is not on a premium plan. For list_organizations, list_ai_models and list_agents without organization_id: none of your organizations is. list_agents with the id of a free organization also answers premium_required.
invalid_inputAn argument is invalid. The text usually names the fields.
rate_limitedToo many requests. The text says how many seconds to wait.
duplicate_requestA write with this idempotency_key already ran, or is still running.
agent_disabledThe agent is disabled.
agent_busyAnother settings change for this agent is in progress. Try again shortly.
byok_engineThe agent runs on the organization's own API key. Change its model and prompt in the dashboard.
model_unavailableThe model does not exist, or this agent cannot run it.
conversation_closedThe conversation is closed. Reopen it first.
visitor_blockedThe visitor is blocked, so replies are paused.
channel_window_closedThe messaging window for this customer is closed (for example WhatsApp's 24 hours). The customer must write first.
channel_not_connectedThe messaging channel is not connected for this agent.
channel_conversation_closedThe channel conversation is closed.
channel_attachments_unsupportedThe channel does not accept these attachments.
cannot_deliverThe reply cannot be delivered to this customer.
requester_email_requiredThe ticket has no requester email.
storage_limitThe organization reached its training storage limit.
training_in_progressThe source is still training.
crawl_already_runningtrain_website_pages asked for pages that are already being fetched for this agent. Check get_training_progress.
pages_already_savedThe pages are already saved. Send replace_existing: true to fetch them again.
content_not_fetchedThe page has no stored text yet.
content_changedThe page text changed since you read it, or base_content_hash is wrong. Read it again.
content_too_longThe page is longer than get_website_source shows. Edit it in the dashboard.
agent_service_unavailableAssistLoop's AI service did not respond or refused the change. Check the agent or source before you retry: part of the change may have happened.
crawl_service_unavailableThe crawl service did not respond. Try again later.
internal_errorSomething went wrong on our side.

Some errors come before any tool runs, as HTTP status codes:

StatusWhen
401No key, or the key expired or was revoked. Refresh it or sign in again.
403 with insufficient_scopeA write tool on a read-only connection.
429More than 300 requests to /mcp in a minute for one person, across all of that person's connections. Every request counts, including initialize and tools/list.

Others are JSON-RPC errors: an unknown tool name answers error code -32602, and an unexpected failure outside a tool answers -32603.

Limits

Tool calls are counted per minute, for each organization and for each user:

Kind of toolPer organizationPer user
Read tools600240
update_agent, update_agent_prompt, update_conversation, create_ticket, update_ticket, add_ticket_comment12030
Tools that reach customers: reply_to_conversation, reply_to_ticket_customer6010
start_website_crawl, train_website_pages105
add_text_source, add_qa_source, update_website_source_content, retrain_source3010

Each row is one shared count: for example, update_agent and create_ticket calls add up toward the same 30 per user. The per-user counts cover every app the person connected: if they also use AssistLoop from Claude or ChatGPT, those calls count too. list_organizations, list_agents and list_ai_models count against the user only.

A call counts once it passes the input, access and idempotency checks, even if the tool then fails (for example with storage_limit). Calls refused by those checks, or with rate_limited, do not count.

An organization can have at most 5 crawls and page-fetch batches (one per train_website_pages call) queued or running at once, including those started from the dashboard or the Public API. Past that, the crawl tools answer rate_limited with a 60-second wait. start_website_crawl for a page the agent is already crawling still returns that crawl (already_running).

Retries and idempotency

A write tool can time out after it has done its work. To retry safely, send an idempotency_key (8 to 64 characters) with every write, and the same key on the retry:

  • For 10 minutes, a second call with the same key, user and tool answers duplicate_request instead of running again. Check the result with a read tool.
  • When the first call was refused before anything changed (for example invalid_input or rate_limited), the key is released, so you can fix the input and retry with the same key.
  • When the first call failed with agent_service_unavailable, crawl_service_unavailable or internal_error, the key stays used for the 10 minutes, because the change may have happened. Check with a read tool; to try again, send a new key.
  • A retry sent while the first call is still running also answers duplicate_request.

Disconnecting

The person who approved the connection can see and disconnect your app in the AssistLoop dashboard, under MCP in the organization's sidebar (click Disconnect, then confirm). The page lists the person's apps across all their organizations. Disconnecting revokes every key the app holds for that person at once, and the next request answers 401.

Security

  • Store refresh keys like passwords. They act as the person who approved the connection.
  • Customers write conversation and ticket text. If an AI model reads it, treat it as data, never as instructions.
  • Two tools reach real customers and cannot be undone: reply_to_conversation and reply_to_ticket_customer.

Not available through MCP

  • Deleting anything, including sources, and creating agents.
  • Uploading files as training sources.
  • Sending messages as the visitor.
  • Agents that run on the organization's own AI provider key (BYOK): their model and prompt are changed in the dashboard.
  • Signing in with an AssistLoop API key, or without a person (no client credentials flow).
  • Custom URL schemes as redirect addresses.