@corva/ui 3.78.0-16 → 3.78.0-17

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/MCP_README.md CHANGED
@@ -711,9 +711,17 @@ To check whether telemetry is currently active for your install, run `/mcp__corv
711
711
 
712
712
  To opt out entirely, set `CORVA_UI_MCP_TELEMETRY_DISABLED=1` in the server's environment (e.g. in the `env` block of
713
713
  your MCP config file) — any non-empty value other than `0` or `false` works. The switch is honored at runtime before
714
- any telemetry configuration is read. Feedback travels over the same channel and is subject to the same sampling rate:
715
- `submit_feedback` and the `feedback` prompt always confirm the message, but with telemetry off nothing reaches the
716
- maintainers, and with a sampling rate below 1 some messages are dropped.
714
+ any telemetry configuration is read.
715
+
716
+ Feedback travels over the same channel, so `submit_feedback` reports one of 2 delivery states:
717
+
718
+ - **queued** — telemetry is enabled and the message was handed to it for export. Delivery is not confirmed. Feedback is
719
+ exempt from the sampling rate, so it is never sampled out.
720
+ - **not sent** — nothing reaches the maintainers, and the response names the cause: the opt-out is set
721
+ (`CORVA_UI_MCP_TELEMETRY_DISABLED`, or `"enabled": false` in the local config), or telemetry has no valid
722
+ configuration in this session (or failed to start).
723
+
724
+ The `feedback` prompt relays that state to you, and `get_diagnostics` shows it as the feedback channel.
717
725
 
718
726
  ## FAQ
719
727
 
@@ -5,7 +5,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
5
  import { OTLPMetricExporter, AggregationTemporalityPreference } from '@opentelemetry/exporter-metrics-otlp-http';
6
6
  import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
7
7
  import { SpanKind, SpanStatusCode, propagation, context } from '@opentelemetry/api';
8
- import { NodeTracerProvider, TraceIdRatioBasedSampler, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
8
+ import { TraceIdRatioBasedSampler, SamplingDecision, NodeTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
9
9
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
10
10
  import { randomUUID } from 'crypto';
11
11
  import { defaultResource, resourceFromAttributes } from '@opentelemetry/resources';
@@ -246,9 +246,9 @@ const validateDsn = dsn => {
246
246
  return url.href;
247
247
  };
248
248
 
249
- const MCP_SERVER_VERSION = '1.8.0';
249
+ const MCP_SERVER_VERSION = '1.9.0';
250
250
 
251
- var version = "3.78.0-16";
251
+ var version = "3.78.0-17";
252
252
 
253
253
  const CORVA_UI_VERSION = version;
254
254
 
@@ -535,11 +535,24 @@ const recordInitSpan = (tracer, attrs) => {
535
535
  }
536
536
  };
537
537
 
538
+ // Feedback leaves the process only as this span, so a ratio below 1 would silently lose messages.
539
+ // Matched by name: recordToolSpan sets gen_ai.tool.name after startSpan, too late for a sampler.
540
+ const FEEDBACK_SPAN_NAME = 'tools/call submit_feedback';
541
+ const createSampler = samplingRate => {
542
+ const ratioSampler = new TraceIdRatioBasedSampler(samplingRate);
543
+ return {
544
+ shouldSample: (context, traceId, spanName) => spanName === FEEDBACK_SPAN_NAME ? {
545
+ decision: SamplingDecision.RECORD_AND_SAMPLED
546
+ } : ratioSampler.shouldSample(context, traceId),
547
+ toString: () => `FeedbackAlwaysOn{${ratioSampler.toString()}}`
548
+ };
549
+ };
550
+
538
551
  const createTracerProvider = config => {
539
552
  const dsn = validateDsn(config.dsn);
540
553
  return new NodeTracerProvider({
541
554
  resource: config.resource,
542
- sampler: new TraceIdRatioBasedSampler(config.samplingRate),
555
+ sampler: createSampler(config.samplingRate),
543
556
  spanProcessors: [
544
557
  // Export spans immediately at end for short-lived local MCP sessions.
545
558
  new SimpleSpanProcessor(new OTLPTraceExporter({
@@ -659,7 +672,7 @@ const readLocalConfig = async logger => {
659
672
  // Fail closed: the file's presence signals the user wants local control over telemetry,
660
673
  // so a broken file must never silently fall back to the remote config that re-enables it.
661
674
  logger.warn(`Local telemetry config file exists but is invalid: ${configPath}. Telemetry disabled (fix or delete the file to re-enable).`);
662
- return 'disabled';
675
+ return 'invalid';
663
676
  } catch (error) {
664
677
  if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
665
678
  // File doesn't exist, which is normal. Silently skip.
@@ -668,10 +681,13 @@ const readLocalConfig = async logger => {
668
681
 
669
682
  // Other errors (JSON parse errors, permissions, etc.) — same fail-closed rule as above.
670
683
  logger.warn(`Failed to read local telemetry config from ${configPath}: ${error instanceof Error ? error.message : String(error)}. Telemetry disabled (fix or delete the file to re-enable).`);
671
- return 'disabled';
684
+ return 'invalid';
672
685
  }
673
686
  };
674
687
 
688
+ /** The user turned telemetry off, as opposed to it being unavailable. */
689
+ const TELEMETRY_OPT_OUT = 'opt-out';
690
+
675
691
  // Read at call time, not module scope: in production bundles the rollup replace plugin bakes
676
692
  // the URL in as a string literal either way, while in dev/tsx mode (and tests) this honors the
677
693
  // process env at the moment of the call.
@@ -688,13 +704,18 @@ const isKillSwitchEnabled = () => {
688
704
  const fetchTelemetryConfig = async logger => {
689
705
  if (isKillSwitchEnabled()) {
690
706
  logger.info('Telemetry disabled via CORVA_UI_MCP_TELEMETRY_DISABLED');
691
- return null;
707
+ return TELEMETRY_OPT_OUT;
692
708
  }
693
709
  const localConfig = await readLocalConfig(logger);
694
710
 
695
711
  // Explicit user opt-out ("enabled": false in the local file) — must stop here, not
696
712
  // fall through to the remote config, which would override the user's choice.
697
713
  if (localConfig === 'disabled') {
714
+ return TELEMETRY_OPT_OUT;
715
+ }
716
+
717
+ // A broken file fails closed too, but it is not the user's choice, so it is not opt-out.
718
+ if (localConfig === 'invalid') {
698
719
  return null;
699
720
  }
700
721
  if (localConfig) {
@@ -743,10 +764,18 @@ const fetchTelemetryConfig = async logger => {
743
764
  const initTelemetry = async logger => {
744
765
  try {
745
766
  const configResult = await fetchTelemetryConfig(logger);
767
+ if (configResult === TELEMETRY_OPT_OUT) {
768
+ return {
769
+ client: createNoopTelemetryClient(),
770
+ info: null,
771
+ disabledReason: 'opt-out'
772
+ };
773
+ }
746
774
  if (!configResult) {
747
775
  return {
748
776
  client: createNoopTelemetryClient(),
749
- info: null
777
+ info: null,
778
+ disabledReason: 'no-config'
750
779
  };
751
780
  }
752
781
  const {
@@ -777,13 +806,15 @@ const initTelemetry = async logger => {
777
806
  info: {
778
807
  configSource: source,
779
808
  samplingRate: config.samplingRate
780
- }
809
+ },
810
+ disabledReason: null
781
811
  };
782
812
  } catch (error) {
783
813
  logger.warn(`Telemetry init failed, continuing without telemetry: ${error}`);
784
814
  return {
785
815
  client: createNoopTelemetryClient(),
786
- info: null
816
+ info: null,
817
+ disabledReason: 'no-config'
787
818
  };
788
819
  }
789
820
  };
@@ -1037,7 +1068,7 @@ Do the following:
1037
1068
  2. Infer a sentiment ('positive', 'negative', or 'neutral') from the feedback. If it is ambiguous, ask the user briefly or default to 'neutral'.
1038
1069
  3. If the feedback is about a specific item, set category (component, hook, util, constant, client, theme, tool, docs, or other) and target (e.g. the component or hook name).
1039
1070
  4. Call \`mcp__corva-ui__submit_feedback\` with the message and the fields above, and set \`source: "user"\` (this feedback is relayed from the user).
1040
- 5. Confirm to the user that the feedback was sent.`;
1071
+ 5. Tell the user the delivery state the tool reported: queued, or not sent with its cause. Do not claim more than the tool reported.`;
1041
1072
  return {
1042
1073
  description: feedbackPromptDescription,
1043
1074
  messages: [{
@@ -22471,11 +22502,54 @@ ${formatMethods(client.methods)}`;
22471
22502
  };
