Metadata-Version: 2.5
Name: agent-status-sdk
Version: 2.4.1
Summary: Fail-open wrap for live MCP and personal-agent traffic on AgentStatus.
Project-URL: Homepage, https://agentstatus.dev
Project-URL: Dashboard, https://agentstatus.dev
Project-URL: Repository, https://github.com/Carmel-Labs-Inc/agent-status-sdk
Author-email: Carmel Labs <dev@carmel.so>
License-Expression: MIT
License-File: LICENSE
Keywords: agentstatus,mcp,observability
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# agent-status-sdk (Python)

Live `tools/call` traffic from **your** MCP server into AgentStatus User Traffic and Analytics.

This package does not register HTTP agents, run residential probes, open a tunnel, or expose Agent Status as an MCP server. Those were the old SDK. Gone in 2.0.0.

Your public MCP URL does not change. Hosts keep calling origin. `observe()` wraps the process you already run.

## Install

```bash
pip install agent-status-sdk
```

No extra extras. Stdlib only.

## What you need from the dashboard

On the MCP agent, User Traffic tab, copy the agent id. That is a UUID, not an API key.

Set it in code or in the environment:

```bash
export AGENTSTATUS_AGENT_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```

Optional:

```bash
export AGENTSTATUS_INGEST="https://api.rora.carmel.so/api/rora"
```

Default ingest is already that URL. You do not pass a Fabric/Rora API key. The observe route is public per agent id, same threat model as the live-mcp URL.

## FastMCP

Register tools first, then wrap, then run.

```python
from mcp.server.fastmcp import FastMCP
from agent_status import observe

mcp = FastMCP("docs")

@mcp.tool()
def search_docs(q: str) -> str:
    return "..."

observe(mcp, agent_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
mcp.run()
```

If `AGENTSTATUS_AGENT_ID` is set, `observe(mcp)` is enough.

## Low-level MCP `Server`

```python
from mcp.server import Server
from agent_status import observe

server = Server("docs")
observe(server, agent_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
```

Call `observe()` after the server object exists. If your framework registers `CallTool` lazily, call it after tools are registered and before `run()`.

If the wrap cannot find `tools/call`, stderr prints a warning and the server still runs. Pass `observe(..., required=True)` if you want boot to fail instead. Do not use `track()` with an MCP id.

## What gets recorded

On each `tools/call`:

- tool name (job)
- duration (time inside the handler)
- RPC / exception error text, truncated
- host name and version from `initialize.clientInfo` when the wrap saw initialize
- session id if you pass it on `report_tool_call`
- a short result excerpt

User Traffic is the event list. Analytics rolls those events (mix, week vs week, errors, latency). Cost and tokens only if you send them. We do not invent them.

`observe()` does not store names, email, chat, or the host thread. Arguments are forwarded as a dict; do not put secrets in tool args if you cannot afford them leaving the process. The ingest strips a small blocked-key set (`email`, `chat`, `prompt`, …) but that is not a privacy guarantee.

## Fail-open

The tool handler always runs first. Recording is a background POST with a 2 second timeout. If AgentStatus is down, DNS fails, or the wrap cannot find a handler, the tool still returns. We miss a row. The customer does not miss a response.

`observe()` itself never raises into your server. A bad server object is returned unchanged.

## If Analytics is empty

1. `observe()` is not on the process hosts actually hit.
2. You wrapped after `run()`, or before `setRequestHandler` / tool registration in a way the wrap missed. Check stderr for the wrap warning.
3. Origin is still serving a second copy of the server without the wrap.
4. Hosts are calling a different deployment than the one with `AGENTSTATUS_AGENT_ID`.
5. You called `track()` with an MCP id, or `observe()` with a personal-agent id. Ingest rejects that.

Connection tests from AgentStatus are our probes. They are not live user traffic.

## Personal agents (`Agent`)

Use `Agent`, not `observe()`. Bind once per thread. Wrap the dispatcher once. `get_calendar` maps to calendar.

```python
from agent_status import Agent

ember = Agent(agent_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")

def handle_thread(thread):
    bound = ember.bind(user=thread.anon_id, session=thread.id)
    call_tool = bound.wrap_tools(call_tool)
    return call_tool("get_calendar", {"when": "tomorrow"})
```

That one wrap is User Traffic and the lookup. After any job the wrap calls a list/get sibling (`create_ticket` → `list_tickets`, `book_table` → `get_calendar`). Tools the wrap has already seen in the same stem count too. You do not set a second URL. If there is no read sibling, the dashboard says we don't know. `landed=` still overrides.

`track("calendar", ...)` still works for a single job.

## Manual report

If you are not using the official MCP Server classes:

```python
from agent_status import report_tool_call

report_tool_call(
    name="search_docs",
    agent_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    duration_ms=21,
    provider="cursor",
)
```

Never raises.

## What this is not

- Not a sidecar. Not a public URL swap.
- Not session replay of Cursor / Claude Code.
- Not hallucination grouping.
- Not wrapping the host. We wrap your server.

## Ingest

`POST https://api.rora.carmel.so/api/rora/public/live-mcp/{agent_id}/observe`

JSON body, `name` required. This path only records. It does not proxy to origin and does not run the tool.
