Install
$ agentstack add skill-deeleeramone-pywry-events ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
PyWry Event System — Agent Reference
> 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_event or > 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 with tvchart_request_state (or the relevant state query) to confirm.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: deeleeramone
- Source: deeleeramone/PyWry
- License: Apache-2.0
- Homepage: https://deeleeramone.github.io/PyWry/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.