@ema.co/mcp-toolkit 2026.4.9-1 → 2026.4.9-3

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.
@@ -85,6 +85,19 @@ export function scoreToLabel(score) {
85
85
  return "inferred";
86
86
  return "low-confidence";
87
87
  }
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+ // Fragment helpers — GitHub-style #fragment on knowledge_ref
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+ /** Strip #fragment from a knowledge_ref, returning the bare document ID. */
92
+ export function stripFragment(ref) {
93
+ const idx = ref.indexOf("#");
94
+ return idx === -1 ? ref : ref.slice(0, idx);
95
+ }
96
+ /** Extract the #fragment from a knowledge_ref, or undefined if none. */
97
+ export function extractFragment(ref) {
98
+ const idx = ref.indexOf("#");
99
+ return idx === -1 ? undefined : ref.slice(idx + 1);
100
+ }
88
101
  /**
89
102
  * Compute confidence adjustment based on the ratio of negative to total feedback.
90
103
  *
@@ -34,20 +34,30 @@ export function toDeDocument(doc) {
34
34
  const textContent = doc.structData?.content
35
35
  || [doc.structData?.name, doc.structData?.summary, doc.structData?.description].filter(Boolean).join("\n");
36
36
  const contentHash = computeContentHash(textContent || doc.id);
37
+ const now = new Date().toISOString();
37
38
  const augmented = {
38
39
  ...doc.structData,
39
40
  content_hash: contentHash,
40
- augmented_at: new Date().toISOString(),
41
+ augmented_at: now,
41
42
  status: doc.structData?.status ?? "active",
42
43
  confidence_score: doc.structData?.confidence_score
43
44
  ?? computeConfidenceScore(doc.structData?.provenance ?? "inferred"),
45
+ // Lifecycle timestamps — set once at publish, preserved on re-publish
46
+ // Aligned with knowledge-service's 3-layer model (produced_at/published_at)
47
+ published_at: doc.structData?.published_at ?? now,
48
+ produced_at: doc.structData?.produced_at ?? now,
44
49
  };
45
- // Compute freshness_tier from TTL if present (set by extraction pipeline from YAML config)
50
+ // Compute freshness_tier: TTL-based (explicit) or content-age (universal)
46
51
  if (doc.structData?.ttl) {
47
52
  const tier = computeFreshnessTier(doc.structData.ttl, doc.structData.extracted_at);
48
53
  if (tier)
49
54
  augmented.freshness_tier = tier;
50
55
  }
56
+ else {
57
+ const tier = computeContentFreshnessTier(augmented.produced_at);
58
+ if (tier)
59
+ augmented.freshness_tier = tier;
60
+ }
51
61
  augmentByType(doc, augmented);
52
62
  return {
53
63
  id: sanitizeId(doc.id),
@@ -58,6 +68,25 @@ export function toDeDocument(doc) {
58
68
  },
59
69
  };
60
70
  }
71
+ /**
72
+ * Universal content-age freshness tier (no TTL config required).
73
+ * Applies to all docs via produced_at — DE's boost-fresh/demote-expired
74
+ * controls handle the ranking impact.
75
+ */
76
+ // TODO(knowledge-service): migrate alongside computeFreshnessTier when shared lib is ready
77
+ export function computeContentFreshnessTier(producedAt) {
78
+ if (!producedAt)
79
+ return undefined;
80
+ const ageMs = Date.now() - new Date(producedAt).getTime();
81
+ if (Number.isNaN(ageMs))
82
+ return undefined;
83
+ const ageDays = ageMs / (86_400_000);
84
+ if (ageDays < 7)
85
+ return "fresh";
86
+ if (ageDays >= 90)
87
+ return "stale";
88
+ return undefined; // neutral — no boost or penalty
89
+ }
61
90
  // ─────────────────────────────────────────────────────────────────────────────
62
91
  // Type-Dispatched Augmentation
63
92
  // ─────────────────────────────────────────────────────────────────────────────
@@ -198,7 +198,7 @@ function formatToolSection(tg) {
198
198
  }
