PyWry Event System — Agent Reference
SkillAI & modelsThe PyWry event system — namespaced events, request/response round-trips, widget IDs, component IDs, and how tool results flow back to the agent.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the PyWry Event System — Agent Reference skill
What this skill tells your AI
The instructions your AI receives, as published by deeleeramone/pywry in pywry/pywry/mcp/skills/events/SKILL.md and read by ahel’s review.
The event bus is the plumbing underneath every MCP tool. You rarely need to think about it — the typed tools wrap emit + wait + state-poll for you — but when you reach for
send_eventor interpret tool results, this is how it works.
Event names are namespaced
Every event has the form namespace:event-name, e.g.:
tvchart:symbol-search— ask the chart to open symbol searchtvchart:state-response— chart's reply with its current statetvchart:data-request— chart asks Python for barstvchart:data-response— Python delivers barstoolbar:request-state— ask a toolbar component for its valuetoolbar:state-response— component's replychat:user-message— user typed somethingchat:ai-response— model produced a tokenpywry:update-theme— dark/light mode change
Never emit an event with a name that doesn't match namespace:event-name
— the framework rejects it.
Widget IDs vs component IDs
widget_id — identifies the top-level PyWry widget (a chart, a grid,
a chat panel, a dashboard). Every MCP tool takes widget_id as an
argument because all events route to the widget first.
componentId — identifies a child inside a widget (a specific toolbar button, a marquee ticker slot, a chart pane). Component IDs are scoped to their containing widget.
When you call send_event(widget_id, event_type, data), the
widget_id picks the target widget; anything identifying a specific
component goes in the data payload (typically as data.componentId
or data.chartId).
Request / response pattern
Some events are fire-and-forget (e.g. tvchart:symbol-search —
"please do this"). Others are request/response round-trips where the
caller wants a reply (e.g. tvchart:request-state → tvchart:state-response).
The framework correlates request/response with a context token:
- Emitter generates a random
contexttoken. - Emitter injects it into the request payload.
- Listener sees the request, attaches the same
contextto its response, and emits the response event. - Emitter sees the matching
contexton the response and wakes up.
All of this is handled inside request_response() in
pywry.mcp.state — you never construct tokens yourself. Typed MCP
tools that need a reply (tvchart_request_state,
tvchart_list_indicators) use this under the hood and return the
stripped response (no context token) in their tool result.
How tool results reach the agent
Agent MCP Server PyWry Widget (JS)
│ │ │
│ tool call ──────► │ │
│ │ widget.emit() ──────► │
│ │ │ (updates chart)
│ │ │
│ │ ◄───── bridge.emit() │
│ │ (state-response) │
│ │ │
│ ◄──── tool result │ │
│ (includes state) │ │
Mutating tools (tvchart_symbol_search, tvchart_change_interval)
poll tvchart:request-state after emitting the mutation, wait for the
chart to actually reflect the change, and return the real post-change
state in the tool result. The state field in the tool result
contains the SAME structure as a direct call to
tvchart_request_state.
If the mutation didn't settle in time, the result contains a note
field explaining the discrepancy — relay the note to the user, do
not invent state.
Emitting events from tools — send_event
Only reach for send_event when no typed MCP tool exists for the
target event. It's a raw passthrough with no state polling:
send_event(widget_id, event_type, data)
→ { "widget_id": ..., "event_sent": true, "event_type": ... }
Example — apply a rare tvchart option with no typed wrapper:
send_event(
widget_id="chart",
event_type="tvchart:apply-options",
data={"chartOptions": {"timeScale": {"secondsVisible": False}}},
)
The returned event_sent: true means the event was successfully
handed to the widget — it does NOT mean the JS handler ran
successfully. For confirmation, follow up with
tvchart_request_state to read the new state.
Event capture (get_events)
Some events fire from the widget to Python (e.g. the user clicked a bar, moved the crosshair). These are automatically captured into a per-widget event buffer and can be retrieved with:
get_events(widget_id, event_types=[...], clear=True)
→ { "events": [{ "event_type": ..., "data": ..., "label": ... }, ...] }
Default captured events for charts:
tvchart:clicktvchart:crosshair-movetvchart:visible-range-changetvchart:drawing-addedtvchart:drawing-deletedtvchart:open-layout-requesttvchart:interval-changetvchart:chart-type-change
Use get_events if the user asks "what did I just click" or "what
was the last drawing I added".
Don'ts
- Do NOT synthesise event payloads. Only report event data the framework actually handed you.
- Do NOT emit events whose name doesn't match
ns:name— they're rejected. - Do NOT emit to a widget id that isn't registered — the tool will return an error listing the registered widgets; correct and retry.
- Do NOT assume
event_sent: truemeans the downstream JS succeeded. When it matters, follow up withtvchart_request_state(or the relevant state query) to confirm.
Signals
- GitHub stars
- 93
- Forks
- 6
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
events- Source
- github.com/deeleeramone/pywry