Add events subscription tools + streaming RPC support#3
Draft
Shimagon wants to merge 1 commit into
Draft
Conversation
…g RPC support
Adds three MCP tools that wrap the PTSL event-subscription / polling RPCs
(PT 2025.06+):
* `subscribe_track_events` — register interest in track-level events
(mute / solo / name changes by default), per-track filter via
`event_data_json: { track_id }`. Idempotent.
* `poll_events` — drain the server-streaming queue for a time
window (default 3000 ms) or until `max_events` is hit, then cancel and
return what was collected.
* `unsubscribe_events` — cancel previously-registered subscriptions;
no-args path uses the in-process bookkeeping set so the caller can
unsubscribe everything in one call.
Streaming RPC plumbing:
* `sendStreamingRequest()` header schema fix: was using `command_id` which
does not match the unary request schema (`command` + `version*`). PTSL
rejected the streaming variant with an unknown-command error.
* `collectStreamingEvents()`: window-based stream drainer that never throws —
failures surface in `closedReason: 'error' | 'busy'`. Uses single-pass
parsing of `response_body_json` and unwraps nested `event_data_json` once.
* `pollInFlight` guard: PTSL silently kills the previous PollEvents stream
when a second is opened. We reject the second concurrent call up-front
with `closedReason: 'busy'` so the caller knows the second call did not
actually start (and the first stream was not stomped).
* `activeSubscriptions` Set: lets `unsubscribe_events` cancel everything
this MCP-server process has subscribed to, in one call.
* `sanitizeErrorPayload()`: drops `session_id` / `*_path` fields from
surfaced PTSL error JSON to avoid leaking NDA-sensitive identifiers.
Permissions:
* `SubscribeToEvents`, `PollEvents`, `UnsubscribeFromEvents` are added to
`READ_ONLY_COMMANDS` because they only mutate server-side filter state,
not the `.ptx` session.
Known limitation noted in README:
* On Pro Tools Intro the subscribe RPC returns a Completed response but
no events are ever emitted to `poll_events` (streaming RPC opens but
stays empty until the window expires). Expected to work on Studio /
Ultimate but currently unverified there.
Related: skrul#1 (READ_ONLY bypass fix).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
Adds three MCP tools wrapping the PTSL event-subscription / polling RPCs (PT 2025.06+) and the streaming-RPC plumbing they need. Also fixes a header-schema bug on the streaming code path that would otherwise have made the existing
sendStreamingRequest()unusable.This is a follow-up to #1 (
READ_ONLYbypass fix). It does not depend on #1 — both branches are cut frommainand modify disjoint regions ofsrc/grpc/commands.ts. There is a separate PR coming that adds new PT 2025.06 command IDs (ImportAudioToClipList,SpotClipsByID,CreateAudioClips,BounceTrack); this one intentionally does not include those.What's in this PR
New MCP tools (
src/tools/events.ts)subscribe_track_eventsevent_data_json: { track_id }. Idempotent. Caps: 64 tracks × 16 event types per call.poll_eventsmax_eventsis hit, then cancel and return what was collected.unsubscribe_eventsStreaming RPC plumbing (
src/grpc/client.ts)sendStreamingRequest()— the existing implementation usedcommand_id, which does not match the unarysendRequest()schema (command+version*triplet). PTSL rejects the streaming variant with an unknown-command error today; this PR aligns the two header schemas so streaming RPCs actually go through.collectStreamingEvents()— a window-based stream drainer that never throws. Failures surface inclosedReason: 'error' | 'busy'plus anerrorfield. Single-pass parse ofresponse_body_json, with a one-time unwrap of nestedevent_data_jsonso callers do not have to double-parse.pollInFlightguard — PTSL allows only one Poll per session. When a second Poll is opened, the server silently kills the first stream, which looks like silent event loss to the caller. The guard rejects the second concurrent call up-front withclosedReason: 'busy'so the caller knows the second call did nothing and the first stream was not stomped.activeSubscriptionsSet — letsunsubscribe_eventscancel everything this MCP-server process subscribed to, in one call. Lives only for the lifetime of the process; if the server restarts, the caller must re-subscribe (PT-side state is keyed bysession_idwhich changes on reconnect).sanitizeErrorPayload()— dropssession_id/*_pathfields from surfaced PTSL error JSON. Useful when the server is deployed in NDA-sensitive environments where a stray error payload should not leak the underlying file path or session UUID.Permissions (
src/grpc/commands.ts)SubscribeToEvents(132),PollEvents(135),UnsubscribeFromEvents(136) are added toREAD_ONLY_COMMANDS. They mutate only server-side filter state, not the.ptxsession, so they are safe to leave gate-free. (#1 includes a broaderREAD_ONLY_COMMANDScleanup; this PR only adds the three new entries and does not touch the lines that PR1 changes.)README
Added an entry to Known Limitations documenting the Intro-tier behavior (see below).
Known limitation: Pro Tools Intro
On my local rig (Pro Tools Intro 2025.10) the subscribe RPC returns a
Completedresponse and theactiveSubscriptionsset populates correctly, but the streaming RPC opens, stays empty for the full window, and closes withclosedReason: 'timeout'— i.e. PT never emits any events to it. This appears to be a tier-gated feature.Expected to work on Pro Tools Studio / Ultimate, but currently unverified there — I do not have a Studio/Ultimate license to test against. If a maintainer or contributor can confirm event emission on a higher tier, that would close the loop on this PR. The shape of the responses can be inferred from the PTSL.proto, so the parsing path should already be correct, but real traffic is the only way to be sure.
Verification
npm run build(tsc) passes locally on this branch.npm run lintis broken onmainindependently of this PR (ESLint v9 needseslint.config.js); not addressed here.poll_eventswhile a first is in flight is rejected immediately withclosedReason: 'busy'rather than racing.Out of scope
READ_ONLY_COMMANDScleanup in Critical: state-mutating commands wrongly classified as READ_ONLY (ALLOW_WRITES bypass) #1 — separate PR for the same reason.Marked draft because the Studio/Ultimate verification is open. Happy to keep iterating if the API shape on those tiers needs different handling.