How to integrate Plain MCP with LlamaIndex

Connect LlamaIndex to Plain MCP. Add a customer to the enterprise group, fetch company details for acme corp, and more using natural language, with authentication handled for you.

Plain logoPlain
Api Key

Plain is a collaborative support platform for B2B teams, focused on consolidating conversations and boosting efficiency with AI. Enjoy streamlined, modern workflows to assist customers across channels from one place.

119 Tools11 Triggers

Introduction

This guide walks you through connecting Plain to LlamaIndex using the Composio tool router. By the end, you'll have a working Plain agent that can create and search support threads, reply to customers, assign and label threads, manage customers and tenants, and set SLA policies through natural language commands.

This guide will help you understand how to give your LlamaIndex agent real control over a Plain account through Composio's Plain MCP server.

Before we dive in, let's take a quick look at the key ideas and tools involved.

Also integrate Plain with

TL;DR

Here's what you'll learn:
  • Set your OpenAI and Composio API keys
  • Install LlamaIndex and Composio packages
  • Create a Composio Tool Router session for Plain
  • Connect LlamaIndex to the Plain MCP server
  • Build a Plain-powered agent using LlamaIndex
  • Interact with Plain through natural language

What is LlamaIndex?

LlamaIndex is a data framework for building LLM applications. It provides tools for connecting LLMs to external data sources and services through agents and tools.

Key features include:

  • ReAct Agent: Reasoning and acting pattern for tool-using agents
  • MCP Tools: Native support for Model Context Protocol
  • Context Management: Maintain conversation context across interactions
  • Async Support: Built for async/await patterns

What is the Plain MCP server, and what's possible with it?

The Plain MCP server is an implementation of the Model Context Protocol that connects your AI agent and assistants like Claude, Cursor, etc directly to your Plain account. It provides structured and secure access to your B2B support workspace, so your agent can perform actions like managing customers, creating support threads, fetching company details, handling issues, and organizing customer groups on your behalf.

  • Customer management and onboarding: Automatically create new customer records, fetch customer information by email or ID, and add customers to specific support groups for better organization.
  • Support thread creation: Let your agent create new support threads tied to customers, making it easy to kick off or escalate conversations without manual intervention.
  • Issue tracking and retrieval: Fetch all external issue links associated with a customer, helping your team stay on top of ongoing problems and resolutions.
  • Company and tier information access: Retrieve detailed company profiles and tier metadata, including contract value, owner details, and more, to personalize support interactions.
  • User and customer cleanup: Safely delete customers or users from the system when offboarding or data hygiene is needed, all through agent-driven actions.

What is the Composio tool router, and how does it fit here?

What is Composio SDK?

Composio's Composio SDK helps agents find the right tools for a task at runtime. You can plug in multiple toolkits (like Gmail, HubSpot, and GitHub), and the agent will identify the relevant app and action to complete multi-step workflows. This can reduce token usage and improve the reliability of tool calls. Read more here: Getting started with Composio SDK

The tool router generates a secure MCP URL that your agents can access to perform actions.

How the Composio SDK works

The Composio SDK follows a three-phase workflow:

  1. Discovery: Searches for tools matching your task and returns relevant toolkits with their details.
  2. Authentication: Checks for active connections. If missing, creates an auth config and returns a connection URL via Auth Link.
  3. Execution: Executes the action using the authenticated connection.

Step-by-step Guide

Step by step10 STEPS
1

Prerequisites

Before you begin, make sure you have:
  • Python 3.8/Node 16 or higher installed
  • A Composio account with the API key
  • An OpenAI API key
  • A Plain account and project
  • Basic familiarity with async Python/Typescript
2

Getting API Keys for OpenAI, Composio, and Plain

OpenAI API key (OPENAI_API_KEY)
  • Go to the OpenAI dashboard
  • Create an API key if you don't have one
  • Assign it to OPENAI_API_KEY in .env
Composio API key and user ID
  • Log into the Composio dashboard
  • Copy your API key from Settings
    • Use this as COMPOSIO_API_KEY
  • Pick a stable user identifier (email or ID)
    • Use this as COMPOSIO_USER_ID
3

Installing dependencies

npm install @composio/llamaindex @llamaindex/openai @llamaindex/tools @llamaindex/workflow dotenv

Create a new Typescript project and install the necessary dependencies:

  • @composio/llamaindex: Composio's LlamaIndex integration
  • @llamaindex/openai: OpenAI LLM integration
  • @llamaindex/tools: MCP client for LlamaIndex
  • @llamaindex/workflow: Workflow framework for LlamaIndex
  • dotenv: Environment variable management
4

Set environment variables

bash
OPENAI_API_KEY=your-openai-api-key
COMPOSIO_API_KEY=your-composio-api-key
COMPOSIO_USER_ID=your-user-id

Create a .env file in your project root:

These credentials will be used to:

  • Authenticate with OpenAI's GPT-5 model
  • Connect to Composio's Tool Router
  • Identify your Composio user session for Plain access
5

Import modules

import "dotenv/config";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { Composio } from "@composio/core";

import { mcp } from "@llamaindex/tools";
import { agent as createAgent } from "@llamaindex/workflow";
import { openai } from "@llamaindex/openai";

dotenv.config();

Create a new file called plain_llamaindex_agent.ts and import the required modules:

Key imports:

  • dotenv.config loads .env at runtime
  • readline gives us a simple CLI chat loop
  • Composio is the main Composio SDK client
  • mcp connects to an MCP endpoint
  • createAgent builds a LlamaIndex agent
  • openai configures the LLM backend
6

Load environment variables and initialize Composio

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const COMPOSIO_API_KEY = process.env.COMPOSIO_API_KEY;
const COMPOSIO_USER_ID = process.env.COMPOSIO_USER_ID;

if (!OPENAI_API_KEY) throw new Error("OPENAI_API_KEY is not set");
if (!COMPOSIO_API_KEY) throw new Error("COMPOSIO_API_KEY is not set");
if (!COMPOSIO_USER_ID) throw new Error("COMPOSIO_USER_ID is not set");

What's happening:

This ensures missing credentials cause early, clear errors before the agent attempts to initialise.

7

Create a Tool Router session and build the agent function

async function buildAgent() {

  console.log(`Initializing Composio client...${COMPOSIO_USER_ID!}...`);
  console.log(`COMPOSIO_USER_ID: ${COMPOSIO_USER_ID!}...`);

  const composio = new Composio({
    apiKey: COMPOSIO_API_KEY,
    provider: new LlamaindexProvider(),
  });

  const session = await composio.create(
    COMPOSIO_USER_ID!,
    {
      toolkits: ["plain"],
    },
  );

  const mcpUrl = session.mcp.url;
  console.log(`Composio Tool Router MCP URL: ${mcpUrl}`);

  const server = mcp({
    url: mcpUrl,
    clientName: "composio_tool_router_with_llamaindex",
    requestInit: {
      headers: {
        "x-api-key": COMPOSIO_API_KEY!,
      },
    },
    // verbose: true,
  });

  const tools = await server.tools();

  const llm = openai({ apiKey: OPENAI_API_KEY, model: "gpt-5" });

  const agent = createAgent({
    name: "composio_tool_router_with_llamaindex",
        description : "An agent that uses Composio Tool Router MCP tools to perform actions.",
    systemPrompt:
      "You are a helpful assistant connected to Composio Tool Router."+
"Use the available tools to answer user queries and perform Plain actions." ,
    llm,
    tools,
  });

  return agent;
}

What's happening here:

  • We create a Composio client using your API key and configure it with the LlamaIndex provider
  • We then create a tool router MCP session for your user, specifying the toolkits we want to use (in this case, plain)
  • The session returns an MCP HTTP endpoint URL that acts as a gateway to all your configured tools
  • LlamaIndex will connect to this endpoint to dynamically discover and use the available Plain tools.
  • The MCP tools are mapped to LlamaIndex-compatible tools and plug them into the Agent.
8

Create an interactive chat loop

