@posthog/mcp 0.14.1 → 0.16.0
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.
- package/LICENSE +64 -0
- package/README.md +64 -0
- package/dist/extensions/constants.d.ts +9 -0
- package/dist/extensions/constants.d.ts.map +1 -1
- package/dist/extensions/constants.js +9 -0
- package/dist/extensions/constants.mjs +9 -0
- package/dist/extensions/event-types.d.ts +1 -0
- package/dist/extensions/event-types.d.ts.map +1 -1
- package/dist/extensions/event-types.js +1 -0
- package/dist/extensions/event-types.mjs +1 -0
- package/dist/extensions/feedback.d.ts +41 -0
- package/dist/extensions/feedback.d.ts.map +1 -0
- package/dist/extensions/feedback.js +259 -0
- package/dist/extensions/feedback.mjs +200 -0
- package/dist/extensions/instrument-highlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-highlevel.js +23 -2
- package/dist/extensions/instrument-highlevel.mjs +25 -4
- package/dist/extensions/instrument-lowlevel.d.ts +4 -3
- package/dist/extensions/instrument-lowlevel.d.ts.map +1 -1
- package/dist/extensions/instrument-lowlevel.js +23 -2
- package/dist/extensions/instrument-lowlevel.mjs +25 -4
- package/dist/extensions/instrumentation.d.ts +33 -13
- package/dist/extensions/instrumentation.d.ts.map +1 -1
- package/dist/extensions/instrumentation.js +103 -9
- package/dist/extensions/instrumentation.mjs +99 -8
- package/dist/extensions/internal.js +9 -2
- package/dist/extensions/internal.mjs +9 -2
- package/dist/extensions/mcp-payloads.d.ts +19 -0
- package/dist/extensions/mcp-payloads.d.ts.map +1 -1
- package/dist/extensions/mcp-payloads.js +217 -7
- package/dist/extensions/mcp-payloads.mjs +210 -6
- package/dist/extensions/posthog-events.d.ts.map +1 -1
- package/dist/extensions/posthog-events.js +1 -0
- package/dist/extensions/posthog-events.mjs +1 -0
- package/dist/extensions/posthog-mcp.d.ts +23 -3
- package/dist/extensions/posthog-mcp.d.ts.map +1 -1
- package/dist/extensions/posthog-mcp.js +27 -1
- package/dist/extensions/posthog-mcp.mjs +27 -1
- package/dist/extensions/sanitization.d.ts.map +1 -1
- package/dist/extensions/sanitization.js +2 -1
- package/dist/extensions/sanitization.mjs +3 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.mjs +5 -1
- package/dist/types.d.ts +114 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +4 -4
- package/src/extensions/constants.ts +9 -0
- package/src/extensions/event-types.ts +1 -0
- package/src/extensions/feedback.ts +349 -0
- package/src/extensions/instrument-highlevel.ts +45 -2
- package/src/extensions/instrument-lowlevel.ts +49 -5
- package/src/extensions/instrumentation.ts +170 -17
- package/src/extensions/internal.ts +13 -2
- package/src/extensions/mcp-payloads.ts +443 -7
- package/src/extensions/posthog-events.ts +1 -0
- package/src/extensions/posthog-mcp.ts +74 -3
- package/src/extensions/sanitization.ts +14 -6
- package/src/index.ts +24 -3
- package/src/types.ts +120 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CollectFeedbackOptions,
|
|
3
|
+
FeedbackExtraPropertySchema,
|
|
4
|
+
FeedbackReport,
|
|
5
|
+
FeedbackSentiment,
|
|
6
|
+
FeedbackType,
|
|
7
|
+
CollectFeedbackConfig,
|
|
8
|
+
CompatibleTextToolResult,
|
|
9
|
+
CompatibleToolsListLike,
|
|
10
|
+
JsonRecord,
|
|
11
|
+
} from '../types'
|
|
12
|
+
import { PostHogMCPAnalyticsProperty } from './constants'
|
|
13
|
+
import { log, type LoggerFn } from './logger'
|
|
14
|
+
import { sanitizeFreeText, sanitizeFreeTextValue } from './mcp-payloads'
|
|
15
|
+
|
|
16
|
+
export const SEND_FEEDBACK_TOOL_NAME = 'send_feedback' as const
|
|
17
|
+
|
|
18
|
+
const FEEDBACK_TYPES: readonly FeedbackType[] = ['missing_capability', 'issue', 'praise', 'other']
|
|
19
|
+
const SENTIMENTS: readonly FeedbackSentiment[] = ['positive', 'neutral', 'negative', 'mixed']
|
|
20
|
+
|
|
21
|
+
// Free-text fields are agent-narrated, like `$mcp_intent`; bound them the same way.
|
|
22
|
+
const MAX_FEEDBACK_TEXT_LENGTH = 2048
|
|
23
|
+
const MAX_FEEDBACK_TOOL_NAME_LENGTH = 256
|
|
24
|
+
const TRUNCATION_SUFFIX = '...'
|
|
25
|
+
|
|
26
|
+
const DEFAULT_FEEDBACK_DESCRIPTION =
|
|
27
|
+
'Send feedback about this server to its developers. Most important: report a missing capability whenever no ' +
|
|
28
|
+
'available tool fits your task, even if you can work around it (feedback_type "missing_capability"). Also ' +
|
|
29
|
+
'welcome: a tool that failed or confused you, an unhelpful error, or something that worked well. This records ' +
|
|
30
|
+
'the feedback; it does not add or change tools. Do not include user PII or sensitive content.'
|
|
31
|
+
|
|
32
|
+
const CORE_FEEDBACK_SCHEMA_PROPERTIES = {
|
|
33
|
+
feedback_type: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: [...FEEDBACK_TYPES],
|
|
36
|
+
description:
|
|
37
|
+
"What kind of feedback this is. Use 'missing_capability' when the tool you needed does not exist in the " +
|
|
38
|
+
'tool list - nothing failed, the capability is absent (this is the most valuable report; send it even if ' +
|
|
39
|
+
"you found a workaround). Use 'issue' when an existing tool behaved badly: it failed, returned a confusing " +
|
|
40
|
+
"error, or its description or schema misled you. Use 'praise' when something worked notably well. Use " +
|
|
41
|
+
"'other' for anything else.",
|
|
42
|
+
},
|
|
43
|
+
summary: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description:
|
|
46
|
+
"One self-contained sentence. For 'missing_capability': the capability you needed, e.g. 'No tool to " +
|
|
47
|
+
"delete multiple cohorts in one call.' For 'issue': the tool and the problem, e.g. 'query-trends rejects " +
|
|
48
|
+
"relative date ranges with an unclear error.'",
|
|
49
|
+
},
|
|
50
|
+
details: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description:
|
|
53
|
+
'Optional longer context: what you tried, exact parameter values, the error text you saw, and any ' +
|
|
54
|
+
'workaround you used. Omit when the summary says it all.',
|
|
55
|
+
},
|
|
56
|
+
friction_points: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description:
|
|
59
|
+
'Optional: the specific moments that slowed you down, as short bullet-like sentences, quoting exact tool ' +
|
|
60
|
+
'names, parameters, or error text.',
|
|
61
|
+
},
|
|
62
|
+
suggested_improvement: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description:
|
|
65
|
+
'Optional: the concrete change that would have helped, e.g. the tool to add, the description to reword, ' +
|
|
66
|
+
'or the error message to improve.',
|
|
67
|
+
},
|
|
68
|
+
tool_name: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description:
|
|
71
|
+
"Optional: the existing tool this feedback is about (for 'issue' or 'praise'). Leave empty for " +
|
|
72
|
+
"'missing_capability' - the point is that no tool fits.",
|
|
73
|
+
},
|
|
74
|
+
sentiment: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
enum: [...SENTIMENTS],
|
|
77
|
+
description: 'Optional: how the experience felt overall.',
|
|
78
|
+
},
|
|
79
|
+
task_completed: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
description: "Optional: whether you still completed the user's task despite the problem.",
|
|
82
|
+
},
|
|
83
|
+
} as const
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Extra-property names a host may not declare: the core fields themselves, the
|
|
87
|
+
* names whose `$mcp_feedback_<key>` property would collide with a core property
|
|
88
|
+
* (`type` → `$mcp_feedback_type`, `tool` → `$mcp_feedback_tool`), and the
|
|
89
|
+
* SDK-injected analytics arguments — the report is parsed from the raw
|
|
90
|
+
* arguments before those are stripped, so an extra by the same name would
|
|
91
|
+
* capture an SDK-owned value.
|
|
92
|
+
*/
|
|
93
|
+
const RESERVED_EXTRA_PROPERTY_KEYS = new Set([
|
|
94
|
+
...Object.keys(CORE_FEEDBACK_SCHEMA_PROPERTIES),
|
|
95
|
+
'type',
|
|
96
|
+
'tool',
|
|
97
|
+
'context',
|
|
98
|
+
'conversation_id',
|
|
99
|
+
'llm_model',
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
type FeedbackToolDescriptor = CompatibleToolsListLike['tools'][number]
|
|
103
|
+
|
|
104
|
+
/** `collectFeedback` normalized to its object form; `undefined` when the feature is off. */
|
|
105
|
+
export function resolveCollectFeedbackOptions(
|
|
106
|
+
config: CollectFeedbackConfig | undefined
|
|
107
|
+
): CollectFeedbackOptions | undefined {
|
|
108
|
+
if (!config) {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
return config === true ? {} : config
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The advertised descriptor: the core feedback schema plus the host's declared
|
|
116
|
+
* `extraProperties`. Throws on a config error (a reserved extra key, or an
|
|
117
|
+
* `extraRequired` entry that was never declared) so a bad setup fails at
|
|
118
|
+
* configuration time instead of silently corrupting the advertised schema.
|
|
119
|
+
*/
|
|
120
|
+
export function getFeedbackToolDescriptor(options: CollectFeedbackOptions = {}): FeedbackToolDescriptor {
|
|
121
|
+
const extraProperties = options.extraProperties ?? {}
|
|
122
|
+
const extraRequired = options.extraRequired ?? []
|
|
123
|
+
|
|
124
|
+
for (const key of Object.keys(extraProperties)) {
|
|
125
|
+
if (RESERVED_EXTRA_PROPERTY_KEYS.has(key)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`collectFeedback.extraProperties key "${key}" collides with a core or SDK-reserved send_feedback field. Rename it.`
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const key of extraRequired) {
|
|
132
|
+
if (!Object.prototype.hasOwnProperty.call(extraProperties, key)) {
|
|
133
|
+
throw new Error(`collectFeedback.extraRequired key "${key}" is not declared in extraProperties.`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Deep copy: the host may reuse or mutate the fragments it handed us.
|
|
138
|
+
const copiedExtras =
|
|
139
|
+
Object.keys(extraProperties).length > 0
|
|
140
|
+
? (JSON.parse(JSON.stringify(extraProperties)) as Record<string, unknown>)
|
|
141
|
+
: undefined
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
name: options.toolName ?? SEND_FEEDBACK_TOOL_NAME,
|
|
145
|
+
description: options.description ?? DEFAULT_FEEDBACK_DESCRIPTION,
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: {
|
|
149
|
+
...(JSON.parse(JSON.stringify(CORE_FEEDBACK_SCHEMA_PROPERTIES)) as Record<string, unknown>),
|
|
150
|
+
...copiedExtras,
|
|
151
|
+
},
|
|
152
|
+
required: ['feedback_type', 'summary', ...extraRequired],
|
|
153
|
+
},
|
|
154
|
+
annotations: {
|
|
155
|
+
title: 'Send feedback',
|
|
156
|
+
readOnlyHint: true,
|
|
157
|
+
// Interacts with an external entity: the report lands in analytics.
|
|
158
|
+
openWorldHint: true,
|
|
159
|
+
// Only records the feedback, so repeat calls are harmless — and advertising
|
|
160
|
+
// it as idempotent makes agents more willing to call it proactively.
|
|
161
|
+
idempotentHint: true,
|
|
162
|
+
destructiveHint: false,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readString(value: unknown): string | undefined {
|
|
168
|
+
return typeof value === 'string' && value.trim() ? value : undefined
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parseFeedbackType(value: unknown): FeedbackType {
|
|
172
|
+
return FEEDBACK_TYPES.includes(value as FeedbackType) ? (value as FeedbackType) : 'other'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseSentiment(value: unknown): FeedbackSentiment | undefined {
|
|
176
|
+
return SENTIMENTS.includes(value as FeedbackSentiment) ? (value as FeedbackSentiment) : undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* True when the value conforms to the declared fragment's `type` and `enum` —
|
|
181
|
+
* the same advisory-schema enforcement the core fields get, so `extras` only
|
|
182
|
+
* ever holds schema-conforming values and a misbehaving agent shows up as
|
|
183
|
+
* absence rather than as an unexpected shape in the host's handler.
|
|
184
|
+
*/
|
|
185
|
+
function matchesExtraSchema(value: unknown, schema: FeedbackExtraPropertySchema): boolean {
|
|
186
|
+
const type = Array.isArray(value) ? 'array' : value === null ? 'null' : typeof value
|
|
187
|
+
if (schema.type !== type && !(schema.type === 'integer' && typeof value === 'number')) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
return !Array.isArray(schema.enum) || schema.enum.includes(value as string)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Parses the raw `send_feedback` arguments into a typed report. Never throws:
|
|
195
|
+
* an invalid `feedback_type` falls back to `other`, missing fields stay
|
|
196
|
+
* undefined, and only **declared** extras whose values match their declared
|
|
197
|
+
* `type`/`enum` are lifted into `extras` — mismatches and anything the agent
|
|
198
|
+
* invented reach the handler via `raw` only and are never captured.
|
|
199
|
+
*/
|
|
200
|
+
export function parseFeedbackReport(
|
|
201
|
+
args: Record<string, unknown> | undefined,
|
|
202
|
+
options: CollectFeedbackOptions = {}
|
|
203
|
+
): FeedbackReport {
|
|
204
|
+
const raw = args ?? {}
|
|
205
|
+
const extras: JsonRecord = {}
|
|
206
|
+
for (const [key, schema] of Object.entries(options.extraProperties ?? {})) {
|
|
207
|
+
if (raw[key] !== undefined && matchesExtraSchema(raw[key], schema)) {
|
|
208
|
+
extras[key] = raw[key]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
feedbackType: parseFeedbackType(raw.feedback_type),
|
|
213
|
+
summary: readString(raw.summary) ?? '',
|
|
214
|
+
sentiment: parseSentiment(raw.sentiment),
|
|
215
|
+
frictionPoints: readString(raw.friction_points),
|
|
216
|
+
suggestedImprovement: readString(raw.suggested_improvement),
|
|
217
|
+
details: readString(raw.details),
|
|
218
|
+
toolName: readString(raw.tool_name),
|
|
219
|
+
taskCompleted: typeof raw.task_completed === 'boolean' ? raw.task_completed : undefined,
|
|
220
|
+
extras,
|
|
221
|
+
raw,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The report's free text, used as the event's `$mcp_intent`. */
|
|
226
|
+
export function buildFeedbackIntent(report: FeedbackReport): string {
|
|
227
|
+
return [report.summary, report.details].filter(Boolean).join('\n\n')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function truncateFeedbackText(value: string, maxLength: number): string {
|
|
231
|
+
return value.length > maxLength ? value.slice(0, maxLength) + TRUNCATION_SUFFIX : value
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Agent-narrated free text can contain a secret the LLM read aloud or personal
|
|
236
|
+
* data it narrated, so it gets exactly the `$mcp_intent` pass (`sanitizeFreeText`:
|
|
237
|
+
* credentials → structured PII → URLs — the order is load-bearing, the URL
|
|
238
|
+
* rewrite would percent-encode the `@` the email pattern anchors on), then a
|
|
239
|
+
* length bound. The event-level pipeline does not process `event.properties`,
|
|
240
|
+
* so this happens here.
|
|
241
|
+
*/
|
|
242
|
+
function captureFreeText(value: string): string {
|
|
243
|
+
return truncateFeedbackText(sanitizeFreeText(value), MAX_FEEDBACK_TEXT_LENGTH)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* A declared extra is agent-supplied like the core free-text fields, so its
|
|
248
|
+
* string leaves get the same intent-grade pass (with the key-based redaction
|
|
249
|
+
* `sanitizeFreeTextValue` keeps for nested objects), then non-scalars are
|
|
250
|
+
* JSON-stringified and everything is bounded.
|
|
251
|
+
*/
|
|
252
|
+
function captureExtraValue(value: unknown): unknown {
|
|
253
|
+
const sanitized = sanitizeFreeTextValue(value)
|
|
254
|
+
if (typeof sanitized === 'string') {
|
|
255
|
+
return truncateFeedbackText(sanitized, MAX_FEEDBACK_TEXT_LENGTH)
|
|
256
|
+
}
|
|
257
|
+
if (sanitized == null || typeof sanitized === 'number' || typeof sanitized === 'boolean') {
|
|
258
|
+
return sanitized
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
return truncateFeedbackText(JSON.stringify(sanitized), MAX_FEEDBACK_TEXT_LENGTH)
|
|
262
|
+
} catch {
|
|
263
|
+
return undefined
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The `$mcp_feedback_*` event properties for one report, declared extras included. */
|
|
268
|
+
export function buildFeedbackEventProperties(report: FeedbackReport): JsonRecord {
|
|
269
|
+
const properties: JsonRecord = {
|
|
270
|
+
[PostHogMCPAnalyticsProperty.FeedbackType]: report.feedbackType,
|
|
271
|
+
}
|
|
272
|
+
if (report.summary) {
|
|
273
|
+
properties[PostHogMCPAnalyticsProperty.FeedbackSummary] = captureFreeText(report.summary)
|
|
274
|
+
}
|
|
275
|
+
if (report.sentiment) {
|
|
276
|
+
properties[PostHogMCPAnalyticsProperty.FeedbackSentiment] = report.sentiment
|
|
277
|
+
}
|
|
278
|
+
if (report.frictionPoints) {
|
|
279
|
+
properties[PostHogMCPAnalyticsProperty.FeedbackFrictionPoints] = captureFreeText(report.frictionPoints)
|
|
280
|
+
}
|
|
281
|
+
if (report.suggestedImprovement) {
|
|
282
|
+
properties[PostHogMCPAnalyticsProperty.FeedbackSuggestedImprovement] = captureFreeText(report.suggestedImprovement)
|
|
283
|
+
}
|
|
284
|
+
if (report.details) {
|
|
285
|
+
properties[PostHogMCPAnalyticsProperty.FeedbackDetails] = captureFreeText(report.details)
|
|
286
|
+
}
|
|
287
|
+
if (report.toolName) {
|
|
288
|
+
// Nominally an identifier, but the schema can't stop an agent from writing
|
|
289
|
+
// prose into it — so it gets the same intent-grade pass as the other free text.
|
|
290
|
+
properties[PostHogMCPAnalyticsProperty.FeedbackTool] = truncateFeedbackText(
|
|
291
|
+
sanitizeFreeText(report.toolName),
|
|
292
|
+
MAX_FEEDBACK_TOOL_NAME_LENGTH
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
if (report.taskCompleted !== undefined) {
|
|
296
|
+
properties[PostHogMCPAnalyticsProperty.FeedbackTaskCompleted] = report.taskCompleted
|
|
297
|
+
}
|
|
298
|
+
for (const [key, value] of Object.entries(report.extras)) {
|
|
299
|
+
const captured = captureExtraValue(value)
|
|
300
|
+
if (captured !== undefined) {
|
|
301
|
+
properties[`$mcp_feedback_${key}`] = captured
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return properties
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The default acknowledgement returned to the agent after it calls
|
|
309
|
+
* `send_feedback`. Reply with this from your dispatcher (custom dispatcher
|
|
310
|
+
* path); the `instrument()` path returns it automatically, or the string your
|
|
311
|
+
* `onFeedback` handler returned instead.
|
|
312
|
+
*/
|
|
313
|
+
export function sendFeedbackResult(): CompatibleTextToolResult {
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: 'text' as const,
|
|
318
|
+
text: "Your feedback was recorded for the server's developers. No additional tools are available - continue with the tools already listed.",
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Runs the host's `onFeedback` handler (when configured) and builds the reply.
|
|
326
|
+
* A returned non-blank string replaces the default acknowledgement; a thrown
|
|
327
|
+
* handler is logged and falls back to it — feedback capture must never break
|
|
328
|
+
* the agent's turn.
|
|
329
|
+
*/
|
|
330
|
+
export async function handleFeedback(
|
|
331
|
+
report: FeedbackReport,
|
|
332
|
+
options: CollectFeedbackOptions = {},
|
|
333
|
+
logger: LoggerFn = log
|
|
334
|
+
): Promise<CompatibleTextToolResult> {
|
|
335
|
+
// Only the type: the summary is agent-narrated free text (possible PII,
|
|
336
|
+
// newlines for log forging, unbounded length) and does not belong in host logs.
|
|
337
|
+
logger(`Agent feedback reported (${report.feedbackType})`)
|
|
338
|
+
if (options.onFeedback) {
|
|
339
|
+
try {
|
|
340
|
+
const reply = await options.onFeedback(report)
|
|
341
|
+
if (typeof reply === 'string' && reply.trim()) {
|
|
342
|
+
return { content: [{ type: 'text' as const, text: reply }] }
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
logger(`Warning: onFeedback handler threw; returning the default acknowledgement - ${error}`)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return sendFeedbackResult()
|
|
349
|
+
}
|
|
@@ -11,6 +11,15 @@ import type {
|
|
|
11
11
|
RegisteredTool,
|
|
12
12
|
ToolCallback,
|
|
13
13
|
} from '../types'
|
|
14
|
+
import {
|
|
15
|
+
buildFeedbackEventProperties,
|
|
16
|
+
buildFeedbackIntent,
|
|
17
|
+
getFeedbackToolDescriptor,
|
|
18
|
+
handleFeedback,
|
|
19
|
+
parseFeedbackReport,
|
|
20
|
+
resolveCollectFeedbackOptions,
|
|
21
|
+
SEND_FEEDBACK_TOOL_NAME,
|
|
22
|
+
} from './feedback'
|
|
14
23
|
import {
|
|
15
24
|
analyticsOwnsParameter,
|
|
16
25
|
getAnalyticsParameterOwnership,
|
|
@@ -22,10 +31,11 @@ import { MCPAnalyticsEventType } from './event-types'
|
|
|
22
31
|
import { getServerTrackingData } from './internal'
|
|
23
32
|
import type { LoggerFn } from './logger'
|
|
24
33
|
import { createWrappedTool, getToolFunction, hasToolFunction } from './mcp-sdk-compat'
|
|
25
|
-
import { handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
34
|
+
import { getReportMissingToolDescriptor, handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
26
35
|
import {
|
|
27
36
|
handleInitializeRequest,
|
|
28
37
|
handleListToolsRequest,
|
|
38
|
+
traceResourceRequest,
|
|
29
39
|
patchRequestHandlers,
|
|
30
40
|
captureToolCall,
|
|
31
41
|
getVirtualToolParameterOwnership,
|
|
@@ -250,11 +260,39 @@ async function handleToolCallRequest(
|
|
|
250
260
|
extra,
|
|
251
261
|
eventType: MCPAnalyticsEventType.mcpMissingCapability,
|
|
252
262
|
explicitContextIntent: context,
|
|
253
|
-
parameterOwnership: getVirtualToolParameterOwnership(
|
|
263
|
+
parameterOwnership: getVirtualToolParameterOwnership(
|
|
264
|
+
data,
|
|
265
|
+
toolName,
|
|
266
|
+
getReportMissingToolDescriptor(toolName).inputSchema
|
|
267
|
+
),
|
|
254
268
|
execute: async () => handleReportMissing({ context }, data.logger),
|
|
255
269
|
})
|
|
256
270
|
}
|
|
257
271
|
|
|
272
|
+
const feedbackOptions = resolveCollectFeedbackOptions(data.options.collectFeedback)
|
|
273
|
+
const isFeedbackCandidate =
|
|
274
|
+
feedbackOptions !== undefined && toolName === (feedbackOptions.toolName ?? SEND_FEEDBACK_TOOL_NAME)
|
|
275
|
+
|
|
276
|
+
if (isFeedbackCandidate && (await isToolAdvertised(server, toolName, extra, data.logger)) === false) {
|
|
277
|
+
const report = parseFeedbackReport(request.params?.arguments, feedbackOptions)
|
|
278
|
+
return await captureToolCall({
|
|
279
|
+
server,
|
|
280
|
+
data,
|
|
281
|
+
request,
|
|
282
|
+
extra,
|
|
283
|
+
eventType: MCPAnalyticsEventType.mcpFeedback,
|
|
284
|
+
explicitContextIntent: buildFeedbackIntent(report),
|
|
285
|
+
omitCapturedParameters: true,
|
|
286
|
+
extraEventProperties: buildFeedbackEventProperties(report),
|
|
287
|
+
parameterOwnership: getVirtualToolParameterOwnership(
|
|
288
|
+
data,
|
|
289
|
+
toolName,
|
|
290
|
+
getFeedbackToolDescriptor(feedbackOptions).inputSchema
|
|
291
|
+
),
|
|
292
|
+
execute: async () => handleFeedback(report, feedbackOptions, data.logger),
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
258
296
|
const registeredTool = toolName ? highLevelServer._registeredTools[toolName] : undefined
|
|
259
297
|
|
|
260
298
|
// Strip SDK-owned arguments before the MCP SDK validates the registered Zod
|
|
@@ -291,6 +329,11 @@ export function instrumentHighLevelServer(server: HighLevelMCPServerLike, logger
|
|
|
291
329
|
handleListToolsRequest(trackedServer, originalHandler, request, extra, logger),
|
|
292
330
|
'tools/call': (trackedServer, originalHandler, request, extra) =>
|
|
293
331
|
handleToolCallRequest(server, trackedServer, originalHandler, request, extra, logger),
|
|
332
|
+
'resources/list': traceResourceRequest(MCPAnalyticsEventType.mcpResourcesList, logger),
|
|
333
|
+
// Both listings publish `$mcp_resources_list`; the captured
|
|
334
|
+
// `request.method` is what tells a static listing from a templated one.
|
|
335
|
+
'resources/templates/list': traceResourceRequest(MCPAnalyticsEventType.mcpResourcesList, logger),
|
|
336
|
+
'resources/read': traceResourceRequest(MCPAnalyticsEventType.mcpResourcesRead, logger),
|
|
294
337
|
}
|
|
295
338
|
patchRequestHandlers(lowLevelServer, handlers)
|
|
296
339
|
|
|
@@ -4,13 +4,23 @@
|
|
|
4
4
|
// Licensed under the MIT License: https://github.com/agentcathq/agentcat-typescript-sdk/blob/main/LICENSE
|
|
5
5
|
|
|
6
6
|
import type { CompatibleRequestHandlerExtra, MCPRequestLike, MCPServerLike } from '../types'
|
|
7
|
+
import {
|
|
8
|
+
buildFeedbackEventProperties,
|
|
9
|
+
buildFeedbackIntent,
|
|
10
|
+
getFeedbackToolDescriptor,
|
|
11
|
+
handleFeedback,
|
|
12
|
+
parseFeedbackReport,
|
|
13
|
+
resolveCollectFeedbackOptions,
|
|
14
|
+
SEND_FEEDBACK_TOOL_NAME,
|
|
15
|
+
} from './feedback'
|
|
7
16
|
import { MCPAnalyticsEventType } from './event-types'
|
|
8
17
|
import { getServerTrackingData } from './internal'
|
|
9
18
|
import type { LoggerFn } from './logger'
|
|
10
|
-
import { handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
19
|
+
import { getReportMissingToolDescriptor, handleReportMissing, resolveMissingCapabilityToolName } from './tools'
|
|
11
20
|
import {
|
|
12
21
|
handleInitializeRequest,
|
|
13
22
|
handleListToolsRequest,
|
|
23
|
+
traceResourceRequest,
|
|
14
24
|
patchRequestHandlers,
|
|
15
25
|
registerFallbackRequestHandler,
|
|
16
26
|
captureToolCall,
|
|
@@ -27,9 +37,10 @@ type MCPRequest = Parameters<MCPRequestHandler>[0]
|
|
|
27
37
|
type MCPRequestExtra = Parameters<MCPRequestHandler>[1]
|
|
28
38
|
|
|
29
39
|
/**
|
|
30
|
-
* Instruments a low-level `Server`: wraps `initialize`, `tools/list`,
|
|
31
|
-
* `tools/call
|
|
32
|
-
*
|
|
40
|
+
* Instruments a low-level `Server`: wraps `initialize`, `tools/list`,
|
|
41
|
+
* `tools/call`, `resources/list`, `resources/templates/list`, and
|
|
42
|
+
* `resources/read`. The tool-call lifecycle is delegated to
|
|
43
|
+
* {@link captureToolCall}, shared with the high-level wrapper.
|
|
33
44
|
*/
|
|
34
45
|
export function instrumentLowLevelServer(server: MCPServerLike, logger: LoggerFn): void {
|
|
35
46
|
try {
|
|
@@ -42,6 +53,11 @@ export function instrumentLowLevelServer(server: MCPServerLike, logger: LoggerFn
|
|
|
42
53
|
'tools/list': (server, originalHandler, request, extra) =>
|
|
43
54
|
handleListToolsRequest(server, originalHandler, request, extra, logger),
|
|
44
55
|
'tools/call': traceToolCall,
|
|
56
|
+
'resources/list': traceResourceRequest(MCPAnalyticsEventType.mcpResourcesList, logger),
|
|
57
|
+
// Both listings publish `$mcp_resources_list`; the captured
|
|
58
|
+
// `request.method` is what tells a static listing from a templated one.
|
|
59
|
+
'resources/templates/list': traceResourceRequest(MCPAnalyticsEventType.mcpResourcesList, logger),
|
|
60
|
+
'resources/read': traceResourceRequest(MCPAnalyticsEventType.mcpResourcesRead, logger),
|
|
45
61
|
}
|
|
46
62
|
patchRequestHandlers(server, handlers)
|
|
47
63
|
|
|
@@ -91,11 +107,39 @@ async function handleToolCallRequest(
|
|
|
91
107
|
extra,
|
|
92
108
|
eventType: MCPAnalyticsEventType.mcpMissingCapability,
|
|
93
109
|
explicitContextIntent: context,
|
|
94
|
-
parameterOwnership: getVirtualToolParameterOwnership(
|
|
110
|
+
parameterOwnership: getVirtualToolParameterOwnership(
|
|
111
|
+
data,
|
|
112
|
+
toolName,
|
|
113
|
+
getReportMissingToolDescriptor(toolName).inputSchema
|
|
114
|
+
),
|
|
95
115
|
execute: async () => handleReportMissing({ context }, data.logger),
|
|
96
116
|
})
|
|
97
117
|
}
|
|
98
118
|
|
|
119
|
+
const feedbackOptions = resolveCollectFeedbackOptions(data.options.collectFeedback)
|
|
120
|
+
const isFeedbackCandidate =
|
|
121
|
+
feedbackOptions !== undefined && toolName === (feedbackOptions.toolName ?? SEND_FEEDBACK_TOOL_NAME)
|
|
122
|
+
|
|
123
|
+
if (isFeedbackCandidate && (await isToolAdvertised(server, toolName, extra, data.logger)) === false) {
|
|
124
|
+
const report = parseFeedbackReport(request.params?.arguments, feedbackOptions)
|
|
125
|
+
return await captureToolCall({
|
|
126
|
+
server,
|
|
127
|
+
data,
|
|
128
|
+
request,
|
|
129
|
+
extra,
|
|
130
|
+
eventType: MCPAnalyticsEventType.mcpFeedback,
|
|
131
|
+
explicitContextIntent: buildFeedbackIntent(report),
|
|
132
|
+
omitCapturedParameters: true,
|
|
133
|
+
extraEventProperties: buildFeedbackEventProperties(report),
|
|
134
|
+
parameterOwnership: getVirtualToolParameterOwnership(
|
|
135
|
+
data,
|
|
136
|
+
toolName,
|
|
137
|
+
getFeedbackToolDescriptor(feedbackOptions).inputSchema
|
|
138
|
+
),
|
|
139
|
+
execute: async () => handleFeedback(report, feedbackOptions, data.logger),
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
99
143
|
return await captureToolCall({
|
|
100
144
|
server,
|
|
101
145
|
data,
|