Add audit events for MCP server block and unblock
What does this MR do and why?
This adds audit events for blocking and unblocking external MCP servers, the kill-switch governance feature for the AI Catalog. It's the second MR in a series; MR 1 (!251329 (merged), "Filter blocked external MCP servers from Duo Workflow config") added the enforcement side (filtering blocked servers out of Duo Workflow config). This MR adds the compliance visibility side.
Two new audit event types are added: block_ai_catalog_mcp_server and unblock_ai_catalog_mcp_server. They're emitted from Ai::Catalog::McpServers::SetBlockService when a group or project owner blocks or allows an external MCP server.
These events are scoped [Group, Project], unlike the existing MCP server CRUD events, which are scoped [Instance]. Blocking is a namespace-level action: the event needs to show up in that group's or project's own audit log and stream to that namespace's destinations, since that's where the namespace's compliance admin is looking. Project blocks are stored against the project's project-namespace internally (for uniform ancestry resolution), but the event is deliberately scoped to the Project itself so it lands in the project's audit log, not somewhere less obvious.
Events only fire on a real state change. block!/unblock! are idempotent (find-or-create for block, delete-if-present for unblock), so a repeated block or a no-op unblock emits nothing. Without this, double-clicking the toggle would write phantom compliance entries. This is detected via previously_new_record? on the created row for block, and the delete_all count for unblock.
This deliberately does not reuse McpServers::BaseService#send_audit_events (it hardcodes InstanceScope, wrong for this case) or AuditEventMessageService (that exists for CRUD change descriptions; these messages are simple one-liners with the server URL, no diffing needed).
No feature flag. This is deliberate: SetBlockService is already live and unflagged in production, and gating compliance visibility behind a flag would defeat the purpose of the feature. Changelog: added, EE: true.
doc/user/compliance/audit_event_types.md has been regenerated, since a spec enforces that the docs stay in sync with the audit type definitions.
Database
No migrations, no new tables, no new indexes. Writes go through the existing Gitlab::Audit::Auditor pipeline into the partitioned audit-event tables.
References
- Resolves: https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/work_items/2742 (this MR covers the audit-events half; the enforcement metric counter is a follow-up after !251329 (merged) merges, since it touches the same file)
- Public issue: #623216 (closed)
- Epic: https://gitlab.com/groups/gitlab-org/-/work_items/21377
- Related (enforcement): !251329 (merged)
Screenshots or screen recordings
Events are visible on both the group and project audit event pages (Secure > Audit events), with author, a message like "Blocked MCP server (URL: ...)", and target "Context7 Docs (ID: 1)".
| Group audit events | Project audit events |
|---|---|
![]() |
![]() |
How to set up and validate locally
-
As a group or project owner, run the
aiCatalogMcpServerSetBlockGraphQL mutation in the GraphQL explorer:mutation { aiCatalogMcpServerSetBlock(input: { id: "gid://gitlab/Ai::Catalog::McpServer/1" groupFullPath: "gitlab-org" blocked: true }) { errors } } -
Check the group's audit events page: a new "Blocked MCP server (URL: ...)" entry should appear.
-
Run the same mutation again with no changes: no new entry should be created (idempotency guard).
-
Rerun with
blocked: false: an "Allowed MCP server (URL: ...)" entry should appear. -
Variant: use
projectFullPathinstead ofgroupFullPath. The event should appear in the project's audit events page instead.
Alternatively, the block toggle can be triggered from Group Settings > GitLab Duo > Governance (MCP Registry), behind the gitlab_duo_governance_settings flag.
Verification done on this branch: 15 RSpec examples on the service spec, covering emission for both group and project scopes, both no-op cases, permission and org-mismatch failures, and project unblock. Also covered: audit-type definition specs and the docs-sync check. Beyond specs, I ran this live on GDK: service calls, GraphQL both in-process and over HTTP, with events verified rendering correctly in the group, project, and instance audit UIs. Streaming was verified end-to-end to a real HTTP destination, with payloads carrying the correct event_type, entity, and message. The role matrix was verified live (guest/developer/maintainer denied and unaudited, owner allowed). Subgroup events land on the subgroup's own log, and events are consumable via the group audit events REST API. A concurrency test surfaced a pre-existing race in McpServerBlock.block! (the losing request raises RecordInvalid); this predates this MR and will be filed and fixed separately.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

