Skip to main content

Vesta customer SDK — instrument an MCP server with one call.

Project description

vesta-analytics

Analytics for MCP servers. One call instruments your server and ships behavioural data to Vesta over OTLP/HTTP — which tools agents actually call, what they pass, where they fail, and who comes back.

pip install vesta-analytics
import vesta
vesta.instrument(server, api_key="vsk_...")

Distribution name vesta-analytics; you import it as vesta.

Two things to know before you install

It cannot break your server. Every capture path is fail-open: if Vesta's exporter is down, misconfigured, or throws, your handler still runs and your result is still returned.

Redaction runs inside your process. Arguments and responses pass through the redaction pass before anything is exported. A field you mark redact never crosses the network — Vesta cannot "un-redact" it later, because it was never sent. Out of the box everything is redacted; you choose what to let through.

1. Get an API key

Vesta is in early access. Request access at https://vesta-analytics.ai/request-access, then sign in and create a key. Keep it secret.

2. Instrument your server

Add the call after your tools are registered and before you serve. The adapter wraps the handlers that exist at the moment you call it, so a tool registered afterwards is not captured.

import vesta
from mcp.server.lowlevel import Server

server = Server("my-server")

@server.call_tool()
async def call_tool(name: str, arguments: dict): ...

vesta.instrument(server, api_key="vsk_...")   # <- after tools, before serve

Calling it twice on the same server is a no-op.

Did it work?

vesta.instrument(server, api_key="vsk_...", verbose=True)

verbose=True logs whether spans exported successfully — that tells you the data left your process.

Then make one real tool call and open https://vesta-analytics.ai/app/settings, where you can watch spans arrive. Export is asynchronous, so give it a few seconds.

If verbose reports a successful export but nothing shows up, email info@datafenix.ai with your surface name and roughly when you made the call.

3. What Vesta sees

Always captured, whatever you configure: the tool name, the MCP method, timing, whether the call errored, and a session id. Redaction governs argument and response values — nothing else.

Those values are your choice, field by field. The default is to redact every one of them — safe, but it means Vesta can tell you a tool was called and how long it took, and almost nothing else. Most of the value (which arguments matter, which options nobody uses, where agents retry) needs values.

Action Vesta sees
capture the real value
hash a stable fingerprint of it — the same input always gives the same digest
redact nothing
detect the text, with anything matching an email/phone/card/IP pattern masked
truncate(n) the first n characters

What to do with each kind of field

Find the fields your tools take, and do this:

Your field Do this What it gives you
user_id, account_id, org_id hash Distinct users, returning users, retention — without Vesta ever holding the id
document_id, ticket_id, project_id hash Which records agents hit hardest, without reading them
mode, filter, sort, status, type capture Which options agents actually use — and which nobody has ever touched
limit, offset, page_size capture Agents pulling 500 rows when 10 would do
locale, currency, timezone capture Where your traffic comes from
Search query, question, prompt detect What agents are actually looking for, with PII masked out
title, description, body, comment detect, or redact if it's a message body Enough to see what agents write, without the whole payload
File paths, resource URIs capture, or truncate(n) if they embed ids Which resources are hot
Person names — assignee_name, author, customer_name redact, or hash to count them Nothing catches these for you — see below
email, phone, ssn, card numbers, passwords, API keys nothing to do Already redacted automatically
Tool responses capture or detect Why calls fail: empty results, auth errors, dependency errors. Redact these and you lose the failure diagnosis

Two things worth knowing, because they're free:

Argument names are captured even when their values are redacted. So agents calling a tool with the wrong keys is detectable no matter how locked-down you are. You don't have to capture a single value to get that.

Redacting responses is the expensive one. Whether a call came back empty, hit an auth error, or failed on a dependency is read out of the response text. Lock that down and Vesta can still tell you a call failed, but not why.

What the safety nets don't catch

The automatic redaction works on field names. detect works on patterns — emails, phone numbers, Luhn-checked cards, IP addresses.

Neither one catches a person's name, or a street address written in prose. There is no pattern to match, so both sail straight through:

{"assignee_name": "Ada Lovelace",
 "note": "call Ada Lovelace at 12 Wilbury Way about a@b.com"}

# with default="capture", text="detect":
{"assignee_name": "Ada Lovelace",                            # <- captured
 "note": "call Ada Lovelace at 12 Wilbury Way about <EMAIL>"} # <- only the email masked

If your tools take names or addresses, redact or hash them yourself. Nothing else will.

A good starting point

Capture the structure, scan free text, hash the ids, redact the secrets:

import vesta
from vesta import RedactionConfig, Field, Tool

vesta.instrument(
    server,
    api_key="vsk_...",
    args=RedactionConfig(
        default="capture",     # values are the point — start from yes
        text="detect",         # ...but mask PII inside free-text strings
        rules=[
            Field("user_id").hash(),      # count distinct users, never read one
            Field("account_id").hash(),
            Tool("billing_*").redact(),   # everything else in billing_* calls
        ],
    ),
    responses=RedactionConfig(default="capture", text="detect"),
)

Three things that config is doing:

  • default="capture" — values go through unless a rule says otherwise.
  • text="detect" — but string values are scanned for PII first, and any match is masked. Without it, strings are captured as they are.
  • responses= — set it, or your responses stay fully redacted. args and responses are configured separately, and each defaults to redact-all.

Switching default to capture is less reckless than it sounds. Whenever default="capture", these field names are still redacted automatically, at any depth: password, email, ssn, phone, address, credit_card, card_number, cvv, dob, date_of_birth, tax_id, passport, api_key, auth_token, bearer, secret, private_key.

