Skip to content

Building Agents

Integrate MCP Server Mattermost into multi-agent systems. This page covers tool metadata, access profiles, and filtering patterns for agent frameworks.

Tool Annotations

Every tool exposes standard MCP annotations that describe its behavior:

Annotation Meaning
readOnlyHint Tool only reads data, no side effects
destructiveHint Operation is irreversible (data loss)
idempotentHint Repeated calls produce the same result

How this server uses them:

Operation type readOnlyHint destructiveHint idempotentHint Example
Read true true list_public_channels, get_user
Write (idempotent) false true join_channel, pin_message
Write (non-idempotent) false post_message
Destructive true delete_message

FastMCP defaults

Unset annotations use FastMCP defaults: readOnlyHint=false, destructiveHint=true, idempotentHint=false. A dash (—) in the table means the default applies.

Capability Metadata

In addition to standard annotations, each tool carries a capability label in meta — a single word describing what the tool does:

Capability Meaning Example tools
read Retrieve data, no side effects list_public_channels, list_my_channels, search_messages, get_user
write Modify state within existing resources post_message, add_reaction, upload_file
create Create new top-level entities create_channel, create_direct_channel
delete Permanently destroy a resource delete_message, delete_bookmark

Classification rules:

  • create — only for new standalone entities (channels, DMs)
  • write — all other modifications, including dependent resources (bookmarks, messages, reactions exist only inside a channel)
  • delete — resource permanently ceases to exist
  • Each tool gets exactly one capability

Wire Format

Here's what a client receives from tools/list for a read-only tool:

{
  "name": "list_public_channels",
  "description": "List public channels available in a team. ...",
  "inputSchema": {
    "type": "object",
    "properties": {
      "team_id": {"type": "string"},
      "page": {"type": "integer", "minimum": 0},
      "per_page": {"type": "integer", "minimum": 1, "maximum": 200}
    },
    "required": ["team_id"]
  },
  "annotations": {
    "readOnlyHint": true,
    "idempotentHint": true
  },
  "tags": ["mattermost", "channel"],
  "_meta": {
    "capability": "read"
  }
}

And for a destructive tool:

{
  "name": "delete_message",
  "description": "Delete a message permanently. ...",
  "inputSchema": {"...": "..."},
  "annotations": {
    "destructiveHint": true
  },
  "tags": ["mattermost", "message"],
  "_meta": {
    "capability": "delete"
  }
}

Agent Profiles

Define profiles as sets of allowed capabilities. Each profile includes all capabilities below it:

PROFILES = {
    "reader":  {"read"},
    "writer":  {"read", "write"},
    "manager": {"read", "write", "create"},
    "admin":   {"read", "write", "create", "delete"},
}
Profile Can do Use case
reader Browse channels, search messages, look up users Monitoring, analytics, digest bots
writer Post messages, react, pin, upload files Chat bots, notification agents
manager Create channels, open DMs Onboarding agents, project setup
admin Delete messages and bookmarks Moderation, cleanup agents

Filtering Tools

Use capability metadata to give each agent only the tools it needs.

pydantic-ai

from pydantic_ai.mcp import MCPServerStdio

server = MCPServerStdio("uvx", args=["mcp-server-mattermost"])

WRITER_CAPS = {"read", "write"}
writer_toolset = server.filtered(
    lambda ctx, td: (td.metadata or {}).get("meta", {}).get("capability") in WRITER_CAPS
)

LangChain

from langchain_mcp_adapters.tools import load_mcp_tools

tools = await load_mcp_tools(session)

allowed = PROFILES["writer"]
agent_tools = [
    t for t in tools
    if (t.metadata or {}).get("_meta", {}).get("capability") in allowed
]

MCP SDK (low-level)

from mcp import ClientSession

async with ClientSession(read_stream, write_stream) as session:
    await session.initialize()
    tools = await session.list_tools()

    allowed = {"read", "write"}
    agent_tools = [
        t for t in tools.tools
        if (t.meta or {}).get("capability") in allowed
    ]

Combining Annotations and Capabilities

Annotations and capabilities serve different purposes but work well together. Capabilities answer "what does this tool do?" while annotations answer "how safe is it to call?"

Example: give an agent only safe, read-only tools that are guaranteed idempotent:

def is_safe_reader(tool) -> bool:
    ann = tool.annotations
    meta = tool.meta or {}
    return (
        meta.get("capability") == "read"
        and getattr(ann, "readOnlyHint", None) is True
        and getattr(ann, "idempotentHint", None) is True
    )

safe_tools = [t for t in tools.tools if is_safe_reader(t)]

Use annotations for runtime safety (confirmation dialogs, retry logic) and capabilities for access control (which tools an agent can see).

Error Responses

When a tool call fails, the server returns a result with isError: true and a text message describing what went wrong. The server automatically retries rate limits (429) and server errors (5xx) with exponential backoff before surfacing the error.

Common errors your agent may encounter:

Error Cause Agent action
Authentication failed Invalid or expired token Stop — fix configuration
Resource not found Wrong channel/user/post ID Check ID and retry with correct one
Client error: ... Missing permissions (403), bad request (400) Read the message — usually explains what's wrong
Rate limit exceeded All retries exhausted Back off and retry later
Server error: ... Mattermost server issue (5xx), all retries exhausted Retry after a delay
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{"type": "text", "text": "Resource not found status=404"}],
    "isError": true
  }
}

Check isError in the result — don't assume every response is success data.

End-to-End Example

A complete agent that connects to the server, filters tools by capability, calls a tool, and handles the result. Uses the MCP SDK directly.

import asyncio

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

PROFILES = {
    "reader":  {"read"},
    "writer":  {"read", "write"},
    "manager": {"read", "write", "create"},
    "admin":   {"read", "write", "create", "delete"},
}


async def main():
    # 1. Connect to the server
    server_params = StdioServerParameters(
        command="uvx",
        args=["mcp-server-mattermost"],
        env={
            "MATTERMOST_URL": "https://your-server.com",
            "MATTERMOST_TOKEN": "your-bot-token",
        },
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # 2. List tools and filter by profile
            all_tools = await session.list_tools()
            allowed = PROFILES["writer"]
            tools = [
                t for t in all_tools.tools
                if (t.meta or {}).get("capability") in allowed
            ]
            print(f"Agent has {len(tools)} tools (writer profile)")

            # 3. Call a tool
            result = await session.call_tool(
                "post_message",
                {
                    "channel_id": "your-channel-id",
                    "message": "Hello from my agent!",
                },
            )

            # 4. Handle the result
            if result.isError:
                error_text = result.content[0].text
                print(f"Tool failed: {error_text}")
            else:
                print(f"Message posted: {result.content[0].text}")


asyncio.run(main())