# Connect your agent Source: https://docs.agentcat.com/agent/connect Use the AgentCat MCP server from Cursor, Claude, VS Code, and more. The AgentCat MCP server lets you query your usage analytics and debug issues directly from your coding agent. It runs at: ``` https://mcp.agentcat.com/mcp ``` Authentication uses OAuth — **you never copy an API key**. The first time your client connects, it opens a browser to sign in to your AgentCat account, then caches the token. ## Claude Code ```bash theme={null} claude mcp add --transport http agentcat https://mcp.agentcat.com/mcp ``` Run `/mcp` inside a session to complete the browser sign-in. ## Claude Desktop Open **Settings → Connectors → Add custom connector**, paste `https://mcp.agentcat.com/mcp`, and sign in. The same custom-connector flow works across Claude's desktop apps (Claude Desktop and Cowork) — remote servers are supported directly, no proxy needed. ## Codex ```bash theme={null} codex mcp add agentcat --url https://mcp.agentcat.com/mcp ``` ## Cursor [Add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=agentcat\&config=eyJ1cmwiOiJodHRwczovL21jcC5hZ2VudGNhdC5jb20vbWNwIn0=) — or add manually to `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "agentcat": { "url": "https://mcp.agentcat.com/mcp" } } } ``` ## VS Code Add to `.vscode/mcp.json` (or run `code --add-mcp '{"name":"agentcat","type":"http","url":"https://mcp.agentcat.com/mcp"}'`): ```json theme={null} { "servers": { "agentcat": { "type": "http", "url": "https://mcp.agentcat.com/mcp" } } } ``` ## Windsurf Add to `~/.codeium/windsurf/mcp_config.json`: ```json theme={null} { "mcpServers": { "agentcat": { "serverUrl": "https://mcp.agentcat.com/mcp" } } } ``` ## Other clients Most clients accept the standard config: ```json theme={null} { "mcpServers": { "agentcat": { "type": "http", "url": "https://mcp.agentcat.com/mcp" } } } ``` ## Tools available `list_projects`, `get_project_overview`, `search_sessions`, `get_session`, `search_issues`, `get_issue`, `get_metric_trend`, `get_metric_breakdown`, `list_skills`, `load_skill`. ## Example prompts * "What are my top tools in the last 7 days?" * "Investigate issue PROJ-42." * "Summarize session ses\_abc123 and tell me why it errored." # Deployment Options Source: https://docs.agentcat.com/deployment/index AgentCat offers flexible deployment models for organizations that need maximum control over their data. Most teams use AgentCat's cloud platform out of the box — it's production-ready with row-level security, encryption at rest and in transit, and SOC 2-compliant infrastructure. For organizations with strict data sovereignty, compliance, or air-gapped requirements, we also offer single-tenant and self-hosted options. > **[Cloud](https://agentcat.com/app)** > > Our fully managed multi-tenant platform with row-level security, encryption, and SOC 2-compliant infrastructure. The fastest way to get started. > **[Single Tenant](/deployment/single-tenant)** > > A dedicated AgentCat instance hosted by us with custom DNS and complete infrastructure isolation. > **[Self-Hosted](/deployment/self-hosted)** > > Deploy AgentCat on your own infrastructure with full control over data residency, networking, and configuration. ## Not Sure Which Option Is Right for You? | | Cloud | Single Tenant | Self-Hosted | | - | - | - | - | | **Data location** | AgentCat shared infrastructure | Dedicated AgentCat infrastructure | Your infrastructure | | **Isolation** | Row-level security | Full infrastructure isolation | Full infrastructure isolation | | **Operations** | Fully managed by AgentCat | Fully managed by AgentCat | Managed by your team | | **Custom DNS** | N/A | Custom subdomain included | You control DNS | | **Compliance** | SOC 2, encryption at rest & in transit | Isolation + managed compliance posture | Full control — you own the audit scope | | **Best for** | Most teams | Dedicated infrastructure without ops burden | Maximum control, air-gapped environments | > **[Talk to us](https://chat.agentcat.com)** > > Book a call to discuss which deployment option is the best fit for your organization. # Self-Hosted Source: https://docs.agentcat.com/deployment/self-hosted Deploy AgentCat on your own infrastructure with full control over data residency and security. AgentCat Self-Hosted gives you a complete AgentCat deployment running entirely within your own infrastructure. Delivered as a Docker Compose package, it includes everything you need to capture, process, and analyze MCP agent interactions without any data leaving your environment. ## Infrastructure Requirements ### Minimum Resources * **4 vCPUs** * **16 GB memory** * **400 GB storage** for approximately 30 million events per 3 months, or use a managed serverless database (e.g., Aurora Serverless, Neon) for elastic storage ### Included Dependencies The Docker Compose deployment includes all required services: * **Kafka** — Event queue for reliable message processing * **PostgreSQL** — Primary data store > [!TIP] > > We recommend connecting an externally managed PostgreSQL instance (e.g., AWS RDS, GCP Cloud SQL) for production workloads to simplify backups and high availability. * **Clickhouse** *(optional)* — High-performance analytics engine, recommended for deployments processing 1M+ events per day * **SMTP service** — Your own email service for notifications and alerts * **LLM API key** — Powers AI features like session summarization and intent detection. Built on [LiteLLM](https://docs.litellm.ai/), so any major LLM provider is supported including OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, and more > [!NOTE] > > If you use AWS Bedrock for your LLM provider, external network requirements are minimized since traffic stays within AWS. ## Management CLI Every self-hosted deployment includes a purpose-built CLI for managing your AgentCat instance: * **Health checks** — Run diagnostics across all services and dependencies. Verify database connectivity, Kafka broker status, worker health, and end-to-end event pipeline flow. * **Log access** — Stream or export logs from any running service. Filter by severity, time range, or service name to quickly isolate issues. * **Queue management** — Monitor consumer lag and throughput across Kafka topics. Run targeted commands to rebalance partitions or flush stuck messages when queue pressure builds. * **Version upgrades** — Pull and apply the latest AgentCat images with a single command. The CLI handles service orchestration, migration execution, and rollback if needed. * **Reporting** — Generate reports on system health, event throughput, storage utilization, and service uptime for operational reviews. ## Updates & Maintenance New AgentCat versions are delivered as updated Docker images. The management CLI handles version upgrades, making it straightforward to stay current with the latest features and security patches. ## Network & Security Self-hosted AgentCat is designed to run in locked-down environments: * **No inbound external traffic required** — The deployment does not need to be exposed to the public internet. Your agents and SDKs only need internal network access to the AgentCat API endpoint * **Runs behind your VPN or firewall** — Full compatibility with private network configurations * **Minimal egress** — Outbound traffic is limited to pulling container images and LLM API calls. Using AWS Bedrock further reduces egress by keeping LLM traffic within your VPC ## Data Residency Because you control the infrastructure, you choose where AgentCat runs. Deploy in any region or cloud provider that meets your compliance requirements — including on-premise data centers. ## Backup & Recovery Database backup and recovery follows your existing infrastructure practices. We recommend configuring automated backups for your PostgreSQL instance, especially if using an externally managed database service. ## Forward Deployed Engineer AgentCat offers an optional **Forward Deployed Engineer (FDE)** — a dedicated AgentCat engineer embedded with your team to own the operational side of your self-hosted deployment. Your FDE handles: * **Deployment & configuration** — End-to-end setup tailored to your infrastructure and security requirements * **Ongoing operations** — Proactive monitoring, upgrades, and capacity planning so your team doesn't have to * **Performance tuning** — Optimization for your specific event volumes and query patterns * **Incident response** — Direct escalation path when issues arise An FDE is ideal for organizations that want the control of self-hosting with the confidence of having a dedicated AgentCat expert managing it day-to-day. *** > **[Book a demo](https://chat.agentcat.com)** > > Talk to our team about self-hosted AgentCat for your organization. # Single Tenant Source: https://docs.agentcat.com/deployment/single-tenant A fully managed, dedicated AgentCat instance with complete infrastructure isolation and custom DNS. AgentCat Single Tenant gives you a completely isolated copy of the AgentCat infrastructure, hosted and managed entirely by our team. You get all the benefits of a dedicated deployment — data isolation, custom DNS, and compliance alignment — without any of the operational overhead. ## Fully Managed AgentCat handles everything for your single-tenant deployment: * **Infrastructure management** — Provisioning, scaling, and monitoring * **Updates** — Automatic upgrades to the latest AgentCat version * **Security** — Patching, encryption, and access controls * **Availability** — Uptime monitoring and incident response Your team focuses on building great agent experiences. We handle the rest. ## Custom DNS Every single-tenant deployment includes custom DNS resolution for both the dashboard and event ingestion endpoints: * **Dashboard**: `mycompany.agentcat.com` * **Event ingestion**: `events.mycompany.agentcat.com` White-label URLs on your own domain are also supported, so your team and SDK integrations point to a URL you control. ## Compliance A single-tenant deployment provides the infrastructure isolation that many compliance frameworks require or strongly recommend. Because your data is fully separated — dedicated databases, dedicated compute, no shared resources — the scope of compliance assessments is simplified significantly. ### Data Protection & Privacy * **GDPR** — Clear data isolation and residency boundaries simplify compliance with data protection by design (Art. 25) and security of processing (Art. 32) * **CCPA / CPRA** — Isolated infrastructure makes it straightforward to fulfill deletion and data access requests with no risk of cross-tenant data leakage * **HIPAA** — Dedicated compute and storage satisfy the Security Rule's requirements for access controls, audit controls, and integrity controls for any PHI in agent interactions > [!TIP] > > If your compliance requirements mandate that no data leaves your own infrastructure, consider [self-hosted](/deployment/self-hosted) instead. For most compliance frameworks, single-tenant provides equivalent isolation with zero operational overhead. *** > **[Book a demo](https://chat.agentcat.com)** > > Talk to our team about a single-tenant AgentCat deployment. # Dashboard Source: https://docs.agentcat.com/features/dashboard See real-time usage and analytics for your MCP server. When you have dozens of users and hundreds of tool calls flowing through your MCP server, you need a way to see the big picture. AgentCat's Dashboard gives you real-time KPIs, usage trends, and tool-level analytics so you can understand adoption and performance at a glance. ![dashboard](https://mintcdn.com/mcpcat/1ZBpCsjHOX5lBBUE/images/dashboard.png?fit=max\&auto=format\&n=1ZBpCsjHOX5lBBUE\&q=85\&s=b8c8632b10e33fa24f147bd5b0ea4179) ## KPI Cards The top of the dashboard shows four key metrics, each with a trend comparison to the previous period: | Metric | Description | | - | - | | **Active Users** | Unique identified users with at least one session | | **Active Sessions** | Total sessions with at least one event | | **Avg Session Length** | Mean duration across all active sessions | | **New Users** | Users seen for the first time in the selected period | ## Activity Chart An overlaid area chart showing **users** and **sessions** over time. Use this to spot adoption trends, weekly patterns, and the impact of releases or announcements on usage. ## Users by Client A donut chart showing how your users are distributed across AI clients — Claude Desktop, Cursor, Windsurf, and others. This helps you understand which clients to prioritize when testing and optimizing your tools. ## Most Used Tools A horizontal bar chart ranking your top 7 tools by call volume. Each bar is split into **success** and **error** segments, so you can immediately see which tools are failing most often. ## Tool Latency A line chart showing tool response time percentiles (**p50**, **p95**, **p99**) over time. Use this to catch performance regressions early and understand the latency experience for your typical and worst-case users. ## Agent Goals If your project has enough data for [Agent Goals](/features/goals), the dashboard includes two additional charts: * **Goal Distribution**: A donut chart showing the top 10 user intents across all sessions * **Goal Success Rate**: A stacked bar chart showing how often each goal is accomplished vs failed, with color-coded progress bars ## Recent Sessions A widget showing the 5 most recent sessions with activity. Each entry shows the user, timestamp, and session activity level. Click any session to jump directly into [Session Replay](/features/session-replay). ## Filter Controls Use the controls at the top of the dashboard to narrow your view: | Filter | Options | | - | - | | **Date Range** | Last 7 days, last month, last 3 months | | **Granularity** | Daily, weekly | | **Actor Trait Filters** | Filter by user properties set via the identify callback | # Data Export Source: https://docs.agentcat.com/features/data-export Export your AgentCat analytics data to S3 for custom analysis, data warehousing, or compliance. AgentCat lets you export your analytics data to your own Amazon S3 bucket. Once configured, your data is exported as Parquet files: ready for your data warehouse, BI tools, or compliance archives. Exports include two tables — sessions and events — with user and agent-goal details denormalized into each row. See [Privacy & Security](/privacy-security) for details on how AgentCat protects your data. ## Prerequisites * Data export enabled for your organization * An AWS account where you can create IAM roles * An S3 bucket in your AWS account to receive exports ## Setting up a destination Navigate to **Settings > Destinations > Add Destination** to start the setup wizard. ![Export destinations list](https://mintcdn.com/mcpcat/59XjMsiVpDb9cyYK/images/destination.png?fit=max\&auto=format\&n=59XjMsiVpDb9cyYK\&q=85\&s=33ca65d949ecfe8de77458927ed9dcc2) ### Step 1: Choose destination type Currently S3 is the only supported destination type. Select it and continue. ### Step 2: Configure your S3 bucket | Field | Required | Description | | - | - | - | | **Destination name** | Yes | A friendly name (e.g., "Production Data Lake") | | **Bucket name** | Yes | Your S3 bucket name | | **AWS region** | Yes | The region where your bucket lives | | **AWS account ID** | Yes | Your 12-digit AWS account ID | | **IAM role name** | Yes | The name of the IAM role AgentCat will assume (e.g., `agentcat-s3-export`) | | **Path prefix** | No | Optional prefix for the export path. Exports land at `s3://bucket/{prefix}/agentcat/...` | | **KMS encryption** | No | If your bucket uses SSE-KMS, provide the KMS key ARN | ### Step 3: Set up the IAM role in your AWS account AgentCat uses cross-account IAM role assumption — your data stays in your AWS account and AgentCat never sees your AWS credentials. AgentCat generates the exact IAM policies you need. The wizard provides setup instructions in five formats: * **AWS Console** — step-by-step manual instructions * **AWS CLI** — copyable shell commands * **Terraform** — HCL code you can add to your infrastructure * **Pulumi** — TypeScript code for Pulumi users * **CloudFormation** — one-click "Launch Stack" button that creates everything automatically The generated IAM role includes: * A **trust policy** that allows AgentCat's service role to assume it, with your organization ID as an external ID for security * A **permissions policy** granting `s3:PutObject`, `s3:PutObjectAcl`, and `s3:DeleteObject` on your bucket path (and `kms:GenerateDataKey` / `kms:Decrypt` if KMS is configured) ### Step 4: Verify the connection After creating the IAM role in your AWS account, return to AgentCat and click **Verify Connection**. AgentCat will: 1. Assume your IAM role via STS 2. Write a test object to your bucket 3. Delete the test object If verification fails, AgentCat shows which step failed with a specific error message (e.g., "Role assumption failed", "Write denied", "KMS access denied") to help you troubleshoot. ## Assigning projects to a destination After your destination is verified and active: 1. Navigate to **Settings > Projects** and select a project 2. In the **Exports** section, click **Add Destination** 3. Select from your verified destinations 4. Toggle the export on or off per project A project can be assigned to multiple destinations, and a destination can receive data from multiple projects. ## What gets exported AgentCat exports your data as two Parquet tables: | Table | Contents | | - | - | | **Sessions** | One row per session, with its identified user, agent goal, timing, and client/server details denormalized in | | **Events** | All captured MCP events (tool calls, resource reads, etc.) with session, user, and error details | > [!NOTE] > > Sessions are exported once they finalize (a session finalizes after it has been inactive long enough to be considered complete). Each hourly export picks up newly finalized sessions and their events, and a cursor ensures every session is exported exactly once. **Privacy:** By default, the event `parameters`, `response`, and `error` columns are excluded from exports — those columns are omitted from the Parquet files entirely. This is controlled by a per-destination column denylist, which you can edit to exclude additional columns or to include these three. All other columns are exported as stored, including IP addresses and user identification data, so the [server-side](/privacy-security/server-side-redaction) and [client-side](/privacy-security/client-side-redaction) redaction applied when events are ingested determines what sensitive values an export can contain. ## Export path structure Exports are written as Hive-partitioned datasets: ``` s3://{bucket}/{prefix}/mcpcat/v1/{table}/project_id={project_id}/dt={YYYY-MM-DD}/hour={HH}/{file}.parquet ``` Example: ``` s3://my-data-lake/analytics/mcpcat/v1/sessions/project_id=proj_abc123/dt=2026-04-03/hour=14/part-0.parquet s3://my-data-lake/analytics/mcpcat/v1/events/project_id=proj_abc123/dt=2026-04-03/hour=14/part-0.parquet ``` Each export batch also writes a manifest under `mcpcat/v1/_manifests/` recording the files and row counts it produced, and each table root carries a `_common_metadata` file so the datasets load directly in Spark, DuckDB, and other Parquet readers. ## Managing destinations * **Enable/disable**: Toggle a destination on or off without deleting it * **Edit**: Update the name or S3 configuration (changing config resets verification — you'll need to re-verify) * **Delete**: Permanently remove a destination (also removes all project assignments) * **Re-verify**: Test the connection again anytime from the destination detail page # Agent Goals Source: https://docs.agentcat.com/features/goals Understand what the agent was trying to accomplish in your product. ## What are Agent Goals? Tool call logs show you *what* happened in a session, but not *why*. Agent Goals use AI to classify the goal behind each session, turning raw event data into actionable insight about what your users are trying to do and whether they were successful. Agent goals was built with a full product workflow in mind, helping you answer questions like: * What are the most common use cases for agents using my product? * How often are those use cases successful vs. not? * What are new use cases? ## How Goals Work AgentCat's AI analyzes patterns across your project's sessions to generate a set of goals. To start classifying goals, we need a minimum threshold of sessions and tool calls so our algorithm can begin clustering and classifying common goals: | Requirement | Threshold | | - | - | | **Sessions** | \~200 sessions in the project | | **Tool calls** | \~1,000 tool call events | Once these thresholds are met, AgentCat begins periodically discovering goals based on samples of sessions and classifying all new sessions into a goal once the session has completed. ## Session Goal In each [Session Replay](/features/session-replay), you can find the session goal. The session goal includes information that helps you understand what the goal was for that session and whether it was achieved or not. ![session goal card](https://mintcdn.com/mcpcat/1ZBpCsjHOX5lBBUE/images/session-goal-card.png?fit=max\&auto=format\&n=1ZBpCsjHOX5lBBUE\&q=85\&s=29d032bf8b57f390e5b94b395b4ebc52) | Property | Description | | - | - | | **Name** | A short label for the goal | | **Description** | A longer explanation of the user intent | | **Status** | Whether the goal was **accomplished** or **failed** for a given session | | **Reasoning** | AI-generated explanation of why the goal was classified as accomplished or failed | Goals go through different states depending on whether you're looking at a project or an individual session: * **Learning:** The project doesn't have enough data to generate goals * **Processing:** This individual session is still being analyzed or has not yet completed * **Active:** Goals are available and ready to view ## Goal Success Rate ![goal success rate](https://mintcdn.com/mcpcat/1ZBpCsjHOX5lBBUE/images/goal-success-rate.png?fit=max\&auto=format\&n=1ZBpCsjHOX5lBBUE\&q=85\&s=b65a3b2253718276992f1172a1ba2c6e) On the [Dashboard](/features/dashboard), you can see an overview of the agent goals across your entire project. You can see the distribution of goals across all recent sessions, how often a goal was succeeding or failing, and a graph of goal success rate over time. Use this to identify goals where users frequently fail. These are your highest-impact improvement opportunities. ## Filtering by Goal On the sessions list page, a **Goals** filter becomes available once goals are active for your project. Use it to find all sessions where agents had a specific goal. For example, filter to "Debug authentication flow" to see every session where an agent was trying to debug auth. # Issues Source: https://docs.agentcat.com/features/issues Automatic error grouping, impact tracking, and trend analysis for your MCP server. ## What are Issues? Errors scattered across individual sessions are hard to triage. You might see the same tool failing for dozens of users without realizing it's the same root cause. AgentCat's Issues feature automatically groups related errors, tracks their impact, and shows you trends over time so you can prioritize what to fix. But agents also make lots of AI-specific errors that still show up as a "successful" tool call. Our error grouping also identifies hallucinations, tool schema mismatches, goal misalignment, and other AI errors so you can understand how often those problems are occuring. ## How Issues Are Captured We capture issues by looking over every tool call in a session and building a broader picture of how that tool call fits into the session, what the goal of the session and that tool call was, and where in the session that tool call occured. The SDK automatically captures issues from multiple sources, including: * Exception messages and types * Stack traces (when available) * Request parameters and response data * The session and user context where the error occurred Issues are grouped by error type, message, function name, and agent intent. When multiple sessions encounter the same error, they roll up into a single issue with an event count and user impact metric. ## Issues List The issues list shows all detected issues with key metrics at a glance. ![issues list](https://mintcdn.com/mcpcat/1ZBpCsjHOX5lBBUE/images/issues-list.png?fit=max\&auto=format\&n=1ZBpCsjHOX5lBBUE\&q=85\&s=d6a13bdd56103bbeddac84a2b342ff03) ### Filters | Filter | Description | | - | - | | **Status** | Unresolved, resolved, or ignored | | **Date range** | Filter by when errors occurred | | **Sort** | Sort by events, users, impact, first seen, or last seen | ## Issue Detail Click an issue to see its full breakdown, such as affected users, clients, and recent occurrences. You can also jump into [Session Replay](/features/session-replay) for any occurrence to see the full context. ![issue detail](https://mintcdn.com/mcpcat/1ZBpCsjHOX5lBBUE/images/issue-detail.png?fit=max\&auto=format\&n=1ZBpCsjHOX5lBBUE\&q=85\&s=5ba85c0b553d83e9fd3846660ad7971b) ### Status Management | Action | Effect | | - | - | | **Resolve** | Marks the issue as fixed. New occurrences will reopen it. | | **Ignore** | Hides the issue from the default list view. | | **Unresolve / Unignore** | Returns the issue to unresolved status. | ### Assigning an Issue You can designate an **assignee** to each issue. This ensures that someone from your team is responsible for looking into an issue, identifying a fix, and ensuring it resolves the issue. You can also assign a **priority** level to each issue to accurately prioritize its impact. ### Error History Chart A bar chart showing error frequency over the last 14 days. Toggle between three metrics: * **Events**: Total error occurrences per day * **Users**: Unique affected users per day * **Sessions**: Unique affected sessions per day # Session Replay Source: https://docs.agentcat.com/features/session-replay Step through every event in a session to understand what happened and debug. ## What is Session Replay? When a user reports an issue or you see an error spike, you need the full context, not just an error message. Session Replay lets you step through every event in a session, see what the user was trying to accomplish, and inspect the exact request and response data for each tool call. ## Session List The sessions page shows all recorded sessions with filtering, sorting, and pagination. ![sessions list](https://mintcdn.com/mcpcat/1ZBpCsjHOX5lBBUE/images/sessions-list.png?fit=max\&auto=format\&n=1ZBpCsjHOX5lBBUE\&q=85\&s=fb99322f33b8d4b547823ffc54a060d0) ### Filters | Filter | Description | | - | - | | **User search** | Search by user name or ID | | **Date range** | Filter sessions by time period | | **User type** | Identified or anonymous users | | **Server** | Filter by MCP server | | **Client** | Filter by AI client | | **Tools** | Filter sessions that used specific tools | | **Goals** | Filter by session goal (visible when [Agent Goals](/features/goals) are active) | | **Activity toggle** | Show only sessions with activity | | **Actor traits** | Filter by user properties set via the identify callback | Sessions can be sorted by date, activity count, or duration, with configurable page sizes. ## Replay View The replay view uses a two-panel layout: a **timeline** on the left showing every event in sequence, and a **detail panel** on the right showing the full data for the selected event. ![session replay](https://mintcdn.com/mcpcat/1ZBpCsjHOX5lBBUE/images/session-replay.png?fit=max\&auto=format\&n=1ZBpCsjHOX5lBBUE\&q=85\&s=dc9fb7b615ac854377a21f1f8f514a8c) ### Timeline The timeline lists every event in chronological order, showing the timestamp, event type badge, and a description. Tool call events show the tool name; custom events show the resource name. * Use the **Show/hide list tools** toggle to filter out `tools/list` events that add noise * The timeline supports infinite scroll for sessions with many events ### Event Detail Panel Click any event in the timeline to see its full details: * **Agent Intent**: an explanation from the agent on why it called this tool (see [Tool Call Context](/sdk/tool-call-context)) * **Error Details**: if the tool call errored, the error message and stack trace * **Parameters**: the full request arguments * **Response**: the tool's response data * **Event Metadata**: information about the event * **Identify Data**: information about the user, if available # Team Management Source: https://docs.agentcat.com/features/team-management Invite teammates and manage access to your organization. AgentCat is built for teams. Every organization can have multiple members who share access to the same projects, sessions, and analytics. The **Members** page in **Settings** is where you manage who has access. ## Members Overview Navigate to **Settings > Members** to see everyone in your organization. The members table shows each person's **name**, **email**, and **status** — either **Member** for active users or **Invited** for pending invitations. A dropdown menu on each row provides actions like removing the member or cancelling a pending invite. ## Inviting Team Members ### From Settings Click the **Add member** button on the Members page to open the invite dialog. Enter a teammate's email address and click **Send Invite**. They will receive an email invitation to join your organization, and their entry will appear in the members table with an **Invited** status until they accept. ### During Setup When you first create your organization, the setup flow includes a team invite step that supports batch entry. Type or paste multiple email addresses separated by commas or spaces, then send all invitations at once. You can always invite more members later from the Settings page. ## Accepting an Invite When someone is invited, they see a banner on their dashboard: "You've been invited to an organization." ![invite banner](https://mintcdn.com/mcpcat/MGwu3r7iyFyBFWNA/images/invite-banner.png?fit=max\&auto=format\&n=MGwu3r7iyFyBFWNA\&q=85\&s=c5f465c4a275c34cdaa7b520193e97a3) Clicking **Accept invite** opens a confirmation dialog that explains what will happen to their current organization: | Scenario | What happens | | - | - | | **You are the only member** of your current organization | Your current organization and all its projects are permanently deleted | | **Other members exist** in your current organization | You are removed from the organization, but it continues for remaining members | After accepting, you are switched into the new organization and gain access to all of its projects. ## Removing Members Open the dropdown menu on any member row and select **Remove** (or **Cancel invite** for pending invitations). A confirmation dialog appears before the action is carried out. Removed members immediately lose access to all projects in the organization. You cannot remove yourself from an organization. # How It Works Source: https://docs.agentcat.com/how-it-works The secret sauce behind AgentCat. Here's what's going on under the hood — how the SDK captures data, how sessions work, and the key concepts you'll see throughout the docs. ## Architecture AgentCat's SDK runs inside your MCP server, intercepting MCP protocol messages to capture analytics before passing them through to your tools. Your server works exactly as before. AgentCat just observes. ![architecture](https://mintcdn.com/mcpcat/p1h-WAAQvjrpHpEc/images/architecture.svg?fit=max\&auto=format\&n=p1h-WAAQvjrpHpEc\&q=85\&s=b53f476a436224f5d993e38e2723b3ee) ## Agent Intent Capture The magic of AgentCat lies in understanding *why* agents call your tools, not just *what* they call. When you install the SDK, AgentCat automatically injects a `context` parameter into your tool schemas. When AI assistants (like Claude Code or Cursor) call your tools, they see this additional parameter, they use it to explain their reasoning for making that tool call. ```json theme={null} { "tool": "search_files", "arguments": { "query": "authentication", "context": "Looking for authentication logic to understand how user sessions are validated before adding a new permission check" } } ``` You can [customize or disable](/sdk/tool-call-context) this behavior. This context is extracted as the agent's intent, giving you insights into: * What users are trying to accomplish * How tools fit into larger workflows * Common patterns and use cases * Missing functionality users need ## Request Tracing AgentCat intercepts all MCP protocol messages by wrapping your server's handlers: ```python Python theme={null} import agentcat from mcp.server import FastMCP # Your existing server server = FastMCP(name="my-tools") # One line to add analytics agentcat.track(server, "proj_YOUR_PROJECT_ID") ``` ```typescript TypeScript theme={null} import { track } from 'agentcat'; import { Server } from '@modelcontextprotocol/sdk'; // Your existing server const server = new Server(); // One line to add analytics track(server, "proj_YOUR_PROJECT_ID"); ``` ```go Go theme={null} import agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: import agentcat "go.agentcat.com/sdk/mcpgo" // Your existing server s := mcp.NewServer(&mcp.Implementation{Name: "my-tools", Version: "1.0.0"}, nil) // One line to add analytics agentcat.Track(s, "proj_YOUR_PROJECT_ID", nil) ``` ```php PHP theme={null} use AgentCat\AgentCat; use Mcp\Server; // Your existing server $builder = Server::builder()->setServerInfo('my-tools', '1.0.0'); // One line to add analytics AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); ``` In PHP, AgentCat runs in the request or process of your server, with no threads: it sends the events after the client has its answer wherever the runtime allows it, and [PHP runtimes](/sdk/php/runtimes) names the exceptions. Every request is captured with: * **Event type** (tool call, resource access, prompt execution) * **Timing data** (start time, duration) * **Request parameters** and **response data** * **Error states** and messages * **Client information** (which AI tool made the request) ## How it Works Under the Hood ### 1. Schema Enhancement When AgentCat wraps your server, it modifies tool schemas to include the context parameter: **Before AgentCat:** ```json theme={null} { "name": "search_files", "inputSchema": { "type": "object", "properties": { "query": { "type": "string" } } } } ``` **After AgentCat:** ```json theme={null} { "name": "search_files", "inputSchema": { "type": "object", "properties": { "query": { "type": "string" }, "context": { "type": "string", "description": "Describe why you are calling this tool..." } }, "required": ["query", "context"] } } ``` ### 2. Transparent Interception AgentCat intercepts requests without affecting your tool's functionality: 1. **Client calls tool** with arguments + context 2. **AgentCat extracts** the context parameter 3. **Your tool receives** original arguments (without context) 4. **Tool executes** normally 5. **AgentCat captures** the response 6. **Event is queued** for analytics Your tools never see the context parameter—they work exactly as before. ### 3. Asynchronous Processing Events are processed asynchronously to ensure zero performance impact: * Thread-safe queue holds events (default: 10,000 capacity) * Worker threads send events to AgentCat API * Automatic retries with exponential backoff * Graceful shutdown ensures no data loss ### 4. Privacy & Control AgentCat is designed with [privacy](/privacy-security) in mind: * **[Redaction functions](/privacy-security/client-side-redaction)** let you scrub sensitive data before sending * **All data [encrypted](/privacy-security/certifications)** at rest * **You control** what events are tracked * **No PII required** - user identification is optional ## Sessions Every interaction with your MCP server happens within a **session**. How sessions are created depends on your server's settings: * **Stateful servers**: The SDK generates a session ID when a client connects. All events during that connection share the same session. * **Stateless servers**: Each request is independent, so the SDK sends events without a session ID. AgentCat's backend groups these events into sessions automatically based on the identified user, the client used, and a 30-minute inactivity window. See [Stateless Servers](/sdk/stateless-servers) for setup details. ![session lifecycle](https://mintcdn.com/mcpcat/rAg26YlX8tmGTobY/images/session-lifecycle.svg?fit=max\&auto=format\&n=rAg26YlX8tmGTobY\&q=85\&s=e4b3860f86e5c36c06e1f8ed31100af1) Sessions follow a simple lifecycle: 1. **Initialization**: A new session starts when a client initializes a connection to your MCP server 2. **Activity**: All tool invocations and interactions are associated with the session 3. **Inactivity**: Sessions remain active as long as the client maintains the connection 4. **Termination**: Sessions end when the client disconnects or after a period of inactivity (usually 30 minutes) > [!NOTE] > > For stateless HTTP servers, sessions don't have an explicit initialization or termination. Instead, AgentCat groups events into sessions server-side based on user identity and timing. Learn more in [Stateless Servers](/sdk/stateless-servers). ### Session Construction Sessions are constructed in two phases — first by the SDK on the client, then refined by the server. #### Client-Side (SDK) The SDK intercepts your MCP server's transport layer and automatically captures every interaction without requiring any code changes to your tools. When a client connects, the SDK: 1. **Generates a unique session ID** (stateful) or **signals stateless mode** so the server can group events — and associates all subsequent events accordingly 2. **Captures metadata** about the connection, like client name and version, server name and version, and SDK language and version 3. **Records each interaction** as a structured event with tool calls, resource reads, prompt requests, along with timing data, parameters, and responses 4. **Streams events asynchronously** to AgentCat's ingestion pipeline so there is zero impact on your server's response times The SDK is designed to be resilient. Events are buffered and sent independently of your tool execution, so even if the network is momentarily unavailable, your MCP server continues to operate normally. #### Server-Side Processing Once events arrive, AgentCat's server reconstructs and enriches each session through several processing stages: 1. **Session assembly**: Events are matched to their session using atomic, concurrent-safe operations. If a session doesn't exist yet, it's created on the first event. Metadata fields are progressively backfilled: if the first event is missing client info, later events in the same session can fill in the gaps. For stateless servers, events arrive without a session ID, and AgentCat groups them by matching the identified user and AI client within a 30-minute window. This requires [user identification](/sdk/identifying-users) — anonymous stateless events each get their own isolated session. 2. **Session consolidation**: Some MCP clients (like Claude Desktop) reset session identifiers mid-conversation. AgentCat detects these inconsistencies and automatically merges fragmented sessions back together, so you see one continuous interaction instead of several broken ones. 3. **Counter denormalization**: As events arrive, session-level metrics (total events, tool calls, errors, agent feedback counts) are updated atomically. This means your dashboard queries are fast even across thousands of sessions, without needing to re-aggregate raw events on every page load. 4. **Error grouping**: When an error occurs during a tool call, AgentCat parses the error, computes a fingerprint, and groups it with similar errors across sessions. This surfaces recurring issues automatically rather than burying them in individual session logs. 5. **Goal categorization**: After a session becomes inactive, AgentCat analyzes the sessions contents to classify an agent goal, giving you insight into *what users were trying to accomplish*, not just what tools they called. For more information, see [Agent Goals](/features/goals) This multi-stage pipeline means sessions in AgentCat are more than simple event logs. They're structured, deduplicated, enriched records that give you a complete picture of every user interaction. ## Projects Projects are the top-level organizational unit for your analytics. Each project represents a distinct MCP server or deployment, identified by a unique project ID (like `proj_abc123xyz`) that you use when initializing AgentCat tracking. Organize your projects to match your needs: * **By server type**: Separate projects for different MCP servers (database tools, file system tools, API integrations) * **By environment**: Keep development, staging, and production data isolated * **By customer or team**: Track usage across different customer deployments or internal teams Each project maintains its own session history, usage metrics, error tracking, user identification, and [custom event tracking](/sdk/custom-events). ## What Gets Tracked AgentCat tracks all MCP protocol events: * **Tool Operations**: `tools/list`, `tools/call` * **Resource Operations**: `resources/list`, `resources/read` * **Prompt Operations**: `prompts/list`, `prompts/get` * **Completions**: `completion/complete` * **Server Lifecycle**: `initialize`, `initialized` * **Custom Events**: [Your own analytics events](/sdk/custom-events) ## Next Steps > **[Quickstart](/quickstart)** > > Add analytics to your MCP server in minutes. > **[Dashboard](/features/dashboard)** > > Explore real-time KPIs and usage analytics. > **[Session Replay](/features/session-replay)** > > Step through individual sessions to debug issues. > **[Privacy & Security](/privacy-security)** > > Learn how AgentCat protects your users' data. # Introduction Source: https://docs.agentcat.com/index AgentCat tells you exactly what agents are doing on your product and where they're getting stuck. **AI agents are a new type of user.** They scale like traditional software but act like humans. And existing analytics tools fail to give you the insights and data you need to optimize your products for agents. ![AgentCat in light mode](https://mintcdn.com/mcpcat/MGwu3r7iyFyBFWNA/images/hero-light.svg?fit=max\&auto=format\&n=MGwu3r7iyFyBFWNA\&q=85\&s=644dac6762384f80d6215a44195e1d8b) AgentCat helps you: * **Replay every session:** step through exactly what your agents did and why they made each tool call * **Understand agent goals:** see what agents were trying to accomplish and whether they were successful * **Automatically capture issues**: track how often agents run into errors, hallucinations, and problems * **Forward telemetry:** send enriched data to your existing tools like Datadog, Sentry, and PostHog via OTEL * **Get a high-level overview:** visualize your traffic by client, server, location, model, and more ## What can AgentCat help with? > **[Session Replay](/features/session-replay)** > > Replay agent sessions to debug issues and understand usage patterns > **[Agent Goals](/features/goals)** > > Understand *why* agents call your tools > **[Identify Issues](/features/issues)** > > Find the most common errors and hallucinations agents are making > **[Dashboard Analytics](/features/dashboard)** > > Visualize usage trends, client distributions, and tool-level performance # Datadog Source: https://docs.agentcat.com/integrations/datadog Integrate AgentCat with your Datadog observability stack. ## Overview Integrate AgentCat with Datadog for monitoring and alerting. Events are sent as both logs and metrics for comprehensive observability. ## Prerequisites 1. A Datadog account with an API key 2. Your Datadog site (e.g., `datadoghq.com` for US, `datadoghq.eu` for EU) ## Configuration ```typescript TypeScript theme={null} agentcat.track(server, null, { exporters: { datadog: { type: "datadog", apiKey: process.env.DD_API_KEY, site: "datadoghq.com", service: "my-mcp-server", env: "production" } } }) ``` ```python Python theme={null} agentcat.track(server, None, agentcat.AgentCatOptions( exporters={ "datadog": { "type": "datadog", "api_key": os.getenv("DD_API_KEY"), "site": "datadoghq.com", "service": "my-mcp-server", "env": "production" } } )) ``` ```go Go theme={null} agentcat.Track(s, "", &agentcat.Options{ Exporters: map[string]any{ "datadog": map[string]any{ "type": "datadog", "apiKey": os.Getenv("DD_API_KEY"), "site": "datadoghq.com", "service": "my-mcp-server", "env": "production", }, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Exporters\DatadogExporter; use AgentCat\Options; AgentCat::track($builder, '', new Options( exporters: [ 'datadog' => new DatadogExporter( apiKey: (string) getenv('DD_API_KEY'), site: 'datadoghq.com', service: 'my-mcp-server', env: 'production', ), ], )); ``` In PHP, without `env` the logs and metrics carry no `env` tag. ## Configuration Fields | Field | Type | Required | Description | | - | - | - | - | | `api_key` / `apiKey` | string | Yes | Your Datadog API key | | `site` | string | Yes | Datadog site (e.g., "datadoghq.com", "datadoghq.eu") | | `service` | string | Yes | Service name for grouping | | `env` | string | No | Environment name (default: "production") | ## Field Mapping ### Trace and Span IDs | AgentCat Field | Datadog Field | Format | | - | - | - | | `session_id` | `trace_id` | Numeric - last 64 bits of SHA256 hash | | `event_id` | `span_id` | Numeric - SHA256 hash | ### Logs Each event creates a log entry with the following structure: | Field | Value | Description | | - | - | - | | `message` | `"{event_type} - {resource_name}"` | Combined event type and resource (e.g., "tools/call - get\_weather") | | `service` | Your configured service name | From exporter configuration | | `ddsource` | `"agentcat"` | Fixed source identifier | | `ddtags` | Comma-separated string | Includes `env`, `event_type`, `resource`, and `error` tags | | `timestamp` | Unix timestamp in milliseconds | Event occurrence time | | `status` | `"error"` or `"info"` | Based on `is_error` flag | #### MCP Event Data All AgentCat event fields are preserved in the `mcp` object: | Field | Description | | - | - | | `mcp.session_id` | Session identifier | | `mcp.event_id` | Event identifier | | `mcp.event_type` | Event type (e.g., "tools/call") | | `mcp.resource` | Resource name | | `mcp.duration_ms` | Duration in milliseconds | | `mcp.user_intent` | User's intent (if captured) | | `mcp.actor_id` | User identifier (if identified) | | `mcp.actor_name` | User name (if identified) | | `mcp.client_name` | MCP client name | | `mcp.client_version` | MCP client version | | `mcp.server_name` | Server name | | `mcp.server_version` | Server version | | `mcp.is_error` | Error flag (boolean) | | `mcp.error` | Error details (if applicable) | ### Metrics | Metric Name | Type | Description | | - | - | - | | `mcp.events.count` | count | Total number of events | | `mcp.event.duration` | gauge | Event duration in milliseconds | | `mcp.errors.count` | count | Number of error events | All metrics include the following tags for filtering and grouping: | Tag | Example Value | Source | | - | - | - | | `service` | `"my-mcp-server"` | Exporter configuration | | `env` | `"production"` | Exporter configuration | | `event_type` | `"tools.call"` | Event type (slashes replaced with dots) | | `resource` | `"get_weather"` | Resource name | # Third-Party Integrations Source: https://docs.agentcat.com/integrations/index Connect AgentCat with your existing observability platforms. ## Overview AgentCat can integrate with your existing observability platforms, allowing you to monitor your MCP servers using familiar tools. You can use these integrations with or without an AgentCat account. When you configure exporters, the `project_id` parameter becomes optional: ```typescript TypeScript theme={null} // Forward to OTLP without an AgentCat account agentcat.track(server, null, { exporters: { otlp: { type: "otlp", endpoint: "http://localhost:4318/v1/traces" } } }) ``` ```python Python theme={null} # Forward to OTLP without an AgentCat account agentcat.track(server, None, agentcat.AgentCatOptions( exporters={ "otlp": { "type": "otlp", "endpoint": "http://localhost:4318/v1/traces" } } )) ``` ```go Go theme={null} import agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: import agentcat "go.agentcat.com/sdk/mcpgo" // Forward to OTLP without an AgentCat account agentcat.Track(s, "", &agentcat.Options{ Exporters: map[string]any{ "otlp": map[string]any{ "type": "otlp", "endpoint": "http://localhost:4318/v1/traces", }, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Exporters\OtlpExporter; use AgentCat\Options; // Forward to OTLP without an AgentCat account AgentCat::track($builder, '', new Options( exporters: [ 'otlp' => new OtlpExporter(endpoint: 'http://localhost:4318/v1/traces'), ], )); ``` In PHP, pass `''` as the project ID for telemetry only (`null` reads `AGENTCAT_PROJECT_ID`), and each exporter as an object of `AgentCat\Exporters`: `OtlpExporter`, `DatadogExporter`, `SentryExporter` or `PostHogExporter`, whose named arguments are the fields of its page. ## Supported Platforms AgentCat currently supports integration with the following observability platforms: * **[OpenTelemetry (OTLP)](/integrations/open-telemetry)** - Connect to any OTLP-compatible backend (Jaeger, Tempo, New Relic, etc.) * **[Datadog](/integrations/datadog)** - Send logs and metrics to Datadog * **[Sentry](/integrations/sentry)** - Track errors and logs in Sentry * **[PostHog](/integrations/posthog)** - Send product analytics events to PostHog ## Using Multiple Integrations You can configure multiple integrations to send events to different platforms simultaneously: ```typescript TypeScript theme={null} agentcat.track(server, "proj_YOUR_ID", { exporters: { otlp: { type: "otlp", endpoint: "http://localhost:4318/v1/traces" }, datadog: { type: "datadog", apiKey: process.env.DD_API_KEY, site: "datadoghq.com", service: "my-mcp-server" }, sentry: { type: "sentry", dsn: process.env.SENTRY_DSN, environment: "production" }, posthog: { type: "posthog", apiKey: process.env.POSTHOG_API_KEY, host: "https://us.i.posthog.com" } } }) ``` ```python Python theme={null} agentcat.track(server, "proj_YOUR_ID", agentcat.AgentCatOptions( exporters={ "otlp": { "type": "otlp", "endpoint": "http://localhost:4318/v1/traces" }, "datadog": { "type": "datadog", "api_key": os.getenv("DD_API_KEY"), "site": "datadoghq.com", "service": "my-mcp-server" }, "sentry": { "type": "sentry", "dsn": os.getenv("SENTRY_DSN"), "environment": "production" }, "posthog": { "type": "posthog", "api_key": os.getenv("POSTHOG_API_KEY"), "host": "https://us.i.posthog.com" } } )) ``` ```go Go theme={null} agentcat.Track(s, "proj_YOUR_ID", &agentcat.Options{ Exporters: map[string]any{ "otlp": map[string]any{ "type": "otlp", "endpoint": "http://localhost:4318/v1/traces", }, "datadog": map[string]any{ "type": "datadog", "apiKey": os.Getenv("DD_API_KEY"), "site": "datadoghq.com", "service": "my-mcp-server", }, "sentry": map[string]any{ "type": "sentry", "dsn": os.Getenv("SENTRY_DSN"), "environment": "production", }, "posthog": map[string]any{ "type": "posthog", "apiKey": os.Getenv("POSTHOG_API_KEY"), "host": "https://us.i.posthog.com", }, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Exporters\DatadogExporter; use AgentCat\Exporters\OtlpExporter; use AgentCat\Exporters\PostHogExporter; use AgentCat\Exporters\SentryExporter; use AgentCat\Options; AgentCat::track($builder, 'proj_YOUR_ID', new Options( exporters: [ 'otlp' => new OtlpExporter(endpoint: 'http://localhost:4318/v1/traces'), 'datadog' => new DatadogExporter( apiKey: (string) getenv('DD_API_KEY'), site: 'datadoghq.com', service: 'my-mcp-server', ), 'sentry' => new SentryExporter( dsn: (string) getenv('SENTRY_DSN'), environment: 'production', ), 'posthog' => new PostHogExporter( apiKey: (string) getenv('POSTHOG_API_KEY'), host: 'https://us.i.posthog.com', ), ], )); ``` In PHP, the first `track()` of a process that passes exporters sets them up for every server it tracks; the [Laravel](/sdk/php/laravel) and [Symfony](/sdk/php/symfony) bridges take them as arrays in their configuration. ## Performance Platform integrations are non-blocking and asynchronous. Integration failures are logged but won't interrupt your MCP server's operation or affect performance. # OpenTelemetry Source: https://docs.agentcat.com/integrations/open-telemetry Connect AgentCat with OpenTelemetry-compatible platforms. ## Overview Integrate AgentCat with any OpenTelemetry Protocol (OTLP) compatible platform like Jaeger, Grafana Tempo, New Relic, or AWS X-Ray. Learn more about OpenTelemetry at [opentelemetry.io](https://opentelemetry.io). ## Configuration ```typescript TypeScript theme={null} agentcat.track(server, null, { exporters: { otlp: { type: "otlp", endpoint: "http://localhost:4318/v1/traces", protocol: "http/protobuf", headers: { "api-key": "your-api-key" } } } }) ``` ```python Python theme={null} agentcat.track(server, None, agentcat.AgentCatOptions( exporters={ "otlp": { "type": "otlp", "endpoint": "http://localhost:4318/v1/traces", "protocol": "http/protobuf", "headers": { "api-key": "your-api-key" } } } )) ``` ```go Go theme={null} agentcat.Track(s, "", &agentcat.Options{ Exporters: map[string]any{ "otlp": map[string]any{ "type": "otlp", "endpoint": "http://localhost:4318/v1/traces", "protocol": "http/protobuf", "headers": map[string]any{ "api-key": "your-api-key", }, }, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Exporters\OtlpExporter; use AgentCat\Options; AgentCat::track($builder, '', new Options( exporters: [ 'otlp' => new OtlpExporter( endpoint: 'http://localhost:4318/v1/traces', headers: ['api-key' => 'your-api-key'], ), ], )); ``` The PHP `OtlpExporter` has no `protocol` argument: it sends OTLP/HTTP with a JSON body, and adds `/v1/traces` to an `endpoint` that does not end with it. ## Configuration Fields | Field | Type | Required | Description | | - | - | - | - | | `endpoint` | string | Yes | OTLP endpoint URL | | `protocol` | string | No | Protocol format: `"http/protobuf"` or `"grpc"` (default: `"http/protobuf"`) | | `headers` | object | No | Additional headers to send with requests | ## Field Mapping AgentCat events are mapped to OTLP spans with the following structure: ### Trace and Span IDs | AgentCat Field | OTLP Field | Format | | - | - | - | | `session_id` | `traceId` | 32-character hex (SHA256 hash, one per session) | | `id` | `spanId` | 16-character hex (SHA256 hash, unique per event) | ### Resource Attributes | AgentCat Internal Field | OTLP Attribute | Description | | - | - | - | | `server_name` | `service.name` | MCP server name | | `server_version` | `service.version` | MCP server version | | `sdk_language` | `telemetry.sdk.name` | `"agentcat-python"` (Python SDK only; the TypeScript SDK reports the SDK via the OTLP scope `name: "agentcat"` instead) | | `agentcat_version` | `telemetry.sdk.version` | AgentCat SDK version (Python SDK only) | ### Span Attributes | AgentCat Field | OTLP Attribute | Description | | - | - | - | | `event_type` | `mcp.event_type` | Event type (e.g., "tools/call") | | `session_id` | `mcp.session_id` | Session identifier | | `project_id` | `mcp.project_id` | AgentCat project ID (if provided) | | `resource_name` | `mcp.resource_name` | Tool/prompt/resource name | | `user_intent` | `mcp.user_intent` | User's intent (if captured) | | `identify_actor_given_id` | `mcp.actor_id` | User identifier (if identified) | | `identify_actor_name` | `mcp.actor_name` | User name (if identified) | | `client_name` | `mcp.client_name` | MCP client name | | `client_version` | `mcp.client_version` | MCP client version | Attributes with empty or falsy string values are automatically filtered out and will not appear in the exported span. ### Span Properties | Property | Value | Description | | - | - | - | | `name` | Event type or `"mcp.event"` | Span display name | | `kind` | `2` (`SPAN_KIND_SERVER`) | Indicates server-side span | | `startTimeUnixNano` | Event timestamp | In nanoseconds | | `endTimeUnixNano` | Start + duration | In nanoseconds (if duration available) | | `status.code` | `1` (OK) or `2` (ERROR) | Based on `is_error` flag | Timestamp values are serialized as strings, not integers. # PostHog Source: https://docs.agentcat.com/integrations/posthog Send MCP analytics events to PostHog as product analytics. ## Overview Integrate AgentCat with PostHog for product analytics. Events are sent as `capture` events via the PostHog `/batch` API, giving you full visibility into MCP tool usage within your existing PostHog dashboards. ## Prerequisites 1. A PostHog project with an API key (starts with `phc_...`) 2. Your PostHog host URL (default: `https://us.i.posthog.com`) ## Configuration ```typescript TypeScript theme={null} agentcat.track(server, null, { exporters: { posthog: { type: "posthog", apiKey: process.env.POSTHOG_API_KEY, host: "https://us.i.posthog.com" } } }) ``` ```python Python theme={null} agentcat.track(server, None, agentcat.AgentCatOptions( exporters={ "posthog": { "type": "posthog", "api_key": os.getenv("POSTHOG_API_KEY"), "host": "https://us.i.posthog.com" } } )) ``` ```go Go theme={null} agentcat.Track(s, "", &agentcat.Options{ Exporters: map[string]any{ "posthog": map[string]any{ "type": "posthog", "apiKey": os.Getenv("POSTHOG_API_KEY"), "host": "https://us.i.posthog.com", }, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Exporters\PostHogExporter; use AgentCat\Options; AgentCat::track($builder, '', new Options( exporters: [ 'posthog' => new PostHogExporter( apiKey: (string) getenv('POSTHOG_API_KEY'), host: 'https://us.i.posthog.com', ), ], )); ``` ## Configuration Fields | Field | Type | Required | Description | | - | - | - | - | | `api_key` / `apiKey` | string | Yes | Your PostHog project API key (starts with `phc_...`) | | `host` | string | No | PostHog host URL (default: `https://us.i.posthog.com`). Use `https://eu.i.posthog.com` for EU, or your own URL for self-hosted. | ## Field Mapping ### Event Names AgentCat events are sent as PostHog `capture` events. Standard MCP event types are mapped to PostHog-friendly names. Any unmapped event types fall back to replacing colons and slashes with underscores. | MCP Event Type | PostHog Event Name | | - | - | | `mcp:tools/call` | `mcp_tool_call` | | `mcp:tools/list` | `mcp_tools_list` | | `mcp:initialize` | `mcp_initialize` | | `mcp:resources/read` | `mcp_resource_read` | | `mcp:resources/list` | `mcp_resources_list` | | `mcp:prompts/get` | `mcp_prompt_get` | | `mcp:prompts/list` | `mcp_prompts_list` | ### Distinct ID PostHog's `distinct_id` is set using the following priority: 1. **Identified user ID** — if the user was identified via the `identify` callback 2. **Session ID** — falls back to the AgentCat session ID 3. **`"anonymous"`** — if neither is available ### Event Properties Each event includes the following properties: | Property | Description | | - | - | | `$session_id` | AgentCat session ID | | `resource_name` | Resource or tool name | | `tool_name` | Tool name (only for tool call events) | | `duration_ms` | Event duration in milliseconds | | `server_name` | MCP server name | | `server_version` | MCP server version | | `client_name` | AI client name (e.g., "Claude Desktop") | | `client_version` | AI client version | | `project_id` | AgentCat project ID | | `user_intent` | Agent's stated intent for the tool call | | `is_error` | Whether the event was an error | | `parameters` | Request parameters | | `response` | Tool response data | ### Person Properties When a user is identified, PostHog person properties are set via `$set`: * `name` — the identified user's name * Any additional fields from `identifyActorData` are merged in ### Error Events When `is_error` is `true`, AgentCat sends **two** events to PostHog: 1. **Regular capture event** — the standard event with all properties above 2. **`$exception` event** — a native PostHog exception event with: | Property | Description | | - | - | | `$exception_message` | Error message | | `$exception_type` | Error type or class | | `$exception_stacktrace` | Stack trace (when available) | | `$exception_source` | `"backend"` — marks this as a server error | This ensures errors appear in PostHog's native error tracking and can trigger alerts. # Sentry Source: https://docs.agentcat.com/integrations/sentry Connect AgentCat with Sentry for error tracking and monitoring. ## Overview Integrate AgentCat with Sentry for error tracking and performance monitoring. Events are sent as logs, and optionally as transactions for performance analysis. ## Prerequisites 1. A Sentry project with a DSN (Data Source Name) 2. DSN can be found in your Sentry project settings under "Client Keys (DSN)" A DSN looks like `https://a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6@o123456.ingest.us.sentry.io/789`. The examples below read it from a `SENTRY_DSN` environment variable rather than hardcoding it. ## Configuration ```typescript TypeScript theme={null} agentcat.track(server, null, { exporters: { sentry: { type: "sentry", dsn: process.env.SENTRY_DSN, environment: "production", release: "1.0.0", enableTracing: true } } }) ``` ```python Python theme={null} agentcat.track(server, None, agentcat.AgentCatOptions( exporters={ "sentry": { "type": "sentry", "dsn": os.getenv("SENTRY_DSN"), "environment": "production", "release": "1.0.0", "enable_tracing": True } } )) ``` ```go Go theme={null} agentcat.Track(s, "", &agentcat.Options{ Exporters: map[string]any{ "sentry": map[string]any{ "type": "sentry", "dsn": os.Getenv("SENTRY_DSN"), "environment": "production", "release": "1.0.0", "enableTracing": true, }, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Exporters\SentryExporter; use AgentCat\Options; AgentCat::track($builder, '', new Options( exporters: [ 'sentry' => new SentryExporter( dsn: (string) getenv('SENTRY_DSN'), environment: 'production', release: '1.0.0', enableTracing: true, ), ], )); ``` In PHP, without `environment` the events carry no `environment` tag, and an invalid DSN logs one line and AgentCat skips that exporter. ## Configuration Fields | Field | Type | Required | Description | | - | - | - | - | | `dsn` | string | Yes | Sentry Data Source Name from project settings | | `environment` | string | No | Environment name (default: "production") | | `release` | string | No | Release version for tracking deployments | | `enableTracing` / `enable_tracing` | boolean | No | Enable performance monitoring. When true, sends transaction events to Sentry's Performance tab in addition to logs, allowing you to track duration and performance metrics. | ## Field Mapping ### Trace and Event IDs | AgentCat Field | Sentry Field | Format | | - | - | - | | `session_id` | `trace_id` | SHA256 hash (one trace per session) | | `event_id` | `event_id` | Double SHA256 hash for uniqueness | ### Log Events All events are sent as logs with the following structure: | Field | Value | Description | | - | - | - | | `timestamp` | Event timestamp | Unix timestamp | | `level` | `"error"` or `"info"` | Based on `is_error` flag | | `body` | `"MCP {event_type}: {resource_name}"` | Formatted message | ### Transaction Events > [!NOTE] > > Transaction events are only sent when `enableTracing` is set to `true` in the > configuration. | Field | Value | Description | | - | - | - | | `transaction` | `"{event_type} - {resource_name}"` | Transaction name | | `start_timestamp` | Timestamp - duration | Calculated start time | | `end_timestamp` | Event timestamp | When the event completed | | `op` | Event type | Operation type (e.g., "tools/call") | | `status` | `"internal_error"` or `"ok"` | Based on `is_error` flag | ### Tags Events include the following tags for filtering and grouping: | AgentCat Field | Tag Name | Condition | | - | - | - | | Environment config | `environment` | Always included | | Release config | `release` | If configured | | `event_type` | `event_type` | Always included | | `resource_name` | `resource` | If available | | `server_name` | `server_name` | If available | | `client_name` | `client_name` | If available | | `identify_actor_given_id` | `actor_id` | If user identified | ### Extra Data Additional context fields stored with events: | AgentCat Field | Extra Field | Description | | - | - | - | | `session_id` | `session_id` | Session identifier | | `project_id` | `project_id` | AgentCat project ID | | `user_intent` | `user_intent` | User's intent | | `duration` | `duration_ms` | Duration in milliseconds | | `identify_actor_name` | `actor_name` | User name | | `identify_actor_data` | `actor_data` | User metadata | | `client_version` | `client_version` | MCP client version | | `server_version` | `server_version` | MCP server version | ### Error Events When `is_error` is `true`, Sentry receives: 1. A log event (always sent) 2. A transaction event (if `enableTracing` is true) 3. A separate error event for the Issues tab This ensures proper error tracking, alerting, and issue management in Sentry. # Certifications Source: https://docs.agentcat.com/privacy-security/certifications AgentCat compliance certifications and infrastructure security measures. ## SOC 2 Compliant We undergo annual SOC 2 Type II audits, demonstrating our commitment to: * **Security**: Protecting data against unauthorized access * **Availability**: Ensuring systems are operational and accessible * **Processing Integrity**: Ensuring data processing is complete, valid, and authorized * **Confidentiality**: Protecting confidential information * **Privacy**: Collecting, using, and disclosing personal information in accordance with our privacy policy ## Infrastructure Security * **Encryption**: All data encrypted in transit (TLS 1.3) and at rest (AES-256) * **Access Control**: Role-based access control with principle of least privilege * **Infrastructure Security**: Hosted on SOC 2 compliant cloud providers * **Regular Audits**: Quarterly security assessments and annual penetration testing * **Data Retention**: Data is retained while your project exists; deleting a project starts a permanent deletion, with a 30-day export window before the data is irreversibly removed # Client-Side Redaction Source: https://docs.agentcat.com/privacy-security/client-side-redaction Sanitize sensitive data before it leaves your environment with custom redaction functions. AgentCat provides powerful client-side redaction capabilities, allowing you to sanitize sensitive information before it ever leaves your environment. This ensures that sensitive data never reaches our servers. ## How it works You can provide a custom redaction function when initializing AgentCat tracking: ```typescript Typescript theme={null} import * as agentcat from "agentcat"; agentcat.track(mcpServer, "proj_YOUR_PROJECT_ID", { redactSensitiveInformation: async (text) => { let result = text; result = result.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, ""); result = result.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, ""); return result; }, }); ``` ```python Python theme={null} import agentcat import re def redact_sensitive_data(text): result = text result = re.sub(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '', result) result = re.sub(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b', '', result) return result agentcat.track(server, "proj_YOUR_PROJECT_ID", agentcat.AgentCatOptions( redact_sensitive_information=redact_sensitive_data, )) ``` ```go Go theme={null} import agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: import agentcat "go.agentcat.com/sdk/mcpgo" agentcat.Track(s, "proj_YOUR_PROJECT_ID", &agentcat.Options{ RedactSensitiveInformation: func(text string) string { result := text result = regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`).ReplaceAllString(result, "") result = regexp.MustCompile(`\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`).ReplaceAllString(result, "") return result }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; AgentCat::track($builder, 'proj_YOUR_PROJECT_ID', new Options( redactSensitiveInformation: static function (string $text): string { $result = preg_replace('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', '', $text) ?? $text; return preg_replace('/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/', '', $result) ?? $result; }, )); ``` In PHP, the hook takes and returns one string, and runs after the client has its answer; in Laravel and Symfony, `redact_sensitive_information` names an invokable class or service instead. ## Key Features * **Recursive Application**: Redaction is applied to all string values in event data, including nested objects and arrays * **Protected Fields**: Essential analytics fields (like `sessionId`, `projectId`, `eventType`) are preserved to maintain functionality * **Type Preservation**: Non-string values (numbers, booleans, dates) are preserved without modification * **Error Handling**: If redaction fails, the entire event is skipped to prevent accidental data leakage # Privacy & Security Source: https://docs.agentcat.com/privacy-security/index How AgentCat protects your users' data with multiple layers of privacy and security. AgentCat is built with privacy at every layer, from client-side redaction before data leaves your server, to automatic PII detection on ours. AgentCat is co-founded by Naseem Al-Naji, creator of [Opal](https://opal.dev), bringing years of experience in building privacy-first developer tools that handle sensitive data with the utmost care. > **[Client-Side Redaction](/privacy-security/client-side-redaction)** > > Sanitize sensitive data before it leaves your environment with custom redaction functions. > **[Server-Side Redaction](/privacy-security/server-side-redaction)** > > Automatic pattern-based redaction of sensitive values in event payloads. > **[Certifications](/privacy-security/certifications)** > > SOC 2 Type II compliance, encryption, and infrastructure security. ## Disabling Telemetry While AgentCat provides valuable analytics, we understand that some users may want to disable telemetry entirely. You can implement this in your MCP server by conditionally calling the track function. ### Disabling Tracking Entirely Add environment variable support to your MCP server so users can opt out: ```typescript Typescript theme={null} import * as agentcat from "agentcat"; // Check for telemetry opt-out const telemetryEnabled = process.env.DISABLE_USER_ANALYTICS !== 'true'; // Only initialize tracking if telemetry is enabled if (telemetryEnabled) { agentcat.track(mcpServer, "proj_YOUR_PROJECT_ID"); } ``` ```python Python theme={null} import os import agentcat # Check for telemetry opt-out telemetry_enabled = os.environ.get("DISABLE_USER_ANALYTICS") != "true" # Only initialize tracking if telemetry is enabled if telemetry_enabled: agentcat.track(server, "proj_YOUR_PROJECT_ID") ``` ```go Go theme={null} import agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: import agentcat "go.agentcat.com/sdk/mcpgo" // Check for telemetry opt-out if os.Getenv("DISABLE_USER_ANALYTICS") != "true" { agentcat.Track(s, "proj_YOUR_PROJECT_ID", nil) } ``` ```php PHP theme={null} use AgentCat\AgentCat; // Check for telemetry opt-out $telemetryEnabled = getenv('DISABLE_USER_ANALYTICS') !== 'true'; // Only initialize tracking if telemetry is enabled if ($telemetryEnabled) { AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); } ``` In PHP, `AGENTCAT_ENABLED=false` also turns AgentCat off everywhere, in Laravel and Symfony applications too, with no code. ### Disabling Auto-Capture By default, AgentCat automatically captures all MCP protocol events (tool calls, initialization, tool listings). If you only need [custom events](/sdk/custom-events) without automatic event capture, set `enableTracing` to `false`: ```typescript Typescript theme={null} import * as agentcat from "agentcat"; agentcat.track(mcpServer, "proj_YOUR_PROJECT_ID", { enableTracing: false }); ``` ```python Python theme={null} import agentcat agentcat.track(server, "proj_YOUR_PROJECT_ID", agentcat.AgentCatOptions(enable_tracing=False)) ``` ```go Go theme={null} agentcat.Track(s, "proj_YOUR_PROJECT_ID", &agentcat.Options{ DisableTracing: true, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; AgentCat::track($builder, 'proj_YOUR_PROJECT_ID', new Options( enableTracing: false, )); ``` In PHP, a custom event that you publish with the tracked server is dropped while `enableTracing` is false; publish it with a session ID string to send it. When tracing is disabled: * **Tool call events** are not recorded * **User identification** still works normally * **Custom events** via `publishCustomEvent` are still sent * **Exporters** (OpenTelemetry, Datadog, Sentry) still receive custom and identify events ### Anonymizing User Sessions For users who want analytics without user identification, pass an `identify` function that returns `null`: ```typescript Typescript theme={null} import * as agentcat from "agentcat"; agentcat.track(mcpServer, "proj_YOUR_PROJECT_ID", { identify: async () => null }); ``` ```python Python theme={null} import agentcat agentcat.track(server, "proj_YOUR_PROJECT_ID", agentcat.AgentCatOptions(identify=lambda req, ctx: None)) ``` ```go Go theme={null} agentcat.Track(s, "proj_YOUR_PROJECT_ID", &agentcat.Options{ Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { return nil }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; AgentCat::track($builder, 'proj_YOUR_PROJECT_ID', new Options( identify: static fn(ToolCall $call): ?Identity => null, )); ``` ### Implementation Best Practices 1. **Document Environment Variables**: Clearly document telemetry options in your MCP server's README 2. **Default to Privacy**: Consider making telemetry opt-in rather than opt-out 3. **Respect User Choice**: Always check environment variables before initializing tracking 4. **Provide Granular Control**: Allow users to disable specific types of tracking Example implementation with multiple privacy options: ```typescript Typescript theme={null} import * as agentcat from "agentcat"; const config = { telemetry: process.env.DISABLE_USER_ANALYTICS !== 'true', anonymize: process.env.ANONYMIZE_SESSIONS === 'true' }; // Initialize AgentCat based on user preferences if (config.telemetry) { agentcat.track(mcpServer, "proj_YOUR_PROJECT_ID", { identify: config.anonymize ? async () => null : undefined }); } ``` ```python Python theme={null} import os import agentcat telemetry_enabled = os.environ.get("DISABLE_USER_ANALYTICS") != "true" anonymize = os.environ.get("ANONYMIZE_SESSIONS") == "true" # Initialize AgentCat based on user preferences if telemetry_enabled: agentcat.track(server, "proj_YOUR_PROJECT_ID", agentcat.AgentCatOptions( identify=(lambda req, ctx: None) if anonymize else None )) ``` ```go Go theme={null} telemetryEnabled := os.Getenv("DISABLE_USER_ANALYTICS") != "true" anonymize := os.Getenv("ANONYMIZE_SESSIONS") == "true" // Initialize AgentCat based on user preferences if telemetryEnabled { opts := &agentcat.Options{} if anonymize { opts.Identify = func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { return nil } } agentcat.Track(s, "proj_YOUR_PROJECT_ID", opts) } ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; $telemetryEnabled = getenv('DISABLE_USER_ANALYTICS') !== 'true'; $anonymize = getenv('ANONYMIZE_SESSIONS') === 'true'; // Initialize AgentCat based on user preferences if ($telemetryEnabled) { AgentCat::track($builder, 'proj_YOUR_PROJECT_ID', new Options( identify: $anonymize ? static fn(ToolCall $call): ?Identity => null : null, )); } ``` ## What the PHP SDK sends The PHP SDK sends the same events as the other SDKs. Two things to know, which the [PHP differences and limits](/sdk/php/limits) page covers in full: * **Request headers.** The event of a call over HTTP carries every request header of that call, verbatim, `Authorization` and `Cookie` included. Remove the ones you do not want in a `redactEvent` hook. * **Diagnostics.** To find broken installs, the SDK sends AgentCat every line it logs, including the line it logs for each event it sends, with the session ID and the actor ID. Turn diagnostics off with `new Options(disableDiagnostics: true)` or `AGENTCAT_DISABLE_DIAGNOSTICS=true`. # Server-Side Redaction Source: https://docs.agentcat.com/privacy-security/server-side-redaction Automatic pattern-based redaction of sensitive values in event payloads before storage. In addition to [client-side redaction](/privacy-security/client-side-redaction), AgentCat redacts sensitive values on our servers before events are stored, catching data that client-side filters may have missed. ## What gets redacted When redaction is enabled, the user intent, parameters, response, and error payloads of every incoming event are scanned, and matched values are replaced with placeholders: * **Email addresses** → `` * **IP addresses** (IPv4 and IPv6) → `` * **URLs and hostnames** → `` * **Credentials** — Bearer, Basic, Digest, Token, and API-key authorization values * **Unique identifiers** — UUIDs and trace IDs * Additional sensitive patterns, such as dates and long numeric identifiers ## How it works 1. **Pattern-based detection**: payload fields are scanned recursively — including nested objects and lists — against a curated set of patterns 2. **Applied before storage**: redaction happens during event processing, so matched values are replaced before the event is written 3. **Best effort**: pattern matching reduces the chance that sensitive data is stored, but it cannot guarantee catching everything. Keep sensitive data out of your events at the source where possible — see [client-side redaction](/privacy-security/client-side-redaction) ## Configuration Server-side redaction is **enabled by default** for all projects. You can toggle this per project in **Settings > Projects > Edit > "Redact sensitive strings"**. > [!NOTE] > > Toggling the setting only affects new events. Previously stored events are not modified. # Quickstart Source: https://docs.agentcat.com/quickstart Install the AgentCat SDK in just 1 minute. ## Installation **1. Install the AgentCat Agent** Run the following command to install the open source [AgentCat SDK](https://github.com/agentcathq): ```bash npm theme={null} npm i agentcat ``` ```bash yarn theme={null} yarn add agentcat ``` ```bash pnpm theme={null} pnpm add agentcat ``` ```bash pip theme={null} pip install agentcat ``` ```bash poetry theme={null} poetry add agentcat ``` ```bash uv theme={null} uv add agentcat ``` ```bash go theme={null} go get go.agentcat.com/sdk/officialsdk@latest ``` ```bash composer theme={null} composer require agentcat/sdk ``` **2. Track your MCP server** ```typescript Typescript theme={null} import * as agentcat from "agentcat"; /* Example MCP server const mcpServer = new Server({ name: "my-mcp-server", version: "1.0.0" }); mcpServer.tool("my-tool", ...) */ // Call .track() AFTER tool call registering agentcat.track(mcpServer, "proj_YOUR_PROJECT_ID") ``` ```python Python theme={null} import agentcat """ Example MCP server (community FastMCP also works: from fastmcp import FastMCP) server = FastMCP( name="my-mcp-server", version="1.0.0" ) @server.tool() async def my_tool(params): pass """ # Call .track() AFTER tool call registering agentcat.track(server, "proj_YOUR_PROJECT_ID") ``` ```go Go theme={null} import agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: import agentcat "go.agentcat.com/sdk/mcpgo" /* Example MCP server (official Go MCP SDK) s := mcp.NewServer(&mcp.Implementation{ Name: "my-mcp-server", Version: "1.0.0", }, nil) mcp.AddTool(s, ...) */ // Call Track() AFTER registering tools agentcat.Track(s, "proj_YOUR_PROJECT_ID", nil) ``` ```php PHP theme={null} use AgentCat\AgentCat; /* Example MCP server (mcp/sdk) $builder = Mcp\Server::builder() ->setServerInfo('my-mcp-server', '1.0.0') ->addTool(...); */ // Call AgentCat::track() on the Builder BEFORE build() AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); ``` In PHP, track the `Mcp\Server\Builder` before `build()`; a Laravel or Symfony application needs no `track()` call (see [Laravel](/sdk/php/laravel) and [Symfony](/sdk/php/symfony)). You can grab your project ID from [AgentCat's dashboard](https://agentcat.com/app/settings/projects/). ## Using without an AgentCat account You can forward events directly to your observability platform: ```typescript TypeScript theme={null} agentcat.track(server, null, { exporters: { otlp: { type: "otlp", endpoint: "http://localhost:4318/v1/traces" } } }) ``` ```python Python theme={null} agentcat.track(server, None, agentcat.AgentCatOptions( exporters={ "otlp": { "type": "otlp", "endpoint": "http://localhost:4318/v1/traces" } } )) ``` ```go Go theme={null} agentcat.Track(s, "", &agentcat.Options{ Exporters: map[string]any{ "otlp": map[string]any{ "type": "otlp", "endpoint": "http://localhost:4318/v1/traces", }, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Exporters\OtlpExporter; use AgentCat\Options; AgentCat::track($builder, '', new Options( exporters: [ 'otlp' => new OtlpExporter(endpoint: 'http://localhost:4318/v1/traces'), ], )); ``` In PHP, `''` means no AgentCat project, while `null` reads the project ID from `AGENTCAT_PROJECT_ID`. Learn more about [integration options →](/integrations) ## Next steps > **[Identify Users](/sdk/identifying-users)** > > Know which user is behind each session > **[Debug Mode](/sdk/debug-mode)** > > Turn on extra logging when things aren't working right > **[Customize Tool Context](/sdk/tool-call-context)** > > Control what context AgentCat captures from each tool call > **[Custom Events](/sdk/custom-events)** > > Track your own events alongside the ones AgentCat captures # Custom Events Source: https://docs.agentcat.com/sdk/custom-events Track custom events beyond what AgentCat captures out of the box. AgentCat automatically tracks MCP protocol events like tool calls and session initialization. Custom events let you go further and track actions specific to your application: feature usage, billing milestones, or anything else that matters to your product. Custom events appear in the session replay timeline with a bolt icon, alongside automatic MCP events. The `resourceName` is used as the label. Click on any custom event to see its full details, including `parameters`, `response`, and `message`. If `isError` is set to `true`, the event is styled as an error in the timeline. ## Publishing Events After calling `agentcat.track()`, use `publishCustomEvent` to send events tied to the current session. Pass the tracked server instance as the first argument. ```typescript TypeScript theme={null} import * as agentcat from "agentcat" await agentcat.publishCustomEvent(mcpServer, "proj_abc123xyz", { resourceName: "subscription_upgraded", parameters: { plan: "pro", source: "settings_page" } }) ``` ```python Python theme={null} import agentcat agentcat.publish_custom_event(server, "proj_abc123xyz", { "resource_name": "subscription_upgraded", "parameters": { "plan": "pro", "source": "settings_page" } }) ``` ```go Go theme={null} import agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: import agentcat "go.agentcat.com/sdk/mcpgo" agentcat.PublishCustomEvent(s, "proj_abc123xyz", agentcat.EventData{ ResourceName: "subscription_upgraded", Parameters: map[string]any{ "plan": "pro", "source": "settings_page", }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\CustomEvent; AgentCat::publishCustomEvent($builder, 'proj_abc123xyz', new CustomEvent( resourceName: 'subscription_upgraded', parameters: [ 'plan' => 'pro', 'source' => 'settings_page', ], )); ``` In PHP, `publishCustomEvent()` queues the event and returns nothing, and an event without a `sessionId` in its `CustomEvent` has no session. ## Using a Session ID If you have a session ID instead of the server instance, you can pass it directly as a string. AgentCat will derive a stable session from it. ```typescript TypeScript theme={null} await agentcat.publishCustomEvent("user-session-12345", "proj_abc123xyz", { resourceName: "report_exported" }) ``` ```python Python theme={null} agentcat.publish_custom_event("user-session-12345", "proj_abc123xyz", { "resource_name": "report_exported" }) ``` ```go Go theme={null} agentcat.PublishCustomEvent("user-session-12345", "proj_abc123xyz", agentcat.EventData{ ResourceName: "report_exported", }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\CustomEvent; AgentCat::publishCustomEvent('user-session-12345', 'proj_abc123xyz', new CustomEvent( resourceName: 'report_exported', )); ``` The PHP SDK uses the string as the event's session ID, unchanged, and applies none of a server's hooks. ## publishCustomEvent Reference | Parameter | Type | Required | Description | | - | - | - | - | | `serverOrSessionId` | Server \| string | Yes | A tracked MCP server instance, or a session ID string | | `projectId` | string | Yes | Your AgentCat project ID (e.g. `proj_abc123xyz`) | | `eventData` | object | No | The event payload (see below) | ## Event Data | Field | Type | Description | | - | - | - | | `resourceName` | string | A name for the event. Shows as the label in the dashboard timeline. If omitted, the event shows as "Custom". | | `parameters` | object | Arbitrary data to attach to the event. Visible in the event detail panel. | | `response` | object | Response data to attach to the event. | | `message` | string | A human-readable description of what happened. | | `duration` | number | Duration of the event in milliseconds. | | `isError` | boolean | Set to `true` to mark this event as an error. | | `error` | object | Error details. Typically includes `message` and optionally `type` or `code`. | | `tags` | Record\ | Indexed string key-value pairs for filtering and grouping. Subject to [tag validation rules](/sdk/event-tags-properties#tag-validation-rules). | | `properties` | Record\ | Arbitrary JSON metadata. No client-side validation constraints. | In PHP, the event data is an `AgentCat\CustomEvent` with these fields as named arguments. All fields in `eventData` are optional. You can call `publishCustomEvent` with just the server and project ID to record a minimal custom event. You can also enrich every auto-captured event with metadata — see [Event Tags & Properties](/sdk/event-tags-properties). ## Examples ### Tracking Feature Usage Record when users interact with specific features to understand adoption and usage patterns. ```typescript TypeScript theme={null} await agentcat.publishCustomEvent(mcpServer, "proj_abc123xyz", { resourceName: "feature_used", parameters: { feature: "data_export", format: "json", recordCount: 250 } }) ``` ```python Python theme={null} agentcat.publish_custom_event(server, "proj_abc123xyz", { "resource_name": "feature_used", "parameters": { "feature": "data_export", "format": "json", "record_count": 250 } }) ``` ```go Go theme={null} agentcat.PublishCustomEvent(s, "proj_abc123xyz", agentcat.EventData{ ResourceName: "feature_used", Parameters: map[string]any{ "feature": "data_export", "format": "json", "recordCount": 250, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\CustomEvent; AgentCat::publishCustomEvent($builder, 'proj_abc123xyz', new CustomEvent( resourceName: 'feature_used', parameters: [ 'feature' => 'data_export', 'format' => 'json', 'recordCount' => 250, ], )); ``` ### Tracking Errors Capture application-level errors that aren't tied to a specific tool call. ```typescript TypeScript theme={null} await agentcat.publishCustomEvent(mcpServer, "proj_abc123xyz", { resourceName: "rate_limit_hit", isError: true, error: { message: "Rate limit exceeded", code: "RATE_LIMIT" }, parameters: { endpoint: "/api/search", limit: 100 } }) ``` ```python Python theme={null} agentcat.publish_custom_event(server, "proj_abc123xyz", { "resource_name": "rate_limit_hit", "is_error": True, "error": {"message": "Rate limit exceeded", "code": "RATE_LIMIT"}, "parameters": { "endpoint": "/api/search", "limit": 100 } }) ``` ```go Go theme={null} agentcat.PublishCustomEvent(s, "proj_abc123xyz", agentcat.EventData{ ResourceName: "rate_limit_hit", IsError: true, Error: map[string]any{"message": "Rate limit exceeded", "code": "RATE_LIMIT"}, Parameters: map[string]any{ "endpoint": "/api/search", "limit": 100, }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\CustomEvent; AgentCat::publishCustomEvent($builder, 'proj_abc123xyz', new CustomEvent( resourceName: 'rate_limit_hit', isError: true, error: ['message' => 'Rate limit exceeded', 'code' => 'RATE_LIMIT'], parameters: [ 'endpoint' => '/api/search', 'limit' => 100, ], )); ``` ### Tracking User Milestones Track when users reach key points in their journey, with duration to measure how long it took. ```typescript TypeScript theme={null} await agentcat.publishCustomEvent(mcpServer, "proj_abc123xyz", { resourceName: "onboarding_completed", message: "User finished all onboarding steps", parameters: { stepsCompleted: 5 }, duration: 200000 }) ``` ```python Python theme={null} agentcat.publish_custom_event(server, "proj_abc123xyz", { "resource_name": "onboarding_completed", "message": "User finished all onboarding steps", "parameters": { "steps_completed": 5 }, "duration": 200000 }) ``` ```go Go theme={null} agentcat.PublishCustomEvent(s, "proj_abc123xyz", agentcat.EventData{ ResourceName: "onboarding_completed", Message: "User finished all onboarding steps", Parameters: map[string]any{ "stepsCompleted": 5, }, Duration: 200000, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\CustomEvent; AgentCat::publishCustomEvent($builder, 'proj_abc123xyz', new CustomEvent( resourceName: 'onboarding_completed', message: 'User finished all onboarding steps', parameters: [ 'stepsCompleted' => 5, ], duration: 200000, )); ``` # Debug Mode Source: https://docs.agentcat.com/sdk/debug-mode Enable debug logging to troubleshoot your AgentCat integration. AgentCat can write detailed logs to `~/agentcat.log` to help you troubleshoot integration issues. Debug mode is off by default and needs to be enabled in your tracking configuration. The log file includes information about event batching, session management, telemetry delivery, and any errors the SDK encounters. ## Enabling Debug Mode Pass the debug option when calling `agentcat.track()`: ```typescript TypeScript theme={null} import * as agentcat from "agentcat" agentcat.track(mcpServer, "proj_abc123xyz", { debug: true }) ``` ```python Python theme={null} import agentcat agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(debug_mode=True)) ``` ```go Go theme={null} import agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: import agentcat "go.agentcat.com/sdk/mcpgo" agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ Debug: true, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use Monolog\Handler\StreamHandler; use Monolog\Logger; // No debug option: AgentCat always logs. With a PSR-3 logger on the Builder, its lines go there. $builder->setLogger(new Logger('mcp', [new StreamHandler(__DIR__ . '/var/log/mcp.log')])); AgentCat::track($builder, 'proj_abc123xyz'); ``` The PHP SDK has no debug option and does not read `AGENTCAT_DEBUG_MODE`: it always writes its lines to `~/agentcat.log` (or to the PSR-3 logger of the first tracked Builder that has one), to `storage/logs/agentcat.log` in [Laravel](/sdk/php/laravel), and to `%kernel.logs_dir%/agentcat.log` in [Symfony](/sdk/php/symfony). ### Environment Variable You can also enable debug mode with an environment variable instead of changing your code: ```bash theme={null} AGENTCAT_DEBUG_MODE=true ``` This accepts `true`, `1`, `yes`, or `on` (case-insensitive). ## Reading the Log File Logs are written to `~/agentcat.log`. You can tail the file while your server is running to see output in real time: ```bash theme={null} tail -f ~/agentcat.log ``` ## When to Use Debug Mode Debug mode is useful when: * Events aren't showing up in the AgentCat dashboard * You want to verify that tracking is set up correctly * You need to confirm that user identification is working * You're troubleshooting exporter or telemetry delivery issues > [!NOTE] > > Debug mode is meant for development and troubleshooting. In production, leave it off to avoid unnecessary disk writes. # Event Tags & Properties Source: https://docs.agentcat.com/sdk/event-tags-properties Attach structured tags and flexible metadata to every event AgentCat captures. AgentCat's SDKs automatically capture rich telemetry on every tool call and major event in your MCP server. But sometimes, you need additional information to query or debug events. Event tags and properties let you attach that context: * **Tags**: string key-value pairs designed for filtering and grouping in the dashboard and exported data * **Properties**: flexible JSON for arbitrary context like feature flags, device info, or build metadata Both are callback options on `agentcat.track()` that attach metadata to every auto-captured event - tool calls, tool lists, and session initialization. You set them up once and every event gets enriched automatically, without modifying individual tool handlers. Both callbacks receive the same `(request, extra)` arguments as [`identify`](/sdk/identifying-users), and if they throw or return null, the event is still sent without metadata. ## Event Tags Use `eventTags` to attach structured, filterable metadata to every event. Tags are ideal for values you'll want to filter or group by in the dashboard, such as environments, regions, trace IDs, or deployment versions. ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { eventTags: async (request, extra) => ({ env: process.env.NODE_ENV ?? "development", region: "us-east-1", trace_id: extra?.requestContext?.traceId ?? "none", }), }); ``` ```python Python theme={null} import os import agentcat def get_event_tags(request, context): return { "env": os.environ.get("APP_ENV", "development"), "region": "us-east-1", "trace_id": getattr(context, "trace_id", "none"), } agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(event_tags=get_event_tags)) ``` ```go Go theme={null} import ( "os" "github.com/modelcontextprotocol/go-sdk/mcp" agentcat "go.agentcat.com/sdk/officialsdk" ) agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ EventTags: func(ctx context.Context, req *mcp.CallToolRequest) map[string]string { env := os.Getenv("APP_ENV") if env == "" { env = "development" } return map[string]string{ "env": env, "region": "us-east-1", } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; use AgentCat\ToolCall; AgentCat::track($builder, 'proj_abc123xyz', new Options( eventTags: static fn(ToolCall $call): array => [ 'env' => getenv('APP_ENV') ?: 'development', 'region' => 'us-east-1', 'trace_id' => $call->headers['x-trace-id'] ?? 'none', ], )); ``` In PHP, both hooks receive the `AgentCat\ToolCall` that [`identify`](/sdk/identifying-users) gets, and run after the client has its answer; `eventTags` returns an array of string keys and string values. ## Event Properties Use `eventProperties` to attach flexible JSON metadata. Properties have no client-side validation constraints beyond being valid JSON. ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { eventProperties: async (request, extra) => ({ device: "desktop", app_version: "2.1.0", feature_flags: ["dark_mode", "beta_ui"], build: { commit: "abc123", branch: "main", }, }), }); ``` ```python Python theme={null} import agentcat def get_event_properties(request, context): return { "device": "desktop", "app_version": "2.1.0", "feature_flags": ["dark_mode", "beta_ui"], "build": { "commit": "abc123", "branch": "main", }, } agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(event_properties=get_event_properties)) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ EventProperties: func(ctx context.Context, req *mcp.CallToolRequest) map[string]any { return map[string]any{ "device": "desktop", "app_version": "2.1.0", "feature_flags": []string{"dark_mode", "beta_ui"}, "build": map[string]any{ "commit": "abc123", "branch": "main", }, } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; use AgentCat\ToolCall; AgentCat::track($builder, 'proj_abc123xyz', new Options( eventProperties: static fn(ToolCall $call): array => [ 'device' => 'desktop', 'app_version' => '2.1.0', 'feature_flags' => ['dark_mode', 'beta_ui'], 'build' => [ 'commit' => 'abc123', 'branch' => 'main', ], ], )); ``` ## Using Both Together Use tags for the values you'd filter by in a dashboard and properties for the detail you'd want when clicking into a specific event. ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { identify: async (request, extra) => { const userId = request.params?.arguments?.userId; if (!userId) return null; return { userId }; }, eventTags: async (request, extra) => ({ env: process.env.NODE_ENV ?? "development", region: process.env.AWS_REGION ?? "unknown", }), eventProperties: async (request, extra) => ({ tool_name: request.params?.name, server_version: process.env.npm_package_version, }), }); ``` ```python Python theme={null} import os import agentcat from agentcat import UserIdentity def identify_user(request, context): user_id = request.params.arguments.get("userId") if not user_id: return None return UserIdentity(user_id=user_id) def get_event_tags(request, context): return { "env": os.environ.get("APP_ENV", "development"), "region": os.environ.get("AWS_REGION", "unknown"), } def get_event_properties(request, context): return { "tool_name": getattr(request.params, "name", None), "server_version": os.environ.get("APP_VERSION"), } agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions( identify=identify_user, event_tags=get_event_tags, event_properties=get_event_properties, )) ``` ```go Go theme={null} import ( "os" "github.com/modelcontextprotocol/go-sdk/mcp" agentcat "go.agentcat.com/sdk/officialsdk" ) agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { args := req.GetArguments() userID, _ := args["userId"].(string) if userID == "" { return nil } return &agentcat.UserIdentity{UserID: userID} }, EventTags: func(ctx context.Context, req *mcp.CallToolRequest) map[string]string { env := os.Getenv("APP_ENV") if env == "" { env = "development" } return map[string]string{ "env": env, "region": os.Getenv("AWS_REGION"), } }, EventProperties: func(ctx context.Context, req *mcp.CallToolRequest) map[string]any { return map[string]any{ "tool_name": req.Params.Name, "server_version": os.Getenv("APP_VERSION"), } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; AgentCat::track($builder, 'proj_abc123xyz', new Options( identify: static function (ToolCall $call): ?Identity { $userId = $call->arguments['userId'] ?? null; return is_string($userId) && $userId !== '' ? new Identity(userId: $userId) : null; }, eventTags: static fn(ToolCall $call): array => [ 'env' => getenv('APP_ENV') ?: 'development', 'region' => getenv('AWS_REGION') ?: 'unknown', ], eventProperties: static fn(ToolCall $call): array => [ 'tool_name' => $call->toolName, 'server_version' => getenv('APP_VERSION') ?: null, ], )); ``` ## Examples ### Tagging by Environment Tag events with your deployment environment and region to compare error rates and tool usage patterns across production, staging, and development. ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { eventTags: async (request, extra) => ({ env: process.env.NODE_ENV ?? "development", region: process.env.AWS_REGION ?? "unknown", version: process.env.npm_package_version ?? "0.0.0", }), }); ``` ```python Python theme={null} import os import agentcat agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions( event_tags=lambda request, context: { "env": os.environ.get("APP_ENV", "development"), "region": os.environ.get("AWS_REGION", "unknown"), "version": os.environ.get("APP_VERSION", "0.0.0"), } )) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ EventTags: func(ctx context.Context, req *mcp.CallToolRequest) map[string]string { return map[string]string{ "env": os.Getenv("APP_ENV"), "region": os.Getenv("AWS_REGION"), "version": os.Getenv("APP_VERSION"), } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; use AgentCat\ToolCall; AgentCat::track($builder, 'proj_abc123xyz', new Options( eventTags: static fn(ToolCall $call): array => [ 'env' => getenv('APP_ENV') ?: 'development', 'region' => getenv('AWS_REGION') ?: 'unknown', 'version' => getenv('APP_VERSION') ?: '0.0.0', ], )); ``` ### Attaching Feature Flags Attach feature flags and build metadata as properties so you can see exactly what configuration was active when a tool call happened — useful for debugging why a tool behaves differently for certain users. ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { eventProperties: async (request, extra) => ({ feature_flags: getActiveFlags(), build: { commit: process.env.GIT_SHA, branch: process.env.GIT_BRANCH, ci: process.env.CI === "true", }, }), }); ``` ```python Python theme={null} import os import agentcat agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions( event_properties=lambda request, context: { "feature_flags": get_active_flags(), "build": { "commit": os.environ.get("GIT_SHA"), "branch": os.environ.get("GIT_BRANCH"), "ci": os.environ.get("CI") == "true", }, } )) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ EventProperties: func(ctx context.Context, req *mcp.CallToolRequest) map[string]any { return map[string]any{ "feature_flags": getActiveFlags(), "build": map[string]any{ "commit": os.Getenv("GIT_SHA"), "branch": os.Getenv("GIT_BRANCH"), "ci": os.Getenv("CI") == "true", }, } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; use AgentCat\ToolCall; use App\FeatureFlags; AgentCat::track($builder, 'proj_abc123xyz', new Options( eventProperties: static fn(ToolCall $call): array => [ 'feature_flags' => FeatureFlags::active(), 'build' => [ 'commit' => getenv('GIT_SHA') ?: null, 'branch' => getenv('GIT_BRANCH') ?: null, 'ci' => getenv('CI') === 'true', ], ], )); ``` ### Correlating with External Observability Tag events with trace and span IDs from your existing observability stack to link AgentCat events directly to traces in Datadog, Sentry, or any OpenTelemetry-compatible platform. ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { eventTags: async (request, extra) => ({ trace_id: extra?.requestContext?.traceId ?? "none", span_id: extra?.requestContext?.spanId ?? "none", service: "my-mcp-server", }), }); ``` ```python Python theme={null} import agentcat def get_event_tags(request, context): return { "trace_id": getattr(context, "trace_id", "none"), "span_id": getattr(context, "span_id", "none"), "service": "my-mcp-server", } agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(event_tags=get_event_tags)) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ EventTags: func(ctx context.Context, req *mcp.CallToolRequest) map[string]string { traceID := "none" spanID := "none" if sc := trace.SpanContextFromContext(ctx); sc.IsValid() { traceID = sc.TraceID().String() spanID = sc.SpanID().String() } return map[string]string{ "trace_id": traceID, "span_id": spanID, "service": "my-mcp-server", } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; use AgentCat\ToolCall; AgentCat::track($builder, 'proj_abc123xyz', new Options( eventTags: static function (ToolCall $call): array { // W3C trace context: traceparent is version-traceid-spanid-flags $traceparent = explode('-', $call->headers['traceparent'] ?? ''); return [ 'trace_id' => $traceparent[1] ?? 'none', 'span_id' => $traceparent[2] ?? 'none', 'service' => 'my-mcp-server', ]; }, )); ``` ## Tag Validation Rules Tags are validated client-side before being attached to events. Invalid entries are silently dropped with a warning written to `~/agentcat.log`. * **Keys**: Must be 32 characters or fewer and match `[a-zA-Z0-9_.:\- ]` (letters, digits, dots, underscores, colons, hyphens, spaces) * **Values**: Must be strings, 200 characters or fewer, and cannot contain newline characters * **Max entries**: 50 tags per event — entries beyond the limit are dropped > [!NOTE] > > Event properties have no client-side validation constraints — any valid JSON > is accepted. Only tags are validated. > [!NOTE] > > Tag validation applies to both `eventTags` callback results and inline `tags` > on [custom events](/sdk/custom-events). Invalid entries are dropped, but the > event is still sent. # Identifying Users Source: https://docs.agentcat.com/sdk/identifying-users How to use AgentCat to identify your user's sessions. AgentCat allows you to identify users and associate their sessions with specific user information. This lets you to track usage patterns, understand user behavior, and provide personalized experiences. User identification happens automatically when a tool is invoked. In stateful mode (the default), this happens once on the first tool call and persists for the session. In [stateless mode](/sdk/stateless-servers), it runs on every request since there is no persistent session. ## Basic Implementation To identify users, provide an `identify` callback function when tracking your MCP server. The simplest approach is to pull the user ID directly from the tool call arguments: ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { identify: async (request, extra) => { const userId = request.params?.arguments?.userId; if (!userId) return null; return { userId, userName: request.params?.arguments?.userName }; } }); ``` ```python Python theme={null} import agentcat from agentcat import UserIdentity def identify_user(request, context): user_id = request.params.arguments.get('userId') if not user_id: return None return UserIdentity( user_id=user_id, user_name=request.params.arguments.get('userName') ) agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(identify=identify_user)) ``` ```go Go theme={null} import ( "github.com/modelcontextprotocol/go-sdk/mcp" agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: agentcat "go.agentcat.com/sdk/mcpgo" ) agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { args := req.GetArguments() userID, _ := args["userId"].(string) if userID == "" { return nil } userName, _ := args["userName"].(string) return &agentcat.UserIdentity{ UserID: userID, UserName: userName, } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; AgentCat::track($builder, 'proj_abc123xyz', new Options( identify: static function (ToolCall $call): ?Identity { $userId = $call->arguments['userId'] ?? null; if (!is_string($userId) || $userId === '') { return null; } $userName = $call->arguments['userName'] ?? null; return new Identity( userId: $userId, userName: is_string($userName) ? $userName : null, ); }, )); ``` In PHP, `identify` receives one `AgentCat\ToolCall` and returns an `AgentCat\Identity` or `null`; it runs for every tool call, after the client has its answer (see the PHP section of Function Parameters below). ## User Identity Object The identify function should return a `UserIdentity` object with the following structure: ```typescript TypeScript theme={null} interface UserIdentity { userId: string; // Required: Unique identifier userName?: string; // Optional: Display name userData?: Record; // Optional: Additional metadata } ``` ```python Python theme={null} from agentcat import UserIdentity UserIdentity( user_id="user123", # Required: Unique identifier user_name="John Doe", # Optional: Display name user_data={ # Optional: Additional metadata "plan": "premium", "company": "Acme Corp" } ) ``` ```go Go theme={null} agentcat.UserIdentity{ UserID: "user123", // Required: Unique identifier UserName: "John Doe", // Optional: Display name UserData: map[string]any{ // Optional: Additional metadata "plan": "premium", "company": "Acme Corp", }, } ``` ```php PHP theme={null} use AgentCat\Identity; $identity = new Identity( userId: 'user123', // Required: Unique identifier userName: 'John Doe', // Optional: Display name userData: [ // Optional: Additional metadata 'plan' => 'premium', 'company' => 'Acme Corp', ], ); ``` ## Common Patterns ### Token-Based Authentication Extract user information from authentication tokens: ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { identify: async (request, extra) => { // Get token from request arguments or auth info const token = request.params?.arguments?.token || extra.authInfo?.token; // Validate token and fetch user const user = await validateTokenAndGetUser(token); return { userId: user.id, userName: user.email, userData: { role: user.role, organization: user.orgId } }; } }); ``` ```python Python theme={null} from mcp.server.auth.middleware.auth_context import get_access_token def identify_from_token(request, context): # Try to get token from MCP auth context first access_token = get_access_token() if access_token: token = access_token.token else: # Fall back to request arguments token = request.params.arguments.get('token') # Validate token and fetch user user = validate_token_and_get_user(token) return UserIdentity( user_id=user.id, user_name=user.email, user_data={ 'role': user.role, 'organization': user.org_id } ) agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(identify=identify_from_token)) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { // Get token from request arguments or context args := req.GetArguments() token, _ := args["token"].(string) // Validate token and fetch user user, err := validateTokenAndGetUser(token) if err != nil { return nil } return &agentcat.UserIdentity{ UserID: user.ID, UserName: user.Email, UserData: map[string]any{ "role": user.Role, "organization": user.OrgID, }, } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; use App\Auth\TokenVerifier; AgentCat::track($builder, 'proj_abc123xyz', new Options( identify: static function (ToolCall $call): ?Identity { // Get the token from the tool arguments or the Authorization header $token = $call->arguments['token'] ?? null; if (!is_string($token)) { $authorization = $call->headers['authorization'] ?? ''; $token = str_starts_with($authorization, 'Bearer ') ? substr($authorization, 7) : null; } if ($token === null) { return null; } // Validate the token and fetch the user $user = TokenVerifier::userFor($token); return new Identity( userId: $user->id, userName: $user->email, userData: [ 'role' => $user->role, 'organization' => $user->orgId, ], ); }, )); ``` ### API Key Identification Identify users based on API keys: ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { identify: async (request, extra) => { const apiKey = request.params?.arguments?.apiKey; const account = await getAccountByApiKey(apiKey); return { userId: account.id, userName: account.organizationName, userData: { tier: account.tier, monthlyUsage: account.currentUsage, region: account.region } }; } }); ``` ```python Python theme={null} def identify_from_api_key(request, context): api_key = request.params.arguments.get('apiKey') account = get_account_by_api_key(api_key) return UserIdentity( user_id=account.id, user_name=account.organization_name, user_data={ 'tier': account.tier, 'monthly_usage': account.current_usage, 'region': account.region } ) agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(identify=identify_from_api_key)) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { args := req.GetArguments() apiKey, _ := args["apiKey"].(string) account := getAccountByApiKey(apiKey) return &agentcat.UserIdentity{ UserID: account.ID, UserName: account.OrganizationName, UserData: map[string]any{ "tier": account.Tier, "monthlyUsage": account.CurrentUsage, "region": account.Region, }, } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; use App\Accounts; AgentCat::track($builder, 'proj_abc123xyz', new Options( identify: static function (ToolCall $call): ?Identity { $apiKey = $call->arguments['apiKey'] ?? null; if (!is_string($apiKey)) { return null; } $account = Accounts::byApiKey($apiKey); return new Identity( userId: $account->id, userName: $account->organizationName, userData: [ 'tier' => $account->tier, 'monthlyUsage' => $account->currentUsage, 'region' => $account->region, ], ); }, )); ``` ### MCP Authentication For servers using MCP's built-in authentication: ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { identify: async (request, extra) => { // Access auth token from extra parameter const authToken = extra.authInfo?.token; if (!authToken) { return null; // Anonymous session } // Use the MCP auth token to identify user const user = await getUserFromMCPToken(authToken); return { userId: user.id, userName: user.email, userData: { authProvider: 'mcp', scopes: extra.authInfo?.scopes } }; } }); ``` ```python Python theme={null} from mcp.server.auth.middleware.auth_context import get_access_token def identify_from_mcp_auth(request, context): # Get the MCP access token access_token = get_access_token() if not access_token: return None # Anonymous session # Use the MCP auth token to identify user user = get_user_from_mcp_token(access_token.token) return UserIdentity( user_id=user.id, user_name=user.email, user_data={ 'auth_provider': 'mcp', 'scopes': access_token.scopes } ) agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(identify=identify_from_mcp_auth)) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { // Extract auth token from context authToken := getAuthTokenFromContext(ctx) if authToken == "" { return nil // Anonymous session } // Use the auth token to identify user user := getUserFromToken(authToken) return &agentcat.UserIdentity{ UserID: user.ID, UserName: user.Email, UserData: map[string]any{ "authProvider": "mcp", }, } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; use App\Auth\TokenVerifier; AgentCat::track($builder, 'proj_abc123xyz', new Options( identify: static function (ToolCall $call): ?Identity { // The bearer token of the MCP authorization, from the request headers $authorization = $call->headers['authorization'] ?? ''; if (!str_starts_with($authorization, 'Bearer ')) { return null; // Anonymous session } // Use the MCP auth token to identify the user $user = TokenVerifier::userFor(substr($authorization, 7)); return new Identity( userId: $user->id, userName: $user->email, userData: ['authProvider' => 'mcp'], ); }, )); ``` The PHP SDK has no separate auth object: the bearer token is in `$call->headers['authorization']`, and `$call->request` and `$call->context` hold the request and context objects of your MCP library. ### Error Handling Handle identification failures gracefully: ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { identify: async (request, extra) => { try { const user = await fetchUserInfo(request); return { userId: user.id, userName: user.name }; } catch (error) { console.error('User identification failed:', error); // Return null to continue with anonymous tracking return null; } } }); ``` ```python Python theme={null} def identify_with_error_handling(request, context): try: user = fetch_user_info(request) return UserIdentity( user_id=user.id, user_name=user.name ) except Exception as e: print(f'User identification failed: {e}') # Return None to continue with anonymous tracking return None agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(identify=identify_with_error_handling)) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { user, err := fetchUserInfo(ctx, req) if err != nil { log.Printf("User identification failed: %v", err) // Return nil to continue with anonymous tracking return nil } return &agentcat.UserIdentity{ UserID: user.ID, UserName: user.Name, } }, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; use App\Users; AgentCat::track($builder, 'proj_abc123xyz', new Options( identify: static function (ToolCall $call): ?Identity { try { $user = Users::forToolCall($call); return new Identity(userId: $user->id, userName: $user->name); } catch (\Throwable $e) { error_log('User identification failed: ' . $e->getMessage()); // Return null to continue with anonymous tracking return null; } }, )); ``` In PHP, AgentCat also catches what the hook throws: it logs the error, and the event stays anonymous. ## Function Parameters The identify function receives parameters that vary between the TypeScript, Python, and Go SDKs: > [!NOTE] > > The request structure follows the MCP (Model Context Protocol) specification. The specific types and fields available depend on your SDK: > > * TypeScript: Uses the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) > * Python: Uses the [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) > * Go: Uses the [official Go MCP SDK](https://github.com/modelcontextprotocol/go-sdk) or [mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) > * PHP: Uses [`mcp/sdk`](https://github.com/modelcontextprotocol/php-sdk) or [`laravel/mcp`](https://github.com/laravel/mcp), through one `AgentCat\ToolCall` ```typescript TypeScript theme={null} // TypeScript SDK parameters identify: async (request, extra) => { // request: MCP request object // extra: RequestHandlerExtra object } ``` ```python Python theme={null} # Python SDK parameters def identify(request, context): # request: MCP request object # context: RequestContext object pass ``` ```go Go theme={null} // Go SDK parameters Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { // ctx: context.Context — request context with cancellation and deadlines // req: *mcp.CallToolRequest — the tool call request with arguments return nil } ``` ```php PHP theme={null} use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; // PHP SDK parameters $options = new Options( identify: static function (ToolCall $call): ?Identity { // $call: AgentCat\ToolCall — a snapshot of the tool call, taken when it arrived return null; }, ); ``` ### request The MCP request object following the JSON-RPC format: ```typescript TypeScript theme={null} { method: string; // The tool/method being called (e.g., "tools/call") params?: { // Optional parameters _meta?: { // Optional metadata progressToken?: string | number; // Token for progress notifications }; name?: string; // Tool name being invoked arguments?: { // Tool-specific arguments passed by the client // Your custom arguments like userId, apiKey, token, etc. passed into tool calls [key: string]: any; }; }; } ``` ```python Python theme={null} # Python request object structure request.method # The tool/method being called (e.g., "tools/call") request.params # Optional parameters object request.params.name # Tool name being invoked request.params.arguments # Tool-specific arguments dict # Access arguments like: request.params.arguments.get('userId') request.params.arguments.get('apiKey') ``` ```go Go theme={null} // Go request object structure (*mcp.CallToolRequest) req.Method() // The tool/method being called req.Params.Name // Tool name being invoked req.GetArguments() // Tool-specific arguments map // Access arguments like: args := req.GetArguments() userID, _ := args["userId"].(string) apiKey, _ := args["apiKey"].(string) ``` ```php PHP theme={null} use AgentCat\ToolCall; // AgentCat\ToolCall: the only argument of a PHP hook $read = static function (ToolCall $call): void { $toolName = $call->toolName; // Tool name being invoked $arguments = $call->arguments; // The raw tool arguments, AgentCat's injected parameters included $meta = $call->meta; // The raw _meta of the call $headers = $call->headers; // HTTP request headers, lower-case names; empty on stdio $clientName = $call->clientName; // MCP client name, when it is known $clientVersion = $call->clientVersion; // MCP client version, when it is known $protocol = $call->protocolVersion; // MCP protocol version of the call, when it is known $transport = $call->transportSessionId; // The transport's Mcp-Session-Id, never the AgentCat session ID $request = $call->request; // The request object of your MCP library $context = $call->context; // The session or context object of your MCP library // Access arguments like: $userId = $call->arguments['userId'] ?? null; $apiKey = $call->arguments['apiKey'] ?? null; }; ``` ### TypeScript: extra The MCP RequestHandlerExtra object providing additional context: ```typescript theme={null} { signal: AbortSignal; // Abort signal for request cancellation authInfo?: AuthInfo; // Validated access token information sessionId?: string; // Session ID from the transport _meta?: RequestMeta; // Metadata from the original request requestId: RequestId; // JSON-RPC ID of the request (string | number) // Functions for sending related messages (not typically used in identify) sendNotification: (notification: Notification) => Promise; sendRequest: (request: Request, resultSchema: U, options?: RequestOptions) => Promise>; } ``` ### Python: context The MCP RequestContext object providing session and client information: ```python theme={null} # Python context object context.session # ServerSession object context.session.client_params.clientInfo.name # Client name context.session.client_params.clientInfo.version # Client version context.lifespan_context # Application-specific context storage ``` > [!NOTE] > > In [stateless mode](/sdk/stateless-servers), `context.session` still exists but represents the per-request context rather than a long-lived session. The `clientInfo` fields are extracted from HTTP headers on each request. > [!NOTE] > > **Python Authentication Access**: In Python MCP servers, authentication tokens are accessed via the `get_access_token()` function from `mcp.server.auth.middleware.auth_context`, not through the context parameter. This is different from the TypeScript SDK where auth info is available in the `extra` parameter. ### Go: ctx and req The Go SDK identify function receives `context.Context` and `*mcp.CallToolRequest`: ```go theme={null} // ctx: standard Go context with cancellation and deadline support ctx.Done() // Channel closed when request is cancelled ctx.Err() // Returns context.Canceled or context.DeadlineExceeded // req: the MCP CallToolRequest req.Params.Name // Tool name being invoked args := req.GetArguments() // Tool arguments as map[string]any ``` > [!NOTE] > > **Go Authentication Access**: In Go MCP servers, authentication information is typically extracted from the `context.Context` parameter. The exact method depends on your authentication middleware and MCP library. ### PHP: the ToolCall The PHP `identify` hook receives one `AgentCat\ToolCall`, an immutable snapshot of the tool call (its fields are in the PHP tab above), and returns an `AgentCat\Identity` or `null`. It runs for every tool call, after the client has its answer, so a slow lookup never adds latency to the call; its result is attached to the event before AgentCat sends it. > [!NOTE] > > **PHP Authentication Access**: Read the bearer token from `$call->headers['authorization']`. On a stdio server, `$call->headers` is empty. ## Common Usage Patterns ```typescript TypeScript theme={null} identify: async (request, extra) => { // Access tool arguments const userId = request.params?.arguments?.userId; const apiKey = request.params?.arguments?.apiKey; // Access auth information (if using MCP auth) const authToken = extra.authInfo?.token; // Tool name being called const toolName = request.params?.name; // Check if request was cancelled if (extra.signal.aborted) { return null; } // Your identification logic here... } ``` ```python Python theme={null} from mcp.server.auth.middleware.auth_context import get_access_token def identify(request, context): # Access tool arguments user_id = request.params.arguments.get('userId') api_key = request.params.arguments.get('apiKey') # Access auth token (if using MCP auth) access_token = get_access_token() if access_token: auth_token = access_token.token # Tool name being called tool_name = request.params.name # Access client information from context client_name = None try: client_info = context.session.client_params.clientInfo client_name = client_info.name if client_info else None except: pass # Your identification logic here... ``` ```go Go theme={null} Identify: func(ctx context.Context, req *mcp.CallToolRequest) *agentcat.UserIdentity { // Access tool arguments args := req.GetArguments() userID, _ := args["userId"].(string) apiKey, _ := args["apiKey"].(string) // Tool name being called toolName := req.Params.Name // Check if request was cancelled if ctx.Err() != nil { return nil } // Your identification logic here... } ``` ```php PHP theme={null} use AgentCat\Identity; use AgentCat\Options; use AgentCat\ToolCall; $options = new Options( identify: static function (ToolCall $call): ?Identity { // Access tool arguments $userId = $call->arguments['userId'] ?? null; $apiKey = $call->arguments['apiKey'] ?? null; // Access the bearer token (if your server uses MCP authorization) $authorization = $call->headers['authorization'] ?? null; // Tool name being called $toolName = $call->toolName; // Client information $clientName = $call->clientName; // Your identification logic here... return null; }, ); ``` ## Important Considerations > [!NOTE] > > **Stateful servers (default):** The identify function is called **once per session** on the first tool invocation. This minimizes overhead while ensuring consistent user tracking throughout the session. > > **[Stateless servers](/sdk/stateless-servers):** The identify function is called on **every request** since there is no persistent session. Keep your identify function lightweight — avoid expensive API calls or database lookups if possible. Without identification, stateless events cannot be grouped into sessions. ### Anonymous Sessions If you don't provide an identify function or if it returns `null`, sessions will be tracked anonymously. This is useful for: * Public APIs where user identification isn't required * Development and testing environments * Respecting user privacy preferences # PHP: Laravel Source: https://docs.agentcat.com/sdk/php/laravel Track every laravel/mcp server of a Laravel application: install, the config/agentcat.php reference, hooks, Octane and tests. The Laravel bridge tracks every [`laravel/mcp`](https://github.com/laravel/mcp) server of your application with no code: you install the package and set your project ID. ## Requirements * PHP 8.3, 8.4 or 8.5. * `laravel/mcp` 1.x, with Laravel 12.41.1 or later, or Laravel 13. On every pull request and every night, CI tests AgentCat with the newest patch of each `laravel/mcp` minor release from 1.0 up. * ext-curl, to send the events. ## Install ```bash theme={null} composer require agentcat/sdk php artisan agentcat:install ``` Laravel discovers AgentCat's service provider. `agentcat:install` asks for your project ID, or takes it as an argument (`php artisan agentcat:install proj_…`): * It checks the ID: `proj_` followed by 27 letters or digits. * It writes `AGENTCAT_PROJECT_ID` into `.env`. When the application has no `.env` file, it creates one with that key only, and says so. * It adds an empty `AGENTCAT_PROJECT_ID=` to `.env.example`, when the application has one. * With `--config`, it also publishes `config/agentcat.php`. * It sends nothing over the network. Without the command, set `AGENTCAT_PROJECT_ID` in `.env` yourself. Without a project ID, no event goes to AgentCat; your [exporters](/integrations) still get them. ## What AgentCat tracks * **Every `laravel/mcp` server** that the container resolves, also when `laravel/mcp` builds the server for each HTTP request. The server of [Laravel Boost](https://github.com/laravel/boost) is excluded by default. * **Every tool call.** AgentCat adds a `session_id` and a `context` parameter to the input schema of each tool (and `agent_id` with `enable_agent_tracking`), and removes them from the arguments before your tool runs. It adds the `get_more_tools` tool. * **After the response.** AgentCat runs your hooks and sends the events in Laravel's `terminating()` callbacks, after the client has its answer. AgentCat reads the headers, method and URL of a call from the request of the server's own HTTP transport, not from `$_SERVER`, so Octane workers record them too. ## Configuration reference To change the defaults, publish the configuration file: ```bash theme={null} php artisan vendor:publish --tag=agentcat-config ``` Every key of `config/agentcat.php`: | Key | Environment variable | Default | Accepted values | | - | - | - | - | | `enabled` | `AGENTCAT_ENABLED` | Unset: on, except while your tests run | `true`, `1`, `yes`, `on` or `false`, `0`, `no`, `off` (any case) | | `project_id` | `AGENTCAT_PROJECT_ID` | Unset: no events go to AgentCat; exporters still run | `proj_` followed by 27 letters or digits | | `exclude` | | `['Laravel\Boost\Mcp\Boost']` | A list of server classes; a string is one class | | `projects` | | `[]` | Server class => project ID | | `log_channel` | `AGENTCAT_LOG_CHANNEL` | Unset: `storage/logs/agentcat.log` | The name of a channel of `config/logging.php` | | `api_url` | `AGENTCAT_API_URL`, then `MCPCAT_API_URL` | Unset: `https://api.agentcat.com` | A URL | | `enable_report_missing` | | `true` | The words of `enabled` | | `enable_tracing` | | `true` | The words of `enabled` | | `enable_tool_call_context` | | `true` | The words of `enabled` | | `custom_context_description` | | `null`: the default description | A string | | `enable_agent_tracking` | | `false` | The words of `enabled` | | `identify` | | `null` | The class name of an invokable class | | `resolve_session_id` | | `null` | The class name of an invokable class | | `event_tags` | | `null` | The class name of an invokable class | | `event_properties` | | `null` | The class name of an invokable class | | `redact_sensitive_information` | | `null` | The class name of an invokable class | | `redact_event` | | `null` | The class name of an invokable class | | `exporters` | | `null`: no exporters | Name => `['type' => 'otlp'`, `'datadog'`, `'sentry'` or `'posthog'`, then the exporter's fields]; a `null` entry is skipped | | `disable_diagnostics` | `AGENTCAT_DISABLE_DIAGNOSTICS`, `DISABLE_DIAGNOSTICS` | Both unset: diagnostics on | `true`, `1`, `yes`, `on` turn diagnostics off; `false`, `0`, `no`, `off` and blank keep them on. In a list, one value that turns them off is enough. Any other value turns them off, with a warning. | | `diagnostics_endpoint` | `AGENTCAT_DIAGNOSTICS_ENDPOINT` | Unset: `https://otel.agentcat.com` | The URL of an OpenTelemetry collector (OTLP/HTTP logs) | | `diagnostics_token` | `AGENTCAT_DIAGNOSTICS_TOKEN` | Unset: AgentCat's | The bearer token of `diagnostics_endpoint` | | `disable_signal_handlers` | | `false` | The words of `enabled` | The file is safe with `php artisan config:cache`: the keys that hold environment variables read them through `env()`, and the service provider hands the configured values to AgentCat when the application boots. A value of the wrong type, or a word that is not in the list, logs one warning line and AgentCat uses the default. When `enabled` holds a word that is not in the list, the `AGENTCAT_ENABLED` variable of the process decides, and without one, the default. An unknown key logs `WARN: Unknown AgentCat option "agentcat." ignored`. ### `exclude` and `projects` `exclude` lists the server classes that AgentCat never tracks. A class also excludes its subclasses. An excluded server touches neither the AgentCat log nor diagnostics. * **`null`, or no `exclude` key**, is the default list: Laravel Boost's server. * **`[]`** excludes nothing, Laravel Boost's server included. * **A blank string** names no server: AgentCat logs one warning and uses the default list. `projects` sends the events of some servers to another project. The first class that the server is an instance of wins. A `null` project (`env()` of an unset variable) means `project_id`. ```php theme={null} return [ // ... 'exclude' => [ 'Laravel\Boost\Mcp\Boost', App\Mcp\Servers\InternalServer::class, ], 'projects' => [ App\Mcp\Servers\BillingServer::class => env('AGENTCAT_BILLING_PROJECT_ID'), ], ]; ``` ### The log file AgentCat writes its own lines to `storage/logs/agentcat.log`, through a Laravel `single` channel that it builds. They stay out of your application's log and its error trackers: `laravel/mcp` builds a server for each HTTP request, so the setup lines repeat on every request. To route them into one of your channels, set `log_channel`, for example to `stack`. ### Hooks are invokable classes A hook in `config/agentcat.php` is the class name of an invokable class. A closure cannot go in this file, because `php artisan config:cache` cannot store it. AgentCat resolves the class from the container when the hook runs (under Octane, from the container of the request), and calls it with the same argument as the [PHP hook](/sdk/identifying-users): an `AgentCat\ToolCall` for `identify`, `resolve_session_id`, `event_tags` and `event_properties`, a string for `redact_sensitive_information`, and an `AgentCat\Event` for `redact_event`. ```php theme={null} namespace App\AgentCat; use AgentCat\Identity; use AgentCat\ToolCall; final class IdentifyActor { public function __invoke(ToolCall $call): ?Identity { $token = $call->headers['authorization'] ?? null; return $token === null ? null : new Identity(userId: hash('sha256', $token)); } } ``` ```php theme={null} // config/agentcat.php return [ // ... 'identify' => App\AgentCat\IdentifyActor::class, ]; ``` A hook class that cannot be resolved, or that throws, logs one line when the hook runs. The tool call is never affected. ### Exporters In `config/agentcat.php`, an exporter is an array with a `type` and the fields of its [integration page](/integrations), with the TypeScript field names: ```php theme={null} return [ // ... 'exporters' => [ 'otlp' => ['type' => 'otlp', 'endpoint' => 'http://localhost:4318/v1/traces'], 'datadog' => [ 'type' => 'datadog', 'apiKey' => env('DD_API_KEY'), 'site' => 'datadoghq.com', 'service' => 'my-mcp-server', ], 'sentry' => null, // a null entry is skipped ], ]; ``` An entry with an unknown type, or a missing field, logs one line and is skipped; the other entries still work. ## Octane and other long-running workers Under [Laravel Octane](https://laravel.com/docs/octane) (Swoole, RoadRunner or FrankenPHP), a worker serves many requests. After each request, AgentCat waits at most 1 second in `terminating()` for its delivery, so the worker is soon free for the next request. * **The wait shows in request durations.** The worker's time for a request that sends events can be up to 1 second longer, in Octane's numbers and in your monitoring. The client has its answer before the wait. * **An idle worker holds its last request's events.** Events that are still waiting after the 1 second go out at the next request of the worker, or when the worker stops. * **When a worker stops**, AgentCat delivers what is left for at most 5 seconds, at Octane's `WorkerStopping` event. Measured on Swoole 6.2.3: when the whole server stops, Swoole did not cut that delivery. A worker that Swoole restarts (for example after Octane's `--max-requests`) is stopped by force about twice its `max_wait_time` server setting after it is told to stop: about 6 seconds with the default of 3. So a `max_wait_time` below 3 seconds can cut the delivery of a restarting worker short, and with the default, the other `WorkerStopping` listeners leave AgentCat its 5 seconds only when they take less than about 1 second. Under PHP-FPM, FrankenPHP's classic mode and Apache, AgentCat does not wait in `terminating()`: it only starts the delivery there, and finishes it at the end of the script, after the response. See [PHP runtimes](/sdk/php/runtimes). AgentCat binds one method object per tracked server in the container, under a key with the server's object ID. In a worker, the binding of a server that is gone stays until PHP reuses its object ID or the container ends. ## Your own method handlers A `tools/list`, `tools/call` or `initialize` class that your server declares in its `$methods` property is wrapped: AgentCat delegates to it and records one event per call. For your own `tools/call` class, the event of a failing tool carries the error of the answer, not the original exception. A method handler that you set in the server's `boot()` method replaces AgentCat's for that server: `laravel/mcp` calls `boot()` after AgentCat has tracked the server. AgentCat then records no event for that method, and logs nothing: ```php theme={null} namespace App\Mcp\Servers; use Laravel\Mcp\Server; use Laravel\Mcp\Server\Methods\CallTool; class AuditedCallTool extends CallTool { // Your own tools/call handling. } final class BillingServer extends Server { protected function boot(): void { // AgentCat never sees this handler: no event for any tools/call of this server. $this->addMethod('tools/call', AuditedCallTool::class); } } ``` Declare such a handler in the `$methods` property instead. ## ToolSearch catalogs A server that exposes its tools through a `laravel/mcp` ToolSearch catalog has the two tools `search_tools` and `execute_tools`. AgentCat injects its parameters into them like into any other tool. One `execute_tools` call is one event: AgentCat does not record the tool calls that run inside it one by one. ## Streamed answers A tool whose `handle()` is a generator answers with a stream. Its notifications pass unchanged, and AgentCat makes the event from the final result, so the event's duration includes the whole stream. A stream that the client abandons publishes no event. ## Client name and version `laravel/mcp` 1.x has no MCP sessions. AgentCat takes the client's name and version from the `_meta` of the call, else from the `initialize` request of an older client to the same server instance. A stdio server keeps that for the whole process. Over HTTP it is lost, because each request builds a new server. The events have no transport session ID and no auth object: the `Authorization` header is in the request headers. ## Responses on Apache mod\_php, php-cgi and `php -S` `laravel/mcp` sends its JSON answers without a `Content-Length` header. Where PHP sends a response only when the script ends, the bridge gives the plain JSON answers of a tracked server their `Content-Length`, so the client has the whole answer before AgentCat delivers. [PHP runtimes](/sdk/php/runtimes) has the rules and the runtimes where it does that. ## Testing AgentCat is off while your tests run (PHPUnit, Pest, or `APP_ENV=testing`): your servers answer as without AgentCat, and `laravel/mcp`'s test helpers see their own answers. To assert on the events of a test, call `AgentCat::fake()` before the test builds a server, and restore the real client in `tearDown()`: ```php theme={null} namespace Tests\Feature; use AgentCat\AgentCat; use AgentCat\Event; use App\Mcp\Servers\WeatherServer; use App\Mcp\Tools\CurrentWeatherTool; use Tests\TestCase; final class WeatherToolTest extends TestCase { protected function tearDown(): void { AgentCat::setClient(null); parent::tearDown(); } public function test_the_weather_tool_call_is_tracked(): void { $fake = AgentCat::fake(); WeatherServer::tool(CurrentWeatherTool::class, ['location' => 'Paris'])->assertHasNoErrors(); $fake->assertTrackedCount(1); $fake->assertTracked(fn (Event $event): bool => $event->isError !== true); } } ``` The [`mcp/sdk` page](/sdk/php/mcp-sdk#testing) says what each assertion of the fake counts. Three things to know in a Laravel suite: * **Two settings outlive the test that made them.** The fake stays the shared client of the PHP process until `AgentCat::setClient(null)`. The `agentcat.enabled` value that the service provider reads when the application boots stays in effect until the next application boots. * **With the fake, AgentCat changes the answers as in production.** A call without a `session_id` gets the issued session ID as the first text block of `content`. A tool with an output schema gets an `mcp_session` entry in its `structuredContent`. An assertion on the exact `structuredContent` (for example `assertStructuredContent()`) then sees that entry. * **A bad value in `config/agentcat.php` logs one line per test application**, also without the fake, because each test boots a new application. The lines go to `storage/logs/agentcat.log`. ## Laravel Boost guidelines The package ships guidelines for AI coding agents in `resources/boost/guidelines/core.blade.php`. When you run `php artisan boost:install`, [Laravel Boost](https://github.com/laravel/boost) adds them to your agent's guidelines, so your coding agent knows how AgentCat is set up: the install command, the configuration keys, the hooks as invokable classes, and `AgentCat::fake()` in tests. # PHP: differences and limits Source: https://docs.agentcat.com/sdk/php/limits Where the PHP SDK differs from the TypeScript SDK, what it sends, and its known limits. The PHP SDK follows the TypeScript SDK: the same events, the same injected parameters, and the same log lines where both SDKs log the same thing. This page lists where it differs, what it sends besides your events, and its known limits. ## Supported versions The Requirements of each guide list the supported versions: [`mcp/sdk`](/sdk/php/mcp-sdk#requirements), [Laravel](/sdk/php/laravel#requirements) and [Symfony](/sdk/php/symfony#requirements). PHP 8.6 is not supported yet. ## Differences from the TypeScript SDK ### Every page of `tools/list` is read When a `tools/call` arrives before a `tools/list` (under PHP-FPM, that is every call), AgentCat lists the server's tools again to learn their schemas. The TypeScript SDK reads only the first page there, so a tool on a later page falls back to a guess that can remove the tool's own `session_id` or `context` argument. The PHP SDK reads the pages until it finds the called tool. It stops at the last page, at a cursor it has seen before, after 100 pages, or after 5 seconds. It lists `get_more_tools` once, on the last page. There is one exception: a server that registers its own `get_more_tools` on a page before the last. AgentCat answers the calls to that tool. A process that has not read your tool's page cannot know your tool, so it lists AgentCat's `get_more_tools` on the last page too, and a client sees two tools of that name. Under PHP-FPM and the other per-request runtimes, that is every request. A long-running process that has read your tool's page lists only yours. Rename your tool: AgentCat logs a warning when it finds one. `laravel/mcp` puts 15 tools on a page, so this matters for every Laravel server with 16 or more tools. It costs time only for a tool on a later page. Under PHP-FPM, a call to the last-page tool of a `laravel/mcp` server with 300 tools takes 57.6 ms instead of 2.7 ms; a call to a tool on the first page costs the same as before. A `tools/list` handler of your own (one that you add to the server, or a `laravel/mcp` class that answers `tools/list`) runs once for each page that AgentCat reads during a `tools/call` under PHP-FPM, so its queries, metrics and log lines repeat that many times. ### Hooks run after the response, with no time limit The TypeScript SDK runs your hooks (`identify`, `resolveSessionId`, `eventTags`, `eventProperties`) while your tool runs, and leaves out a hook that does not finish within 30 seconds. The PHP SDK runs them after the client has its answer, so they never add latency. Synchronous PHP cannot interrupt a hook: there is no 30-second limit, and the event waits until each hook returns. In the same way, the new listing of the tools (above) checks its 5 seconds between pages, but it cannot interrupt a `tools/list` handler that hangs. A slow hook makes the end of a request or a process slower: see [the 5-second budget](/sdk/php/runtimes). On a stdio server, the hooks of a call run when the next call starts, so a slow hook delays the next call. ### A `context` that is not a string When an agent sends a number, a boolean or an object as the `context` argument, AgentCat drops the event before any hook runs, and logs `Failed to truncate event: UnexpectedValueException: userIntent is not a string ()`. The TypeScript SDK runs the hooks first, then drops the event. A list as `context` is sent with no intent; the TypeScript SDK sends the list. ### Errors in custom events The `error` of a custom event is sent as you pass it. A `\Throwable` arrives as an empty object, `{}`, as an `Error` does in the TypeScript SDK. Pass the details you want: ```php theme={null} use AgentCat\AgentCat; use AgentCat\CustomEvent; use App\Billing\Payments; try { Payments::chargeOpenInvoices(); } catch (\RuntimeException $e) { AgentCat::publishCustomEvent($builder, 'proj_YOUR_PROJECT_ID', new CustomEvent( resourceName: 'payment_failed', isError: true, error: ['message' => $e->getMessage(), 'type' => $e::class], )); } ``` A first argument of `publishCustomEvent()` that is neither an object nor a string is a PHP `TypeError` (with `declare(strict_types=1)`), not the TypeScript SDK's error message. ### The project ID `AgentCat::track($server, null)` reads the project ID from `AGENTCAT_PROJECT_ID`. `''` sends the events to no AgentCat project, only to your exporters. In the TypeScript SDK, `null` means no project. ### The client of a per-request server Under PHP-FPM and the other per-request runtimes, an `mcp/sdk` server with a session store keeps the client name and version of the `initialize` handshake for the later calls of that session. The TypeScript SDK reports none there. ### Tools that ask for more input A tool that asks the client for more input is called again for each round, and each round is its own event, with both versions of the protocol. On the handshake protocol, the TypeScript SDK records only the final result. ### Additions of the PHP SDK * `Options(disableSignalHandlers: true)` turns off AgentCat's SIGTERM and SIGINT handlers. The TypeScript SDK has no such switch. * `AgentCat::flush()` and `AgentCat::shutdown()` deliver on demand. They are rarely needed. * `AgentCat::fake()` records the events of your tests. * AgentCat sends through ext-curl. It uses the proxy settings that libcurl reads (`HTTPS_PROXY`, `NO_PROXY`), and it does not follow redirects. ## What the PHP SDK sends ### Request headers The event of a call over HTTP carries every request header of that call, verbatim, `Authorization` and `Cookie` included, under `parameters.extra.requestInfo.headers`. The TypeScript SDK does the same. On `mcp/sdk` 0.9 and later, when the server checked an OAuth access token for the request, the event also carries that token's scopes and claims, under `parameters.extra.authInfo`. To keep a header or the token's details out of your events, remove them in a `redactEvent` hook: ```php theme={null} use AgentCat\AgentCat; use AgentCat\Event; use AgentCat\Options; AgentCat::track($builder, 'proj_YOUR_PROJECT_ID', new Options( redactEvent: static function (Event $event): Event { $headers = $event->parameters->extra->requestInfo->headers ?? null; if ($headers instanceof \stdClass) { unset($headers->authorization, $headers->cookie); } $extra = $event->parameters->extra ?? null; if ($extra instanceof \stdClass) { unset($extra->authInfo); } return $event; }, )); ``` ### Diagnostics To find broken installs, AgentCat sends its own log lines to AgentCat (`https://otel.agentcat.com`): every line it logs, with its time and level, also when a PSR-3 logger or a log channel of yours gets the lines instead of `agentcat.log`. These lines can contain: * session IDs and actor IDs: AgentCat logs one line for each event it sends, in the form `Successfully sent event | | session | | ms | `; * tool names, and error messages, which hold whatever the code that threw put in them; * the answers of exporters that failed, quoted. Each batch also carries your project ID (without one, an install ID: a hash of the host name and the install path), the OS type and version, the architecture, the versions of PHP, of the SDK and of `mcp/sdk`, the process ID and `APP_ENV`. The lines that AgentCat logs at the very end of a request or process, after it has posted its last diagnostics, never reach diagnostics: for example the `Successfully sent event` lines of the events that this last pass sends, the outcomes of the exports in it, and `Shutting down with events still in queue`. Diagnostics are on by default, and off while your tests run. To turn them off: * `new Options(disableDiagnostics: true)` on the first `track()` of the process, or `disable_diagnostics` in the Laravel or Symfony configuration; * or the environment variable `AGENTCAT_DISABLE_DIAGNOSTICS=true`. The TypeScript name `DISABLE_DIAGNOSTICS=true` works too, but only to turn them off; when the two disagree, off wins. `AGENTCAT_DIAGNOSTICS_ENDPOINT` and `AGENTCAT_DIAGNOSTICS_TOKEN` send them to your own OpenTelemetry collector instead. The PHP SDK does not read the unprefixed `DIAGNOSTICS_ENDPOINT` and `DIAGNOSTICS_TOKEN` of the TypeScript SDK, so your application's own variables of those names never reach AgentCat. ## Exporters The exporters behave as in the TypeScript SDK: * **Retries.** When the AgentCat API fails and AgentCat retries, it sends the event to your exporters again on each attempt, so an exporter can get up to 4 copies. * **OTLP and fractional durations.** An OTLP export of an event whose duration is not a whole number (a custom event with `duration: 1.5`, for example) sends nothing and logs `Telemetry export failed for : OTLP export error: RangeError: The number 1.5 cannot be converted to a BigInt because it is not an integer`. * **Sentry.** An event's timestamp is the end of its transaction, which starts `duration` earlier. `environment` and `release` are tags of transactions and error events. * **PostHog** gets the `parameters` and the `response` of each call. * **One set per process.** The first `track()` of a process that passes exporters sets them up for every server. A later `track()` with other exporters logs `WARN: AgentCat exporters were set up by an earlier track(); the exporters of this call are ignored`. ## Known limits * **Session IDs.** AgentCat accepts a `session_id` that has the shape of an AgentCat session ID. It does not check that AgentCat issued it. * **Your own shutdown functions.** AgentCat delivers in a shutdown function that runs after yours. A shutdown function of yours that calls `exit()` ends the shutdown before that: the waiting events are lost. * **Long names.** AgentCat cuts tool names, server names and versions at 256 characters plus `...`, as the TypeScript SDK does. The AgentCat API keeps at most 255 characters for a name and 50 for a version, so it drops such events. * **Forks.** A process that forks after its first event copies the waiting events into the child, so they can be sent twice. * **Runtimes.** [PHP runtimes](/sdk/php/runtimes) lists the limits of each runtime: sessions and time limits under PHP-FPM, responses without `Content-Length` on Apache, signals in containers, async runtimes, and hardened hosts. # PHP: mcp/sdk Source: https://docs.agentcat.com/sdk/php/mcp-sdk Track a plain mcp/sdk server in PHP: stdio and HTTP servers, your own request handlers, signals, custom events and tests. This page covers a server built with the official PHP MCP library, [`mcp/sdk`](https://github.com/modelcontextprotocol/php-sdk), without a framework bridge. For a Laravel or Symfony application, see [Laravel](/sdk/php/laravel) and [Symfony](/sdk/php/symfony): there you write no `track()` call. ## Requirements * PHP 8.3, 8.4 or 8.5. * `mcp/sdk` 0.8.1 or later. On every pull request and every night, CI tests AgentCat with the newest patch of each `mcp/sdk` minor release from 0.8.1 up. * ext-curl, to send the events. ext-pcntl is recommended for stdio servers: without it, the events that wait when the process gets SIGTERM or SIGINT are lost (see [Signals](#signals)). ```bash theme={null} composer require agentcat/sdk mcp/sdk ``` ## Track the server Pass the `Mcp\Server\Builder`, or the `Mcp\Server` built from it, to `AgentCat::track()`. The place of the call does not matter: it can come before or after your tools, your request handlers and `build()`. ```php theme={null} setServerInfo('echo-mcp', '0.1.0') ->addTool(static fn(string $msg): string => $msg, 'echo', description: 'Echo a message'); // Track the server with AgentCat AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); $builder->build()->run(new StdioTransport()); ``` The built server works the same way: ```php theme={null} use AgentCat\AgentCat; use Mcp\Server; use Mcp\Server\Transport\StdioTransport; $server = Server::builder() ->setServerInfo('echo-mcp', '0.1.0') ->addTool(static fn(string $msg): string => $msg, 'echo', description: 'Echo a message') ->build(); AgentCat::track($server, 'proj_YOUR_PROJECT_ID'); $server->run(new StdioTransport()); ``` * **The project ID.** `null` reads it from the `AGENTCAT_PROJECT_ID` environment variable. `''` sends the events to no AgentCat project, only to your [exporters](/integrations). * **Options.** The third argument is an `AgentCat\Options` object with named arguments, for example `new Options(identify: …, enableAgentTracking: true)`. The [SDK pages](/sdk/identifying-users) show each option with a PHP tab. * **It never throws.** When AgentCat cannot track the server, it logs why and returns the Builder or server unchanged. The server then answers as it would without AgentCat. * **Tracking again changes nothing.** `track()` again on the same Builder or server, or on the Builder and then the `Mcp\Server` built from it (or the reverse), logs `[SESSION DEBUG] track() - Server already being tracked, skipping initialization` and returns it. * **After `build()`, three kinds of calls give no event.** A call to a tool name that the server does not have, a call whose arguments fail the tool's input schema, and a call that a `tools/call` handler of your own answers. `mcp/sdk` answers them before any tool runs, as it does without AgentCat. Tracked before `build()`, the server gives an event for each of them. * **A server with a reference handler of your own is tracked only before `build()`.** With `setReferenceHandler()`, AgentCat's tool handler would run inside yours. After `build()`, AgentCat logs `… the mcp/sdk server has a reference handler of its own (…): track the Builder before build(). …` and leaves the server untracked. Under PHP-FPM, each request runs your script, so each request builds and tracks a new Builder, and that costs no more than tracking one long-lived server. Within one process, `track()` for each server is cheap: the event queue, the exporters and the diagnostics are set up once and shared. ## HTTP servers An HTTP server tracks the Builder in the same way. This script answers one request with `StreamableHttpTransport`, under PHP-FPM, FrankenPHP or Apache: ```php theme={null} setServerInfo('echo-mcp', '0.1.0') ->addTool(static fn(string $msg): string => $msg, 'echo', description: 'Echo a message'); AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); $factory = new Psr17Factory(); $response = $builder->build()->run(new StreamableHttpTransport($factory->createServerRequestFromGlobals(), $factory, $factory)); $body = (string) $response->getBody(); http_response_code($response->getStatusCode()); foreach ($response->getHeaders() as $name => $values) { foreach ($values as $value) { header($name . ': ' . $value, false); } } // Apache mod_php, php-cgi and php -S: with its length, the client has the whole answer before AgentCat delivers. header('Content-Length: ' . strlen($body)); echo $body; ``` AgentCat runs your hooks and sends the events after the client has its answer. How it does that depends on the runtime: see [PHP runtimes](/sdk/php/runtimes). Without the `Content-Length` line, a client on Apache mod\_php, php-cgi or `php -S` waits until AgentCat has delivered, up to 5 seconds. ### CLI-hosted HTTP servers record no headers AgentCat reads the HTTP request of a plain `mcp/sdk` call from PHP's request globals (`$_SERVER`). An HTTP server that runs as one long CLI process (ReactPHP, AMPHP, a `stream_socket_server()` loop) has no request there. Its events carry no request headers and no URL, and `ToolCall::$headers` is empty in your hooks. The [Laravel](/sdk/php/laravel) and [Symfony](/sdk/php/symfony) bridges read the request of their framework instead, also in worker runtimes. ## Your own request handlers A `tools/call` or `tools/list` handler that you add with `addRequestHandler()` answers as usual, whether you add it before or after `track()`. AgentCat's own handlers come first and delegate to yours: your handler answers, its listing gets AgentCat's parameters, and AgentCat records one event per call. On a server tracked after `build()`, your `tools/call` handler answers without AgentCat, as the list above says. A reference handler that you set with `setReferenceHandler()`, before or after `track()`, runs your tools, and the event of a failing tool keeps its exception. With one, track the Builder before `build()`. ## Stateless endpoints and `buildStateless()` `Builder::buildStateless()` returns an `Mcp\Server\Stateless\StatelessProtocol`, not an `Mcp\Server`. Track the Builder, before or after you call it. For a custom event, pass the Builder (or an `Mcp\Server` built from it) to `AgentCat::publishCustomEvent()`, not the `StatelessProtocol`. AgentCat cannot map that object to its Builder and throws: ```text theme={null} InvalidArgumentException: Server is not tracked: pass the Mcp\Server\Builder (or the Mcp\Server built from it) to AgentCat::publishCustomEvent(); Builder::buildStateless() returns a StatelessProtocol, which AgentCat cannot map to its Builder ``` ## Custom events from a tool A tool can publish a [custom event](/sdk/custom-events) for its server. Give the tool the Builder: ```php theme={null} use AgentCat\AgentCat; use AgentCat\CustomEvent; use Mcp\Server; $builder = Server::builder()->setServerInfo('billing-mcp', '1.0.0'); $builder->addTool(static function (string $plan) use ($builder): string { AgentCat::publishCustomEvent($builder, 'proj_YOUR_PROJECT_ID', new CustomEvent( resourceName: 'subscription_upgraded', parameters: ['plan' => $plan], )); return 'Upgraded to ' . $plan; }, 'upgrade', description: 'Upgrade the subscription'); AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); ``` `publishCustomEvent()` queues the event and returns nothing. AgentCat sends it with the other events. The project ID is required, but for a tracked server the event goes to the project the server was tracked with. ## Signals A stdio server runs until its client closes the input or a signal stops it. With ext-pcntl, AgentCat installs handlers for SIGTERM and SIGINT when it gets its first event, or already in `track()` while diagnostics are on. At one of these signals, AgentCat sends the waiting events (for at most 5 seconds). Then it calls the handler that the signal had before, or it ends the process as the signal would have. * **Async signals for the whole process.** When AgentCat installs a handler, it calls `pcntl_async_signals(true)`. From then on, every PHP signal handler of the process runs between two opcodes, not at `pcntl_signal_dispatch()` or at a tick. * **A signal that you ignore stays ignored**, when you ignore it in PHP with `pcntl_signal(…, SIG_IGN)` before `track()`. * **An ignore that the process inherited is not visible to PHP.** A parent process (a shell, `nohup`, a supervisor) can start PHP with SIGINT or SIGTERM ignored. PHP cannot see that, so AgentCat installs its handler for the signal, and the signal then ends the process after the delivery. To keep ignoring it, ignore it again in PHP, before `track()`: ```php theme={null} use AgentCat\AgentCat; // Keep ignoring SIGINT: PHP cannot see an ignore inherited from the parent process. pcntl_signal(SIGINT, SIG_IGN); AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); ``` * **Turn the handlers off** with `disableSignalHandlers: true`. A signal then ends the process as it did before, without the last delivery. The switch is process-wide and applies only until AgentCat installs its handlers, so set it on the first `track()` of the process. A later `track()` with the switch logs `WARN: AgentCat signal handlers are already installed; disableSignalHandlers is ignored`. ```php theme={null} use AgentCat\AgentCat; use AgentCat\Options; AgentCat::track($builder, 'proj_YOUR_PROJECT_ID', new Options(disableSignalHandlers: true)); ``` Without ext-pcntl, or on a host that disables its functions, AgentCat installs nothing and logs one line (not on Windows, which has no ext-pcntl). [PHP runtimes](/sdk/php/runtimes#the-cli-and-containers) covers stdio delivery, containers, the Docker images without ext-pcntl, and Windows. ## Testing AgentCat does nothing while your tests run: under PHPUnit or Pest, or when `APP_ENV` is `testing` or `test`. `track()` then tracks nothing, and `publishCustomEvent()` checks the project ID and returns. To assert on the events of a test, call `AgentCat::fake()` first, before the test tracks its server: ```php theme={null} setServerInfo('echo-mcp', '0.1.0') ->addTool(static fn(string $msg): string => $msg, 'echo', description: 'Echo a message'); AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); $builder->build()->run(new InMemoryTransport([ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}', '{"jsonrpc":"2.0","method":"notifications/initialized"}', '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"msg":"hi"}}}', ])); $fake->assertTrackedCount(1); $fake->assertTracked(static fn(Event $event): bool => $event->resourceName === 'echo'); } } ``` The fake records every event after your hooks, redaction and truncation, and sends nothing. It logs to its own `logs()` list, not to `~/agentcat.log`. Each method counts different events: | Method | What it checks | | - | - | | `assertTracked(?Closure $filter = null)` | At least one tool call (`mcp:tools/call`) was recorded, and one matches the filter when you give one. Custom events do not count. | | `assertTrackedCount(int $count)` | Exactly `$count` tool calls were recorded. Custom events do not count. | | `assertNothingTracked()` | No event of any type was recorded, custom events included. | | `assertCustomEventPublished(?Closure $filter = null)` | At least one custom event (`agentcat:custom`) was recorded, and one matches the filter. | | `events()` | Every recorded event, in order (`list`). | | `logs()` | The lines AgentCat logged while the fake was the shared client, hook errors included. | `events()`, `logs()` and the assertions first run the hooks that still wait. The assertions use PHPUnit when it is loaded, and throw `AssertionError` otherwise. * **Call `fake()` before `track()`.** In a test environment, a server tracked before `fake()` is not tracked at all, and a failing assertion says so. * **Restore the real client** with `AgentCat::setClient(null)` in `tearDown()`: the fake is the shared client of the PHP process until then. * **`AGENTCAT_ENABLED=true` turns AgentCat on in tests**, with real delivery. A server tracked before `fake()` then keeps the client that tracked it. `AGENTCAT_ENABLED=false` turns AgentCat off everywhere; the fake still records. ## Rarely needed: `flush()` and `shutdown()` AgentCat delivers on its own, between requests and when the process ends, so most servers never call these. * `AgentCat::flush(?float $timeoutSeconds = null): bool` sends the waiting events and exporter requests and waits for them. It returns `true` when nothing is left to send, and `false` when the timeout ended first. It does not wait for AgentCat's own diagnostics. A raw worker loop calls it after each request: see [PHP runtimes](/sdk/php/runtimes). * `AgentCat::shutdown(): void` stops accepting events, then sends the waiting ones for at most 5 seconds. Events after it are dropped. # PHP runtimes Source: https://docs.agentcat.com/sdk/php/runtimes When the PHP SDK runs your hooks and sends its events on PHP-FPM, FrankenPHP, Apache, NGINX Unit, the CLI, containers, Swoole, RoadRunner, Workerman and hardened hosts. AgentCat runs your hooks and sends the events after the client has its answer, wherever the runtime allows it. This page says how each PHP runtime does it, and what you must do, if anything. There are two exceptions: on Apache mod\_php, php-cgi and `php -S`, a client whose answer has no `Content-Length` waits for the delivery (see the Apache section), and a stdio server delivers the events of a call when the next call arrives, so the client of that next call waits for it (see the CLI section). ## Overview | Runtime | When AgentCat runs the hooks and sends the events | What you do | | - | - | - | | PHP-FPM | At the end of the script, after `fastcgi_finish_request()` has sent the response | Nothing | | NGINX Unit | Like PHP-FPM | Nothing | | FrankenPHP, classic mode | At the end of the script, after `frankenphp_finish_request()` | Nothing | | FrankenPHP, worker mode | Laravel Octane and Symfony: after each request. A raw worker loop: when you call `AgentCat::flush()` | A raw loop calls `AgentCat::flush(1.0)` after each request | | Apache mod\_php, php-cgi, `php -S` | At the end of the script. The client has its answer first when the answer has a `Content-Length` | A plain `mcp/sdk` script sets `Content-Length`. The Laravel and Symfony bridges set it for you | | The CLI (stdio servers) | When the next `tools/call` or `tools/list` request arrives, and when the input ends | Install ext-pcntl; in a container, use `docker run --init` | | Swoole, OpenSwoole, RoadRunner, Workerman | Laravel Octane and Symfony Runtime: after each request. A raw loop: when you call `AgentCat::flush()` | A raw loop calls `AgentCat::flush(1.0)` after each request | AgentCat needs ext-curl, ext-json and ext-mbstring. ext-pcntl is optional and recommended for CLI servers. ## The 5-second budget When a script or a process ends, AgentCat runs the hooks that still wait and sends the waiting events, for at most 5 seconds in all. Events that are still waiting when the time is up are lost, and AgentCat logs `Shutting down with events still in queue`. AgentCat cannot stop a hook that runs: synchronous PHP cannot interrupt it. A slow hook, or a hook that waits for a slow service, therefore makes the end of a request or a process last longer than 5 seconds. Keep hooks fast. ## PHP-FPM `fastcgi_finish_request()` sends the response to the client. Then, in its shutdown function, AgentCat runs the hooks and sends the events. The FPM worker stays busy during that time, up to the 5-second budget: count it when you size the pool. * **The session lock.** On every web runtime, before it delivers, AgentCat writes and closes an active PHP session (`session_write_close()`), so the next request of the same client does not wait for the lock. A destructor that writes to `$_SESSION` after that loses its write. * **The time limit.** On every web runtime, when `max_execution_time` is set, AgentCat gives its delivery its own limit with `set_time_limit(6)`, so a request that used most of its time does not end the delivery with a fatal error. That call can also shorten a longer limit for code that runs after AgentCat's shutdown function. * **`request_terminate_timeout`.** This limit of the FPM pool counts wall-clock time, and PHP code cannot change it. When it is shorter than your slowest request plus the delivery, FPM kills the worker during the delivery, and the events of that request are lost. Leave at least 6 seconds plus the time your hooks take over your slowest request. * **Diagnostics.** AgentCat sends its diagnostics after the response, within the budget. Each diagnostics request has a 250 ms connect timeout and a 1-second total timeout, so a collector that does not answer holds the worker only briefly. * **A host that disables `fastcgi_finish_request()`** gets the behavior of Apache mod\_php (below): the response goes out at the end of the script. ## FrankenPHP **Classic mode** works like PHP-FPM, with `frankenphp_finish_request()`. **Worker mode** keeps the script alive between requests, so shutdown functions do not run after each request. Laravel Octane and Symfony Runtime need nothing: AgentCat delivers in their terminate hooks and waits at most 1 second after each request. A raw `frankenphp_handle_request()` loop must call `AgentCat::flush()` after each request: ```php theme={null} setServerInfo('worker-mcp', '1.0.0') ->addTool(static fn(string $msg): string => $msg, 'echo', description: 'Echo a message'); AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); $factory = new Psr17Factory(); $response = $builder->build()->run(new StreamableHttpTransport($factory->createServerRequestFromGlobals(), $factory, $factory)); http_response_code($response->getStatusCode()); foreach ($response->getHeaders() as $name => $values) { header($name . ': ' . implode(', ', $values)); } echo $response->getBody(); }; while (frankenphp_handle_request($handler)) { AgentCat::flush(1.0); // after each response: deliver this request's events, waiting at most 1 second } ``` Copy the `while` loop exactly: the `AgentCat::flush(1.0)` call after `frankenphp_handle_request()` is what delivers each request's events. Events that are still waiting after the 1 second go out after a later request, or when the worker stops. * **Worker detection.** AgentCat detects worker mode from the `FRANKENPHP_WORKER` environment variable that FrankenPHP sets. Inside a request, `$_SERVER` does not have it, so keep the variable in the worker's environment. * **Errors after the response.** The official FrankenPHP Docker image has no `php.ini`, so `log_errors` is off, and a fatal error after the response (in a hook, for example) leaves no trace. Turn it on in the Caddyfile, or ship a `php.ini`: ```caddyfile theme={null} { frankenphp { php_ini log_errors 1 } } ``` ## Apache mod\_php, php-cgi and `php -S` These runtimes have no function that sends the response before the script ends. So at the start of its last shutdown pass, AgentCat sends and closes the output buffers that can be closed, and flushes them. A client that knows the length of the answer (`Content-Length`) then has all of it before AgentCat runs the hooks and delivers. A client of an answer without a length waits until the script ends, up to the 5-second budget. * **The Laravel and Symfony bridges set `Content-Length`** on the plain JSON answers of a tracked server. They never set it on a streamed (SSE) answer, on an answer without a body (1xx, 204, 304) or to a `HEAD` request, on an answer that has a `Content-Length` or `Transfer-Encoding` already, or after other output went out first. * **A plain `mcp/sdk` script sets it itself**, before it echoes the answer: `header('Content-Length: ' . strlen($body));` (see the [HTTP server](/sdk/php/mcp-sdk#http-servers) of the `mcp/sdk` page). * **Headers after that point.** A destructor that sets a header after AgentCat closed the buffers gets a "headers already sent" warning. * **`zlib.output_compression`.** AgentCat closes the compression buffer with the others. Output written after that, for example an `echo` in a destructor, is not part of the compressed body and breaks it. * **Warnings.** Under Laravel and Symfony, the framework's error handler catches PHP's warnings, so they reach the framework's log, not Apache's error log. ## NGINX Unit NGINX Unit is supported like PHP-FPM, and you need nothing. Its PHP module reports the SAPI of `php -S` (`cli-server`); AgentCat recognizes Unit by its `SERVER_SOFTWARE` (`Unit/`) and sends the response with Unit's `fastcgi_finish_request()` before it delivers. ## The CLI and containers A stdio server has no request end. AgentCat runs the hooks and sends the events of a call when the next `tools/call` or `tools/list` request arrives, and when the input ends (see [differences and limits](/sdk/php/limits) for what that means for slow hooks). AgentCat writes nothing to STDOUT or STDERR: STDOUT stays the JSON-RPC stream. * **Signals.** With ext-pcntl, AgentCat sends the waiting events at SIGTERM and SIGINT. The [`mcp/sdk` page](/sdk/php/mcp-sdk#signals) explains the handlers, inherited ignores and `disableSignalHandlers`. The official PHP Docker images have no ext-pcntl: add `RUN docker-php-ext-install pcntl` to your image. * **PID 1.** In a container, the process with PID 1 ignores a signal whose handler is the default one. After its delivery, AgentCat gives the signal back to that default handler, which then does not end the process. Start the server with `docker run --init` (or `init: true` in Docker Compose), as production images should, so that an init process is PID 1. * **Thread-safe (ZTS) builds**, such as the official `php:*-zts` images: a second SIGTERM or Ctrl+C during the delivery waits until the delivery ends, up to the 5-second budget. * **Windows** has no ext-pcntl, so the events that wait at Ctrl+C are lost. AgentCat logs no line for that. * **Symfony's `bin/console mcp:server`** ends with exit code 0 after the delivery at SIGTERM, because Symfony Console handles the signal itself, not 143. ## Swoole, OpenSwoole, RoadRunner and Workerman * **With Laravel Octane or Symfony Runtime**, AgentCat delivers in the framework's terminate hooks: Laravel's `terminating()` callbacks and Symfony's `kernel.terminate`. It waits at most 1 second after each request. Events that are still waiting go out at a later request of the worker, or when the worker stops. See [Laravel](/sdk/php/laravel) for Octane's worker stop. * **A raw worker loop** calls `AgentCat::flush(1.0)` after each request, as in the [FrankenPHP loop](#frankenphp). * **No signal handlers under Swoole and OpenSwoole.** When ext-swoole or ext-openswoole is loaded, AgentCat installs no SIGTERM or SIGINT handlers: those servers stop their workers with handlers that PHP cannot see. The check looks at the loaded extension, so a stdio server on a PHP with ext-swoole loaded gets no delivery at those signals either. * **RoadRunner and Workerman** install PHP signal handlers. AgentCat keeps them: after its delivery, it calls the handler that was there before. * **No async runtimes.** Version 1 does not support async runtimes that interleave calls in one process, such as Swoole coroutines or ReactPHP. Serve one request at a time per worker. ## Hardened hosts Shared hosts often list functions in `disable_functions`. AgentCat keeps your server answering whatever they disable. Where it loses a feature, it logs one line. "Once per process" means once per request under PHP-FPM, mod\_php and php-cgi, because PHP keeps no state between their requests. | Disabled | What AgentCat does | Log line (once per process) | | - | - | - | | Any of the ext-curl functions AgentCat calls: `curl_init`, `curl_setopt_array`, `curl_getinfo`, `curl_error`, `curl_strerror`, `curl_multi_init`, `curl_multi_add_handle`, `curl_multi_remove_handle`, `curl_multi_exec`, `curl_multi_select`, `curl_multi_info_read`, `curl_multi_strerror` | Sends nothing: no events, no exports, no diagnostics. A host that disables `curl_multi_exec` loses every event. (`curl_exec` is not needed.) | `WARN: AgentCat cannot send events, telemetry exports or diagnostics: ext-curl functions are missing or disabled ()` | | `pcntl_signal`, `pcntl_signal_get_handler` or `pcntl_async_signals` | No delivery at SIGTERM or SIGINT | `WARN: AgentCat cannot send waiting events at SIGTERM or SIGINT: ext-pcntl functions are disabled (); remove them from disable_functions` | | ext-pcntl not installed (CLI only; not on Windows, not under Swoole or OpenSwoole, not with `disableSignalHandlers`) | No delivery at SIGTERM or SIGINT | `WARN: AgentCat cannot send waiting events at SIGTERM or SIGINT: ext-pcntl is not loaded; install ext-pcntl` | | `register_shutdown_function` | Events still go out between requests and at `AgentCat::flush()`, but those that wait when the script ends are lost | `WARN: AgentCat cannot send waiting events when the script ends: register_shutdown_function is disabled` | | `posix_kill` or `posix_getpid` | After the delivery at a signal, ends the process with `exit(128 + )` | None | | `getenv` | Reads the environment from `$_ENV`, then `$_SERVER` | None | | `hrtime` | Uses `microtime()` | None | | `usleep` | Waits for a retry with `time_nanosleep()` | None | | `usleep` and `time_nanosleep` | A delivery ends at the first retry that is not due yet. An event whose first attempt failed goes out only if a later delivery retries it, so at the end of a request it is lost | `WARN: AgentCat cannot wait for the retry of a failed event: usleep and time_nanosleep are disabled; a delivery ends at the first retry that is not due yet; remove one of them from disable_functions` | | `ignore_user_abort`, `set_time_limit`, `ini_set`, `ini_get` | Skips that step of the request end; exporter bodies use the `serialize_precision` of `php.ini` | None | | `fastcgi_finish_request` | Sends the response at the end of the script, as on Apache mod\_php | None | | `session_status`, `session_write_close` | Does not close the session before it delivers | None | | `php_uname`, `getmypid`, `gethostname` | Leaves those values out of diagnostics | None | | `file_put_contents` | The log file stays empty; a PSR-3 logger still gets every line | None | A failure that repeats on every pass of one delivery is logged once, with `( more times in this delivery)` after it. What AgentCat needs: * **ext-curl, with the `curl_multi_*` functions**, to send anything. * **One of `hrtime()` and `microtime()`.** With both disabled, delivery fails. * **PHP's core string, array, math, JSON, PCRE, mbstring, hash and error-handler functions**, like every Composer package. No hosting list disables them. # PHP: Symfony Source: https://docs.agentcat.com/sdk/php/symfony Track every symfony/mcp-bundle server of a Symfony application: setup, the configuration reference, hooks as services and the logs. The Symfony bundle tracks every [`symfony/mcp-bundle`](https://github.com/symfony/mcp-bundle) server of your application with no code: you register the bundle and set your project ID. ## Requirements * PHP 8.3, 8.4 or 8.5. * `symfony/mcp-bundle` 0.13 or 0.14, with Symfony 7.3 or 8.x. On every pull request and every night, CI tests AgentCat with the newest patch of each `symfony/mcp-bundle` minor release from 0.13 up. * ext-curl, to send the events. `symfony/mcp-bundle` 0.14 requires `mcp/sdk` 0.8, so a Symfony application stays on `mcp/sdk` 0.8 until the bundle releases a version for `mcp/sdk` 0.9. ## Setup Install the package: ```bash theme={null} composer require agentcat/sdk ``` Register the bundle in `config/bundles.php`: ```php theme={null} return [ // ... AgentCat\Symfony\AgentCatBundle::class => ['all' => true], ]; ``` Then set your project ID in `.env.local`, or in the environment of your server: ```dotenv theme={null} AGENTCAT_PROJECT_ID=proj_YOUR_PROJECT_ID ``` That is all: every server of the `mcp.servers` configuration is tracked. AgentCat adds a `session_id` and a `context` parameter to each tool, removes them from the arguments before your tool runs, and adds the `get_more_tools` tool. It runs your hooks and sends the events on `kernel.terminate`, after the response. A `config/packages/agentcat.yaml` file is optional. It holds the settings below, under the `agentcat` key. ## Configuration reference `php bin/console config:dump-reference agentcat` prints every key with its default: ```yaml theme={null} # Default configuration for extension with alias: "agentcat" agentcat: # On or off for every MCP server and every AgentCat call: true or false. Unset: on, except while your tests run. enabled: '%env(default::AGENTCAT_ENABLED)%' # Your AgentCat project ID (proj_…). Unset: no events go to AgentCat; exporters still run. project_id: '%env(default::AGENTCAT_PROJECT_ID)%' # Servers AgentCat never tracks: names of mcp.servers entries; a string is one name. Names written here are read when the container compiles, names from environment variables when each server is built. exclude_servers: [] # Another project for some servers: mcp.servers name => project ID. A null project is project_id. projects: # Prototype server: ~ # The service ID of a PSR-3 logger for AgentCat's own lines, for example monolog.logger.mcp, written here, not in an environment variable. Unset: their own file, agentcat.log in the log directory. logger: null # The AgentCat API. Unset: AgentCat's. MCPCAT_API_URL, the TypeScript SDK's name, is read too. api_url: '%env(default::AGENTCAT_API_URL)%' # Adds the get_more_tools tool, so agents can report tools they miss. Default: true. Takes true, false, 1, 0, yes, no, on, off or an %env()% value. enable_report_missing: true # Publishes one event for every tool call. Default: true. Takes true, false, 1, 0, yes, no, on, off or an %env()% value. enable_tracing: true # Adds a context parameter to your tools, so agents explain each call. Default: true. Takes true, false, 1, 0, yes, no, on, off or an %env()% value. enable_tool_call_context: true # Replaces the default description of the context parameter. Unset: the default description. custom_context_description: null # Adds an agent_id parameter to your tools, to tell agents apart. Default: false. Takes true, false, 1, 0, yes, no, on, off or an %env()% value. enable_agent_tracking: false # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs. identify: null # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs. resolve_session_id: null # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs. event_tags: null # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs. event_properties: null # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs. redact_sensitive_information: null # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs. redact_event: null # Your own telemetry tools: name => {type: otlp|datadog|sentry|posthog, …}. A ~ entry is skipped. exporters: null # Diagnostics: AgentCat's own log lines, sent to AgentCat so that it can find broken installs. true turns them off; in a list, one true value is enough. DISABLE_DIAGNOSTICS, the TypeScript SDK's name, is read too. disable_diagnostics: # Defaults: - %env(default::AGENTCAT_DISABLE_DIAGNOSTICS)% - %env(default::DISABLE_DIAGNOSTICS)% # Where diagnostics go: an OpenTelemetry collector (OTLP/HTTP logs). Unset: AgentCat's. diagnostics_endpoint: '%env(default::AGENTCAT_DIAGNOSTICS_ENDPOINT)%' # The bearer token of diagnostics_endpoint. Unset: AgentCat's. diagnostics_token: '%env(default::AGENTCAT_DIAGNOSTICS_TOKEN)%' # Keeps AgentCat from installing its SIGTERM and SIGINT handlers in CLI servers. Default: false. Takes true, false, 1, 0, yes, no, on, off or an %env()% value. disable_signal_handlers: false ``` A value of the wrong type logs one warning line, at the first server that AgentCat tracks, and AgentCat uses the default. ### Write `%env(default::…)%` for every AgentCat value Take each AgentCat value from the environment with the `default::` processor, as the defaults above do: ```yaml theme={null} agentcat: projects: billing: '%env(default::AGENTCAT_BILLING_PROJECT_ID)%' ``` > [!WARNING] > > A bare `%env(NAME)%` whose variable is not set makes the whole `agentcat` configuration unreadable. AgentCat then tracks no server at all: every server answers without AgentCat, and AgentCat logs one line for each server. With `default::`, an unset variable is `null`, which means the default. ### `exclude_servers` and `projects` Both use the names of the `mcp.servers` entries of your `symfony/mcp-bundle` configuration: ```yaml theme={null} agentcat: exclude_servers: [internal] projects: billing: '%env(default::AGENTCAT_BILLING_PROJECT_ID)%' ``` A server that `exclude_servers` names in the configuration itself is left out when the container compiles: AgentCat never touches it. A name that comes from an environment variable is read when the server is built. A `null` project is `project_id`. ### Hooks are invokable services A hook is the service ID of an invokable service, written in the configuration (not in an environment variable). AgentCat gets the service when the hook runs, and calls it with the same argument as the [PHP hook](/sdk/identifying-users): an `AgentCat\ToolCall` for `identify`, `resolve_session_id`, `event_tags` and `event_properties`, a string for `redact_sensitive_information`, and an `AgentCat\Event` for `redact_event`. ```php theme={null} namespace App\Mcp; use AgentCat\Identity; use AgentCat\ToolCall; final class IdentifyActor { public function __invoke(ToolCall $call): ?Identity { $token = $call->headers['authorization'] ?? null; return $token === null ? null : new Identity(userId: hash('sha256', $token)); } } ``` ```yaml theme={null} agentcat: identify: App\Mcp\IdentifyActor ``` With autowiring, the class name is the service ID. A missing service never breaks the container: AgentCat logs one line when the hook runs, and the tool call is not affected. **Read the request from the `ToolCall`, not from the `RequestStack`.** AgentCat runs the hooks after the response, on `kernel.terminate` or in its shutdown function, where the `RequestStack` is empty. `$call->headers` holds the headers of the call, with lower-case names. ## The log file AgentCat writes its own lines to `%kernel.logs_dir%/agentcat.log` (for example `var/log/agentcat.log`), not to the `mcp` logger of the bundle. Under PHP-FPM, each request builds the container and tracks the servers again, so the setup lines repeat on every request; in their own file, they stay out of your application's log and its error trackers. * **To route them elsewhere**, set `logger` to the service ID of a PSR-3 logger, for example `monolog.logger.mcp`. An ID that is not a PSR-3 logger logs `WARN: AgentCat option "agentcat.logger" must be the ID of a PSR-3 logger service, got ""; the default is used` to the default file. * **To give the `mcp` channel a file of its own**, with `symfony/mcp-bundle`'s lines (and AgentCat's, with `logger: monolog.logger.mcp`), add a Monolog handler for that channel: ```yaml theme={null} monolog: handlers: mcp: type: stream path: '%kernel.logs_dir%/mcp.log' channels: ['mcp'] ``` * **Lines written before any server is built** go to `~/agentcat.log`, for example the line of an `AgentCat::publishCustomEvent()` call in a request that serves no MCP call. ## When AgentCat tracks a server AgentCat tracks a server when the container builds its Builder: at an MCP request, in `bin/console mcp:server`, or in the debug tools. It never tracks at boot, so a console command or a worker that builds no MCP server gets no AgentCat setup, no diagnostics and no signal handlers. `bin/console list` builds no server. In debug mode, the profiler builds every server's Builder at the end of each request, and `bin/console debug:mcp` builds them all too. AgentCat then tracks them: you see the setup lines, and with diagnostics on, one diagnostics request, also for dev pages that serve no MCP call. ## Your own request handlers AgentCat's handlers come before every request handler of the server: the handlers that you tag `mcp.request_handler`, and the handlers that a compiler pass of your own adds with an `addRequestHandler()` call. AgentCat delegates to yours: your handler answers, and AgentCat records one event per call. ## After the response AgentCat delivers on `kernel.terminate`, in a listener that runs last (priority -2048): * **In Symfony Runtime workers** (RoadRunner, Swoole, FrankenPHP worker mode), it waits at most 1 second after each request, so the worker is soon free. Events still waiting go out at the next request of the worker, or when the worker stops. * **Under PHP-FPM, FrankenPHP's classic mode and the other per-request runtimes**, it only starts the delivery there. Its shutdown function finishes it after the response, within 5 seconds. AgentCat reads the headers of each call from the main request of the `RequestStack`, so a worker records the request it serves. `bin/console mcp:server` (stdio) has no request and records none. ### Responses on Apache mod\_php, php-cgi and `php -S` There, PHP sends a response only when the script ends, and `kernel.terminate` runs before the client has it. The bundle gives the plain JSON answers of a tracked server their `Content-Length`, so the client has the whole answer before `kernel.terminate` and AgentCat's delivery. [PHP runtimes](/sdk/php/runtimes) has the rules. ## Testing AgentCat is off while your tests run (PHPUnit, Pest, or `APP_ENV=test`). To assert on the events of a functional test, call `AgentCat::fake()` before the kernel builds a server, and restore the real client with `AgentCat::setClient(null)` in `tearDown()`. The [`mcp/sdk` page](/sdk/php/mcp-sdk#testing) says what each assertion of the fake counts. # Agent Feedback Source: https://docs.agentcat.com/sdk/report-missing-tools See what agents needed but could not find on your MCP server. When an AI agent can't find the right tool on your server, the user hits a dead end and you never hear about it. Agent feedback closes that loop. AgentCat gives the agent a way to say what it needed or what went wrong, so you can see the gaps in your server and how often agents hit them. This feature is enabled by default — no configuration needed. ## What You'll See ![agent feedback event in session replay](https://mintcdn.com/mcpcat/MGwu3r7iyFyBFWNA/images/missing-tool.png?fit=max\&auto=format\&n=MGwu3r7iyFyBFWNA\&q=85\&s=90085c2bea587d9438333637d0a47481) Agent feedback shows up in three places: **Sessions list** — The **Issues** column counts feedback notes next to errors. This gives you a quick sense of how often agents are hitting gaps. **Session Replay** — Feedback events appear in the timeline with a warning badge. Click one to see what the agent needed and the context behind the request. This is where you'll find the detail you need to decide what to build next. **Alerts** — The **Agent feedback** alert posts each note to Slack or email as it arrives, with a link back to the session. ## Disabling the Feature If you don't want AgentCat to add the `get_more_tools` tool to your server, disable it: ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { enableReportMissing: false }) ``` ```python Python theme={null} agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(enable_report_missing=False)) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ DisableReportMissing: true, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; AgentCat::track($builder, 'proj_abc123xyz', new Options( enableReportMissing: false, )); ``` # Stateless Servers Source: https://docs.agentcat.com/sdk/stateless-servers Set up AgentCat for a stateless MCP server for better horizontal scaling support. ## What are stateless MCP servers? Today many MCP servers use a stateful implementation where the client maintains a persistent connection. AgentCat's SDK can track a session across that connection naturally since it maintains a session ID when the client connects and associates all events with it. Stateless MCP servers don't maintain a session ID and each request is an independent HTTP call with no persistent connection. This is most commonly done to support horizontal scaling. AgentCat works for stateless MCP servers that pass an identity function. It uses the user's identity and client to group sessions. > [!NOTE] > > There are changes coming to the protocol and MCP SDKs to make servers stateless by default. For now, it's currently opt-in for all SDKs. ## Setup Pass the `stateless` option when tracking your server: ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_YOUR_PROJECT_ID", { stateless: true }); ``` ```python Python theme={null} agentcat.track(server, "proj_YOUR_PROJECT_ID", agentcat.AgentCatOptions(stateless=True)) ``` ```go Go theme={null} agentcat.Track(s, "proj_YOUR_PROJECT_ID", &agentcat.Options{ Stateless: true, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; // No stateless option: the same call works for every transport. AgentCat::track($builder, 'proj_YOUR_PROJECT_ID'); ``` This is the recommended approach — it's clear, reliable, and works with any transport or framework. The PHP SDK has no `stateless` option: on every transport, AgentCat adds a `session_id` parameter to each tool, which agents send back on each call, and groups the calls of a task by it. > [!NOTE] > > If you're using FastMCP with `stateless_http=True` and don't set the `stateless` option explicitly, the Python SDK will attempt to detect stateless mode automatically. You can explicitly force stateless mode off by setting `stateless=False`. ## How sessions are grouped When AgentCat receives events without a session ID, it groups them using three dimensions: **the identified user**, **the AI client** (e.g., Claude Desktop, Cursor), and **a 30-minute inactivity window**. If two events come from the same user via the same client within 30 minutes of each other, they belong to the same session. Different AI clients get separate sessions, even for the same user. If someone is interacting via both Claude Desktop and Cursor simultaneously, those are distinct sessions. Events without an identified user (anonymous) each get their own isolated session. AgentCat can't group what it can't identify. | Event | User | Client | Timestamp | Assigned Session | | - | - | - | - | - | | 1 | alice | Claude Desktop | 10:00 | ses\_001 | | 2 | bob | Cursor | 10:02 | ses\_002 | | 3 | alice | Claude Desktop | 10:15 | ses\_001 (reused, within 30 min) | | 4 | alice | Cursor | 10:20 | ses\_003 (different client) | | 5 | alice | Claude Desktop | 10:55 | ses\_004 (new session, >30 min from event 3) | ## Why identifying users matters In stateless mode, every request is independent, so `identify` runs every time. Keep your identify function lightweight by avoiding expensive API calls or database lookups if possible. > [!WARNING] > > Without an identify function, stateless tracking is severely limited. Every event becomes its own anonymous session with no way to group them. Learn more about setting up user identification in [Identifying Users](/sdk/identifying-users). # Tool Call Context Source: https://docs.agentcat.com/sdk/tool-call-context Configure how AgentCat captures agent intent from tool calls. AgentCat automatically injects a `context` parameter into your tool schemas. When an AI agent calls one of your tools, it fills in this parameter with a short explanation of why it's making the call. AgentCat captures this as the event's agent intent and strips it from the arguments before your tool handler runs. For a deeper look at how this works under the hood, see [How AgentCat Works](/how-it-works). This feature is enabled by default. You can customize the prompt that guides the AI's response, or disable it entirely. ## Customizing the Description The default description asks the AI to provide a 15-25 word explanation in third person. If you want to tailor the prompt to your domain, pass a `customContextDescription`: ```typescript TypeScript theme={null} import * as agentcat from "agentcat" agentcat.track(mcpServer, "proj_abc123xyz", { customContextDescription: "Describe the business objective this tool call supports and which team requested it." }) ``` ```python Python theme={null} import agentcat agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions( custom_context_description="Describe the business objective this tool call supports and which team requested it." )) ``` ```go Go theme={null} import agentcat "go.agentcat.com/sdk/officialsdk" // For mark3labs/mcp-go: import agentcat "go.agentcat.com/sdk/mcpgo" agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ CustomContextDescription: "Describe the business objective this tool call supports and which team requested it.", }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; AgentCat::track($builder, 'proj_abc123xyz', new Options( customContextDescription: 'Describe the business objective this tool call supports and which team requested it.', )); ``` The description you provide replaces the default prompt entirely. Write it as an instruction directed at the AI client. ## Tips for Writing a Good Description A few things to keep in mind when writing your own context description: * **Be specific to your domain.** Instead of "explain why you're calling this tool", try "describe which customer request or support ticket this action relates to." * **Set a word count.** LLMs tend to be verbose. Asking for 15-25 words keeps the context concise and useful for analytics. * **Ask for third person.** First-person context ("I need to...") is less useful for analytics than third-person ("Searching for..."). * **Remind about sensitive data.** If your tools handle credentials or personal information, instruct the AI not to include those in the context string. ## Disabling Context Collection If you don't want AgentCat to modify your tool schemas, disable context collection: ```typescript TypeScript theme={null} agentcat.track(mcpServer, "proj_abc123xyz", { enableToolCallContext: false }) ``` ```python Python theme={null} agentcat.track(server, "proj_abc123xyz", agentcat.AgentCatOptions(enable_tool_call_context=False)) ``` ```go Go theme={null} agentcat.Track(s, "proj_abc123xyz", &agentcat.Options{ DisableToolCallContext: true, }) ``` ```php PHP theme={null} use AgentCat\AgentCat; use AgentCat\Options; AgentCat::track($builder, 'proj_abc123xyz', new Options( enableToolCallContext: false, )); ``` > [!NOTE] > > Disabling context collection means you won't see user intent data in the dashboard or session replays. Tool calls will still be tracked, but without the "why" behind each call.