Rules match a field name at any depth

MCP arguments are usually nested, so a bare name matches that field wherever it appears — top level, inside filters, inside a list. Use a dotted path when you mean one exact place:

Field("assignee_id").hash()          # every assignee_id, at any depth
Field("filters.assignee_id").hash()  # only that one
Field("**.email").redact()           # explicit glob; same as the bare name

When rules disagree

The most specific rule wins:

  1. Field — and an exact path beats a bare name
  2. Tool / Prompt / Resource — matching the thing being called
  3. Method — e.g. Method("resources/*")
  4. text=, then default=

So a field rule overrides a tool rule, and overrides the automatic denylist above. In the config on this page, a user_id inside a billing_* call is hashed, not redacted — the field rule is more specific than the tool rule. That's usually what you want (you can still count users on billing calls), but if you need a tool redacted with no exceptions, don't also write a field rule that captures or hashes something inside it.

What hash does and doesn't protect

hash is a stable fingerprint — the same input always produces the same digest, so you can count distinct users, group by them, and follow cohorts over time. It's keyed with a secret derived from your API key, which Vesta stores only as a hash of itself. So Vesta cannot reverse your hashes from anything it stores, and two customers hashing the same value get different digests.

But this is pseudonymisation, not anonymisation. A hashed identifier still distinguishes one person from another, and hashed data is still personal data. hash is for identifiers you want to count. Anything you'd rather Vesta never held belongs in redact.

detect masks emails, phone numbers, Luhn-checked cards, IPs and national-id patterns inside free text. It matches patterns, so it won't catch names or addresses written in prose — a useful safety net, not a guarantee.

Key rotation

Your hash key is derived from your API key, so rotating that key changes every digest: the same user then looks like a new user. If you rotate keys, pass a stable secret of your own instead, and Vesta never sees it.

vesta.instrument(server, api_key="vsk_...", hash_salt=os.environ["VESTA_HASH_SALT"])

4. Tell Vesta who the user is

Distinct users, returning users and retention all come from a user_id.

If your server authenticates with OAuth, you get this for free — Vesta derives a pseudonymous user id from the token's subject. Nothing to configure.

If it doesn't, supply one. The callable receives the framework's request object:

vesta.instrument(
    server,
    api_key="vsk_...",
    session_context=lambda request: {"user_id": your_user_id(request)},
)

Whatever you return is hashed inside your process before it goes anywhere — the same keyed hash described above — so Vesta never holds the raw value. It's safe to return an email or an internal id here; neither reaches us.

5. Supported frameworks

  • Official mcp SDK — the low-level mcp.server.lowlevel.Server, and the FastMCP bundled with it.
  • Community fastmcp — the standalone package.

Detection is structural, so the MCP packages are optional extras — install only the one you use:

pip install "vesta-analytics[mcp]"       # official SDK
pip install "vesta-analytics[fastmcp]"   # standalone fastmcp

Captured methods: tools/call, prompts/get, resources/read, initialize, and the */list methods.

Shutdown / stdio delivery

Short stdio sessions export nothing until the process exits, so the shutdown flush is the whole delivery path — keep your server alive a moment after a call or you'll lose the batch. instrument() installs that flush and hard-bounds it (2s, VESTA_FLUSH_DEADLINE_MS to override), so a dead collector can never hang your process. The returned InstrumentHandle exposes flush() and shutdown() if you'd rather drive the lifecycle yourself; ignoring it is fine.

Reference

vesta.instrument(
    server,                      # your MCP server object
    *,
    api_key: str,                # required
    endpoint: str = "https://ingest.vesta-analytics.ai/v1/traces",
    session_context: Callable[[Any], dict] | None = None,
    args: RedactionConfig | None = None,        # default: redact everything
    responses: RedactionConfig | None = None,   # default: redact everything
    transport: str | None = None,               # autodetected
    verbose: bool = False,
    hash_salt: str | None = None,               # default: derived from api_key
) -> InstrumentHandle | None

Environment variables: VESTA_FLUSH_DEADLINE_MS and VESTA_TRANSPORT. The API key is passed to instrument(); there is no environment variable for it.

Requires Python 3.12+. Apache-2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vesta_analytics-0.2.0.tar.gz (81.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vesta_analytics-0.2.0-py3-none-any.whl (49.1 kB view details)

Uploaded Python 3

File details

Details for the file vesta_analytics-0.2.0.tar.gz.

File metadata

  • Download URL: vesta_analytics-0.2.0.tar.gz
  • Upload date:
  • Size: 81.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.11

File hashes

Hashes for vesta_analytics-0.2.0.tar.gz
Algorithm Hash digest
SHA256 f12fb1ea09d89d1d3ea8b23765a2c98420f0c4272af9a37bdea3a07a94bcfe26
MD5 b15d3929e289470effedc4daf6006228
BLAKE2b-256 75cfe32fd1089444606bc2516956a36a7b37299d5dd06bbda63c729ee2438a91

See more details on using hashes here.

File details

Details for the file vesta_analytics-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for vesta_analytics-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2f5a2696cb9930b272627e2294d9fcc69ee59c44f346fa2d27bd4beeaa782efc
MD5 0c94114e937ee6f4527f2505564c0cdd
BLAKE2b-256 9c3a74f2af6b7e6ccb0425a2c7f7984ca2a47814c1300510a105571366d9307d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page