async function chatLoop(agent: ReturnType<typeof createAgent>) {
  const rl = readline.createInterface({ input, output });

  console.log("Type 'quit' or 'exit' to stop.");

  while (true) {
    let userInput: string;

    try {
      userInput = (await rl.question("\nYou: ")).trim();
    } catch {
      console.log("\nAgent: Bye!");
      break;
    }

    if (!userInput) {
      continue;
    }

    const lower = userInput.toLowerCase();
    if (lower === "quit" || lower === "exit") {
      console.log("Agent: Bye!");
      break;
    }

    try {
      process.stdout.write("Agent: ");

      const stream = agent.runStream(userInput);
      let finalResult: any = null;

      for await (const event of stream) {
        // The event.data contains the streamed content
        const data: any = event.data;

        // Check for streaming delta content
        if (data?.delta) {
          process.stdout.write(data.delta);
        }

        // Store final result for fallback
        if (data?.result || data?.message) {
          finalResult = data;
        }
      }

      // If no streaming happened, show the final result
      if (finalResult) {
        const answer =
          finalResult.result ??
          finalResult.message?.content ??
          finalResult.message ??
          "";
        if (answer && typeof answer === "string" && !answer.includes("[object")) {
          process.stdout.write(answer);
        }
      }

      console.log(); // New line after streaming completes
    } catch (err: any) {
      console.error("\nAgent error:", err?.message ?? err);
    }
  }

  rl.close();
}

What's happening:

  • We're creating a direct terminal interface to chat with Plain
  • The LLM's responses are streamed to the CLI for faster interaction.
  • The agent uses context to maintain conversation history
  • The agent processes the request, selects appropriate Plain tools, and returns a result
  • We extract the answer from the result data structure and display it to the user
  • You can type 'quit' or 'exit' to stop the chat loop gracefully
  • Agent responses and any errors are streamed in a clear, readable format
9

Define the main entry point

async function main() {
  try {
    const agent = await buildAgent();
    await chatLoop(agent);
  } catch (err) {
    console.error("Failed to start agent:", err);
    process.exit(1);
  }
}

main();

What's happening here:

  • We're orchestrating the entire application flow
  • The agent gets built with proper error handling
  • Then we kick off the interactive chat loop so you can start talking to Plain
10

Run the agent

npx ts-node llamaindex-agent.ts

When prompted, authenticate and authorise your agent with Plain, then start asking questions.

Complete Code

Here's the complete code to get you started with Plain and LlamaIndex:

import "dotenv/config";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { Composio } from "@composio/core";
import { LlamaindexProvider } from "@composio/llamaindex";

import { mcp } from "@llamaindex/tools";
import { agent as createAgent } from "@llamaindex/workflow";
import { openai } from "@llamaindex/openai";

dotenv.config();

const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const COMPOSIO_API_KEY = process.env.COMPOSIO_API_KEY;
const COMPOSIO_USER_ID = process.env.COMPOSIO_USER_ID;

if (!OPENAI_API_KEY) {
    throw new Error("OPENAI_API_KEY is not set in the environment");
  }
if (!COMPOSIO_API_KEY) {
    throw new Error("COMPOSIO_API_KEY is not set in the environment");
  }
if (!COMPOSIO_USER_ID) {
    throw new Error("COMPOSIO_USER_ID is not set in the environment");
  }

async function buildAgent() {

  console.log(`Initializing Composio client...${COMPOSIO_USER_ID!}...`);
  console.log(`COMPOSIO_USER_ID: ${COMPOSIO_USER_ID!}...`);

  const composio = new Composio({
    apiKey: COMPOSIO_API_KEY,
    provider: new LlamaindexProvider(),
  });

  const session = await composio.create(
    COMPOSIO_USER_ID!,
    {
      toolkits: ["plain"],
    },
  );

  const mcpUrl = session.mcp.url;
  console.log(`Composio Tool Router MCP URL: ${mcpUrl}`);

  const server = mcp({
    url: mcpUrl,
    clientName: "composio_tool_router_with_llamaindex",
    requestInit: {
      headers: {
        "x-api-key": COMPOSIO_API_KEY!,
      },
    },
    // verbose: true,
  });

  const tools = await server.tools();

  const llm = openai({ apiKey: OPENAI_API_KEY, model: "gpt-5" });

  const agent = createAgent({
    name: "composio_tool_router_with_llamaindex",
    description:
      "An agent that uses Composio Tool Router MCP tools to perform actions.",
    systemPrompt:
      "You are a helpful assistant connected to Composio Tool Router."+
"Use the available tools to answer user queries and perform Plain actions." ,
    llm,
    tools,
  });

  return agent;
}

