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.argsandresponsesare 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:
Field— and an exact path beats a bare nameTool/Prompt/Resource— matching the thing being calledMethod— e.g.Method("resources/*")text=, thendefault=
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
mcpSDK — the low-levelmcp.server.lowlevel.Server, and theFastMCPbundled 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f12fb1ea09d89d1d3ea8b23765a2c98420f0c4272af9a37bdea3a07a94bcfe26
|
|
| MD5 |
b15d3929e289470effedc4daf6006228
|
|
| BLAKE2b-256 |
75cfe32fd1089444606bc2516956a36a7b37299d5dd06bbda63c729ee2438a91
|
File details
Details for the file vesta_analytics-0.2.0-py3-none-any.whl.
File metadata
- Download URL: vesta_analytics-0.2.0-py3-none-any.whl
- Upload date:
- Size: 49.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2f5a2696cb9930b272627e2294d9fcc69ee59c44f346fa2d27bd4beeaa782efc
|
|
| MD5 |
0c94114e937ee6f4527f2505564c0cdd
|
|
| BLAKE2b-256 |
9c3a74f2af6b7e6ccb0425a2c7f7984ca2a47814c1300510a105571366d9307d
|