Skip to content

Tag the recordings operations as their own domain, and require a tag on every operation - #922

Draft
jeremy wants to merge 2 commits into
mainfrom
sdk/tag-events-recordings
Draft

jeremy wants to merge 2 commits into
mainfrom
sdk/tag-events-recordings

Conversation

@jeremy

@jeremy jeremy commented Sep 16, 2026

Copy link
Copy Markdown
Member

Why

basecamp-mcp-server builds its MCP tool catalog by grouping SDK operations by their OpenAPI tag — one tag per operation, each tag becoming one domain tool (basecamp_recordings, basecamp_events, …). An operation with no tag of its own gets dropped or folded into an unrelated domain.

Two recently-shipped, deliberately-designed surfaces were exposed to this:

  • Event feed (account-wide feed, agent inbox, stream-ticket minting — the AccountClient.EventFeed() service added in 0.19.0). Already fixedAccount event feed, inbox, and stream tickets: spec operations and generated clients #898 tagged PollEvents/PollInbox/CreateStreamTicket with EventFeed. No change needed here; noted for completeness.
  • Recordings (recording lifecycle). Still folded in: the six ops were tagged Automation, so adopting the SDK's native tag mapping would fold basecamp_recordings into basecamp_admin. This PR fixes that.

What was untagged / mis-tagged

All 265 operations already carry exactly one tag in openapi.json; the recordings gap was mis-tagging, not a literal absent tag. These six carried Automation:

ListRecordings, SpotlightRecording, UnspotlightRecording, TrashRecording, ArchiveRecording, UnarchiveRecording

The tags added

Retagged those six under a new Recordings tag in spec/overlays/tags.smithy, mirroring the Recordings service every SDK generator already emits for exactly these ops. Recording boosts stay under Boosts and the recording timesheet stays under Schedule → Timesheets, matching the SDK service groupings — this is the coherent recordings domain, nothing more.

Regeneration

make smithy-build, make url-routes, and the TypeScript generator. Because each SDK generator assigns services from its own split table (not the OpenAPI tag), all seven SDKs regenerate byte-identical — verified: ruby regen produced zero diff, and check-operation-assignment-parity (all 6 SDKs), check-service-inventory-parity, and kt-check-drift are all clean. The only committed artifacts that change are the three that copy the tag verbatim:

  • openapi.json (6 operation tags: AutomationRecordings)
  • go/pkg/basecamp/url-routes.json (the resource field for recording routes now reads Recordings instead of the stale Automation — a latent consistency fix)
  • typescript/src/generated/openapi-stripped.json

behavior-model.json is unchanged — tags live only in the OpenAPI projection.

Require-tags guard (fail-closed) — implemented

Survey first: 0 untagged operations, 0 multi-tagged — all 265 carry exactly one tag. Because the exception set is empty, the fail-closed check is clean to add, so I implemented it rather than deferring.

scripts/check-required-tags.rb reads the committed openapi.json (the artifact catalog.Load consumes) and fails if any operation has zero tags or more than one. It's an accident-class guard: it stops a new operation added to the Smithy model without a matching tags.smithy entry from shipping untagged. Allowlist is empty and documented — every Basecamp API operation is a catalog operation; a genuinely tag-less op would be added by operationId with a reason rather than by weakening the check. It also fails closed on a spec that yields no operations (a truncated openapi.json can't pass vacuously).

scripts/test-check-required-tags.rb drives it with crafted specs (untagged, empty-tagged, multi-tagged, allowlisted, empty, path-level parameters) since the live run only exercises the passing case. Both are wired into make check (check-targets) and the spec-gates CI job (bash/ruby, no toolchain). Adversarial check: stripping a tag from a copy of openapi.json makes the gate exit 1 and name the offender.

Follow-up linkage

After this merges and a new SDK release is cut, mcp-server's #140 / M2 can consume the Recordings (and EventFeed) tags so basecamp_recordings and basecamp_events keep their own MCP domains instead of folding into basecamp_admin/basecamp_schedules.


