@@ -20,17 +20,19 @@ import { defineStepper } from 'components/redpanda-ui/components/stepper';
2020import { Heading , Text } from 'components/redpanda-ui/components/typography' ;
2121import { useLintHints } from 'components/ui/lint-hint/use-lint-hints' ;
2222import { useSecretDetection } from 'components/ui/secret/use-secret-detection' ;
23+ import {
24+ ServiceAccountSelector ,
25+ type ServiceAccountSelectorRef ,
26+ } from 'components/ui/service-account/service-account-selector' ;
2327import { ExpandedYamlDialog } from 'components/ui/yaml/expanded-yaml-dialog' ;
2428import { useYamlLabelSync } from 'components/ui/yaml/use-yaml-label-sync' ;
29+ import { config , isFeatureFlagEnabled } from 'config' ;
2530import { ArrowLeft , FileText , Hammer , Loader2 } from 'lucide-react' ;
26- import {
27- CreateMCPServerRequestSchema ,
28- LintMCPConfigRequestSchema ,
29- } from 'protogen/redpanda/api/dataplane/v1alpha3/mcp_pb' ;
30- import React , { useMemo , useState } from 'react' ;
31+ import { MCPServer_ServiceAccountSchema } from 'protogen/redpanda/api/dataplane/v1/mcp_pb' ;
32+ import React , { useEffect , useMemo , useRef , useState } from 'react' ;
3133import { useFieldArray , useForm } from 'react-hook-form' ;
3234import { useCreateMCPServerMutation , useLintMCPConfigMutation } from 'react-query/api/remote-mcp' ;
33- import { useListSecretsQuery } from 'react-query/api/secret' ;
35+ import { useCreateSecretMutation , useListSecretsQuery } from 'react-query/api/secret' ;
3436import { useNavigate } from 'react-router-dom' ;
3537import { toast } from 'sonner' ;
3638import { formatToastErrorMessageGRPC } from 'utils/toast.utils' ;
@@ -64,20 +66,59 @@ export const RemoteMCPCreatePage: React.FC = () => {
6466 const navigate = useNavigate ( ) ;
6567 const { mutateAsync : createServer , isPending : isCreateMCPServerPending } = useCreateMCPServerMutation ( ) ;
6668 const { mutateAsync : lintConfig , isPending : isLintConfigPending } = useLintMCPConfigMutation ( ) ;
69+ const { mutateAsync : createSecret , isPending : isCreateSecretPending } = useCreateSecretMutation ( {
70+ skipInvalidation : true ,
71+ } ) ;
6772
6873 // State for expanded YAML dialog
6974 const [ expandedTool , setExpandedTool ] = useState < { index : number ; isOpen : boolean } | null > ( null ) ;
7075
7176 // Query existing secrets
7277 const { data : secretsData } = useListSecretsQuery ( ) ;
7378
79+ // Ref to ServiceAccountSelector to call createServiceAccount
80+ const serviceAccountSelectorRef = useRef < ServiceAccountSelectorRef > ( null ) ;
81+
82+ // Track the created service account info and pending state
83+ const [ serviceAccountInfo , setServiceAccountInfo ] = useState < {
84+ secretName : string ;
85+ serviceAccountId : string ;
86+ } | null > ( null ) ;
87+ const [ isCreateServiceAccountPending , setIsCreateServiceAccountPending ] = useState ( false ) ;
88+
7489 // Form setup
7590 const form = useForm < FormValues > ( {
7691 resolver : zodResolver ( FormSchema ) ,
7792 defaultValues : initialValues ,
7893 mode : 'onChange' ,
7994 } ) ;
8095
96+ // Track the display name to auto-generate service account name
97+ const displayName = form . watch ( 'displayName' ) ;
98+ const serviceAccountName = form . watch ( 'serviceAccountName' ) ;
99+
100+ // Auto-generate service account name when MCP server name changes
101+ useEffect ( ( ) => {
102+ if ( displayName ) {
103+ const clusterType = config . isServerless ? 'serverless' : 'cluster' ;
104+ const sanitizedServerName = displayName . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 - ] / g, '-' ) ;
105+ const generatedName = `${ clusterType } -${ config . clusterId } -mcp-${ sanitizedServerName } -sa` ;
106+
107+ // Only update if the field is empty or matches the previous auto-generated pattern
108+ const currentValue = form . getValues ( 'serviceAccountName' ) ;
109+ if ( ! currentValue || currentValue . startsWith ( `${ clusterType } -${ config . clusterId } -mcp-` ) ) {
110+ form . setValue ( 'serviceAccountName' , generatedName , { shouldValidate : false } ) ;
111+ }
112+ }
113+ } , [ displayName , form ] ) ;
114+
115+ // Clear cached service account when service account name changes
116+ useEffect ( ( ) => {
117+ if ( serviceAccountInfo && serviceAccountName ) {
118+ setServiceAccountInfo ( null ) ;
119+ }
120+ } , [ serviceAccountName , serviceAccountInfo ] ) ;
121+
81122 const {
82123 fields : tagFields ,
83124 append : appendTag ,
@@ -117,7 +158,11 @@ export const RemoteMCPCreatePage: React.FC = () => {
117158
118159 const handleNext = async ( isOnMetadataStep : boolean , goNext : ( ) => void ) => {
119160 if ( isOnMetadataStep ) {
120- const valid = await form . trigger ( [ 'displayName' , 'description' , 'resourcesTier' , 'tags' ] ) ;
161+ const fieldsToValidate : Array < keyof FormValues > = [ 'displayName' , 'description' , 'resourcesTier' , 'tags' ] ;
162+ if ( isFeatureFlagEnabled ( 'enableMcpServiceAccount' ) ) {
163+ fieldsToValidate . push ( 'serviceAccountName' ) ;
164+ }
165+ const valid = await form . trigger ( fieldsToValidate ) ;
121166 if ( ! valid ) {
122167 return ;
123168 }
@@ -139,11 +184,9 @@ export const RemoteMCPCreatePage: React.FC = () => {
139184 } ,
140185 } ;
141186
142- const response = await lintConfig (
143- create ( LintMCPConfigRequestSchema , {
144- tools : toolsMap ,
145- } )
146- ) ;
187+ const response = await lintConfig ( {
188+ tools : toolsMap ,
189+ } ) ;
147190
148191 // Update lint hints for this tool
149192 setLintHints ( ( prev ) => ( {
@@ -152,6 +195,30 @@ export const RemoteMCPCreatePage: React.FC = () => {
152195 } ) ) ;
153196 } ;
154197
198+ const createServiceAccountIfNeeded = async (
199+ serverName : string
200+ ) : Promise < { secretName : string ; serviceAccountId : string } | null > => {
201+ // If we already created one in this session, use it
202+ if ( serviceAccountInfo ) {
203+ return serviceAccountInfo ;
204+ }
205+
206+ // Call the ServiceAccountSelector to create the service account
207+ if ( ! serviceAccountSelectorRef . current ) {
208+ toast . error ( 'Service account selector not initialized' ) ;
209+ return null ;
210+ }
211+
212+ // The pending state is automatically tracked via onPendingChange callback
213+ const result = await serviceAccountSelectorRef . current . createServiceAccount ( serverName ) ;
214+
215+ if ( result ) {
216+ setServiceAccountInfo ( result ) ;
217+ }
218+
219+ return result ;
220+ } ;
221+
155222 // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: complexity 56, refactor later
156223 const handleValidationError = ( error : ConnectError ) => {
157224 if ( error . code === ConnectCode . InvalidArgument && error . details ) {
@@ -235,19 +302,47 @@ export const RemoteMCPCreatePage: React.FC = () => {
235302 } ;
236303 }
237304
305+ const useMcpServiceAccount = isFeatureFlagEnabled ( 'enableMcpServiceAccount' ) ;
306+ let serviceAccountConfig : ReturnType < typeof create < typeof MCPServer_ServiceAccountSchema > > | undefined ;
307+
308+ // Create service account if feature flag is enabled
309+ // NOTE: Service account and secret are created before MCP server creation.
310+ // If server creation fails, these resources will not be automatically cleaned up.
311+ // This matches the AI agents implementation pattern.
312+ if ( useMcpServiceAccount ) {
313+ const serviceAccountResult = await createServiceAccountIfNeeded ( values . displayName ) ;
314+ if ( ! serviceAccountResult ) {
315+ return ; // Error already shown by createServiceAccountIfNeeded
316+ }
317+
318+ const { secretName, serviceAccountId } = serviceAccountResult ;
319+
320+ // Add service_account_id and secret_id to tags for easy deletion
321+ tagsMap . service_account_id = serviceAccountId ;
322+ tagsMap . secret_id = secretName ;
323+
324+ serviceAccountConfig = create ( MCPServer_ServiceAccountSchema , {
325+ clientId : `\${secrets.${ secretName } .client_id}` ,
326+ clientSecret : `\${secrets.${ secretName } .client_secret}` ,
327+ } ) ;
328+ }
329+
330+ const mcpServerPayload = {
331+ displayName : values . displayName . trim ( ) ,
332+ description : values . description ?. trim ( ) ?? '' ,
333+ tools : toolsMap ,
334+ tags : tagsMap ,
335+ resources : {
336+ cpuShares : tier ?. cpu ?? '200m' ,
337+ memoryShares : tier ?. memory ?? '800M' ,
338+ } ,
339+ ...( useMcpServiceAccount && { serviceAccount : serviceAccountConfig } ) ,
340+ } ;
341+
238342 await createServer (
239- create ( CreateMCPServerRequestSchema , {
240- mcpServer : {
241- displayName : values . displayName . trim ( ) ,
242- description : values . description ?. trim ( ) ?? '' ,
243- tools : toolsMap ,
244- tags : tagsMap ,
245- resources : {
246- cpuShares : tier ?. cpu ?? '200m' ,
247- memoryShares : tier ?. memory ?? '800M' ,
248- } ,
249- } ,
250- } ) ,
343+ {
344+ mcpServer : mcpServerPayload ,
345+ } ,
251346 {
252347 onError : handleValidationError ,
253348 onSuccess : ( data ) => {
@@ -329,17 +424,28 @@ export const RemoteMCPCreatePage: React.FC = () => {
329424
330425 < Stepper . Controls className = { methods . isFirst ? 'flex justify-end' : 'flex justify-between' } >
331426 { ! methods . isFirst && (
332- < Button disabled = { isCreateMCPServerPending } onClick = { methods . prev } variant = "outline" >
427+ < Button
428+ disabled = { isCreateMCPServerPending || isCreateServiceAccountPending }
429+ onClick = { methods . prev }
430+ variant = "outline"
431+ >
333432 < ArrowLeft className = "h-4 w-4" />
334433 Previous
335434 </ Button >
336435 ) }
337436 { methods . isLast ? (
338437 < Button
339- disabled = { isCreateMCPServerPending || hasFormErrors || hasLintingIssues || hasSecretWarnings }
438+ disabled = {
439+ isCreateMCPServerPending ||
440+ isCreateServiceAccountPending ||
441+ isCreateSecretPending ||
442+ hasFormErrors ||
443+ hasLintingIssues ||
444+ hasSecretWarnings
445+ }
340446 onClick = { form . handleSubmit ( onSubmit ) }
341447 >
342- { isCreateMCPServerPending ? (
448+ { isCreateMCPServerPending || isCreateServiceAccountPending || isCreateSecretPending ? (
343449 < div className = "flex items-center gap-2" >
344450 < Loader2 className = "h-4 w-4 animate-spin" />
345451 < Text as = "span" > Creating...</ Text >
@@ -358,6 +464,15 @@ export const RemoteMCPCreatePage: React.FC = () => {
358464 ) }
359465 </ Stepper . Controls >
360466 </ Form >
467+ { isFeatureFlagEnabled ( 'enableMcpServiceAccount' ) && (
468+ < ServiceAccountSelector
469+ createSecret = { createSecret }
470+ onPendingChange = { setIsCreateServiceAccountPending }
471+ ref = { serviceAccountSelectorRef }
472+ resourceType = "MCP server"
473+ serviceAccountName = { serviceAccountName }
474+ />
475+ ) }
361476
362477 { /* Expanded YAML Editor Dialog */ }
363478 { expandedTool && (
0 commit comments