async function chatLoop(agent: ReturnType<typeof createAgent>) {
  const rl = readline.createInterface({ input, output });

  console.log("Type 'quit' or 'exit' to stop.");

  while (true) {
    let userInput: string;

    try {
      userInput = (await rl.question("\nYou: ")).trim();
    } catch {
      console.log("\nAgent: Bye!");
      break;
    }

    if (!userInput) {
      continue;
    }

    const lower = userInput.toLowerCase();
    if (lower === "quit" || lower === "exit") {
      console.log("Agent: Bye!");
      break;
    }

    try {
      process.stdout.write("Agent: ");

      const stream = agent.runStream(userInput);
      let finalResult: any = null;

      for await (const event of stream) {
        // The event.data contains the streamed content
        const data: any = event.data;

        // Check for streaming delta content
        if (data?.delta) {
          process.stdout.write(data.delta);
        }

        // Store final result for fallback
        if (data?.result || data?.message) {
          finalResult = data;
        }
      }

      // If no streaming happened, show the final result
      if (finalResult) {
        const answer =
          finalResult.result ??
          finalResult.message?.content ??
          finalResult.message ??
          "";
        if (answer && typeof answer === "string" && !answer.includes("[object")) {
          process.stdout.write(answer);
        }
      }

      console.log(); // New line after streaming completes
    } catch (err: any) {
      console.error("\nAgent error:", err?.message ?? err);
    }
  }

  rl.close();
}

async function main() {
  try {
    const agent = await buildAgent();
    await chatLoop(agent);
  } catch (err: any) {
    console.error("Failed to start agent:", err?.message ?? err);
    process.exit(1);
  }
}

main();

Conclusion

You've successfully connected Plain to LlamaIndex through Composio's Tool Router MCP layer. Key takeaways:
  • Tool Router dynamically exposes Plain tools through an MCP endpoint
  • LlamaIndex's ReActAgent handles reasoning and orchestration; Composio handles integrations
  • The agent becomes more capable without increasing prompt size
  • Async Python provides clean, efficient execution of agent workflows
You can easily extend this to other toolkits like Gmail, Notion, Stripe, GitHub, and more by adding them to the toolkits parameter.
TOOLS & TRIGGERS

Supported Tools and Triggers

Every Plain action and event your agent gets out of the box.

Add Customer To Group

Add a customer to up to 25 customer groups.

Add Customer To Tenants

Idempotently add a customer to one or more tenants.

Add Generated Reply

Add a suggested reply for teammate review without sending it to the customer.

Add Labels To Thread

Add one or more labels to a thread by label-type ID.

Add Members to Tier

Add or move up to 25 tenants or companies into one tier.

Archive Label Type

Archive a label type while preserving labels already on threads.

Bulk Delete Thread Fields

Sequentially delete thread-field values by thread and schema key.

Bulk Set Thread Fields

Create or replace multiple typed thread-field values.

Change Thread Assignment

Assign or unassign a thread's primary assignee, or add or remove additional assignees.

Create Attachment Download URL

Create a sensitive three-minute URL and return its virus-scan result.

Create Attachment Upload Form

Create a sensitive, two-hour presigned upload capability; no file is uploaded.

Create Autoresponder

Create an autoresponder that automatically sends outbound customer replies.

Create Customer Group

Creates a new customer group in Plain for organizing and segmenting customers.

Create Email/Slack Discussion

Immediately and irreversibly start an external EMAIL or SLACK discussion.

Create Customer Or Thread Event

Create a custom customer-wide or single-thread timeline event.

Create Label Type

Create a label type that can be applied to threads.

Create Note

Create an internal note on an existing customer thread.

Create Tier SLA

Create one legacy tier SLA; next-response requires a first-response SLA on the tier.

Create SLA Policy

Create a named SLA policy with zero or more unique target types.

Create Snippet

Create a reusable plain-text and optional Markdown reply snippet.

Create Task