Summary by cubic

Retags the six recording lifecycle operations from Automation to Recordings so MCP catalog generation gives them their own basecamp_recordings domain instead of folding them into basecamp_admin. Adds a fail-closed check that every operation in openapi.json carries exactly one tag.

  • All seven SDKs regenerate byte-identical; only the artifacts that copy tags change — openapi.json, Go url-routes.json (whose stale Automation resource label becomes Recordings), and TypeScript openapi-stripped.json.
  • The new tag mirrors the Recordings service each SDK generator already emits; recording boosts stay under Boosts and the recording timesheet stays under Schedule.
  • scripts/check-required-tags.rb rejects zero, multiple, or absent operations and is wired into make check and the spec-gates CI job, with a self-test covering the negative cases.

Written for commit de86704. Summary will update on new commits.

Review in cubic

…s own domain

basecamp/mcp's catalog.Load groups SDK operations into MCP domain tools by
their OpenAPI tag, one tag per operation. The six recording-lifecycle
operations — ListRecordings, SpotlightRecording, UnspotlightRecording,
TrashRecording, ArchiveRecording, UnarchiveRecording — carried the Automation
tag, so adopting the SDK's native tag mapping would fold basecamp_recordings
into basecamp_admin and silently drop the deliberately-designed recordings
tool surface. (The account event-feed surface already got its own EventFeed
tag in #898; this closes the matching gap for recordings.)

Retag the six under a new Recordings tag, mirroring the Recordings service
every SDK generator already emits for exactly these operations. Recording
boosts stay under Boosts and the recording timesheet stays under Schedule ->
Timesheets, matching the SDK service groupings. Each generator assigns services
from its own split table rather than from the OpenAPI tag, so all seven SDKs
regenerate byte-identical; the artifacts that copy the tag verbatim change:
openapi.json, the go url-routes resource field (which now reads "Recordings"
for these routes instead of the stale "Automation"), and the TypeScript
vendored openapi-stripped.json.

Regenerated via make smithy-build, make url-routes, and the TypeScript
generator. behavior-model.json is unchanged: tags live only in the OpenAPI
projection.
…e tag

The recordings-into-Automation fold happened because nothing enforced that
every operation carries its own tag: an operation added to the Smithy model
without a matching entry in spec/overlays/tags.smithy reaches openapi.json
untagged, and catalog.Load then drops it or folds it into an unrelated domain.
This is an accident-class guard — anyone who can add an operation can add a
tag; the check exists so they cannot do the first and forget the second.

check-required-tags.rb reads the committed openapi.json — the same artifact
catalog.Load consumes — and fails if any operation has zero tags or more than
one. The allowlist is empty and documented: every Basecamp API operation is a
catalog operation and must carry a domain tag; a genuinely tag-less operation
would be added by operationId with a reason rather than by weakening the check.
It fails closed on a spec that yields no operations, so a truncated
openapi.json cannot pass vacuously. The smithy-verify "OpenAPI is up to date"
step already proves openapi.json is regenerated from the model, so a dropped
tag reaches this gate rather than hiding behind a stale file.

test-check-required-tags.rb drives the check with crafted openapi documents
(untagged, empty-tagged, multi-tagged, allowlisted, empty, and path-level
parameters) since the live run only ever exercises the passing case. Both are
wired into make check and the spec-gates CI job (bash/ruby, no toolchain).
@github-actions

Copy link
Copy Markdown
Contributor

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • .github/workflows/test.yml

Shadow mode — this check is informational only. When activated, changes to these paths will require approval from a maintainer.

@github-actions github-actions Bot added github-actions Pull requests that update GitHub Actions typescript Pull requests that update TypeScript code go spec Changes to the Smithy spec or OpenAPI labels Sep 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

github-actions Pull requests that update GitHub Actions go spec Changes to the Smithy spec or OpenAPI typescript Pull requests that update TypeScript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant