diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/tools/bulk_import/sections/source.tsx b/x-pack/platform/plugins/shared/agent_builder/public/application/components/tools/bulk_import/sections/source.tsx index ed7880c0a2588..459dfd9791c86 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/tools/bulk_import/sections/source.tsx +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/tools/bulk_import/sections/source.tsx @@ -26,7 +26,8 @@ const mcpActionLinkStyles = (euiThemeContext: UseEuiTheme) => css` export const SourceSection = () => { const euiThemeContext = useEuiTheme(); - const { control, formState, setValue } = useFormContext(); + const { control, formState, setValue, trigger, getFieldState } = + useFormContext(); const { errors } = formState; const handleConnectorCreated = useCallback( @@ -100,6 +101,9 @@ export const SourceSection = () => { onBlur(); // Clear tool selection when connector changes setValue('tools', []); + if (getFieldState('namespace').isDirty) { + trigger('namespace'); + } }} isLoading={isLoadingConnectors} inputRef={ref} diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/components/tools/form/validation/bulk_import_mcp_tool_form_validation.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/components/tools/form/validation/bulk_import_mcp_tool_form_validation.ts index efec367a3ddfa..542fcdc8fa58a 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/components/tools/form/validation/bulk_import_mcp_tool_form_validation.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/components/tools/form/validation/bulk_import_mcp_tool_form_validation.ts @@ -75,44 +75,50 @@ export const useBulkImportMcpToolFormValidationSchema = () => { const { toolsService } = useAgentBuilderServices(); const queryClient = useQueryClient(); - return z.object({ - connectorId: z - .string() - .min(1, { message: bulkImportMcpI18nMessages.connectorId.requiredError }), - tools: z - .array( - z.object({ - name: z.string(), - description: z.string(), - }) - ) - .min(1, { message: bulkImportMcpI18nMessages.tools.requiredError }), - namespace: z - .string() - .min(1, { message: bulkImportMcpI18nMessages.namespace.requiredError }) - .max(toolIdMaxLength, { message: bulkImportMcpI18nMessages.namespace.tooLongError }) - .regex(toolIdRegexp, { message: bulkImportMcpI18nMessages.namespace.formatError }) - .refine( - (name) => !isInProtectedNamespace(name) && !hasNamespaceName(name), - (name) => ({ - message: bulkImportMcpI18nMessages.namespace.protectedNamespaceError(name), - }) - ) - .superRefine(async (value, ctx) => { - if (value.length > 0) { - const { isValid } = await queryClient.fetchQuery({ - queryKey: queryKeys.tools.namespace.validate(value), - queryFn: () => toolsService.validateNamespace({ namespace: value }), - staleTime: 0, + return z + .object({ + connectorId: z + .string() + .min(1, { message: bulkImportMcpI18nMessages.connectorId.requiredError }), + tools: z + .array( + z.object({ + name: z.string(), + description: z.string(), + }) + ) + .min(1, { message: bulkImportMcpI18nMessages.tools.requiredError }), + namespace: z + .string() + .min(1, { message: bulkImportMcpI18nMessages.namespace.requiredError }) + .max(toolIdMaxLength, { message: bulkImportMcpI18nMessages.namespace.tooLongError }) + .regex(toolIdRegexp, { message: bulkImportMcpI18nMessages.namespace.formatError }) + .refine( + (name) => !isInProtectedNamespace(name) && !hasNamespaceName(name), + (name) => ({ + message: bulkImportMcpI18nMessages.namespace.protectedNamespaceError(name), + }) + ), + labels: z.array(z.string()), + }) + .superRefine(async (data, ctx) => { + if (data.namespace.length > 0 && data.connectorId.length > 0) { + const { isValid } = await queryClient.fetchQuery({ + queryKey: queryKeys.tools.namespace.validate(data.namespace, data.connectorId), + queryFn: () => + toolsService.validateNamespace({ + namespace: data.namespace, + connectorId: data.connectorId, + }), + staleTime: 0, + }); + if (!isValid) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: bulkImportMcpI18nMessages.namespace.conflictError, + path: ['namespace'], }); - if (!isValid) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: bulkImportMcpI18nMessages.namespace.conflictError, - }); - } } - }), - labels: z.array(z.string()), - }); + } + }); }; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/tools/use_validate_namespace.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/tools/use_validate_namespace.ts index 6d29d84714e1d..ddc2b4bfb0a79 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/tools/use_validate_namespace.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/hooks/tools/use_validate_namespace.ts @@ -11,17 +11,22 @@ import { useAgentBuilderServices } from '../use_agent_builder_service'; export interface UseValidateNamespaceOptions { namespace: string; + connectorId?: string; enabled?: boolean; } -export const useValidateNamespace = ({ namespace, enabled }: UseValidateNamespaceOptions) => { +export const useValidateNamespace = ({ + namespace, + connectorId, + enabled, +}: UseValidateNamespaceOptions) => { const { toolsService } = useAgentBuilderServices(); const isEnabled = enabled ?? namespace.length > 0; const { data, isLoading, error, isError, isFetching } = useQuery({ - queryKey: queryKeys.tools.namespace.validate(namespace), - queryFn: () => toolsService.validateNamespace({ namespace }), + queryKey: queryKeys.tools.namespace.validate(namespace, connectorId), + queryFn: () => toolsService.validateNamespace({ namespace, connectorId }), enabled: isEnabled, staleTime: 0, }); diff --git a/x-pack/platform/plugins/shared/agent_builder/public/application/query_keys.ts b/x-pack/platform/plugins/shared/agent_builder/public/application/query_keys.ts index e027cba5dc852..334fab82b6c00 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/application/query_keys.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/application/query_keys.ts @@ -40,7 +40,8 @@ export const queryKeys = { mcp: () => ['tools', 'health', 'mcp'] as const, }, namespace: { - validate: (namespace: string) => ['tools', 'namespace', 'validate', namespace] as const, + validate: (namespace: string, connectorId?: string) => + ['tools', 'namespace', 'validate', namespace, connectorId] as const, }, }, }; diff --git a/x-pack/platform/plugins/shared/agent_builder/public/services/tools/tools_service.ts b/x-pack/platform/plugins/shared/agent_builder/public/services/tools/tools_service.ts index e01a8283b4ff9..dbab9ed9193eb 100644 --- a/x-pack/platform/plugins/shared/agent_builder/public/services/tools/tools_service.ts +++ b/x-pack/platform/plugins/shared/agent_builder/public/services/tools/tools_service.ts @@ -168,10 +168,10 @@ export class ToolsService { ); } - async validateNamespace({ namespace }: { namespace: string }) { + async validateNamespace({ namespace, connectorId }: { namespace: string; connectorId?: string }) { return await this.http.get( `${internalApiPath}/tools/_validate_namespace`, - { query: { namespace } } + { query: { namespace, connector_id: connectorId } } ); } } diff --git a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/tools.ts b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/tools.ts index a45dbda2634a7..0772da8c946f2 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/tools.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/tools.ts @@ -238,6 +238,7 @@ export function registerInternalToolsRoutes({ validate: { query: schema.object({ namespace: schema.string(), + connector_id: schema.maybe(schema.string()), }), }, options: { access: 'internal' }, @@ -246,33 +247,54 @@ export function registerInternalToolsRoutes({ }, }, wrapHandler(async (ctx, request, response) => { - const { namespace } = request.query; + const { namespace, connector_id: connectorId } = request.query; const { tools: toolService } = getInternalServices(); const registry = await toolService.getRegistry({ request }); const allTools = await registry.list({}); - // Extract namespaces from tool IDs - // A tool ID like "mcp.github.tool_name" has namespace "mcp.github" - // A tool ID like "mcp.tool_name" has namespace "mcp" - // A tool ID like "tool_name" has no namespace - const existingNamespaces = new Set(); - allTools.forEach((tool) => { + const toolsInNamespace = allTools.filter((tool) => { const lastDotIndex = tool.id.lastIndexOf('.'); if (lastDotIndex > 0) { const toolNamespace = tool.id.substring(0, lastDotIndex); - existingNamespaces.add(toolNamespace); + return toolNamespace === namespace; } + return false; }); - const conflictingNamespaces = Array.from(existingNamespaces).filter( - (existingNamespace) => existingNamespace === namespace - ); + if (toolsInNamespace.length === 0) { + return response.ok({ + body: { + isValid: true, + conflictingNamespaces: [], + }, + }); + } + + // If connectorId is provided, check if all tools in the namespace belong to the same connector + // This allows reusing a namespace for the same MCP server + if (connectorId) { + const allToolsBelongToSameConnector = toolsInNamespace.every((tool) => { + if (isMcpTool(tool)) { + return tool.configuration.connector_id === connectorId; + } + return false; + }); + + if (allToolsBelongToSameConnector) { + return response.ok({ + body: { + isValid: true, + conflictingNamespaces: [], + }, + }); + } + } return response.ok({ body: { - isValid: conflictingNamespaces.length === 0, - conflictingNamespaces, + isValid: false, + conflictingNamespaces: [namespace], }, }); })