Create a task with optional company, tenant or account context and an optional assignee.

Create Thread

Create a thread after obtaining a valid customer identifier.

Create Thread Field Schema

Create a custom thread-field schema.

Create Tier

Create a support tier, optionally assigning tenant or company members.

Delete Autoresponder

Permanently delete a native autoresponder.

Delete Customer

Permanently and irreversibly deletes a Plain customer by ID, including all associated threads, timeline entries, and other customer data.

Delete Customer Group

Permanently delete an empty customer group after confirmation.

Delete Note

Soft-delete an internal note.

Delete Tier SLA

Delete a legacy SLA from a tier.

Delete SLA Policy

Delete an SLA policy, its targets, and their trackers.

Delete Snippet

Soft-delete a snippet while preserving historical references.

Delete Task

Soft-delete a task and remove its thread links.

Delete Tenant

Delete a tenant, unlink its customers, and remove its field values.

Delete Tenant Field Value

Clear one tenant field value without deleting its schema.

Delete Tenant Field Schema

Permanently delete a tenant-field schema and all values stored against it.

Delete Thread

Permanently delete a thread and all associated data.

Delete Thread Field Schema

Delete a schema and all values stored against it.

Delete Tier

Permanently delete a tier and all of its memberships.

Delete User

Permanently removes a Plain workspace user by ID.

Escalate Thread Or Select Path

Advance a thread's escalation or attach/detach its escalation path.

Fetch Company

Tool to fetch company details by ID.

Fetch Issues

