mcpeye 0.1.3 → 0.1.4
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.
- checksums.yaml +4 -4
- data/README.md +20 -3
- data/lib/mcpeye/tracker.rb +48 -6
- data/lib/mcpeye/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d50e6d30ef0e2cf6796b7b7b67397c40c11290f289152d124c3ff5417ff6150
|
|
4
|
+
data.tar.gz: 23729136fae41b35dcf3152354d59fd5e2753d7429831b1f474bc01de56c6d5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 84b70bcbed2a41a80a198b44e32b4f84eaa7f753aa3701e708eebc7210e78833dd6a1d67f471ac7e62aa7f1326dd96a75eaa9b3e8946465e78432e2554ee67e7
|
|
7
|
+
data.tar.gz: a75cc956872617ae3da0400b44b8a54a1f3e2bb7b265ca6e4dad5fb5ae50f453d24ec3256cfda971563091ff77f1a4b33059d6bfc73b378745530ffdefd015a4
|
data/README.md
CHANGED
|
@@ -165,9 +165,19 @@ MCPEYE = Mcpeye.track(
|
|
|
165
165
|
ENV.fetch("MCPEYE_PROJECT_ID"),
|
|
166
166
|
ingest_url: ENV.fetch("MCPEYE_INGEST_URL", "http://localhost:3001"),
|
|
167
167
|
ingest_secret: ENV["MCPEYE_INGEST_SECRET"],
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
|
|
168
|
+
# End-user identity. `userId`/`userEmail` are resolved PER CALL on the request
|
|
169
|
+
# thread (so a thread/request-local value like Rails Current / RequestStore is
|
|
170
|
+
# attributed correctly even on a multi-user / stateless server); client/serverVersion
|
|
171
|
+
# are read per flush. Pass an OPAQUE, stable userId. This is what powers the
|
|
172
|
+
# dashboard's search-by-id/email — without it, sessions read "user not identified".
|
|
173
|
+
identify: lambda {
|
|
174
|
+
{
|
|
175
|
+
userId: Current.user_id&.to_s, # who the end user is (for search)
|
|
176
|
+
userEmail: Current.user_email, # human-readable, optional (PII you store)
|
|
177
|
+
client: Current.client, # process/connection-level
|
|
178
|
+
serverVersion: MyApp::VERSION,
|
|
179
|
+
}
|
|
180
|
+
},
|
|
171
181
|
# Drop your own domain-sensitive fields on top of the built-in denylist:
|
|
172
182
|
denylist_fields: %w[ssn account_number],
|
|
173
183
|
# Forward diagnostics into your logger instead of stderr:
|
|
@@ -175,6 +185,13 @@ MCPEYE = Mcpeye.track(
|
|
|
175
185
|
)
|
|
176
186
|
```
|
|
177
187
|
|
|
188
|
+
> **Multi-user / stateless servers (e.g. a streamable-HTTP MCP).** Because `identify`
|
|
189
|
+
> runs per call on the request thread, set your per-request user context BEFORE the
|
|
190
|
+
> tool dispatches (e.g. in middleware: `RequestStore.store[:current_user_id] = ...`)
|
|
191
|
+
> and read it in `identify` (`RequestStore.store[:current_user_id]`). Each captured
|
|
192
|
+
> call is then attributed to the right user, even though one flushed batch may mix
|
|
193
|
+
> users.
|
|
194
|
+
|
|
178
195
|
### Puma / Unicorn (forking servers)
|
|
179
196
|
|
|
180
197
|
A thread does **not** survive `fork`, so in a clustered server start the flush
|
data/lib/mcpeye/tracker.rb
CHANGED
|
@@ -24,7 +24,12 @@ module Mcpeye
|
|
|
24
24
|
#
|
|
25
25
|
# The wire shape matches @mcpeye/core's IngestPayload exactly (and is
|
|
26
26
|
# byte-compatible with the TS and Python SDKs):
|
|
27
|
-
# { projectId, identity: { userId?, client?, serverVersion? },
|
|
27
|
+
# { projectId, identity: { userId?, userEmail?, client?, serverVersion? },
|
|
28
|
+
# events: [{ ..., userId?, userEmail? }] }
|
|
29
|
+
# Each event also carries its OWN userId/userEmail, resolved PER CALL on the
|
|
30
|
+
# request thread (see #resolve_call_identity) — so attribution is correct on a
|
|
31
|
+
# multi-user server where one flushed batch mixes users; the batch identity is the
|
|
32
|
+
# fallback.
|
|
28
33
|
#
|
|
29
34
|
# Prime directive: this NEVER raises into, or alters, the host MCP server. The
|
|
30
35
|
# only intentional raise is an empty `project_id` at construction (fail loud
|
|
@@ -182,10 +187,15 @@ module Mcpeye
|
|
|
182
187
|
# ingest_secret — shared secret sent as x-mcpeye-secret.
|
|
183
188
|
# Defaults to ENV["MCPEYE_INGEST_SECRET"].
|
|
184
189
|
# redact — when true (default) scrub arguments/result/intent/error client-side.
|
|
185
|
-
# identity — static Hash { userId:, client:, serverVersion: }
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
190
|
+
# identity — static Hash { userId:, userEmail:, client:, serverVersion: }
|
|
191
|
+
# (batch-level fallback).
|
|
192
|
+
# identify — optional callable for dynamic attribution. Evaluated PER CALL
|
|
193
|
+
# on the request thread for userId/userEmail (correct per-event
|
|
194
|
+
# attribution on a multi-user server) AND once per flush for the
|
|
195
|
+
# batch identity. Return { userId:, userEmail:, client:,
|
|
196
|
+
# serverVersion: }. A raising identify yields no identity for that
|
|
197
|
+
# call/flush, never breaks the host. Example:
|
|
198
|
+
# identify: -> { { userId: RequestStore.store[:current_user_id] } }
|
|
189
199
|
# flush_threshold — eager-flush buffer size (default 20).
|
|
190
200
|
# flush_interval — background flush interval in seconds (default nil = no
|
|
191
201
|
# background thread; stay zero-thread unless asked).
|
|
@@ -519,6 +529,14 @@ module Mcpeye
|
|
|
519
529
|
event["errorMessage"] = guard_error_message(error_message.to_s) unless error_message.nil?
|
|
520
530
|
# Only a non-blank intent is recorded (whitespace-only is dropped, TS/Python parity).
|
|
521
531
|
event["intent"] = guard_error_message(intent.to_s) if intent && !intent.to_s.strip.empty?
|
|
532
|
+
# Per-CALL end-user identity. build_event runs on the caller's (request) thread
|
|
533
|
+
# — via the official call_tool hook or the wrap proc — so `identify` is evaluated
|
|
534
|
+
# where the host's per-request user context is live (unlike the per-flush batch
|
|
535
|
+
# identity, which runs on the flush thread). This is what makes attribution
|
|
536
|
+
# correct on a multi-user server. Per-event values win over the batch identity.
|
|
537
|
+
uid, uemail = resolve_call_identity
|
|
538
|
+
event["userId"] = uid if uid
|
|
539
|
+
event["userEmail"] = uemail if uemail
|
|
522
540
|
event
|
|
523
541
|
end
|
|
524
542
|
|
|
@@ -649,6 +667,30 @@ module Mcpeye
|
|
|
649
667
|
{}
|
|
650
668
|
end
|
|
651
669
|
|
|
670
|
+
# Per-CALL end-user identity for ONE event, resolved on the caller's (request)
|
|
671
|
+
# thread. Uses `identify` (evaluated per call) when given, else the static
|
|
672
|
+
# `identity`. Returns [user_id, user_email] as normalized Strings or nils.
|
|
673
|
+
# Fail-open: a raising identify yields [nil, nil], never into the host call.
|
|
674
|
+
def resolve_call_identity
|
|
675
|
+
raw = @identify ? @identify.call : @identity
|
|
676
|
+
raw = {} unless raw.is_a?(Hash)
|
|
677
|
+
[norm_ident(raw[:userId] || raw["userId"]), norm_ident(raw[:userEmail] || raw["userEmail"])]
|
|
678
|
+
rescue StandardError => e
|
|
679
|
+
@on_error.call(e)
|
|
680
|
+
[nil, nil]
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Coerce an identity value to a trimmed String <=256 chars, or nil if blank.
|
|
684
|
+
# Coerces a non-String id (e.g. an Integer user id) like normalize_identity.
|
|
685
|
+
def norm_ident(value)
|
|
686
|
+
return nil if value.nil?
|
|
687
|
+
|
|
688
|
+
s = value.to_s.strip
|
|
689
|
+
return nil if s.empty?
|
|
690
|
+
|
|
691
|
+
s.length > 256 ? s[0, 256] : s
|
|
692
|
+
end
|
|
693
|
+
|
|
652
694
|
# Prepend a failed batch to the front of the buffer (oldest-first, so it
|
|
653
695
|
# retries first) and re-apply the cap. Caller must NOT hold @mutex.
|
|
654
696
|
def requeue(batch)
|
|
@@ -1057,7 +1099,7 @@ module Mcpeye
|
|
|
1057
1099
|
def normalize_identity(identity)
|
|
1058
1100
|
h = identity.is_a?(Hash) ? identity : {}
|
|
1059
1101
|
out = {}
|
|
1060
|
-
%w[userId client serverVersion].each do |k|
|
|
1102
|
+
%w[userId userEmail client serverVersion].each do |k|
|
|
1061
1103
|
v = h[k.to_sym]
|
|
1062
1104
|
v = h[k] if v.nil?
|
|
1063
1105
|
next if v.nil?
|
data/lib/mcpeye/version.rb
CHANGED