How to integrate Plain MCP with Pydantic AI

Connect Pydantic AI 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 Pydantic AI 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 Pydantic AI 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:
  • How to set up your Composio API key and User ID
  • How to create a Composio Tool Router session for Plain
  • How to attach an MCP Server to a Pydantic AI agent
  • How to stream responses and maintain chat history
  • How to build a simple REPL-style chat interface to test your Plain workflows

What is Pydantic AI?

Pydantic AI is a Python framework for building AI agents with strong typing and validation. It leverages Pydantic's data validation capabilities to create robust, type-safe AI applications.

Key features include:

  • Type Safety: Built on Pydantic for automatic data validation
  • MCP Support: Native support for Model Context Protocol servers
  • Streaming: Built-in support for streaming responses
  • Async First: Designed 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 step09 STEPS
1

Prerequisites

Before starting, make sure you have:
  • Python 3.9 or higher
  • A Composio account with an active API key
  • Basic familiarity with Python and async programming
2

Getting API Keys for OpenAI and Composio

OpenAI API Key
  • Go to the OpenAI dashboard and create an API key. You'll need credits to use the models, or you can connect to another model provider.
  • Keep the API key safe.
Composio API Key
  • Log in to the Composio dashboard.
  • Navigate to your API settings and generate a new API key.
  • Store this key securely as you'll need it for authentication.
3

Install dependencies

bash
pip install composio pydantic-ai python-dotenv

Install the required libraries.

What's happening:

  • composio connects your agent to external SaaS tools like Plain
  • pydantic-ai lets you create structured AI agents with tool support
  • python-dotenv loads your environment variables securely from a .env file
4

Set up environment variables

bash
COMPOSIO_API_KEY=your_composio_api_key_here
USER_ID=your_user_id_here
OPENAI_API_KEY=your_openai_api_key

Create a .env file in your project root.

What's happening:

  • COMPOSIO_API_KEY authenticates your agent to Composio's API
  • USER_ID associates your session with your account for secure tool access
  • OPENAI_API_KEY to access OpenAI LLMs
5

Import dependencies

python
import asyncio
import os
from dotenv import load_dotenv
from composio import Composio
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStreamableHTTP

load_dotenv()
What's happening:
  • We load environment variables and import required modules
  • Composio manages connections to Plain
  • MCPServerStreamableHTTP connects to the Plain MCP server endpoint
  • Agent from Pydantic AI lets you define and run the AI assistant
6

Create a Tool Router Session

python
async def main():
    api_key = os.getenv("COMPOSIO_API_KEY")
    user_id = os.getenv("USER_ID")
    if not api_key or not user_id:
        raise RuntimeError("Set COMPOSIO_API_KEY and USER_ID in your environment")

    # Create a Composio Tool Router session for Plain
    composio = Composio(api_key=api_key)
    session = composio.create(
        user_id=user_id,
        toolkits=["plain"],
    )
    url = session.mcp.url
    if not url:
        raise ValueError("Composio session did not return an MCP URL")
What's happening:
  • We're creating a Tool Router session that gives your agent access to Plain tools
  • The create method takes the user ID and specifies which toolkits should be available
  • The returned session.mcp.url is the MCP server URL that your agent will use
7

Initialize the Pydantic AI Agent

python
# Attach the MCP server to a Pydantic AI Agent
plain_mcp = MCPServerStreamableHTTP(url, headers={"x-api-key": COMPOSIO_API_KEY})
agent = Agent(
    "openai:gpt-5",
    toolsets=[plain_mcp],
    instructions=(
        "You are a Plain assistant. Use Plain tools to help users "
        "with their requests. Ask clarifying questions when needed."
    ),
)
What's happening:
  • The MCP client connects to the Plain endpoint
  • The agent uses GPT-5 to interpret user commands and perform Plain operations
  • The instructions field defines the agent's role and behavior
8

Build the chat interface

