Skip to content

feat(mcp): sync_custom_registry tool with org-scoped service refactor#2568

Merged
topher-lo merged 5 commits intomainfrom
feat/registry-sync-mcp-2
Apr 26, 2026
Merged

feat(mcp): sync_custom_registry tool with org-scoped service refactor#2568
topher-lo merged 5 commits intomainfrom
feat/registry-sync-mcp-2

Conversation

@topher-lo
Copy link
Copy Markdown
Contributor

@topher-lo topher-lo commented Apr 25, 2026

Summary

  • Adds the workflows_sync_custom_registry MCP tool so org admins can sync their custom action registry from any MCP client without picking a workspace.
  • Consolidates the sync pipeline onto RegistryReposService.sync_repository, gated by @require_scope("org:registry:update"), so the existing HTTP route and the new MCP tool share one source of truth instead of duplicating entitlement / SSH / force-version-delete / action-sync logic.
  • Mirrors the API layer's multi-org guard in MCP via a new resolve_org_role_for_request helper that queries OrganizationMembership directly (same pattern as _resolve_org_for_regular_user).

Why

#2565 added the same MCP tool but left the existing POST /registry/repos/{id}/sync route owning the full sync pipeline inline, with the service method re-implementing it next door. This PR keeps the new MCP capability while turning the route into a thin caller of the service so the two paths can't drift.

Behavior changes

  • RegistryReposService.sync_repository(repository, sync_params) now takes a RegistryRepository (not repository_id) so callers reuse the org-scoped lookup they already did. The route fetches once at the top for 404 handling and reuses the same instance for the 422 validation-error body.
  • RegistryReposService.list_repositories is gated on org:registry:read — it had no other callers, and read scope matches the operation's intent.
  • sync_custom_registry filters out both DEFAULT_REGISTRY_ORIGIN and DEFAULT_LOCAL_REGISTRY_ORIGIN so a deployment running both built-ins still resolves to a single custom repo. The "exactly one custom registry" check stays as a defensive guard documented inline.
  • The MCP tool no longer takes workspace_id; the org is resolved from the auth token. Multi-org tokens get a clear error matching the HTTP API's behavior.
  • sync_repository and the new Updated repository log line key off repository_id rather than origin to remove a theoretical credential-leak path.

Test plan

  • uv run pytest tests/unit/test_registry_repositories_service.py tests/unit/test_registry_repositories_router.py tests/unit/api/test_api_registry_repositories_entitlements.py tests/unit/test_route_scope_guards.py tests/unit/test_mcp_server.py (256 passed)
  • uv run ruff check and uv run ruff format --check on changed files
  • uv run basedpyright --warnings tracecat/registry/repositories tracecat/mcp/server.py tracecat/mcp/auth.py (0 errors, 0 warnings)
  • Manual: hit POST /registry/repos/{id}/sync against a registered git+ssh repo via just cluster up -d and confirm response shape is unchanged.
  • Manual: invoke workflows_sync_custom_registry from an MCP client against an org with one custom registry; confirm success and per-repo result fields.
  • Manual: invoke against an org with no custom registry → expect No custom registry repository found ToolError.

🤖 Generated with Claude Code


Summary by cubic

Adds the workflows_sync_custom_registry MCP tool to sync an org’s custom action registry without a workspace, and consolidates the sync flow into RegistryReposService.sync_repository with org-scoped scopes and entitlements. Honors token org scoping (with single-tenant superuser default‑org fallback), adds a cross‑org 404, and returns generic 500s.

  • New Features

    • workflows_sync_custom_registry: sync to HEAD or target_commit_sha, optional force, and clear ToolErrors for missing scope or org context; filters out DEFAULT_REGISTRY_ORIGIN and DEFAULT_LOCAL_REGISTRY_ORIGIN and reports per‑repo results (version, commit_sha, actions_count, errors).
    • resolve_org_role_for_request: intersects token org scope with memberships; clear errors for multi‑org tokens and unowned scopes; single‑tenant superusers fall back to the default org.
  • Refactors

    • Moved the full sync pipeline into RegistryReposService.sync_repository, gated by @require_scope("org:registry:update"), with entitlement checks for non‑default repos; route now delegates and returns a generic 500 detail.
    • RegistryReposService.list_repositories now requires org:registry:read; sync_repository rejects cross‑org repositories with RegistryNotFound (mapped to 404); logs use repository_id instead of origin.

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

@mintlify
Copy link
Copy Markdown
Contributor

mintlify Bot commented Apr 25, 2026

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
tracecat 🟢 Ready View Preview Apr 25, 2026, 8:01 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@zeropath-ai
Copy link
Copy Markdown

zeropath-ai Bot commented Apr 25, 2026

