mcpeye 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcpeye-0.1.0/.gitignore +49 -0
- mcpeye-0.1.0/PKG-INFO +209 -0
- mcpeye-0.1.0/README.md +187 -0
- mcpeye-0.1.0/package.json +11 -0
- mcpeye-0.1.0/pyproject.toml +41 -0
- mcpeye-0.1.0/src/mcpeye/__init__.py +50 -0
- mcpeye-0.1.0/src/mcpeye/intent.py +83 -0
- mcpeye-0.1.0/src/mcpeye/py.typed +0 -0
- mcpeye-0.1.0/src/mcpeye/redaction.py +105 -0
- mcpeye-0.1.0/src/mcpeye/request_capability.py +85 -0
- mcpeye-0.1.0/src/mcpeye/tracker.py +1082 -0
- mcpeye-0.1.0/tests/conftest.py +270 -0
- mcpeye-0.1.0/tests/test_buffer.py +197 -0
- mcpeye-0.1.0/tests/test_capture.py +320 -0
- mcpeye-0.1.0/tests/test_intent.py +84 -0
- mcpeye-0.1.0/tests/test_redaction.py +150 -0
- mcpeye-0.1.0/tests/test_request_capability.py +255 -0
- mcpeye-0.1.0/tests/test_wrap_tool.py +150 -0
mcpeye-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# deps
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
|
|
5
|
+
# build output
|
|
6
|
+
dist/
|
|
7
|
+
.next/
|
|
8
|
+
out/
|
|
9
|
+
build/
|
|
10
|
+
*.tsbuildinfo
|
|
11
|
+
|
|
12
|
+
# turbo
|
|
13
|
+
.turbo/
|
|
14
|
+
|
|
15
|
+
# env
|
|
16
|
+
.env
|
|
17
|
+
.env.local
|
|
18
|
+
.env.*.local
|
|
19
|
+
|
|
20
|
+
# logs
|
|
21
|
+
*.log
|
|
22
|
+
npm-debug.log*
|
|
23
|
+
pnpm-debug.log*
|
|
24
|
+
|
|
25
|
+
# os / editor
|
|
26
|
+
.DS_Store
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/*
|
|
29
|
+
!.vscode/extensions.json
|
|
30
|
+
|
|
31
|
+
# generated reports (local self-host output) — anchored to the repo root so it does
|
|
32
|
+
# NOT also match the app route source at apps/web/app/reports/
|
|
33
|
+
/reports/
|
|
34
|
+
|
|
35
|
+
# python
|
|
36
|
+
__pycache__/
|
|
37
|
+
*.py[cod]
|
|
38
|
+
.venv/
|
|
39
|
+
dist/
|
|
40
|
+
*.egg-info/
|
|
41
|
+
|
|
42
|
+
# ruby
|
|
43
|
+
/packages/sdk-ruby/pkg/
|
|
44
|
+
/packages/sdk-ruby/*.gem
|
|
45
|
+
/packages/sdk-ruby/Gemfile.lock
|
|
46
|
+
.bundle/
|
|
47
|
+
|
|
48
|
+
# Internal MVP feature specs (local only — implement from these; not for the public repo)
|
|
49
|
+
specs/
|
mcpeye-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcpeye
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: mcpeye Python SDK — open-source product analytics for MCP servers. See why your agent is failing.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mcpeye/mcpeye
|
|
6
|
+
Project-URL: Repository, https://github.com/mcpeye/mcpeye
|
|
7
|
+
Author: mcpeye
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: agent-analytics,ai-agents,llm,mcp,mcp-analytics,mcp-server,mcpeye,model-context-protocol,observability,product-analytics,self-hosted,session-replay
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: pydantic>=2.0
|
|
19
|
+
Provides-Extra: mcp
|
|
20
|
+
Requires-Dist: mcp>=1.0.0; extra == 'mcp'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# mcpeye (Python SDK)
|
|
24
|
+
|
|
25
|
+
Open-source product analytics for MCP servers. **See why your agent is failing.**
|
|
26
|
+
|
|
27
|
+
`mcpeye` instruments your MCP server so you can answer the question the
|
|
28
|
+
hero **Intent Gap Report** is built around: *what did users ask the tools to do
|
|
29
|
+
that the tools could not deliver?*
|
|
30
|
+
|
|
31
|
+
It works by injecting an optional `mcpeyeIntent` parameter into every tool's
|
|
32
|
+
input schema. The agent self-reports, in its own words, why it is calling a tool
|
|
33
|
+
and any blocker the user hit — so intent is captured at near-zero cost, with **no
|
|
34
|
+
per-call LLM**. The clustering LLM runs later, server-side, only to build reports.
|
|
35
|
+
|
|
36
|
+
This is the Python SDK. The [TypeScript SDK](../sdk-typescript) is the reference
|
|
37
|
+
implementation; behaviour and the wire contract are identical across SDKs.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install mcpeye
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.9+. Depends on `httpx` and `pydantic`. The `mcp` package is
|
|
46
|
+
**not** a hard dependency — it is your server's framework, imported lazily by
|
|
47
|
+
`track()`. If you use the automatic `track()` path, install it alongside (or use
|
|
48
|
+
the extra, which pins `mcp>=1.0.0`, itself Python 3.10+):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install "mcpeye[mcp]"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
Call `track(...)` once, **after** you have registered your `list_tools` and
|
|
57
|
+
`call_tool` handlers:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from mcp.server.lowlevel import Server
|
|
61
|
+
import mcpeye
|
|
62
|
+
|
|
63
|
+
server = Server("my-server", version="1.2.3")
|
|
64
|
+
|
|
65
|
+
@server.list_tools()
|
|
66
|
+
async def list_tools():
|
|
67
|
+
return [ ... ]
|
|
68
|
+
|
|
69
|
+
@server.call_tool()
|
|
70
|
+
async def call_tool(name, arguments):
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
# Instrument it. mcpeyeIntent is injected into every tool schema, calls are
|
|
74
|
+
# captured, redacted, buffered, and shipped to the ingest API.
|
|
75
|
+
mcpeye.track(
|
|
76
|
+
server,
|
|
77
|
+
project_id="proj_123",
|
|
78
|
+
ingest_url="http://localhost:3001", # or set MCPEYE_INGEST_URL
|
|
79
|
+
ingest_secret="...", # or set MCPEYE_INGEST_SECRET
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`track` returns the same `server` instance, instrumented in place.
|
|
84
|
+
|
|
85
|
+
### What `track` does
|
|
86
|
+
|
|
87
|
+
1. **Injects `mcpeyeIntent`** into each tool's `inputSchema` (and teaches the
|
|
88
|
+
server's tool cache about it, so the built-in input validation accepts the
|
|
89
|
+
parameter). The agent fills it in; your tool handler never sees it — it is
|
|
90
|
+
stripped from the arguments before your code runs.
|
|
91
|
+
2. **Captures every tool call**: tool name, redacted arguments, redacted result,
|
|
92
|
+
error state + message, duration, and the reported intent. Each captured field
|
|
93
|
+
is size-bounded (oversized or unserializable values become a small marker) so a
|
|
94
|
+
multi-MB tool result can never blow the ingest body limit or grow the buffer.
|
|
95
|
+
3. **Adds a reserved `mcpeye_request_capability` tool** (active missing-capability
|
|
96
|
+
capture). When the agent wants a capability none of your tools cover, it can
|
|
97
|
+
call this tool to say so in the user's words. mcpeye answers it locally with a
|
|
98
|
+
canned acknowledgement — it is **never** forwarded to your server — and records
|
|
99
|
+
it as a normal tool call with `tool_name = "mcpeye_request_capability"`, which
|
|
100
|
+
the report folds into "Top missing capabilities" as a high-confidence,
|
|
101
|
+
explicitly-requested entry. This catches the *silent miss*, where the right move
|
|
102
|
+
is to call no tool at all. Disable with `capture_missing_capabilities=False`.
|
|
103
|
+
4. **Ships** batches as the shared `IngestPayload` JSON to `<ingest_url>/ingest`
|
|
104
|
+
with the `x-mcpeye-secret` header, using `httpx`. Shipping happens on a
|
|
105
|
+
background daemon thread — **never on the tool-call thread** — so a slow or
|
|
106
|
+
unreachable ingest endpoint adds no latency to your tools. It flushes on a timer
|
|
107
|
+
(default every 5s), eagerly when the batch fills (`flush_at`), and a final time
|
|
108
|
+
at process exit. Shipping is best-effort: ingest failures are swallowed (routed
|
|
109
|
+
to `on_error`) and retried; analytics can never take down your MCP server.
|
|
110
|
+
|
|
111
|
+
> **Strict ingest URL.** Unlike the TypeScript SDK (which defaults to
|
|
112
|
+
> `http://localhost:3001`), the Python SDK raises `ValueError` at setup time if no
|
|
113
|
+
> ingest URL is configured — there is no implicit localhost default, so a
|
|
114
|
+
> misconfigured server fails loudly at your call site instead of silently dropping
|
|
115
|
+
> telemetry into a dead port.
|
|
116
|
+
|
|
117
|
+
## Configuration
|
|
118
|
+
|
|
119
|
+
| Argument | Env fallback | Default |
|
|
120
|
+
| ------------------ | ----------------------- | ------------------------------- |
|
|
121
|
+
| `ingest_url` | `MCPEYE_INGEST_URL` | — (required, raises if unset) |
|
|
122
|
+
| `ingest_secret` | `MCPEYE_INGEST_SECRET` | none |
|
|
123
|
+
| `redact` | — | `True` |
|
|
124
|
+
| `user_id` | — | none |
|
|
125
|
+
| `client` | — | none |
|
|
126
|
+
| `server_version` | — | `server.version` when available |
|
|
127
|
+
| `flush_at` | — | `20` buffered events |
|
|
128
|
+
| `flush_interval_s` | — | `5.0` seconds (background timer) |
|
|
129
|
+
| `denylist_fields` | — | none (adds to the built-in denylist) |
|
|
130
|
+
| `capture_missing_capabilities` | — | `True` (inject `mcpeye_request_capability`) |
|
|
131
|
+
| `on_error` | — | debug log on the `mcpeye` logger |
|
|
132
|
+
|
|
133
|
+
> **Manifest cost.** With `capture_missing_capabilities=True`, your server's
|
|
134
|
+
> `tools/list` gains one extra tool — a few hundred tokens in any model context
|
|
135
|
+
> that lists tools, and one more entry in any tool picker / doc generator. That is
|
|
136
|
+
> the price of seeing silent misses; pass `False` to keep it out of the manifest.
|
|
137
|
+
|
|
138
|
+
### Diagnostics
|
|
139
|
+
|
|
140
|
+
Every swallowed error (transport failure, capture failure) is routed to `on_error`,
|
|
141
|
+
which defaults to a `debug`-level log on `logging.getLogger("mcpeye")`. Pass your
|
|
142
|
+
own sink to see why telemetry might be silent — it is wrapped so it can never throw
|
|
143
|
+
back into your server:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
mcpeye.track(server, "proj_123", ingest_url="http://localhost:3001",
|
|
147
|
+
on_error=lambda err: print("mcpeye:", err))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Redaction
|
|
151
|
+
|
|
152
|
+
When `redact=True` (the default), arguments, results, and the reported intent are
|
|
153
|
+
scrubbed **client-side before anything leaves your process**. The v1 redaction is
|
|
154
|
+
a conservative regex pass — it over-redacts rather than leak — covering:
|
|
155
|
+
|
|
156
|
+
- emails
|
|
157
|
+
- API keys: `sk-…`, `sk-ant-…`, `ghp_…`/`gho_…`/etc., `AKIA…`
|
|
158
|
+
- `Bearer …` tokens and JWTs
|
|
159
|
+
- credit-card-shaped digit runs
|
|
160
|
+
- loose international phone numbers
|
|
161
|
+
- a field-name denylist (`password`, `secret`, `token`, `apiKey`, `authorization`, …)
|
|
162
|
+
|
|
163
|
+
Self-hosting is the real privacy mitigation; redaction reduces the blast radius
|
|
164
|
+
of obvious secrets in free-text. You can use the primitives directly:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from mcpeye import redact_string, redact_value
|
|
168
|
+
|
|
169
|
+
redact_string("ping me at a@b.com") # -> "ping me at [REDACTED_EMAIL]"
|
|
170
|
+
redact_value({"password": "hunter2"}) # -> {"password": "[REDACTED_FIELD]"}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Manual instrumentation (`wrap_tool`)
|
|
174
|
+
|
|
175
|
+
The `mcp` package's `Server` internals vary across versions. `track` attaches to
|
|
176
|
+
`server.request_handlers`; if that layout ever changes, `track` raises a clear
|
|
177
|
+
error and you can fall back to instrumenting individual tool handlers:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import mcpeye
|
|
181
|
+
|
|
182
|
+
@mcpeye.wrap_tool(project_id="proj_123", tool_name="search",
|
|
183
|
+
ingest_url="http://localhost:3001")
|
|
184
|
+
async def search(arguments):
|
|
185
|
+
...
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`wrap_tool` pulls `mcpeyeIntent` out of the arguments, times the call, captures
|
|
189
|
+
the outcome, and ships it — the same capture path as `track`, scoped to one tool.
|
|
190
|
+
It accepts `redact`, `ingest_url`, `ingest_secret`, `denylist_fields`, `flush_at`,
|
|
191
|
+
`flush_interval_s`, and `on_error` (not `user_id`/`client`/`server_version`, which
|
|
192
|
+
are server-level identity). Note: it does not inject the parameter into a published
|
|
193
|
+
schema, so add `mcpeyeIntent` to that tool's `inputSchema` yourself (see
|
|
194
|
+
`mcpeye.intent_param_json_schema`) if you want agents to populate it.
|
|
195
|
+
|
|
196
|
+
## Development
|
|
197
|
+
|
|
198
|
+
Unit tests stub the `mcp` package, so they run with only `httpx`, `pydantic`, and
|
|
199
|
+
`pytest` installed (no `mcp`):
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
python3 -m venv .venv && .venv/bin/pip install httpx pydantic pytest
|
|
203
|
+
.venv/bin/pip install -e packages/sdk-python --no-deps
|
|
204
|
+
.venv/bin/python -m pytest packages/sdk-python/tests -q
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
mcpeye-0.1.0/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# mcpeye (Python SDK)
|
|
2
|
+
|
|
3
|
+
Open-source product analytics for MCP servers. **See why your agent is failing.**
|
|
4
|
+
|
|
5
|
+
`mcpeye` instruments your MCP server so you can answer the question the
|
|
6
|
+
hero **Intent Gap Report** is built around: *what did users ask the tools to do
|
|
7
|
+
that the tools could not deliver?*
|
|
8
|
+
|
|
9
|
+
It works by injecting an optional `mcpeyeIntent` parameter into every tool's
|
|
10
|
+
input schema. The agent self-reports, in its own words, why it is calling a tool
|
|
11
|
+
and any blocker the user hit — so intent is captured at near-zero cost, with **no
|
|
12
|
+
per-call LLM**. The clustering LLM runs later, server-side, only to build reports.
|
|
13
|
+
|
|
14
|
+
This is the Python SDK. The [TypeScript SDK](../sdk-typescript) is the reference
|
|
15
|
+
implementation; behaviour and the wire contract are identical across SDKs.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install mcpeye
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Python 3.9+. Depends on `httpx` and `pydantic`. The `mcp` package is
|
|
24
|
+
**not** a hard dependency — it is your server's framework, imported lazily by
|
|
25
|
+
`track()`. If you use the automatic `track()` path, install it alongside (or use
|
|
26
|
+
the extra, which pins `mcp>=1.0.0`, itself Python 3.10+):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install "mcpeye[mcp]"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
Call `track(...)` once, **after** you have registered your `list_tools` and
|
|
35
|
+
`call_tool` handlers:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from mcp.server.lowlevel import Server
|
|
39
|
+
import mcpeye
|
|
40
|
+
|
|
41
|
+
server = Server("my-server", version="1.2.3")
|
|
42
|
+
|
|
43
|
+
@server.list_tools()
|
|
44
|
+
async def list_tools():
|
|
45
|
+
return [ ... ]
|
|
46
|
+
|
|
47
|
+
@server.call_tool()
|
|
48
|
+
async def call_tool(name, arguments):
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
# Instrument it. mcpeyeIntent is injected into every tool schema, calls are
|
|
52
|
+
# captured, redacted, buffered, and shipped to the ingest API.
|
|
53
|
+
mcpeye.track(
|
|
54
|
+
server,
|
|
55
|
+
project_id="proj_123",
|
|
56
|
+
ingest_url="http://localhost:3001", # or set MCPEYE_INGEST_URL
|
|
57
|
+
ingest_secret="...", # or set MCPEYE_INGEST_SECRET
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`track` returns the same `server` instance, instrumented in place.
|
|
62
|
+
|
|
63
|
+
### What `track` does
|
|
64
|
+
|
|
65
|
+
1. **Injects `mcpeyeIntent`** into each tool's `inputSchema` (and teaches the
|
|
66
|
+
server's tool cache about it, so the built-in input validation accepts the
|
|
67
|
+
parameter). The agent fills it in; your tool handler never sees it — it is
|
|
68
|
+
stripped from the arguments before your code runs.
|
|
69
|
+
2. **Captures every tool call**: tool name, redacted arguments, redacted result,
|
|
70
|
+
error state + message, duration, and the reported intent. Each captured field
|
|
71
|
+
is size-bounded (oversized or unserializable values become a small marker) so a
|
|
72
|
+
multi-MB tool result can never blow the ingest body limit or grow the buffer.
|
|
73
|
+
3. **Adds a reserved `mcpeye_request_capability` tool** (active missing-capability
|
|
74
|
+
capture). When the agent wants a capability none of your tools cover, it can
|
|
75
|
+
call this tool to say so in the user's words. mcpeye answers it locally with a
|
|
76
|
+
canned acknowledgement — it is **never** forwarded to your server — and records
|
|
77
|
+
it as a normal tool call with `tool_name = "mcpeye_request_capability"`, which
|
|
78
|
+
the report folds into "Top missing capabilities" as a high-confidence,
|
|
79
|
+
explicitly-requested entry. This catches the *silent miss*, where the right move
|
|
80
|
+
is to call no tool at all. Disable with `capture_missing_capabilities=False`.
|
|
81
|
+
4. **Ships** batches as the shared `IngestPayload` JSON to `<ingest_url>/ingest`
|
|
82
|
+
with the `x-mcpeye-secret` header, using `httpx`. Shipping happens on a
|
|
83
|
+
background daemon thread — **never on the tool-call thread** — so a slow or
|
|
84
|
+
unreachable ingest endpoint adds no latency to your tools. It flushes on a timer
|
|
85
|
+
(default every 5s), eagerly when the batch fills (`flush_at`), and a final time
|
|
86
|
+
at process exit. Shipping is best-effort: ingest failures are swallowed (routed
|
|
87
|
+
to `on_error`) and retried; analytics can never take down your MCP server.
|
|
88
|
+
|
|
89
|
+
> **Strict ingest URL.** Unlike the TypeScript SDK (which defaults to
|
|
90
|
+
> `http://localhost:3001`), the Python SDK raises `ValueError` at setup time if no
|
|
91
|
+
> ingest URL is configured — there is no implicit localhost default, so a
|
|
92
|
+
> misconfigured server fails loudly at your call site instead of silently dropping
|
|
93
|
+
> telemetry into a dead port.
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
| Argument | Env fallback | Default |
|
|
98
|
+
| ------------------ | ----------------------- | ------------------------------- |
|
|
99
|
+
| `ingest_url` | `MCPEYE_INGEST_URL` | — (required, raises if unset) |
|
|
100
|
+
| `ingest_secret` | `MCPEYE_INGEST_SECRET` | none |
|
|
101
|
+
| `redact` | — | `True` |
|
|
102
|
+
| `user_id` | — | none |
|
|
103
|
+
| `client` | — | none |
|
|
104
|
+
| `server_version` | — | `server.version` when available |
|
|
105
|
+
| `flush_at` | — | `20` buffered events |
|
|
106
|
+
| `flush_interval_s` | — | `5.0` seconds (background timer) |
|
|
107
|
+
| `denylist_fields` | — | none (adds to the built-in denylist) |
|
|
108
|
+
| `capture_missing_capabilities` | — | `True` (inject `mcpeye_request_capability`) |
|
|
109
|
+
| `on_error` | — | debug log on the `mcpeye` logger |
|
|
110
|
+
|
|
111
|
+
> **Manifest cost.** With `capture_missing_capabilities=True`, your server's
|
|
112
|
+
> `tools/list` gains one extra tool — a few hundred tokens in any model context
|
|
113
|
+
> that lists tools, and one more entry in any tool picker / doc generator. That is
|
|
114
|
+
> the price of seeing silent misses; pass `False` to keep it out of the manifest.
|
|
115
|
+
|
|
116
|
+
### Diagnostics
|
|
117
|
+
|
|
118
|
+
Every swallowed error (transport failure, capture failure) is routed to `on_error`,
|
|
119
|
+
which defaults to a `debug`-level log on `logging.getLogger("mcpeye")`. Pass your
|
|
120
|
+
own sink to see why telemetry might be silent — it is wrapped so it can never throw
|
|
121
|
+
back into your server:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
mcpeye.track(server, "proj_123", ingest_url="http://localhost:3001",
|
|
125
|
+
on_error=lambda err: print("mcpeye:", err))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Redaction
|
|
129
|
+
|
|
130
|
+
When `redact=True` (the default), arguments, results, and the reported intent are
|
|
131
|
+
scrubbed **client-side before anything leaves your process**. The v1 redaction is
|
|
132
|
+
a conservative regex pass — it over-redacts rather than leak — covering:
|
|
133
|
+
|
|
134
|
+
- emails
|
|
135
|
+
- API keys: `sk-…`, `sk-ant-…`, `ghp_…`/`gho_…`/etc., `AKIA…`
|
|
136
|
+
- `Bearer …` tokens and JWTs
|
|
137
|
+
- credit-card-shaped digit runs
|
|
138
|
+
- loose international phone numbers
|
|
139
|
+
- a field-name denylist (`password`, `secret`, `token`, `apiKey`, `authorization`, …)
|
|
140
|
+
|
|
141
|
+
Self-hosting is the real privacy mitigation; redaction reduces the blast radius
|
|
142
|
+
of obvious secrets in free-text. You can use the primitives directly:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from mcpeye import redact_string, redact_value
|
|
146
|
+
|
|
147
|
+
redact_string("ping me at a@b.com") # -> "ping me at [REDACTED_EMAIL]"
|
|
148
|
+
redact_value({"password": "hunter2"}) # -> {"password": "[REDACTED_FIELD]"}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Manual instrumentation (`wrap_tool`)
|
|
152
|
+
|
|
153
|
+
The `mcp` package's `Server` internals vary across versions. `track` attaches to
|
|
154
|
+
`server.request_handlers`; if that layout ever changes, `track` raises a clear
|
|
155
|
+
error and you can fall back to instrumenting individual tool handlers:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import mcpeye
|
|
159
|
+
|
|
160
|
+
@mcpeye.wrap_tool(project_id="proj_123", tool_name="search",
|
|
161
|
+
ingest_url="http://localhost:3001")
|
|
162
|
+
async def search(arguments):
|
|
163
|
+
...
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`wrap_tool` pulls `mcpeyeIntent` out of the arguments, times the call, captures
|
|
167
|
+
the outcome, and ships it — the same capture path as `track`, scoped to one tool.
|
|
168
|
+
It accepts `redact`, `ingest_url`, `ingest_secret`, `denylist_fields`, `flush_at`,
|
|
169
|
+
`flush_interval_s`, and `on_error` (not `user_id`/`client`/`server_version`, which
|
|
170
|
+
are server-level identity). Note: it does not inject the parameter into a published
|
|
171
|
+
schema, so add `mcpeyeIntent` to that tool's `inputSchema` yourself (see
|
|
172
|
+
`mcpeye.intent_param_json_schema`) if you want agents to populate it.
|
|
173
|
+
|
|
174
|
+
## Development
|
|
175
|
+
|
|
176
|
+
Unit tests stub the `mcp` package, so they run with only `httpx`, `pydantic`, and
|
|
177
|
+
`pytest` installed (no `mcp`):
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
python3 -m venv .venv && .venv/bin/pip install httpx pydantic pytest
|
|
181
|
+
.venv/bin/pip install -e packages/sdk-python --no-deps
|
|
182
|
+
.venv/bin/python -m pytest packages/sdk-python/tests -q
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcpeye/sdk-python",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "mcpeye Python SDK (distribution name: mcpeye). turbo wrapper — the real build is via pyproject.toml.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "true",
|
|
8
|
+
"typecheck": "true",
|
|
9
|
+
"clean": "true"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcpeye"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "mcpeye Python SDK — open-source product analytics for MCP servers. See why your agent is failing."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "mcpeye" }]
|
|
13
|
+
keywords = ["mcp", "model-context-protocol", "mcp-server", "mcp-analytics", "product-analytics", "agent-analytics", "ai-agents", "observability", "session-replay", "self-hosted", "llm", "mcpeye"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
]
|
|
22
|
+
# `mcp` is the USER's server framework, not something mcpeye needs at runtime: it
|
|
23
|
+
# is imported lazily inside track(), and the SDK's own code is 3.9-clean. Keeping
|
|
24
|
+
# it OUT of the hard deps means `pip install mcpeye` works on 3.9+ and the unit
|
|
25
|
+
# tests run with no `mcp` installed (they stub the Server shape). Servers that use
|
|
26
|
+
# the auto-`track` path install `mcpeye[mcp]` (mcp itself requires Python >=3.10).
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.27",
|
|
29
|
+
"pydantic>=2.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
mcp = ["mcp>=1.0.0"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/mcpeye/mcpeye"
|
|
37
|
+
Repository = "https://github.com/mcpeye/mcpeye"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
# `py.typed` ships automatically because it lives inside the packaged dir.
|
|
41
|
+
packages = ["src/mcpeye"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""mcpeye -- open-source intent-gap analytics for MCP servers.
|
|
2
|
+
|
|
3
|
+
See why your agent is failing.
|
|
4
|
+
|
|
5
|
+
Public API::
|
|
6
|
+
|
|
7
|
+
import mcpeye
|
|
8
|
+
|
|
9
|
+
mcpeye.track(server, "proj_123", ingest_url="http://localhost:3001")
|
|
10
|
+
|
|
11
|
+
Falls back to :func:`mcpeye.wrap_tool` for per-tool instrumentation when the
|
|
12
|
+
automatic ``track`` hook cannot attach to your ``Server``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .intent import (
|
|
18
|
+
INTENT_PARAM_DESCRIPTION,
|
|
19
|
+
INTENT_PARAM_NAME,
|
|
20
|
+
inject_intent_param,
|
|
21
|
+
intent_param_json_schema,
|
|
22
|
+
)
|
|
23
|
+
from .redaction import redact_string, redact_value
|
|
24
|
+
from .request_capability import (
|
|
25
|
+
REQUEST_CAPABILITY_ACK,
|
|
26
|
+
REQUEST_CAPABILITY_TOOL_DESCRIPTION,
|
|
27
|
+
REQUEST_CAPABILITY_TOOL_NAME,
|
|
28
|
+
request_capability_descriptor,
|
|
29
|
+
request_capability_input_schema,
|
|
30
|
+
)
|
|
31
|
+
from .tracker import track, wrap_tool
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"track",
|
|
37
|
+
"wrap_tool",
|
|
38
|
+
"redact_string",
|
|
39
|
+
"redact_value",
|
|
40
|
+
"INTENT_PARAM_NAME",
|
|
41
|
+
"INTENT_PARAM_DESCRIPTION",
|
|
42
|
+
"intent_param_json_schema",
|
|
43
|
+
"inject_intent_param",
|
|
44
|
+
"REQUEST_CAPABILITY_TOOL_NAME",
|
|
45
|
+
"REQUEST_CAPABILITY_TOOL_DESCRIPTION",
|
|
46
|
+
"REQUEST_CAPABILITY_ACK",
|
|
47
|
+
"request_capability_input_schema",
|
|
48
|
+
"request_capability_descriptor",
|
|
49
|
+
"__version__",
|
|
50
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""The injected-intent contract.
|
|
2
|
+
|
|
3
|
+
mcpeye's cheap capture trick: the SDK injects an optional ``mcpeyeIntent``
|
|
4
|
+
parameter into every tool's input schema. The agent self-reports, in its own
|
|
5
|
+
words, why it is calling the tool and any blocker the user hit -- so we capture
|
|
6
|
+
intent at near-zero cost, with NO per-call LLM. The LLM runs later, in the
|
|
7
|
+
worker, only to cluster sessions into reports.
|
|
8
|
+
|
|
9
|
+
The description below is what the agent reads. It is deliberately specific about
|
|
10
|
+
surfacing failures/blockers AND naming any capability the user wanted that no tool
|
|
11
|
+
provides -- those attempted-but-failed asks, phrased as the user's own unmet need,
|
|
12
|
+
are the hero signal (the Intent Gap Report) and the highest-value, most roadmap-
|
|
13
|
+
actionable thing this whole product captures.
|
|
14
|
+
|
|
15
|
+
This is the Python port of ``@mcpeye/core``'s ``intent.ts``; the strings MUST
|
|
16
|
+
match byte-for-byte across every SDK. ``tests/test_intent.py`` asserts this.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict
|
|
22
|
+
|
|
23
|
+
INTENT_PARAM_NAME = "mcpeyeIntent"
|
|
24
|
+
|
|
25
|
+
# Byte-for-byte identical to packages/core/src/intent.ts INTENT_PARAM_DESCRIPTION.
|
|
26
|
+
# If you change one, change all SDKs (TS, Python, Ruby) together.
|
|
27
|
+
INTENT_PARAM_DESCRIPTION = (
|
|
28
|
+
"Explain why you are calling this tool and how it fits into the user's overall workflow. "
|
|
29
|
+
"This parameter is used only for product analytics and user-intent tracking. "
|
|
30
|
+
"Write 25-35 words, in the third person. "
|
|
31
|
+
"Exclude sensitive information such as credentials, passwords, or personal data. "
|
|
32
|
+
"Describe any blocker or failure the user hit. "
|
|
33
|
+
"Most important: if the user wanted to do something these tools cannot do, state the missing "
|
|
34
|
+
"capability they needed, in their own words (for example: 'wanted to export the report as CSV, "
|
|
35
|
+
"but no export tool exists')."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# JSON-Schema fragment merged into each tool's inputSchema by the SDKs.
|
|
39
|
+
intent_param_json_schema: Dict[str, Any] = {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": INTENT_PARAM_DESCRIPTION,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def inject_intent_param(input_schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
46
|
+
"""Return a copy of a JSON-Schema object with the ``mcpeyeIntent`` property merged in.
|
|
47
|
+
|
|
48
|
+
Non-destructive: the original schema is left untouched (``schema`` and
|
|
49
|
+
``properties`` are copied). Behaviour, matching the TS reference
|
|
50
|
+
``augmentListToolsResult``:
|
|
51
|
+
|
|
52
|
+
- Object-shaped schema (``type == "object"`` or has ``properties``): merge in
|
|
53
|
+
``mcpeyeIntent`` and default ``type`` to ``"object"`` when absent.
|
|
54
|
+
- Empty / no-type schema (``{}`` or a tool with no usable input schema): treat
|
|
55
|
+
as an object and synthesize ``{"type": "object", "properties": {mcpeyeIntent}}``
|
|
56
|
+
so a parameterless tool still advertises the intent param.
|
|
57
|
+
- A schema with an explicit non-object ``type`` (e.g. ``{"type": "array"}``):
|
|
58
|
+
returned unchanged -- there is no sensible place to add the parameter, and
|
|
59
|
+
capture still works without it.
|
|
60
|
+
- When ``mcpeyeIntent`` already exists as a property (a tool owns the name), the
|
|
61
|
+
schema is returned with that property left exactly as the tool declared it.
|
|
62
|
+
"""
|
|
63
|
+
if not isinstance(input_schema, dict):
|
|
64
|
+
return input_schema
|
|
65
|
+
|
|
66
|
+
explicit_type = input_schema.get("type")
|
|
67
|
+
is_object = (
|
|
68
|
+
explicit_type == "object"
|
|
69
|
+
or "properties" in input_schema
|
|
70
|
+
or (explicit_type is None and len(input_schema) == 0)
|
|
71
|
+
)
|
|
72
|
+
if not is_object:
|
|
73
|
+
return input_schema
|
|
74
|
+
|
|
75
|
+
schema = dict(input_schema)
|
|
76
|
+
properties = dict(schema.get("properties") or {})
|
|
77
|
+
# Do not clobber a real tool param that happens to share the name.
|
|
78
|
+
if INTENT_PARAM_NAME not in properties:
|
|
79
|
+
properties[INTENT_PARAM_NAME] = dict(intent_param_json_schema)
|
|
80
|
+
schema["properties"] = properties
|
|
81
|
+
# Synthesize the object type for an empty / typeless schema (TS parity).
|
|
82
|
+
schema.setdefault("type", "object")
|
|
83
|
+
return schema
|
|
File without changes
|