199
199
  if (tg.toolName === "feedback") {
200
200
  extras.push("\nYour feedback is automatically collected and analyzed to improve the toolkit for all agents.");
201
- extras.push("Include `knowledge_ref=\"<doc_id>\"` to correlate feedback with specific knowledge documents — this drives automatic confidence scoring (documents with high negative feedback are auto-downgraded).");
201
+ extras.push("Include `knowledge_ref` to correlate feedback with specific knowledge documents — this drives both confidence scoring (MCP-side) and DE native ranking (UserEvents). All categories emit DE events: positive feedback sends conversion signals, negative feedback (correction, confusion, gap) sends zero-value conversions so DE learns what didn't work. Use `#fragment` for sub-document precision: `knowledge_ref=\"topic_auth#L51-L56\"` (lines), `knowledge_ref=\"topic_auth#authentication\"` (section).");
202
202
  }
203
203
  return `## ${title}
204
204
  Use the \`${tg.toolName}\` tool${tg.quickTip ? `. ${tg.quickTip}` : ""}:
@@ -16,9 +16,8 @@ import { submitFeedback, listFeedback, listTelemetry, analyzeFeedback, rotateLog
16
16
  import { markProbeResponded } from "./probes.js";
17
17
  import { appendToOutbox, flushOutbox, getOutboxStats, readLocalMessages } from "./outbox.js";
18
18
  import { isRemoteEnabled } from "./remote-store.js";
19
- import { writeUserEvent } from "../../../knowledge/search-client.js";
20
- import { getOrCreateClientId } from "./client-id.js";
21
- import { getAttributionToken } from "../knowledge/session-state.js";
19
+ import { stripFragment } from "../../../knowledge/pipeline/confidence.js";
20
+ import { emitKnowledgeUserEvent, categoryToEvent } from "../knowledge/user-events.js";
22
21
  import { analyzeGlobal } from "./global-analysis.js";
23
22
  import { TOOLKIT_VERSION } from "../env/config.js";
24
23
  const VALID_CATEGORIES = ALL_CATEGORIES;
@@ -134,43 +133,25 @@ async function handleSubmit(args) {
134
133
  }
135
134
  }
136
135
  // Confidence loop: if feedback references a knowledge doc, update its confidence in DE
136
+ // Strip #fragment — confidence scoring operates at the document level
137
137
  let confidenceUpdate;
138
- if (knowledgeRef && CONFIDENCE_CATEGORIES.includes(category)) {
138
+ const docId = knowledgeRef ? stripFragment(knowledgeRef) : undefined;
139
+ if (docId && CONFIDENCE_CATEGORIES.includes(category)) {
139
140
  try {
140
141
  const { processConfidenceFeedback } = await import("../knowledge/confidence-loop.js");
141
- confidenceUpdate = await processConfidenceFeedback(category, knowledgeRef, qualityData) ?? undefined;
142
+ confidenceUpdate = await processConfidenceFeedback(category, docId, qualityData) ?? undefined;
142
143
  }
143
144
  catch {
144
145
  // Best-effort — don't block feedback submission
145
146
  }
146
147
  }
147
- // UserEvent emission: fire DE conversion/view-item for positive feedback with knowledge_ref.
148
+ // UserEvent emission: fire DE event for ALL feedback with knowledge_ref.
149
+ // Positive → conversion (value 1.0), negative → conversion (value 0.0), neutral → view-item.
148
150
  // Independent of confidence loop — no guards, no cooldown. Fire-and-forget.
149
- if (knowledgeRef) {
150
- const isSuccess = category === "success";
151
- const isHighQuality = category === "quality"
152
- && (qualityData?.accuracy ?? 0) >= 4
153
- && (qualityData?.usefulness ?? 0) >= 4;
154
- const isInteraction = category === "interaction";
155
- if (isSuccess || isHighQuality || isInteraction) {
156
- const conversionType = isSuccess ? "knowledge-success"
157
- : isHighQuality ? "knowledge-quality-high"
158
- : undefined; // interaction → view-item, no conversionType
159
- getOrCreateClientId()
160
- .then((clientId) => {
161
- const token = getAttributionToken(knowledgeRef);
162
- writeUserEvent({
163
- eventType: conversionType ? "conversion" : "view-item",
164
- userPseudoId: clientId,
165
- ...(token ? { attributionToken: token } : {}),
166
- documents: [{
167
- id: knowledgeRef,
168
- ...(conversionType ? { conversionValue: isSuccess ? 1.0 : 0.8 } : {}),
169
- }],
170
- ...(conversionType ? { conversionType } : {}),
171
- }).catch(() => { });
172
- })
173
- .catch(() => { });
151
+ if (docId) {
152
+ const eventOpts = categoryToEvent(category, qualityData);
153
+ if (eventOpts) {
154
+ emitKnowledgeUserEvent({ ...eventOpts, docIds: [docId] });
174
155
  }
175
156
  }
176
157
  return {
@@ -22,7 +22,7 @@ import { SESSION_ID } from "./session.js";
22
22
  // Category classification lives in confidence.ts (knowledge pipeline layer)
23
23
  // Re-export here so feedback handlers can use it without reaching into pipeline internals
24
24
  export { FeedbackSignal, CATEGORY_SIGNAL, } from "../../../knowledge/pipeline/confidence.js";
25
- import { CATEGORY_SIGNAL, FeedbackSignal } from "../../../knowledge/pipeline/confidence.js";
25
+ import { CATEGORY_SIGNAL, FeedbackSignal, stripFragment, extractFragment } from "../../../knowledge/pipeline/confidence.js";
26
26
  /** All valid category names — derived from the signal map */
27
27
  export const ALL_CATEGORIES = Object.keys(CATEGORY_SIGNAL);
28
28
  /** Categories that affect confidence scoring (negative, positive, or quality) */
@@ -359,28 +359,42 @@ export async function analyzeFeedback(rootOverride) {
359
359
  };
360
360
  deduplicateEntries(feedback.filter((e) => e.category === "gap"), "Documentation gap");
361
361
  deduplicateEntries(feedback.filter((e) => e.category === "confusion"), "Unclear guidance");
362
- // Graph-correlated insights: group feedback by knowledge_ref
362
+ // Graph-correlated insights: group feedback by knowledge_ref (doc-level)
363
+ // Fragment hotspots tracked per document (e.g., "L51-L56": 3)
363
364
  const graphCorrelations = {};
364
365
  for (const entry of feedback) {
365
366
  const ref = entry.quality_data?.knowledge_ref;
366
367
  if (!ref)
367
368
  continue;
368
- if (!graphCorrelations[ref]) {
369
- graphCorrelations[ref] = { count: 0, categories: {} };
369
+ const docId = stripFragment(ref);
370
+ const fragment = extractFragment(ref);
371
+ if (!graphCorrelations[docId]) {
372
+ graphCorrelations[docId] = { count: 0, categories: {} };
373
+ }
374
+ graphCorrelations[docId].count++;
375
+ graphCorrelations[docId].categories[entry.category] =
376
+ (graphCorrelations[docId].categories[entry.category] ?? 0) + 1;
377
+ if (fragment) {
378
+ if (!graphCorrelations[docId].fragments) {
379
+ graphCorrelations[docId].fragments = {};
380
+ }
381
+ graphCorrelations[docId].fragments[fragment] =
382
+ (graphCorrelations[docId].fragments[fragment] ?? 0) + 1;
370
383
  }
371
- graphCorrelations[ref].count++;
372
- graphCorrelations[ref].categories[entry.category] =
373
- (graphCorrelations[ref].categories[entry.category] ?? 0) + 1;
374
384
  }
375
385
  // Compute average accuracy for quality-rated nodes
376
386
  for (const entry of feedback) {
377
387
  const ref = entry.quality_data?.knowledge_ref;
378
388
  if (!ref || !entry.quality_data?.accuracy)
379
389
  continue;
380
- const corr = graphCorrelations[ref];
390
+ const docId = stripFragment(ref);
391
+ const corr = graphCorrelations[docId];
381
392
  if (!corr)
382
393
  continue;
383
- const qualityEntries = feedback.filter((e) => e.quality_data?.knowledge_ref === ref && e.quality_data?.accuracy != null);
394
+ const qualityEntries = feedback.filter((e) => {
395
+ const eRef = e.quality_data?.knowledge_ref;
396
+ return eRef && stripFragment(eRef) === docId && e.quality_data?.accuracy != null;
397
+ });
384
398
  corr.avgAccuracy =
385
399
  qualityEntries.reduce((sum, e) => sum + (e.quality_data.accuracy ?? 0), 0) /
386
400
  qualityEntries.length;
@@ -20,8 +20,7 @@
20
20
  */
21
21
  import { getConsultedDocs } from "./session-state.js";
22
22
  import { processConfidenceFeedback } from "./confidence-loop.js";
23
- import { writeUserEvent } from "../../../knowledge/search-client.js";
24
- import { getOrCreateClientId } from "../feedback/client-id.js";
23
+ import { emitKnowledgeUserEvent } from "./user-events.js";
25
24
  /** Weight multiplier per signal layer */
26
25
  const LAYER_WEIGHTS = {
27
26
  system: 0.1, // API accepted it — weakest signal
@@ -178,27 +177,17 @@ export async function emitOutcomeFeedback(event) {
178
177
  }
179
178
  }
180
179
  }
181
- // Fire UserEvents (fire-and-forget)
182
- getOrCreateClientId()
183
- .then((clientId) => {
184
- // Conversion value = outcome quality × layer weight
185
- const baseValue = outcome.quality === "success" ? 1.0
186
- : outcome.quality === "partial" ? 0.5
187
- : 0;
188
- const conversionValue = baseValue * weight;
189
- const isConversion = outcome.isPositive && outcome.quality !== "accepted";
190
- const documents = [...docs].map((docId) => ({
191
- id: docId,
192
- ...(isConversion ? { conversionValue } : {}),
193
- }));
194
- writeUserEvent({
195
- eventType: isConversion ? "conversion" : "view-item",
196
- userPseudoId: clientId,
197
- ...(isConversion ? { conversionType: eventType } : {}),
198
- documents,
199
- }).catch(() => { });
200
- })
201
- .catch(() => { });
180
+ // Fire UserEvents via centralized emitter (handles attribution tokens + fragment stripping)
181
+ const baseValue = outcome.quality === "success" ? 1.0
182
+ : outcome.quality === "partial" ? 0.5
183
+ : 0;
184
+ const conversionValue = baseValue * weight;
185
+ emitKnowledgeUserEvent({
186
+ docIds: [...docs],
187
+ signal: outcome.isPositive ? "positive" : "negative",
188
+ conversionType: eventType,
189
+ conversionValue,
190
+ });
202
191
  console.error(`[OUTCOME-FEEDBACK] ${eventType} (layer=${layer}, weight=${weight}): ` +
203
192
  `${docs.size} consulted docs, ${updates} confidence updates`);
204
193
  return { docs_processed: docs.size, confidence_updates: updates, event_type: eventType };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Centralized DE UserEvent emission for knowledge feedback signals.
3
+ *
4
+ * Single entry point for all feedback → DE UserEvent emission.
5
+ * Handles: attribution token lookup, fragment stripping, signal→event mapping,
6
+ * and fire-and-forget delivery.
7
+ *
8
+ * Ownership boundary:
9
+ * - MCP OWNS: interaction/feedback collection, signal classification
10
+ * - knowledge-service WILL OWN: DE transport, attribution, event schema
11
+ *
12
+ * TODO(knowledge-service): migrate DE transport when knowledge-service owns DE integration.
13
+ * MCP will continue to own feedback collection and signal classification.
14
+ * knowledge-service will own: writeUserEvent, attribution cache, event→DE mapping.
15
+ */
16
+ import { writeUserEvent } from "../../../knowledge/search-client.js";
17
+ import { getOrCreateClientId } from "../feedback/client-id.js";
18
+ import { getAttributionToken } from "./session-state.js";
19
+ import { stripFragment } from "../../../knowledge/pipeline/confidence.js";
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ // Feedback category → signal mapping
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // TODO(knowledge-service): signal classification stays in MCP (owns feedback semantics)
24
+ const CATEGORY_EVENT_MAP = {
25
+ // Positive signals
26
+ success: { signal: "positive", conversionType: "knowledge-success", value: 1.0 },
27
+ interaction: { signal: "neutral", conversionType: "", value: 0 },
28
+ // Negative signals
29
+ correction: { signal: "negative", conversionType: "knowledge-correction", value: 0.0 },
30
+ confusion: { signal: "negative", conversionType: "knowledge-confusion", value: 0.0 },
31
+ gap: { signal: "negative", conversionType: "knowledge-gap", value: 0.0 },
32
+ error_unclear: { signal: "negative", conversionType: "knowledge-error", value: 0.0 },
33
+ };
34
+ /**
35
+ * Map a feedback category + quality data to UserEvent parameters.
36
+ * Returns undefined for categories that shouldn't emit events (e.g., suggestion, probe_response).
37
+ */
38
+ export function categoryToEvent(category, qualityData) {
39
+ // Quality category: only emit for high-quality ratings
40
+ if (category === "quality") {
41
+ const isHighQuality = (qualityData?.accuracy ?? 0) >= 4
42
+ && (qualityData?.usefulness ?? 0) >= 4;
43
+ if (!isHighQuality)
44
+ return undefined;
45
+ return {
46
+ docIds: [], // caller fills in
47
+ signal: "positive",
48
+ conversionType: "knowledge-quality-high",
49
+ conversionValue: 0.8,
50
+ };
51
+ }
52
+ const mapped = CATEGORY_EVENT_MAP[category];
53
+ if (!mapped)
54
+ return undefined;
55
+ return {
56
+ docIds: [], // caller fills in
57
+ signal: mapped.signal,
58
+ conversionType: mapped.conversionType || undefined,
59
+ conversionValue: mapped.value,
60
+ };
61
+ }
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+ // Centralized emitter
64
+ // ─────────────────────────────────────────────────────────────────────────────
65
+ /**
66
+ * Emit a DE UserEvent for knowledge feedback signals.
67
+ *
68
+ * Handles attribution token lookup, fragment stripping, and fire-and-forget delivery.
69
+ * All feedback paths (explicit feedback, outcome feedback) should use this function
70
+ * instead of calling writeUserEvent directly.
71
+ *
72
+ * TODO(knowledge-service): DE transport (writeUserEvent, attribution) moves to shared lib.
73
+ * This function becomes: knowledgeService.emitFeedbackEvent({ docIds, signal, ... })
74
+ */
75
+ export function emitKnowledgeUserEvent(opts) {
76
+ if (opts.docIds.length === 0)
77
+ return;
78
+ const isConversion = opts.signal !== "neutral";
79
+ const conversionValue = opts.conversionValue
80
+ ?? (opts.signal === "positive" ? 1.0 : opts.signal === "negative" ? 0.0 : undefined);
81
+ getOrCreateClientId()
82
+ .then((clientId) => {
83
+ // Strip fragments and dedupe doc IDs
84
+ const seen = new Set();
85
+ const documents = [];
86
+ let attributionToken;
87
+ for (const rawId of opts.docIds) {
88
+ const docId = stripFragment(rawId);
89
+ if (!docId || seen.has(docId))
90
+ continue;
91
+ seen.add(docId);
92
+ // Use first available attribution token (all docs from same search share one)
93
+ if (!attributionToken) {
94
+ attributionToken = getAttributionToken(docId);
95
+ }
96
+ documents.push({
97
+ id: docId,
98
+ ...(isConversion && conversionValue !== undefined ? { conversionValue } : {}),
99
+ });
100
+ }
101
+ if (documents.length === 0)
102
+ return;
103
+ writeUserEvent({
104
+ eventType: isConversion ? "conversion" : "view-item",
105
+ userPseudoId: clientId,
106
+ ...(attributionToken ? { attributionToken } : {}),
107
+ ...(isConversion && opts.conversionType ? { conversionType: opts.conversionType } : {}),
108
+ documents,
109
+ }).catch(() => { });
110
+ })
111
+ .catch(() => { });
112
+ }
@@ -692,13 +692,22 @@ The toolkit also collects anonymous telemetry (tool usage, error rates, latency)
692
692
 
693
693
  ## Knowledge Confidence Scoring
694
694
 
695
- Feedback drives automatic confidence scoring on knowledge documents. Include a knowledge_ref to correlate feedback with specific documents:
696
- - knowledge_ref="<document_id>" — top-level parameter, triggers real-time confidence update
697
- - Negative categories (downgrade score): gap, confusion, correction, error_unclear
698
- - Positive categories (upgrade score): success, interaction
699
- - Nuanced: quality (depends on accuracy/usefulness scores — <3 negative, >=4 positive, no scores = neutral)
695
+ Feedback drives two parallel DE ranking signals when knowledge_ref is provided:
700
696
 
701
- Documents with negative feedback are auto-downgraded. Labels are derived from score thresholds:
697
+ 1. **Confidence Loop (MCP-side)** immediate structData.confidence_score update via PATCH
698
+ 2. **DE UserEvents (native ranking)** — conversion/view-item events for DE's ML-based ranking
699
+
700
+ All feedback categories emit DE UserEvents:
701
+ - Positive (conversion, value=1.0): success, interaction
702
+ - Negative (conversion, value=0.0): correction, confusion, gap, error_unclear
703
+ - Quality: depends on accuracy/usefulness scores (>=4 both → positive at 0.8, else skipped)
704
+ - Neutral (view-item): interaction
705
+
706
+ Include knowledge_ref to correlate feedback with specific documents:
707
+ - knowledge_ref="<document_id>" — triggers both confidence update and DE event
708
+ - Use #fragment for sub-document precision: knowledge_ref="topic_auth#L51-L56"
709
+
710
+ Labels derived from confidence score thresholds:
702
711
  - verified (>= 0.80) — no significant negative feedback
703
712
  - inferred (0.50–0.79) — some negative feedback received
704
713
  - low-confidence (< 0.50) — multiple negative reports, auto-downgraded in search
package/dist/mcp/tools.js CHANGED
@@ -786,10 +786,11 @@ A **profile** = tenant + environment + auth. Example: "acme-corp-prod" = Acme Co
786
786
  - \`feedback(method="submit", category="suggestion", message="...")\` - suggest improvements
787
787
  - \`feedback(method="submit", category="probe_response", message="<answer>", context="<probe.id>")\` - respond to a _probe question
788
788
  - \`feedback(method="submit", category="quality", message="...", knowledge_ref="topic_authentication")\` - rate knowledge accuracy (drives confidence scoring)
789
- - \`feedback(method="submit", category="correction", message="...", knowledge_ref="rule_get-before-modify")\` - report wrong information
789
+ - \`feedback(method="submit", category="correction", message="...", knowledge_ref="rule_get-before-modify#L51-L56")\` - report wrong info at specific lines
790
+ - \`feedback(method="submit", category="correction", message="...", knowledge_ref="topic_auth#authentication")\` - report issue in a section
790
791
 
791
792
  ## Knowledge Confidence
792
- Include \`knowledge_ref\` to correlate feedback with specific knowledge documents. Documents with high negative feedback are auto-downgraded in search results.
793
+ Include \`knowledge_ref\` to correlate feedback with specific knowledge documents. Use \`#fragment\` for sub-document precision (e.g., \`topic_auth#L51-L56\` for lines, \`topic_auth#authentication\` for sections). Documents with high negative feedback are auto-downgraded in search results.
793
794
 
794
795
  ## Review Feedback
795
796
  - \`feedback(method="list")\` - view recent feedback
@@ -842,7 +843,7 @@ Messages are emitted by MCP response actions (search, publish, deploy) and stay
842
843
  },
843
844
  knowledge_ref: {
844
845
  type: "string",
845
- description: "Knowledge document ID this feedback relates to (e.g. 'topic_authentication', 'rule_get-before-modify'). Drives automatic confidence scoring documents with negative feedback rank lower in search.",
846
+ description: "Knowledge document ID, optionally with #fragment for sub-document precision. Examples: 'topic_auth', 'topic_auth#L51-L56' (lines), 'topic_auth#L51' (single line), 'topic_auth#authentication' (section). Same as GitHub URL fragments. Drives confidence scoring at the document level (fragment stripped for scoring, preserved for reporting).",
846
847
  },
847
848
  severity: {
848
849
  type: "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ema.co/mcp-toolkit",
3
- "version": "2026.4.9-1",
3
+ "version": "2026.4.9-3",
4
4
  "description": "Ema AI Employee toolkit - MCP server, CLI, and SDK for managing AI Employees across environments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",