python
# Simple REPL with message history
history = []
print("Chat started! Type 'exit' or 'quit' to end.\n")
print("Try asking the agent to help you with Plain.\n")

while True:
    user_input = input("You: ").strip()
    if user_input.lower() in {"exit", "quit", "bye"}:
        print("\nGoodbye!")
        break
    if not user_input:
        continue

    print("\nAgent is thinking...\n", flush=True)

    async with agent.run_stream(user_input, message_history=history) as stream_result:
        collected_text = ""
        async for chunk in stream_result.stream_output():
            text_piece = None
            if isinstance(chunk, str):
                text_piece = chunk
            elif hasattr(chunk, "delta") and isinstance(chunk.delta, str):
                text_piece = chunk.delta
            elif hasattr(chunk, "text"):
                text_piece = chunk.text
            if text_piece:
                collected_text += text_piece
        result = stream_result

    print(f"Agent: {collected_text}\n")
    history = result.all_messages()
What's happening:
  • The agent reads input from the terminal and streams its response
  • Plain API calls happen automatically under the hood
  • The model keeps conversation history to maintain context across turns
9

Run the application

python
if __name__ == "__main__":
    asyncio.run(main())
What's happening:
  • The asyncio loop launches the agent and keeps it running until you exit

Complete Code

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

python
import asyncio
import os
from dotenv import load_dotenv
from composio import Composio
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStreamableHTTP

load_dotenv()

async def main():
    api_key = os.getenv("COMPOSIO_API_KEY")
    user_id = os.getenv("USER_ID")
    if not api_key or not user_id:
        raise RuntimeError("Set COMPOSIO_API_KEY and USER_ID in your environment")

    # Create a Composio Tool Router session for Plain
    composio = Composio(api_key=api_key)
    session = composio.create(
        user_id=user_id,
        toolkits=["plain"],
    )
    url = session.mcp.url
    if not url:
        raise ValueError("Composio session did not return an MCP URL")

    # Attach the MCP server to a Pydantic AI Agent
    plain_mcp = MCPServerStreamableHTTP(url, headers={"x-api-key": COMPOSIO_API_KEY})
    agent = Agent(
        "openai:gpt-5",
        toolsets=[plain_mcp],
        instructions=(
            "You are a Plain assistant. Use Plain tools to help users "
            "with their requests. Ask clarifying questions when needed."
        ),
    )

    # Simple REPL with message history
    history = []
    print("Chat started! Type 'exit' or 'quit' to end.\n")
    print("Try asking the agent to help you with Plain.\n")

    while True:
        user_input = input("You: ").strip()
        if user_input.lower() in {"exit", "quit", "bye"}:
            print("\nGoodbye!")
            break
        if not user_input:
            continue

        print("\nAgent is thinking...\n", flush=True)

        async with agent.run_stream(user_input, message_history=history) as stream_result:
            collected_text = ""
            async for chunk in stream_result.stream_output():
                text_piece = None
                if isinstance(chunk, str):
                    text_piece = chunk
                elif hasattr(chunk, "delta") and isinstance(chunk.delta, str):
                    text_piece = chunk.delta
                elif hasattr(chunk, "text"):
                    text_piece = chunk.text
                if text_piece:
                    collected_text += text_piece
            result = stream_result

        print(f"Agent: {collected_text}\n")
        history = result.all_messages()

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

Conclusion

You've built a Pydantic AI agent that can interact with Plain through Composio's Tool Router. With this setup, your agent can perform real Plain actions through natural language. You can extend this further by:
  • Adding other toolkits like Gmail, HubSpot, or Salesforce
  • Building a web-based chat interface around this agent
  • Using multiple MCP endpoints to enable cross-app workflows (for example, Gmail + Plain for workflow automation)
This architecture makes your AI agent "agent-native", able to securely use APIs in a unified, composable way without custom integrations.
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. Pydantic AI 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