diff --git a/ui/src/api/domain/agentTemplates.ts b/ui/src/api/domain/agentTemplates.ts index 29257121fc..b0a0bfd4a2 100644 --- a/ui/src/api/domain/agentTemplates.ts +++ b/ui/src/api/domain/agentTemplates.ts @@ -6,8 +6,13 @@ export interface AgentTemplateLocalRef { name: string; } -/** A model or MCP reference, local when namespace is omitted. */ +/** + * A model or MCP reference, local when namespace is omitted. Omitted `group` and + * `kind` mean the default kind; the form keeps them so an edit cannot repoint a reference. + */ export interface ObjectReference { + group?: string; + kind?: string; name: string; namespace?: string; } @@ -31,7 +36,8 @@ export interface ConfigMapKeyRef { * non-empty list limits the binding to those names. */ export interface McpToolBinding { - server: ObjectReference & { kind: "RemoteMCPServer" }; + /** The API server stores `kind: RemoteMCPServer` when it is omitted. */ + server: ObjectReference; tools?: string[]; requireApproval?: boolean; } diff --git a/ui/src/components/agent-template-form/agentTemplateDraft.test.ts b/ui/src/components/agent-template-form/agentTemplateDraft.test.ts index 3b23406ae2..4556806894 100644 --- a/ui/src/components/agent-template-form/agentTemplateDraft.test.ts +++ b/ui/src/components/agent-template-form/agentTemplateDraft.test.ts @@ -333,7 +333,20 @@ describe("cross-namespace references", () => { }; const draft = draftFromSpec(spec, "team-a"); expect(draft.modelConfig).toEqual({ name: "default", namespace: "shared" }); - expect(draft.mcpTools[0].serverRef).toEqual({ name: "tools", namespace: "shared" }); + expect(draft.mcpTools[0].serverRef).toEqual({ kind: "RemoteMCPServer", name: "tools", namespace: "shared" }); + const saved = specFromDraft(draft, spec); + expect(saved.modelConfig).toEqual(spec.modelConfig); + expect(saved.tools).toEqual(spec.tools); + }); + + // Dropping group or kind would point the reference at a same-named default object. + it("keeps the group and kind of model and MCP references through an edit", () => { + const spec = { + modelConfig: { group: "example.com", kind: "Foo", name: "default" }, + tools: [{ mcp: { server: { group: "example.com", kind: "Foo", name: "tools" } } }], + }; + const draft = draftFromSpec(spec, "team-a"); + draft.description = "edited"; const saved = specFromDraft(draft, spec); expect(saved.modelConfig).toEqual(spec.modelConfig); expect(saved.tools).toEqual(spec.tools); diff --git a/ui/src/components/agent-template-form/agentTemplateDraft.ts b/ui/src/components/agent-template-form/agentTemplateDraft.ts index c090370beb..998c9f70a2 100644 --- a/ui/src/components/agent-template-form/agentTemplateDraft.ts +++ b/ui/src/components/agent-template-form/agentTemplateDraft.ts @@ -134,7 +134,7 @@ export function draftFromSpec(spec: AgentTemplateSpec, namespace: string): Agent mcpTools: tools .filter((binding) => binding.mcp) .map((binding) => ({ - serverRef: { name: binding.mcp?.server.name ?? "", namespace: binding.mcp?.server.namespace }, + serverRef: { ...binding.mcp?.server, name: binding.mcp?.server.name ?? "" }, tools: [...(binding.mcp?.tools ?? [])], requireApproval: binding.mcp?.requireApproval, })), @@ -167,8 +167,8 @@ export function specFromDraft( .filter((tool) => tool.serverRef.name.trim() !== "") .map((tool) => ({ mcp: { - // The form only selects RemoteMCPServers. - server: { kind: "RemoteMCPServer" as const, ...normalizedReference(tool.serverRef, draft.namespace) }, + // A new pick is a RemoteMCPServer; a loaded reference keeps its own group and kind. + server: { kind: "RemoteMCPServer", ...normalizedReference(tool.serverRef, draft.namespace) }, ...(tool.tools.length > 0 ? { tools: [...tool.tools] } : {}), // omitempty on the CRD: false is the default, so only true is sent. ...(tool.requireApproval ? { requireApproval: true } : {}), @@ -313,11 +313,16 @@ export function draftProblems( return problems; } -/** Keep local manifests concise while preserving shared resource identity. */ +/** Keep local manifests concise while preserving shared resource identity, group and kind. */ function normalizedReference(ref: ObjectReference, ownerNamespace: string): ObjectReference { const name = ref.name.trim(); const namespace = ref.namespace?.trim(); - return !namespace || namespace === ownerNamespace ? { name } : { name, namespace }; + return { + ...(ref.group ? { group: ref.group } : {}), + ...(ref.kind ? { kind: ref.kind } : {}), + name, + ...(namespace && namespace !== ownerNamespace ? { namespace } : {}), + }; } /** Parses only the object-valued JSON shape the public API accepts. */