22472
22503
  };
22473
22504
 
22505
+ const FEEDBACK_CATEGORIES = [...ENTITY_TYPES, 'theme', 'tool', 'docs', 'other'];
22506
+ const submitFeedbackToolName = 'submit_feedback';
22507
+ const submitFeedbackToolTitle = 'Submit Feedback';
22508
+ const submitFeedbackToolDescription = `Send feedback about @corva/ui or this MCP server so the maintainers can improve docs and coverage.
22509
+ Use this when the user says something like "send feedback that…", or proactively when you hit a docs/coverage gap (a missing component, a wrong or incomplete prop, a hook that lacks an example, a search that should have matched but didn't).
22510
+
22511
+ Always infer sentiment and category from the feedback itself — do not leave them at the defaults. Reserve sentiment 'neutral' for genuinely factual/mixed feedback and category 'other' only when none of the specific categories fit.
22512
+ Set source to 'agent' when you are reporting a gap on your own initiative, or 'user' when relaying something the user said.
22513
+ Keep message specific and actionable; name the component/hook/util in target when the feedback is about a particular item.
22514
+ The response states whether the message was queued or not sent, and why; relay that state to the user as is.`;
22515
+ const submitFeedbackToolSchema = {
22516
+ message: z.string().min(1).describe('The feedback text (required). Be specific and actionable.'),
22517
+ sentiment: z.enum(['positive', 'negative', 'neutral']).optional().describe("Tone of the feedback — classify it from the message; don't default to neutral unless the feedback is genuinely neutral/factual (default: neutral)"),
22518
+ category: z.enum(FEEDBACK_CATEGORIES).optional().describe("What the feedback is about — pick the most specific category; use 'other' only when none apply"),
22519
+ target: z.string().optional().describe('The specific component/hook/util/tool the feedback is about (e.g. "Button")'),
22520
+ source: z.enum(['user', 'agent']).optional().describe("Who originated the feedback: 'user' (relayed) or 'agent' (default: user)")
22521
+ };
22522
+ const NOT_SENT_CAUSES = {
22523
+ 'opt-out': 'telemetry is turned off by opt-out (CORVA_UI_MCP_TELEMETRY_DISABLED, or "enabled": false in the local telemetry config)',
22524
+ 'no-config': 'telemetry has no valid configuration in this session, or it failed to start'
22525
+ };
22526
+ const describeFeedbackDelivery = delivery => delivery.state === 'queued' ? 'queued — handed to telemetry for export; delivery is not confirmed' : `not sent — ${NOT_SENT_CAUSES[delivery.cause]}`;
22527
+ const formatResponse = (delivery, sentiment) => delivery.state === 'queued' ? `Feedback ${describeFeedbackDelivery(delivery)}. (${sentiment})\nThe @corva/ui team uses this to improve the library and its docs.` : `Feedback ${describeFeedbackDelivery(delivery)}. Feedback travels only through telemetry, so the @corva/ui team will not receive this message. Tell the user, so they can share it with the team another way.`;
22528
+ const handleSubmitFeedback = (args, delivery) => {
22529
+ const message = args.message?.trim() ?? '';
22530
+ if (!message) {
22531
+ throw new Error('Feedback message cannot be empty.');
22532
+ }
22533
+ const sentiment = args.sentiment ?? 'neutral';
22534
+ const source = args.source ?? 'user';
22535
+ const category = args.category?.trim() || undefined;
22536
+ const target = args.target?.trim() || undefined;
22537
+ return {
22538
+ response: createToolResponse(formatResponse(delivery, sentiment)),
22539
+ delivery,
22540
+ message,
22541
+ sentiment,
22542
+ category,
22543
+ target,
22544
+ source
22545
+ };
22546
+ };
22547
+
22474
22548
  const diagnosticsToolName = 'get_diagnostics';
