From 5bf6a67c069455f0a5ec04918a6564aceb7064d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 8 May 2026 19:08:04 +0400 Subject: [PATCH] mcp: add TrackerResource tools and drop SCRIPT/IFRAME from tracker_type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove SCRIPT/IFRAME from the tracker_type enum in the MCP spec. - Add TrackerResource schema in components/schemas. - Add 6 tools: listTrackerResources, getTrackerResource, addTrackerResource, updateTrackerResource, deleteTrackerResource, moveTrackerResourceToCategory with input/output schemas. - Add types/tracker_resource.go helper and resolver implementations. Signed-off-by: Émile Ré --- pkg/server/api/mcp/v1/schema.resolvers.go | 85 ++++++ pkg/server/api/mcp/v1/specification.yaml | 254 +++++++++++++++++- .../api/mcp/v1/types/tracker_resource.go | 56 ++++ 3 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 pkg/server/api/mcp/v1/types/tracker_resource.go diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index eb746aaad..2363f8843 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -5180,3 +5180,88 @@ func (r *Resolver) PublishDocumentTool(ctx context.Context, req *mcp.CallToolReq } return nil, output, nil } + +func (r *Resolver) ListTrackerResourcesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListTrackerResourcesInput) (*mcp.CallToolResult, types.ListTrackerResourcesOutput, error) { + r.MustAuthorize(ctx, input.CookieCategoryID, probo.ActionTrackerResourceList) + scope := coredata.NewScopeFromObjectID(input.CookieCategoryID) + cursor := types.NewCursor(input.Size, input.Cursor, page.OrderBy[coredata.TrackerResourceOrderField]{Field: coredata.TrackerResourceOrderFieldCreatedAt, Direction: page.OrderDirectionAsc}) + resources, err := r.cookieBanner.ListTrackerResourcesForCategory(ctx, scope, input.CookieCategoryID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list tracker resources: %w", err)) + } + p := page.NewPage(resources, cursor) + return nil, types.NewListTrackerResourcesOutput(p), nil +} + +func (r *Resolver) GetTrackerResourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetTrackerResourceInput) (*mcp.CallToolResult, types.GetTrackerResourceOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionTrackerResourceGet) + scope := coredata.NewScopeFromObjectID(input.ID) + resource, err := r.cookieBanner.GetTrackerResource(ctx, scope, input.ID) + if err != nil { + return nil, types.GetTrackerResourceOutput{}, fmt.Errorf("cannot get tracker resource: %w", err) + } + return nil, types.GetTrackerResourceOutput{TrackerResource: types.NewTrackerResource(resource)}, nil +} + +func (r *Resolver) AddTrackerResourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddTrackerResourceInput) (*mcp.CallToolResult, types.AddTrackerResourceOutput, error) { + r.MustAuthorize(ctx, input.CookieCategoryID, probo.ActionTrackerResourceCreate) + scope := coredata.NewScopeFromObjectID(input.CookieCategoryID) + description := "" + if input.Description != nil { + description = *input.Description + } + resource, err := r.cookieBanner.CreateTrackerResource(ctx, scope, cookiebanner.CreateTrackerResourceRequest{ + CookieCategoryID: input.CookieCategoryID, + ResourceType: coredata.TrackerResourceType(input.ResourceType), + Origin: input.Origin, + Path: input.Path, + DisplayName: input.DisplayName, + Description: description, + }) + if err != nil { + return nil, types.AddTrackerResourceOutput{}, fmt.Errorf("cannot create tracker resource: %w", err) + } + return nil, types.AddTrackerResourceOutput{TrackerResource: types.NewTrackerResource(resource)}, nil +} + +func (r *Resolver) UpdateTrackerResourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateTrackerResourceInput) (*mcp.CallToolResult, types.UpdateTrackerResourceOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionTrackerResourceUpdate) + scope := coredata.NewScopeFromObjectID(input.ID) + updateReq := cookiebanner.UpdateTrackerResourceRequest{TrackerResourceID: input.ID} + if v := UnwrapOmittable(input.DisplayName); v != nil && *v != nil { + updateReq.DisplayName = *v + } + if v := UnwrapOmittable(input.Description); v != nil && *v != nil { + updateReq.Description = *v + } + if v := UnwrapOmittable(input.Excluded); v != nil && *v != nil { + updateReq.Excluded = *v + } + resource, err := r.cookieBanner.UpdateTrackerResource(ctx, scope, updateReq) + if err != nil { + return nil, types.UpdateTrackerResourceOutput{}, fmt.Errorf("cannot update tracker resource: %w", err) + } + return nil, types.UpdateTrackerResourceOutput{TrackerResource: types.NewTrackerResource(resource)}, nil +} + +func (r *Resolver) DeleteTrackerResourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteTrackerResourceInput) (*mcp.CallToolResult, types.DeleteTrackerResourceOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionTrackerResourceDelete) + scope := coredata.NewScopeFromObjectID(input.ID) + if err := r.cookieBanner.DeleteTrackerResource(ctx, scope, input.ID); err != nil { + return nil, types.DeleteTrackerResourceOutput{}, fmt.Errorf("cannot delete tracker resource: %w", err) + } + return nil, types.DeleteTrackerResourceOutput{DeletedID: input.ID}, nil +} + +func (r *Resolver) MoveTrackerResourceToCategoryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.MoveTrackerResourceToCategoryInput) (*mcp.CallToolResult, types.MoveTrackerResourceToCategoryOutput, error) { + r.MustAuthorize(ctx, input.TrackerResourceID, probo.ActionTrackerResourceUpdate) + scope := coredata.NewScopeFromObjectID(input.TrackerResourceID) + result, err := r.cookieBanner.MoveTrackerResourceToCategory(ctx, scope, cookiebanner.MoveTrackerResourceToCategoryRequest{ + TrackerResourceID: input.TrackerResourceID, + TargetCookieCategoryID: input.TargetCookieCategoryID, + }) + if err != nil { + return nil, types.MoveTrackerResourceToCategoryOutput{}, fmt.Errorf("cannot move tracker resource: %w", err) + } + return nil, types.MoveTrackerResourceToCategoryOutput{TrackerResource: types.NewTrackerResource(result.TrackerResource)}, nil +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 915a81eb5..cb3c6ad74 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -9450,7 +9450,7 @@ components: description: Cookie category ID tracker_type: type: string - enum: [COOKIE, LOCAL_STORAGE, SESSION_STORAGE, INDEXED_DB, SCRIPT, IFRAME] + enum: [COOKIE, LOCAL_STORAGE, SESSION_STORAGE, INDEXED_DB] description: Type of tracker pattern: type: string @@ -9492,6 +9492,68 @@ components: format: date-time description: Last update timestamp + TrackerResource: + type: object + required: + - id + - organization_id + - cookie_banner_id + - cookie_category_id + - resource_type + - origin + - path + - display_name + - description + - excluded + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + description: Tracker resource ID + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + cookie_banner_id: + $ref: "#/components/schemas/GID" + description: Cookie banner ID + cookie_category_id: + $ref: "#/components/schemas/GID" + description: Cookie category ID + resource_type: + type: string + enum: [SCRIPT, IFRAME] + description: Type of tracked resource + origin: + type: string + description: Resource origin (scheme://host[:port]) + path: + type: string + description: Resource path + display_name: + type: string + description: Human-friendly display name + description: + type: string + description: Resource description + excluded: + type: boolean + description: Whether this resource is excluded from the consent banner + last_detected_at: + type: + - string + - "null" + format: date-time + description: Timestamp when this resource was last detected + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Last update timestamp + CookieBannerVersion: type: object required: @@ -9984,7 +10046,7 @@ components: $ref: "#/components/schemas/GID" tracker_type: type: string - enum: [COOKIE, LOCAL_STORAGE, SESSION_STORAGE, INDEXED_DB, SCRIPT, IFRAME] + enum: [COOKIE, LOCAL_STORAGE, SESSION_STORAGE, INDEXED_DB] pattern: type: string match_type: @@ -10071,6 +10133,143 @@ components: tracker_pattern: $ref: "#/components/schemas/TrackerPattern" + ListTrackerResourcesInput: + type: object + required: + - cookie_category_id + properties: + cookie_category_id: + $ref: "#/components/schemas/GID" + size: + type: integer + cursor: + $ref: "#/components/schemas/CursorKey" + + ListTrackerResourcesOutput: + type: object + required: + - tracker_resources + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + tracker_resources: + type: array + items: + $ref: "#/components/schemas/TrackerResource" + + GetTrackerResourceInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + + GetTrackerResourceOutput: + type: object + required: + - tracker_resource + properties: + tracker_resource: + $ref: "#/components/schemas/TrackerResource" + + AddTrackerResourceInput: + type: object + required: + - cookie_category_id + - resource_type + - origin + - path + - display_name + properties: + cookie_category_id: + $ref: "#/components/schemas/GID" + resource_type: + type: string + enum: [SCRIPT, IFRAME] + origin: + type: string + path: + type: string + display_name: + type: string + description: + type: string + + AddTrackerResourceOutput: + type: object + required: + - tracker_resource + properties: + tracker_resource: + $ref: "#/components/schemas/TrackerResource" + + UpdateTrackerResourceInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + display_name: + type: + - string + - "null" + go.probo.inc/mcpgen/omittable: true + description: + type: + - string + - "null" + go.probo.inc/mcpgen/omittable: true + excluded: + type: + - boolean + - "null" + go.probo.inc/mcpgen/omittable: true + + UpdateTrackerResourceOutput: + type: object + required: + - tracker_resource + properties: + tracker_resource: + $ref: "#/components/schemas/TrackerResource" + + DeleteTrackerResourceInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + + DeleteTrackerResourceOutput: + type: object + required: + - deleted_id + properties: + deleted_id: + $ref: "#/components/schemas/GID" + + MoveTrackerResourceToCategoryInput: + type: object + required: + - tracker_resource_id + - target_cookie_category_id + properties: + tracker_resource_id: + $ref: "#/components/schemas/GID" + target_cookie_category_id: + $ref: "#/components/schemas/GID" + + MoveTrackerResourceToCategoryOutput: + type: object + required: + - tracker_resource + properties: + tracker_resource: + $ref: "#/components/schemas/TrackerResource" + PublishCookieBannerVersionInput: type: object required: @@ -12353,6 +12552,57 @@ tools: $ref: "#/components/schemas/MoveTrackerPatternToCategoryInput" outputSchema: $ref: "#/components/schemas/MoveTrackerPatternToCategoryOutput" + - name: listTrackerResources + description: List all tracker resources for a category + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListTrackerResourcesInput" + outputSchema: + $ref: "#/components/schemas/ListTrackerResourcesOutput" + - name: getTrackerResource + description: Get a tracker resource by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetTrackerResourceInput" + outputSchema: + $ref: "#/components/schemas/GetTrackerResourceOutput" + - name: addTrackerResource + description: Create a new tracker resource for a category + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddTrackerResourceInput" + outputSchema: + $ref: "#/components/schemas/AddTrackerResourceOutput" + - name: updateTrackerResource + description: Update an existing tracker resource + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateTrackerResourceInput" + outputSchema: + $ref: "#/components/schemas/UpdateTrackerResourceOutput" + - name: deleteTrackerResource + description: Delete a tracker resource + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteTrackerResourceInput" + outputSchema: + $ref: "#/components/schemas/DeleteTrackerResourceOutput" + - name: moveTrackerResourceToCategory + description: Move a tracker resource to a different category + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/MoveTrackerResourceToCategoryInput" + outputSchema: + $ref: "#/components/schemas/MoveTrackerResourceToCategoryOutput" - name: publishCookieBannerVersion description: Publish the current draft version of a cookie banner hints: diff --git a/pkg/server/api/mcp/v1/types/tracker_resource.go b/pkg/server/api/mcp/v1/types/tracker_resource.go new file mode 100644 index 000000000..8c7eec54b --- /dev/null +++ b/pkg/server/api/mcp/v1/types/tracker_resource.go @@ -0,0 +1,56 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewTrackerResource(tr *coredata.TrackerResource) *TrackerResource { + return &TrackerResource{ + ID: tr.ID, + OrganizationID: tr.OrganizationID, + CookieBannerID: tr.CookieBannerID, + CookieCategoryID: tr.CookieCategoryID, + ResourceType: TrackerResourceResourceType(tr.ResourceType), + Origin: tr.Origin, + Path: tr.Path, + DisplayName: tr.DisplayName, + Description: tr.Description, + Excluded: tr.Excluded, + LastDetectedAt: tr.LastDetectedAt, + CreatedAt: tr.CreatedAt, + UpdatedAt: tr.UpdatedAt, + } +} + +func NewListTrackerResourcesOutput(pg *page.Page[*coredata.TrackerResource, coredata.TrackerResourceOrderField]) ListTrackerResourcesOutput { + resources := make([]*TrackerResource, 0, len(pg.Data)) + for _, tr := range pg.Data { + resources = append(resources, NewTrackerResource(tr)) + } + + var nextCursor *page.CursorKey + if len(pg.Data) > 0 { + cursorKey := pg.Data[len(pg.Data)-1].CursorKey(pg.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListTrackerResourcesOutput{ + NextCursor: nextCursor, + TrackerResources: resources, + } +}