You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Telemetry for agent tool calls uses two different meanings of "toolset". The addon-mcp event reports the MCP grouping (dev, docs, test), while the CLI and SDK expose toolsets named stories, review, docs, and test. The tools-command event copies the MCP grouping, so a CLI call to review create is logged as toolset: dev, and the CLI toolset and tool names appear nowhere except inside a command string.
On top of that, one CLI invocation writes two tools-command records: one from the command wrapper (command, success, duration, attach fields) and one from the method handler (event, toolset, counters), with no link between them. Counting rows double counts every successful command.
Finally, docs show-story never reports telemetry on any surface. Its predecessor in @storybook/mcp never had a telemetry hook, and the port preserved that gap.
Solution
Every surface reports the same vocabulary: the CLI toolset and tool names, a generated event derived from them (tool:stories_changed), and the handler's payload. CLI and SDK records become one record per invocation and drop the command string since it is derivable from the two parts. addon-mcp records gain tool and switch toolset to the CLI name, but keep their pre-toolset event names (tool:getChangedStories) so MCP data stays continuous across versions. The show-story method gains a telemetry report on both surfaces.
A handler no longer sends telemetry and never names an event. It returns a payload as part of its outcome, next to data and markdown. The toolset layer that runs the handler completes that into one report: toolset and tool from where the method is registered, a generated event (tool:<toolset>_<method>), and the payload. The CLI and SDK forward that report as is and add their run fields. The MCP adapter forwards it too, adds its session fields, and replaces event with the pre-toolset name it keeps per tool in the addon's own registry; the toolsets know nothing of those names.
Before and after: CLI npx storybook tools stories changed
Field
Before, record 1
Before, record 2
After, single record
command
stories changed
event
tool:getChangedStories
tool:stories_changed
toolset
dev
stories
tool
changed
success
true
true
outcome
success
success
duration
312
312
client
cli
cli
cli
requestedMode
auto
auto
auto
resolvedMode
local
local
local
attachMode
local
local
local
host
in-process
in-process
in-process
storyCount
4
4
newStoryCount
1
1
modifiedStoryCount
3
3
affectedStoryCount
0
0
The same applies to every other method. The SDK produces the same record with client: sdk.
Before and after: CLI intercepts
Case
Before
After
unknown tool stories nope
command: stories nope
toolset: stories, tool: nope
garbage name ../x list
command: (invalid) list
toolset: (invalid), tool: list
attach gate, nothing parsed
command: (none)
no toolset, no tool
Before and after: MCP stories-changed
Field
Before
After
event
tool:getChangedStories
tool:getChangedStories
toolset
dev
stories
tool
changed
payload and session fields
unchanged
unchanged
docs and test keep their toolset value; only the dev grouping splits into stories and review.
New: show-story on both surfaces
Field
CLI
MCP
event
tool:docs_showStory
tool:getDocumentationForStory
toolset
docs
docs
tool
show-story
show-story
found
true
true
storyId
button--primary
button--primary
lookup
storyId
storyId
resultTokenCount
412
412
The CLI row also carries success, duration, and the attach fields.
User Stories
As a product lead building dashboards, I want every CLI tools-command record to carry toolset and tool in the CLI's own names, so that I can group agentic usage by stories, review, docs, and test without parsing strings.
As a product lead, I want one tools-command record per CLI invocation, so that counting records counts invocations.
As a product lead, I want the handler's payload (story counts, test results, token counts) on the same record as success and duration, so that I can correlate outcome with payload in one query.
As a product lead, I want CLI and SDK records to carry an event generated from toolset and tool, so that a new tool gets a consistent name without anyone inventing one.
As a product lead, I want addon-mcp records to keep their pre-toolset event names and payload and gain the same toolset and tool as CLI records, so that one query groups usage across surfaces and MCP data still joins to pre-10.6.1 data on event.
As a product lead, I want docs show-story reported on both surfaces with found, storyId, lookup, and resultTokenCount, so that the docs toolset is fully observable.
As a product lead, I want to see which input shape agents use for show-story, so that I can decide whether to keep both shapes.
As a product lead, I want intercepts and attach-gate failures to keep producing one record without payload, so that success rates stay comparable with the current release.
As a product lead, I want intercept records to carry the sanitized toolset and tool the agent typed, so that I can see which unknown names agents reach for.
As a product lead, I want SDK invocations to carry the same fields as CLI ones with client: sdk, so that the two clients are comparable.
As a product lead, I want a CLI call that fails inside the handler to still produce one record with success: false, so that failures are never lost.
As a maintainer, I want the toolset name to live only in the registry id, so that a method can never report a name that disagrees with where it is registered.
As a maintainer adding a new toolset method, I want toolset and tool filled in automatically on the CLI and SDK, so that I cannot forget them.
As a maintainer, I want the pre-toolset event names to live only in the addon-mcp registry, so that toolsets and the CLI never learn them and they can be dropped in one place later.
As a maintainer, I want a handler to return its report as data on its outcome, so that reporting cannot happen twice, too late, or on a path the surface does not see.
As a maintainer, I want the report a handler outcome carries to already be in the CLI vocabulary, so that every surface forwards it without translating.
As a maintainer, I want a handler outcome from a child host to carry its report inside the outcome that already crosses IPC, so that attachMode does not change what gets logged and no separate telemetry message exists.
As an SDK consumer, I want the handler's report on the outcome that call returns, so that I read it there and no separate telemetry option exists.
As a maintainer reading a handler test, I want the telemetry assertion to be a plain equality on the returned outcome, so that I can see what the handler reports without knowing how any surface forwards it.
As a maintainer, I want telemetry failures never to affect the tool result, so that agents are unaffected by a slow endpoint.
As a maintainer, I want the change shipped as a 10.6.1 patch, so that the data break happens once, at the point the product lead accepted.
As an agent using the CLI, I want no change in output or exit codes, so that my workflows keep working.
As a Storybook user, I want no change in what MCP tools are available or how they respond, so that telemetry changes stay invisible to me.
Implementation Decisions
Handler contract: a handler outcome gains an optional telemetry field holding payload (counts and flags). That field is the only way a handler reports. The per-call telemetry callback on the handler context and the helper that wrapped it are removed. Handlers never write event, toolset, or tool. A handler that throws has no outcome and therefore no report; that case is dropped on purpose.
Toolset layer completes the report: the code that invokes a handler, shared by all three surfaces, fills toolset and tool from the toolset id and the method name, spelled as the CLI spells them, and generates event as tool:<toolsetId>_<methodName> with the method name as written in code (tool:stories_findByComponent). The outcome that reaches a surface carries one complete report: toolset, tool, event, payload. A new method gets all three names without writing anything.
CLI and SDK forward: the invocation reporter in the CLI command wiring and in the SDK call spreads the outcome's report into the one tools-command record and adds success, outcome, duration, and the attach fields. No translation, no collector, no per-call sink.
MCP adapter keeps the old event names: the toolset-backed MCP tool adapter sends the report's toolset, tool, and payload as the addon-mcp event, plus the session fields it already adds, with event set to the pre-toolset name (tool:getChangedStories) that the addon's tool registry declares per tool. That is the only place those names exist. The MCP grouping (dev, docs, test) stays what it is for the enable gate and the X-MCP-Toolsets header, and is no longer written to telemetry. The UI-instructions tool and session:initialized are untouched.
command removed: tools-command no longer carries command. toolset and tool are the two parts the agent invoked, spelled as the CLI spells them (stories, find-by-component), each sanitized to (invalid) when not a name-shaped token, and absent when no part was parsed. The SDK derives both from the method ref.
Child host: the outcome already crosses IPC as the call result, so the report travels with it. The separate telemetry message type and the pending-sink bookkeeping in the child client are removed.
SDK telemetry option removed: the per-call telemetry option on the SDK is deleted. Its only caller was the CLI, which now reads the report from the outcome, and any other caller gets the same report back from call.
No compatibility layer: the Node SDK has no external users yet, so its options and return shapes change freely; no shim, wrapper, or fallback is kept for an older SDK shape. The pre-toolset event names on addon-mcp records are the one concession to existing data.
show-story report: event tool:getDocumentationForStory, payload found, storyId (the resolved id when found, otherwise the requested one), lookup set to storyId or name, and resultTokenCount from the rendered markdown, mirroring the sibling show report.
Intercepts and gates: unchanged apart from the command replacement, one record without payload.
Telemetry types: the tools-command payload type gains toolset, tool, event, and an open payload bag, and loses command.
Testing Decisions
A good test asserts either the outcome a handler returns or the payload a surface hands to its mocked telemetry function. Nothing in between is asserted.
Handlers carry most of the coverage: the stories, docs, review, and vitest definition tests call the method through the toolset layer and assert the complete report on the outcome with a plain equality: toolset, tool, event, payload. Add showStory cases for found, missing story, missing component, and both lookup shapes.
CLI: the tools-command telemetry tests in the command wiring test have the mocked run return an outcome with a report and assert one tools-command call that is the report plus success, duration, and the attach fields. Intercept, placeholder, and help cases assert toolset and tool instead of command.
SDK: the createTools test asserts one record with client: sdk and that the outcome returned by call carries the report. The child-client test asserts the report arrives inside the outcome from the child.
MCP: one adapter test asserts that the addon-mcp payload is the tool's declared pre-toolset event, the report's toolset, tool, and payload, and one that an outcome without a report sends nothing.
Prior art: the tools-command telemetry describe block in the command wiring test, the surface-owned-fields test in the MCP adapter test, and the telemetry describe block in the vitest toolset definition test.
Out of Scope
Removing the X-MCP-Toolsets header and the toolsets disable option, planned for Storybook 11 as its own PR.
Renaming the legacy event names to the current tool names.
Adding fields to the skills-get event or the MCP UI-instructions tool.
Adding an invocation-level record with success and duration to the addon-mcp event.
Documentation changes; the docs list no telemetry events.
Further Notes
Discussed with Michael and Jeppe on 7 September 2026. Michael accepted one data break at 10.6.1; Jeppe reviews the PR.
The pre-toolset event names such as tool:getChangedStories derive from the pre-10.6 MCP tool names. They no longer match any tool name an agent sees and survive only on addon-mcp records, as a join key to older MCP data. CLI records from 10.6.0 carried them too; from 10.6.1 the CLI uses the generated names, accepted as part of the same data break.
Dashboards keyed on command for 10.6.0 data lose that key; accepted.
Revised 9 September 2026, third time, after Jeppe's review and a Slack thread with Michael: toolsets stop naming events; the event is generated from toolset and method on every surface, and only addon-mcp keeps the pre-toolset names, in its own registry. Kasper's proposal.
Revised 9 September 2026, after a second conversation with Michael: addon-mcp records also move to the CLI toolset and tool names, since the legacy event keeps joining them to older data. This drops the MCP-side toolset override the previous revision kept for compatibility.
Ships as a patch:yes PR against next.
Revised 9 September 2026: the first version of this spec kept the per-call telemetry callback and had the CLI collect and merge the handler's report. That was implemented in Telemetry: One tools-command record per invocation with CLI toolset and tool names #36210, and the collector, the SDK sink wrapping, and the IPC telemetry message it required made the change hard to read. Handlers now return the report on the outcome instead. The records that reach telemetry are identical in both versions.
Problem Statement
Telemetry for agent tool calls uses two different meanings of "toolset". The
addon-mcpevent reports the MCP grouping (dev,docs,test), while the CLI and SDK expose toolsets namedstories,review,docs, andtest. Thetools-commandevent copies the MCP grouping, so a CLI call toreview createis logged astoolset: dev, and the CLI toolset and tool names appear nowhere except inside acommandstring.On top of that, one CLI invocation writes two
tools-commandrecords: one from the command wrapper (command,success,duration, attach fields) and one from the method handler (event,toolset, counters), with no link between them. Counting rows double counts every successful command.Finally,
docs show-storynever reports telemetry on any surface. Its predecessor in@storybook/mcpnever had a telemetry hook, and the port preserved that gap.Solution
Every surface reports the same vocabulary: the CLI toolset and tool names, a generated
eventderived from them (tool:stories_changed), and the handler's payload. CLI and SDK records become one record per invocation and drop thecommandstring since it is derivable from the two parts.addon-mcprecords gaintooland switchtoolsetto the CLI name, but keep their pre-toolseteventnames (tool:getChangedStories) so MCP data stays continuous across versions. Theshow-storymethod gains a telemetry report on both surfaces.A handler no longer sends telemetry and never names an event. It returns a payload as part of its outcome, next to
dataandmarkdown. The toolset layer that runs the handler completes that into one report:toolsetandtoolfrom where the method is registered, a generatedevent(tool:<toolset>_<method>), and the payload. The CLI and SDK forward that report as is and add their run fields. The MCP adapter forwards it too, adds its session fields, and replaceseventwith the pre-toolset name it keeps per tool in the addon's own registry; the toolsets know nothing of those names.Before and after: CLI
npx storybook tools stories changedcommandstories changedeventtool:getChangedStoriestool:stories_changedtoolsetdevstoriestoolchangedsuccesstruetrueoutcomesuccesssuccessduration312312clientclicliclirequestedModeautoautoautoresolvedModelocallocallocalattachModelocallocallocalhostin-processin-processin-processstoryCount44newStoryCount11modifiedStoryCount33affectedStoryCount00The same applies to every other method. The SDK produces the same record with
client: sdk.Before and after: CLI intercepts
stories nopecommand: stories nopetoolset: stories,tool: nope../x listcommand: (invalid) listtoolset: (invalid),tool: listcommand: (none)toolset, notoolBefore and after: MCP
stories-changedeventtool:getChangedStoriestool:getChangedStoriestoolsetdevstoriestoolchangeddocsandtestkeep theirtoolsetvalue; only thedevgrouping splits intostoriesandreview.New:
show-storyon both surfaceseventtool:docs_showStorytool:getDocumentationForStorytoolsetdocsdocstoolshow-storyshow-storyfoundtruetruestoryIdbutton--primarybutton--primarylookupstoryIdstoryIdresultTokenCount412412The CLI row also carries
success,duration, and the attach fields.User Stories
tools-commandrecord to carrytoolsetandtoolin the CLI's own names, so that I can group agentic usage bystories,review,docs, andtestwithout parsing strings.tools-commandrecord per CLI invocation, so that counting records counts invocations.successandduration, so that I can correlate outcome with payload in one query.eventgenerated fromtoolsetandtool, so that a new tool gets a consistent name without anyone inventing one.addon-mcprecords to keep their pre-toolseteventnames and payload and gain the sametoolsetandtoolas CLI records, so that one query groups usage across surfaces and MCP data still joins to pre-10.6.1 data onevent.docs show-storyreported on both surfaces withfound,storyId,lookup, andresultTokenCount, so that the docs toolset is fully observable.show-story, so that I can decide whether to keep both shapes.toolsetandtoolthe agent typed, so that I can see which unknown names agents reach for.client: sdk, so that the two clients are comparable.success: false, so that failures are never lost.toolsetandtoolfilled in automatically on the CLI and SDK, so that I cannot forget them.attachModedoes not change what gets logged and no separate telemetry message exists.callreturns, so that I read it there and no separate telemetry option exists.Implementation Decisions
telemetryfield holdingpayload(counts and flags). That field is the only way a handler reports. The per-call telemetry callback on the handler context and the helper that wrapped it are removed. Handlers never writeevent,toolset, ortool. A handler that throws has no outcome and therefore no report; that case is dropped on purpose.toolsetandtoolfrom the toolset id and the method name, spelled as the CLI spells them, and generateseventastool:<toolsetId>_<methodName>with the method name as written in code (tool:stories_findByComponent). The outcome that reaches a surface carries one complete report:toolset,tool,event,payload. A new method gets all three names without writing anything.callspreads the outcome's report into the onetools-commandrecord and addssuccess,outcome,duration, and the attach fields. No translation, no collector, no per-call sink.toolset,tool, and payload as theaddon-mcpevent, plus the session fields it already adds, witheventset to the pre-toolset name (tool:getChangedStories) that the addon's tool registry declares per tool. That is the only place those names exist. The MCP grouping (dev,docs,test) stays what it is for the enable gate and theX-MCP-Toolsetsheader, and is no longer written to telemetry. The UI-instructions tool andsession:initializedare untouched.commandremoved:tools-commandno longer carriescommand.toolsetandtoolare the two parts the agent invoked, spelled as the CLI spells them (stories,find-by-component), each sanitized to(invalid)when not a name-shaped token, and absent when no part was parsed. The SDK derives both from the method ref.telemetryoption on the SDK is deleted. Its only caller was the CLI, which now reads the report from the outcome, and any other caller gets the same report back fromcall.eventnames onaddon-mcprecords are the one concession to existing data.show-storyreport: eventtool:getDocumentationForStory, payloadfound,storyId(the resolved id when found, otherwise the requested one),lookupset tostoryIdorname, andresultTokenCountfrom the rendered markdown, mirroring the siblingshowreport.commandreplacement, one record without payload.tools-commandpayload type gainstoolset,tool,event, and an openpayloadbag, and losescommand.Testing Decisions
toolset,tool,event,payload. AddshowStorycases for found, missing story, missing component, and both lookup shapes.tools-command telemetrytests in the command wiring test have the mocked run return an outcome with a report and assert onetools-commandcall that is the report plussuccess,duration, and the attach fields. Intercept, placeholder, and help cases asserttoolsetandtoolinstead ofcommand.createToolstest asserts one record withclient: sdkand that the outcome returned bycallcarries the report. The child-client test asserts the report arrives inside the outcome from the child.addon-mcppayload is the tool's declared pre-toolsetevent, the report'stoolset,tool, and payload, and one that an outcome without a report sends nothing.tools-command telemetrydescribe block in the command wiring test, the surface-owned-fields test in the MCP adapter test, and thetelemetrydescribe block in the vitest toolset definition test.Out of Scope
X-MCP-Toolsetsheader and the toolsets disable option, planned for Storybook 11 as its own PR.eventnames to the current tool names.skills-getevent or the MCP UI-instructions tool.successanddurationto theaddon-mcpevent.Further Notes
tool:getChangedStoriesderive from the pre-10.6 MCP tool names. They no longer match any tool name an agent sees and survive only onaddon-mcprecords, as a join key to older MCP data. CLI records from 10.6.0 carried them too; from 10.6.1 the CLI uses the generated names, accepted as part of the same data break.commandfor 10.6.0 data lose that key; accepted.addon-mcpkeeps the pre-toolset names, in its own registry. Kasper's proposal.addon-mcprecords also move to the CLItoolsetandtoolnames, since the legacyeventkeeps joining them to older data. This drops the MCP-sidetoolsetoverride the previous revision kept for compatibility.patch:yesPR againstnext.