No security or compliance issues detected. Reviewed everything up to dfa7e14.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► docs/snippets/mcp-tools.mdx
    Add workflows_sync_custom_registry tool description
► tracecat/mcp/server.py
    Implement sync_custom_registry tool
    Add CustomRegistrySyncResult and CustomRegistrySyncResponse models
Refactor ► tracecat/mcp/auth.py
    Add resolve_org_role_for_request function
► tracecat/mcp/server.py
    Add _resolve_org_role helper function
Bug Fix ► tests/unit/api/test_api_registry_repositories_entitlements.py
    Remove unused check_entitlement patch in test
► tests/unit/test_mcp_auth.py
    Fix _patch_resolve_org_role_deps to mock get_async_session_bypass_rls_context_manager correctly
    Update test_resolve_org_role_single_tenant_superuser_uses_default_org to mock get_default_organization_id
Other relevant categories ► tests/unit/test_mcp_server.py
    Add tests for sync_custom_registry tool
    Add _patch_sync_custom_registry helper
► tests/unit/test_registry_repositories_service.py
    Add tests for sync_repository and list_repositories scope requirements
    Add tests for cross-org repository rejection in sync_repository
► tracecat/registry/repositories/router.py
    Update sync_registry_repository endpoint to use RegistryReposService.sync_repository
    Handle EntitlementRequired and RegistryNotFound exceptions
► tracecat/registry/repositories/service.py
    Implement sync_repository method
    Add scope requirement for list_repositories and sync_repository
    Add defense-in-depth check for cross-org repositories in sync_repository

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 8 files

Confidence score: 3/5

  • There is some merge risk because resolve_org_role_for_request in tracecat/mcp/auth.py appears to ignore token org scoping, which can incorrectly reject valid single-org tokens for users with multiple org memberships.
  • tracecat/registry/repositories/router.py currently surfaces raw exception text in 500 responses; sanitizing this to a generic message is important to avoid leaking internal details at API boundaries.
  • Pay close attention to tracecat/mcp/auth.py and tracecat/registry/repositories/router.py - auth role resolution may block legitimate access, and error handling should avoid exposing unexpected internal exception content.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tracecat/registry/repositories/router.py">

<violation number="1" location="tracecat/registry/repositories/router.py:129">
P2: Do not include raw exception messages in 500 responses; return a sanitized generic error message instead.