Fetches external issue tracker links (Jira, Linear, GitHub, etc.

Fetch Tier

Fetch a tier by ID.

Get Autoresponder

Fetch a native Plain autoresponder by ID through a machine-user-safe list query.

Get Customer By Email

Fetch customer details by email address.

Get Customer By External ID

Fetch a customer by the exact stable external ID from your own system.

Get Customer By ID

Retrieve a customer's ID, full name, email address, and creation and update timestamps by customer ID.

Get Customer Group

Fetch one customer group by its Plain ID.

Get Customers

Tool to fetch a list of customers.

Get Email/Slack Discussion

Fetch metadata for one EMAIL or SLACK side-conversation by ID.

Get Knowledge Gap

Fetch one AI-detected knowledge gap and its linked task and signals.

Get Label Type

Fetch one label type by Plain ID or external ID.

Get SLA Policy

Fetch one SLA policy by its Plain ID.

Get Snippet

Fetch one snippet by ID, including a soft-deleted snippet.

Get Task by ID or Ref

Fetch one task by either its Plain ID or human-readable T-123 ref.

Get Tenant

Fetch one tenant by its Plain ID, including current custom field values.

Get Thread By ID

Fetch a thread by ID, including customer info, status, priority, labels, and assignments.

Get Thread By Reference

Fetch a thread by ref or by its customer-scoped external ID.

Get User By ID

Fetch workspace user/team member by ID.

Import Thread

Import one historical thread without triggering SLAs or autoresponders.

Import Thread Messages

Import up to 25 historical messages and return one result per input message.

List AI Feedback

List feedback submitted on Plain AI features, optionally filtered by feature, sentiment, or time.

List Autoresponders

List native Plain autoresponders in priority order.

List Customer Groups

List one paginated page of customer groups with optional external-ID filtering.

List Deleted Threads

List deletion-audit records for threads.

List Email/Slack Discussions

List only EMAIL and SLACK side-conversations with filters and pagination.

List Generated Replies

List current suggested replies for a thread's most recent inbound message.

List Knowledge Gaps

List AI-detected knowledge gaps with status filters and useful sort orders.

List Label Types

List active and/or archived label types.

List SLA Policies

List named SLA policies, optionally filtering by name or target type.

List Snippets

List the workspace snippet library with cursor pagination.

List Tasks

List workspace tasks with pagination, filtering, and sorting.

List Tenant Field Schemas

List tenant-field schemas before reading or writing custom values.

List Tenants

List tenants with current filters and cursor pagination.

List Thread Field Schemas

List the workspace's custom thread-field schemas.

List Threads By External ID

List a page of threads with an external ID across all customers.

List Threads With Rich Filters

List threads with the provider's complete public filter and sort surface.

List Tiers

List tiers with cursor pagination.

Mark Customer As Spam

Idempotently hide a customer's threads as spam and exclude them from metrics.

List Threads

Retrieve up to 50 threads from one page, optionally filtered by status.

Remove Customer From Group

Removes a customer from one or more customer groups in Plain.

Remove Customer From Tenants

Idempotently remove a customer from one or more tenants.

Remove Labels From Thread

Remove one or more concrete labels from a thread.

Remove Members from Tier

Remove up to 25 tenants or companies from their current tiers.

Reorder Autoresponders

Reorder native autoresponders in one call.

Reorder Thread Field Schemas

Reorder one or more thread-field schemas.

Reply To Email

Immediately and irreversibly send an external reply to an inbound email.

Run GraphQL Query

Execute an arbitrary GraphQL document against Plain without a safety gate; mutations can immediately and irreversibly change data.

Search Customers

Search customer profile fields or timeline text using explicit OR conditions.

Search Tenants

Search tenants by name, domain, or exact external ID.

Search Threads

Search thread titles, messages, and customer names or emails.

Send Email/Slack Discussion Message

Immediately and irreversibly deliver a message in an existing discussion.

Send Message

Immediately and irreversibly deliver a message through an existing thread.

Send New Email

Immediately and irreversibly send an external email to a customer.

Set Customer Tenants

Replace a customer's complete tenant membership set in one call.

Transition Thread Status

Move a thread to Todo, Snoozed, or Done.

Unarchive Label Type

Restore an archived label type for use on new threads.

Unmark Customer As Spam

Idempotently clear a customer's spam flag and restore inbox visibility.

Update Autoresponder

Update an autoresponder that automatically sends outbound customer replies.

Update Company

Upserts (creates or updates) a company in Plain.

Update Company Tier

Assign one company to a tier or clear its tier membership.

Update Generated Reply

Record teammate feedback on a generated reply.

Update Label Type

Update mutable label-type properties using field wrappers.

Update Note

Replace an internal note's content and optional attachment list.

Update Tier SLA

Update a legacy tier SLA without changing its SLA type.

Update SLA Policy

Replace an SLA policy's targets; omitted target types and their trackers are deleted.

Update Snippet

Update selected snippet fields using Plain's field-level wrappers.

Update Task

Update selected fields on an existing task.

Update Tenant Tier

Assign one tenant to a tier or clear its tier membership.

Update Thread Title

Tool to update a thread's title.

Update Thread Attributes

Change one thread priority, customer, external ID, tenant, or tier attribute.

Update Thread Field Schema

Update mutable properties of a thread-field schema.

Apply or Clear Thread SLA Policy

Apply an SLA policy, or clear it so the thread falls back to its tier's SLAs.

Update Tier

Update selected fields on an existing support tier.

Upsert Customer

Tool to upsert (create or update) a customer.

Upsert Customer Group

Idempotently create or update a customer group using machine-user-safe calls.

Upsert Tenant

Create or update a tenant while synchronizing its stable external ID.

Upsert Tenant Field

Set one typed custom field value for a tenant.

Upsert Tenant Field Schemas

Create or update tenant-field schemas keyed by source and external field ID.

FAQ

Frequently asked questions

With a standalone Plain MCP server, the agents and LLMs can only access a fixed set of Plain tools tied to that server. However, with the Composio Tool Router, agents can dynamically load tools from Plain and many other apps based on the task at hand, all through a single MCP endpoint.

Yes, you can. LlamaIndex fully supports MCP integration. You get structured tool calling, message history handling, and model orchestration while Tool Router takes care of discovering and serving the right Plain tools.

Yes, absolutely. You can configure which Plain scopes and actions are allowed when connecting your account to Composio. You can also bring your own OAuth credentials or API configuration so you keep full control over what the agent can do.

All sensitive data such as tokens, keys, and configuration is fully encrypted at rest and in transit. Composio is SOC 2 Type 2 compliant and follows strict security practices so your Plain data and credentials are handled as safely as possible.

Start with Plain.It takes 30 seconds.

Managed auth, hosted MCP servers, and every Plain tool your agent needs.Free to start.

Start building