22475
22549
  const diagnosticsToolTitle = 'Get Server Diagnostics';
22476
22550
  const diagnosticsToolDescription = `Get MCP server health metrics.
22477
22551
  Returns server version, uptime, memory usage, request statistics, telemetry status,
22478
- project identity (app key and package name), and the prompts this server registers.`;
22552
+ whether feedback can reach the maintainers, project identity (app key and package name), and the prompts this server registers.`;
22479
22553
  const diagnosticsToolSchema = {};
22480
22554
  const formatUptime = ms => {
22481
22555
  const seconds = Math.floor(ms / 1000);
@@ -22534,6 +22608,9 @@ const handleGetDiagnostics = (stats, telemetry, identity, registeredPromptNames)
22534
22608
  text += `${bold('Telemetry sampling rate')}: ${telemetry.samplingRate}\n`;
22535
22609
  }
22536
22610
  }
22611
+ if (telemetry.feedbackDelivery) {
22612
+ text += `${bold('Feedback channel')}: ${describeFeedbackDelivery(telemetry.feedbackDelivery)}\n`;
22613
+ }
22537
22614
  } else {
22538
22615
  text += `${bold('Status')}: Disabled\n`;
22539
22616
  }
@@ -22550,41 +22627,6 @@ const handleGetDiagnostics = (stats, telemetry, identity, registeredPromptNames)
22550
22627
  };
22551
22628
  };
22552
22629
 
22553
- const FEEDBACK_CATEGORIES = [...ENTITY_TYPES, 'theme', 'tool', 'docs', 'other'];
22554
- const submitFeedbackToolName = 'submit_feedback';
22555
- const submitFeedbackToolTitle = 'Submit Feedback';
22556
- const submitFeedbackToolDescription = `Send feedback about @corva/ui or this MCP server so the maintainers can improve docs and coverage.
22557
- Use this when the user says something like "send feedback that…", or proactively when you hit a docs/coverage gap (a missing component, a wrong or incomplete prop, a hook that lacks an example, a search that should have matched but didn't).
22558
-
22559
- Always infer sentiment and category from the feedback itself — do not leave them at the defaults. Reserve sentiment 'neutral' for genuinely factual/mixed feedback and category 'other' only when none of the specific categories fit.
22560
- Set source to 'agent' when you are reporting a gap on your own initiative, or 'user' when relaying something the user said.
22561
- Keep message specific and actionable; name the component/hook/util in target when the feedback is about a particular item.`;
22562
- const submitFeedbackToolSchema = {
22563
- message: z.string().min(1).describe('The feedback text (required). Be specific and actionable.'),
22564
- sentiment: z.enum(['positive', 'negative', 'neutral']).optional().describe("Tone of the feedback — classify it from the message; don't default to neutral unless the feedback is genuinely neutral/factual (default: neutral)"),
22565
- category: z.enum(FEEDBACK_CATEGORIES).optional().describe("What the feedback is about — pick the most specific category; use 'other' only when none apply"),
22566
- target: z.string().optional().describe('The specific component/hook/util/tool the feedback is about (e.g. "Button")'),
22567
- source: z.enum(['user', 'agent']).optional().describe("Who originated the feedback: 'user' (relayed) or 'agent' (default: user)")
22568
- };
22569
- const handleSubmitFeedback = args => {
22570
- const message = args.message?.trim() ?? '';
22571
- if (!message) {
22572
- throw new Error('Feedback message cannot be empty.');
22573
- }
22574
- const sentiment = args.sentiment ?? 'neutral';
22575
- const source = args.source ?? 'user';
22576
- const category = args.category?.trim() || undefined;
22577
- const target = args.target?.trim() || undefined;
22578
- return {
22579
- response: createToolResponse(`Thanks — your feedback was recorded. (${sentiment})\nThe @corva/ui team uses this to improve the library and its docs.`),
22580
- message,
22581
- sentiment,
22582
- category,
22583
- target,
22584
- source
22585
- };
22586
- };
22587
-
22588
22630
  /**
22589
22631
  * Authoring types for inline component migrations.
22590
22632
  *
@@ -23038,7 +23080,8 @@ const registerTools = deps => {
23038
23080
  getStats,
23039
23081
  getTelemetryStatus,
23040
23082
  getProjectIdentity,
23041
- getTelemetry
23083
+ getTelemetry,
23084
+ getFeedbackDelivery
23042
23085
  } = deps;
23043
23086
 
23044
23087
  // TODO: every `inputSchema: …ToolSchema as any` below is unexplained — the cast predates any
@@ -23215,7 +23258,7 @@ const registerTools = deps => {
23215
23258
  const parentContext = extractContextFromMeta(extra?._meta);
23216
23259
  try {
23217
23260
  return executeToolWithObservability(submitFeedbackToolName, args, () => {
23218
- const result = handleSubmitFeedback(args);
23261
+ const result = handleSubmitFeedback(args, getFeedbackDelivery());
23219
23262
  // Supplemental low-cardinality counter, in addition to the standard
23220
23263
  // tool-call span/metrics recorded by the wrapper. No-op when disabled.
23221
23264
  const telemetry = getTelemetry();
@@ -23226,7 +23269,7 @@ const registerTools = deps => {
23226
23269
  ...result,
23227
23270
  isEmpty: false
23228
23271
  };
23229
- }, r => `feedback recorded (${r.sentiment})`, {
23272
+ }, r => `feedback ${r.delivery.state} (${r.sentiment})`, {
23230
23273
  parentContext,
23231
23274
  getSpanAttributes: r => ({
23232
23275
  // Indexed copy of the message so it is searchable/groupable in
@@ -23339,6 +23382,7 @@ class CorvaUiMcpServer {
23339
23382
 
23340
23383
  telemetry = createNoopTelemetryClient();
23341
23384
  telemetryInfo = null;
23385
+ telemetryDisabledReason = null;
23342
23386
  telemetryInitPromise = null;
23343
23387
  shutdownRequested = false;
23344
23388
  pendingInitData = null;
@@ -23562,7 +23606,8 @@ class CorvaUiMcpServer {
23562
23606
  getStats: () => this.getStats(),
23563
23607
  getTelemetryStatus: () => this.getTelemetryStatus(),
23564
23608
  getProjectIdentity: () => this.projectIdentity,
23565
- getTelemetry: () => this.telemetry
23609
+ getTelemetry: () => this.telemetry,
23610
+ getFeedbackDelivery: () => this.getFeedbackDelivery()
23566
23611
  });
23567
23612
  }
23568
23613
  setupPrompts() {
@@ -23580,10 +23625,12 @@ class CorvaUiMcpServer {
23580
23625
  const telemetryTimer = this.mcpLogger.time('telemetry-init');
23581
23626
  this.telemetryInitPromise = initTelemetry(this.mcpLogger).then(({
23582
23627
  client,
23583
- info
23628
+ info,
23629
+ disabledReason
23584
23630
  }) => {
23585
23631
  this.telemetry = client;
23586
23632
  this.telemetryInfo = info;
23633
+ this.telemetryDisabledReason = disabledReason;
23587
23634
  }).catch(() => {
23588
23635
  /* telemetry is best-effort — it must never take the server down */
23589
23636
  });
@@ -23635,7 +23682,16 @@ class CorvaUiMcpServer {
23635
23682
  enabled: this.telemetry.isEnabled(),
23636
23683
  sessionId: this.telemetry.isEnabled() ? this.telemetry.sessionId : undefined,
23637
23684
  configSource: this.telemetryInfo?.configSource,
23638
- samplingRate: this.telemetryInfo?.samplingRate
23685
+ samplingRate: this.telemetryInfo?.samplingRate,
23686
+ feedbackDelivery: this.getFeedbackDelivery()
23687
+ };
23688
+ }
23689
+ getFeedbackDelivery() {
23690
+ return this.telemetry.isEnabled() ? {
23691
+ state: 'queued'
23692
+ } : {
23693
+ state: 'not-sent',
23694
+ cause: this.telemetryDisabledReason ?? 'no-config'
23639
23695
  };
23640
23696
  }
23641
23697
  getStats() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corva/ui",
3
- "version": "3.78.0-16",
3
+ "version": "3.78.0-17",
4
4
  "license": "UNLICENSED",
5
5
  "description": "Shared components/utils for Corva ui projects",
6
6
  "repository": {