Skip to content

Add events subscription tools + streaming RPC support#3

Draft
Shimagon wants to merge 1 commit into
skrul:mainfrom
Shimagon:feat/events-tool-streaming
Draft

Add events subscription tools + streaming RPC support#3
Shimagon wants to merge 1 commit into
skrul:mainfrom
Shimagon:feat/events-tool-streaming

Conversation

@Shimagon
Copy link
Copy Markdown

@Shimagon Shimagon commented May 7, 2026

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_ONLY bypass fix). It does not depend on #1 — both branches are cut from main and modify disjoint regions of src/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)

Tool Purpose
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. Caps: 64 tracks × 16 event types per call.
poll_events Drain the server-streaming queue for a time window (default 3000 ms, max 10000) 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 (src/grpc/client.ts)

  • Header schema fix on sendStreamingRequest() — the existing implementation used command_id, which does not match the unary sendRequest() 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 in closedReason: 'error' | 'busy' plus an error field. Single-pass parse of response_body_json, with a one-time unwrap of nested event_data_json so callers do not have to double-parse.
  • pollInFlight guard — 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 with closedReason: 'busy' so the caller knows the second call did nothing and the first stream was not stomped.
  • activeSubscriptions Set — lets unsubscribe_events cancel 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 by session_id which changes on reconnect).
  • sanitizeErrorPayload() — drops session_id / *_path fields 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 to READ_ONLY_COMMANDS. They mutate only server-side filter state, not the .ptx session, so they are safe to leave gate-free. (#1 includes a broader READ_ONLY_COMMANDS cleanup; 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 Completed response and the activeSubscriptions set populates correctly, but the streaming RPC opens, stays empty for the full window, and closes with closedReason: '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 lint is broken on main independently of this PR (ESLint v9 needs eslint.config.js); not addressed here.
  • Subscribe response and tool registration verified end-to-end in Claude Desktop against PT Intro.
  • Concurrency guard: a second poll_events while a first is in flight is rejected immediately with closedReason: 'busy' rather than racing.

Out of scope

Marked draft because the Studio/Ultimate verification is open. Happy to keep iterating if the API shape on those tiers needs different handling.

…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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant