The prerelease compatibility tests have failed. This may indicate upcoming breaking changes in MCP or FastMCP.
@@ -423,7 +423,7 @@ jobs:
repo_tag = '${{ github.repository }}'.replace('/', '_')
params = {
- 'from': 'no-reply@mcpcat.io', # Use Resend's default verified domain
+ 'from': 'no-reply@agentcat.com', # Requires agentcat.com verified in Resend (rebrand Phase 1)
'to': [alert_email], # Use variable, not inline secret
'subject': subject_line,
'html': email_html,
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4f17d39..0ad3f50 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,14 +1,14 @@
-# Contributing to MCPcat ๐
+# Contributing to AgentCat ๐
-Thank you for your interest in contributing to MCPcat! We're excited to have you join our community of developers building analytics tools for MCP servers.
+Thank you for your interest in contributing to AgentCat! We're excited to have you join our community of developers building analytics tools for MCP servers.
## Getting Started
1. **Fork the repository** on GitHub
2. **Clone your fork** locally:
```bash
- git clone https://github.com/YOUR-USERNAME/mcpcat-python-sdk.git
- cd mcpcat-python-sdk
+ git clone https://github.com/YOUR-USERNAME/agentcat-python-sdk.git
+ cd agentcat-python-sdk
```
3. **Install dependencies** using uv:
```bash
@@ -80,7 +80,7 @@ git commit -m "docs: update API documentation"
- Why these changes are needed
- Any relevant context or screenshots
-4. **Wait for review** - The MCPcat team will review your PR within 2 business days
+4. **Wait for review** - The AgentCat team will review your PR within 2 business days
5. **Address feedback** if any changes are requested
@@ -92,7 +92,7 @@ You don't need to open an issue before submitting a PR. Feel free to submit pull
## Good First Issues
-Looking for a place to start? Check out issues labeled [`good first issue`](https://github.com/MCPCat/mcpcat-python-sdk/labels/good%20first%20issue) - these are great for newcomers to the codebase.
+Looking for a place to start? Check out issues labeled [`good first issue`](https://github.com/agentcathq/agentcat-python-sdk/labels/good%20first%20issue) - these are great for newcomers to the codebase.
## Testing
@@ -116,7 +116,7 @@ uv run ruff check .
uv run ruff format .
# Type checking (if applicable)
-uv run mypy src/mcpcat --ignore-missing-imports
+uv run mypy src/agentcat --ignore-missing-imports
```
Our CI will run these same checks on your PR.
@@ -133,9 +133,9 @@ While we don't restrict adding new dependencies, they are generally **discourage
## Project Structure
```
-mcpcat-python-sdk/
+agentcat-python-sdk/
โโโ src/ # Source code
-โ โโโ mcpcat/ # Main package
+โ โโโ agentcat/ # Main package
โ โโโ modules/ # Core modules
โ โโโ thirdparty/ # Vendored dependencies
โ โโโ types.py # Type definitions
@@ -149,22 +149,22 @@ mcpcat-python-sdk/
## Community
- **Discord**: Join our [Discord server](https://discord.gg/n9qpyhzp2u) for discussions
-- **Documentation**: Visit [docs.mcpcat.io](https://docs.mcpcat.io) for detailed guides
-- **Issues**: Browse [open issues](https://github.com/MCPCat/mcpcat-python-sdk/issues) for areas needing help
+- **Documentation**: Visit [docs.agentcat.com](https://docs.agentcat.com) for detailed guides
+- **Issues**: Browse [open issues](https://github.com/agentcathq/agentcat-python-sdk/issues) for areas needing help
## Versioning
-The MCPcat team handles versioning and releases. Your contributions will be included in the next appropriate release based on semantic versioning principles.
+The AgentCat team handles versioning and releases. Your contributions will be included in the next appropriate release based on semantic versioning principles.
## Recognition
-All contributors are recognized in our repository. Your contributions help make MCPcat better for everyone building MCP servers!
+All contributors are recognized in our repository. Your contributions help make AgentCat better for everyone building MCP servers!
## Questions?
If you have questions about contributing, feel free to:
- Ask in our [Discord server](https://discord.gg/n9qpyhzp2u)
-- Open a [discussion](https://github.com/MCPCat/mcpcat-python-sdk/discussions) on GitHub
+- Open a [discussion](https://github.com/agentcathq/agentcat-python-sdk/discussions) on GitHub
-Thank you for contributing to MCPcat! ๐ฑ
\ No newline at end of file
+Thank you for contributing to AgentCat! ๐ฑ
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index db3d4fb..4f90dac 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2024 MCPCat
+Copyright (c) 2024 AgentCat, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 0000000..3f85276
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,92 @@
+# Migrating from `mcpcat` to `agentcat`
+
+MCPcat is now **AgentCat** โ same team, same product, new name. The PyPI package has been renamed from `mcpcat` to [`agentcat`](https://pypi.org/project/agentcat/), starting fresh at `v1.0.0`.
+
+## Nothing breaks if you stay
+
+We keep every existing surface alive **permanently** โ not on a deprecation timer:
+
+- The `mcpcat` PyPI package stays published and functional
+- `api.mcpcat.io` keeps accepting events forever
+- The `MCPCAT_API_URL` environment variable keeps working
+- Your project, data, and history stay unified regardless of which SDK sends them
+
+If you never touch your integration, nothing stops working. Migrate on your own schedule โ new features only land in `agentcat`.
+
+## What changed
+
+| | `mcpcat` (old) | `agentcat` (new) |
+|---|---|---|
+| PyPI package | `mcpcat` | `agentcat` (starts at `v1.0.0`) |
+| Import | `import mcpcat` | `import agentcat` |
+| Default endpoint | `https://api.mcpcat.io` | `https://api.agentcat.com` |
+| Public types | `MCPCatOptions` / `MCPCatData` | `AgentCatOptions` / `AgentCatData` |
+| Endpoint override | `MCPCAT_API_URL` | `AGENTCAT_API_URL` (`MCPCAT_API_URL` still honored) |
+| Debug logging | `MCPCAT_DEBUG_MODE` | `AGENTCAT_DEBUG_MODE` (no fallback) |
+| Local log file | `~/mcpcat.log` | `~/agentcat.log` |
+
+There are no other API changes โ `track()`, its options, the `identify` and redaction hooks, and the telemetry exporters all work exactly as before.
+
+> **Note:** `agentcat` does not install a `mcpcat` compatibility module โ a shim would collide with the real `mcpcat` distribution when both are installed. The import rename is required.
+
+## Steps
+
+1. **Swap the package:**
+
+ ```bash
+ pip uninstall mcpcat
+ pip install agentcat
+ # or, for Jlowin's/Prefect's FastMCP support:
+ pip install "agentcat[community]"
+ ```
+
+2. **Rename your imports:**
+
+ ```diff
+ - import mcpcat
+ - from mcpcat import MCPCatOptions
+ + import agentcat
+ + from agentcat import AgentCatOptions
+
+ - mcpcat.track(server, "proj_0000000", MCPCatOptions(identify=identify_user))
+ + agentcat.track(server, "proj_0000000", AgentCatOptions(identify=identify_user))
+ ```
+
+3. **Rename any imported types 1:1** โ `MCPCatOptions` โ `AgentCatOptions`, `MCPCatData` โ `AgentCatData`. (`UserIdentity` is unchanged.)
+
+4. **Environment variables (optional):** if you override the endpoint, prefer `AGENTCAT_API_URL` (the old `MCPCAT_API_URL` name is still read as a fallback). If you use debug logging, rename `MCPCAT_DEBUG_MODE` โ `AGENTCAT_DEBUG_MODE` โ this one has no fallback.
+
+5. **Log tooling (if any):** the SDK now writes to `~/agentcat.log` instead of `~/mcpcat.log`.
+
+Your project ID does not change, and your dashboard history is continuous.
+
+## Or let an AI agent do it
+
+Paste this into your coding agent (Claude Code, Cursor, Copilot, etc.) from your project root:
+
+```text
+Migrate this project from the `mcpcat` PyPI package to its renamed successor `agentcat` (same API, new package name):
+
+1. Replace the `mcpcat` dependency with `agentcat` using this project's package manager (pip/uv/poetry; e.g. `pip uninstall mcpcat && pip install agentcat`). If the project uses the FastMCP extra, install "agentcat[community]".
+2. Update every `import mcpcat` / `from mcpcat import ...` to `import agentcat` / `from agentcat import ...`. There is no compatibility shim โ this rename is required.
+3. Rename these types 1:1 wherever they're used: MCPCatOptions โ AgentCatOptions, MCPCatData โ AgentCatData. (UserIdentity is unchanged.)
+4. If the env var MCPCAT_API_URL appears anywhere (code, .env files, CI, deploy config), rename it to AGENTCAT_API_URL. (Optional โ the old name is still read as a fallback.)
+5. If the env var MCPCAT_DEBUG_MODE appears anywhere, rename it to AGENTCAT_DEBUG_MODE. (Required โ it has NO fallback.)
+6. Update any references to the log path ~/mcpcat.log โ ~/agentcat.log.
+7. Do NOT change the project ID passed to track() โ it stays the same.
+8. Run the project's tests to verify, and report anything that referenced mcpcat which you could not migrate mechanically (e.g. dashboards or filters keying on source=mcpcat).
+```
+
+## Heads-up if you forward telemetry to your own tools
+
+If you use the exporters (Datadog, Sentry, OTLP), the `source` value and tag namespaces stamped into **your** observability platform change from `mcpcat` to `agentcat`. Update any saved filters, monitors, or dashboards that key on them โ a one-time change on your side.
+
+## FAQ
+
+**Do I have to migrate?** No โ and there is no deadline. The old package and endpoint stay up permanently.
+
+**Will my data/history split?** No. Both SDKs report into the same platform and your history stays unified under your project.
+
+**What about the GitHub repo?** The org is being renamed; old repo URLs will redirect automatically, and stars/issues are preserved.
+
+**Questions?** Open an issue or email [hi@agentcat.com](mailto:hi@agentcat.com).
diff --git a/README.md b/README.md
index 6698262..cab7954 100644
--- a/README.md
+++ b/README.md
@@ -1,84 +1,82 @@
-> [!IMPORTANT]
-> **MCPcat is now AgentCat** ๐ฑ โ same team, same product, new name. This package has been renamed to [`agentcat`](https://pypi.org/project/agentcat/) (`pip install agentcat`, starts at v1.0.0). `mcpcat` keeps working forever, but new features land only in `agentcat`. Upgrading takes a few minutes โ see the [migration guide](./MIGRATION.md).
-
+> [!IMPORTANT]
+> **MCPcat is now AgentCat** ๐ฑ โ same team, same product, new name. This package was previously published as [`mcpcat`](https://pypi.org/project/mcpcat/), which keeps working forever, but new features land here. Upgrading takes a few minutes โ see the [migration guide](./MIGRATION.md).
+
> [!NOTE]
-> Looking for the TypeScript SDK? Check it out here [mcpcat-typescript](https://github.com/mcpcat/mcpcat-typescript-sdk).
+> Looking for the TypeScript SDK? Check it out here [agentcat-typescript](https://github.com/agentcathq/agentcat-typescript-sdk).
-MCPcat is an analytics platform for MCP server owners ๐ฑ. It captures user intentions and behavior patterns to help you understand what AI users actually need from your tools โ eliminating guesswork and accelerating product development all with one-line of code.
+AgentCat is an analytics platform for MCP server owners ๐ฑ. It captures user intentions and behavior patterns to help you understand what AI users actually need from your tools โ eliminating guesswork and accelerating product development all with one-line of code.
-This SDK also provides a free and simple way to forward telemetry like logs, traces, and errors to any Open Telemetry collector or popular tools like Datadog and Sentry.
+This SDK also provides a free and simple way to forward telemetry like logs, traces, and errors to any Open Telemetry collector or popular tools like Datadog and Sentry.
```bash
# Basic installation (includes official MCP SDK)
-pip install mcpcat
+pip install agentcat
# With Jlowin's/Prefect's FastMCP support
-pip install "mcpcat[community]"
+pip install "agentcat[community]"
```
-To learn more about us, check us out [here](https://mcpcat.io)
+To learn more about us, check us out [here](https://agentcat.com)
-## Why use MCPcat? ๐ค
+## Why use AgentCat? ๐ค
-MCPcat helps developers and product owners build, improve, and monitor their MCP servers by capturing user analytics and tracing tool calls.
+AgentCat helps developers and product owners build, improve, and monitor their MCP servers by capturing user analytics and tracing tool calls.
-Use MCPcat for:
+Use AgentCat for:
- **User session replay** ๐ฌ. Follow alongside your users to understand why they're using your MCP servers, what functionality you're missing, and what clients they're coming from.
- **Trace debugging** ๐. See where your users are getting stuck, track and find when LLMs get confused by your API, and debug sessions across all deployments of your MCP server.
- **Existing platform support** ๐. Get logging and tracing out of the box for your existing observability platforms (OpenTelemetry, Datadog, Sentry) โ eliminating the tedious work of implementing telemetry yourself.
-
+
## Getting Started
-To get started with MCPcat, first create an account and obtain your project ID by signing up at [mcpcat.io](https://mcpcat.io). For detailed setup instructions visit our [documentation](https://docs.mcpcat.io).
+To get started with AgentCat, first create an account and obtain your project ID by signing up at [agentcat.com](https://agentcat.com). For detailed setup instructions visit our [documentation](https://docs.agentcat.com).
-Once you have your project ID, integrate MCPcat into your MCP server:
+Once you have your project ID, integrate AgentCat into your MCP server:
```python
-import mcpcat
+import agentcat
from mcp.server import FastMCP
server = FastMCP(name="echo-mcp", version="1.0.0")
-mcpcat.track(server, "proj_0000000")
+agentcat.track(server, "proj_0000000")
```
### Identifying users
-You can identify your user sessions with a simple callback MCPcat exposes, called `identify`.
+You can identify your user sessions with a simple callback AgentCat exposes, called `identify`.
```python
+from agentcat import AgentCatOptions, UserIdentity
+
def identify_user(request, extra):
user = myapi.get_user(request.params.arguments.token)
return UserIdentity(
@@ -89,82 +87,86 @@ def identify_user(request, extra):
},
)
-mcpcat.track(server, "proj_0000000", MCPCatOptions(identify=identify_user))
+agentcat.track(server, "proj_0000000", AgentCatOptions(identify=identify_user))
```
### Redacting sensitive data
-MCPcat redacts all data sent to its servers and encrypts at rest, but for additional security, it offers a hook to do your own redaction on all text data returned back to our servers.
+AgentCat redacts all data sent to its servers and encrypts at rest, but for additional security, it offers a hook to do your own redaction on all text data returned back to our servers.
```python
+from agentcat import AgentCatOptions
+
# Sync version
def redact_sync(text):
return custom_redact(text)
-mcpcat.track(server, "proj_0000000", redact_sensitive_information=redact_sync)
+agentcat.track(server, "proj_0000000", AgentCatOptions(redact_sensitive_information=redact_sync))
```
### Forwarding data to existing observability platforms
-MCPcat seamlessly integrates with your existing observability stack, providing automatic logging and tracing without the tedious setup typically required. Export telemetry data to multiple platforms simultaneously:
+AgentCat seamlessly integrates with your existing observability stack, providing automatic logging and tracing without the tedious setup typically required. Export telemetry data to multiple platforms simultaneously:
```python
-from mcpcat import MCPCatOptions, ExporterConfig
+import os
+
+from agentcat import AgentCatOptions
-mcpcat.track(
- server,
+agentcat.track(
+ server,
"proj_0000000", # Or None if you just want to use the SDK to forward telemetry
- MCPCatOptions(
+ AgentCatOptions(
exporters={
# OpenTelemetry - works with Jaeger, Tempo, New Relic, etc.
- "otlp": ExporterConfig(
- type="otlp",
- endpoint="http://localhost:4318/v1/traces"
- ),
+ "otlp": {
+ "type": "otlp",
+ "endpoint": "http://localhost:4318/v1/traces",
+ },
# Datadog
- "datadog": ExporterConfig(
- type="datadog",
- api_key=os.getenv("DD_API_KEY"),
- site="datadoghq.com",
- service="my-mcp-server"
- ),
+ "datadog": {
+ "type": "datadog",
+ "api_key": os.getenv("DD_API_KEY"),
+ "site": "datadoghq.com",
+ "service": "my-mcp-server",
+ },
# Sentry
- "sentry": ExporterConfig(
- type="sentry",
- dsn=os.getenv("SENTRY_DSN"),
- environment="production"
- )
+ "sentry": {
+ "type": "sentry",
+ "dsn": os.getenv("SENTRY_DSN"),
+ "environment": "production",
+ },
}
)
)
```
-Learn more about our free and open source [telemetry integrations](https://docs.mcpcat.io/telemetry/integrations).
+Learn more about our free and open source [telemetry integrations](https://docs.agentcat.com/telemetry/integrations).
### Internal diagnostics
-To help us catch and fix broken installs, the SDK sends MCPcat a small, anonymized
+To help us catch and fix broken installs, the SDK sends AgentCat a small, anonymized
signal when setup or runtime errors occur โ never your tool calls, your responses,
or anything about your users. Records carry only operational metadata, such as your
-project ID (or an anonymous install ID when none is set). Your local `~/mcpcat.log`
+project ID (or an anonymous install ID when none is set). Your local `~/agentcat.log`
is unchanged.
Diagnostics are on by default and can be turned off completely with either:
-- `track(server, project_id, MCPCatOptions(disable_diagnostics=True))`, or
+- `track(server, project_id, AgentCatOptions(disable_diagnostics=True))`, or
- the `DISABLE_DIAGNOSTICS` environment variable.
## Free for open source
-MCPcat is free for qualified open source projects. We believe in supporting the ecosystem that makes MCP possible. If you maintain an open source MCP server, you can access our full analytics platform at no cost.
+AgentCat is free for qualified open source projects. We believe in supporting the ecosystem that makes MCP possible. If you maintain an open source MCP server, you can access our full analytics platform at no cost.
-**How to apply**: Email hi@mcpcat.io with your repository link
+**How to apply**: Email hi@agentcat.com with your repository link
-_Already using MCPcat? We'll upgrade your account immediately._
+_Already using AgentCat? We'll upgrade your account immediately._
## Community Cats ๐ฑ
-Meet the cats behind MCPcat! Add your cat to our community by submitting a PR with your cat's photo in the `docs/cats/` directory.
+Meet the cats behind AgentCat! Add your cat to our community by submitting a PR with your cat's photo in the `docs/cats/` directory.
diff --git a/docs/static/architecture.png b/docs/static/architecture.png
new file mode 100644
index 0000000..d89d36c
Binary files /dev/null and b/docs/static/architecture.png differ
diff --git a/docs/static/logo-dark.svg b/docs/static/logo-dark.svg
deleted file mode 100644
index 1e12087..0000000
--- a/docs/static/logo-dark.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/docs/static/logo-light.svg b/docs/static/logo-light.svg
deleted file mode 100644
index b7f6fb8..0000000
--- a/docs/static/logo-light.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/docs/static/og-image.png b/docs/static/og-image.png
new file mode 100644
index 0000000..a8fe6de
Binary files /dev/null and b/docs/static/og-image.png differ
diff --git a/docs/superpowers/plans/2026-04-29-e2e-streamable-http-suite.md b/docs/superpowers/plans/2026-04-29-e2e-streamable-http-suite.md
deleted file mode 100644
index 966d3e3..0000000
--- a/docs/superpowers/plans/2026-04-29-e2e-streamable-http-suite.md
+++ /dev/null
@@ -1,2101 +0,0 @@
-# E2E Streamable-HTTP Test Suite Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Add a `tests/e2e/` tree (~38 tests across 12 modules) that exercises MCPCat under real Streamable HTTP transport, covering header capture, header-derived client info, stateless mode, identify-per-event, and redaction over the wire โ for the official MCP SDK, community FastMCP v3, and a smoke for community FastMCP v2.
-
-**Architecture:** Module-scoped uvicorn-in-thread fixtures (official + v2) and FastMCP-native shttp_server fixture (v3) boot a real MCP server on `127.0.0.1:` for each test module. Tests connect with the real `streamablehttp_client(url, headers=...)` / `fastmcp.Client(StreamableHttpTransport(url, headers=...))` and assert on events captured via a mocked `EventQueue` api_client. Each test module gets its own server instance so `MCPCatOptions` can vary per file (stateless mode, identify hook, redaction).
-
-**Tech Stack:** Python 3.12, pytest + pytest-asyncio, uvicorn (already a transitive dep), httpx, the official `mcp` SDK and `fastmcp` v3.
-
----
-
-## File Structure
-
-**New files:**
-- `tests/e2e/__init__.py`
-- `tests/e2e/conftest.py` โ top-level `capture_queue` fixture
-- `tests/e2e/_helpers.py` โ port utilities, optional raw POST helper
-- `tests/e2e/official/__init__.py`
-- `tests/e2e/official/conftest.py` โ uvicorn-in-thread fixture for official SDK servers
-- `tests/e2e/official/test_event_capture_http.py` โ 5 tests
-- `tests/e2e/official/test_request_extra_http.py` โ 6 tests
-- `tests/e2e/official/test_session_http.py` โ 4 tests
-- `tests/e2e/official/test_stateless_http.py` โ 3 tests
-- `tests/e2e/official/test_identify_http.py` โ 5 tests
-- `tests/e2e/official/test_redaction_http.py` โ 3 tests
-- `tests/e2e/community_v3/__init__.py`
-- `tests/e2e/community_v3/conftest.py` โ FastMCP-native shttp_server fixture
-- `tests/e2e/community_v3/test_event_capture_http.py` โ 3 tests
-- `tests/e2e/community_v3/test_request_extra_http.py` โ 3 tests
-- `tests/e2e/community_v3/test_stateless_http.py` โ 2 tests
-- `tests/e2e/community_v3/test_identify_http.py` โ 2 tests
-- `tests/e2e/community_v2/__init__.py`
-- `tests/e2e/community_v2/conftest.py` โ uvicorn-in-thread fixture for v2 servers
-- `tests/e2e/community_v2/test_request_extra_http.py` โ 1 test
-- `tests/e2e/community_v2/test_identify_http.py` โ 1 test
-
-**Modified files:**
-- `pyproject.toml` โ register the `e2e` pytest marker
-
----
-
-## Task 1: Top-level scaffolding (marker + capture_queue + helpers)
-
-**Files:**
-- Modify: `pyproject.toml`
-- Create: `tests/e2e/__init__.py`
-- Create: `tests/e2e/conftest.py`
-- Create: `tests/e2e/_helpers.py`
-
-- [ ] **Step 1: Register the `e2e` marker in pyproject.toml**
-
-Open `pyproject.toml`, find `[tool.pytest.ini_options]` (already exists), and add a `markers` entry. The current section ends with `python_functions = "test_*"`. After that line, add:
-
-```toml
-markers = [
- "e2e: real HTTP transport tests (uvicorn-in-thread; ~25s overhead)",
-]
-```
-
-- [ ] **Step 2: Create `tests/e2e/__init__.py`** (empty file)
-
-```bash
-mkdir -p tests/e2e && : > tests/e2e/__init__.py
-```
-
-- [ ] **Step 3: Create `tests/e2e/_helpers.py`**
-
-```python
-"""Shared helpers for the e2e Streamable-HTTP test suite."""
-
-from __future__ import annotations
-
-import socket
-import time
-from typing import Optional
-
-
-def find_free_port() -> int:
- """Bind to 127.0.0.1:0, capture the assigned port, release the socket."""
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- try:
- sock.bind(("127.0.0.1", 0))
- return sock.getsockname()[1]
- finally:
- sock.close()
-
-
-def wait_for_port(port: int, host: str = "127.0.0.1", timeout: float = 5.0) -> None:
- """Poll until a TCP connection succeeds. Raise TimeoutError otherwise.
-
- Used by uvicorn-in-thread fixtures to gate test execution on server-ready.
- """
- deadline = time.time() + timeout
- last_err: Optional[Exception] = None
- while time.time() < deadline:
- try:
- with socket.create_connection((host, port), timeout=0.5):
- return
- except OSError as e:
- last_err = e
- time.sleep(0.05)
- raise TimeoutError(
- f"Port {host}:{port} not accepting connections after {timeout}s "
- f"(last error: {last_err!r})"
- )
-```
-
-- [ ] **Step 4: Create `tests/e2e/conftest.py`**
-
-```python
-"""Shared pytest fixtures for the e2e Streamable-HTTP suite.
-
-`capture_queue` mocks the global event queue for the duration of a test and
-yields the list that accumulates published events. Restores the real queue on
-teardown.
-"""
-
-from __future__ import annotations
-
-from typing import Any, List
-from unittest.mock import MagicMock
-
-import pytest
-
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
-
-
-@pytest.fixture
-def capture_queue() -> List[Any]:
- """Replace the global EventQueue with a mock that records every publish.
-
- Yields the list of captured PublishEventRequest objects. Tests assert on
- its contents after the in-flight HTTP round-trip plus a short settle.
- """
- from mcpcat.modules.event_queue import event_queue as original
-
- captured: List[Any] = []
- mock = MagicMock()
- mock.publish_event = MagicMock(side_effect=lambda req: captured.append(req))
- set_event_queue(EventQueue(api_client=mock))
- yield captured
- set_event_queue(original)
-```
-
-- [ ] **Step 5: Verify pytest collects the new marker**
-
-Run: `uv run pytest --markers | grep e2e`
-Expected output includes:
-```
-@pytest.mark.e2e: real HTTP transport tests (uvicorn-in-thread; ~25s overhead)
-```
-
-- [ ] **Step 6: Commit**
-
-```bash
-git add pyproject.toml tests/e2e/__init__.py tests/e2e/conftest.py tests/e2e/_helpers.py
-git commit -m "test(e2e): add scaffolding for Streamable-HTTP test suite
-
-Adds the e2e pytest marker, capture_queue fixture, and port utilities.
-Subsequent commits will populate the per-SDK harnesses and tests."
-```
-
----
-
-## Task 2: Official SDK harness + smoke test
-
-**Files:**
-- Create: `tests/e2e/official/__init__.py`
-- Create: `tests/e2e/official/conftest.py`
-- Create: `tests/e2e/official/test_event_capture_http.py` (smoke only โ 1 test)
-
-- [ ] **Step 1: Create `tests/e2e/official/__init__.py`** (empty)
-
-```bash
-mkdir -p tests/e2e/official && : > tests/e2e/official/__init__.py
-```
-
-- [ ] **Step 2: Create `tests/e2e/official/conftest.py`**
-
-```python
-"""Uvicorn-in-thread harness for the official MCP SDK.
-
-A test module declares an `MCPCATCAT_OPTIONS_FACTORY` (callable returning
-`MCPCatOptions`) at module scope; `official_http_server` boots a fresh
-FastMCP todo server for the module, calls `mcpcat.track(...)` with those
-options, mounts the server's Streamable-HTTP app, and yields the URL.
-
-Module-scoped: one boot per test file, not per test.
-"""
-
-from __future__ import annotations
-
-import threading
-from typing import Any, Callable, Optional, Tuple
-
-import pytest
-import uvicorn
-
-import mcpcat
-from mcpcat import MCPCatOptions
-
-from tests.e2e._helpers import find_free_port, wait_for_port
-from tests.test_utils.todo_server import create_todo_server
-
-
-def _default_options_factory() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True)
-
-
-@pytest.fixture(scope="module")
-def official_http_server(request) -> Tuple[str, Any]:
- """Boot a Streamable-HTTP MCP server for the test module.
-
- Reads the module attribute `MCPCAT_OPTIONS_FACTORY` (Callable[[], MCPCatOptions])
- if defined; otherwise uses tracing-only defaults.
-
- Yields:
- (url, server) โ the Streamable-HTTP URL (e.g. "http://127.0.0.1:54321/mcp")
- and the FastMCP server instance under test.
- """
- options_factory: Callable[[], MCPCatOptions] = getattr(
- request.module, "MCPCAT_OPTIONS_FACTORY", _default_options_factory
- )
- options = options_factory()
- server = create_todo_server()
- mcpcat.track(server, "test_project", options)
-
- app = server._mcp_server.streamable_http_app()
- port = find_free_port()
- config = uvicorn.Config(
- app, host="127.0.0.1", port=port, log_level="error", lifespan="on"
- )
- uv_server = uvicorn.Server(config)
- thread = threading.Thread(target=uv_server.run, daemon=True)
- thread.start()
- try:
- wait_for_port(port, timeout=5.0)
- except TimeoutError:
- uv_server.should_exit = True
- thread.join(timeout=2.0)
- raise
-
- url = f"http://127.0.0.1:{port}/mcp"
- yield url, server
-
- uv_server.should_exit = True
- thread.join(timeout=5.0)
-```
-
-- [ ] **Step 3: Create `tests/e2e/official/test_event_capture_http.py` (smoke โ 1 test)**
-
-```python
-"""Event-capture smoke and round-trip tests over real Streamable HTTP."""
-
-from __future__ import annotations
-
-import time
-
-import pytest
-from mcp import ClientSession
-from mcp.client.streamable_http import streamablehttp_client
-
-
-pytestmark = pytest.mark.e2e
-
-
-@pytest.mark.asyncio
-async def test_initialize_event_captured(official_http_server, capture_queue):
- """Real handshake produces a mcp:initialize event."""
- url, _server = official_http_server
- async with streamablehttp_client(url) as (read, write, _get_session_id):
- async with ClientSession(read, write) as client:
- await client.initialize()
-
- # Events publish from a worker thread; settle briefly.
- time.sleep(0.5)
- init_events = [e for e in capture_queue if e.event_type == "mcp:initialize"]
- assert init_events, (
- f"expected an mcp:initialize event, got {[e.event_type for e in capture_queue]}"
- )
-```
-
-- [ ] **Step 4: Run the smoke**
-
-Run: `uv run pytest tests/e2e/official/test_event_capture_http.py -v`
-Expected: PASS (1 test).
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add tests/e2e/official/__init__.py tests/e2e/official/conftest.py tests/e2e/official/test_event_capture_http.py
-git commit -m "test(e2e): add official SDK uvicorn-in-thread harness + smoke
-
-Adds the module-scoped uvicorn fixture and a single round-trip smoke test
-proving real Streamable-HTTP traffic produces an mcp:initialize event."
-```
-
----
-
-## Task 3: Complete `tests/e2e/official/test_event_capture_http.py` (5 tests)
-
-**Files:**
-- Modify: `tests/e2e/official/test_event_capture_http.py`
-
-- [ ] **Step 1: Replace the file with the full set of 5 tests**
-
-```python
-"""Event-capture and round-trip tests over real Streamable HTTP."""
-
-from __future__ import annotations
-
-import asyncio
-import time
-
-import pytest
-from mcp import ClientSession
-from mcp.client.streamable_http import streamablehttp_client
-
-
-pytestmark = pytest.mark.e2e
-
-
-@pytest.mark.asyncio
-async def test_initialize_event_captured(official_http_server, capture_queue):
- """Real handshake produces a mcp:initialize event."""
- url, _server = official_http_server
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
-
- time.sleep(0.5)
- init_events = [e for e in capture_queue if e.event_type == "mcp:initialize"]
- assert init_events, (
- f"expected mcp:initialize, got {[e.event_type for e in capture_queue]}"
- )
-
-
-@pytest.mark.asyncio
-async def test_tools_list_event_captured(official_http_server, capture_queue):
- url, _server = official_http_server
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.list_tools()
-
- time.sleep(0.5)
- list_events = [e for e in capture_queue if e.event_type == "mcp:tools/list"]
- assert list_events, "expected a mcp:tools/list event"
-
-
-@pytest.mark.asyncio
-async def test_tools_call_event_captured(official_http_server, capture_queue):
- url, _server = official_http_server
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "hi", "context": "e2e smoke"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events, "expected a mcp:tools/call event"
- assert call_events[0].resource_name == "add_todo"
-
-
-@pytest.mark.asyncio
-async def test_event_duration_is_non_zero(official_http_server, capture_queue):
- """Tool that exists in the test server runs through real HTTP; duration > 0."""
- url, _server = official_http_server
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "duration test", "context": "duration"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events, "expected a mcp:tools/call event"
- # duration is integer milliseconds; real HTTP round-trip is always > 0.
- assert call_events[0].duration is not None
- assert call_events[0].duration >= 0
-
-
-@pytest.mark.asyncio
-async def test_concurrent_clients_get_distinct_session_ids(
- official_http_server, capture_queue
-):
- """Two stateful clients connecting concurrently should get distinct session ids
- on emitted events (via mcp-session-id header issued by the SDK)."""
- url, _server = official_http_server
-
- async def call_once(text: str) -> None:
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": text, "context": "concurrent"}
- )
-
- await asyncio.gather(call_once("a"), call_once("b"))
- time.sleep(0.7)
-
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert len(call_events) >= 2, f"expected >=2 call events, got {len(call_events)}"
- session_ids = {
- (e.parameters or {}).get("extra", {}).get("sessionId") for e in call_events
- }
- # Each connection gets its own MCP session id; the set should have >= 2 distinct values.
- assert len(session_ids - {None}) >= 2, (
- f"expected distinct sessionIds across concurrent clients, got {session_ids}"
- )
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/official/test_event_capture_http.py -v`
-Expected: 5 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/official/test_event_capture_http.py
-git commit -m "test(e2e): complete official event-capture http suite
-
-5 tests covering initialize/list_tools/tools_call event emission, duration
-recording, and concurrent-client session id distinctness over real HTTP."
-```
-
----
-
-## Task 4: `tests/e2e/official/test_request_extra_http.py` (6 tests)
-
-**Files:**
-- Create: `tests/e2e/official/test_request_extra_http.py`
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""parameters.extra.requestInfo.headers parity tests over real Streamable HTTP."""
-
-from __future__ import annotations
-
-import time
-
-import pytest
-from mcp import ClientSession
-from mcp.client.streamable_http import streamablehttp_client
-
-
-pytestmark = pytest.mark.e2e
-
-
-def _last_call_event(capture_queue):
- return [e for e in capture_queue if e.event_type == "mcp:tools/call"][-1]
-
-
-def _extra(event):
- return (event.parameters or {}).get("extra", {})
-
-
-@pytest.mark.asyncio
-async def test_custom_header_lands_in_request_info(
- official_http_server, capture_queue
-):
- url, _ = official_http_server
- async with streamablehttp_client(
- url, headers={"X-Demo-Header": "demo-value"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "h", "context": "header test"}
- )
-
- time.sleep(0.5)
- headers = _extra(_last_call_event(capture_queue)).get("requestInfo", {}).get(
- "headers", {}
- )
- assert headers.get("x-demo-header") == "demo-value", (
- f"expected x-demo-header in extra.requestInfo.headers, got {headers}"
- )
-
-
-@pytest.mark.asyncio
-async def test_user_agent_preserved(official_http_server, capture_queue):
- url, _ = official_http_server
- async with streamablehttp_client(
- url, headers={"User-Agent": "Cursor/2.6.22"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "ua", "context": "ua"}
- )
-
- time.sleep(0.5)
- headers = _extra(_last_call_event(capture_queue)).get("requestInfo", {}).get(
- "headers", {}
- )
- # The transport may add framing headers, but our header must survive.
- assert headers.get("user-agent") == "Cursor/2.6.22"
-
-
-@pytest.mark.asyncio
-async def test_mcp_session_id_header_promoted_to_extra_session_id(
- official_http_server, capture_queue
-):
- """The SDK issues mcp-session-id during initialize; subsequent requests carry
- it back, so extra.sessionId on tool/call events must equal that header."""
- url, _ = official_http_server
- async with streamablehttp_client(url) as (read, write, get_sid):
- async with ClientSession(read, write) as client:
- await client.initialize()
- sdk_session_id = get_sid()
- await client.call_tool(
- "add_todo", {"text": "sid", "context": "session id"}
- )
-
- time.sleep(0.5)
- extra = _extra(_last_call_event(capture_queue))
- assert sdk_session_id, "MCP SDK should have issued an mcp-session-id"
- assert extra.get("sessionId") == sdk_session_id, (
- f"extra.sessionId={extra.get('sessionId')} should match SDK session id "
- f"{sdk_session_id}"
- )
-
-
-@pytest.mark.asyncio
-async def test_request_id_present_per_call(official_http_server, capture_queue):
- url, _ = official_http_server
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "r1", "context": "req"}
- )
- await client.call_tool(
- "add_todo", {"text": "r2", "context": "req"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert len(call_events) >= 2
- request_ids = [_extra(e).get("requestId") for e in call_events]
- assert all(rid is not None for rid in request_ids), request_ids
- # JSON-RPC ids should be unique per request on a single session.
- assert len(set(request_ids)) == len(request_ids), (
- f"expected distinct requestIds, got {request_ids}"
- )
-
-
-@pytest.mark.asyncio
-async def test_meta_progresstoken_passes_through(
- official_http_server, capture_queue
-):
- """Client sends _meta.progressToken; it must surface in extra.meta."""
- url, _ = official_http_server
-
- # Use raw httpx because ClientSession does not expose progressToken cleanly.
- # See dual approach: drive client.send_request with custom params if needed.
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- # progress_token is a legitimate kwarg on call_tool in modern SDKs;
- # if absent in this SDK version, the test is best-effort and asserts
- # the meta dict exists.
- try:
- await client.call_tool(
- "add_todo",
- {"text": "meta", "context": "meta"},
- progress_token="tok-7",
- )
- except TypeError:
- # SDK does not accept progress_token directly; just call.
- await client.call_tool(
- "add_todo", {"text": "meta", "context": "meta"}
- )
-
- time.sleep(0.5)
- extra = _extra(_last_call_event(capture_queue))
- meta = extra.get("meta") or {}
- # If progress_token was accepted, confirm it round-tripped. Otherwise just
- # confirm meta is at least a dict (i.e. wiring is in place).
- if "progressToken" in meta:
- assert meta["progressToken"] == "tok-7"
-
-
-@pytest.mark.asyncio
-async def test_initialize_event_also_carries_extra(
- official_http_server, capture_queue
-):
- url, _ = official_http_server
- async with streamablehttp_client(
- url, headers={"X-Init-Header": "init-value"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
-
- time.sleep(0.5)
- init_events = [e for e in capture_queue if e.event_type == "mcp:initialize"]
- assert init_events
- headers = (
- (init_events[0].parameters or {})
- .get("extra", {})
- .get("requestInfo", {})
- .get("headers", {})
- )
- assert headers.get("x-init-header") == "init-value"
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/official/test_request_extra_http.py -v`
-Expected: 6 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/official/test_request_extra_http.py
-git commit -m "test(e2e): parameters.extra.requestInfo.headers parity over HTTP
-
-6 tests covering custom headers, User-Agent, mcp-session-id, request id
-distinctness, _meta passthrough, and initialize-event extra capture."
-```
-
----
-
-## Task 5: `tests/e2e/official/test_session_http.py` (4 tests)
-
-**Files:**
-- Create: `tests/e2e/official/test_session_http.py`
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""Header-derived client_info extraction tests over real Streamable HTTP.
-
-The MCPCatOptions used here disable enable_tool_call_context for cleaner
-assertions on session-derived fields (we don't need the context-injection
-machinery to test client_info extraction).
-"""
-
-from __future__ import annotations
-
-import time
-
-import pytest
-from mcp import ClientSession
-from mcp.client.streamable_http import streamablehttp_client
-
-from mcpcat import MCPCatOptions
-
-
-def MCPCAT_OPTIONS_FACTORY() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True, stateless=True)
-
-
-pytestmark = pytest.mark.e2e
-
-
-def _last_event(capture_queue, event_type: str):
- return [e for e in capture_queue if e.event_type == event_type][-1]
-
-
-@pytest.mark.asyncio
-async def test_user_agent_parsed_into_client_name_version(
- official_http_server, capture_queue
-):
- url, _ = official_http_server
- async with streamablehttp_client(
- url, headers={"User-Agent": "Cursor/2.6.22"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "ua-parse", "context": "ua"}
- )
-
- time.sleep(0.5)
- ev = _last_event(capture_queue, "mcp:tools/call")
- assert ev.client_name == "Cursor", f"expected Cursor, got {ev.client_name}"
- assert ev.client_version == "2.6.22"
-
-
-@pytest.mark.asyncio
-async def test_x_mcp_client_headers_override_user_agent(
- official_http_server, capture_queue
-):
- url, _ = official_http_server
- async with streamablehttp_client(
- url,
- headers={
- "User-Agent": "Cursor/2.6.22",
- "X-MCP-Client-Name": "CustomClient",
- "X-MCP-Client-Version": "9.9.9",
- },
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "override", "context": "override"}
- )
-
- time.sleep(0.5)
- ev = _last_event(capture_queue, "mcp:tools/call")
- assert ev.client_name == "CustomClient"
- assert ev.client_version == "9.9.9"
-
-
-@pytest.mark.asyncio
-async def test_initialize_clientinfo_wins_over_headers(
- official_http_server, capture_queue
-):
- """The clientInfo passed in InitializeRequest.params should beat header parsing."""
- url, _ = official_http_server
- async with streamablehttp_client(
- url, headers={"User-Agent": "Cursor/2.6.22"}
- ) as (read, write, _):
- async with ClientSession(
- read,
- write,
- client_info={"name": "MyAgent", "version": "1.2.3"},
- ) as client:
- await client.initialize()
-
- time.sleep(0.5)
- init = _last_event(capture_queue, "mcp:initialize")
- assert init.client_name == "MyAgent"
- assert init.client_version == "1.2.3"
-
-
-@pytest.mark.asyncio
-async def test_unparseable_user_agent_does_not_crash(
- official_http_server, capture_queue
-):
- url, _ = official_http_server
- async with streamablehttp_client(
- url, headers={"User-Agent": "not-a-recognizable-format"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "weird-ua", "context": "weird"}
- )
-
- time.sleep(0.5)
- ev = _last_event(capture_queue, "mcp:tools/call")
- # Implementation falls through to setting client_name = full UA string.
- assert ev.client_name == "not-a-recognizable-format"
- # No crash, no exception in the worker โ by virtue of getting here.
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/official/test_session_http.py -v`
-Expected: 4 PASS.
-
-NOTE: `ClientSession(..., client_info=...)` must be supported by the installed `mcp` SDK version. If the test fails because `client_info` is not a kwarg, replace the kwarg with the SDK-specific call signature (the test asserts behavior, not specific signature).
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/official/test_session_http.py
-git commit -m "test(e2e): client_info extraction from real HTTP headers
-
-4 tests: User-Agent parsing, X-MCP-Client-* override, initialize clientInfo
-takes precedence, malformed User-Agent doesn't crash."
-```
-
----
-
-## Task 6: `tests/e2e/official/test_stateless_http.py` (3 tests)
-
-**Files:**
-- Create: `tests/e2e/official/test_stateless_http.py`
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""Stateless mode behavior over real Streamable HTTP."""
-
-from __future__ import annotations
-
-import asyncio
-import time
-
-import pytest
-from mcp import ClientSession
-from mcp.client.streamable_http import streamablehttp_client
-
-from mcpcat import MCPCatOptions
-
-
-def MCPCAT_OPTIONS_FACTORY() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True, stateless=True)
-
-
-pytestmark = pytest.mark.e2e
-
-
-@pytest.mark.asyncio
-async def test_stateless_mode_returns_null_session_id(
- official_http_server, capture_queue
-):
- url, _ = official_http_server
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "s", "context": "stateless"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events
- # In stateless mode, the SDK-level session_id field on the event is None
- # (server-issued sessions are disabled). The transport-level mcp-session-id
- # header may or may not be present depending on SDK behavior.
- assert call_events[0].session_id is None
-
-
-@pytest.mark.asyncio
-async def test_stateless_two_clients_different_uas_dont_bleed(
- official_http_server, capture_queue
-):
- """Concurrent stateless requests with different User-Agents must produce
- events whose client_name reflects the *requesting* connection, not a
- cached value from a different connection."""
- url, _ = official_http_server
-
- async def call_with_ua(ua: str, text: str) -> None:
- async with streamablehttp_client(url, headers={"User-Agent": ua}) as (
- read,
- write,
- _,
- ):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": text, "context": "no-bleed"}
- )
-
- await asyncio.gather(
- call_with_ua("Cursor/2.6.22", "a"),
- call_with_ua("Claude/1.0.0", "b"),
- )
- time.sleep(0.7)
-
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- client_names = {ev.client_name for ev in call_events}
- assert "Cursor" in client_names and "Claude" in client_names, (
- f"stateless mode bled client_info across requests: {client_names}"
- )
-
-
-@pytest.mark.asyncio
-async def test_stateless_no_session_info_pollution(
- official_http_server, capture_queue
-):
- """After multiple stateless requests, the server's data.session_info
- fields should remain unset (None), proving we're not caching."""
- url, server = official_http_server
-
- async with streamablehttp_client(
- url, headers={"User-Agent": "First/1.0"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool("add_todo", {"text": "1", "context": "x"})
-
- async with streamablehttp_client(
- url, headers={"User-Agent": "Second/2.0"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool("add_todo", {"text": "2", "context": "x"})
-
- time.sleep(0.5)
-
- from mcpcat.modules.internal import get_server_tracking_data
-
- data = get_server_tracking_data(server)
- assert data is not None
- # In stateless mode, we never write to session_info.client_name.
- assert data.session_info.client_name is None, (
- f"stateless mode polluted session_info.client_name = "
- f"{data.session_info.client_name}"
- )
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/official/test_stateless_http.py -v`
-Expected: 3 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/official/test_stateless_http.py
-git commit -m "test(e2e): stateless mode behavior over real HTTP
-
-3 tests: null session_id, no client_info bleed across concurrent clients,
-no session_info pollution after multiple stateless requests."
-```
-
----
-
-## Task 7: `tests/e2e/official/test_identify_http.py` (5 tests)
-
-**Files:**
-- Create: `tests/e2e/official/test_identify_http.py`
-
-NOTE: This file relies on the `mcpcat:identify` self-event introduced in `61bd6a2 feat: run identify hook per event and drop session identity cache`. Identify hook now runs on every event in both stateful and stateless modes.
-
-The identify-hook scenarios vary per test (each test defines its own callback), which means we can't use the module-scoped server with a single options factory. Solution: this file declares no `MCPCAT_OPTIONS_FACTORY` (so the harness uses tracing-only defaults with NO identify hook), and each test patches `MCPCatData.options.identify` on the running server's tracking data before exercising the call. This is the same pattern `tests/test_stateless.py` uses.
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""Identify-per-event behavior over real Streamable HTTP.
-
-Tests mutate the running server's MCPCatData.options.identify to vary the hook
-per scenario. The default options-factory is tracing-only with no identify;
-identifyswapping on the live server matches the pattern used by
-tests/test_stateless.py.
-"""
-
-from __future__ import annotations
-
-import time
-from typing import Any, Optional
-
-import pytest
-from mcp import ClientSession
-from mcp.client.streamable_http import streamablehttp_client
-
-from mcpcat.modules.internal import get_server_tracking_data
-from mcpcat.types import UserIdentity
-
-
-pytestmark = pytest.mark.e2e
-
-
-def _set_identify(server, fn) -> None:
- data = get_server_tracking_data(server)
- assert data is not None
- data.options.identify = fn
-
-
-def _last_call(capture_queue):
- return [e for e in capture_queue if e.event_type == "mcp:tools/call"][-1]
-
-
-@pytest.mark.asyncio
-async def test_identify_hook_receives_real_request_extra(
- official_http_server, capture_queue
-):
- url, server = official_http_server
- received_extras: list = []
-
- def identify(request: Any, extra: Any) -> Optional[UserIdentity]:
- received_extras.append(extra)
- return UserIdentity(user_id="alice", user_name="Alice", user_data=None)
-
- _set_identify(server, identify)
- try:
- async with streamablehttp_client(
- url, headers={"X-Identify-Hook": "yes"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "id", "context": "id"}
- )
-
- time.sleep(0.5)
- # Hook was called at least once with a real extra. The "extra" passed
- # to the hook is the (request_context, request_context) pair the SDK
- # provides; the exact shape varies by version, but it must be non-None.
- assert received_extras, "identify hook never invoked"
- # And the captured event should reflect the identity.
- ev = _last_call(capture_queue)
- assert ev.identify_actor_given_id == "alice"
- finally:
- _set_identify(server, None)
-
-
-@pytest.mark.asyncio
-async def test_mcpcat_identify_self_event_published_per_request(
- official_http_server, capture_queue
-):
- url, server = official_http_server
-
- def identify(_req: Any, _extra: Any) -> Optional[UserIdentity]:
- return UserIdentity(user_id="bob", user_name=None, user_data=None)
-
- _set_identify(server, identify)
- try:
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "self", "context": "x"}
- )
-
- time.sleep(0.5)
- identify_events = [
- e for e in capture_queue if e.event_type == "mcpcat:identify"
- ]
- assert identify_events, (
- f"expected mcpcat:identify event, got "
- f"{[e.event_type for e in capture_queue]}"
- )
- assert identify_events[0].identify_actor_given_id == "bob"
- finally:
- _set_identify(server, None)
-
-
-@pytest.mark.asyncio
-async def test_identify_can_change_identity_mid_session(
- official_http_server, capture_queue
-):
- url, server = official_http_server
- counter = {"n": 0}
-
- def identify(_req: Any, _extra: Any) -> Optional[UserIdentity]:
- counter["n"] += 1
- if counter["n"] == 1:
- return UserIdentity(user_id="user-A", user_name=None, user_data=None)
- return UserIdentity(user_id="user-B", user_name=None, user_data=None)
-
- _set_identify(server, identify)
- try:
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize() # n=1 -> user-A
- await client.call_tool( # n=2 -> user-B
- "add_todo", {"text": "mid", "context": "x"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- init_events = [e for e in capture_queue if e.event_type == "mcp:initialize"]
- assert init_events and call_events
- assert init_events[0].identify_actor_given_id == "user-A"
- assert call_events[0].identify_actor_given_id == "user-B"
- finally:
- _set_identify(server, None)
-
-
-@pytest.mark.asyncio
-async def test_identify_returning_none_yields_no_self_event(
- official_http_server, capture_queue
-):
- url, server = official_http_server
-
- def identify(_req: Any, _extra: Any) -> Optional[UserIdentity]:
- return None
-
- _set_identify(server, identify)
- try:
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "none", "context": "x"}
- )
-
- time.sleep(0.5)
- identify_events = [
- e for e in capture_queue if e.event_type == "mcpcat:identify"
- ]
- assert not identify_events, (
- f"identify returned None; should NOT publish self-event, got "
- f"{len(identify_events)}"
- )
- finally:
- _set_identify(server, None)
-
-
-@pytest.mark.asyncio
-async def test_identify_exception_does_not_break_tool_call(
- official_http_server, capture_queue
-):
- url, server = official_http_server
-
- def identify(_req: Any, _extra: Any) -> Optional[UserIdentity]:
- raise RuntimeError("identify exploded")
-
- _set_identify(server, identify)
- try:
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- # Tool call must still succeed despite identify raising.
- await client.call_tool(
- "add_todo", {"text": "boom", "context": "x"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events, "tool/call event must still publish despite hook crash"
- finally:
- _set_identify(server, None)
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/official/test_identify_http.py -v`
-Expected: 5 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/official/test_identify_http.py
-git commit -m "test(e2e): identify-per-event behavior over real HTTP
-
-5 tests: hook receives real extra; mcpcat:identify self-event per request;
-mid-session identity change; None return suppresses self-event;
-exception in hook doesn't break tool_call."
-```
-
----
-
-## Task 8: `tests/e2e/official/test_redaction_http.py` (3 tests)
-
-**Files:**
-- Create: `tests/e2e/official/test_redaction_http.py`
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""Redaction over real-wire payloads."""
-
-from __future__ import annotations
-
-import time
-
-import pytest
-from mcp import ClientSession
-from mcp.client.streamable_http import streamablehttp_client
-
-from mcpcat import MCPCatOptions
-from mcpcat.modules.internal import get_server_tracking_data
-
-
-pytestmark = pytest.mark.e2e
-
-
-def _set_redact(server, fn) -> None:
- data = get_server_tracking_data(server)
- assert data is not None
- data.options.redact_sensitive_information = fn
-
-
-@pytest.mark.asyncio
-async def test_redact_function_runs_on_real_event_payload(
- official_http_server, capture_queue
-):
- url, server = official_http_server
-
- def redact(s: str) -> str:
- return s.replace("secret-todo-text", "[REDACTED]")
-
- _set_redact(server, redact)
- try:
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo",
- {"text": "secret-todo-text", "context": "redact"},
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events
- params = call_events[0].parameters or {}
- # The redaction walks all string fields recursively.
- text = (params.get("arguments") or {}).get("text", "")
- assert "secret-todo-text" not in text
- assert "[REDACTED]" in text
- finally:
- _set_redact(server, None)
-
-
-@pytest.mark.asyncio
-async def test_redaction_can_scrub_authorization_header_in_extra(
- official_http_server, capture_queue
-):
- url, server = official_http_server
-
- def redact(s: str) -> str:
- if s.startswith("Bearer "):
- return "Bearer [REDACTED]"
- return s
-
- _set_redact(server, redact)
- try:
- async with streamablehttp_client(
- url, headers={"Authorization": "Bearer super-secret-token-xyz"}
- ) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "auth", "context": "auth"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events
- headers = (
- (call_events[0].parameters or {})
- .get("extra", {})
- .get("requestInfo", {})
- .get("headers", {})
- )
- auth = headers.get("authorization")
- assert auth is not None
- assert "super-secret-token-xyz" not in auth
- assert auth == "Bearer [REDACTED]"
- finally:
- _set_redact(server, None)
-
-
-@pytest.mark.asyncio
-async def test_redaction_failure_drops_event(official_http_server, capture_queue):
- url, server = official_http_server
-
- def redact(_s: str) -> str:
- raise RuntimeError("redaction exploded")
-
- _set_redact(server, redact)
- try:
- async with streamablehttp_client(url) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool(
- "add_todo", {"text": "drop", "context": "drop"}
- )
-
- time.sleep(1.0)
- # Event_queue logs and drops the event when redaction raises. Verify
- # no tool/call event was published. (Initialize event was also redacted
- # and dropped.)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert not call_events, (
- f"redaction failure must drop event, got {len(call_events)}"
- )
- finally:
- _set_redact(server, None)
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/official/test_redaction_http.py -v`
-Expected: 3 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/official/test_redaction_http.py
-git commit -m "test(e2e): redaction over real-wire payloads
-
-3 tests: redact runs on event params; can scrub Authorization header in
-extra.requestInfo.headers; redaction raise drops event."
-```
-
----
-
-## Task 9: Community v3 harness + smoke test
-
-**Files:**
-- Create: `tests/e2e/community_v3/__init__.py`
-- Create: `tests/e2e/community_v3/conftest.py`
-- Create: `tests/e2e/community_v3/test_event_capture_http.py` (smoke โ 1 test)
-
-NOTE: This task assumes the FastMCP package installed in `.venv` exposes the v3 API. The shttp_server fixture pattern lifts the approach used by `model-context-protocol-sdks/fastmcp/tests/server/http/test_http_dependencies.py`.
-
-- [ ] **Step 1: Create `tests/e2e/community_v3/__init__.py`** (empty)
-
-```bash
-mkdir -p tests/e2e/community_v3 && : > tests/e2e/community_v3/__init__.py
-```
-
-- [ ] **Step 2: Create `tests/e2e/community_v3/conftest.py`**
-
-```python
-"""FastMCP v3 Streamable-HTTP harness.
-
-Boots a community FastMCP v3 server with `mcp.run_streamable_http_async(...)`
-on a random port; tests connect with `fastmcp.Client(StreamableHttpTransport(url, headers=...))`.
-
-Module-scoped: one boot per test file.
-"""
-
-from __future__ import annotations
-
-import asyncio
-import threading
-from typing import Any, Callable, Tuple
-
-import pytest
-
-import mcpcat
-from mcpcat import MCPCatOptions
-
-from tests.e2e._helpers import find_free_port, wait_for_port
-
-try:
- from fastmcp import FastMCP
-
- HAS_FASTMCP_V3 = True
-except ImportError:
- FastMCP = None # type: ignore
- HAS_FASTMCP_V3 = False
-
-
-def _create_v3_todo_server() -> Any:
- if FastMCP is None:
- raise RuntimeError("fastmcp v3 is not installed; cannot run v3 e2e tests")
- mcp = FastMCP("v3-todo-server")
-
- @mcp.tool
- def add_todo(text: str, context: str = "") -> str:
- return f'Added todo: "{text}"'
-
- @mcp.tool
- def list_todos(context: str = "") -> str:
- return "no todos"
-
- return mcp
-
-
-def _default_options_factory() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True)
-
-
-@pytest.fixture(scope="module")
-def v3_http_server(request) -> Tuple[str, Any]:
- if not HAS_FASTMCP_V3:
- pytest.skip("fastmcp v3 not installed")
-
- options_factory: Callable[[], MCPCatOptions] = getattr(
- request.module, "MCPCAT_OPTIONS_FACTORY", _default_options_factory
- )
- options = options_factory()
- server = _create_v3_todo_server()
- mcpcat.track(server, "test_project", options)
-
- port = find_free_port()
- loop_ready = threading.Event()
- loop_holder: dict = {}
-
- def _run() -> None:
- loop = asyncio.new_event_loop()
- loop_holder["loop"] = loop
- asyncio.set_event_loop(loop)
- loop_ready.set()
- try:
- loop.run_until_complete(
- server.run_streamable_http_async(host="127.0.0.1", port=port)
- )
- except Exception:
- pass
-
- thread = threading.Thread(target=_run, daemon=True)
- thread.start()
- loop_ready.wait(timeout=5.0)
- try:
- wait_for_port(port, timeout=5.0)
- except TimeoutError:
- loop = loop_holder.get("loop")
- if loop:
- loop.call_soon_threadsafe(loop.stop)
- thread.join(timeout=2.0)
- raise
-
- url = f"http://127.0.0.1:{port}/mcp"
- yield url, server
-
- loop = loop_holder.get("loop")
- if loop:
- loop.call_soon_threadsafe(loop.stop)
- thread.join(timeout=5.0)
-```
-
-- [ ] **Step 3: Create `tests/e2e/community_v3/test_event_capture_http.py` (smoke โ 1 test)**
-
-```python
-"""FastMCP v3 event-capture smoke."""
-
-from __future__ import annotations
-
-import time
-
-import pytest
-
-
-pytestmark = pytest.mark.e2e
-
-
-@pytest.mark.asyncio
-async def test_initialize_via_v3(v3_http_server, capture_queue):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _server = v3_http_server
- async with Client(StreamableHttpTransport(url)) as client:
- # Connecting performs the initialize handshake.
- await client.list_tools()
-
- time.sleep(0.5)
- init_events = [e for e in capture_queue if e.event_type == "mcp:initialize"]
- assert init_events, (
- f"expected mcp:initialize, got {[e.event_type for e in capture_queue]}"
- )
-```
-
-- [ ] **Step 4: Run the smoke**
-
-Run: `uv run pytest tests/e2e/community_v3/test_event_capture_http.py -v`
-Expected: PASS (1 test).
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add tests/e2e/community_v3/
-git commit -m "test(e2e): community FastMCP v3 harness + smoke
-
-Adds run_streamable_http_async-in-thread fixture and an initialize
-smoke test proving the harness wires up correctly."
-```
-
----
-
-## Task 10: Complete `tests/e2e/community_v3/test_event_capture_http.py` (3 tests)
-
-**Files:**
-- Modify: `tests/e2e/community_v3/test_event_capture_http.py`
-
-- [ ] **Step 1: Replace the file with the full set**
-
-```python
-"""FastMCP v3 event-capture tests over real Streamable HTTP."""
-
-from __future__ import annotations
-
-import time
-
-import pytest
-
-pytestmark = pytest.mark.e2e
-
-
-@pytest.mark.asyncio
-async def test_initialize_via_v3(v3_http_server, capture_queue):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v3_http_server
- async with Client(StreamableHttpTransport(url)) as client:
- await client.list_tools()
-
- time.sleep(0.5)
- assert any(e.event_type == "mcp:initialize" for e in capture_queue)
-
-
-@pytest.mark.asyncio
-async def test_call_tool_via_v3(v3_http_server, capture_queue):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v3_http_server
- async with Client(StreamableHttpTransport(url)) as client:
- await client.call_tool(
- "add_todo", {"text": "v3-call", "context": "x"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events
- assert call_events[0].resource_name == "add_todo"
-
-
-@pytest.mark.asyncio
-async def test_list_tools_via_v3(v3_http_server, capture_queue):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v3_http_server
- async with Client(StreamableHttpTransport(url)) as client:
- await client.list_tools()
-
- time.sleep(0.5)
- assert any(e.event_type == "mcp:tools/list" for e in capture_queue)
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/community_v3/test_event_capture_http.py -v`
-Expected: 3 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/community_v3/test_event_capture_http.py
-git commit -m "test(e2e): complete v3 event-capture suite (3 tests)"
-```
-
----
-
-## Task 11: `tests/e2e/community_v3/test_request_extra_http.py` (3 tests)
-
-**Files:**
-- Create: `tests/e2e/community_v3/test_request_extra_http.py`
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""parameters.extra.requestInfo.headers parity for FastMCP v3 over real HTTP."""
-
-from __future__ import annotations
-
-import time
-
-import pytest
-
-pytestmark = pytest.mark.e2e
-
-
-def _last_call(capture_queue):
- return [e for e in capture_queue if e.event_type == "mcp:tools/call"][-1]
-
-
-def _extra(event):
- return (event.parameters or {}).get("extra", {})
-
-
-@pytest.mark.asyncio
-async def test_custom_header_lands_in_extra(v3_http_server, capture_queue):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v3_http_server
- async with Client(
- StreamableHttpTransport(url, headers={"X-V3-Header": "v3-value"})
- ) as client:
- await client.call_tool(
- "add_todo", {"text": "v3-h", "context": "v3-h"}
- )
-
- time.sleep(0.5)
- headers = _extra(_last_call(capture_queue)).get("requestInfo", {}).get(
- "headers", {}
- )
- assert headers.get("x-v3-header") == "v3-value", (
- f"expected x-v3-header in extra.requestInfo.headers, got {headers}"
- )
-
-
-@pytest.mark.asyncio
-async def test_initialize_event_carries_headers_via_v3(
- v3_http_server, capture_queue
-):
- """Initialize-event headers must arrive whether via the primary
- request_context.request path or the FastMCP _current_http_request fallback."""
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v3_http_server
- async with Client(
- StreamableHttpTransport(url, headers={"X-V3-Init": "init-v"})
- ) as client:
- await client.list_tools()
-
- time.sleep(0.5)
- init_events = [e for e in capture_queue if e.event_type == "mcp:initialize"]
- assert init_events
- headers = (
- (init_events[0].parameters or {})
- .get("extra", {})
- .get("requestInfo", {})
- .get("headers", {})
- )
- assert headers.get("x-v3-init") == "init-v", (
- f"expected x-v3-init on initialize event, got headers={headers}"
- )
-
-
-@pytest.mark.asyncio
-async def test_meta_progresstoken_passes_through(v3_http_server, capture_queue):
- """If the FastMCP v3 client surfaces a way to set progressToken, verify it
- rides through extra.meta. If not, just verify extra.meta is at least a dict."""
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v3_http_server
- async with Client(StreamableHttpTransport(url)) as client:
- await client.call_tool(
- "add_todo", {"text": "v3-meta", "context": "meta"}
- )
-
- time.sleep(0.5)
- extra = _extra(_last_call(capture_queue))
- # meta key may or may not be present depending on whether the client
- # supplied progressToken; if present, it must be a dict.
- meta = extra.get("meta")
- if meta is not None:
- assert isinstance(meta, dict)
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/community_v3/test_request_extra_http.py -v`
-Expected: 3 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/community_v3/test_request_extra_http.py
-git commit -m "test(e2e): v3 parameters.extra.requestInfo.headers parity (3 tests)"
-```
-
----
-
-## Task 12: `tests/e2e/community_v3/test_stateless_http.py` (2 tests)
-
-**Files:**
-- Create: `tests/e2e/community_v3/test_stateless_http.py`
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""FastMCP v3 stateless mode over real HTTP."""
-
-from __future__ import annotations
-
-import asyncio
-import time
-
-import pytest
-
-from mcpcat import MCPCatOptions
-
-
-def MCPCAT_OPTIONS_FACTORY() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True, stateless=True)
-
-
-pytestmark = pytest.mark.e2e
-
-
-@pytest.mark.asyncio
-async def test_v3_stateless_via_global_settings(v3_http_server, capture_queue):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v3_http_server
- async with Client(StreamableHttpTransport(url)) as client:
- await client.call_tool(
- "add_todo", {"text": "s", "context": "stateless-v3"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events
- assert call_events[0].session_id is None
-
-
-@pytest.mark.asyncio
-async def test_v3_stateless_two_clients_dont_bleed(
- v3_http_server, capture_queue
-):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v3_http_server
-
- async def call(ua: str, text: str) -> None:
- async with Client(
- StreamableHttpTransport(url, headers={"User-Agent": ua})
- ) as client:
- await client.call_tool(
- "add_todo", {"text": text, "context": "no-bleed"}
- )
-
- await asyncio.gather(
- call("Cursor/2.6.22", "a"), call("Claude/1.0.0", "b")
- )
- time.sleep(0.7)
-
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- client_names = {ev.client_name for ev in call_events}
- assert "Cursor" in client_names and "Claude" in client_names, (
- f"v3 stateless mode bled client_info: {client_names}"
- )
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/community_v3/test_stateless_http.py -v`
-Expected: 2 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/community_v3/test_stateless_http.py
-git commit -m "test(e2e): v3 stateless mode over real HTTP (2 tests)"
-```
-
----
-
-## Task 13: `tests/e2e/community_v3/test_identify_http.py` (2 tests)
-
-**Files:**
-- Create: `tests/e2e/community_v3/test_identify_http.py`
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""Identify-per-event behavior under FastMCP v3 middleware over real HTTP."""
-
-from __future__ import annotations
-
-import time
-from typing import Any, Optional
-
-import pytest
-
-from mcpcat.modules.internal import get_server_tracking_data
-from mcpcat.types import UserIdentity
-
-
-pytestmark = pytest.mark.e2e
-
-
-def _set_identify(server, fn) -> None:
- data = get_server_tracking_data(server)
- assert data is not None
- data.options.identify = fn
-
-
-@pytest.mark.asyncio
-async def test_identify_hook_runs_under_v3_middleware(
- v3_http_server, capture_queue
-):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, server = v3_http_server
- seen: list = []
-
- def identify(_req: Any, extra: Any) -> Optional[UserIdentity]:
- seen.append(extra)
- return UserIdentity(user_id="v3-user", user_name=None, user_data=None)
-
- _set_identify(server, identify)
- try:
- async with Client(StreamableHttpTransport(url)) as client:
- await client.call_tool(
- "add_todo", {"text": "id-v3", "context": "id"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events
- assert call_events[0].identify_actor_given_id == "v3-user"
- assert seen, "identify hook never invoked under v3"
- finally:
- _set_identify(server, None)
-
-
-@pytest.mark.asyncio
-async def test_mcpcat_identify_self_event_via_v3_middleware(
- v3_http_server, capture_queue
-):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, server = v3_http_server
-
- def identify(_req: Any, _extra: Any) -> Optional[UserIdentity]:
- return UserIdentity(user_id="v3-bob", user_name=None, user_data=None)
-
- _set_identify(server, identify)
- try:
- async with Client(StreamableHttpTransport(url)) as client:
- await client.call_tool(
- "add_todo", {"text": "self-v3", "context": "x"}
- )
-
- time.sleep(0.5)
- identify_events = [
- e for e in capture_queue if e.event_type == "mcpcat:identify"
- ]
- assert identify_events, (
- f"expected mcpcat:identify under v3, got "
- f"{[e.event_type for e in capture_queue]}"
- )
- assert identify_events[0].identify_actor_given_id == "v3-bob"
- finally:
- _set_identify(server, None)
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/community_v3/test_identify_http.py -v`
-Expected: 2 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/community_v3/test_identify_http.py
-git commit -m "test(e2e): v3 identify-per-event over real HTTP (2 tests)"
-```
-
----
-
-## Task 14: Community v2 harness + headers smoke
-
-**Files:**
-- Create: `tests/e2e/community_v2/__init__.py`
-- Create: `tests/e2e/community_v2/conftest.py`
-- Create: `tests/e2e/community_v2/test_request_extra_http.py` (1 test)
-
-NOTE: Community FastMCP v2 has a different runtime API than v3. The `mcp.run(transport="streamable-http", host=..., port=...)` form works but blocks. Use the same uvicorn-in-thread pattern as the official harness, calling `streamable_http_app()` on the v2 server. If `streamable_http_app` is not exposed at the v2 API, fall back to `mcp.run_async(...)` in a thread (similar to v3 but for v2). Verify which is available before proceeding.
-
-- [ ] **Step 1: Create `tests/e2e/community_v2/__init__.py`** (empty)
-
-```bash
-mkdir -p tests/e2e/community_v2 && : > tests/e2e/community_v2/__init__.py
-```
-
-- [ ] **Step 2: Identify the v2 HTTP entry point**
-
-Run: `uv run python -c "from fastmcp import FastMCP; m = FastMCP('x'); print([a for a in dir(m) if 'http' in a.lower() or 'streamable' in a.lower()])"`
-
-Pick the appropriate API from output:
-- If `streamable_http_app` is on the list โ use the official-style uvicorn pattern.
-- If `run_streamable_http_async` is the primary surface โ use the v3-style threaded asyncio loop pattern.
-
-- [ ] **Step 3: Create `tests/e2e/community_v2/conftest.py`**
-
-If `streamable_http_app` is available (most likely path):
-
-```python
-"""Community FastMCP v2 Streamable-HTTP harness."""
-
-from __future__ import annotations
-
-import threading
-from typing import Any, Callable, Tuple
-
-import pytest
-import uvicorn
-
-import mcpcat
-from mcpcat import MCPCatOptions
-
-from tests.e2e._helpers import find_free_port, wait_for_port
-
-try:
- from fastmcp import FastMCP as CommunityFastMCP
- HAS_FASTMCP_V2 = True
-except ImportError:
- CommunityFastMCP = None # type: ignore
- HAS_FASTMCP_V2 = False
-
-
-def _create_v2_todo_server() -> Any:
- if CommunityFastMCP is None:
- raise RuntimeError("community fastmcp not installed")
- mcp = CommunityFastMCP("v2-todo")
-
- @mcp.tool()
- def add_todo(text: str, context: str = "") -> str:
- return f'Added: "{text}"'
-
- return mcp
-
-
-def _default_options_factory() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True)
-
-
-@pytest.fixture(scope="module")
-def v2_http_server(request) -> Tuple[str, Any]:
- if not HAS_FASTMCP_V2:
- pytest.skip("community fastmcp not installed")
-
- options_factory: Callable[[], MCPCatOptions] = getattr(
- request.module, "MCPCAT_OPTIONS_FACTORY", _default_options_factory
- )
- server = _create_v2_todo_server()
- mcpcat.track(server, "test_project", options_factory())
-
- # Use whichever ASGI factory v2 exposes. Adjust this line per Step 2 finding.
- app = server.streamable_http_app()
- port = find_free_port()
- config = uvicorn.Config(
- app, host="127.0.0.1", port=port, log_level="error", lifespan="on"
- )
- uv_server = uvicorn.Server(config)
- thread = threading.Thread(target=uv_server.run, daemon=True)
- thread.start()
- try:
- wait_for_port(port, timeout=5.0)
- except TimeoutError:
- uv_server.should_exit = True
- thread.join(timeout=2.0)
- raise
-
- url = f"http://127.0.0.1:{port}/mcp"
- yield url, server
- uv_server.should_exit = True
- thread.join(timeout=5.0)
-```
-
-If only `run_streamable_http_async` is available, mirror the v3 conftest pattern (create an asyncio loop in a thread, run `await server.run_streamable_http_async(host, port)`).
-
-- [ ] **Step 4: Create `tests/e2e/community_v2/test_request_extra_http.py`**
-
-```python
-"""Community FastMCP v2 headers smoke."""
-
-from __future__ import annotations
-
-import time
-
-import pytest
-
-
-pytestmark = pytest.mark.e2e
-
-
-@pytest.mark.asyncio
-async def test_v2_custom_header_lands_in_extra(v2_http_server, capture_queue):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, _ = v2_http_server
- async with Client(
- StreamableHttpTransport(url, headers={"X-V2-Header": "v2-value"})
- ) as client:
- await client.call_tool(
- "add_todo", {"text": "v2-h", "context": "v2-h"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events
- headers = (
- (call_events[0].parameters or {})
- .get("extra", {})
- .get("requestInfo", {})
- .get("headers", {})
- )
- assert headers.get("x-v2-header") == "v2-value", (
- f"expected x-v2-header in extra.requestInfo.headers, got {headers}"
- )
-```
-
-- [ ] **Step 5: Run the file**
-
-Run: `uv run pytest tests/e2e/community_v2/test_request_extra_http.py -v`
-Expected: 1 PASS.
-
-- [ ] **Step 6: Commit**
-
-```bash
-git add tests/e2e/community_v2/
-git commit -m "test(e2e): community v2 harness + headers smoke"
-```
-
----
-
-## Task 15: `tests/e2e/community_v2/test_identify_http.py` (1 test)
-
-**Files:**
-- Create: `tests/e2e/community_v2/test_identify_http.py`
-
-- [ ] **Step 1: Create the file**
-
-```python
-"""Community FastMCP v2 identify-per-event smoke."""
-
-from __future__ import annotations
-
-import time
-from typing import Any, Optional
-
-import pytest
-
-from mcpcat.modules.internal import get_server_tracking_data
-from mcpcat.types import UserIdentity
-
-
-pytestmark = pytest.mark.e2e
-
-
-def _set_identify(server, fn) -> None:
- data = get_server_tracking_data(server)
- assert data is not None
- data.options.identify = fn
-
-
-@pytest.mark.asyncio
-async def test_v2_identify_hook_receives_real_extra(
- v2_http_server, capture_queue
-):
- from fastmcp import Client
- from fastmcp.client.transports import StreamableHttpTransport
-
- url, server = v2_http_server
- seen: list = []
-
- def identify(_req: Any, extra: Any) -> Optional[UserIdentity]:
- seen.append(extra)
- return UserIdentity(user_id="v2-user", user_name=None, user_data=None)
-
- _set_identify(server, identify)
- try:
- async with Client(
- StreamableHttpTransport(url, headers={"X-Identify-V2": "yes"})
- ) as client:
- await client.call_tool(
- "add_todo", {"text": "id-v2", "context": "id"}
- )
-
- time.sleep(0.5)
- call_events = [e for e in capture_queue if e.event_type == "mcp:tools/call"]
- assert call_events
- assert call_events[0].identify_actor_given_id == "v2-user"
- assert seen, "v2 identify hook never invoked"
- finally:
- _set_identify(server, None)
-```
-
-- [ ] **Step 2: Run the file**
-
-Run: `uv run pytest tests/e2e/community_v2/test_identify_http.py -v`
-Expected: 1 PASS.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add tests/e2e/community_v2/test_identify_http.py
-git commit -m "test(e2e): community v2 identify-per-event smoke"
-```
-
----
-
-## Task 16: Final integration check
-
-**Files:**
-- None (verification only)
-
-- [ ] **Step 1: Run the entire e2e suite**
-
-Run: `uv run pytest tests/e2e/ -v`
-Expected: ~38 PASS (26 official + 10 v3 + 2 v2). Runtime: ~25โ30s.
-
-- [ ] **Step 2: Run the entire repo test suite**
-
-Run: `uv run pytest -q`
-Expected: ~443 passing, no regressions, runtime ~165s.
-
-- [ ] **Step 3: Run the community-specific subsuite to confirm no fixture collisions**
-
-Run: `uv run pytest tests/community/ tests/e2e/community_v2/ tests/e2e/community_v3/ -q`
-Expected: All pass.
-
-- [ ] **Step 4: If anything fails**
-
-For each failing test:
-1. Read the failure carefully.
-2. Determine whether it's a wiring issue (test) or a real SDK behavior gap.
-3. If wiring: fix in the affected test file, re-run that file alone.
-4. If real gap: stop and surface the discrepancy โ DO NOT modify production code in this plan; that's a separate change.
-
-- [ ] **Step 5: No commit needed if all green.** If fixes were made in Step 4, commit them as `test(e2e): fix harness/test wiring discovered in final integration`.
-
----
-
-## Self-Review notes
-
-- **Spec coverage:** All 12 spec-listed test files are produced (Tasks 2,3 โ 1; Tasks 4โ8 โ 5 more in official; Tasks 9โ13 โ 5 in v3; Tasks 14โ15 โ 2 in v2).
-- **Identify-per-event coverage:** Task 7 (5 tests) covers the new behavior end-to-end including mid-session change, None return, and exception swallow. Tasks 13 and 15 cover v3 and v2 smoke respectively.
-- **The `test_initialize_uses_fastmcp_get_http_request_fallback` ambiguity** flagged during brainstorming is addressed in Task 11 by reframing to `test_initialize_event_carries_headers_via_v3` โ asserts headers arrive on the initialize event without locking to a specific code path.
-- **v2 harness API uncertainty** is deliberately surfaced as Step 2 of Task 14 (run a probe to identify the available API) rather than assumed.
-- **No placeholders.** All steps include exact code, exact commands, and exact expected output.
-
-## Verification (overall)
-
-- `uv run pytest tests/e2e/ -v` โ ~38 PASS.
-- `uv run pytest -q` โ ~443 PASS (was 405).
-- `git log --oneline origin/main..HEAD` shows ~16 commits, one per task.
diff --git a/docs/superpowers/specs/2026-04-29-e2e-streamable-http-suite-design.md b/docs/superpowers/specs/2026-04-29-e2e-streamable-http-suite-design.md
deleted file mode 100644
index bf16df2..0000000
--- a/docs/superpowers/specs/2026-04-29-e2e-streamable-http-suite-design.md
+++ /dev/null
@@ -1,206 +0,0 @@
-# E2E Streamable-HTTP Test Suite โ Design
-
-## Context
-
-The MCPCat Python SDK currently has 405 in-memory tests. All of them use either:
-
-- The official MCP SDK's `create_connected_server_and_client_session` โ in-memory `MemoryStream`-based transport.
-- FastMCP's `Client(server)` direct in-memory connection.
-
-Neither boots a real ASGI app. As a result, `request_context.request` is always `None` in tests, and code paths that depend on the Starlette `Request` (header capture, header-derived client info, stateless mode behavior, FastMCP's `_current_http_request` ContextVar fallback, the new per-event identify hook receiving real `extra`) only get tested via monkey-patched fakes.
-
-This is brittle. Real users hit the SDK over Streamable HTTP, and we have no test coverage that exercises the actual transport. The recently merged identify-per-event change (`#30`) and the in-flight header-capture work (`feat/request-extra-headers`) both produce signal that only shows up under a real HTTP transport.
-
-This spec defines a transport-parity test suite that covers transport-sensitive scenarios under real Streamable HTTP. The intended outcome is a `tests/e2e/` tree that runs as part of the default `pytest` invocation, gives us confidence the SDK behaves correctly under real HTTP, and grows naturally as new transport-sensitive features are added.
-
-## Scope
-
-**In scope (transport-sensitive scenarios):**
-
-- Event capture for initialize / tools/list / tools/call under real HTTP framing.
-- HTTP header capture into `event.parameters.extra.requestInfo.headers` (TS-parity).
-- Header-derived client info extraction (User-Agent, x-mcp-client-name/version).
-- Stateless mode with real per-request headers (no cross-request bleed).
-- Identify hook receiving real `extra`, mid-session identity changes, `mcpcat:identify` self-events.
-- Redaction over real wire payloads.
-- FastMCP v3's `get_http_request()` ContextVar fallback under real transport.
-
-**Out of scope (transport-agnostic, stays in-memory):**
-
-- Tool-context schema injection (`inputSchema` modification).
-- `get_more_tools` / `report_missing` semantics.
-- Truncation / sanitization / validation pipeline transforms.
-- Telemetry exporter wiring.
-- Tag / property validation rules.
-
-## Layout
-
-```
-tests/e2e/
- __init__.py
- conftest.py # cross-cutting capture-queue fixture
- official/ # official MCP SDK FastMCP โ full set (6 files)
- __init__.py
- conftest.py # uvicorn-in-thread fixture, port helpers
- test_event_capture_http.py
- test_request_extra_http.py
- test_session_http.py
- test_stateless_http.py
- test_identify_http.py
- test_redaction_http.py
- community_v3/ # FastMCP v3 โ full set (4 files)
- __init__.py
- conftest.py # FastMCP-native shttp_server fixture
- test_event_capture_http.py
- test_request_extra_http.py
- test_stateless_http.py
- test_identify_http.py
- community_v2/ # FastMCP v2 โ smoke only (2 files)
- __init__.py
- conftest.py # uvicorn-in-thread for v2 server
- test_request_extra_http.py
- test_identify_http.py
-```
-
-Existing `tests/` and `tests/community/` stay as the in-memory contract. `tests/e2e/` is the additive transport-parity layer.
-
-## Harness
-
-### Top-level `tests/e2e/conftest.py`
-
-A single fixture, `capture_queue`, that mocks the global event queue and yields the list that accumulates published events. Replaces the inline mock-queue idiom currently duplicated across in-memory tests.
-
-```python
-@pytest.fixture
-def capture_queue():
- from mcpcat.modules.event_queue import event_queue as original
- captured: list = []
- mock = MagicMock()
- mock.publish_event = MagicMock(side_effect=lambda req: captured.append(req))
- set_event_queue(EventQueue(api_client=mock))
- yield captured
- set_event_queue(original)
-```
-
-### `tests/e2e/official/conftest.py`
-
-Module-scoped uvicorn-in-thread fixture that boots the official SDK's Streamable-HTTP app on `127.0.0.1:`:
-
-```python
-@pytest.fixture(scope="module")
-def official_http_server(request):
- """Boot a Streamable-HTTP MCP server. One per module.
-
- Tests parametrize indirectly when they need different MCPCatOptions.
- """
- factory = getattr(request, "param", _default_official_factory)
- server, mcpcat_options = factory()
- track(server, "test_project", mcpcat_options)
- app = streamable_http_app(server._mcp_server) # or `server` for low-level
- port = _find_free_port()
- config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="error")
- uv = uvicorn.Server(config)
- thread = threading.Thread(target=uv.run, daemon=True)
- thread.start()
- _wait_for_port(port, timeout=5.0)
- yield f"http://127.0.0.1:{port}/mcp", server
- uv.should_exit = True
- thread.join(timeout=5.0)
-```
-
-Tests connect with `streamablehttp_client(url)`:
-
-```python
-async def test_x(official_http_server, capture_queue):
- url, server = official_http_server
- async with streamablehttp_client(url, headers={"X-Custom": "v"}) as (read, write, _):
- async with ClientSession(read, write) as client:
- await client.initialize()
- await client.call_tool("add_todo", {"text": "hi"})
- assert any("x-custom" in (e.parameters or {}).get("extra", {}).get("requestInfo", {}).get("headers", {}) for e in capture_queue)
-```
-
-### `tests/e2e/community_v3/conftest.py`
-
-Lifts the `shttp_server` fixture pattern from `model-context-protocol-sdks/fastmcp/tests/server/http/test_http_dependencies.py`. Uses `mcp.run_streamable_http_async(host, port)` and `fastmcp.Client(url)` for the client.
-
-### `tests/e2e/community_v2/conftest.py`
-
-Same uvicorn-in-thread shape as official/, but instantiates a community v2 FastMCP server and uses its `streamable_http_app`.
-
-### `tests/e2e/_helpers.py`
-
-- `_find_free_port()` โ bind/release on `127.0.0.1:0`.
-- `_wait_for_port(port, timeout)` โ poll until accepts connection or timeout raises.
-- `mcp_post(url, payload, headers)` โ raw httpx POST for the rare test that needs to send headers the MCP client doesn't expose.
-
-## Test Scenarios
-
-### `tests/e2e/official/`
-
-**`test_event_capture_http.py`** โ `test_initialize_event_captured`, `test_tools_list_event_captured`, `test_tools_call_event_captured`, `test_event_duration_is_non_zero`, `test_concurrent_clients_get_distinct_session_ids`.
-
-**`test_request_extra_http.py`** โ `test_custom_header_lands_in_request_info`, `test_user_agent_preserved`, `test_mcp_session_id_header_promoted_to_extra_session_id`, `test_request_id_present_per_call`, `test_meta_progresstoken_passes_through`, `test_initialize_event_also_carries_extra`.
-
-**`test_session_http.py`** โ `test_user_agent_parsed_into_client_name_version`, `test_x_mcp_client_headers_override_user_agent`, `test_initialize_clientinfo_wins_over_headers`, `test_unparseable_user_agent_does_not_crash`.
-
-**`test_stateless_http.py`** โ `test_stateless_mode_returns_null_session_id`, `test_stateless_two_clients_different_uas_dont_bleed`, `test_stateless_no_session_info_pollution`.
-
-**`test_identify_http.py`** โ `test_identify_hook_receives_real_request_extra`, `test_mcpcat_identify_self_event_published_per_request`, `test_identify_can_change_identity_mid_session`, `test_identify_returning_none_yields_no_self_event`, `test_identify_exception_does_not_break_tool_call`.
-
-**`test_redaction_http.py`** โ `test_redact_function_runs_on_real_event_payload`, `test_redaction_can_scrub_authorization_header_in_extra`, `test_redaction_failure_drops_event`.
-
-### `tests/e2e/community_v3/`
-
-**`test_event_capture_http.py`** โ `test_initialize_via_v3`, `test_call_tool_via_v3`, `test_list_tools_via_v3`.
-
-**`test_request_extra_http.py`** โ `test_custom_header_lands_in_extra`, `test_initialize_event_carries_headers_via_v3` (asserts headers arrive on the initialize event under real v3 transport โ covers both the primary `request_context.request` path and the `_current_http_request` ContextVar fallback, whichever the SDK happens to use), `test_meta_progresstoken_passes_through`.
-
-**`test_stateless_http.py`** โ `test_v3_stateless_via_global_settings`, `test_v3_stateless_two_clients_dont_bleed`.
-
-**`test_identify_http.py`** โ `test_identify_hook_runs_under_v3_middleware`, `test_mcpcat_identify_self_event_via_v3_middleware`.
-
-### `tests/e2e/community_v2/`
-
-**`test_request_extra_http.py`** โ `test_v2_custom_header_lands_in_extra`.
-
-**`test_identify_http.py`** โ `test_v2_identify_hook_receives_real_extra`.
-
-**Total: ~38 tests across 12 modules** โ official (26) + community v3 (10) + community v2 (2).
-
-## Markers and CI
-
-- All e2e tests get `@pytest.mark.e2e`.
-- `pyproject.toml` registers the marker:
- ```toml
- [tool.pytest.ini_options]
- markers = ["e2e: real HTTP transport tests"]
- ```
-- The default `pytest` invocation **includes** e2e (no `-m` filter) โ fast inner-loop dev still gets transport coverage.
-- Estimated runtime impact: ~12 module-scoped uvicorn boots ร ~1.5s = ~20s fixture overhead, plus ~38 tests ร ~50ms = ~2s execution. Total e2e cost: ~25s on top of the existing ~140s suite.
-
-## Failure semantics
-
-- Server-ready timeout (5s) raises a clear fixture-level error.
-- Teardown does `uv.should_exit = True` then `thread.join(timeout=5.0)`; if a thread fails to die we fail teardown loudly rather than leaking.
-- Tests assert event shape, not exact values where nondeterministic (timestamps, ksuid IDs).
-
-## Dependencies
-
-- `uvicorn` and `httpx` are transitive deps of `mcp` and `fastmcp` already.
-- No new top-level dependencies required.
-
-## Reused utilities
-
-- `MCPCatOptions`, `track`, `mcpcat.modules.event_queue.{EventQueue, set_event_queue}` โ existing public API.
-- `streamable_http_app` from `mcp.server.streamable_http_manager` โ official SDK ASGI factory.
-- `streamablehttp_client` from `mcp.client.streamable_http` โ official SDK client.
-- `fastmcp.Client(url)` โ community v3 client.
-- The shttp_server fixture pattern in `model-context-protocol-sdks/fastmcp/tests/server/http/test_http_dependencies.py` โ model for v3 conftest.
-
-## Verification
-
-- `uv run pytest tests/e2e/ -v` โ full e2e suite (target: ~38 passing).
-- `uv run pytest` (default invocation, includes e2e) โ full suite (target: 405 + ~38 = ~443 passing, no regressions).
-- `uv run pytest tests/e2e/official/test_request_extra_http.py -v` โ header capture proven over real HTTP, replacing the monkey-patched fakery in the existing `tests/test_request_extra.py`.
-- Spot-check: in `tests/e2e/community_v3/test_request_extra_http.py::test_initialize_uses_fastmcp_get_http_request_fallback`, assert that initialize-event headers are populated despite `request_context.request` being `None` โ proves the `_current_http_request` ContextVar fallback works end-to-end.
diff --git a/pyproject.toml b/pyproject.toml
index c567d6b..cddbfa0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,15 @@
[project]
-name = "mcpcat"
-version = "0.1.15"
-description = "DEPRECATED โ renamed to 'agentcat' (pip install agentcat). Analytics Tool for MCP Servers - provides insights into MCP tool usage patterns"
+name = "agentcat"
+version = "1.0.0"
+description = "Analytics tool for MCP (Model Context Protocol) servers and AI agents - tracks tool usage patterns and provides insights"
authors = [
- { name = "MCPCat", email = "support@mcpcat.io" },
+ { name = "AgentCat, Inc.", email = "support@agentcat.com" },
]
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.10"
classifiers = [
- "Development Status :: 7 - Inactive",
+ "Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
@@ -19,16 +19,15 @@ classifiers = [
]
dependencies = [
"mcp>=1.2.0",
- "mcpcat-api==0.1.9",
+ "agentcat-api==1.0.0",
"pydantic>=2.0.0,<2.12",
"requests>=2.31.0",
]
[project.urls]
-"Homepage" = "https://github.com/MCPCat/mcpcat-python-sdk"
-"Bug Tracker" = "https://github.com/MCPCat/mcpcat-python-sdk/issues"
-"Repository" = "https://github.com/MCPCat/mcpcat-python-sdk"
-"Migration" = "https://pypi.org/project/agentcat/"
+"Homepage" = "https://github.com/agentcathq/agentcat-python-sdk"
+"Bug Tracker" = "https://github.com/agentcathq/agentcat-python-sdk/issues"
+"Repository" = "https://github.com/agentcathq/agentcat-python-sdk"
[project.optional-dependencies]
community = [
@@ -47,18 +46,18 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
-packages = ["src/mcpcat"]
+packages = ["src/agentcat"]
-# Explicit sdist allowlist: the default file selection sweeps untracked
-# working-tree files (local tool configs, build outputs) into the tarball.
[tool.hatch.build.targets.sdist]
only-include = [
- "src/mcpcat",
+ "src/agentcat",
"tests",
"README.md",
"LICENSE",
"CONTRIBUTING.md",
"pyproject.toml",
+ "docs/static",
+ "docs/cats",
]
[tool.pytest.ini_options]
@@ -80,9 +79,6 @@ filterwarnings = [
# would still surface. Format: action:message:category:module:lineno
"ignore:websockets\\.legacy is deprecated:DeprecationWarning:websockets.legacy",
"ignore:websockets\\.server\\.WebSocketServerProtocol is deprecated:DeprecationWarning:uvicorn.protocols.websockets.websockets_impl",
- # Our own rename notice (mcpcat -> agentcat) fires on every import of the
- # package; visibility is covered by tests/test_deprecation_notice.py.
- "ignore:The 'mcpcat' package has been renamed:FutureWarning",
]
[tool.mypy]
@@ -122,7 +118,7 @@ exclude = [
]
[tool.ruff.isort]
-known-first-party = ["mcpcat"]
+known-first-party = ["agentcat"]
[dependency-groups]
dev = [
diff --git a/src/mcpcat/__init__.py b/src/agentcat/__init__.py
similarity index 74%
rename from src/mcpcat/__init__.py
rename to src/agentcat/__init__.py
index 5e0280f..933c5f3 100644
--- a/src/mcpcat/__init__.py
+++ b/src/agentcat/__init__.py
@@ -1,4 +1,4 @@
-"""MCPCat - Analytics Tool for MCP Servers."""
+"""AgentCat - Analytics Tool for MCP Servers."""
import os
import warnings
@@ -6,22 +6,10 @@
from importlib.metadata import version
from typing import Any
-__version__ = version("mcpcat")
-
-# FutureWarning (not DeprecationWarning): it must be visible by default to end
-# users of servers that embed this SDK, and it writes to stderr, which is safe
-# for stdio MCP transports.
-warnings.warn(
- "The 'mcpcat' package has been renamed to 'agentcat'. This is the final "
- "mcpcat release; it keeps working, but all new development ships in "
- "'agentcat'. Migrate: pip install agentcat (import agentcat; "
- "agentcat.track(...)). See https://pypi.org/project/agentcat/",
- FutureWarning,
- stacklevel=2,
-)
+__version__ = version("agentcat")
-from mcpcat.modules.overrides.mcp_server import override_lowlevel_mcp_server
-from mcpcat.modules.session import get_session_info, new_session_id
+from agentcat.modules.overrides.mcp_server import override_lowlevel_mcp_server
+from agentcat.modules.session import get_session_info, new_session_id
from .modules.compatibility import (
COMPATIBILITY_ERROR_MESSAGE,
@@ -37,8 +25,8 @@
EventPropertiesFunction,
EventTagsFunction,
IdentifyFunction,
- MCPCatData,
- MCPCatOptions,
+ AgentCatData,
+ AgentCatOptions,
RedactionFunction,
UserIdentity,
)
@@ -50,7 +38,7 @@ def _detect_stateless(server) -> bool:
Best-effort: community FastMCP v3 deprecated per-instance .settings
in favor of global fastmcp.settings, but the global isn't per-server.
The deprecated shim is the only per-instance API available.
- MCPCatOptions(stateless=True) is the recommended explicit path.
+ AgentCatOptions(stateless=True) is the recommended explicit path.
"""
try:
with warnings.catch_warnings():
@@ -59,7 +47,7 @@ def _detect_stateless(server) -> bool:
if result:
write_to_log(
"Auto-detected stateless HTTP mode from your FastMCP server's .settings. "
- "If this is incorrect, please pass stateless=False to MCPCatOptions and file a bug report."
+ "If this is incorrect, please pass stateless=False to AgentCatOptions and file a bug report."
)
return result
except (AttributeError, RuntimeError):
@@ -67,14 +55,14 @@ def _detect_stateless(server) -> bool:
def track(
- server: Any, project_id: str | None = None, options: MCPCatOptions | None = None
+ server: Any, project_id: str | None = None, options: AgentCatOptions | None = None
) -> Any:
"""
- Initialize MCPCat tracking with optional telemetry export.
+ Initialize AgentCat tracking with optional telemetry export.
Args:
server: MCP server instance to track
- project_id: MCPCat project ID (optional if using telemetry-only mode)
+ project_id: AgentCat project ID (optional if using telemetry-only mode)
options: Configuration options including telemetry exporters
Returns:
@@ -88,16 +76,16 @@ def track(
Attach custom metadata to every auto-captured event using
`event_tags` (string key-value pairs, validated) and
`event_properties` (flexible JSON). See
- https://docs.mcpcat.io/sdk/event-tags-properties.
+ https://docs.agentcat.com/sdk/event-tags-properties.
- >>> import os, mcpcat
- >>> mcpcat.track(server, "proj_abc123", mcpcat.MCPCatOptions(
+ >>> import os, agentcat
+ >>> agentcat.track(server, "proj_abc123", agentcat.AgentCatOptions(
... event_tags=lambda req, ctx: {"env": os.environ.get("APP_ENV", "dev")},
... event_properties=lambda req, ctx: {"feature_flags": ["dark_mode"]},
... ))
"""
if options is None:
- options = MCPCatOptions()
+ options = AgentCatOptions()
set_debug_mode(options.debug_mode)
@@ -112,7 +100,7 @@ def track(
if not project_id and not options.exporters:
raise ValueError(
"Either project_id or exporters must be provided. "
- "Use project_id for MCPCat, exporters for telemetry-only mode, or both."
+ "Use project_id for AgentCat, exporters for telemetry-only mode, or both."
)
if not is_compatible_server(server):
@@ -140,13 +128,13 @@ def track(
else "lowlevel"
)
write_to_log(
- f"MCPCat setup started | project {project_id or '(telemetry-only)'} | "
+ f"AgentCat setup started | project {project_id or '(telemetry-only)'} | "
f"server {server_kind}"
)
if options.exporters:
- from mcpcat.modules.event_queue import set_telemetry_manager
- from mcpcat.modules.telemetry import TelemetryManager
+ from agentcat.modules.event_queue import set_telemetry_manager
+ from agentcat.modules.telemetry import TelemetryManager
telemetry_manager = TelemetryManager(options.exporters)
set_telemetry_manager(telemetry_manager)
@@ -156,7 +144,7 @@ def track(
session_id = new_session_id()
session_info = get_session_info(lowlevel_server)
- data = MCPCatData(
+ data = AgentCatData(
session_id=session_id,
project_id=project_id,
last_activity=datetime.now(timezone.utc),
@@ -166,10 +154,14 @@ def track(
)
set_server_tracking_data(lowlevel_server, data)
- # Resolve API base URL: option > env var > default
- api_base_url = options.api_base_url or os.environ.get("MCPCAT_API_URL")
+ # Resolve API base URL: option > new env var > legacy env var > default
+ api_base_url = (
+ options.api_base_url
+ or os.environ.get("AGENTCAT_API_URL")
+ or os.environ.get("MCPCAT_API_URL")
+ )
if api_base_url:
- from mcpcat.modules.event_queue import event_queue
+ from agentcat.modules.event_queue import event_queue
event_queue.configure(api_base_url)
if not data.tracker_initialized:
@@ -185,18 +177,18 @@ def track(
if project_id:
write_to_log(
- f"MCPCat initialized with dynamic tracking for session "
+ f"AgentCat initialized with dynamic tracking for session "
f"{session_id} on project {project_id}"
)
else:
write_to_log(
- f"MCPCat initialized in telemetry-only mode for session {session_id}"
+ f"AgentCat initialized in telemetry-only mode for session {session_id}"
)
# Metadata-only setup-complete beacon (INFO). A start-without-complete
# (or the ERROR diagnostics below) signals a failed setup.
write_to_log(
- f"MCPCat setup complete | project {project_id or '(telemetry-only)'} | "
+ f"AgentCat setup complete | project {project_id or '(telemetry-only)'} | "
f"tracing={options.enable_tracing} "
f"context={options.enable_tool_call_context} "
f"report_missing={options.enable_report_missing} "
@@ -209,7 +201,7 @@ def track(
write_to_log(f"Warning: Failed to track server - {e}")
raise
except Exception as e:
- write_to_log(f"Error initializing MCPCat: {e}")
+ write_to_log(f"Error initializing AgentCat: {e}")
return server
@@ -217,14 +209,14 @@ def track(
def _apply_server_tracking(
server: Any,
lowlevel_server: Any,
- data: MCPCatData,
+ data: AgentCatData,
is_community_v3: bool,
is_official_fastmcp: bool,
is_community_v2: bool,
) -> None:
"""Apply the appropriate tracking method based on server type."""
if is_community_v3:
- from mcpcat.modules.overrides.community_v3.integration import (
+ from agentcat.modules.overrides.community_v3.integration import (
apply_community_v3_integration,
)
@@ -234,10 +226,10 @@ def _apply_server_tracking(
)
elif is_official_fastmcp:
- from mcpcat.modules.overrides.mcp_server import (
+ from agentcat.modules.overrides.mcp_server import (
override_lowlevel_mcp_server_minimal,
)
- from mcpcat.modules.overrides.official.monkey_patch import (
+ from agentcat.modules.overrides.official.monkey_patch import (
apply_official_fastmcp_patches,
)
@@ -245,7 +237,7 @@ def _apply_server_tracking(
override_lowlevel_mcp_server_minimal(lowlevel_server, data)
elif is_community_v2:
- from mcpcat.modules.overrides.community.monkey_patch import (
+ from agentcat.modules.overrides.community.monkey_patch import (
patch_community_fastmcp,
)
@@ -260,7 +252,7 @@ def _apply_server_tracking(
# Main API
"track",
# Configuration
- "MCPCatOptions",
+ "AgentCatOptions",
# Types for identify functionality
"UserIdentity",
"IdentifyFunction",
diff --git a/src/mcpcat/modules/__init__.py b/src/agentcat/modules/__init__.py
similarity index 96%
rename from src/mcpcat/modules/__init__.py
rename to src/agentcat/modules/__init__.py
index 66f8cde..4a0d2a2 100644
--- a/src/mcpcat/modules/__init__.py
+++ b/src/agentcat/modules/__init__.py
@@ -1,4 +1,4 @@
-"""MCPCat modules."""
+"""AgentCat modules."""
from .compatibility import is_compatible_server, is_official_fastmcp_server
from .context_parameters import (
diff --git a/src/mcpcat/modules/compatibility.py b/src/agentcat/modules/compatibility.py
similarity index 99%
rename from src/mcpcat/modules/compatibility.py
rename to src/agentcat/modules/compatibility.py
index 57a1070..fe43d3f 100644
--- a/src/mcpcat/modules/compatibility.py
+++ b/src/agentcat/modules/compatibility.py
@@ -187,7 +187,7 @@ def has_necessary_attributes(server: Any) -> bool:
def is_compatible_server(server: Any) -> bool:
- """Check if the server is compatible with MCPCat."""
+ """Check if the server is compatible with AgentCat."""
# If it's FastMCP v3 (community), it's compatible
if is_community_fastmcp_v3(server):
return True
diff --git a/src/mcpcat/modules/constants.py b/src/agentcat/modules/constants.py
similarity index 83%
rename from src/mcpcat/modules/constants.py
rename to src/agentcat/modules/constants.py
index d6dda6a..8146fb4 100644
--- a/src/mcpcat/modules/constants.py
+++ b/src/agentcat/modules/constants.py
@@ -1,9 +1,9 @@
INACTIVITY_TIMEOUT_IN_MINUTES = 30
-LOG_PATH = "mcpcat.log" # Default log file path
+LOG_PATH = "agentcat.log" # Default log file path
SESSION_ID_PREFIX = "ses"
EVENT_ID_PREFIX = "evt"
-MCPCAT_API_URL = "https://api.mcpcat.io" # Default API URL for MCPCat events
-MCPCAT_SOURCE = "mcpcat" # Source attribution for telemetry exporters
+AGENTCAT_API_URL = "https://api.agentcat.com" # Default API URL for AgentCat events
+AGENTCAT_SOURCE = "agentcat" # Source attribution for telemetry exporters
DEFAULT_CONTEXT_DESCRIPTION = "Explain why you are calling this tool and how it fits into the user's overall goal. This parameter is used for analytics and user intent tracking. YOU MUST provide 15-25 words (count carefully). NEVER use first person ('I', 'we', 'you') - maintain third-person perspective. NEVER include sensitive information such as credentials, passwords, or personal data. Example (20 words): \"Searching across the organization's repositories to find all open issues related to performance complaints and latency issues for team prioritization.\""
# Maximum number of exceptions to capture in a cause chain
@@ -13,7 +13,7 @@
MAX_STACK_FRAMES = 50
# Internal SDK diagnostics (privacy-first, metadata-only OTLP logs)
-DIAGNOSTICS_SCOPE_NAME = "mcpcat-diagnostics"
+DIAGNOSTICS_SCOPE_NAME = "agentcat-diagnostics"
DEFAULT_DIAGNOSTICS_ENDPOINT = "https://otel.agentcat.com"
# Public shared ingestion key โ NOT a secret; ships in the package to deter
# drive-by traffic, paired with a server-side rate limit. Override with the
diff --git a/src/mcpcat/modules/context_parameters.py b/src/agentcat/modules/context_parameters.py
similarity index 100%
rename from src/mcpcat/modules/context_parameters.py
rename to src/agentcat/modules/context_parameters.py
diff --git a/src/mcpcat/modules/diagnostics.py b/src/agentcat/modules/diagnostics.py
similarity index 94%
rename from src/mcpcat/modules/diagnostics.py
rename to src/agentcat/modules/diagnostics.py
index cef0131..073c93a 100644
--- a/src/mcpcat/modules/diagnostics.py
+++ b/src/agentcat/modules/diagnostics.py
@@ -1,14 +1,14 @@
"""Privacy-first internal SDK diagnostics.
-Mirrors every internal MCPCat log line to MCPCat's own monitoring as an
+Mirrors every internal AgentCat log line to AgentCat's own monitoring as an
OTLP/HTTP log record, so we can detect when a developer's SDK fails to set up.
**Only operational metadata is ever sent โ never event payloads or user data.**
Records carry environment/identity metadata (the attempted ``project_id``, or an
anonymous install-id hash when none) plus a metadata-only message body. The local
-``~/mcpcat.log`` is unaffected.
+``~/agentcat.log`` is unaffected.
-On by default; opt out via ``MCPCatOptions(disable_diagnostics=True)`` or the
+On by default; opt out via ``AgentCatOptions(disable_diagnostics=True)`` or the
``DISABLE_DIAGNOSTICS`` env var. Auto-disabled in test environments
(``PYTEST_CURRENT_TEST`` / ``PYTEST_VERSION`` set) unless explicitly force-enabled
with ``DISABLE_DIAGNOSTICS=false``. Nothing here ever throws into the host, and the
@@ -52,7 +52,7 @@
def _sdk_version() -> str:
try:
- return version("mcpcat")
+ return version("agentcat")
except Exception:
return "unknown"
@@ -100,7 +100,7 @@ def _env_diagnostics_flag() -> str:
opt-in that overrides the test-environment auto-disable.
- ``"disabled"``: anything else disables diagnostics.
- Mirrors the ``MCPCAT_DEBUG_MODE`` idiom in logging.py.
+ Mirrors the ``AGENTCAT_DEBUG_MODE`` idiom in logging.py.
"""
try:
raw = os.environ.get("DISABLE_DIAGNOSTICS")
@@ -154,17 +154,17 @@ def _build_static_attributes(project_id: str | None) -> list[dict[str, Any]]:
try:
# Identity / traceability
if project_id:
- out += _attr("mcpcat.project_id", project_id)
+ out += _attr("agentcat.project_id", project_id)
else:
- out += _attr("mcpcat.install_id", _compute_install_id())
+ out += _attr("agentcat.install_id", _compute_install_id())
# SDK
- out += _attr("mcpcat.sdk.language", "python")
- out += _attr("mcpcat.sdk.version", _sdk_version())
+ out += _attr("agentcat.sdk.language", "python")
+ out += _attr("agentcat.sdk.version", _sdk_version())
# Best-effort: resolved MCP SDK version (distribution name may differ).
try:
- out += _attr("mcpcat.mcp_sdk.version", version("mcp"))
+ out += _attr("agentcat.mcp_sdk.version", version("mcp"))
except Exception:
pass
diff --git a/src/mcpcat/modules/event_queue.py b/src/agentcat/modules/event_queue.py
similarity index 95%
rename from src/mcpcat/modules/event_queue.py
rename to src/agentcat/modules/event_queue.py
index d778b45..473ab2e 100644
--- a/src/mcpcat/modules/event_queue.py
+++ b/src/agentcat/modules/event_queue.py
@@ -1,4 +1,4 @@
-"""Event queue implementation for MCPCat."""
+"""Event queue implementation for AgentCat."""
import atexit
import queue
@@ -13,8 +13,8 @@
if TYPE_CHECKING:
from .telemetry import TelemetryManager
-from mcpcat_api import ApiClient, Configuration, EventsApi
-from mcpcat.modules.constants import EVENT_ID_PREFIX, MCPCAT_API_URL
+from agentcat_api import ApiClient, Configuration, EventsApi
+from agentcat.modules.constants import AGENTCAT_API_URL, EVENT_ID_PREFIX
from ..types import Event, UnredactedEvent
from ..utils import generate_prefixed_ksuid
@@ -28,7 +28,7 @@
class EventQueue:
- """Manages event queue and sending to MCPCat API."""
+ """Manages event queue and sending to AgentCat API."""
def __init__(self, api_client=None):
self.queue: queue.Queue[UnredactedEvent] = queue.Queue(maxsize=10000)
@@ -38,7 +38,7 @@ def __init__(self, api_client=None):
# Allow injection of api_client for testing
if api_client is None:
- config = Configuration(host=MCPCAT_API_URL)
+ config = Configuration(host=AGENTCAT_API_URL)
api_client_instance = ApiClient(configuration=config)
self.api_client = EventsApi(api_client=api_client_instance)
else:
@@ -136,7 +136,7 @@ def _process_event(self, event: UnredactedEvent) -> None:
if event:
event.id = event.id or generate_prefixed_ksuid("evt")
- # Send to MCPCat API only if project_id exists
+ # Send to AgentCat API only if project_id exists
if event.project_id:
self._send_event(event)
@@ -148,7 +148,7 @@ def _process_event(self, event: UnredactedEvent) -> None:
write_to_log(f"Telemetry export submission failed: {e}")
if not event.project_id and not _telemetry_manager:
- # Warn if we have neither MCPCat nor telemetry configured
+ # Warn if we have neither AgentCat nor telemetry configured
write_to_log(
"Warning: Event has no project_id and no telemetry exporters configured"
)
@@ -226,7 +226,7 @@ def destroy(self) -> None:
# Flush any buffered SDK diagnostics on the way out. Lazy import to avoid
# an import cycle; never let diagnostics break shutdown.
try:
- from mcpcat.modules.diagnostics import flush_diagnostics
+ from agentcat.modules.diagnostics import flush_diagnostics
flush_diagnostics()
except Exception:
diff --git a/src/mcpcat/modules/exceptions.py b/src/agentcat/modules/exceptions.py
similarity index 98%
rename from src/mcpcat/modules/exceptions.py
rename to src/agentcat/modules/exceptions.py
index aec91b2..c540d38 100644
--- a/src/mcpcat/modules/exceptions.py
+++ b/src/agentcat/modules/exceptions.py
@@ -1,4 +1,4 @@
-"""Exception tracking module for MCPCat."""
+"""Exception tracking module for AgentCat."""
import contextvars
import linecache
@@ -9,8 +9,8 @@
import types
from typing import Any
-from mcpcat.types import ChainedErrorData, ErrorData, StackFrame
-from mcpcat.modules.constants import MAX_EXCEPTION_CHAIN_DEPTH, MAX_STACK_FRAMES
+from agentcat.types import ChainedErrorData, ErrorData, StackFrame
+from agentcat.modules.constants import MAX_EXCEPTION_CHAIN_DEPTH, MAX_STACK_FRAMES
_captured_error: contextvars.ContextVar[BaseException | None] = contextvars.ContextVar(
"_captured_error", default=None
diff --git a/src/mcpcat/modules/exporters/__init__.py b/src/agentcat/modules/exporters/__init__.py
similarity index 91%
rename from src/mcpcat/modules/exporters/__init__.py
rename to src/agentcat/modules/exporters/__init__.py
index b4e131d..bacecaf 100644
--- a/src/mcpcat/modules/exporters/__init__.py
+++ b/src/agentcat/modules/exporters/__init__.py
@@ -13,7 +13,7 @@ def export(self, event: Event) -> None:
Export an event to the telemetry backend.
Args:
- event: The MCPCat event to export
+ event: The AgentCat event to export
Note:
This method should handle all errors internally and never
diff --git a/src/mcpcat/modules/exporters/datadog.py b/src/agentcat/modules/exporters/datadog.py
similarity index 93%
rename from src/mcpcat/modules/exporters/datadog.py
rename to src/agentcat/modules/exporters/datadog.py
index 700c7a2..dedd7a6 100644
--- a/src/mcpcat/modules/exporters/datadog.py
+++ b/src/agentcat/modules/exporters/datadog.py
@@ -1,4 +1,4 @@
-"""Datadog exporter for MCPCat telemetry."""
+"""Datadog exporter for AgentCat telemetry."""
import json
import re
@@ -7,7 +7,7 @@
import requests
from ...types import Event, DatadogExporterConfig
-from ...modules.constants import MCPCAT_SOURCE
+from ...modules.constants import AGENTCAT_SOURCE
from ...modules.logging import write_to_log
from . import Exporter
from .trace_context import trace_context
@@ -25,7 +25,7 @@ def _sanitize_dd_tag_value(value: str) -> str:
class DatadogExporter(Exporter):
- """Exports MCPCat events to Datadog logs and metrics."""
+ """Exports AgentCat events to Datadog logs and metrics."""
def __init__(self, config: DatadogExporterConfig):
"""
@@ -57,7 +57,7 @@ def export(self, event: Event) -> None:
Export an event to Datadog logs and metrics.
Args:
- event: MCPCat event to export
+ event: AgentCat event to export
"""
write_to_log("DatadogExporter: Sending event immediately to Datadog")
@@ -121,10 +121,10 @@ def _send_metrics(self, metrics: List[Dict[str, Any]]) -> None:
def event_to_log(self, event: Event) -> Dict[str, Any]:
"""
- Convert MCPCat event to Datadog log format.
+ Convert AgentCat event to Datadog log format.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
Datadog log dictionary
@@ -141,13 +141,13 @@ def event_to_log(self, event: Event) -> Dict[str, Any]:
if event.is_error:
tags.append("error:true")
- tags.append(f"source:{MCPCAT_SOURCE}")
+ tags.append(f"source:{AGENTCAT_SOURCE}")
# Add customer-defined tags (namespaced to avoid collisions with reserved Datadog tags)
if getattr(event, "tags", None):
for key, value in event.tags.items():
tags.append(
- f"mcpcat.{_sanitize_dd_tag_key(key)}:{_sanitize_dd_tag_value(value)}"
+ f"agentcat.{_sanitize_dd_tag_key(key)}:{_sanitize_dd_tag_value(value)}"
)
# Get timestamp in milliseconds
@@ -160,7 +160,7 @@ def event_to_log(self, event: Event) -> Dict[str, Any]:
log: Dict[str, Any] = {
"message": f"{event.event_type or 'unknown'} - {event.resource_name or 'unknown'}",
"service": self.service,
- "ddsource": MCPCAT_SOURCE,
+ "ddsource": AGENTCAT_SOURCE,
"ddtags": ",".join(tags),
"timestamp": timestamp_ms,
"status": "error" if event.is_error else "info",
@@ -202,10 +202,10 @@ def event_to_log(self, event: Event) -> Dict[str, Any]:
def event_to_metrics(self, event: Event) -> List[Dict[str, Any]]:
"""
- Convert MCPCat event to Datadog metrics.
+ Convert AgentCat event to Datadog metrics.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
List of Datadog metric dictionaries
diff --git a/src/mcpcat/modules/exporters/otlp.py b/src/agentcat/modules/exporters/otlp.py
similarity index 88%
rename from src/mcpcat/modules/exporters/otlp.py
rename to src/agentcat/modules/exporters/otlp.py
index 5fae79d..4381f3f 100644
--- a/src/mcpcat/modules/exporters/otlp.py
+++ b/src/agentcat/modules/exporters/otlp.py
@@ -1,4 +1,4 @@
-"""OpenTelemetry Protocol (OTLP) exporter for MCPCat telemetry."""
+"""OpenTelemetry Protocol (OTLP) exporter for AgentCat telemetry."""
import json
from datetime import datetime
@@ -7,14 +7,15 @@
import requests
from ...types import Event, OTLPExporterConfig
-from ...modules.constants import MCPCAT_SOURCE
+from ...modules.constants import AGENTCAT_SOURCE
from ...modules.logging import write_to_log
+from ...modules.session import get_agentcat_version
from . import Exporter
from .trace_context import trace_context
class OTLPExporter(Exporter):
- """Exports MCPCat events to OpenTelemetry collectors via OTLP."""
+ """Exports AgentCat events to OpenTelemetry collectors via OTLP."""
def __init__(self, config: OTLPExporterConfig):
"""
@@ -52,7 +53,7 @@ def export(self, event: Event) -> None:
Export an event to OTLP collector.
Args:
- event: MCPCat event to export
+ event: AgentCat event to export
"""
try:
# Convert event to OTLP span format
@@ -68,8 +69,10 @@ def export(self, event: Event) -> None:
"scopeSpans": [
{
"scope": {
- "name": "mcpcat",
- "version": event.mcpcat_version or "0.1.0",
+ "name": "agentcat",
+ "version": event.agentcat_version
+ or get_agentcat_version()
+ or "unknown",
},
"spans": [span],
}
@@ -91,10 +94,10 @@ def export(self, event: Event) -> None:
def _convert_to_otlp_span(self, event: Event) -> Dict[str, Any]:
"""
- Convert MCPCat event to OTLP span format.
+ Convert AgentCat event to OTLP span format.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
OTLP span dictionary
@@ -128,7 +131,7 @@ def _get_resource_attributes(self, event: Event) -> List[Dict[str, Any]]:
Get resource-level attributes for OTLP.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
List of attribute key-value pairs
@@ -150,14 +153,14 @@ def _get_resource_attributes(self, event: Event) -> List[Dict[str, Any]]:
# Add SDK information
attributes.append(
- {"key": "telemetry.sdk.name", "value": {"stringValue": "mcpcat-python"}}
+ {"key": "telemetry.sdk.name", "value": {"stringValue": "agentcat-python"}}
)
- if event.mcpcat_version:
+ if event.agentcat_version:
attributes.append(
{
"key": "telemetry.sdk.version",
- "value": {"stringValue": event.mcpcat_version},
+ "value": {"stringValue": event.agentcat_version},
}
)
@@ -168,13 +171,13 @@ def _get_span_attributes(self, event: Event) -> List[Dict[str, Any]]:
Get span-level attributes for OTLP.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
List of attribute key-value pairs
"""
attributes: List[Dict[str, Any]] = [
- {"key": "source", "value": {"stringValue": MCPCAT_SOURCE}},
+ {"key": "source", "value": {"stringValue": AGENTCAT_SOURCE}},
]
# Add MCP-specific attributes
@@ -242,14 +245,14 @@ def _get_span_attributes(self, event: Event) -> List[Dict[str, Any]]:
# Add customer-defined tags as individual attributes
for key, value in (getattr(event, "tags", None) or {}).items():
attributes.append(
- {"key": f"mcpcat.tag.{key}", "value": {"stringValue": value}}
+ {"key": f"agentcat.tag.{key}", "value": {"stringValue": value}}
)
# Add customer-defined properties as JSON
if getattr(event, "properties", None):
attributes.append(
{
- "key": "mcpcat.properties",
+ "key": "agentcat.properties",
"value": {"stringValue": json.dumps(event.properties)},
}
)
diff --git a/src/mcpcat/modules/exporters/sentry.py b/src/agentcat/modules/exporters/sentry.py
similarity index 95%
rename from src/mcpcat/modules/exporters/sentry.py
rename to src/agentcat/modules/exporters/sentry.py
index eb33389..b8b3f5b 100644
--- a/src/mcpcat/modules/exporters/sentry.py
+++ b/src/agentcat/modules/exporters/sentry.py
@@ -1,4 +1,4 @@
-"""Sentry exporter for MCPCat telemetry."""
+"""Sentry exporter for AgentCat telemetry."""
import json
import re
@@ -7,14 +7,15 @@
import requests
from ...types import Event, SentryExporterConfig
-from ...modules.constants import MCPCAT_SOURCE
+from ...modules.constants import AGENTCAT_SOURCE
from ...modules.logging import write_to_log
+from ...modules.session import get_agentcat_version
from . import Exporter
from .trace_context import trace_context
class SentryExporter(Exporter):
- """Exports MCPCat events to Sentry as logs, transactions, and error events."""
+ """Exports AgentCat events to Sentry as logs, transactions, and error events."""
def __init__(self, config: SentryExporterConfig):
"""
@@ -42,7 +43,8 @@ def __init__(self, config: SentryExporterConfig):
self.endpoint = f"{protocol}://{host}{port}{path}/api/{project_id}/envelope/"
# Build auth header
- self.auth_header = f"Sentry sentry_version=7, sentry_client=mcpcat/1.0.0, sentry_key={self.parsed_dsn['public_key']}"
+ sdk_version = get_agentcat_version() or "unknown"
+ self.auth_header = f"Sentry sentry_version=7, sentry_client=agentcat/{sdk_version}, sentry_key={self.parsed_dsn['public_key']}"
# Create session for connection pooling
self.session = requests.Session()
@@ -85,7 +87,7 @@ def export(self, event: Event) -> None:
Export an event to Sentry.
Args:
- event: MCPCat event to export
+ event: AgentCat event to export
"""
try:
# ALWAYS send log
@@ -174,10 +176,10 @@ def export(self, event: Event) -> None:
def event_to_log(self, event: Event) -> Dict[str, Any]:
"""
- Convert MCPCat event to Sentry log format.
+ Convert AgentCat event to Sentry log format.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
Sentry log dictionary
@@ -215,7 +217,7 @@ def build_log_attributes(self, event: Event) -> Dict[str, Dict[str, Any]]:
Build log attributes for Sentry.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
Attributes dictionary
@@ -308,10 +310,10 @@ def create_log_envelope(self, log: Dict[str, Any]) -> str:
def event_to_transaction(self, event: Event) -> Dict[str, Any]:
"""
- Convert MCPCat event to Sentry transaction.
+ Convert AgentCat event to Sentry transaction.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
Sentry transaction dictionary
@@ -361,13 +363,13 @@ def build_tags(self, event: Event) -> Dict[str, str]:
Build tags for Sentry transaction/error.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
Tags dictionary
"""
tags: Dict[str, str] = {
- "source": MCPCAT_SOURCE,
+ "source": AGENTCAT_SOURCE,
}
if self.environment:
@@ -388,17 +390,17 @@ def build_tags(self, event: Event) -> Dict[str, str]:
# Namespace customer tags to avoid collisions with Sentry reserved fields
if getattr(event, "tags", None):
for key, value in event.tags.items():
- tags[f"mcpcat.{key}"] = value
+ tags[f"agentcat.{key}"] = value
return tags
def build_contexts(
self, event: Event, trace_ctx: Dict[str, Any]
) -> Dict[str, Any]:
- """Build Sentry contexts dict, embedding customer properties under `mcpcat`."""
+ """Build Sentry contexts dict, embedding customer properties under `agentcat`."""
contexts: Dict[str, Any] = {"trace": trace_ctx}
if getattr(event, "properties", None):
- contexts["mcpcat"] = event.properties
+ contexts["agentcat"] = event.properties
return contexts
def build_extra(self, event: Event) -> Dict[str, Any]:
@@ -406,7 +408,7 @@ def build_extra(self, event: Event) -> Dict[str, Any]:
Build extra data for Sentry transaction/error.
Args:
- event: MCPCat event
+ event: AgentCat event
Returns:
Extra data dictionary
@@ -436,10 +438,10 @@ def event_to_error_event(
self, event: Event, transaction: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
- Convert MCPCat event to Sentry error event.
+ Convert AgentCat event to Sentry error event.
Args:
- event: MCPCat event
+ event: AgentCat event
transaction: Optional transaction for context
Returns:
diff --git a/src/mcpcat/modules/exporters/trace_context.py b/src/agentcat/modules/exporters/trace_context.py
similarity index 100%
rename from src/mcpcat/modules/exporters/trace_context.py
rename to src/agentcat/modules/exporters/trace_context.py
diff --git a/src/mcpcat/modules/identify.py b/src/agentcat/modules/identify.py
similarity index 78%
rename from src/mcpcat/modules/identify.py
rename to src/agentcat/modules/identify.py
index a0b2d5e..9f002fa 100644
--- a/src/mcpcat/modules/identify.py
+++ b/src/agentcat/modules/identify.py
@@ -1,13 +1,13 @@
from datetime import datetime, timezone
-from mcpcat.modules import event_queue
-from mcpcat.modules.internal import get_server_tracking_data
-from mcpcat.modules.logging import write_to_log
-from mcpcat.types import EventType, UnredactedEvent, UserIdentity
+from agentcat.modules import event_queue
+from agentcat.modules.internal import get_server_tracking_data
+from agentcat.modules.logging import write_to_log
+from agentcat.types import EventType, UnredactedEvent, UserIdentity
def identify_session(server, request: any, context: any) -> UserIdentity | None:
- """Run the configured identify hook and publish a `mcpcat:identify` event.
+ """Run the configured identify hook and publish an `agentcat:identify` event.
Returns the resulting UserIdentity, or None if no hook is configured, the
hook raises, or it returns a non-UserIdentity value.
@@ -29,7 +29,7 @@ def identify_session(server, request: any, context: any) -> UserIdentity | None:
event = UnredactedEvent(
session_id=data.session_id,
timestamp=datetime.now(timezone.utc),
- event_type=EventType.MCPCAT_IDENTIFY.value,
+ event_type=EventType.AGENTCAT_IDENTIFY.value,
identify_actor_given_id=identify_result.user_id,
identify_actor_name=identify_result.user_name,
identify_data=identify_result.user_data or {},
diff --git a/src/mcpcat/modules/internal.py b/src/agentcat/modules/internal.py
similarity index 90%
rename from src/mcpcat/modules/internal.py
rename to src/agentcat/modules/internal.py
index e915ebd..9884ecf 100644
--- a/src/mcpcat/modules/internal.py
+++ b/src/agentcat/modules/internal.py
@@ -1,17 +1,17 @@
-"""Internal data storage for MCPCat."""
+"""Internal data storage for AgentCat."""
import inspect
import weakref
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
-from ..types import EventType, MCPCatData, ToolRegistration, UnredactedEvent
+from ..types import EventType, AgentCatData, ToolRegistration, UnredactedEvent
from .compatibility import is_official_fastmcp_server
from .logging import write_to_log
from .validation import validate_tags
# WeakKeyDictionary to store data associated with server instances
-_server_data_map: weakref.WeakKeyDictionary[Any, MCPCatData] = (
+_server_data_map: weakref.WeakKeyDictionary[Any, AgentCatData] = (
weakref.WeakKeyDictionary()
)
@@ -27,14 +27,14 @@ def _get_server_key(server: Any) -> Any:
return server
-def set_server_tracking_data(server: Any, data: MCPCatData) -> None:
- """Store MCPCat data for a server instance."""
+def set_server_tracking_data(server: Any, data: AgentCatData) -> None:
+ """Store AgentCat data for a server instance."""
key = _get_server_key(server)
_server_data_map[key] = data
-def get_server_tracking_data(server: Any) -> MCPCatData | None:
- """Retrieve MCPCat data for a server instance."""
+def get_server_tracking_data(server: Any) -> AgentCatData | None:
+ """Retrieve AgentCat data for a server instance."""
key = _get_server_key(server)
return _server_data_map.get(key, None)
@@ -67,7 +67,7 @@ def register_tool(server: Any, name: str) -> None:
def mark_tool_tracked(server: Any, name: str) -> None:
- """Mark a tool as being tracked by MCPCat for this server."""
+ """Mark a tool as being tracked by AgentCat for this server."""
data = get_server_tracking_data(server)
if data and name in data.tool_registry:
data.tool_registry[name].tracked = True
@@ -121,7 +121,7 @@ def get_original_methods() -> Dict[str, Any]:
async def resolve_event_tags(
- data: MCPCatData, request: Any, extra: Any
+ data: AgentCatData, request: Any, extra: Any
) -> Optional[Dict[str, str]]:
"""Resolve the event_tags callback and return validated tags.
@@ -147,7 +147,7 @@ async def resolve_event_tags(
async def resolve_event_properties(
- data: MCPCatData, request: Any, extra: Any
+ data: AgentCatData, request: Any, extra: Any
) -> Optional[Dict[str, Any]]:
"""Resolve the event_properties callback and return the result.
@@ -171,7 +171,7 @@ async def resolve_event_properties(
async def attach_event_metadata(
event: UnredactedEvent,
- data: Optional[MCPCatData],
+ data: Optional[AgentCatData],
request: Any,
extra: Any,
) -> None:
diff --git a/src/mcpcat/modules/logging.py b/src/agentcat/modules/logging.py
similarity index 83%
rename from src/mcpcat/modules/logging.py
rename to src/agentcat/modules/logging.py
index 0e96984..761dac6 100644
--- a/src/mcpcat/modules/logging.py
+++ b/src/agentcat/modules/logging.py
@@ -1,14 +1,14 @@
-"""Logging functionality for MCPCat."""
+"""Logging functionality for AgentCat."""
import os
from collections.abc import Callable
from datetime import datetime, timezone
-from mcpcat.types import MCPCatOptions
+from agentcat.types import AgentCatOptions
# Initialize debug_mode from environment variable at module load time
-_env_debug = os.getenv("MCPCAT_DEBUG_MODE")
+_env_debug = os.getenv("AGENTCAT_DEBUG_MODE")
if _env_debug is not None:
debug_mode = _env_debug.lower() in ("true", "1", "yes", "on")
else:
@@ -16,7 +16,7 @@
# Optional sink that receives every (clean, newline-free) log entry. Used by the
-# diagnostics module to mirror internal logs to MCPCat's monitoring. Fires
+# diagnostics module to mirror internal logs to AgentCat's monitoring. Fires
# independent of debug_mode and must never break logging.
_diagnostics_sink: Callable[[str], None] | None = None
@@ -44,8 +44,8 @@ def write_to_log(message: str) -> None:
except Exception:
pass
- # Always use ~/mcpcat.log
- log_path = os.path.expanduser("~/mcpcat.log")
+ # Always use ~/agentcat.log
+ log_path = os.path.expanduser("~/agentcat.log")
try:
if debug_mode:
diff --git a/src/mcpcat/modules/overrides/community/monkey_patch.py b/src/agentcat/modules/overrides/community/monkey_patch.py
similarity index 94%
rename from src/mcpcat/modules/overrides/community/monkey_patch.py
rename to src/agentcat/modules/overrides/community/monkey_patch.py
index 72a5f88..c7f8106 100644
--- a/src/mcpcat/modules/overrides/community/monkey_patch.py
+++ b/src/agentcat/modules/overrides/community/monkey_patch.py
@@ -1,7 +1,7 @@
"""Monkey-patching implementation for community FastMCP servers.
This module patches community FastMCP servers to intercept tool operations
-and add MCPCat tracking capabilities.
+and add AgentCat tracking capabilities.
"""
from datetime import datetime, timezone
@@ -10,18 +10,18 @@
from mcp.types import CallToolRequest
from mcp import ServerResult
-from mcpcat.modules import event_queue
-from mcpcat.modules.compatibility import is_mcp_error_response
-from mcpcat.modules.exceptions import capture_exception
-from mcpcat.modules.identify import identify_session
-from mcpcat.modules.internal import attach_event_metadata, get_server_tracking_data
-from mcpcat.modules.logging import write_to_log
-from mcpcat.modules.request_extra import params_with_extra
-from mcpcat.modules.session import (
+from agentcat.modules import event_queue
+from agentcat.modules.compatibility import is_mcp_error_response
+from agentcat.modules.exceptions import capture_exception
+from agentcat.modules.identify import identify_session
+from agentcat.modules.internal import attach_event_metadata, get_server_tracking_data
+from agentcat.modules.logging import write_to_log
+from agentcat.modules.request_extra import params_with_extra
+from agentcat.modules.session import (
get_client_info_from_request_context,
get_server_session_id,
)
-from mcpcat.types import EventType, UnredactedEvent
+from agentcat.types import EventType, UnredactedEvent
from ..mcp_server import override_lowlevel_mcp_server_minimal, safe_request_context
diff --git a/src/mcpcat/modules/overrides/community/tool_manager.py b/src/agentcat/modules/overrides/community/tool_manager.py
similarity index 95%
rename from src/mcpcat/modules/overrides/community/tool_manager.py
rename to src/agentcat/modules/overrides/community/tool_manager.py
index 5e6d328..ef8fc95 100644
--- a/src/mcpcat/modules/overrides/community/tool_manager.py
+++ b/src/agentcat/modules/overrides/community/tool_manager.py
@@ -1,7 +1,7 @@
from typing import Any
-from mcpcat.modules.compatibility import is_community_fastmcp_server
-from mcpcat.modules.internal import (
+from agentcat.modules.compatibility import is_community_fastmcp_server
+from agentcat.modules.internal import (
get_server_tracking_data,
get_original_method,
store_original_method,
@@ -9,8 +9,8 @@
register_tool,
mark_tool_tracked,
)
-from mcpcat.modules.logging import write_to_log
-from mcpcat.modules.tools import handle_report_missing
+from agentcat.modules.logging import write_to_log
+from agentcat.modules.tools import handle_report_missing
from fastmcp import FastMCP
@@ -43,7 +43,7 @@ def _ensure_context_parameter(tool: Any, description: str) -> None:
def patch_community_fastmcp_tool_manager(server: Any) -> None:
- """Patch the community FastMCP tool manager to add MCPCat tracking.
+ """Patch the community FastMCP tool manager to add AgentCat tracking.
This function modifies the tool manager to:
1. Add context parameter to existing tools
@@ -81,7 +81,7 @@ async def get_more_tools(context: str) -> str:
# Force the correct schema - Pydantic's TypeAdapter can mangle
# the type on async closures into anyOf: [string, null]
- from mcpcat.modules.tools import GET_MORE_TOOLS_SCHEMA
+ from agentcat.modules.tools import GET_MORE_TOOLS_SCHEMA
if hasattr(server._tool_manager, "_tools") and "get_more_tools" in server._tool_manager._tools:
server._tool_manager._tools["get_more_tools"].parameters = GET_MORE_TOOLS_SCHEMA
diff --git a/src/agentcat/modules/overrides/community_v3/__init__.py b/src/agentcat/modules/overrides/community_v3/__init__.py
new file mode 100644
index 0000000..17f329c
--- /dev/null
+++ b/src/agentcat/modules/overrides/community_v3/__init__.py
@@ -0,0 +1,11 @@
+"""Community FastMCP v3 integration using the middleware system."""
+
+from agentcat.modules.overrides.community_v3.integration import (
+ apply_community_v3_integration,
+)
+from agentcat.modules.overrides.community_v3.middleware import AgentCatMiddleware
+
+__all__ = [
+ "AgentCatMiddleware",
+ "apply_community_v3_integration",
+]
diff --git a/src/mcpcat/modules/overrides/community_v3/integration.py b/src/agentcat/modules/overrides/community_v3/integration.py
similarity index 68%
rename from src/mcpcat/modules/overrides/community_v3/integration.py
rename to src/agentcat/modules/overrides/community_v3/integration.py
index 3806d23..eccf6d3 100644
--- a/src/mcpcat/modules/overrides/community_v3/integration.py
+++ b/src/agentcat/modules/overrides/community_v3/integration.py
@@ -1,6 +1,6 @@
"""Integration module for Community FastMCP v3.
-This module provides the function to apply MCPCat tracking to
+This module provides the function to apply AgentCat tracking to
FastMCP v3 servers using the middleware system.
"""
@@ -8,37 +8,37 @@
from pydantic import Field
-from mcpcat.modules.logging import write_to_log
-from mcpcat.modules.overrides.community_v3.middleware import MCPCatMiddleware
-from mcpcat.types import MCPCatData
+from agentcat.modules.logging import write_to_log
+from agentcat.modules.overrides.community_v3.middleware import AgentCatMiddleware
+from agentcat.types import AgentCatData
-def apply_community_v3_integration(server: Any, mcpcat_data: MCPCatData) -> None:
- """Apply MCPCat tracking to a Community FastMCP v3 server.
+def apply_community_v3_integration(server: Any, agentcat_data: AgentCatData) -> None:
+ """Apply AgentCat tracking to a Community FastMCP v3 server.
This function:
- 1. Creates an MCPCatMiddleware instance
+ 1. Creates an AgentCatMiddleware instance
2. Inserts it at the beginning of the middleware chain (position 0)
3. Registers get_more_tools tool if enabled
Args:
server: A Community FastMCP v3 server instance.
- mcpcat_data: MCPCat tracking configuration.
+ agentcat_data: AgentCat tracking configuration.
"""
try:
# Create middleware instance
- middleware = MCPCatMiddleware(mcpcat_data, server)
+ middleware = AgentCatMiddleware(agentcat_data, server)
# Insert at beginning of middleware chain (position 0)
- # This ensures MCPCat sees all requests first
+ # This ensures AgentCat sees all requests first
server.middleware.insert(0, middleware)
write_to_log(
- f"Inserted MCPCatMiddleware at position 0 for server {id(server)}"
+ f"Inserted AgentCatMiddleware at position 0 for server {id(server)}"
)
# Register get_more_tools if enabled
- if mcpcat_data.options.enable_report_missing:
- _register_get_more_tools_v3(server, mcpcat_data)
+ if agentcat_data.options.enable_report_missing:
+ _register_get_more_tools_v3(server, agentcat_data)
write_to_log(
f"Successfully applied Community FastMCP v3 integration "
@@ -50,16 +50,16 @@ def apply_community_v3_integration(server: Any, mcpcat_data: MCPCatData) -> None
raise
-def _register_get_more_tools_v3(server: Any, mcpcat_data: MCPCatData) -> None:
+def _register_get_more_tools_v3(server: Any, agentcat_data: AgentCatData) -> None:
"""Register the get_more_tools tool for FastMCP v3.
Args:
server: A Community FastMCP v3 server instance.
- mcpcat_data: MCPCat tracking configuration.
+ agentcat_data: AgentCat tracking configuration.
"""
from fastmcp.tools.tool import Tool
- from mcpcat.modules.tools import handle_report_missing
+ from agentcat.modules.tools import handle_report_missing
# Define the get_more_tools function
async def get_more_tools(
diff --git a/src/mcpcat/modules/overrides/community_v3/middleware.py b/src/agentcat/modules/overrides/community_v3/middleware.py
similarity index 91%
rename from src/mcpcat/modules/overrides/community_v3/middleware.py
rename to src/agentcat/modules/overrides/community_v3/middleware.py
index 082bcaf..b597be4 100644
--- a/src/mcpcat/modules/overrides/community_v3/middleware.py
+++ b/src/agentcat/modules/overrides/community_v3/middleware.py
@@ -1,6 +1,6 @@
-"""MCPCat Middleware for Community FastMCP v3.
+"""AgentCat Middleware for Community FastMCP v3.
-This module provides a middleware implementation that integrates MCPCat
+This module provides a middleware implementation that integrates AgentCat
tracking capabilities with the FastMCP v3 middleware system.
"""
@@ -13,47 +13,47 @@
import mcp.types as mt
-from mcpcat.modules import event_queue
-from mcpcat.modules.exceptions import (
+from agentcat.modules import event_queue
+from agentcat.modules.exceptions import (
capture_exception,
clear_captured_error,
get_captured_error,
store_captured_error,
)
-from mcpcat.modules.identify import identify_session
-from mcpcat.modules.internal import attach_event_metadata, mark_tool_tracked, register_tool
-from mcpcat.modules.logging import write_to_log
-from mcpcat.modules.request_extra import params_with_extra
-from mcpcat.modules.session import (
+from agentcat.modules.identify import identify_session
+from agentcat.modules.internal import attach_event_metadata, mark_tool_tracked, register_tool
+from agentcat.modules.logging import write_to_log
+from agentcat.modules.request_extra import params_with_extra
+from agentcat.modules.session import (
get_client_info_from_request_context,
get_server_session_id,
)
-from mcpcat.types import EventType, MCPCatData, UnredactedEvent
+from agentcat.types import EventType, AgentCatData, UnredactedEvent
if TYPE_CHECKING:
from fastmcp.server.middleware import CallNext, MiddlewareContext
from fastmcp.tools.tool import Tool, ToolResult
-class MCPCatMiddleware:
- """Middleware for MCPCat tracking in FastMCP v3.
+class AgentCatMiddleware:
+ """Middleware for AgentCat tracking in FastMCP v3.
This middleware intercepts tool calls, list_tools, and initialize events
- to provide analytics tracking for MCPCat.
+ to provide analytics tracking for AgentCat.
Attributes:
- mcpcat_data: The MCPCat tracking data configuration.
+ agentcat_data: The AgentCat tracking data configuration.
server: The FastMCP server instance.
"""
- def __init__(self, mcpcat_data: MCPCatData, server: Any) -> None:
- """Initialize the MCPCat middleware.
+ def __init__(self, agentcat_data: AgentCatData, server: Any) -> None:
+ """Initialize the AgentCat middleware.
Args:
- mcpcat_data: MCPCat tracking configuration.
+ agentcat_data: AgentCat tracking configuration.
server: The FastMCP v3 server instance.
"""
- self.mcpcat_data = mcpcat_data
+ self.agentcat_data = agentcat_data
self.server = server
async def __call__(
@@ -132,7 +132,7 @@ async def on_initialize(
client_name=client_name,
client_version=client_version,
)
- await attach_event_metadata(event, self.mcpcat_data, context.message, request_context)
+ await attach_event_metadata(event, self.agentcat_data, context.message, request_context)
try:
result = await call_next(context)
@@ -182,7 +182,7 @@ async def on_call_tool(
# Extract user intent and determine if we should remove context from arguments
user_intent = None
should_remove_context = (
- self.mcpcat_data.options.enable_tool_call_context
+ self.agentcat_data.options.enable_tool_call_context
and tool_name != "get_more_tools"
)
@@ -208,7 +208,7 @@ async def on_call_tool(
client_name=client_name,
client_version=client_version,
)
- await attach_event_metadata(event, self.mcpcat_data, context.message, request_context)
+ await attach_event_metadata(event, self.agentcat_data, context.message, request_context)
# Create modified context without context parameter if needed
call_context = context
@@ -292,7 +292,7 @@ async def on_list_tools(
client_name=client_name,
client_version=client_version,
)
- await attach_event_metadata(event, self.mcpcat_data, context.message, request_context)
+ await attach_event_metadata(event, self.agentcat_data, context.message, request_context)
try:
tools = list(await call_next(context))
@@ -301,7 +301,7 @@ async def on_list_tools(
register_tool(self.server, tool.name)
mark_tool_tracked(self.server, tool.name)
- if self.mcpcat_data.options.enable_tool_call_context:
+ if self.agentcat_data.options.enable_tool_call_context:
tools = self._inject_context_into_tools(tools)
event.response = {"tools": [self._tool_to_dict(t) for t in tools]}
@@ -325,7 +325,7 @@ def _get_session_id(self) -> str:
return get_server_session_id(self.server)
except Exception as e:
write_to_log(f"Error getting session ID: {e}")
- return self.mcpcat_data.session_id
+ return self.agentcat_data.session_id
def _get_request_context(self, context: MiddlewareContext[Any]) -> Any:
"""Get the MCP request context from middleware context.
@@ -347,7 +347,7 @@ def _publish_event(self, event: UnredactedEvent, event_name: str) -> None:
event: The event to publish.
event_name: Human-readable name for error logging.
"""
- if not self.mcpcat_data.options.enable_tracing:
+ if not self.agentcat_data.options.enable_tracing:
return
try:
@@ -379,7 +379,7 @@ def _inject_context_into_tools(self, tools: list[Tool]) -> list[Tool]:
Returns:
List of tools with context parameter injected.
"""
- context_description = self.mcpcat_data.options.custom_context_description
+ context_description = self.agentcat_data.options.custom_context_description
modified_tools = []
for tool in tools:
diff --git a/src/mcpcat/modules/overrides/mcp_server.py b/src/agentcat/modules/overrides/mcp_server.py
similarity index 92%
rename from src/mcpcat/modules/overrides/mcp_server.py
rename to src/agentcat/modules/overrides/mcp_server.py
index 555d7b9..12e632c 100644
--- a/src/mcpcat/modules/overrides/mcp_server.py
+++ b/src/agentcat/modules/overrides/mcp_server.py
@@ -6,15 +6,15 @@
from mcp.types import CallToolRequest, ListToolsRequest, InitializeRequest
from mcp.shared.context import RequestContext
-from mcpcat.modules import event_queue
-from mcpcat.modules.compatibility import is_mcp_error_response
-from mcpcat.modules.identify import identify_session
-from mcpcat.modules.internal import attach_event_metadata
-from mcpcat.modules.logging import write_to_log
-from mcpcat.modules.request_extra import params_with_extra
-from mcpcat.modules.tools import handle_report_missing
-
-from ...types import EventType, MCPCatData, UnredactedEvent
+from agentcat.modules import event_queue
+from agentcat.modules.compatibility import is_mcp_error_response
+from agentcat.modules.identify import identify_session
+from agentcat.modules.internal import attach_event_metadata
+from agentcat.modules.logging import write_to_log
+from agentcat.modules.request_extra import params_with_extra
+from agentcat.modules.tools import handle_report_missing
+
+from ...types import EventType, AgentCatData, UnredactedEvent
from ..session import get_client_info_from_request_context, get_server_session_id
@@ -28,10 +28,10 @@ def safe_request_context(server: Server) -> Optional[RequestContext]:
return request_context
-"""Tool management and interception for MCPCat."""
+"""Tool management and interception for AgentCat."""
-def override_lowlevel_mcp_server(server: Server, data: MCPCatData) -> None:
+def override_lowlevel_mcp_server(server: Server, data: AgentCatData) -> None:
"""Set up tool list and call handlers for FastMCP."""
# Store original request handlers - we only need to intercept at the low-level
# TODO: original_call_tool_handler = server.request_handlers.get(InitializeRequest)
@@ -40,7 +40,7 @@ def override_lowlevel_mcp_server(server: Server, data: MCPCatData) -> None:
original_list_tools_handler = server.request_handlers.get(ListToolsRequest)
async def wrapped_initialize_handler(request: InitializeRequest) -> ServerResult:
- """Intercept initialize requests to add MCPCat data to the request context."""
+ """Intercept initialize requests to add AgentCat data to the request context."""
session_id = get_server_session_id(server)
request_context = safe_request_context(server)
identity = identify_session(server, request, request_context)
@@ -78,7 +78,7 @@ async def wrapped_initialize_handler(request: InitializeRequest) -> ServerResult
return result
async def wrapped_list_tools_handler(request: ListToolsRequest) -> ServerResult:
- """Intercept list_tools requests to add MCPCat tools and modify existing ones."""
+ """Intercept list_tools requests to add AgentCat tools and modify existing ones."""
session_id = get_server_session_id(server)
request_context = safe_request_context(server)
client_name, client_version = get_client_info_from_request_context(server, request_context)
@@ -162,7 +162,7 @@ async def wrapped_list_tools_handler(request: ListToolsRequest) -> ServerResult:
return result
async def wrapped_call_tool_handler(request: CallToolRequest) -> ServerResult:
- """Intercept call_tool requests to add MCPCat tracking and handle special tools."""
+ """Intercept call_tool requests to add AgentCat tracking and handle special tools."""
tool_name = request.params.name
arguments = request.params.arguments or {}
session_id = get_server_session_id(server)
@@ -210,7 +210,7 @@ async def wrapped_call_tool_handler(request: CallToolRequest) -> ServerResult:
# Log warning if context is missing and tool is not report_missing
if event.user_intent is None and tool_name != "get_more_tools":
write_to_log(
- f"Tool '{tool_name}' called without context. mcpcat.track() might have been called BEFORE tool initialization."
+ f"Tool '{tool_name}' called without context. agentcat.track() might have been called BEFORE tool initialization."
)
# If tracing is enabled, wrap the call with timing and logging
@@ -241,7 +241,7 @@ async def wrapped_call_tool_handler(request: CallToolRequest) -> ServerResult:
server.request_handlers[InitializeRequest] = wrapped_initialize_handler
-def override_lowlevel_mcp_server_minimal(server: Server, data: MCPCatData) -> None:
+def override_lowlevel_mcp_server_minimal(server: Server, data: AgentCatData) -> None:
"""Set up minimal handlers for FastMCP servers (non-tool events only).
This is used for FastMCP servers where tool tracking is handled by monkey-patching.
@@ -252,7 +252,7 @@ def override_lowlevel_mcp_server_minimal(server: Server, data: MCPCatData) -> No
original_list_tools_handler = server.request_handlers.get(ListToolsRequest)
async def wrapped_initialize_handler(request: InitializeRequest) -> ServerResult:
- """Intercept initialize requests to add MCPCat data to the request context."""
+ """Intercept initialize requests to add AgentCat data to the request context."""
session_id = get_server_session_id(server)
request_context = safe_request_context(server)
try:
diff --git a/src/mcpcat/modules/overrides/official/monkey_patch.py b/src/agentcat/modules/overrides/official/monkey_patch.py
similarity index 92%
rename from src/mcpcat/modules/overrides/official/monkey_patch.py
rename to src/agentcat/modules/overrides/official/monkey_patch.py
index c51359a..28f3d6b 100644
--- a/src/mcpcat/modules/overrides/official/monkey_patch.py
+++ b/src/agentcat/modules/overrides/official/monkey_patch.py
@@ -1,7 +1,7 @@
"""Monkey-patching implementation for dynamic tool tracking.
This module patches MCP server methods to intercept tool registration and execution,
-enabling MCPCat to track tools regardless of when they are registered.
+enabling AgentCat to track tools regardless of when they are registered.
"""
import inspect
@@ -11,15 +11,15 @@
from pydantic import Field
-from mcpcat.modules import event_queue
-from mcpcat.modules.compatibility import is_official_fastmcp_server, is_mcp_error_response
-from mcpcat.modules.exceptions import (
+from agentcat.modules import event_queue
+from agentcat.modules.compatibility import is_official_fastmcp_server, is_mcp_error_response
+from agentcat.modules.exceptions import (
capture_exception,
clear_captured_error,
get_captured_error,
store_captured_error,
)
-from mcpcat.modules.internal import (
+from agentcat.modules.internal import (
attach_event_metadata,
get_original_method,
get_server_tracking_data,
@@ -28,29 +28,29 @@
register_tool,
store_original_method,
)
-from mcpcat.modules.logging import write_to_log
-from mcpcat.modules.request_extra import params_with_extra
-from mcpcat.modules.session import (
+from agentcat.modules.logging import write_to_log
+from agentcat.modules.request_extra import params_with_extra
+from agentcat.modules.session import (
get_client_info_from_request_context,
get_server_session_id,
)
-from mcpcat.types import EventType, MCPCatData, UnredactedEvent
+from agentcat.types import EventType, AgentCatData, UnredactedEvent
from ..mcp_server import safe_request_context
-def get_current_mcpcat_data(server: Any, fallback: MCPCatData) -> MCPCatData:
- """Get the current MCPCat data for a server."""
+def get_current_agentcat_data(server: Any, fallback: AgentCatData) -> AgentCatData:
+ """Get the current AgentCat data for a server."""
data = get_server_tracking_data(server)
return data if data else fallback
-def patch_fastmcp_tool_manager(server: Any, mcpcat_data: MCPCatData) -> bool:
+def patch_fastmcp_tool_manager(server: Any, agentcat_data: AgentCatData) -> bool:
"""Monkey-patch FastMCP's ToolManager to intercept tool operations.
Args:
server: FastMCP server instance
- mcpcat_data: MCPCat tracking data
+ agentcat_data: AgentCat tracking data
Returns:
True if patching was successful, False otherwise
@@ -66,7 +66,7 @@ def patch_fastmcp_tool_manager(server: Any, mcpcat_data: MCPCatData) -> bool:
return False
# Add the get_more_tools tool if enabled
- if mcpcat_data.options.enable_report_missing:
+ if agentcat_data.options.enable_report_missing:
# Create the get_more_tools function that returns CallToolResult
async def get_more_tools(
context: Annotated[
@@ -77,7 +77,7 @@ async def get_more_tools(
],
) -> List[Any]:
"""Check for additional tools whenever your task might benefit from specialized capabilities."""
- from mcpcat.modules.tools import handle_report_missing
+ from agentcat.modules.tools import handle_report_missing
result = await handle_report_missing({"context": context})
# Return just the content list for FastMCP
@@ -184,9 +184,9 @@ def patched_add_tool(
register_tool(server, tool_name)
# Get current data for this server
- current_data = get_current_mcpcat_data(server, mcpcat_data)
+ current_data = get_current_agentcat_data(server, agentcat_data)
- # If MCPCat is already initialized, we need to wrap this tool
+ # If AgentCat is already initialized, we need to wrap this tool
if data.tracker_initialized and current_data.options.enable_tracing:
write_to_log(
f"Late-registered FastMCP tool detected: {tool_name}"
@@ -223,7 +223,7 @@ async def patched_call_tool(
context: Any | None = None,
**kwargs # Accept any additional parameters for version compatibility
) -> Any:
- """Patched call_tool that adds MCPCat tracking."""
+ """Patched call_tool that adds AgentCat tracking."""
# Initialize variables for tracking
event = None
current_data = None
@@ -232,11 +232,11 @@ async def patched_call_tool(
# Try to get tracking data, but don't fail if we can't
try:
session_id = get_server_session_id(server._mcp_server)
- current_data = get_current_mcpcat_data(server, mcpcat_data)
+ current_data = get_current_agentcat_data(server, agentcat_data)
except Exception as e:
write_to_log(f"Error getting tracking data: {e}")
session_id = "unknown"
- current_data = mcpcat_data
+ current_data = agentcat_data
# Handle session identification (non-critical)
request_context = None
@@ -250,7 +250,7 @@ async def patched_call_tool(
)
# Call identify_session for custom identification
- from mcpcat.modules.identify import identify_session
+ from agentcat.modules.identify import identify_session
# Create a mock request for identify_session
mock_request = type(
@@ -422,12 +422,12 @@ async def patched_call_tool(
write_to_log(f"Error publishing event: {e}")
# Don't re-raise, let the tool result be returned
- # Patch list_tools to add MCPCat tools and context
+ # Patch list_tools to add AgentCat tools and context
def patched_list_tools() -> List[Any]:
- """Patched list_tools that adds MCPCat modifications."""
+ """Patched list_tools that adds AgentCat modifications."""
try:
# Get current data for this server
- current_data = get_current_mcpcat_data(server, mcpcat_data)
+ current_data = get_current_agentcat_data(server, agentcat_data)
# Get original tools with safety check
if not callable(original_list_tools):
@@ -461,7 +461,7 @@ def patched_list_tools() -> List[Any]:
# Create a function for get_more_tools
async def get_more_tools_fn(context: str) -> Any:
"""Check for additional tools whenever your task might benefit from specialized capabilities."""
- from mcpcat.modules.tools import handle_report_missing
+ from agentcat.modules.tools import handle_report_missing
return await handle_report_missing({"context": context})
@@ -552,26 +552,26 @@ async def get_more_tools_fn(context: str) -> Any:
return False
-def apply_official_fastmcp_patches(server: Any, mcpcat_data: MCPCatData) -> bool:
+def apply_official_fastmcp_patches(server: Any, agentcat_data: AgentCatData) -> bool:
"""Apply monkey patches for FastMCP servers only.
Args:
server: FastMCP server instance
- mcpcat_data: MCPCat tracking data
+ agentcat_data: AgentCat tracking data
Returns:
True if patching was successful
"""
- # The MCPCat data is already stored by the caller
+ # The AgentCat data is already stored by the caller
# Just verify we can get it
data = get_server_tracking_data(server)
if not data:
- write_to_log(f"Warning: MCPCat data not found for server {id(server)}")
+ write_to_log(f"Warning: AgentCat data not found for server {id(server)}")
return False
# Only patch FastMCP servers
if is_official_fastmcp_server(server):
- if patch_fastmcp_tool_manager(server, mcpcat_data):
+ if patch_fastmcp_tool_manager(server, agentcat_data):
write_to_log(
f"Monkey patches applied successfully to FastMCP server {id(server)}"
)
diff --git a/src/mcpcat/modules/redaction.py b/src/agentcat/modules/redaction.py
similarity index 96%
rename from src/mcpcat/modules/redaction.py
rename to src/agentcat/modules/redaction.py
index 35bf6ed..58b0dca 100644
--- a/src/mcpcat/modules/redaction.py
+++ b/src/agentcat/modules/redaction.py
@@ -1,9 +1,9 @@
-"""PII redaction for MCPCat logs."""
+"""PII redaction for AgentCat logs."""
from typing import Any, TYPE_CHECKING, Callable, Set
if TYPE_CHECKING:
- from mcpcat.types import Event, UnredactedEvent
+ from agentcat.types import Event, UnredactedEvent
# Set of field names that should be protected from redaction.
diff --git a/src/mcpcat/modules/request_extra.py b/src/agentcat/modules/request_extra.py
similarity index 99%
rename from src/mcpcat/modules/request_extra.py
rename to src/agentcat/modules/request_extra.py
index ba458f9..e35387e 100644
--- a/src/mcpcat/modules/request_extra.py
+++ b/src/agentcat/modules/request_extra.py
@@ -13,7 +13,7 @@
from typing import Any, Optional
-from mcpcat.modules.logging import write_to_log
+from agentcat.modules.logging import write_to_log
def _headers_to_dict(request: Any) -> Optional[dict]:
diff --git a/src/mcpcat/modules/sanitization.py b/src/agentcat/modules/sanitization.py
similarity index 91%
rename from src/mcpcat/modules/sanitization.py
rename to src/agentcat/modules/sanitization.py
index a8a722f..ad209b8 100644
--- a/src/mcpcat/modules/sanitization.py
+++ b/src/agentcat/modules/sanitization.py
@@ -1,7 +1,7 @@
-"""Content sanitization for MCPCat events.
+"""Content sanitization for AgentCat events.
Strips binary/non-text content (images, audio, base64 blobs) from event
-payloads before they are sent to the MCPCat API or telemetry exporters.
+payloads before they are sent to the AgentCat API or telemetry exporters.
"""
import copy
@@ -10,7 +10,7 @@
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
- from mcpcat.types import UnredactedEvent
+ from agentcat.types import UnredactedEvent
# Threshold for base64 scanning โ strings shorter than this are passed through.
_BASE64_SIZE_THRESHOLD = 10_240
@@ -20,14 +20,14 @@
_BASE64_PATTERN = re.compile(r"^[A-Za-z0-9+/\n\r]+=*$")
# Redaction messages
-_IMAGE_REDACTED = "[image content redacted - not supported by mcpcat]"
-_AUDIO_REDACTED = "[audio content redacted - not supported by mcpcat]"
-_BLOB_RESOURCE_REDACTED = "[binary resource content redacted - not supported by mcpcat]"
-_BINARY_DATA_REDACTED = "[binary data redacted - not supported by mcpcat]"
+_IMAGE_REDACTED = "[image content redacted - not supported by agentcat]"
+_AUDIO_REDACTED = "[audio content redacted - not supported by agentcat]"
+_BLOB_RESOURCE_REDACTED = "[binary resource content redacted - not supported by agentcat]"
+_BINARY_DATA_REDACTED = "[binary data redacted - not supported by agentcat]"
def _unsupported_type_redacted(type_name: str) -> str:
- return f'[unsupported content type "{type_name}" redacted - not supported by mcpcat]'
+ return f'[unsupported content type "{type_name}" redacted - not supported by agentcat]'
# ---------------------------------------------------------------------------
diff --git a/src/mcpcat/modules/session.py b/src/agentcat/modules/session.py
similarity index 90%
rename from src/mcpcat/modules/session.py
rename to src/agentcat/modules/session.py
index 8e9f989..f6d2603 100644
--- a/src/mcpcat/modules/session.py
+++ b/src/agentcat/modules/session.py
@@ -1,4 +1,4 @@
-"""Session management for MCPCat."""
+"""Session management for AgentCat."""
import re
import sys
@@ -7,11 +7,11 @@
from mcp.shared.context import RequestContext
from mcp.server import Server
-from mcpcat.modules.constants import INACTIVITY_TIMEOUT_IN_MINUTES, SESSION_ID_PREFIX
-from mcpcat.modules.internal import get_server_tracking_data, set_server_tracking_data
-from mcpcat.modules.logging import write_to_log
+from agentcat.modules.constants import INACTIVITY_TIMEOUT_IN_MINUTES, SESSION_ID_PREFIX
+from agentcat.modules.internal import get_server_tracking_data, set_server_tracking_data
+from agentcat.modules.logging import write_to_log
-from ..types import MCPCatData, SessionInfo
+from ..types import AgentCatData, SessionInfo
from ..utils import generate_prefixed_ksuid
@@ -20,12 +20,12 @@ def new_session_id() -> str:
return generate_prefixed_ksuid(SESSION_ID_PREFIX)
-def get_mcpcat_version() -> str | None:
- """Get the current MCPCat SDK version."""
+def get_agentcat_version() -> str | None:
+ """Get the current AgentCat SDK version."""
try:
import importlib.metadata
- return importlib.metadata.version("mcpcat")
+ return importlib.metadata.version("agentcat")
except Exception:
return None
@@ -143,12 +143,12 @@ def get_client_info_from_request_context(
return (None, None)
-def get_session_info(server: Server, data: MCPCatData | None = None) -> SessionInfo:
+def get_session_info(server: Server, data: AgentCatData | None = None) -> SessionInfo:
"""Get session information for the current MCP session."""
session_info = SessionInfo(
ip_address=None, # grab from django
sdk_language=f"Python {sys.version_info.major}.{sys.version_info.minor}",
- mcpcat_version=get_mcpcat_version(),
+ agentcat_version=get_agentcat_version(),
server_name=server.name if hasattr(server, "name") else None,
server_version=server.version if hasattr(server, "version") else None,
client_name=data.session_info.client_name
@@ -174,7 +174,7 @@ def set_last_activity(server: Server) -> None:
data = get_server_tracking_data(server)
if not data:
- raise Exception("MCPCat data not initialized for this server")
+ raise Exception("AgentCat data not initialized for this server")
data.last_activity = datetime.now(timezone.utc)
set_server_tracking_data(server, data)
@@ -184,7 +184,7 @@ def get_server_session_id(server: Server) -> str | None:
data = get_server_tracking_data(server)
if not data:
- raise Exception("MCPCat data not initialized for this server")
+ raise Exception("AgentCat data not initialized for this server")
if data.is_stateless:
return None
diff --git a/src/mcpcat/modules/telemetry.py b/src/agentcat/modules/telemetry.py
similarity index 100%
rename from src/mcpcat/modules/telemetry.py
rename to src/agentcat/modules/telemetry.py
diff --git a/src/mcpcat/modules/tools.py b/src/agentcat/modules/tools.py
similarity index 92%
rename from src/mcpcat/modules/tools.py
rename to src/agentcat/modules/tools.py
index 8a1b537..71b005f 100644
--- a/src/mcpcat/modules/tools.py
+++ b/src/agentcat/modules/tools.py
@@ -1,8 +1,8 @@
-"""Tool management and interception for MCPCat."""
+"""Tool management and interception for AgentCat."""
from typing import Any, TYPE_CHECKING
from mcp.types import CallToolResult, TextContent
-from mcpcat.modules.version_detection import has_fastmcp_support
+from agentcat.modules.version_detection import has_fastmcp_support
from .logging import write_to_log
diff --git a/src/mcpcat/modules/truncation.py b/src/agentcat/modules/truncation.py
similarity index 93%
rename from src/mcpcat/modules/truncation.py
rename to src/agentcat/modules/truncation.py
index 2c43da2..da6799c 100644
--- a/src/mcpcat/modules/truncation.py
+++ b/src/agentcat/modules/truncation.py
@@ -1,4 +1,4 @@
-"""Event truncation for MCPcat.
+"""Event truncation for AgentCat.
Enforces a maximum event payload size by truncating oversized string
values, limiting nesting depth and collection breadth, and detecting
@@ -10,7 +10,7 @@
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
- from mcpcat.types import UnredactedEvent
+ from agentcat.types import UnredactedEvent
from .logging import write_to_log
@@ -30,7 +30,7 @@ def _truncate_string(value: str, max_bytes: int = MAX_STRING_BYTES) -> str:
if byte_size <= max_bytes:
return value
- marker = f"[string truncated by MCPcat from {byte_size} bytes]"
+ marker = f"[string truncated by AgentCat from {byte_size} bytes]"
marker_bytes = len(marker.encode("utf-8"))
keep_bytes = max_bytes - marker_bytes
@@ -75,11 +75,11 @@ def _truncate_value(
if i >= max_breadth:
remaining = len(items) - max_breadth
result["__truncated__"] = (
- f"[... {remaining} more items truncated by MCPcat]"
+ f"[... {remaining} more items truncated by AgentCat]"
)
break
if at_depth_limit and isinstance(v, (dict, list, tuple)):
- result[str(k)] = f"[nested content truncated by MCPcat at depth {max_depth}]"
+ result[str(k)] = f"[nested content truncated by AgentCat at depth {max_depth}]"
else:
result[str(k)] = _truncate_value(
v, max_depth=max_depth, max_string_bytes=max_string_bytes,
@@ -90,7 +90,7 @@ def _truncate_value(
if isinstance(value, (list, tuple)):
if at_depth_limit:
- return f"[nested content truncated by MCPcat at depth {max_depth}]"
+ return f"[nested content truncated by AgentCat at depth {max_depth}]"
result_list = [
_truncate_value(
item, max_depth=max_depth, max_string_bytes=max_string_bytes,
@@ -103,12 +103,12 @@ def _truncate_value(
if len(value) > max_breadth:
remaining = len(value) - max_breadth
result_list.append(
- f"[... {remaining} more items truncated by MCPcat]"
+ f"[... {remaining} more items truncated by AgentCat]"
)
return result_list
if at_depth_limit:
- return f"[nested content truncated by MCPcat at depth {max_depth}]"
+ return f"[nested content truncated by AgentCat at depth {max_depth}]"
# Fallback for unknown types โ repr and truncate
return _truncate_string(repr(value), max_bytes=max_string_bytes)
diff --git a/src/mcpcat/modules/validation.py b/src/agentcat/modules/validation.py
similarity index 96%
rename from src/mcpcat/modules/validation.py
rename to src/agentcat/modules/validation.py
index b3016fe..eb7af60 100644
--- a/src/mcpcat/modules/validation.py
+++ b/src/agentcat/modules/validation.py
@@ -12,7 +12,7 @@
def validate_tags(tags: dict) -> Optional[dict]:
- """Validate and filter a tags dict against MCPCat tag constraints.
+ """Validate and filter a tags dict against AgentCat tag constraints.
Invalid entries are dropped with a warning logged via write_to_log.
Returns None if no valid entries remain.
diff --git a/src/mcpcat/modules/version_detection.py b/src/agentcat/modules/version_detection.py
similarity index 100%
rename from src/mcpcat/modules/version_detection.py
rename to src/agentcat/modules/version_detection.py
diff --git a/src/agentcat/thirdparty/__init__.py b/src/agentcat/thirdparty/__init__.py
new file mode 100644
index 0000000..61aa796
--- /dev/null
+++ b/src/agentcat/thirdparty/__init__.py
@@ -0,0 +1 @@
+"""Third-party utilities for AgentCat."""
diff --git a/src/agentcat/thirdparty/ksuid/__init__.py b/src/agentcat/thirdparty/ksuid/__init__.py
new file mode 100644
index 0000000..f6c4876
--- /dev/null
+++ b/src/agentcat/thirdparty/ksuid/__init__.py
@@ -0,0 +1,5 @@
+"""KSUID (K-Sortable Unique Identifier) implementation for AgentCat."""
+
+from .ksuid import Ksuid, KsuidMs
+
+__all__ = ["Ksuid", "KsuidMs"]
diff --git a/src/mcpcat/thirdparty/ksuid/baseconv.py b/src/agentcat/thirdparty/ksuid/baseconv.py
similarity index 100%
rename from src/mcpcat/thirdparty/ksuid/baseconv.py
rename to src/agentcat/thirdparty/ksuid/baseconv.py
diff --git a/src/mcpcat/thirdparty/ksuid/ksuid.py b/src/agentcat/thirdparty/ksuid/ksuid.py
similarity index 100%
rename from src/mcpcat/thirdparty/ksuid/ksuid.py
rename to src/agentcat/thirdparty/ksuid/ksuid.py
diff --git a/src/mcpcat/types.py b/src/agentcat/types.py
similarity index 87%
rename from src/mcpcat/types.py
rename to src/agentcat/types.py
index 163767a..41f15a9 100644
--- a/src/mcpcat/types.py
+++ b/src/agentcat/types.py
@@ -1,14 +1,14 @@
-"""Type definitions for MCPCat."""
+"""Type definitions for AgentCat."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, Optional, Set, TypedDict, Literal, Union, NotRequired
-from mcpcat_api import PublishEventRequest
+from agentcat_api import PublishEventRequest
from pydantic import BaseModel
-from mcpcat.modules.constants import DEFAULT_CONTEXT_DESCRIPTION
+from agentcat.modules.constants import DEFAULT_CONTEXT_DESCRIPTION
# Type alias for identify function
IdentifyFunction = Callable[[dict[str, Any], Any], Optional["UserIdentity"]]
@@ -42,18 +42,21 @@ class SessionInfo(BaseModel):
ip_address: Optional[str] = None
sdk_language: Optional[str] = None
- mcpcat_version: Optional[str] = None
+ agentcat_version: Optional[str] = None
server_name: Optional[str] = None
server_version: Optional[str] = None
client_name: Optional[str] = None
client_version: Optional[str] = None
- identify_actor_given_id: Optional[str] = None # Actor ID for mcpcat:identify events
- identify_actor_name: Optional[str] = None # Actor name for mcpcat:identify events
+ identify_actor_given_id: Optional[str] = None # Actor ID for agentcat:identify events
+ identify_actor_name: Optional[str] = None # Actor name for agentcat:identify events
identify_data: Optional[dict[str, Any]] = None
class Event(PublishEventRequest):
- pass
+ # The generated client marks project_id as required on the wire, but the SDK
+ # constructs events before the project ID is known: event_queue merges it in
+ # at publish time, and telemetry-only mode sends events without one.
+ project_id: Optional[str] = None
# Error tracking types
@@ -109,7 +112,7 @@ class EventType(str, Enum):
MCP_RESOURCES_UNSUBSCRIBE = "mcp:resources/unsubscribe"
MCP_TOOLS_CALL = "mcp:tools/call"
MCP_TOOLS_LIST = "mcp:tools/list"
- MCPCAT_IDENTIFY = "mcpcat:identify"
+ AGENTCAT_IDENTIFY = "agentcat:identify"
class UnredactedEvent(Event):
@@ -164,8 +167,8 @@ class SentryExporterConfig(TypedDict):
@dataclass
-class MCPCatOptions:
- """Configuration options for MCPCat."""
+class AgentCatOptions:
+ """Configuration options for AgentCat."""
enable_report_missing: bool = True
enable_tracing: bool = True
@@ -177,20 +180,20 @@ class MCPCatOptions:
debug_mode: bool = False
api_base_url: str | None = None
stateless: bool | None = None
- # Disables MCPCat's internal SDK diagnostics โ anonymous, metadata-only
+ # Disables AgentCat's internal SDK diagnostics โ anonymous, metadata-only
# setup/error reporting used to detect failed installs. On by default; also
# disable-able via the DISABLE_DIAGNOSTICS env var. Automatically disabled in
# test environments (PYTEST_CURRENT_TEST / PYTEST_VERSION set) so test suites
# never send anything; set DISABLE_DIAGNOSTICS=false to re-enable there. Never
- # sends event payloads or user data; the local ~/mcpcat.log is unaffected.
+ # sends event payloads or user data; the local ~/agentcat.log is unaffected.
disable_diagnostics: bool = False
# Callback invoked on every auto-captured event (initialize, tools/list,
# tools/call) to attach string key-value tags. Tags are intended for
- # structured metadata you'll filter or group by in the MCPCat dashboard
+ # structured metadata you'll filter or group by in the AgentCat dashboard
# (e.g. trace IDs, environments, regions). Validated client-side: keys
# must be <=32 chars matching [a-zA-Z0-9$_.:\- ], values must be strings
# <=200 chars without newlines, max 50 entries per event. Invalid entries
- # are dropped with a warning logged to ~/mcpcat.log when debug_mode=True.
+ # are dropped with a warning logged to ~/agentcat.log when debug_mode=True.
# May be sync or async. Receives the same (request, extra) arguments as
# `identify`. If the callback raises or returns None/{}, tags are omitted.
event_tags: EventTagsFunction | None = None
@@ -204,14 +207,14 @@ class MCPCatOptions:
@dataclass
-class MCPCatData:
+class AgentCatData:
"""Internal data structure for tracking."""
project_id: str | None
session_id: str
session_info: SessionInfo
last_activity: datetime
- options: MCPCatOptions
+ options: AgentCatOptions
# Dynamic tracking fields (initialized on demand)
tool_registry: Dict[str, ToolRegistration] = field(default_factory=dict)
diff --git a/src/mcpcat/utils.py b/src/agentcat/utils.py
similarity index 98%
rename from src/mcpcat/utils.py
rename to src/agentcat/utils.py
index 92257f2..134a54e 100644
--- a/src/mcpcat/utils.py
+++ b/src/agentcat/utils.py
@@ -1,4 +1,4 @@
-"""Utility functions for MCPCat."""
+"""Utility functions for AgentCat."""
from typing import Optional
from datetime import datetime, timezone
diff --git a/src/mcpcat/modules/overrides/community_v3/__init__.py b/src/mcpcat/modules/overrides/community_v3/__init__.py
deleted file mode 100644
index a2c2bf9..0000000
--- a/src/mcpcat/modules/overrides/community_v3/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""Community FastMCP v3 integration using the middleware system."""
-
-from mcpcat.modules.overrides.community_v3.integration import (
- apply_community_v3_integration,
-)
-from mcpcat.modules.overrides.community_v3.middleware import MCPCatMiddleware
-
-__all__ = [
- "MCPCatMiddleware",
- "apply_community_v3_integration",
-]
diff --git a/src/mcpcat/thirdparty/__init__.py b/src/mcpcat/thirdparty/__init__.py
deleted file mode 100644
index 2071895..0000000
--- a/src/mcpcat/thirdparty/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Third-party utilities for MCPCat."""
diff --git a/src/mcpcat/thirdparty/ksuid/__init__.py b/src/mcpcat/thirdparty/ksuid/__init__.py
deleted file mode 100644
index 380a105..0000000
--- a/src/mcpcat/thirdparty/ksuid/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""KSUID (K-Sortable Unique Identifier) implementation for MCPCat."""
-
-from .ksuid import Ksuid, KsuidMs
-
-__all__ = ["Ksuid", "KsuidMs"]
diff --git a/tests/__init__.py b/tests/__init__.py
index d801804..cad1123 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1 +1 @@
-"""Tests for MCPCat."""
+"""Tests for AgentCat."""
diff --git a/tests/community/test_community_dynamic_tracking.py b/tests/community/test_community_dynamic_tracking.py
index e30bfbd..56ab5dd 100644
--- a/tests/community/test_community_dynamic_tracking.py
+++ b/tests/community/test_community_dynamic_tracking.py
@@ -3,13 +3,13 @@
import pytest
-from mcpcat import track
-from mcpcat.modules.internal import (
+from agentcat import track
+from agentcat.modules.internal import (
get_server_tracking_data,
get_tool_timeline,
reset_all_tracking_data,
)
-from mcpcat.types import MCPCatOptions
+from agentcat.types import AgentCatOptions
from ..test_utils.community_client import create_community_test_client
from ..test_utils.community_todo_server import (
@@ -184,7 +184,7 @@ def tool1(x: int) -> str:
return str(x)
# Enable tracking
- options = MCPCatOptions()
+ options = AgentCatOptions()
track(server, "test-project", options)
# Register second tool
@@ -230,7 +230,7 @@ async def test_context_injection_with_dynamic_tracking(self):
server = FastMCP("test-server")
# Enable tracking with context
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test-project", options)
# Add tool after tracking
@@ -281,7 +281,7 @@ async def test_report_missing_tool_with_dynamic_tracking(self):
server = FastMCP("test-server")
# Enable tracking with report_missing
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test-project", options)
# Test calling get_more_tools
@@ -321,7 +321,7 @@ async def test_multiple_servers_isolation(self):
server2 = FastMCP("server2")
# Track both servers
- options = MCPCatOptions()
+ options = AgentCatOptions()
track(server1, "project1", options)
track(server2, "project2", options)
@@ -362,7 +362,7 @@ async def test_existing_todo_server_tools(self):
server = create_community_todo_server()
# Enable tracking
- options = MCPCatOptions()
+ options = AgentCatOptions()
track(server, "test-project", options)
# Test existing tools work correctly
diff --git a/tests/community/test_community_event_capture.py b/tests/community/test_community_event_capture.py
index 084cd56..84ab85f 100644
--- a/tests/community/test_community_event_capture.py
+++ b/tests/community/test_community_event_capture.py
@@ -6,13 +6,13 @@
import pytest
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
-from mcpcat.modules.internal import (
+from agentcat import AgentCatOptions, track
+from agentcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat.modules.internal import (
get_server_tracking_data,
set_server_tracking_data,
)
-from mcpcat.types import UserIdentity
+from agentcat.types import UserIdentity
from ..test_utils.community_client import create_community_test_client
from ..test_utils.community_todo_server import (
@@ -35,7 +35,7 @@ class TestCommunityEventCapture:
def setup_and_teardown(self):
"""Set up and tear down for each test."""
# Store original event queue
- from mcpcat.modules.event_queue import event_queue as original_queue
+ from agentcat.modules.event_queue import event_queue as original_queue
yield
# Restore original event queue after test
@@ -59,7 +59,7 @@ def capture_event(publish_event_request):
# Create and track server
server = create_community_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -106,7 +106,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_community_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -136,7 +136,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_community_todo_server()
- options = MCPCatOptions(enable_tracing=True, enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tracing=True, enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -182,7 +182,7 @@ def identify_fn(request, context):
)
server = create_community_todo_server()
- options = MCPCatOptions(enable_tracing=True, identify=identify_fn)
+ options = AgentCatOptions(enable_tracing=True, identify=identify_fn)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -217,7 +217,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_community_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -257,7 +257,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_community_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -304,7 +304,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_community_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -338,7 +338,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_community_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
diff --git a/tests/community/test_community_fastmcp.py b/tests/community/test_community_fastmcp.py
index 44e77ff..836e3ca 100644
--- a/tests/community/test_community_fastmcp.py
+++ b/tests/community/test_community_fastmcp.py
@@ -1,8 +1,8 @@
-"""Basic tests for Community FastMCP integration with MCPCat."""
+"""Basic tests for Community FastMCP integration with AgentCat."""
import pytest
-from mcpcat import MCPCatOptions, track
+from agentcat import AgentCatOptions, track
from ..test_utils.community_client import create_community_test_client
from ..test_utils.community_todo_server import (
@@ -52,7 +52,7 @@ async def test_tool_registration(self):
@pytest.mark.asyncio
async def test_is_community_fastmcp_server(self):
"""Test is_community_fastmcp_server identifies community FastMCP."""
- from mcpcat.modules.compatibility import (
+ from agentcat.modules.compatibility import (
is_community_fastmcp_server,
is_compatible_server,
is_official_fastmcp_server,
@@ -70,9 +70,9 @@ async def test_is_community_fastmcp_server(self):
"Server should NOT be identified as official FastMCP"
)
- # Should be compatible with MCPCat
+ # Should be compatible with AgentCat
assert is_compatible_server(server) is True, (
- "Server should be compatible with MCPCat"
+ "Server should be compatible with AgentCat"
)
@@ -102,14 +102,14 @@ async def test_tool_execution(self):
assert "โ" in str(result) # Completed
-class TestCommunityFastMCPWithMCPCat:
- """Test Community FastMCP integration with MCPCat tracking."""
+class TestCommunityFastMCPWithAgentCat:
+ """Test Community FastMCP integration with AgentCat tracking."""
@pytest.mark.asyncio
- async def test_mcpcat_tracking_basic(self):
- """Test that MCPCat can track a community FastMCP server."""
+ async def test_agentcat_tracking_basic(self):
+ """Test that AgentCat can track a community FastMCP server."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=False)
+ options = AgentCatOptions(enable_tool_call_context=False)
# This will likely fail initially due to incompatibilities
# but demonstrates the intended usage
@@ -117,14 +117,14 @@ async def test_mcpcat_tracking_basic(self):
async with create_community_test_client(server) as client:
# Test that tracking doesn't break basic functionality
- result = await client.call_tool("add_todo", {"text": "MCPCat test"})
+ result = await client.call_tool("add_todo", {"text": "AgentCat test"})
assert "Added todo" in str(result)
@pytest.mark.asyncio
- async def test_mcpcat_tracking_with_context(self):
- """Test MCPCat context injection with community FastMCP."""
+ async def test_agentcat_tracking_with_context(self):
+ """Test AgentCat context injection with community FastMCP."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
# Track the server with context enabled
track(server, "test_project", options)
@@ -149,7 +149,7 @@ async def test_mcpcat_tracking_with_context(self):
f"Tool {tool.name} schema has no properties"
)
- # This assertion will fail, showing that MCPCat's context injection
+ # This assertion will fail, showing that AgentCat's context injection
# doesn't work with community FastMCP
assert "context" in schema["properties"], (
f"Tool {tool.name} is missing 'context' parameter. "
diff --git a/tests/community/test_community_report_missing.py b/tests/community/test_community_report_missing.py
index e6570ba..9db041c 100644
--- a/tests/community/test_community_report_missing.py
+++ b/tests/community/test_community_report_missing.py
@@ -4,7 +4,7 @@
from unittest.mock import MagicMock
import time
-from mcpcat import MCPCatOptions, track
+from agentcat import AgentCatOptions, track
from ..test_utils.community_client import create_community_test_client
from ..test_utils.community_todo_server import (
@@ -28,7 +28,7 @@ async def test_report_missing_tool_injection(self):
server = create_community_todo_server()
# Track the server with report_missing enabled
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
# Use client to list all tools and verify report_missing is injected
@@ -51,7 +51,7 @@ async def test_report_missing_tool_injection(self):
async def test_report_missing_tool_schema(self):
"""Test that get_more_tools has context as a required string, not anyOf."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -83,7 +83,7 @@ async def test_report_missing_disabled_by_default(self):
server = create_community_todo_server()
# Track with report_missing disabled
- options = MCPCatOptions(enable_report_missing=False)
+ options = AgentCatOptions(enable_report_missing=False)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -99,7 +99,7 @@ async def test_report_missing_disabled_by_default(self):
async def test_report_missing_tool_call_success(self):
"""Call report_missing tool and verify it executes successfully."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -116,7 +116,7 @@ async def test_report_missing_tool_call_success(self):
async def test_report_missing_with_valid_params(self):
"""Test with various valid parameters."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -136,7 +136,7 @@ async def test_report_missing_with_valid_params(self):
async def test_report_missing_with_missing_params(self):
"""Test error handling when required parameters are missing."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -154,7 +154,7 @@ async def test_report_missing_with_missing_params(self):
async def test_report_missing_with_other_tools(self):
"""Verify report_missing doesn't interfere with existing server tools."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -176,7 +176,7 @@ async def test_report_missing_with_other_tools(self):
async def test_multiple_report_missing_calls(self):
"""Test calling report_missing multiple times in succession."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -200,7 +200,7 @@ async def test_multiple_report_missing_calls(self):
async def test_report_missing_with_context_enabled(self):
"""Test interaction when both report_missing and tool_context are enabled."""
server = create_community_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_report_missing=True, enable_tool_call_context=True
)
track(server, "test_project", options)
@@ -230,7 +230,7 @@ async def test_report_missing_with_context_enabled(self):
async def test_report_missing_with_null_values(self):
"""Test with null/None values for parameters."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -242,7 +242,7 @@ async def test_report_missing_with_null_values(self):
@pytest.mark.asyncio
async def test_report_missing_publishes_event(self):
"""Verify that calling report_missing tool publishes an event to the queue."""
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
# Create a mock API client
mock_api_client = MagicMock()
@@ -256,7 +256,7 @@ async def test_report_missing_publishes_event(self):
try:
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True, enable_tracing=True)
+ options = AgentCatOptions(enable_report_missing=True, enable_tracing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -304,14 +304,14 @@ async def test_report_missing_publishes_event(self):
finally:
# Clean up: restore original event queue
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
set_event_queue(EventQueue())
@pytest.mark.asyncio
async def test_multiple_tool_calls_publish_multiple_events(self):
"""Verify that multiple tool calls result in multiple events being published."""
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
# Create a mock API client
mock_api_client = MagicMock()
@@ -325,7 +325,7 @@ async def test_multiple_tool_calls_publish_multiple_events(self):
try:
server = create_community_todo_server()
- options = MCPCatOptions(enable_report_missing=True, enable_tracing=True)
+ options = AgentCatOptions(enable_report_missing=True, enable_tracing=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -395,7 +395,7 @@ async def test_multiple_tool_calls_publish_multiple_events(self):
finally:
# Clean up: restore original event queue
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
set_event_queue(EventQueue())
diff --git a/tests/community/test_community_tool_context.py b/tests/community/test_community_tool_context.py
index 8e434bc..74ee026 100644
--- a/tests/community/test_community_tool_context.py
+++ b/tests/community/test_community_tool_context.py
@@ -2,8 +2,8 @@
import pytest
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.constants import DEFAULT_CONTEXT_DESCRIPTION
+from agentcat import AgentCatOptions, track
+from agentcat.modules.constants import DEFAULT_CONTEXT_DESCRIPTION
from ..test_utils.community_client import create_community_test_client
from ..test_utils.community_todo_server import (
@@ -25,7 +25,7 @@ class TestCommunityToolContext:
async def test_context_parameter_injection_enabled(self):
"""Test that context parameter is added when enable_tool_call_context=True."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -54,7 +54,7 @@ async def test_context_parameter_injection_enabled(self):
async def test_context_parameter_not_injected_when_disabled(self):
"""Test that context parameter is NOT added when enable_tool_call_context=False."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=False)
+ options = AgentCatOptions(enable_tool_call_context=False)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -74,7 +74,7 @@ async def test_context_parameter_not_injected_when_disabled(self):
async def test_schema_with_existing_properties(self):
"""Test with tools that have existing inputSchema and properties."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -106,7 +106,7 @@ def simple_tool():
"""A tool with no parameters."""
return "success"
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -138,7 +138,7 @@ def empty_tool():
"""Tool with empty schema."""
return "success"
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -153,7 +153,7 @@ def empty_tool():
async def test_schema_with_existing_required_fields(self):
"""Test with tools that already have required fields."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -171,7 +171,7 @@ async def test_schema_with_existing_required_fields(self):
async def test_tool_call_with_valid_context(self):
"""Test calling a tool with valid context parameter."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -191,7 +191,7 @@ async def test_tool_call_with_valid_context(self):
async def test_tool_call_without_context_still_works(self):
"""Test that tool calls without context still work (context is stripped)."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -208,7 +208,7 @@ async def test_tool_call_without_context_still_works(self):
async def test_tool_call_with_empty_context(self):
"""Test calling a tool with empty string context."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -227,7 +227,7 @@ async def test_tool_call_with_empty_context(self):
async def test_get_more_tools_exclusion_with_context(self):
"""Test that get_more_tools doesn't get context when both features are enabled."""
server = create_community_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_report_missing=True, enable_tool_call_context=True
)
track(server, "test_project", options)
@@ -250,7 +250,7 @@ async def test_get_more_tools_exclusion_with_context(self):
@pytest.mark.asyncio
async def test_tool_with_existing_context_parameter(self):
- """Test that existing context parameter is overwritten with MCPCat's version."""
+ """Test that existing context parameter is overwritten with AgentCat's version."""
if not HAS_COMMUNITY_FASTMCP:
pytest.skip("Community FastMCP not available")
@@ -263,7 +263,7 @@ def tool_with_context(context: str, data: str):
"""Tool that already has a context parameter."""
return f"Original context: {context}, data: {data}"
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -273,7 +273,7 @@ def tool_with_context(context: str, data: str):
# Verify context exists
assert "context" in tool.inputSchema["properties"]
- # Check if it has MCPCat's description
+ # Check if it has AgentCat's description
context_schema = tool.inputSchema["properties"]["context"]
assert (
context_schema.get("description")
@@ -287,7 +287,7 @@ def tool_with_context(context: str, data: str):
async def test_original_functionality_preserved(self):
"""Verify that original tool functionality remains intact with context."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -320,7 +320,7 @@ async def test_original_functionality_preserved(self):
async def test_context_not_passed_to_original_handler(self):
"""Verify that context parameter is stripped before passing to original handler."""
server = create_community_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -345,7 +345,7 @@ async def test_dynamically_added_tool_gets_context(self):
server = FastMCP("test-server")
# Track with context enabled
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
# Add tool AFTER tracking
@@ -374,7 +374,7 @@ async def test_custom_context_description(self):
"""Test that custom context description is correctly applied in community FastMCP."""
server = create_community_todo_server()
custom_description = "Explain why you need to use this tool"
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=custom_description
)
@@ -396,7 +396,7 @@ async def test_custom_context_description(self):
async def test_custom_context_description_empty_string(self):
"""Test edge case with empty string custom description in community FastMCP."""
server = create_community_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=""
)
@@ -418,7 +418,7 @@ async def test_custom_context_description_special_characters(self):
"""Test custom description with special characters and Unicode in community FastMCP."""
server = create_community_todo_server()
special_description = "Why use this? ๐ Include: 'quotes', \"double\", newlines\n, tabs\t."
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=special_description
)
@@ -438,7 +438,7 @@ async def test_custom_context_description_very_long(self):
server = create_community_todo_server()
# Create a very long description
long_description = "This is a very detailed description for the context. " * 50
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=long_description
)
@@ -458,7 +458,7 @@ async def test_default_context_description(self):
"""Verify the default description is used when not specified in community FastMCP."""
server = create_community_todo_server()
# Don't specify custom_context_description, should use default
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_community_test_client(server) as client:
@@ -495,7 +495,7 @@ def tool3():
return "Tool 3"
custom_desc = "Custom community context for all tools"
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=custom_desc
)
@@ -515,7 +515,7 @@ async def test_custom_context_with_tool_call(self):
"""Test tool calls work correctly with custom context description in community FastMCP."""
server = create_community_todo_server()
custom_desc = "Provide reasoning for this action in the community server"
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=custom_desc
)
@@ -551,7 +551,7 @@ async def test_custom_context_with_dynamically_added_tool(self):
# Track with custom context description
custom_desc = "Dynamic tool custom context"
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=custom_desc
)
diff --git a/tests/community/test_community_tracking_timing.py b/tests/community/test_community_tracking_timing.py
index efab758..190a9d1 100644
--- a/tests/community/test_community_tracking_timing.py
+++ b/tests/community/test_community_tracking_timing.py
@@ -2,8 +2,8 @@
import pytest
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.internal import (
+from agentcat import AgentCatOptions, track
+from agentcat.modules.internal import (
get_server_tracking_data,
reset_all_tracking_data,
)
@@ -43,7 +43,7 @@ async def test_track_empty_server_then_add_tools(self):
server = FastMCP("empty-server")
# Track the empty server first (disable report_missing to truly have no tools)
- options = MCPCatOptions(enable_report_missing=False)
+ options = AgentCatOptions(enable_report_missing=False)
track(server, "test-project", options)
# Verify tracking is initialized even with no tools
@@ -96,7 +96,7 @@ def existing_tool2(x: int) -> str:
return f"Existing2: {x + 1}"
# Track server with some tools (disable report_missing for cleaner counts)
- options = MCPCatOptions(enable_report_missing=False)
+ options = AgentCatOptions(enable_report_missing=False)
track(server, "test-project", options)
# Test initial tools work (registered when list_tools or call_tool invoked)
@@ -175,7 +175,7 @@ async def async_tool_d(x: int) -> str:
return f"D: {x - 5}"
# Track server AFTER all tools added (disable report_missing for cleaner counts)
- options = MCPCatOptions(enable_report_missing=False)
+ options = AgentCatOptions(enable_report_missing=False)
track(server, "test-project", options)
# Test all tools work correctly
@@ -210,7 +210,7 @@ async def test_track_with_options_on_empty_server(self):
server = FastMCP("empty-with-options")
# Track with options on empty server
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
enable_report_missing=True,
enable_tracing=True
@@ -286,7 +286,7 @@ def tool2(x: int) -> str:
return f"Tool2: {x * 2}"
# Third track call with options
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "project3", options)
@server.tool
@@ -336,7 +336,7 @@ def step2_tool(x: int) -> str:
return f"Step2: {x * 2}"
# Track again with options
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test-project", options)
# Add third and fourth tools
diff --git a/tests/e2e/community_v2/conftest.py b/tests/e2e/community_v2/conftest.py
index 7e8f414..582730e 100644
--- a/tests/e2e/community_v2/conftest.py
+++ b/tests/e2e/community_v2/conftest.py
@@ -15,9 +15,9 @@
import pytest
-import mcpcat
-from mcpcat import MCPCatOptions
-from mcpcat.modules.compatibility import (
+import agentcat
+from agentcat import AgentCatOptions
+from agentcat.modules.compatibility import (
is_community_fastmcp_v2,
is_community_fastmcp_v3,
)
@@ -45,8 +45,8 @@ def add_todo(text: str, context: str = "") -> str:
return mcp
-def _default_options_factory() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True)
+def _default_options_factory() -> AgentCatOptions:
+ return AgentCatOptions(enable_tracing=True)
@pytest.fixture(scope="module")
@@ -62,11 +62,11 @@ def v2_http_server(request) -> Tuple[str, Any]:
if not is_community_fastmcp_v2(server):
pytest.skip("server is not detected as community FastMCP v2")
- options_factory: Callable[[], MCPCatOptions] = getattr(
- request.module, "MCPCAT_OPTIONS_FACTORY", _default_options_factory
+ options_factory: Callable[[], AgentCatOptions] = getattr(
+ request.module, "AGENTCAT_OPTIONS_FACTORY", _default_options_factory
)
options = options_factory()
- mcpcat.track(server, "test_project", options)
+ agentcat.track(server, "test_project", options)
import uvicorn
diff --git a/tests/e2e/community_v2/test_identify_http.py b/tests/e2e/community_v2/test_identify_http.py
index 8d6a695..93ed1f9 100644
--- a/tests/e2e/community_v2/test_identify_http.py
+++ b/tests/e2e/community_v2/test_identify_http.py
@@ -10,8 +10,8 @@
import pytest
-from mcpcat.modules.internal import get_server_tracking_data
-from mcpcat.types import UserIdentity
+from agentcat.modules.internal import get_server_tracking_data
+from agentcat.types import UserIdentity
pytestmark = pytest.mark.e2e
diff --git a/tests/e2e/community_v3/conftest.py b/tests/e2e/community_v3/conftest.py
index cb12152..1f42ce8 100644
--- a/tests/e2e/community_v3/conftest.py
+++ b/tests/e2e/community_v3/conftest.py
@@ -14,15 +14,15 @@
import pytest
-import mcpcat
-from mcpcat import MCPCatOptions
+import agentcat
+from agentcat import AgentCatOptions
from tests.e2e._helpers import find_free_port, wait_for_port
try:
from fastmcp import FastMCP
- from mcpcat.modules.compatibility import is_community_fastmcp_v3
+ from agentcat.modules.compatibility import is_community_fastmcp_v3
HAS_FASTMCP_V3 = True
except ImportError:
FastMCP = None # type: ignore
@@ -45,8 +45,8 @@ def list_todos(context: str = "") -> str:
return mcp
-def _default_options_factory() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True)
+def _default_options_factory() -> AgentCatOptions:
+ return AgentCatOptions(enable_tracing=True)
@pytest.fixture(scope="module")
@@ -58,11 +58,11 @@ def v3_http_server(request) -> Tuple[str, Any]:
if not is_community_fastmcp_v3(server):
pytest.skip("installed fastmcp is not v3")
- options_factory: Callable[[], MCPCatOptions] = getattr(
- request.module, "MCPCAT_OPTIONS_FACTORY", _default_options_factory
+ options_factory: Callable[[], AgentCatOptions] = getattr(
+ request.module, "AGENTCAT_OPTIONS_FACTORY", _default_options_factory
)
options = options_factory()
- mcpcat.track(server, "test_project", options)
+ agentcat.track(server, "test_project", options)
import uvicorn
diff --git a/tests/e2e/community_v3/test_identify_http.py b/tests/e2e/community_v3/test_identify_http.py
index 4392857..3c794fb 100644
--- a/tests/e2e/community_v3/test_identify_http.py
+++ b/tests/e2e/community_v3/test_identify_http.py
@@ -7,8 +7,8 @@
import pytest
-from mcpcat.modules.internal import get_server_tracking_data
-from mcpcat.types import UserIdentity
+from agentcat.modules.internal import get_server_tracking_data
+from agentcat.types import UserIdentity
pytestmark = pytest.mark.e2e
@@ -51,7 +51,7 @@ def identify(_req: Any, extra: Any) -> Optional[UserIdentity]:
@pytest.mark.asyncio
-async def test_mcpcat_identify_self_event_via_v3_middleware(
+async def test_agentcat_identify_self_event_via_v3_middleware(
v3_http_server, capture_queue
):
from fastmcp import Client
@@ -71,10 +71,10 @@ def identify(_req: Any, _extra: Any) -> Optional[UserIdentity]:
time.sleep(0.5)
identify_events = [
- e for e in capture_queue if e.event_type == "mcpcat:identify"
+ e for e in capture_queue if e.event_type == "agentcat:identify"
]
assert identify_events, (
- f"expected mcpcat:identify under v3, got "
+ f"expected agentcat:identify under v3, got "
f"{[e.event_type for e in capture_queue]}"
)
assert identify_events[0].identify_actor_given_id == "v3-bob"
diff --git a/tests/e2e/community_v3/test_stateless_http.py b/tests/e2e/community_v3/test_stateless_http.py
index 3b8d617..935c005 100644
--- a/tests/e2e/community_v3/test_stateless_http.py
+++ b/tests/e2e/community_v3/test_stateless_http.py
@@ -7,11 +7,11 @@
import pytest
-from mcpcat import MCPCatOptions
+from agentcat import AgentCatOptions
-def MCPCAT_OPTIONS_FACTORY() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True, stateless=True)
+def AGENTCAT_OPTIONS_FACTORY() -> AgentCatOptions:
+ return AgentCatOptions(enable_tracing=True, stateless=True)
pytestmark = pytest.mark.e2e
@@ -53,7 +53,7 @@ async def call_once(text: str) -> None:
await asyncio.gather(call_once("a"), call_once("b"))
time.sleep(0.7)
- from mcpcat.modules.internal import get_server_tracking_data
+ from agentcat.modules.internal import get_server_tracking_data
data = get_server_tracking_data(server)
assert data is not None
diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py
index f046068..0cf0524 100644
--- a/tests/e2e/conftest.py
+++ b/tests/e2e/conftest.py
@@ -12,7 +12,7 @@
import pytest
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat.modules.event_queue import EventQueue, set_event_queue
@pytest.fixture
@@ -22,7 +22,7 @@ def capture_queue() -> List[Any]:
Yields the list of captured PublishEventRequest objects. Tests assert on
its contents after the in-flight HTTP round-trip plus a short settle.
"""
- from mcpcat.modules.event_queue import event_queue as original
+ from agentcat.modules.event_queue import event_queue as original
captured: List[Any] = []
mock = MagicMock()
diff --git a/tests/e2e/official/conftest.py b/tests/e2e/official/conftest.py
index e7a2fdc..0850cb8 100644
--- a/tests/e2e/official/conftest.py
+++ b/tests/e2e/official/conftest.py
@@ -1,8 +1,8 @@
"""Uvicorn-in-thread harness for the official MCP SDK.
-A test module declares an `MCPCAT_OPTIONS_FACTORY` (callable returning
-`MCPCatOptions`) at module scope; `official_http_server` boots a fresh
-FastMCP todo server for the module, calls `mcpcat.track(...)` with those
+A test module declares an `AGENTCAT_OPTIONS_FACTORY` (callable returning
+`AgentCatOptions`) at module scope; `official_http_server` boots a fresh
+FastMCP todo server for the module, calls `agentcat.track(...)` with those
options, mounts the server's Streamable-HTTP app, and yields the URL.
Module-scoped: one boot per test file, not per test.
@@ -16,34 +16,34 @@
import pytest
import uvicorn
-import mcpcat
-from mcpcat import MCPCatOptions
+import agentcat
+from agentcat import AgentCatOptions
from tests.e2e._helpers import find_free_port, wait_for_port
from tests.test_utils.todo_server import create_todo_server
-def _default_options_factory() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True)
+def _default_options_factory() -> AgentCatOptions:
+ return AgentCatOptions(enable_tracing=True)
@pytest.fixture(scope="module")
def official_http_server(request) -> Tuple[str, Any]:
"""Boot a Streamable-HTTP MCP server for the test module.
- Reads the module attribute `MCPCAT_OPTIONS_FACTORY` (Callable[[], MCPCatOptions])
+ Reads the module attribute `AGENTCAT_OPTIONS_FACTORY` (Callable[[], AgentCatOptions])
if defined; otherwise uses tracing-only defaults.
Yields:
(url, server) โ the Streamable-HTTP URL (e.g. "http://127.0.0.1:54321/mcp")
and the FastMCP server instance under test.
"""
- options_factory: Callable[[], MCPCatOptions] = getattr(
- request.module, "MCPCAT_OPTIONS_FACTORY", _default_options_factory
+ options_factory: Callable[[], AgentCatOptions] = getattr(
+ request.module, "AGENTCAT_OPTIONS_FACTORY", _default_options_factory
)
options = options_factory()
server = create_todo_server()
- mcpcat.track(server, "test_project", options)
+ agentcat.track(server, "test_project", options)
app = server.streamable_http_app()
port = find_free_port()
diff --git a/tests/e2e/official/test_identify_http.py b/tests/e2e/official/test_identify_http.py
index fcd3eb9..f005663 100644
--- a/tests/e2e/official/test_identify_http.py
+++ b/tests/e2e/official/test_identify_http.py
@@ -1,6 +1,6 @@
"""Identify-per-event behavior over real Streamable HTTP.
-Tests mutate the running server's MCPCatData.options.identify to vary the hook
+Tests mutate the running server's AgentCatData.options.identify to vary the hook
per scenario. The default options-factory is tracing-only with no identify;
identify-swapping on the live server matches the pattern used by
tests/test_stateless.py.
@@ -17,8 +17,8 @@
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
-from mcpcat.modules.internal import get_server_tracking_data
-from mcpcat.types import UserIdentity
+from agentcat.modules.internal import get_server_tracking_data
+from agentcat.types import UserIdentity
pytestmark = pytest.mark.e2e
@@ -65,7 +65,7 @@ def identify(request: Any, extra: Any) -> Optional[UserIdentity]:
@pytest.mark.asyncio
-async def test_mcpcat_identify_self_event_published_per_request(
+async def test_agentcat_identify_self_event_published_per_request(
official_http_server, capture_queue
):
url, server = official_http_server
@@ -84,10 +84,10 @@ def identify(_req: Any, _extra: Any) -> Optional[UserIdentity]:
time.sleep(0.5)
identify_events = [
- e for e in capture_queue if e.event_type == "mcpcat:identify"
+ e for e in capture_queue if e.event_type == "agentcat:identify"
]
assert identify_events, (
- f"expected mcpcat:identify event, got "
+ f"expected agentcat:identify event, got "
f"{[e.event_type for e in capture_queue]}"
)
assert identify_events[0].identify_actor_given_id == "bob"
@@ -153,7 +153,7 @@ def identify(_req: Any, _extra: Any) -> Optional[UserIdentity]:
time.sleep(0.5)
identify_events = [
- e for e in capture_queue if e.event_type == "mcpcat:identify"
+ e for e in capture_queue if e.event_type == "agentcat:identify"
]
assert not identify_events, (
f"identify returned None; should NOT publish self-event, got "
diff --git a/tests/e2e/official/test_redaction_http.py b/tests/e2e/official/test_redaction_http.py
index 1f06eaa..30cb60f 100644
--- a/tests/e2e/official/test_redaction_http.py
+++ b/tests/e2e/official/test_redaction_http.py
@@ -1,6 +1,6 @@
"""Redaction over real-wire payloads.
-KNOWN BUG (xfail-tracked): `mcpcat.modules.redaction.redact_event` only
+KNOWN BUG (xfail-tracked): `agentcat.modules.redaction.redact_event` only
recurses into `dict` and `list` types, not Pydantic `UnredactedEvent` objects.
The event_queue worker invokes `redact_event(event, ...)` where `event` is an
`UnredactedEvent`; the call returns the input unchanged, so customer-supplied
@@ -20,7 +20,7 @@
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
-from mcpcat.modules.internal import get_server_tracking_data
+from agentcat.modules.internal import get_server_tracking_data
pytestmark = pytest.mark.e2e
diff --git a/tests/e2e/official/test_session_http.py b/tests/e2e/official/test_session_http.py
index 5915ee0..3e46252 100644
--- a/tests/e2e/official/test_session_http.py
+++ b/tests/e2e/official/test_session_http.py
@@ -3,7 +3,7 @@
NOTE: User-Agent / X-MCP-Client-Name header parsing is *not* tested e2e
because real MCP clients always populate session.client_params.clientInfo
during initialize, and that path wins over header parsing in the SDK
-(see src/mcpcat/modules/session.py::get_client_info_from_request_context).
+(see src/agentcat/modules/session.py::get_client_info_from_request_context).
The header-fallback path is covered by unit tests in tests/test_stateless.py
that mock ctx.session = None to force the fallback. e2e here verifies the
real-world path: clientInfo from the SDK propagates to events.
@@ -21,11 +21,11 @@
from mcp.client.streamable_http import streamablehttp_client
from mcp.types import Implementation
-from mcpcat import MCPCatOptions
+from agentcat import AgentCatOptions
-def MCPCAT_OPTIONS_FACTORY() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True, stateless=True)
+def AGENTCAT_OPTIONS_FACTORY() -> AgentCatOptions:
+ return AgentCatOptions(enable_tracing=True, stateless=True)
pytestmark = pytest.mark.e2e
diff --git a/tests/e2e/official/test_stateless_http.py b/tests/e2e/official/test_stateless_http.py
index 78b1601..d79f0cc 100644
--- a/tests/e2e/official/test_stateless_http.py
+++ b/tests/e2e/official/test_stateless_http.py
@@ -10,11 +10,11 @@
from mcp.client.streamable_http import streamablehttp_client
from mcp.types import Implementation
-from mcpcat import MCPCatOptions
+from agentcat import AgentCatOptions
-def MCPCAT_OPTIONS_FACTORY() -> MCPCatOptions:
- return MCPCatOptions(enable_tracing=True, stateless=True)
+def AGENTCAT_OPTIONS_FACTORY() -> AgentCatOptions:
+ return AgentCatOptions(enable_tracing=True, stateless=True)
pytestmark = pytest.mark.e2e
@@ -102,7 +102,7 @@ async def test_stateless_no_session_info_pollution(
time.sleep(0.5)
- from mcpcat.modules.internal import get_server_tracking_data
+ from agentcat.modules.internal import get_server_tracking_data
data = get_server_tracking_data(server)
assert data is not None
diff --git a/tests/test_api_base_url.py b/tests/test_api_base_url.py
index 14140bd..3135b08 100644
--- a/tests/test_api_base_url.py
+++ b/tests/test_api_base_url.py
@@ -3,34 +3,34 @@
import os
from unittest.mock import MagicMock, patch
-from mcpcat.types import MCPCatOptions
+from agentcat.types import AgentCatOptions
-class TestMCPCatOptionsApiBaseUrl:
- """Test api_base_url field on MCPCatOptions."""
+class TestAgentCatOptionsApiBaseUrl:
+ """Test api_base_url field on AgentCatOptions."""
def test_default_is_none(self):
- """MCPCatOptions should have api_base_url default to None."""
- opts = MCPCatOptions()
+ """AgentCatOptions should have api_base_url default to None."""
+ opts = AgentCatOptions()
assert opts.api_base_url is None
def test_can_set_api_base_url(self):
- """MCPCatOptions should accept an api_base_url parameter."""
- opts = MCPCatOptions(api_base_url="https://custom.example.com")
+ """AgentCatOptions should accept an api_base_url parameter."""
+ opts = AgentCatOptions(api_base_url="https://custom.example.com")
assert opts.api_base_url == "https://custom.example.com"
class TestEventQueueConfigure:
"""Test EventQueue.configure() method."""
- @patch("mcpcat.modules.event_queue.EventsApi")
- @patch("mcpcat.modules.event_queue.ApiClient")
- @patch("mcpcat.modules.event_queue.Configuration")
+ @patch("agentcat.modules.event_queue.EventsApi")
+ @patch("agentcat.modules.event_queue.ApiClient")
+ @patch("agentcat.modules.event_queue.Configuration")
def test_configure_changes_api_base_url(
self, mock_configuration, mock_api_client, mock_events_api
):
"""configure() should recreate the API client with the new base URL."""
- from mcpcat.modules.event_queue import EventQueue
+ from agentcat.modules.event_queue import EventQueue
eq = EventQueue(api_client=MagicMock())
eq.configure("https://custom.example.com")
@@ -44,20 +44,20 @@ def test_configure_changes_api_base_url(
)
assert eq.api_client == mock_events_api.return_value
- @patch("mcpcat.modules.event_queue.EventsApi")
- @patch("mcpcat.modules.event_queue.ApiClient")
- @patch("mcpcat.modules.event_queue.Configuration")
+ @patch("agentcat.modules.event_queue.EventsApi")
+ @patch("agentcat.modules.event_queue.ApiClient")
+ @patch("agentcat.modules.event_queue.Configuration")
def test_default_url_used_when_not_configured(
self, mock_configuration, mock_api_client, mock_events_api
):
- """EventQueue() should use MCPCAT_API_URL by default."""
- from mcpcat.modules.constants import MCPCAT_API_URL
- from mcpcat.modules.event_queue import EventQueue
+ """EventQueue() should use AGENTCAT_API_URL by default."""
+ from agentcat.modules.constants import AGENTCAT_API_URL
+ from agentcat.modules.event_queue import EventQueue
eq = EventQueue()
# Check that Configuration was called with the default URL
- mock_configuration.assert_called_with(host=MCPCAT_API_URL)
+ mock_configuration.assert_called_with(host=AGENTCAT_API_URL)
class TestTrackApiBaseUrl:
@@ -65,41 +65,41 @@ class TestTrackApiBaseUrl:
# Common patches needed to isolate track() from real MCP server logic
TRACK_PATCHES = [
- "mcpcat.is_community_fastmcp_v3",
- "mcpcat.is_community_fastmcp_v2",
- "mcpcat.is_official_fastmcp_server",
- "mcpcat.is_compatible_server",
- "mcpcat._apply_server_tracking",
- "mcpcat.get_session_info",
- "mcpcat.set_server_tracking_data",
+ "agentcat.is_community_fastmcp_v3",
+ "agentcat.is_community_fastmcp_v2",
+ "agentcat.is_official_fastmcp_server",
+ "agentcat.is_compatible_server",
+ "agentcat._apply_server_tracking",
+ "agentcat.get_session_info",
+ "agentcat.set_server_tracking_data",
]
def _call_track_with_patches(self, options, env_vars=None):
"""Helper to call track() with all internals mocked, returning a mock event_queue."""
- from mcpcat import track
+ from agentcat import track
mock_eq = MagicMock()
patches = {}
for p in self.TRACK_PATCHES:
patches[p] = patch(p)
- eq_patch = patch("mcpcat.modules.event_queue.event_queue", mock_eq)
+ eq_patch = patch("agentcat.modules.event_queue.event_queue", mock_eq)
started = []
try:
for name, p in patches.items():
m = p.start()
started.append(p)
- if name == "mcpcat.is_compatible_server":
+ if name == "agentcat.is_compatible_server":
m.return_value = True
elif name in (
- "mcpcat.is_community_fastmcp_v3",
- "mcpcat.is_community_fastmcp_v2",
- "mcpcat.is_official_fastmcp_server",
+ "agentcat.is_community_fastmcp_v3",
+ "agentcat.is_community_fastmcp_v2",
+ "agentcat.is_official_fastmcp_server",
):
m.return_value = False
- elif name == "mcpcat.get_session_info":
- from mcpcat.types import SessionInfo
+ elif name == "agentcat.get_session_info":
+ from agentcat.types import SessionInfo
m.return_value = SessionInfo()
eq_patch.start()
@@ -110,8 +110,9 @@ def _call_track_with_patches(self, options, env_vars=None):
with patch.dict(os.environ, env_vars, clear=True):
track(server, project_id="proj-123", options=options)
else:
- # Clear MCPCAT_API_URL from env to avoid interference
+ # Clear API URL env vars to avoid interference
env = os.environ.copy()
+ env.pop("AGENTCAT_API_URL", None)
env.pop("MCPCAT_API_URL", None)
with patch.dict(os.environ, env, clear=True):
track(server, project_id="proj-123", options=options)
@@ -123,21 +124,41 @@ def _call_track_with_patches(self, options, env_vars=None):
def test_option_overrides_default(self):
"""api_base_url option should trigger configure() on event_queue."""
- opts = MCPCatOptions(api_base_url="https://custom.example.com")
+ opts = AgentCatOptions(api_base_url="https://custom.example.com")
mock_eq = self._call_track_with_patches(opts)
mock_eq.configure.assert_called_once_with("https://custom.example.com")
def test_env_var_overrides_default(self):
"""MCPCAT_API_URL env var should trigger configure() when no option set."""
- opts = MCPCatOptions()
+ opts = AgentCatOptions()
mock_eq = self._call_track_with_patches(
opts, env_vars={"MCPCAT_API_URL": "https://env.example.com"}
)
mock_eq.configure.assert_called_once_with("https://env.example.com")
+ def test_agentcat_env_var_overrides_default(self):
+ """AGENTCAT_API_URL env var should trigger configure() when no option set."""
+ opts = AgentCatOptions()
+ mock_eq = self._call_track_with_patches(
+ opts, env_vars={"AGENTCAT_API_URL": "https://new.example.com"}
+ )
+ mock_eq.configure.assert_called_once_with("https://new.example.com")
+
+ def test_agentcat_env_var_takes_precedence_over_mcpcat(self):
+ """AGENTCAT_API_URL wins over the legacy MCPCAT_API_URL fallback."""
+ opts = AgentCatOptions()
+ mock_eq = self._call_track_with_patches(
+ opts,
+ env_vars={
+ "AGENTCAT_API_URL": "https://new.example.com",
+ "MCPCAT_API_URL": "https://legacy.example.com",
+ },
+ )
+ mock_eq.configure.assert_called_once_with("https://new.example.com")
+
def test_option_takes_precedence_over_env_var(self):
"""api_base_url option should take precedence over MCPCAT_API_URL env var."""
- opts = MCPCatOptions(api_base_url="https://option.example.com")
+ opts = AgentCatOptions(api_base_url="https://option.example.com")
mock_eq = self._call_track_with_patches(
opts, env_vars={"MCPCAT_API_URL": "https://env.example.com"}
)
@@ -145,6 +166,6 @@ def test_option_takes_precedence_over_env_var(self):
def test_no_configure_when_using_default(self):
"""configure() should NOT be called when neither option nor env var is set."""
- opts = MCPCatOptions()
+ opts = AgentCatOptions()
mock_eq = self._call_track_with_patches(opts)
mock_eq.configure.assert_not_called()
diff --git a/tests/test_context_parameters.py b/tests/test_context_parameters.py
index 1b66155..6fefa53 100644
--- a/tests/test_context_parameters.py
+++ b/tests/test_context_parameters.py
@@ -4,7 +4,7 @@
from copy import deepcopy
from typing import Any
-from mcpcat.modules.context_parameters import (
+from agentcat.modules.context_parameters import (
add_context_parameter_to_tools,
add_context_parameter_to_schema,
)
diff --git a/tests/test_deprecation_notice.py b/tests/test_deprecation_notice.py
deleted file mode 100644
index 40cb995..0000000
--- a/tests/test_deprecation_notice.py
+++ /dev/null
@@ -1,31 +0,0 @@
-"""The mcpcat -> agentcat rename notice must be visible with default warning
-filters (the reason it is a FutureWarning), and must never touch stdout,
-which would corrupt stdio MCP transports."""
-
-import subprocess
-import sys
-
-
-def _import_mcpcat_subprocess() -> subprocess.CompletedProcess:
- # A fresh interpreter with default warning filters โ pytest's own filter
- # config must not leak into what real users see.
- return subprocess.run(
- [sys.executable, "-c", "import mcpcat"],
- capture_output=True,
- text=True,
- timeout=60,
- )
-
-
-def test_rename_notice_visible_by_default():
- result = _import_mcpcat_subprocess()
- assert result.returncode == 0
- assert "FutureWarning" in result.stderr
- assert "renamed to 'agentcat'" in result.stderr
- assert "pip install agentcat" in result.stderr
-
-
-def test_rename_notice_never_writes_to_stdout():
- result = _import_mcpcat_subprocess()
- assert result.returncode == 0
- assert result.stdout == ""
diff --git a/tests/test_diagnostics_attributes.py b/tests/test_diagnostics_attributes.py
index 99604de..e13890b 100644
--- a/tests/test_diagnostics_attributes.py
+++ b/tests/test_diagnostics_attributes.py
@@ -2,7 +2,8 @@
import pytest
-from mcpcat.modules import diagnostics
+from agentcat.modules import diagnostics
+from agentcat.modules.constants import DIAGNOSTICS_SCOPE_NAME
@pytest.fixture(autouse=True)
@@ -24,24 +25,24 @@ def _attrs() -> dict[str, str]:
def test_project_id_present_no_install_id():
diagnostics.init_diagnostics("proj_ABC")
attrs = _attrs()
- assert attrs.get("mcpcat.project_id") == "proj_ABC"
- assert "mcpcat.install_id" not in attrs
+ assert attrs.get("agentcat.project_id") == "proj_ABC"
+ assert "agentcat.install_id" not in attrs
def test_anonymous_install_id_when_no_project():
diagnostics.init_diagnostics(None)
attrs = _attrs()
- assert "mcpcat.project_id" not in attrs
- assert attrs.get("mcpcat.install_id")
+ assert "agentcat.project_id" not in attrs
+ assert attrs.get("agentcat.install_id")
def test_install_id_stable_across_reinit():
diagnostics.init_diagnostics(None)
- first = _attrs().get("mcpcat.install_id")
+ first = _attrs().get("agentcat.install_id")
diagnostics._reset_diagnostics_for_test()
diagnostics.init_diagnostics(None)
- second = _attrs().get("mcpcat.install_id")
+ second = _attrs().get("agentcat.install_id")
assert first and first == second
@@ -49,8 +50,9 @@ def test_install_id_stable_across_reinit():
def test_sdk_and_environment_metadata_present():
diagnostics.init_diagnostics("proj_1")
attrs = _attrs()
- assert attrs.get("mcpcat.sdk.language") == "python"
- assert attrs.get("mcpcat.sdk.version")
+ assert DIAGNOSTICS_SCOPE_NAME == "agentcat-diagnostics"
+ assert attrs.get("agentcat.sdk.language") == "python"
+ assert attrs.get("agentcat.sdk.version")
assert attrs.get("os.type")
assert attrs.get("process.runtime.name")
assert attrs.get("process.runtime.version")
diff --git a/tests/test_diagnostics_auth.py b/tests/test_diagnostics_auth.py
index 0c816ea..7b71f96 100644
--- a/tests/test_diagnostics_auth.py
+++ b/tests/test_diagnostics_auth.py
@@ -4,8 +4,8 @@
import pytest
-from mcpcat.modules import diagnostics
-from mcpcat.modules.logging import write_to_log
+from agentcat.modules import diagnostics
+from agentcat.modules.logging import write_to_log
@pytest.fixture(autouse=True)
@@ -20,7 +20,7 @@ def reset(monkeypatch):
def _flush_and_get_headers() -> dict:
write_to_log("a log line")
- with patch("mcpcat.modules.diagnostics.requests.post") as mock_post:
+ with patch("agentcat.modules.diagnostics.requests.post") as mock_post:
diagnostics.flush_diagnostics()
mock_post.assert_called_once()
return mock_post.call_args.kwargs["headers"]
diff --git a/tests/test_diagnostics_export.py b/tests/test_diagnostics_export.py
index 3635566..7da2e6b 100644
--- a/tests/test_diagnostics_export.py
+++ b/tests/test_diagnostics_export.py
@@ -4,8 +4,8 @@
import pytest
-from mcpcat.modules import diagnostics
-from mcpcat.modules.logging import write_to_log
+from agentcat.modules import diagnostics
+from agentcat.modules.logging import write_to_log
@pytest.fixture(autouse=True)
@@ -22,7 +22,7 @@ def test_flush_posts_otlp_shaped_json():
diagnostics.init_diagnostics("proj_1")
write_to_log("Warning: something happened")
- with patch("mcpcat.modules.diagnostics.requests.post") as mock_post:
+ with patch("agentcat.modules.diagnostics.requests.post") as mock_post:
diagnostics.flush_diagnostics()
mock_post.assert_called_once()
@@ -40,7 +40,7 @@ def test_flush_posts_otlp_shaped_json():
a["key"]: a["value"]["stringValue"]
for a in payload["resourceLogs"][0]["resource"]["attributes"]
}
- assert resource_attrs.get("mcpcat.project_id") == "proj_1"
+ assert resource_attrs.get("agentcat.project_id") == "proj_1"
def test_flush_swallows_post_errors():
@@ -48,7 +48,7 @@ def test_flush_swallows_post_errors():
write_to_log("some log line")
with patch(
- "mcpcat.modules.diagnostics.requests.post",
+ "agentcat.modules.diagnostics.requests.post",
side_effect=RuntimeError("network down"),
):
# Must not raise.
@@ -59,7 +59,7 @@ def test_no_post_when_disabled():
diagnostics.init_diagnostics("proj_1", disabled=True)
write_to_log("ignored")
- with patch("mcpcat.modules.diagnostics.requests.post") as mock_post:
+ with patch("agentcat.modules.diagnostics.requests.post") as mock_post:
diagnostics.flush_diagnostics()
mock_post.assert_not_called()
@@ -67,6 +67,6 @@ def test_no_post_when_disabled():
def test_no_post_when_buffer_empty():
diagnostics.init_diagnostics("proj_1")
- with patch("mcpcat.modules.diagnostics.requests.post") as mock_post:
+ with patch("agentcat.modules.diagnostics.requests.post") as mock_post:
diagnostics.flush_diagnostics()
mock_post.assert_not_called()
diff --git a/tests/test_diagnostics_integration.py b/tests/test_diagnostics_integration.py
index b4f4f4f..b5cc33d 100644
--- a/tests/test_diagnostics_integration.py
+++ b/tests/test_diagnostics_integration.py
@@ -4,8 +4,8 @@
import pytest
-from mcpcat.modules import diagnostics
-from mcpcat.modules.logging import write_to_log
+from agentcat.modules import diagnostics
+from agentcat.modules.logging import write_to_log
@pytest.fixture(autouse=True)
@@ -19,9 +19,9 @@ def reset(monkeypatch):
def test_end_to_end_buffers_and_posts():
diagnostics.init_diagnostics("proj_int")
- write_to_log("MCPCat setup started | project proj_int | server lowlevel")
+ write_to_log("AgentCat setup started | project proj_int | server lowlevel")
- with patch("mcpcat.modules.diagnostics.requests.post") as mock_post:
+ with patch("agentcat.modules.diagnostics.requests.post") as mock_post:
diagnostics.flush_diagnostics()
mock_post.assert_called_once()
@@ -34,6 +34,6 @@ def test_disabled_never_posts():
diagnostics.init_diagnostics("proj_int", disabled=True)
write_to_log("nothing should buffer")
- with patch("mcpcat.modules.diagnostics.requests.post") as mock_post:
+ with patch("agentcat.modules.diagnostics.requests.post") as mock_post:
diagnostics.flush_diagnostics()
mock_post.assert_not_called()
diff --git a/tests/test_diagnostics_no_payload.py b/tests/test_diagnostics_no_payload.py
index ac922b3..4be84b9 100644
--- a/tests/test_diagnostics_no_payload.py
+++ b/tests/test_diagnostics_no_payload.py
@@ -10,11 +10,11 @@
import pytest
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules import diagnostics
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
-from mcpcat.modules.logging import set_diagnostics_sink
-from mcpcat.types import UserIdentity
+from agentcat import AgentCatOptions, track
+from agentcat.modules import diagnostics
+from agentcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat.modules.logging import set_diagnostics_sink
+from agentcat.types import UserIdentity
from .test_utils.client import create_test_client
from .test_utils.todo_server import create_todo_server
@@ -59,7 +59,7 @@ def identify_fn(request, context):
track(
server,
"test-project",
- MCPCatOptions(
+ AgentCatOptions(
enable_report_missing=True,
enable_tracing=True,
identify=identify_fn,
@@ -76,9 +76,9 @@ def identify_fn(request, context):
all_logs = "\n".join(captured)
# Setup beacons present + metadata-only.
- assert "MCPCat setup started" in all_logs
+ assert "AgentCat setup started" in all_logs
assert "test-project" in all_logs
- assert "MCPCat setup complete" in all_logs
+ assert "AgentCat setup complete" in all_logs
assert "tracing=True" in all_logs
# Report-missing logs only the context length, never the context text.
diff --git a/tests/test_diagnostics_optout.py b/tests/test_diagnostics_optout.py
index baafd6e..dcfe22a 100644
--- a/tests/test_diagnostics_optout.py
+++ b/tests/test_diagnostics_optout.py
@@ -8,7 +8,7 @@
import pytest
-from mcpcat.modules import diagnostics
+from agentcat.modules import diagnostics
@pytest.fixture(autouse=True)
diff --git a/tests/test_diagnostics_record.py b/tests/test_diagnostics_record.py
index 7808772..9651e03 100644
--- a/tests/test_diagnostics_record.py
+++ b/tests/test_diagnostics_record.py
@@ -1,6 +1,6 @@
"""Tests for OTLP record construction + severity inference."""
-from mcpcat.modules import diagnostics
+from agentcat.modules import diagnostics
def test_warning_failed_is_error():
@@ -24,7 +24,7 @@ def test_lowercase_warning_is_info():
def test_plain_is_info():
- rec = diagnostics._build_record_for_test("MCPCat setup complete | project x")
+ rec = diagnostics._build_record_for_test("AgentCat setup complete | project x")
assert rec["severityText"] == "INFO"
assert rec["severityNumber"] == 9
diff --git a/tests/test_diagnostics_sink.py b/tests/test_diagnostics_sink.py
index 4c78352..a2fbdfe 100644
--- a/tests/test_diagnostics_sink.py
+++ b/tests/test_diagnostics_sink.py
@@ -1,6 +1,6 @@
"""Tests for the diagnostics sink hook in logging.write_to_log."""
-from mcpcat.modules.logging import (
+from agentcat.modules.logging import (
set_debug_mode,
set_diagnostics_sink,
write_to_log,
diff --git a/tests/test_diagnostics_test_env.py b/tests/test_diagnostics_test_env.py
index 90e5c00..2f257af 100644
--- a/tests/test_diagnostics_test_env.py
+++ b/tests/test_diagnostics_test_env.py
@@ -1,6 +1,6 @@
"""Regression: diagnostics must auto-disable inside test environments.
-Ports MCPCat/mcpcat-typescript-sdk#44 to Python. A pytest run must never ship
+Ports AgentCat/agentcat-typescript-sdk#44 to Python. A pytest run must never ship
OTLP diagnostics to the live collector, even when a test calls ``track()`` with
default options and no ``DISABLE_DIAGNOSTICS`` override. Consumers of the SDK get
this protection for free; explicit ``DISABLE_DIAGNOSTICS=false`` opts back in.
@@ -10,8 +10,8 @@
import pytest
-import mcpcat
-from mcpcat.modules import diagnostics
+import agentcat
+from agentcat.modules import diagnostics
@pytest.fixture(autouse=True)
@@ -25,11 +25,11 @@ def test_track_does_not_enable_diagnostics_in_pytest(monkeypatch):
# Simulate a consumer's test suite: no explicit opt-out in the environment.
monkeypatch.delenv("DISABLE_DIAGNOSTICS", raising=False)
- with patch("mcpcat.modules.diagnostics.requests.post") as mock_post:
+ with patch("agentcat.modules.diagnostics.requests.post") as mock_post:
# track() runs init_diagnostics before validating the server, so even
# the error path would latch diagnostics on without the guard.
with pytest.raises(TypeError):
- mcpcat.track(object(), "proj_test_env")
+ agentcat.track(object(), "proj_test_env")
assert diagnostics.is_diagnostics_enabled() is False
diagnostics.flush_diagnostics()
diff --git a/tests/test_dynamic_tracking.py b/tests/test_dynamic_tracking.py
index 4d0bbb1..6b985c3 100644
--- a/tests/test_dynamic_tracking.py
+++ b/tests/test_dynamic_tracking.py
@@ -9,9 +9,9 @@
from mcp.server import Server
from mcp import Tool
-from mcpcat import track
-from mcpcat.types import MCPCatOptions
-from mcpcat.modules.internal import (
+from agentcat import track
+from agentcat.types import AgentCatOptions
+from agentcat.modules.internal import (
get_server_tracking_data,
reset_all_tracking_data,
get_tool_timeline,
@@ -150,7 +150,7 @@ def tool1(x: int) -> str:
return str(x)
# Enable tracking
- options = MCPCatOptions()
+ options = AgentCatOptions()
track(fastmcp_server, "test-project", options)
# Register second tool
@@ -188,7 +188,7 @@ def tool2(x: int) -> str:
async def test_context_injection_with_dynamic_tracking(self, fastmcp_server):
"""Test that context injection works with dynamic tracking and tool still functions."""
# Enable tracking with context
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(fastmcp_server, "test-project", options)
# Add tool after tracking
@@ -234,7 +234,7 @@ def context_tool(x: int) -> str:
async def test_report_missing_tool_with_dynamic_tracking(self, fastmcp_server):
"""Test that the get_more_tools tool is added with dynamic tracking and works correctly."""
# Enable tracking with report_missing
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(fastmcp_server, "test-project", options)
# List tools
@@ -287,7 +287,7 @@ async def call_tool(name: str, arguments: dict) -> List[Any]:
raise ValueError(f"Unknown tool: {name}")
# Enable dynamic tracking
- options = MCPCatOptions()
+ options = AgentCatOptions()
track(lowlevel_server, "test-project", options)
# List tools to trigger tracking
@@ -320,7 +320,7 @@ async def test_multiple_servers_isolation(self):
server2 = FastMCP("server2")
# Track both servers
- options = MCPCatOptions()
+ options = AgentCatOptions()
track(server1, "project1", options)
track(server2, "project2", options)
diff --git a/tests/test_event_capture_completeness.py b/tests/test_event_capture_completeness.py
index 967c10b..5e4cd93 100644
--- a/tests/test_event_capture_completeness.py
+++ b/tests/test_event_capture_completeness.py
@@ -6,10 +6,10 @@
from datetime import datetime, timezone
from mcp import Implementation
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
-from mcpcat.modules.internal import get_server_tracking_data, set_server_tracking_data
-from mcpcat.types import UserIdentity
+from agentcat import AgentCatOptions, track
+from agentcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat.modules.internal import get_server_tracking_data, set_server_tracking_data
+from agentcat.types import UserIdentity
from .test_utils.client import create_test_client
from .test_utils.todo_server import create_todo_server
@@ -22,7 +22,7 @@ class TestEventCaptureCompleteness:
def setup_and_teardown(self):
"""Set up and tear down for each test."""
# Store original event queue
- from mcpcat.modules.event_queue import event_queue as original_queue
+ from agentcat.modules.event_queue import event_queue as original_queue
yield
# Restore original event queue after test
@@ -46,7 +46,7 @@ def capture_event(publish_event_request):
# Create and track server
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -93,7 +93,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
# Create client with default info
@@ -126,7 +126,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -144,7 +144,7 @@ def capture_event(publish_event_request):
@pytest.mark.asyncio
async def test_event_contains_sdk_info(self):
- """Test that events capture SDK language and MCPCat version."""
+ """Test that events capture SDK language and AgentCat version."""
mock_api_client = MagicMock()
captured_events = []
@@ -157,7 +157,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -172,7 +172,7 @@ def capture_event(publish_event_request):
# SDK info should be captured
assert event.sdk_language is not None
assert event.sdk_language.startswith("Python ")
- assert event.mcpcat_version is not None # Should be the installed version
+ assert event.agentcat_version is not None # Should be the installed version
@pytest.mark.asyncio
async def test_event_contains_user_intent_from_context(self):
@@ -189,7 +189,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True, enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tracing=True, enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -242,7 +242,7 @@ def identify_fn(request, context):
)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True, identify=identify_fn)
+ options = AgentCatOptions(enable_tracing=True, identify=identify_fn)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -277,7 +277,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -301,7 +301,7 @@ def capture_event(publish_event_request):
# Session info fields
assert event.sdk_language is not None
- assert event.mcpcat_version is not None
+ assert event.agentcat_version is not None
assert event.server_name == "todo-server"
assert event.server_version == None
assert event.client_name == "mcp"
@@ -322,7 +322,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -357,7 +357,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -392,7 +392,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
# Creating the client triggers initialization
@@ -415,7 +415,7 @@ def capture_event(publish_event_request):
assert event.client_name == "test-client"
assert event.client_version == "1.0.0"
assert event.sdk_language is not None
- assert event.mcpcat_version is not None
+ assert event.agentcat_version is not None
@pytest.mark.asyncio
async def test_identify_function_integration(self):
@@ -444,7 +444,7 @@ def custom_identify(request, server):
return None
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True, identify=custom_identify)
+ options = AgentCatOptions(enable_tracing=True, identify=custom_identify)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -484,7 +484,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
diff --git a/tests/test_event_queue.py b/tests/test_event_queue.py
index ceb2c8c..1e5b987 100644
--- a/tests/test_event_queue.py
+++ b/tests/test_event_queue.py
@@ -7,9 +7,9 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock, call, patch
-from mcpcat.modules.event_queue import EventQueue, publish_event
-from mcpcat.modules.logging import write_to_log
-from mcpcat.types import Event, MCPCatData, MCPCatOptions, SessionInfo, UnredactedEvent
+from agentcat.modules.event_queue import EventQueue, publish_event
+from agentcat.modules.logging import write_to_log
+from agentcat.types import Event, AgentCatData, AgentCatOptions, SessionInfo, UnredactedEvent
class TestEventQueue:
@@ -57,7 +57,7 @@ def test_add_event_when_shutdown(self):
timestamp=datetime.now(timezone.utc),
)
- with patch("mcpcat.modules.event_queue.write_to_log") as mock_log:
+ with patch("agentcat.modules.event_queue.write_to_log") as mock_log:
eq.add(event)
assert mock_log.called
assert any(
@@ -97,7 +97,7 @@ def test_add_event_queue_full(self):
eq.queue.put_nowait(event1)
eq.queue.put_nowait(event2)
- with patch("mcpcat.modules.event_queue.write_to_log") as mock_log:
+ with patch("agentcat.modules.event_queue.write_to_log") as mock_log:
eq.add(event3)
assert mock_log.called
assert any(
@@ -133,7 +133,7 @@ def test_add_event_queue_full_drops_new_event(self):
eq.queue.put_nowait(event1)
- with patch("mcpcat.modules.event_queue.write_to_log") as mock_log:
+ with patch("agentcat.modules.event_queue.write_to_log") as mock_log:
eq.add(event2)
# Event2 should not have been added
@@ -146,7 +146,7 @@ def test_add_event_queue_full_drops_new_event(self):
assert "event-2" in log_message
assert "mcp:tools/call" in log_message
- @patch("mcpcat.modules.event_queue.redact_event")
+ @patch("agentcat.modules.event_queue.redact_event")
def test_process_event_with_redaction(self, mock_redact):
"""Test processing event with redaction function."""
eq = EventQueue()
@@ -189,8 +189,8 @@ def test_process_event_with_redaction(self, mock_redact):
assert called_event.redaction_fn is None
mock_send.assert_called_once()
- @patch("mcpcat.modules.event_queue.redact_event")
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.redact_event")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_process_event_redaction_failure(self, mock_log, mock_redact):
"""Test processing event when redaction fails."""
eq = EventQueue()
@@ -217,7 +217,7 @@ def test_process_event_redaction_failure(self, mock_log, mock_redact):
assert "test-id" in log_message
mock_send.assert_not_called()
- @patch("mcpcat.modules.event_queue.generate_prefixed_ksuid")
+ @patch("agentcat.modules.event_queue.generate_prefixed_ksuid")
def test_process_event_without_id(self, mock_ksuid):
"""Test processing event without ID generates one."""
eq = EventQueue()
@@ -240,7 +240,7 @@ def test_process_event_without_id(self, mock_ksuid):
assert sent_event.id == generated_id
mock_send.assert_called_once()
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_send_event_success(self, mock_log):
"""Test sending event successfully."""
eq = EventQueue()
@@ -264,7 +264,7 @@ def test_send_event_success(self, mock_log):
)
assert mock_log.call_count >= 1 # At least one success log
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_send_event_success_logs_session_not_payload(self, mock_log):
"""Success log carries session metadata, never a serialized payload."""
eq = EventQueue()
@@ -288,7 +288,7 @@ def test_send_event_success_logs_session_not_payload(self, mock_log):
assert "Event details" not in logged
assert "model_dump_json" not in logged
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_send_event_with_retries(self, mock_log):
"""Test sending event with retries on failure."""
eq = EventQueue()
@@ -321,7 +321,7 @@ def test_send_event_with_retries(self, mock_log):
wait_calls = [call[1]["timeout"] for call in eq._shutdown_event.wait.call_args_list]
assert wait_calls[0] < wait_calls[1] # Exponential backoff
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_send_event_max_retries_exceeded(self, mock_log):
"""Test sending event when max retries exceeded."""
eq = EventQueue()
@@ -350,7 +350,7 @@ def test_send_event_max_retries_exceeded(self, mock_log):
# Check that failure was logged
assert any("retries" in str(call).lower() for call in mock_log.call_args_list)
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_send_event_aborts_retry_on_shutdown(self, mock_log):
"""Test that retry is aborted when shutdown is signaled during backoff wait."""
eq = EventQueue()
@@ -380,7 +380,7 @@ def test_send_event_aborts_retry_on_shutdown(self, mock_log):
# Log should mention shutdown
assert any("shutdown" in str(call).lower() for call in mock_log.call_args_list)
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_send_event_early_return_on_shutdown_detected(self, mock_log):
"""Test that no retry is attempted when shutdown is already set at exception handler entry."""
eq = EventQueue()
@@ -460,7 +460,7 @@ def test_destroy(self, mock_sleep):
@patch("time.time")
@patch("time.sleep")
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_destroy_with_timeout(self, mock_log, mock_sleep, mock_time):
"""Test destroy with events still in queue after timeout."""
eq = EventQueue()
@@ -519,7 +519,7 @@ def mock_process(event):
# Verify event was picked up by worker
assert eq.queue.qsize() == 0
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_worker_thread_exception_handling(self, mock_log):
"""Test worker thread handles exceptions gracefully."""
eq = EventQueue()
@@ -555,22 +555,22 @@ def test_worker_thread_exception_handling(self, mock_log):
class TestPublishEvent:
"""Test publish_event function."""
- @patch("mcpcat.modules.event_queue.get_server_tracking_data")
- @patch("mcpcat.modules.event_queue.get_session_info")
- @patch("mcpcat.modules.event_queue.set_last_activity")
- @patch("mcpcat.modules.event_queue.event_queue")
+ @patch("agentcat.modules.event_queue.get_server_tracking_data")
+ @patch("agentcat.modules.event_queue.get_session_info")
+ @patch("agentcat.modules.event_queue.set_last_activity")
+ @patch("agentcat.modules.event_queue.event_queue")
def test_publish_event_success(
self, mock_eq, mock_set_activity, mock_session, mock_tracking
):
"""Test publishing event successfully."""
# Mock server and data
mock_server = MagicMock()
- mock_data = MCPCatData(
+ mock_data = AgentCatData(
project_id="project-123",
session_id="session-123",
session_info=SessionInfo(),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(redact_sensitive_information=None),
+ options=AgentCatOptions(redact_sensitive_information=None),
)
mock_tracking.return_value = mock_data
@@ -601,8 +601,8 @@ def test_publish_event_success(
assert added_event.event_type == "mcp:tools/call"
assert added_event.session_id is not None
- @patch("mcpcat.modules.event_queue.get_server_tracking_data")
- @patch("mcpcat.modules.event_queue.write_to_log")
+ @patch("agentcat.modules.event_queue.get_server_tracking_data")
+ @patch("agentcat.modules.event_queue.write_to_log")
def test_publish_event_no_tracking_data(self, mock_log, mock_tracking):
"""Test publishing event when no tracking data available."""
mock_server = MagicMock()
@@ -621,21 +621,21 @@ def test_publish_event_no_tracking_data(self, mock_log, mock_tracking):
"tracking data" in str(call).lower() for call in mock_log.call_args_list
)
- @patch("mcpcat.modules.event_queue.get_server_tracking_data")
- @patch("mcpcat.modules.event_queue.get_session_info")
- @patch("mcpcat.modules.event_queue.set_last_activity")
- @patch("mcpcat.modules.event_queue.event_queue")
+ @patch("agentcat.modules.event_queue.get_server_tracking_data")
+ @patch("agentcat.modules.event_queue.get_session_info")
+ @patch("agentcat.modules.event_queue.set_last_activity")
+ @patch("agentcat.modules.event_queue.event_queue")
def test_publish_event_calculates_duration(
self, mock_eq, mock_set_activity, mock_session, mock_tracking
):
"""Test publishing event calculates duration if not provided."""
mock_server = MagicMock()
- mock_data = MCPCatData(
+ mock_data = AgentCatData(
project_id="project-123",
session_id="session-123",
session_info=SessionInfo(),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
mock_tracking.return_value = mock_data
mock_session.return_value = SessionInfo()
@@ -649,7 +649,7 @@ def test_publish_event_calculates_duration(
)
# Mock current time to be 1 second later
- with patch("mcpcat.modules.event_queue.datetime") as mock_datetime:
+ with patch("agentcat.modules.event_queue.datetime") as mock_datetime:
mock_datetime.now.return_value.timestamp.return_value = (
event_timestamp.timestamp() + 1
)
@@ -660,21 +660,21 @@ def test_publish_event_calculates_duration(
assert event.duration is not None
assert event.duration > 0
- @patch("mcpcat.modules.event_queue.get_server_tracking_data")
- @patch("mcpcat.modules.event_queue.get_session_info")
- @patch("mcpcat.modules.event_queue.set_last_activity")
- @patch("mcpcat.modules.event_queue.event_queue")
+ @patch("agentcat.modules.event_queue.get_server_tracking_data")
+ @patch("agentcat.modules.event_queue.get_session_info")
+ @patch("agentcat.modules.event_queue.set_last_activity")
+ @patch("agentcat.modules.event_queue.event_queue")
def test_publish_event_no_duration_no_timestamp(
self, mock_eq, mock_set_activity, mock_session, mock_tracking
):
"""Test publishing event with no duration and no timestamp sets duration to None."""
mock_server = MagicMock()
- mock_data = MCPCatData(
+ mock_data = AgentCatData(
project_id="project-123",
session_id="session-123",
session_info=SessionInfo(),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
mock_tracking.return_value = mock_data
mock_session.return_value = SessionInfo()
@@ -687,22 +687,22 @@ def test_publish_event_no_duration_no_timestamp(
# Check duration is None
assert event.duration is None
- @patch("mcpcat.modules.event_queue.get_server_tracking_data")
- @patch("mcpcat.modules.event_queue.get_session_info")
- @patch("mcpcat.modules.event_queue.set_last_activity")
- @patch("mcpcat.modules.event_queue.event_queue")
+ @patch("agentcat.modules.event_queue.get_server_tracking_data")
+ @patch("agentcat.modules.event_queue.get_session_info")
+ @patch("agentcat.modules.event_queue.set_last_activity")
+ @patch("agentcat.modules.event_queue.event_queue")
def test_publish_event_with_redaction_function(
self, mock_eq, mock_set_activity, mock_session, mock_tracking
):
"""Test publishing event includes redaction function from options."""
mock_server = MagicMock()
mock_redaction_fn = MagicMock()
- mock_data = MCPCatData(
+ mock_data = AgentCatData(
project_id="project-123",
session_id="session-123",
session_info=SessionInfo(),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(redact_sensitive_information=mock_redaction_fn),
+ options=AgentCatOptions(redact_sensitive_information=mock_redaction_fn),
)
mock_tracking.return_value = mock_data
mock_session.return_value = SessionInfo()
@@ -720,15 +720,15 @@ def test_publish_event_with_redaction_function(
assert added_event.redaction_fn == mock_redaction_fn
-@patch("mcpcat.modules.event_queue.signal.signal")
-@patch("mcpcat.modules.event_queue.atexit.register")
+@patch("agentcat.modules.event_queue.signal.signal")
+@patch("agentcat.modules.event_queue.atexit.register")
def test_shutdown_handlers_registered(mock_atexit, mock_signal):
"""Test that shutdown handlers are registered on module import."""
# Import the module to trigger registration
import importlib
- import mcpcat.modules.event_queue
+ import agentcat.modules.event_queue
- importlib.reload(mcpcat.modules.event_queue)
+ importlib.reload(agentcat.modules.event_queue)
# Check signal handlers registered
assert mock_signal.call_count >= 2
@@ -742,11 +742,11 @@ def test_shutdown_handlers_registered(mock_atexit, mock_signal):
@patch("os._exit")
-@patch("mcpcat.modules.event_queue.signal.signal")
-@patch("mcpcat.modules.event_queue.event_queue")
+@patch("agentcat.modules.event_queue.signal.signal")
+@patch("agentcat.modules.event_queue.event_queue")
def test_shutdown_handler_function(mock_event_queue, mock_signal, mock_exit):
"""Test the _shutdown_handler function."""
- from mcpcat.modules.event_queue import _shutdown_handler
+ from agentcat.modules.event_queue import _shutdown_handler
# Call the shutdown handler with proper signal handler arguments
_shutdown_handler(signal.SIGINT, None)
diff --git a/tests/test_event_tags_properties.py b/tests/test_event_tags_properties.py
index ec1219a..445b67d 100644
--- a/tests/test_event_tags_properties.py
+++ b/tests/test_event_tags_properties.py
@@ -6,28 +6,28 @@
import pytest
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.constants import MCPCAT_SOURCE
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
-from mcpcat.modules.internal import (
+from agentcat import AgentCatOptions, track
+from agentcat.modules.constants import AGENTCAT_SOURCE
+from agentcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat.modules.internal import (
attach_event_metadata,
resolve_event_properties,
resolve_event_tags,
)
-from mcpcat.modules.redaction import redact_event
-from mcpcat.types import EventType, MCPCatData, SessionInfo, UnredactedEvent
+from agentcat.modules.redaction import redact_event
+from agentcat.types import EventType, AgentCatData, SessionInfo, UnredactedEvent
from .test_utils.client import create_test_client
from .test_utils.todo_server import create_todo_server
-def _make_data(event_tags=None, event_properties=None) -> MCPCatData:
- return MCPCatData(
+def _make_data(event_tags=None, event_properties=None) -> AgentCatData:
+ return AgentCatData(
project_id="p",
session_id="ses_x",
last_activity=None,
session_info=SessionInfo(),
- options=MCPCatOptions(
+ options=AgentCatOptions(
event_tags=event_tags, event_properties=event_properties
),
)
@@ -180,7 +180,7 @@ class TestFastMCPIntegration:
@pytest.fixture(autouse=True)
def restore_queue(self):
- from mcpcat.modules.event_queue import event_queue as original
+ from agentcat.modules.event_queue import event_queue as original
yield
set_event_queue(original)
@@ -195,7 +195,7 @@ async def test_callbacks_flow_through_to_published_event(self):
track(
server,
"proj_test",
- MCPCatOptions(
+ AgentCatOptions(
event_tags=lambda req, ctx: {"env": "test", "bad!": "dropped"},
event_properties=lambda req, ctx: {"flag": True, "build": "abc"},
),
@@ -222,7 +222,7 @@ def boom(req, ctx):
raise RuntimeError("callback broken")
server = create_todo_server()
- track(server, "proj_test", MCPCatOptions(event_tags=boom, event_properties=boom))
+ track(server, "proj_test", AgentCatOptions(event_tags=boom, event_properties=boom))
async with create_test_client(server) as client:
await client.call_tool("add_todo", {"text": "hi"})
@@ -250,7 +250,7 @@ async def async_props(req, ctx):
track(
server,
"proj_test",
- MCPCatOptions(event_tags=async_tags, event_properties=async_props),
+ AgentCatOptions(event_tags=async_tags, event_properties=async_props),
)
async with create_test_client(server) as client:
@@ -276,7 +276,7 @@ def _event(self, **kwargs):
return event
def test_source_and_customer_tags_added_to_ddtags(self):
- from mcpcat.modules.exporters.datadog import DatadogExporter
+ from agentcat.modules.exporters.datadog import DatadogExporter
exporter = DatadogExporter(
{"type": "datadog", "api_key": "k", "site": "datadoghq.com", "service": "svc", "env": "prod"}
@@ -287,17 +287,17 @@ def test_source_and_customer_tags_added_to_ddtags(self):
)
log = exporter.event_to_log(event)
ddtags = log["ddtags"].split(",")
- assert f"source:{MCPCAT_SOURCE}" in ddtags
- assert "mcpcat.trace_id:abc_def" in ddtags # key sanitized + value commaโ_
- assert "mcpcat.region:us-east-1" in ddtags
- assert log["ddsource"] == MCPCAT_SOURCE
+ assert f"source:{AGENTCAT_SOURCE}" in ddtags
+ assert "agentcat.trace_id:abc_def" in ddtags # key sanitized + value commaโ_
+ assert "agentcat.region:us-east-1" in ddtags
+ assert log["ddsource"] == AGENTCAT_SOURCE
assert log["mcp"]["tags"] == {"Trace:Id": "abc,def", "Region": "us-east-1"}
assert log["mcp"]["properties"] == {"flag": True}
class TestOTLPExporter:
def test_source_and_customer_tags_and_properties(self):
- from mcpcat.modules.exporters.otlp import OTLPExporter
+ from agentcat.modules.exporters.otlp import OTLPExporter
exporter = OTLPExporter({"type": "otlp"})
event = UnredactedEvent(
@@ -308,14 +308,14 @@ def test_source_and_customer_tags_and_properties(self):
event.properties = {"flag": True, "count": 3}
attrs = exporter._get_span_attributes(event)
keys = {a["key"]: a["value"].get("stringValue") for a in attrs}
- assert keys.get("source") == MCPCAT_SOURCE
- assert keys.get("mcpcat.tag.env") == "prod"
- assert json.loads(keys["mcpcat.properties"]) == {"flag": True, "count": 3}
+ assert keys.get("source") == AGENTCAT_SOURCE
+ assert keys.get("agentcat.tag.env") == "prod"
+ assert json.loads(keys["agentcat.properties"]) == {"flag": True, "count": 3}
class TestSentryExporter:
def test_source_and_customer_tags_namespaced_and_properties_in_context(self):
- from mcpcat.modules.exporters.sentry import SentryExporter
+ from agentcat.modules.exporters.sentry import SentryExporter
exporter = SentryExporter(
{
@@ -335,10 +335,10 @@ def test_source_and_customer_tags_namespaced_and_properties_in_context(self):
event.properties = {"flag": True, "nested": {"x": 1}}
tags = exporter.build_tags(event)
- assert tags["source"] == MCPCAT_SOURCE
- assert tags["mcpcat.env"] == "test"
- assert tags["mcpcat.trace_id"] == "abc"
+ assert tags["source"] == AGENTCAT_SOURCE
+ assert tags["agentcat.env"] == "test"
+ assert tags["agentcat.trace_id"] == "abc"
contexts = exporter.build_contexts(event, {"trace_id": "t"})
assert contexts["trace"] == {"trace_id": "t"}
- assert contexts["mcpcat"] == {"flag": True, "nested": {"x": 1}}
+ assert contexts["agentcat"] == {"flag": True, "nested": {"x": 1}}
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index eb5c5df..208359d 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -7,9 +7,9 @@
import pytest
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
-from mcpcat.modules.exceptions import (
+from agentcat import AgentCatOptions, track
+from agentcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat.modules.exceptions import (
capture_exception,
extract_context_line,
filename_for_module,
@@ -432,7 +432,7 @@ class TestExceptionIntegration:
@pytest.fixture(autouse=True)
def setup_and_teardown(self):
"""Set up and tear down for each test."""
- from mcpcat.modules.event_queue import event_queue as original_queue
+ from agentcat.modules.event_queue import event_queue as original_queue
yield
set_event_queue(original_queue)
@@ -458,7 +458,7 @@ async def test_tool_raises_value_error(self):
captured_events = self._create_mock_event_capture()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -485,7 +485,7 @@ async def test_tool_raises_runtime_error(self):
captured_events = self._create_mock_event_capture()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -512,7 +512,7 @@ async def test_tool_raises_custom_error(self):
captured_events = self._create_mock_event_capture()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -539,7 +539,7 @@ async def test_tool_raises_captures_stack_frames(self):
captured_events = self._create_mock_event_capture()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -578,7 +578,7 @@ async def test_tool_raises_has_in_app_frames(self):
captured_events = self._create_mock_event_capture()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -612,7 +612,7 @@ async def test_tool_raises_captures_context_lines(self):
captured_events = self._create_mock_event_capture()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -647,7 +647,7 @@ async def test_mcp_protocol_error(self):
captured_events = self._create_mock_event_capture()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
diff --git a/tests/test_exporter_sdk_version.py b/tests/test_exporter_sdk_version.py
new file mode 100644
index 0000000..f85ee8d
--- /dev/null
+++ b/tests/test_exporter_sdk_version.py
@@ -0,0 +1,68 @@
+"""Exporters must derive the SDK version from the installed distribution.
+
+Pins that OTLP scope version and the Sentry auth header do not carry stale
+hardcoded version literals.
+"""
+
+import importlib.metadata
+from datetime import datetime, timezone
+from unittest.mock import MagicMock, patch
+
+from agentcat.modules.exporters.otlp import OTLPExporter
+from agentcat.modules.exporters.sentry import SentryExporter
+from agentcat.types import Event
+
+INSTALLED_VERSION = importlib.metadata.version("agentcat")
+
+
+def make_event(**kwargs) -> Event:
+ defaults = dict(
+ id="evt-test-id",
+ event_type="mcp:tools/call",
+ project_id="project-123",
+ session_id="session-123",
+ timestamp=datetime.now(timezone.utc),
+ )
+ defaults.update(kwargs)
+ return Event(**defaults)
+
+
+class TestOTLPScopeVersion:
+ def _export_and_get_scope(self, event: Event) -> dict:
+ exporter = OTLPExporter({"endpoint": "http://localhost:4318/v1/traces"})
+ exporter.session = MagicMock()
+ exporter.export(event)
+ assert exporter.session.post.called
+ payload = exporter.session.post.call_args.kwargs["json"]
+ return payload["resourceSpans"][0]["scopeSpans"][0]["scope"]
+
+ def test_scope_version_uses_event_version_when_present(self):
+ scope = self._export_and_get_scope(make_event(agentcat_version="9.9.9"))
+ assert scope["version"] == "9.9.9"
+
+ def test_scope_version_falls_back_to_installed_distribution(self):
+ scope = self._export_and_get_scope(make_event(agentcat_version=None))
+ assert scope["version"] == INSTALLED_VERSION
+
+ def test_scope_version_falls_back_to_unknown_when_unresolvable(self):
+ with patch(
+ "agentcat.modules.exporters.otlp.get_agentcat_version", return_value=None
+ ):
+ scope = self._export_and_get_scope(make_event(agentcat_version=None))
+ assert scope["version"] == "unknown"
+
+
+class TestSentryAuthHeaderVersion:
+ DSN = "https://abcdef1234567890@o123.ingest.sentry.io/456"
+
+ def test_auth_header_uses_installed_distribution_version(self):
+ exporter = SentryExporter({"dsn": self.DSN})
+ assert f"sentry_client=agentcat/{INSTALLED_VERSION}" in exporter.auth_header
+
+ def test_auth_header_falls_back_to_unknown_when_unresolvable(self):
+ with patch(
+ "agentcat.modules.exporters.sentry.get_agentcat_version",
+ return_value=None,
+ ):
+ exporter = SentryExporter({"dsn": self.DSN})
+ assert "sentry_client=agentcat/unknown" in exporter.auth_header
diff --git a/tests/test_logging.py b/tests/test_logging.py
index 2550942..ecc79d8 100644
--- a/tests/test_logging.py
+++ b/tests/test_logging.py
@@ -1,14 +1,12 @@
"""Tests for the logging module."""
-import os
import time
import uuid
-from pathlib import Path
from unittest.mock import patch
import pytest
-from mcpcat.modules.logging import write_to_log, set_debug_mode
+from agentcat.modules.logging import write_to_log, set_debug_mode
class TestLogging:
@@ -16,18 +14,47 @@ class TestLogging:
@pytest.fixture(autouse=True)
def cleanup_log_file(self):
- """Clean up the log file before and after each test."""
- log_path = os.path.expanduser("~/mcpcat.log")
-
- # Clean up before test
- if os.path.exists(log_path):
- os.remove(log_path)
+ """Reset debug mode after each test.
+ Every test patches agentcat.modules.logging.os.path.expanduser to a
+ tmp_path location, so no cleanup of the real ~/agentcat.log is needed.
+ """
yield
- # Clean up after test
- if os.path.exists(log_path):
- os.remove(log_path)
+ # Reset debug mode so later teardown (e.g. event-queue shutdown)
+ # doesn't write a stray line to the real ~/agentcat.log
+ set_debug_mode(False)
+
+ def test_write_to_log_uses_agentcat_log_path(self, tmp_path):
+ """Test that write_to_log resolves ~/agentcat.log (no ~/mcpcat.log fallback)."""
+ # Enable debug mode
+ set_debug_mode(True)
+
+ # Use a unique file name for this test
+ unique_id = str(uuid.uuid4())
+ log_file = tmp_path / f"test_agentcat_{unique_id}.log"
+
+ # Mock os.path.expanduser to use our temp file
+ with patch(
+ "agentcat.modules.logging.os.path.expanduser", return_value=str(log_file)
+ ) as mock_expanduser:
+ test_message = f"Default path test {unique_id}"
+ write_to_log(test_message)
+
+ # The code must resolve the new default path
+ mock_expanduser.assert_any_call("~/agentcat.log")
+
+ # The old path must NOT be resolved (no fallback)
+ assert all(
+ call.args != ("~/mcpcat.log",)
+ for call in mock_expanduser.call_args_list
+ ), "write_to_log wrongly resolved ~/mcpcat.log"
+
+ # The log line lands in the (patched) log file
+ assert log_file.exists(), "Log file was not created"
+ assert test_message in log_file.read_text(), (
+ "Log message not found in log file"
+ )
def test_write_to_log_creates_file(self, tmp_path):
"""Test that write_to_log creates the log file if it doesn't exist."""
@@ -36,11 +63,11 @@ def test_write_to_log_creates_file(self, tmp_path):
# Use a unique file name for this test
unique_id = str(uuid.uuid4())
- log_file = tmp_path / f"test_mcpcat_{unique_id}.log"
+ log_file = tmp_path / f"test_agentcat_{unique_id}.log"
# Mock os.path.expanduser to use our temp file
with patch(
- "mcpcat.modules.logging.os.path.expanduser", return_value=str(log_file)
+ "agentcat.modules.logging.os.path.expanduser", return_value=str(log_file)
):
# Write a test message
test_message = f"Test log message {unique_id}"
@@ -65,11 +92,11 @@ def test_write_to_log_checks_debug_mode(self, tmp_path):
# Use a unique file name for this test
unique_id = str(uuid.uuid4())
- log_file = tmp_path / f"test_mcpcat_{unique_id}.log"
+ log_file = tmp_path / f"test_agentcat_{unique_id}.log"
# Mock os.path.expanduser to use our temp file
with patch(
- "mcpcat.modules.logging.os.path.expanduser", return_value=str(log_file)
+ "agentcat.modules.logging.os.path.expanduser", return_value=str(log_file)
):
# Write a test message
test_message = f"Test log message {unique_id}"
@@ -92,11 +119,11 @@ def test_write_to_log_checks_debug_mode(self, tmp_path):
# Use a unique file name for this test
unique_id = str(uuid.uuid4())
- log_file = tmp_path / f"test_mcpcat_{unique_id}.log"
+ log_file = tmp_path / f"test_agentcat_{unique_id}.log"
# Mock os.path.expanduser to use our temp file
with patch(
- "mcpcat.modules.logging.os.path.expanduser", return_value=str(log_file)
+ "agentcat.modules.logging.os.path.expanduser", return_value=str(log_file)
):
# Write a test message
test_message = f"Test log message {unique_id}"
@@ -112,11 +139,11 @@ def test_write_to_log_appends_messages(self, tmp_path):
# Use a unique file name for this test
unique_id = str(uuid.uuid4())
- log_file = tmp_path / f"test_mcpcat_{unique_id}.log"
+ log_file = tmp_path / f"test_agentcat_{unique_id}.log"
# Mock os.path.expanduser to use our temp file
with patch(
- "mcpcat.modules.logging.os.path.expanduser", return_value=str(log_file)
+ "agentcat.modules.logging.os.path.expanduser", return_value=str(log_file)
):
# Write multiple messages with unique identifiers
messages = [
@@ -163,11 +190,11 @@ def test_write_to_log_handles_directory_creation(self, tmp_path):
# Use a unique file name for this test
unique_id = str(uuid.uuid4())
- log_file = tmp_path / f"test_mcpcat_{unique_id}.log"
+ log_file = tmp_path / f"test_agentcat_{unique_id}.log"
# Mock os.path.expanduser to use our temp file
with patch(
- "mcpcat.modules.logging.os.path.expanduser", return_value=str(log_file)
+ "agentcat.modules.logging.os.path.expanduser", return_value=str(log_file)
):
# Write a test message
test_message = f"Test with directory creation {unique_id}"
@@ -184,11 +211,11 @@ def test_write_to_log_silently_handles_errors(self, tmp_path, monkeypatch):
# Use a unique file name for this test
unique_id = str(uuid.uuid4())
- log_file = tmp_path / f"test_mcpcat_{unique_id}.log"
+ log_file = tmp_path / f"test_agentcat_{unique_id}.log"
# Mock os.path.expanduser to use our temp file
with patch(
- "mcpcat.modules.logging.os.path.expanduser", return_value=str(log_file)
+ "agentcat.modules.logging.os.path.expanduser", return_value=str(log_file)
):
# Make the parent directory read-only to cause write failure
log_file.parent.chmod(0o444)
@@ -210,11 +237,11 @@ def test_log_format(self, tmp_path):
# Use a unique file name for this test
unique_id = str(uuid.uuid4())
- log_file = tmp_path / f"test_mcpcat_{unique_id}.log"
+ log_file = tmp_path / f"test_agentcat_{unique_id}.log"
# Mock os.path.expanduser to use our temp file
with patch(
- "mcpcat.modules.logging.os.path.expanduser", return_value=str(log_file)
+ "agentcat.modules.logging.os.path.expanduser", return_value=str(log_file)
):
# Write a test message
test_message = f"Test format validation {unique_id}"
diff --git a/tests/test_mcp_version_compatibility.py b/tests/test_mcp_version_compatibility.py
index f578b6b..fc2eb11 100644
--- a/tests/test_mcp_version_compatibility.py
+++ b/tests/test_mcp_version_compatibility.py
@@ -2,7 +2,7 @@
import pytest
-from mcpcat.modules.compatibility import is_compatible_server
+from agentcat.modules.compatibility import is_compatible_server
from mcp import ClientSession
from .test_utils.client import create_test_client
diff --git a/tests/test_multiple_servers.py b/tests/test_multiple_servers.py
index 90a49a9..1c009a1 100644
--- a/tests/test_multiple_servers.py
+++ b/tests/test_multiple_servers.py
@@ -5,8 +5,8 @@
from unittest.mock import MagicMock
import time
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat import AgentCatOptions, track
+from agentcat.modules.event_queue import EventQueue, set_event_queue
from .test_utils.client import create_test_client
from .test_utils.todo_server import create_todo_server
@@ -35,21 +35,21 @@ def capture_event(publish_event_request):
server3 = create_todo_server()
# Track each with different options
- options1 = MCPCatOptions(
+ options1 = AgentCatOptions(
enable_tracing=True,
enable_tool_call_context=True,
enable_report_missing=True,
)
track(server1, "project1", options1)
- options2 = MCPCatOptions(
+ options2 = AgentCatOptions(
enable_tracing=True,
enable_tool_call_context=False, # Different from server1
enable_report_missing=False, # Different from server1
)
track(server2, "project2", options2)
- options3 = MCPCatOptions(
+ options3 = AgentCatOptions(
enable_tracing=False, # No tracing at all
enable_tool_call_context=True,
enable_report_missing=True,
@@ -157,7 +157,7 @@ def capture_event(publish_event_request):
server = create_todo_server()
# First track with context disabled
- options1 = MCPCatOptions(
+ options1 = AgentCatOptions(
enable_tool_call_context=False, enable_report_missing=False
)
track(server, "test_project", options1)
@@ -174,7 +174,7 @@ def capture_event(publish_event_request):
assert "get_more_tools" not in tool_names
# Re-track with context enabled
- options2 = MCPCatOptions(
+ options2 = AgentCatOptions(
enable_tool_call_context=True, enable_report_missing=True
)
track(server, "test_project", options2)
@@ -209,7 +209,7 @@ def capture_event(publish_event_request):
# Track each with different project IDs
for i, server in enumerate(servers):
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tracing=True,
enable_tool_call_context=(i % 2 == 0), # Alternate context
enable_report_missing=(i % 3 == 0), # Every third has report_missing
@@ -265,8 +265,8 @@ async def test_server_isolation_with_late_tools(self):
server2 = create_todo_server()
# Track both servers with different options
- options1 = MCPCatOptions(enable_report_missing=True)
- options2 = MCPCatOptions(enable_report_missing=False)
+ options1 = AgentCatOptions(enable_report_missing=True)
+ options2 = AgentCatOptions(enable_report_missing=False)
track(server1, "project1", options1)
track(server2, "project2", options2)
@@ -317,7 +317,7 @@ def capture_event(publish_event_request):
# Custom identify for server1
def identify1(request, server):
- from mcpcat.types import UserIdentity
+ from agentcat.types import UserIdentity
if hasattr(request, "params") and hasattr(request.params, "arguments"):
args = request.params.arguments
@@ -331,7 +331,7 @@ def identify1(request, server):
# Different custom identify for server2
def identify2(request, server):
- from mcpcat.types import UserIdentity
+ from agentcat.types import UserIdentity
if hasattr(request, "params") and hasattr(request.params, "arguments"):
args = request.params.arguments
@@ -343,8 +343,8 @@ def identify2(request, server):
)
return None
- options1 = MCPCatOptions(enable_tracing=True, identify=identify1)
- options2 = MCPCatOptions(enable_tracing=True, identify=identify2)
+ options1 = AgentCatOptions(enable_tracing=True, identify=identify1)
+ options2 = AgentCatOptions(enable_tracing=True, identify=identify2)
track(server1, "project1", options1)
track(server2, "project2", options2)
@@ -383,6 +383,6 @@ def cleanup_event_queue():
"""Reset event queue after each test."""
yield
# Reset to default event queue
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
set_event_queue(EventQueue())
diff --git a/tests/test_redaction.py b/tests/test_redaction.py
index 59a8def..533c649 100644
--- a/tests/test_redaction.py
+++ b/tests/test_redaction.py
@@ -2,7 +2,7 @@
import pytest
from typing import Any, Dict
-from mcpcat.modules.redaction import (
+from agentcat.modules.redaction import (
redact_strings_in_object,
redact_event,
PROTECTED_FIELDS,
@@ -310,13 +310,13 @@ def redact_fn(s: str) -> str:
assert result["metadata"]["ip"] == "[REDACTED]"
def test_identify_event_special_fields(self):
- """Test mcpcat:identify event with special protected fields."""
+ """Test agentcat:identify event with special protected fields."""
def redact_fn(s: str) -> str:
return "XXX"
identify_event = {
- "event_type": "mcpcat:identify",
+ "event_type": "agentcat:identify",
"identify_actor_given_id": "user123", # Protected
"identify_actor_name": "John Doe", # Protected
"identify_data": { # Protected
diff --git a/tests/test_report_missing.py b/tests/test_report_missing.py
index cbe5e89..73a44b9 100644
--- a/tests/test_report_missing.py
+++ b/tests/test_report_missing.py
@@ -4,7 +4,7 @@
from unittest.mock import MagicMock
import time
-from mcpcat import MCPCatOptions, track
+from agentcat import AgentCatOptions, track
from .test_utils.client import create_test_client
from .test_utils.todo_server import create_todo_server
@@ -20,7 +20,7 @@ async def test_report_missing_tool_injection(self):
server = create_todo_server()
# Track the server with report_missing enabled
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
# Use client to list all tools and verify report_missing is injected
@@ -45,7 +45,7 @@ async def test_report_missing_disabled_by_default(self):
server = create_todo_server()
# Track with report_missing disabled
- options = MCPCatOptions(enable_report_missing=False)
+ options = AgentCatOptions(enable_report_missing=False)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -61,7 +61,7 @@ async def test_report_missing_disabled_by_default(self):
async def test_report_missing_tool_call_success(self):
"""Call report_missing tool and verify it executes successfully."""
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -80,7 +80,7 @@ async def test_report_missing_tool_call_success(self):
async def test_report_missing_with_valid_params(self):
"""Test with both required parameters (missing_tool, description)."""
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -106,7 +106,7 @@ async def test_report_missing_with_valid_params(self):
async def test_report_missing_with_missing_params(self):
"""Test error handling when required parameters are missing."""
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -125,7 +125,7 @@ async def test_report_missing_with_missing_params(self):
async def test_report_missing_with_extra_params(self):
"""Test that extra parameters are ignored/handled properly."""
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -144,7 +144,7 @@ async def test_report_missing_with_extra_params(self):
async def test_report_missing_with_other_tools(self):
"""Verify report_missing doesn't interfere with existing server tools."""
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -166,7 +166,7 @@ async def test_report_missing_with_other_tools(self):
async def test_multiple_report_missing_calls(self):
"""Test calling report_missing multiple times in succession."""
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -192,7 +192,7 @@ async def test_multiple_report_missing_calls(self):
async def test_report_missing_with_context_enabled(self):
"""Test interaction when both report_missing and tool_context are enabled."""
server = create_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_report_missing=True, enable_tool_call_context=True
)
track(server, "test_project", options)
@@ -233,7 +233,7 @@ async def test_report_missing_on_server_without_tools(self):
async def test_report_missing_with_null_values(self):
"""Test with null/None values for parameters."""
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True)
+ options = AgentCatOptions(enable_report_missing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -246,7 +246,7 @@ async def test_report_missing_with_null_values(self):
@pytest.mark.asyncio
async def test_report_missing_publishes_event(self):
"""Verify that calling report_missing tool publishes an event to the queue."""
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
# Create a mock API client
mock_api_client = MagicMock()
@@ -260,7 +260,7 @@ async def test_report_missing_publishes_event(self):
try:
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True, enable_tracing=True)
+ options = AgentCatOptions(enable_report_missing=True, enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -307,14 +307,14 @@ async def test_report_missing_publishes_event(self):
finally:
# Clean up: restore original event queue
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
set_event_queue(EventQueue())
@pytest.mark.asyncio
async def test_multiple_tool_calls_publish_multiple_events(self):
"""Verify that multiple tool calls result in multiple events being published."""
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
# Create a mock API client
mock_api_client = MagicMock()
@@ -328,7 +328,7 @@ async def test_multiple_tool_calls_publish_multiple_events(self):
try:
server = create_todo_server()
- options = MCPCatOptions(enable_report_missing=True, enable_tracing=True)
+ options = AgentCatOptions(enable_report_missing=True, enable_tracing=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -396,6 +396,6 @@ async def test_multiple_tool_calls_publish_multiple_events(self):
finally:
# Clean up: restore original event queue
- from mcpcat.modules.event_queue import EventQueue, set_event_queue
+ from agentcat.modules.event_queue import EventQueue, set_event_queue
set_event_queue(EventQueue())
diff --git a/tests/test_request_extra.py b/tests/test_request_extra.py
index b33a537..804b50c 100644
--- a/tests/test_request_extra.py
+++ b/tests/test_request_extra.py
@@ -12,9 +12,9 @@
import pytest
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
-from mcpcat.modules.request_extra import (
+from agentcat import AgentCatOptions, track
+from agentcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat.modules.request_extra import (
extract_request_extra,
params_with_extra,
)
@@ -250,7 +250,7 @@ class TestExtraOnPublishedEvents:
@pytest.fixture(autouse=True)
def restore_queue(self):
- from mcpcat.modules.event_queue import event_queue as original_queue
+ from agentcat.modules.event_queue import event_queue as original_queue
yield
set_event_queue(original_queue)
@@ -267,7 +267,7 @@ def capture_event(publish_event_request):
set_event_queue(EventQueue(api_client=mock_api_client))
server = create_todo_server()
- track(server, "test_project", MCPCatOptions(enable_tracing=True))
+ track(server, "test_project", AgentCatOptions(enable_tracing=True))
async with create_test_client(server) as client:
await client.call_tool(
@@ -296,7 +296,7 @@ def capture_event(publish_event_request):
set_event_queue(EventQueue(api_client=mock_api_client))
server = create_todo_server()
- track(server, "test_project", MCPCatOptions(enable_tracing=True))
+ track(server, "test_project", AgentCatOptions(enable_tracing=True))
# Fake an HTTP request_context for the duration of the call. Patch BOTH
# the module symbol and the import sites so monkey-patched call sites pick it up.
@@ -310,8 +310,8 @@ def capture_event(publish_event_request):
meta={"progressToken": "tok-7"},
)
- from mcpcat.modules.overrides import mcp_server as mcp_server_mod
- from mcpcat.modules.overrides.official import monkey_patch as official_mp
+ from agentcat.modules.overrides import mcp_server as mcp_server_mod
+ from agentcat.modules.overrides.official import monkey_patch as official_mp
monkeypatch.setattr(
mcp_server_mod, "safe_request_context", lambda _server: fake_ctx
@@ -355,13 +355,13 @@ def capture_event(publish_event_request):
set_event_queue(EventQueue(api_client=mock_api_client))
server = create_todo_server()
- track(server, "test_project", MCPCatOptions(enable_tracing=True))
+ track(server, "test_project", AgentCatOptions(enable_tracing=True))
fake_ctx = _http_request_context(
headers={"x-list-header": "list-value"},
request_id="req-list",
)
- from mcpcat.modules.overrides import mcp_server as mcp_server_mod
+ from agentcat.modules.overrides import mcp_server as mcp_server_mod
monkeypatch.setattr(
mcp_server_mod, "safe_request_context", lambda _server: fake_ctx
diff --git a/tests/test_sanitization.py b/tests/test_sanitization.py
index 4e33838..b3cb31d 100644
--- a/tests/test_sanitization.py
+++ b/tests/test_sanitization.py
@@ -4,7 +4,7 @@
import pytest
-from mcpcat.modules.sanitization import (
+from agentcat.modules.sanitization import (
sanitize_event,
_scan_for_base64,
_AUDIO_REDACTED,
@@ -13,7 +13,7 @@
_IMAGE_REDACTED,
_unsupported_type_redacted,
)
-from mcpcat.types import UnredactedEvent
+from agentcat.types import UnredactedEvent
def _make_event(**overrides) -> UnredactedEvent:
diff --git a/tests/test_session.py b/tests/test_session.py
index 6dacebd..e10550e 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -7,16 +7,16 @@
import pytest
from freezegun import freeze_time
-from mcpcat.modules.constants import INACTIVITY_TIMEOUT_IN_MINUTES, SESSION_ID_PREFIX
-from mcpcat.modules.internal import get_server_tracking_data, set_server_tracking_data
-from mcpcat.modules.session import (
- get_mcpcat_version,
+from agentcat.modules.constants import INACTIVITY_TIMEOUT_IN_MINUTES, SESSION_ID_PREFIX
+from agentcat.modules.internal import get_server_tracking_data, set_server_tracking_data
+from agentcat.modules.session import (
+ get_agentcat_version,
get_server_session_id,
get_session_info,
new_session_id,
set_last_activity,
)
-from mcpcat.types import MCPCatData, MCPCatOptions, SessionInfo
+from agentcat.types import AgentCatData, AgentCatOptions, SessionInfo
from .test_utils.todo_server import create_todo_server
@@ -44,21 +44,21 @@ def test_session_id_format(self):
assert len(parts[1]) > 0 # KSUID part should not be empty
-class TestGetMcpcatVersion:
- """Test the get_mcpcat_version function."""
+class TestGetAgentcatVersion:
+ """Test the get_agentcat_version function."""
@patch("importlib.metadata.version")
def test_returns_correct_version(self, mock_version):
- """Test that get_mcpcat_version returns the correct version."""
+ """Test that get_agentcat_version returns the correct version."""
mock_version.return_value = "1.2.3"
- assert get_mcpcat_version() == "1.2.3"
- mock_version.assert_called_once_with("mcpcat")
+ assert get_agentcat_version() == "1.2.3"
+ mock_version.assert_called_once_with("agentcat")
@patch("importlib.metadata.version")
def test_returns_none_on_exception(self, mock_version):
- """Test that get_mcpcat_version returns None when an exception occurs."""
+ """Test that get_agentcat_version returns None when an exception occurs."""
mock_version.side_effect = Exception("Package not found")
- assert get_mcpcat_version() is None
+ assert get_agentcat_version() is None
class TestGetSessionInfo:
@@ -68,8 +68,8 @@ def setup_method(self):
"""Set up test fixtures."""
self.server = create_todo_server()
- def test_without_mcpcat_data(self):
- """Test get_session_info without MCPCat data."""
+ def test_without_agentcat_data(self):
+ """Test get_session_info without AgentCat data."""
session_info = get_session_info(self.server, None)
assert session_info.ip_address is None
@@ -77,7 +77,7 @@ def test_without_mcpcat_data(self):
session_info.sdk_language
== f"Python {sys.version_info.major}.{sys.version_info.minor}"
)
- assert session_info.mcpcat_version == get_mcpcat_version()
+ assert session_info.agentcat_version == get_agentcat_version()
assert session_info.server_name == "todo-server"
assert (
session_info.server_version is None
@@ -88,14 +88,14 @@ def test_without_mcpcat_data(self):
assert session_info.identify_actor_name is None
assert session_info.identify_data is None
- def test_with_mcpcat_data_no_actor(self):
- """Test get_session_info with MCPCat data but no identified actor."""
- data = MCPCatData(
+ def test_with_agentcat_data_no_actor(self):
+ """Test get_session_info with AgentCat data but no identified actor."""
+ data = AgentCatData(
project_id="test_project",
session_id="test_session",
session_info=SessionInfo(client_name="TestClient", client_version="2.0.0"),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
session_info = get_session_info(self.server, data)
@@ -111,12 +111,12 @@ def test_with_mcpcat_data_no_actor(self):
def test_session_info_never_carries_identity(self):
"""get_session_info returns None for identity fields."""
- data = MCPCatData(
+ data = AgentCatData(
project_id="test_project",
session_id="test_session",
session_info=SessionInfo(client_name="TestClient", client_version="2.0.0"),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
session_info = get_session_info(self.server, data)
@@ -140,14 +140,14 @@ def test_server_without_name_or_version(self):
def test_get_session_info_with_tracked_server(self):
"""Test get_session_info when server has tracked data."""
# Set up initial data
- data = MCPCatData(
+ data = AgentCatData(
project_id="test_project",
session_id="test_session",
session_info=SessionInfo(
client_name="TrackedClient", client_version="3.0.0"
),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
# Store data using set_server_tracking_data
@@ -177,12 +177,12 @@ def test_updates_last_activity(self):
"""Test that set_last_activity updates the last activity timestamp."""
initial_time = datetime.now(timezone.utc)
- data = MCPCatData(
+ data = AgentCatData(
project_id="test_project",
session_id="test_session",
session_info=SessionInfo(),
last_activity=initial_time,
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
# Set up the server with tracking data
@@ -200,7 +200,7 @@ def test_raises_exception_when_no_data(self):
with pytest.raises(Exception) as exc_info:
set_last_activity(self.server)
- assert str(exc_info.value) == "MCPCat data not initialized for this server"
+ assert str(exc_info.value) == "AgentCat data not initialized for this server"
class TestGetServerSessionId:
@@ -212,12 +212,12 @@ def setup_method(self):
self.initial_time = datetime.now(timezone.utc)
self.initial_session_id = "ses_initial123"
- self.data = MCPCatData(
+ self.data = AgentCatData(
project_id="test_project",
session_id=self.initial_session_id,
session_info=SessionInfo(),
last_activity=self.initial_time,
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
def test_returns_existing_session_when_not_timed_out(self):
@@ -269,7 +269,7 @@ def test_raises_exception_when_no_data(self):
with pytest.raises(Exception) as exc_info:
get_server_session_id(self.server)
- assert str(exc_info.value) == "MCPCat data not initialized for this server"
+ assert str(exc_info.value) == "AgentCat data not initialized for this server"
def test_multiple_calls_with_activity(self):
"""Test multiple calls to get_server_session_id with activity between them."""
@@ -325,14 +325,14 @@ def test_full_session_lifecycle(self):
# Create initial session data
initial_session_id = new_session_id()
- data = MCPCatData(
+ data = AgentCatData(
project_id="integration_project",
session_id=initial_session_id,
session_info=SessionInfo(
client_name="IntegrationClient", client_version="1.0.0"
),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
set_server_tracking_data(server, data)
@@ -354,14 +354,14 @@ def test_full_session_lifecycle(self):
new_sid = get_server_session_id(server)
assert new_sid != initial_session_id
- new_data = MCPCatData(
+ new_data = AgentCatData(
project_id="integration_project",
session_id=new_sid,
session_info=SessionInfo(
client_name="IntegrationClient", client_version="1.0.0"
),
last_activity=datetime(2024, 1, 1, 12, 31, 0),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
session_info = get_session_info(server, new_data)
assert session_info.identify_actor_given_id is None
@@ -372,12 +372,12 @@ def test_session_persistence_across_function_calls(self):
server = create_todo_server()
# Initialize tracking data
- data = MCPCatData(
+ data = AgentCatData(
project_id="persistence_test",
session_id=new_session_id(),
session_info=SessionInfo(),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
set_server_tracking_data(server, data)
@@ -400,14 +400,14 @@ def test_session_info_updates_tracked_data(self):
"""get_session_info updates the tracked data's session_info."""
server = create_todo_server()
- data = MCPCatData(
+ data = AgentCatData(
project_id="update_test",
session_id="update_session",
session_info=SessionInfo(
client_name="UpdateClient", client_version="1.0.0"
),
last_activity=datetime.now(timezone.utc),
- options=MCPCatOptions(),
+ options=AgentCatOptions(),
)
set_server_tracking_data(server, data)
diff --git a/tests/test_stateless.py b/tests/test_stateless.py
index c723e79..222e8cf 100644
--- a/tests/test_stateless.py
+++ b/tests/test_stateless.py
@@ -3,15 +3,15 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
-import mcpcat
-from mcpcat.modules.internal import (
+import agentcat
+from agentcat.modules.internal import (
get_server_tracking_data,
set_server_tracking_data,
reset_all_tracking_data,
)
-from mcpcat.modules.session import get_server_session_id, get_client_info_from_request_context
-from mcpcat.modules.identify import identify_session
-from mcpcat.types import MCPCatData, MCPCatOptions, SessionInfo, UserIdentity
+from agentcat.modules.session import get_server_session_id, get_client_info_from_request_context
+from agentcat.modules.identify import identify_session
+from agentcat.types import AgentCatData, AgentCatOptions, SessionInfo, UserIdentity
from .test_utils.todo_server import create_todo_server
@@ -34,11 +34,11 @@ def teardown_method(self):
reset_all_tracking_data()
def _setup_data(self, stateless=False, identify=None):
- """Create and store MCPCatData on the server."""
- options = MCPCatOptions()
+ """Create and store AgentCatData on the server."""
+ options = AgentCatOptions()
if identify:
options.identify = identify
- data = MCPCatData(
+ data = AgentCatData(
project_id="test_project",
session_id="ses_existing123",
session_info=SessionInfo(),
@@ -50,7 +50,7 @@ def _setup_data(self, stateless=False, identify=None):
return data
def test_stateless_option_sets_flag(self):
- """MCPCatOptions(stateless=True) should set is_stateless on data."""
+ """AgentCatOptions(stateless=True) should set is_stateless on data."""
data = self._setup_data(stateless=True)
assert data.is_stateless is True
@@ -60,7 +60,7 @@ def test_stateless_session_id_is_none(self):
session_id = get_server_session_id(self.server)
assert session_id is None
- @patch("mcpcat.modules.identify.event_queue")
+ @patch("agentcat.modules.identify.event_queue")
def test_stateless_identify_runs_every_time(self, mock_event_queue):
"""In stateless mode, identify should run on every call (no early-return guard)."""
mock_fn = MagicMock(return_value=UserIdentity(
@@ -73,7 +73,7 @@ def test_stateless_identify_runs_every_time(self, mock_event_queue):
assert mock_fn.call_count == 2
- @patch("mcpcat.modules.identify.event_queue")
+ @patch("agentcat.modules.identify.event_queue")
def test_stateless_identify_returns_identity(self, mock_event_queue):
"""In stateless mode, identify_session() should return the UserIdentity."""
self._setup_data(stateless=True, identify=_make_identify_fn())
@@ -84,7 +84,7 @@ def test_stateless_identify_returns_identity(self, mock_event_queue):
assert result.user_id == "user_123"
assert result.user_name == "Test User"
- @patch("mcpcat.modules.identify.event_queue")
+ @patch("agentcat.modules.identify.event_queue")
def test_stateful_identify_runs_every_time(self, mock_event_queue):
"""Stateful mode runs identify on every request."""
mock_fn = MagicMock(return_value=UserIdentity(
@@ -105,8 +105,8 @@ def test_stateful_identify_runs_every_time(self, mock_event_queue):
def test_track_stateless_true_sets_flag(self):
"""track() with stateless=True should set is_stateless on data."""
server = create_todo_server()
- options = MCPCatOptions(stateless=True)
- mcpcat.track(server, "test_project", options)
+ options = AgentCatOptions(stateless=True)
+ agentcat.track(server, "test_project", options)
data = get_server_tracking_data(server)
assert data.is_stateless is True
@@ -116,21 +116,21 @@ def test_track_stateless_false_overrides_detection(self):
# Mock the server to look stateless
server.settings = MagicMock()
server.settings.stateless_http = True
- options = MCPCatOptions(stateless=False)
- mcpcat.track(server, "test_project", options)
+ options = AgentCatOptions(stateless=False)
+ agentcat.track(server, "test_project", options)
data = get_server_tracking_data(server)
assert data.is_stateless is False
def test_track_stateless_none_auto_detects(self):
"""track() with stateless=None (default) should auto-detect from server."""
server = create_todo_server()
- options = MCPCatOptions() # stateless=None by default
- mcpcat.track(server, "test_project", options)
+ options = AgentCatOptions() # stateless=None by default
+ agentcat.track(server, "test_project", options)
data = get_server_tracking_data(server)
# create_todo_server() is not stateless, so should be False
assert data.is_stateless is False
- @patch("mcpcat.modules.identify.event_queue")
+ @patch("agentcat.modules.identify.event_queue")
def test_stateless_identify_bad_return(self, mock_event_queue):
"""In stateless mode, identify returning non-UserIdentity should return None."""
bad_fn = MagicMock(return_value="not a UserIdentity")
@@ -141,7 +141,7 @@ def test_stateless_identify_bad_return(self, mock_event_queue):
assert result is None
assert bad_fn.call_count == 1
- @patch("mcpcat.modules.identify.event_queue")
+ @patch("agentcat.modules.identify.event_queue")
def test_stateless_identify_exception(self, mock_event_queue):
"""In stateless mode, identify raising should return None, not propagate."""
raising_fn = MagicMock(side_effect=RuntimeError("identify exploded"))
diff --git a/tests/test_tag_validation.py b/tests/test_tag_validation.py
index b5c5bd4..2c9c330 100644
--- a/tests/test_tag_validation.py
+++ b/tests/test_tag_validation.py
@@ -4,7 +4,7 @@
import pytest
-from mcpcat.modules.validation import (
+from agentcat.modules.validation import (
MAX_TAG_ENTRIES,
MAX_TAG_KEY_LENGTH,
MAX_TAG_VALUE_LENGTH,
@@ -14,7 +14,7 @@
@pytest.fixture(autouse=True)
def mock_log():
- with patch("mcpcat.modules.validation.write_to_log") as mock:
+ with patch("agentcat.modules.validation.write_to_log") as mock:
yield mock
diff --git a/tests/test_tool_context.py b/tests/test_tool_context.py
index e17016e..92b69fe 100644
--- a/tests/test_tool_context.py
+++ b/tests/test_tool_context.py
@@ -8,9 +8,9 @@
from mcp.server.fastmcp import FastMCP
from mcp.types import Tool
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.constants import DEFAULT_CONTEXT_DESCRIPTION
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat import AgentCatOptions, track
+from agentcat.modules.constants import DEFAULT_CONTEXT_DESCRIPTION
+from agentcat.modules.event_queue import EventQueue, set_event_queue
from .test_utils.client import create_test_client
from .test_utils.todo_server import create_todo_server
@@ -23,7 +23,7 @@ class TestToolContext:
async def test_context_parameter_injection_enabled(self):
"""Test that context parameter is added when enable_tool_call_context=True."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -52,7 +52,7 @@ async def test_context_parameter_injection_enabled(self):
async def test_context_parameter_not_injected_when_disabled(self):
"""Test that context parameter is NOT added when enable_tool_call_context=False."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=False)
+ options = AgentCatOptions(enable_tool_call_context=False)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -72,7 +72,7 @@ async def test_context_parameter_not_injected_when_disabled(self):
async def test_schema_with_existing_properties(self):
"""Test with tools that have existing inputSchema and properties."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -100,7 +100,7 @@ def simple_tool():
return "success"
server = mcp
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -128,7 +128,7 @@ def empty_tool():
return "success"
server = mcp
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -143,7 +143,7 @@ def empty_tool():
async def test_schema_with_existing_required_fields(self):
"""Test with tools that already have required fields."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -168,7 +168,7 @@ def optional_params_tool(param1: str = "default"):
return f"Result: {param1}"
server = mcp
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -188,7 +188,7 @@ async def test_server_with_no_tools(self):
mcp = FastMCP("empty-server")
server = mcp
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -204,7 +204,7 @@ async def test_server_with_no_tools(self):
async def test_get_more_tools_exclusion_with_context(self):
"""Test that get_more_tools doesn't get context when both features are enabled."""
server = create_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_report_missing=True, enable_tool_call_context=True
)
track(server, "test_project", options)
@@ -238,7 +238,7 @@ def complex_tool(
return "success"
server = mcp
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -268,7 +268,7 @@ def validated_tool(age: Annotated[int, Field(ge=0, le=150)], email: str):
return "success"
server = mcp
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -306,7 +306,7 @@ def tool_with_context(context: str, data: str):
return f"Original context: {context}, data: {data}"
server = mcp
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -350,7 +350,7 @@ def composed_tool(data: Union[str, int], required_field: str):
return f"Data: {data}, Required: {required_field}"
server = mcp
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -378,7 +378,7 @@ def composed_tool(data: Union[str, int], required_field: str):
async def test_tool_call_with_valid_context(self):
"""Test calling a tool with valid context parameter."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -399,7 +399,7 @@ async def test_tool_call_with_valid_context(self):
async def test_tool_call_without_context_fails(self):
"""Test that tool calls without context fail validation."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -418,7 +418,7 @@ async def test_tool_call_without_context_fails(self):
async def test_tool_call_with_empty_context(self):
"""Test calling a tool with empty string context."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -438,7 +438,7 @@ async def test_tool_call_with_empty_context(self):
async def test_tool_call_with_long_context(self):
"""Test calling a tool with very long context string."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -456,7 +456,7 @@ async def test_tool_call_with_long_context(self):
async def test_tool_call_with_unicode_context(self):
"""Test calling a tool with special characters/unicode in context."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -474,7 +474,7 @@ async def test_tool_call_with_unicode_context(self):
async def test_tool_call_with_null_context(self):
"""Test calling a tool with null/None context value."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -491,7 +491,7 @@ async def test_tool_call_with_null_context(self):
async def test_original_functionality_preserved(self):
"""Verify that original tool functionality remains intact with context."""
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -525,7 +525,7 @@ async def test_context_not_passed_to_original_handler(self):
# This test verifies the current implementation behavior
# Context is added to schema but stripped from arguments before passing to handler
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -546,11 +546,11 @@ async def test_multiple_track_calls(self):
server = create_todo_server()
# First track with context disabled
- options1 = MCPCatOptions(enable_tool_call_context=False)
+ options1 = AgentCatOptions(enable_tool_call_context=False)
track(server, "project1", options1)
# Second track with context enabled
- options2 = MCPCatOptions(enable_tool_call_context=True)
+ options2 = AgentCatOptions(enable_tool_call_context=True)
track(server, "project2", options2)
async with create_test_client(server) as client:
@@ -567,7 +567,7 @@ async def test_changing_options_between_calls(self):
server = create_todo_server()
# Track with context enabled
- options_enabled = MCPCatOptions(enable_tool_call_context=True)
+ options_enabled = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options_enabled)
async with create_test_client(server) as client:
@@ -578,7 +578,7 @@ async def test_changing_options_between_calls(self):
# The current implementation updates the tracking data with new options
# Track again with context disabled
- options_disabled = MCPCatOptions(enable_tool_call_context=False)
+ options_disabled = AgentCatOptions(enable_tool_call_context=False)
track(server, "test_project", options_disabled)
async with create_test_client(server) as client:
@@ -597,7 +597,7 @@ async def test_error_handling_graceful_fallback(self):
# This test would require mocking internal functions to force errors
# For now, we'll test that the system is resilient
server = create_todo_server()
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -617,7 +617,7 @@ async def test_custom_context_description(self):
"""Test that custom context description is correctly applied."""
server = create_todo_server()
custom_description = "Explain your reasoning for using this tool"
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=custom_description
)
@@ -639,7 +639,7 @@ async def test_custom_context_description(self):
async def test_custom_context_description_empty_string(self):
"""Test edge case with empty string custom description."""
server = create_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description="" # Empty string
)
@@ -661,7 +661,7 @@ async def test_custom_context_description_special_characters(self):
"""Test custom description with special characters and Unicode."""
server = create_todo_server()
special_description = "Why are you using this? ๐ค Include: quotes\"', newlines\n, tabs\t, etc."
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=special_description
)
@@ -681,7 +681,7 @@ async def test_custom_context_description_very_long(self):
server = create_todo_server()
# Create a very long description
long_description = "This is a very detailed description. " * 50 # ~1800 characters
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=long_description
)
@@ -701,7 +701,7 @@ async def test_default_context_description(self):
"""Verify the default description is used when not specified."""
server = create_todo_server()
# Don't specify custom_context_description, should use default
- options = MCPCatOptions(enable_tool_call_context=True)
+ options = AgentCatOptions(enable_tool_call_context=True)
track(server, "test_project", options)
async with create_test_client(server) as client:
@@ -734,7 +734,7 @@ def tool3():
server = mcp
custom_desc = "Custom context for all tools"
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=custom_desc
)
@@ -755,14 +755,14 @@ async def test_custom_context_description_change_between_tracks(self):
server = create_todo_server()
# First track with one description
- options1 = MCPCatOptions(
+ options1 = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description="First description"
)
track(server, "test_project", options1)
# Second track with different description
- options2 = MCPCatOptions(
+ options2 = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description="Second description"
)
@@ -783,7 +783,7 @@ async def test_custom_context_with_tool_call(self):
"""Test tool calls work correctly with custom context description."""
server = create_todo_server()
custom_desc = "Provide detailed reasoning for this action"
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tool_call_context=True,
custom_context_description=custom_desc
)
@@ -816,7 +816,7 @@ class TestGetMoreToolsContextSchema:
async def test_get_more_tools_context_has_string_type(self):
"""get_more_tools context parameter should have type 'string', not a union type."""
server = create_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_report_missing=True,
enable_tool_call_context=True,
)
@@ -842,7 +842,7 @@ async def test_get_more_tools_context_has_string_type(self):
async def test_get_more_tools_context_has_description(self):
"""get_more_tools context parameter should have a meaningful description."""
server = create_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_report_missing=True,
enable_tool_call_context=True,
)
@@ -867,7 +867,7 @@ async def test_get_more_tools_context_has_description(self):
async def test_get_more_tools_context_is_required(self):
"""get_more_tools context parameter should be required."""
server = create_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_report_missing=True,
enable_tool_call_context=True,
)
@@ -892,7 +892,7 @@ class TestUserIntentCaptureInEvents:
@pytest.fixture(autouse=True)
def setup_and_teardown(self):
"""Set up and tear down mock event queue."""
- from mcpcat.modules.event_queue import event_queue as original_queue
+ from agentcat.modules.event_queue import event_queue as original_queue
yield
set_event_queue(original_queue)
@@ -912,7 +912,7 @@ def capture_event(publish_event_request):
set_event_queue(test_queue)
server = create_todo_server()
- options = MCPCatOptions(
+ options = AgentCatOptions(
enable_tracing=True,
enable_report_missing=True,
enable_tool_call_context=True,
diff --git a/tests/test_truncation.py b/tests/test_truncation.py
index d19d47f..324b104 100644
--- a/tests/test_truncation.py
+++ b/tests/test_truncation.py
@@ -5,9 +5,9 @@
import pytest
-from mcpcat import MCPCatOptions, track
-from mcpcat.modules.event_queue import EventQueue, set_event_queue
-from mcpcat.modules.truncation import (
+from agentcat import AgentCatOptions, track
+from agentcat.modules.event_queue import EventQueue, set_event_queue
+from agentcat.modules.truncation import (
_truncate_value,
truncate_event,
MAX_STRING_BYTES,
@@ -17,7 +17,7 @@
MIN_DEPTH,
TRUNCATABLE_FIELDS,
)
-from mcpcat.types import UnredactedEvent
+from agentcat.types import UnredactedEvent
def _make_event(**overrides) -> UnredactedEvent:
@@ -45,7 +45,7 @@ def test_string_over_limit_truncated_with_marker(self):
original = "a" * (MAX_STRING_BYTES + 500)
result = _truncate_value(original)
byte_size = len(original.encode("utf-8"))
- expected_suffix = f"[string truncated by MCPcat from {byte_size} bytes]"
+ expected_suffix = f"[string truncated by AgentCat from {byte_size} bytes]"
assert result.endswith(expected_suffix)
assert len(result.encode("utf-8")) < len(original.encode("utf-8"))
@@ -54,7 +54,7 @@ def test_utf8_multibyte_truncated_by_bytes_no_broken_codepoints(self):
s = "\U0001f600" * 2561 # 10,244 bytes โ just over limit
result = _truncate_value(s)
byte_size = len(s.encode("utf-8"))
- assert f"[string truncated by MCPcat from {byte_size} bytes]" in result
+ assert f"[string truncated by AgentCat from {byte_size} bytes]" in result
# Verify valid UTF-8 โ would raise if broken
result.encode("utf-8")
@@ -86,7 +86,7 @@ def test_exceeds_max_depth_replaced_with_marker(self):
inner = inner["nested"]
# The dict at the limit is kept, but its nested children are markers
assert isinstance(inner, dict)
- assert inner["nested"] == f"[nested content truncated by MCPcat at depth {MAX_DEPTH}]"
+ assert inner["nested"] == f"[nested content truncated by AgentCat at depth {MAX_DEPTH}]"
def test_max_depth_zero_preserves_top_level_mapping(self):
value = {
@@ -96,7 +96,7 @@ def test_max_depth_zero_preserves_top_level_mapping(self):
result = _truncate_value(value, max_depth=0)
assert isinstance(result, dict)
assert result["event_type"] == "mcp:tools/call"
- assert result["parameters"] == "[nested content truncated by MCPcat at depth 0]"
+ assert result["parameters"] == "[nested content truncated by AgentCat at depth 0]"
class TestBreadthLimiting:
@@ -112,7 +112,7 @@ def test_dict_over_breadth_limit_trimmed_with_marker(self):
result = _truncate_value(d)
assert len(result) == MAX_BREADTH + 1 # MAX_BREADTH items + 1 marker
assert "__truncated__" in result
- assert "5 more items truncated by MCPcat" in result["__truncated__"]
+ assert "5 more items truncated by AgentCat" in result["__truncated__"]
def test_list_at_breadth_limit_unchanged(self):
lst = list(range(MAX_BREADTH))
@@ -123,7 +123,7 @@ def test_list_over_breadth_limit_trimmed_with_marker(self):
lst = list(range(MAX_BREADTH + 30))
result = _truncate_value(lst)
assert len(result) == MAX_BREADTH + 1 # MAX_BREADTH items + 1 marker string
- assert "30 more items truncated by MCPcat" in result[-1]
+ assert "30 more items truncated by AgentCat" in result[-1]
class TestCircularReferences:
@@ -168,25 +168,25 @@ def test_large_string_in_parameters_truncated(self):
assert result is not event
# The big string should be truncated
assert len(result.parameters["data"]) < len(big)
- assert "truncated by MCPcat" in result.parameters["data"]
+ assert "truncated by AgentCat" in result.parameters["data"]
def test_large_string_in_response_truncated(self):
big = "x" * 200_000
event = _make_event(response={"output": big})
result = truncate_event(event)
- assert "truncated by MCPcat" in result.response["output"]
+ assert "truncated by AgentCat" in result.response["output"]
def test_large_string_in_error_truncated(self):
big = "x" * 200_000
event = _make_event(error={"message": "fail", "stack": big})
result = truncate_event(event)
- assert "truncated by MCPcat" in result.error["stack"]
+ assert "truncated by AgentCat" in result.error["stack"]
def test_large_identify_data_truncated(self):
big = "x" * 200_000
event = _make_event(identify_data={"bio": big})
result = truncate_event(event)
- assert "truncated by MCPcat" in result.identify_data["bio"]
+ assert "truncated by AgentCat" in result.identify_data["bio"]
def test_original_event_not_mutated(self):
big = "x" * 200_000
@@ -272,7 +272,7 @@ def test_exception_during_truncation_returns_original(self):
big = "x" * 200_000
event = _make_event(parameters={"data": big})
with patch(
- "mcpcat.modules.truncation._truncate_value",
+ "agentcat.modules.truncation._truncate_value",
side_effect=RuntimeError("boom"),
):
result = truncate_event(event)
@@ -286,7 +286,7 @@ class TestPipelineIntegration:
def test_truncation_is_imported_in_event_queue(self):
"""Verify truncate_event is used in event_queue module."""
import inspect
- from mcpcat.modules.event_queue import EventQueue
+ from agentcat.modules.event_queue import EventQueue
source = inspect.getsource(EventQueue._process_event)
assert "truncate_event" in source
@@ -296,7 +296,7 @@ class TestTruncationWithTodoServer:
@pytest.fixture(autouse=True)
def setup_and_teardown(self):
- from mcpcat.modules.event_queue import event_queue as original_queue
+ from agentcat.modules.event_queue import event_queue as original_queue
yield
set_event_queue(original_queue)
@@ -321,7 +321,7 @@ async def test_oversized_parameter_is_truncated(self):
captured_events = self._capture_setup()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
# Use varied text so the sanitizer doesn't flag it as binary data
@@ -342,7 +342,7 @@ async def test_oversized_parameter_is_truncated(self):
# The parameter string should have been truncated
captured_text = event.parameters["arguments"]["text"]
assert len(captured_text) < len(oversized_text)
- assert "truncated by MCPcat" in captured_text
+ assert "truncated by AgentCat" in captured_text
# Whole event must fit within the size limit
event_bytes = len(event.model_dump_json().encode("utf-8"))
@@ -357,7 +357,7 @@ async def test_oversized_response_is_truncated(self):
captured_events = self._capture_setup()
server = create_todo_server()
- options = MCPCatOptions(enable_tracing=True)
+ options = AgentCatOptions(enable_tracing=True)
track(server, "test_project", options)
# Add many todos so list_todos returns a large response
@@ -392,7 +392,7 @@ def test_1mb_user_intent(self):
assert result is not event
result_bytes = len(result.model_dump_json().encode("utf-8"))
assert result_bytes <= MAX_EVENT_BYTES
- assert "truncated by MCPcat" in result.user_intent
+ assert "truncated by AgentCat" in result.user_intent
def test_1mb_in_parameters(self):
event = _make_event(parameters={"context": self.ONE_MB})
@@ -400,21 +400,21 @@ def test_1mb_in_parameters(self):
assert result is not event
result_bytes = len(result.model_dump_json().encode("utf-8"))
assert result_bytes <= MAX_EVENT_BYTES
- assert "truncated by MCPcat" in result.parameters["context"]
+ assert "truncated by AgentCat" in result.parameters["context"]
def test_1mb_in_response(self):
event = _make_event(response={"output": self.ONE_MB})
result = truncate_event(event)
result_bytes = len(result.model_dump_json().encode("utf-8"))
assert result_bytes <= MAX_EVENT_BYTES
- assert "truncated by MCPcat" in result.response["output"]
+ assert "truncated by AgentCat" in result.response["output"]
def test_1mb_in_error(self):
event = _make_event(error={"message": "fail", "stack": self.ONE_MB})
result = truncate_event(event)
result_bytes = len(result.model_dump_json().encode("utf-8"))
assert result_bytes <= MAX_EVENT_BYTES
- assert "truncated by MCPcat" in result.error["stack"]
+ assert "truncated by AgentCat" in result.error["stack"]
def test_1mb_in_all_fields_simultaneously(self):
event = _make_event(
@@ -537,7 +537,7 @@ def test_metadata_fields_preserved_when_payload_forces_extreme_truncation(self):
assert result.user_intent == "short intent"
# Payload was truncated
- assert "truncated by MCPcat" in result.parameters["data"]
+ assert "truncated by AgentCat" in result.parameters["data"]
# Still under size limit
result_bytes = len(result.model_dump_json().encode("utf-8"))
@@ -554,6 +554,6 @@ def test_large_user_intent_truncated_but_metadata_preserved(self):
result = truncate_event(event)
assert result.event_type == "mcp:tools/call"
assert result.resource_name == "my_tool"
- assert "truncated by MCPcat" in result.user_intent
+ assert "truncated by AgentCat" in result.user_intent
result_bytes = len(result.model_dump_json().encode("utf-8"))
assert result_bytes <= MAX_EVENT_BYTES
diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py
index 9e9966d..7166fb8 100644
--- a/tests/test_utils/__init__.py
+++ b/tests/test_utils/__init__.py
@@ -1,11 +1,11 @@
-"""Test utilities for MCPCat tests."""
+"""Test utilities for AgentCat tests."""
import os
from pathlib import Path
import pytest
-LOG_FILE = "mcpcat.log"
+LOG_FILE = "agentcat.log"
def cleanup_log_file():
diff --git a/tests/test_utils/client.py b/tests/test_utils/client.py
index 6a34077..ad2f13e 100644
--- a/tests/test_utils/client.py
+++ b/tests/test_utils/client.py
@@ -1,4 +1,4 @@
-"""Test client utilities for MCPCat tests."""
+"""Test client utilities for AgentCat tests."""
from contextlib import asynccontextmanager
from typing import Any, AsyncGenerator