Repository navigation
Conversation
core_mcpserver_create/update/validate and core_workflow_create/update/validate take a tags argument, validated (^[a-z0-9]+(-[a-z0-9]+)*$, at most 63 characters, unique) and stored comma-joined in the ui.giantswarm.io/tags annotation. On update an omitted tags keeps them, [] removes them, a list replaces them. get/list return tags as an array, dropping malformed entries.
core_workflow_create/_update/_validate refuse invalid tags as a tool error worded as core_mcpserver_*'s ("invalid tags: ...") instead of a protocol error or a doubled "validation failed" prefix. updateWorkflow takes the parsed tri-state tags from WorkflowUpdateRequest. tagsArg returns a fresh ArgMetadata per tool. Adds the workflow-tags scenario.
ValidateTags refuses more than api.MaxTags (50) tags with "at most 50 tags", so core_mcpserver_* and core_workflow_* answer "invalid tags: at most 50 tags". ParseTags does not cap on read. Matches the developer portal's cap on agents, connectors and skills.
Published Helm chart
|
| Chart | muster-crds |
| Version | 5.54.6-reecc69cat20261102124207h2c3ed80 |
| OCI reference | oci://gsoci.azurecr.io/charts/giantswarm/muster-crds:5.54.6-reecc69cat20261102124207h2c3ed80 |
| Digest | sha256:71e48789117315c477a75c84af639c7a274390598fb2058932cf2ffbe75e5fb1 |
| Registry | public — gsoci.azurecr.io |
| Git catalog | giantswarm-test-catalog (index) |
Pull this chart
helm pull oci://gsoci.azurecr.io/charts/giantswarm/muster-crds --version 5.54.6-reecc69cat20261102124207h2c3ed80Posted by architect-orb · build 43437 · commit 2c3ed80 · updated in place on every push
Published Helm chart
|
| Chart | muster |
| Version | 5.54.6-reecc69cat20261102124207h2c3ed80 |
| OCI reference | oci://gsoci.azurecr.io/charts/giantswarm/muster:5.54.6-reecc69cat20261102124207h2c3ed80 |
| Digest | sha256:cbdbcb3b6661d6bebd23f5da25e624e4a92d16dc6f960c6dd546393672f19962 |
| Registry | public — gsoci.azurecr.io |
| Git catalog | giantswarm-test-catalog (index) |
Pull this chart
helm pull oci://gsoci.azurecr.io/charts/giantswarm/muster --version 5.54.6-reecc69cat20261102124207h2c3ed80Posted by architect-orb · build 43433 · commit 2c3ed80 · updated in place on every push
| // lowercases each entry, and drops entries that are not valid tags and | ||
| // repeats. It does not enforce MaxTags. It never fails, so a malformed annotation set by hand does not | ||
| // break a read. It returns nil when no valid tag remains. | ||
| func ParseTags(raw string) []string { |
There was a problem hiding this comment.
ParseTags returns more than MaxTags on read while ValidateTags refuses more than MaxTags on write, so a resource with an over-long hand-set or GitOps tag list can't have its tags edited by echoing them back.
A manifest sets ui.giantswarm.io/tags to 60 valid tags. core_mcpserver_get returns all 60. The portal user removes one tag and sends the remaining 59 to core_mcpserver_update, which is refused with invalid tags: at most 50 tags. Any edit fails until the user removes 10 tags they never added. Either cap the read at MaxTags or let an update that doesn't grow the list through.
| seen := map[string]struct{}{} | ||
| for _, entry := range strings.Split(raw, ",") { | ||
| tag := strings.ToLower(strings.TrimSpace(entry)) | ||
| if checkTag(tag) != nil { |
There was a problem hiding this comment.
Readers silently drop malformed entries, and a round-trip update (get, edit, send the list back) then permanently deletes those entries from the annotation without saying so.
Someone sets ui.giantswarm.io/tags: team_a,triage with kubectl. core_workflow_get returns ["triage"]. The portal adds k8s and sends ["triage","k8s"] with replace semantics, so the stored annotation becomes triage,k8s and team_a is gone without a warning. Normalising (Triage to triage) also rewrites the stored value.
| existing.Spec.Description = req.Description | ||
| } | ||
| updateDisplayName(existing, req.DisplayName) | ||
| existing.Annotations = api.ApplyTagsAnnotation(existing.Annotations, req.Tags) |
There was a problem hiding this comment.
handleMCPServerUpdate does a get-modify-update with no conflict retry, so a tags edit from the portal fails if the status reconciler bumps resourceVersion between the Get and the Update. The workflow update path retries with retry.RetryOnConflict.
In Kubernetes mode a user saves new tags just as the MCPServer reconciler writes status. The Update carries the stale resourceVersion, the apiserver answers 409 Conflict, and the tool returns Failed to update MCP server, although nothing about the tags conflicted. updateWorkflow handles the same case with RetryOnConflict.
| Type: api.ArgTypeArray, | ||
| Required: false, | ||
| Description: api.TagsArgDescription, | ||
| Schema: map[string]interface{}{ |
There was a problem hiding this comment.
The tags argument's JSON Schema only declares an array of strings. The real constraints (maxItems 50, uniqueItems, item pattern ^[a-z0-9]+(-[a-z0-9]+)*$, maxLength 63) appear only in the prose description, so schema-driven clients can't validate before calling.
The portal already reads each tool's input schema to find tags. It can't derive the tag rules from that schema, so it has to hard-code the regex and limits again, and the copies can drift. An LLM client following the schema sends ["Triage"] and gets a refusal it could have avoided. Adding maxItems, uniqueItems and items.pattern/items.maxLength to the schema (once, in a shared helper) fixes both. The same applies to the inline schema in internal/mcpserver/api_adapter.go:655.
| workflowInfo["description"] = wf.Description | ||
| } | ||
| if len(wf.Tags) > 0 { | ||
| workflowInfo[fieldTags] = wf.Tags |
There was a problem hiding this comment.
Adding tags to each core_workflow_list entry changes muster list workflow table output: the table formatter fills free columns alphabetically, so a TAGS column now appears. The CHANGELOG doesn't mention it.
optimizeColumns for workflows uses name, description, then fills up to 6 columns alphabetically from all keys (available, tags). With any tagged workflow, muster list workflow gains a tags column showing a raw list. Scripts parsing the table see a new column. Either add tags to unwantedColumnsByResourceType for workflows or document the CLI change.
| } | ||
| existing.Spec = desired.Spec | ||
| // Tri-state: omitted tags (nil) keep the current tags. | ||
| existing.Annotations = api.ApplyTagsAnnotation(existing.Annotations, tags) |
There was a problem hiding this comment.
updateWorkflow now has two sources for tags: the tags parameter (from ParseRequest), which is applied, and wf.Tags/desired.Annotations (from convertTags on the same args), which are computed and thrown away. The comment above (only its spec is applied onto the live object) is now stale.
A future change that copies desired.ObjectMeta or desired.Annotations onto existing would turn an omitted tags into a clear, because desired carries nil annotations, which breaks the keep-when-omitted contract. Pass a single source (for example set wf.Tags only on create and drop the separate parameter, or skip building annotations in the update's desired object) and update the comment.
|
|
||
| // convertTags reads and validates a tags argument. nil (omitted) yields no | ||
| // tags. | ||
| func convertTags(raw interface{}) ([]string, error) { |
There was a problem hiding this comment.
convertTags re-decodes and re-validates the tags argument that api.ParseRequest already decoded into req.Tags and invalidTagsResult already validated, so a single core_workflow_create checks the tags four times.
handleCreate runs ParseRequest and invalidTagsResult, then convertToWorkflow (convertTags), then createWorkflow, which runs ValidateWorkflowFromStructured (convertToWorkflow again) and convertToWorkflow a third time. That is four tag validations and three hand-written type assertions duplicating the JSON decode, with two different error paths for the same input. Validate once (either in the handler from req.Tags, or in convertToWorkflow) and pass the parsed tags through.
| api.SchemaKeyRequired: []string{"name", "instanceArg"}, | ||
| }}, | ||
| {Name: "displayName", Type: api.ArgTypeString, Required: false, Description: "Name UIs show for the server, stored trimmed in the ui.giantswarm.io/display-name annotation; blank sets none, on update removes it"}, | ||
| {Name: "tags", Type: api.ArgTypeArray, Required: false, Description: api.TagsArgDescription, Schema: map[string]interface{}{ |
There was a problem hiding this comment.
The tags ArgMetadata (name, type, description, items schema) is defined twice: inline here and as tagsArg() in internal/workflow/tags.go. It belongs in internal/api/tags.go next to TagsArgDescription.
Any schema change (for example adding the maxItems/pattern constraints) has to be made in two packages, and the MCPServer and Workflow tools' tags schemas can drift. An api.TagsArg() helper, used by both adapters, removes the copy.
|
|
||
| // invalidTagsResult refuses tags that fail api.ValidateTags with a tool | ||
| // error worded as core_mcpserver_*'s. It returns nil for valid tags. | ||
| func invalidTagsResult(tags []string) *api.CallToolResult { |
There was a problem hiding this comment.
The invalid tags: %v refusal and the if req.Tags != nil { validate(*req.Tags) } update guard are written out once here (invalidTagsResult) and three more times inline in internal/mcpserver/api_adapter.go (1067, 1141, 1203).
Four copies of the same refusal wording and nil-guard. The comment here even says it is worded as core_mcpserver_*'s, so the wording is kept in sync by hand. A shared helper in internal/api (for example ValidateTagsUpdate(*[]string) error plus one error-formatting function) lets both adapters call it.
|
|
||
| // TagsArgDescription describes the tags argument of the MCPServer and | ||
| // Workflow mutation tools. | ||
| const TagsArgDescription = "Up to 50 unique tags (ui.giantswarm.io/tags): a-z0-9 words joined by single hyphens, max 63 chars. Update: omitted keeps, [] clears, a list replaces" |
There was a problem hiding this comment.
TagsArgDescription, used on create and validate as well as update, describes update semantics (Update: omitted keeps, [] clears, a list replaces) that don't apply to create or validate.
A client or LLM reading core_workflow_create's or core_mcpserver_validate's schema sees replace/keep/clear semantics that mean nothing there. Use a create/validate description and an update description, the way the displayName arg distinguishes on update.
| wf.Description = desc | ||
| } | ||
|
|
||
| tags, err := convertTags(args[fieldTags]) |
There was a problem hiding this comment.
CLAUDE.md (repo root) says File size: Keep under 400 lines, refactor larger files, and this PR adds more code to internal/workflow/api_adapter.go (already 2883 lines) and internal/mcpserver/api_adapter.go (1486 lines).
Rule in CLAUDE.md, Conventions: **File size**: Keep under 400 lines, refactor larger files. The PR adds about 27 lines to the 2883-line workflow adapter (handler guards, list field, convertToWorkflow tags) and about 19 to the mcpserver adapter. The helpers went to tags.go, but the handler wiring could also sit behind one call per handler to keep the large files from growing.
| } | ||
|
|
||
| // tooManyTags returns api.MaxTags+1 distinct valid tags as a tool argument. | ||
| func tooManyTags() []interface{} { |
There was a problem hiding this comment.
tooManyTags is copied verbatim in internal/mcpserver/tags_test.go:179, and numberedTags in internal/api/tags_test.go builds the same list.
Three copies of the same test fixture across packages. A change to MaxTags semantics or tag format has to touch all three. A small exported test helper (or reusing numberedTags through an internal testutil package) avoids the duplication.
api.TagsArg declares the tags argument for core_mcpserver_* and core_workflow_* with pattern, maxLength, maxItems and uniqueItems in its JSON Schema. api.InvalidTagsResult is the single refusal for create, update and validate. Workflow create and update take the tags parsed by ParseRequest instead of re-decoding them from the raw arguments.
QuentinBisson
left a comment
There was a problem hiding this comment.
LGTM. I pushed two commits: one tags argument definition with the format rules in its JSON Schema, plus a shared refusal helper (convertTags and workflow/tags.go are gone), and CHANGELOG notes for the workflow list TAGS column. Not changed: a hand-set list of more than 50 tags is refused on edit, not trimmed.
Free-form tags on MCPServers and Workflows, so the agent-platform portal can filter, search and link connectors and workflows by tag, like agents. Towards giantswarm/giantswarm#38901.
What changes
tagsarg (string array) oncore_mcpserver_create/update/validateandcore_workflow_create/update/validate.^[a-z0-9]+(-[a-z0-9]+)*$, at most 63 characters, unique, at most 50 tags. Refusals come back as a tool error:invalid tags: ….ui.giantswarm.io/tags.[]clears, a list replaces. Other annotations (registered-by*,display-name) survive.core_mcpserver_get/listandcore_workflow_get/listreturntags(omitted when empty). Readers drop malformed entries instead of failing.docs/reference/mcp-tools.md,docs/reference/crds.md),schema.json, CHANGELOG.Compatibility
Annotation-only: no CRD, RBAC or chart change. The portal checks each tool's input schema for
tagsand never sends it to a muster without this change.GitOps-owned objects behave like
display-name: if the manifest declares the annotation, Flux reverts a live edit.Testing
mcpserver-tagsandworkflow-tags; the mcpserver and workflow scenario concepts pass.go vet, golangci-lint clean.The portal side is a separate giantswarm/backstage PR.
Known limits (unchanged by this PR)
core_workflow_updatestill replaces the whole spec and requiressteps, so a tags-only update must resend the spec. Onlytagshas keep-when-omitted semantics.--tagsflag onmuster create; the CLI is out of scope.