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.
@@ -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