(Based on your team's feedback about sanitizing unexpected error responses at router boundaries.) [FEEDBACK_USED]</violation>
</file>

<file name="tracecat/mcp/auth.py">

<violation number="1" location="tracecat/mcp/auth.py:1330">
P1: `resolve_org_role_for_request` ignores token org scoping and resolves ambiguity from all memberships, which can reject valid single-org tokens for multi-org users.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread tracecat/mcp/auth.py Outdated
Comment thread tracecat/registry/repositories/router.py Outdated
@blacksmith-sh
Copy link
Copy Markdown
Contributor

blacksmith-sh Bot commented Apr 25, 2026

Found 8 test failures on Blacksmith runners:

Failures

Test View Logs
test_large_collection_regressions/
test_looped_subflow_large_payload_no_s3_select_dependency
View Logs
test_workflows/test_child_workflow_success View Logs
test_workflows/test_multiple_child_workflow_override_environment_correct View Logs
test_workflows/test_workflow_env_and_trigger_access_in_stream View Logs
test_workflows/test_workflow_scatter_gather[nested-for-loop] View Logs
test_workflows/test_workflow_scatter_gather[nested-varying-sizes] View Logs
test_workflows/test_workflow_scatter_gather[parallel-scatter-gather-join] View Logs
test_workflows/
test_workflow_scatter_gather[scatter-reshape-even-gather-drop-nulls-with-nones]
View Logs

Fix in Cursor

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd9d6e5b49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tracecat/mcp/auth.py Outdated
@topher-lo topher-lo added the mcp Tracecat MCP Connector label Apr 25, 2026
@topher-lo topher-lo temporarily deployed to internal-registry-ci April 25, 2026 20:30 — with GitHub Actions Inactive
@topher-lo topher-lo temporarily deployed to internal-registry-ci April 25, 2026 20:30 — with GitHub Actions Inactive
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae11ef3119

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tracecat/mcp/auth.py
@topher-lo topher-lo temporarily deployed to internal-registry-ci April 25, 2026 20:50 — with GitHub Actions Inactive
@topher-lo topher-lo temporarily deployed to internal-registry-ci April 25, 2026 20:50 — with GitHub Actions Inactive
@topher-lo topher-lo temporarily deployed to internal-registry-ci April 25, 2026 20:55 — with GitHub Actions Inactive
@topher-lo topher-lo temporarily deployed to internal-registry-ci April 25, 2026 20:55 — with GitHub Actions Inactive
@topher-lo topher-lo temporarily deployed to internal-registry-ci April 26, 2026 01:55 — with GitHub Actions Inactive
@topher-lo topher-lo temporarily deployed to internal-registry-ci April 26, 2026 01:56 — with GitHub Actions Inactive
topher-lo and others added 5 commits April 25, 2026 22:00
…ctor

Introduce a workspace-less MCP tool for syncing the org's custom action
registry, and consolidate the sync pipeline onto RegistryReposService so
both the HTTP route and MCP tool share a single source of truth gated by
@require_scope("org:registry:update").

- Move full sync pipeline (entitlement check, force-version delete, SSH
  context, action sync, repo update) into RegistryReposService.sync_repository
- Slim sync_registry_repository route to exception-to-HTTP translation; pre-
  fetch repo once and reuse for the 422 validation-error path
- Add sync_custom_registry MCP tool that resolves org context from the token
  via new resolve_org_role_for_request helper (mirrors API's
  _resolve_org_for_regular_user multi-org error handling)
- Filter both DEFAULT_REGISTRY_ORIGIN and DEFAULT_LOCAL_REGISTRY_ORIGIN so
  deployments running both still resolve to a single custom repo
- Gate list_repositories on org:registry:read (matches its semantics)
- Log repository_id rather than origin to avoid theoretical credential leakage
- Add docs entry, service-level scope tests, and full MCP tool coverage

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Honor token org scoping in resolve_org_role_for_request, sanitize the
sync route's 500 detail, and replace the heavy fakes in the new MCP
tests with stdlib AsyncMock.

- resolve_org_role_for_request now intersects identity.organization_ids
  with the user's OrganizationMembership rows before the ambiguity
  check, mirroring list_user_workspaces. Single-org-scoped tokens
  resolve cleanly even when the user is a member of multiple orgs.
- sync_registry_repository's 500 path returns a generic detail; the
  exception is still logged with full context.
- tests/unit/test_mcp_server.py drops _UNSET, three handwritten fake
  classes, and two patch helpers; the nine sync_custom_registry tests
  now use AsyncMock + a single _patch_sync_custom_registry helper.
- tests/unit/test_mcp_auth.py adds four focused tests for the new
  org-scoping intersection (scoped+multi-org, unscoped+single-org,
  unscoped+multi-org, token scoped to unowned org).

Verified: ruff/format/basedpyright clean; 306 affected unit tests pass;
7 of the 8 CI temporal failures pass locally on this branch
(test_workflow_table_actions_in_loop has an httpx ConnectError to the
executor service that is environmental and unrelated to this changeset).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Single-tenant platform superusers don't carry OrganizationMembership rows,
so the previous code rejected them with "no organization memberships" the
moment sync_custom_registry routed through resolve_org_role_for_request.
Mirror the HTTP API's _resolve_org_for_superuser pattern: when the user
is a superuser and TRACECAT__EE_MULTI_TENANT is off, fall back to
get_default_organization_id instead of querying memberships.

Add a focused test covering the fallback path.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Pull get_default_organization_id (in mcp/auth.py) and
get_setting/ssh_context/Entitlement/check_entitlement (in
registry/repositories/service.py) up to module level. Verified
non-circular by inspecting the import graph.

Two imports remain lazy with a comment explaining why:
  - tracecat.git.utils.parse_git_url
  - tracecat.registry.actions.service.RegistryActionsService
Both transitively import RegistryReposService back via
tracecat.registry.repository, which would cycle if pulled to module
scope.

Update the superuser-fallback test to monkeypatch
mcp_auth.get_default_organization_id directly now that the symbol is
bound at module level.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The service method now accepts a RegistryRepository instance and trusts
the caller to have done the org-scoped lookup. Both real callers (the
HTTP route's get_repository_by_id and the MCP tool's list_repositories)
do filter by org, so no exploit exists today, but a future caller that
bypasses those lookups would expose IDOR.

Add an inline assertion on repository.organization_id == self.organization_id
inside sync_repository. On mismatch raise the existing domain exception
RegistryNotFound("Registry repository not found"); the route catches it
before the broader RegistryError clause and maps it to the same 404 it
already returns for missing IDs. Cross-org probing is therefore
indistinguishable from a missing repository.

Add a focused unit test covering the cross-org rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dfa7e14f9e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tracecat/mcp/server.py
@topher-lo topher-lo merged commit 1d20a64 into main Apr 26, 2026
16 of 18 checks passed
@topher-lo topher-lo deleted the feat/registry-sync-mcp-2 branch April 26, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mcp Tracecat MCP Connector

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant