1- import { useState } from 'react' ;
1+ import { useState , useEffect , useCallback } from 'react' ;
22import { Button } from '@/components/ui/button' ;
33import { Badge } from '@/components/ui/badge' ;
44import { Input } from '@/components/ui/input' ;
@@ -10,14 +10,18 @@ import {
1010 Server ,
1111 Users ,
1212 Globe ,
13- CheckCircle
13+ CheckCircle ,
14+ Cpu
1415} from 'lucide-react' ;
1516import { useFhirServers } from '@/stores/smartStore' ;
17+ import { useAuth } from '@/stores/authStore' ;
1618import type { ScopeSet , SmartAppFormData } from '@/lib/types/api' ;
19+ import type { GetAdminMcpServers200ResponseServersInner } from '@/lib/api-client' ;
1720
1821type SmartAppType = 'backend-service' | 'standalone-app' | 'ehr-launch' | 'agent' ;
1922type AuthenticationType = 'asymmetric' | 'symmetric' | 'none' ;
2023type ServerAccessType = 'all-servers' | 'selected-servers' | 'user-person-servers' ;
24+ type McpAccessType = 'none' | 'all-mcp-servers' | 'selected-mcp-servers' ;
2125
2226interface SmartAppAddFormProps {
2327 open : boolean ;
@@ -28,6 +32,31 @@ interface SmartAppAddFormProps {
2832
2933export function SmartAppAddForm ( { open, onClose, onAddApp, scopeSets } : SmartAppAddFormProps ) {
3034 const { servers, loading : serversLoading } = useFhirServers ( ) ;
35+ const { clientApis } = useAuth ( ) ;
36+
37+ // MCP servers state
38+ const [ mcpServers , setMcpServers ] = useState < GetAdminMcpServers200ResponseServersInner [ ] > ( [ ] ) ;
39+ const [ mcpServersLoading , setMcpServersLoading ] = useState ( false ) ;
40+
41+ // Fetch MCP servers when form opens
42+ const fetchMcpServers = useCallback ( async ( ) => {
43+ if ( ! clientApis ?. mcpManagement ) return ;
44+ setMcpServersLoading ( true ) ;
45+ try {
46+ const response = await clientApis . mcpManagement . getAdminMcpServers ( ) ;
47+ setMcpServers ( response . servers || [ ] ) ;
48+ } catch ( err ) {
49+ console . error ( 'Failed to fetch MCP servers:' , err ) ;
50+ } finally {
51+ setMcpServersLoading ( false ) ;
52+ }
53+ } , [ clientApis ] ) ;
54+
55+ useEffect ( ( ) => {
56+ if ( open ) {
57+ fetchMcpServers ( ) ;
58+ }
59+ } , [ open , fetchMcpServers ] ) ;
3160
3261 const [ newApp , setNewApp ] = useState < SmartAppFormData > ( {
3362 name : '' ,
@@ -42,6 +71,8 @@ export function SmartAppAddForm({ open, onClose, onAddApp, scopeSets }: SmartApp
4271 authenticationType : 'symmetric' , // UI-only field - not sent to backend
4372 serverAccessType : 'all-servers' ,
4473 allowedServerIds : [ ] ,
74+ mcpAccessType : 'none' , // MCP server access control
75+ allowedMcpServerNames : [ ] ,
4576 publicClient : false ,
4677 webOrigins : [ ] ,
4778 smartVersion : '2.0.0' ,
@@ -108,6 +139,19 @@ export function SmartAppAddForm({ open, onClose, onAddApp, scopeSets }: SmartApp
108139 }
109140 } ;
110141
142+ const getMcpAccessTypeDescription = ( mcpAccessType : McpAccessType ) : string => {
143+ switch ( mcpAccessType ) {
144+ case 'none' :
145+ return 'App has no access to MCP servers (AI capabilities disabled)' ;
146+ case 'all-mcp-servers' :
147+ return 'App can access all configured MCP servers' ;
148+ case 'selected-mcp-servers' :
149+ return 'App is restricted to specific MCP servers only' ;
150+ default :
151+ return '' ;
152+ }
153+ } ;
154+
111155 const getScopeSetName = ( scopeSetId ?: string ) => {
112156 if ( ! scopeSetId ) return 'Custom' ;
113157 const scopeSet = scopeSets . find ( set => set . id === scopeSetId ) ;
@@ -140,6 +184,9 @@ export function SmartAppAddForm({ open, onClose, onAddApp, scopeSets }: SmartApp
140184 appType : newApp . appType ! ,
141185 serverAccessType : newApp . serverAccessType ! ,
142186 allowedServerIds : newApp . allowedServerIds || [ ] ,
187+ // MCP server access control
188+ mcpAccessType : newApp . mcpAccessType ,
189+ allowedMcpServerNames : newApp . mcpAccessType === 'selected-mcp-servers' ? newApp . allowedMcpServerNames : [ ] ,
143190 // authenticationType is UI-only - backend infers from jwksUri/publicKey presence
144191 jwksUri : newApp . jwksUri ,
145192 publicKey : newApp . publicKey ,
@@ -159,6 +206,8 @@ export function SmartAppAddForm({ open, onClose, onAddApp, scopeSets }: SmartApp
159206 authenticationType : 'symmetric' , // UI-only
160207 serverAccessType : 'all-servers' ,
161208 allowedServerIds : [ ] ,
209+ mcpAccessType : 'none' ,
210+ allowedMcpServerNames : [ ] ,
162211 } ) ;
163212 onClose ( ) ;
164213 } ;
@@ -177,6 +226,8 @@ export function SmartAppAddForm({ open, onClose, onAddApp, scopeSets }: SmartApp
177226 authenticationType : 'symmetric' , // UI-only
178227 serverAccessType : 'all-servers' ,
179228 allowedServerIds : [ ] ,
229+ mcpAccessType : 'none' ,
230+ allowedMcpServerNames : [ ] ,
180231 } ) ;
181232 onClose ( ) ;
182233 } ;
@@ -559,6 +610,132 @@ export function SmartAppAddForm({ open, onClose, onAddApp, scopeSets }: SmartApp
559610 </ div >
560611 </ div >
561612
613+ { /* MCP Server Access Section */ }
614+ < div className = "space-y-6 p-6 bg-violet-500/10 rounded-xl border border-violet-500/20" >
615+ < div className = "flex items-center space-x-3" >
616+ < div className = "w-8 h-8 bg-violet-500/10 rounded-lg flex items-center justify-center shadow-sm" >
617+ < Cpu className = "w-4 h-4 text-violet-600 dark:text-violet-400" />
618+ </ div >
619+ < div >
620+ < h4 className = "text-lg font-bold text-foreground tracking-tight" > MCP Server Access (AI Capabilities)</ h4 >
621+ < p className = "text-muted-foreground text-sm font-medium" > Control which AI/MCP servers this application can use</ p >
622+ </ div >
623+ </ div >
624+
625+ < div className = "space-y-4" >
626+ < div className = "space-y-3" >
627+ < Label htmlFor = "mcpAccessType" className = "text-sm font-semibold text-foreground" > MCP Access Type</ Label >
628+ < select
629+ id = "mcpAccessType"
630+ value = { newApp . mcpAccessType || 'none' }
631+ onChange = { ( e ) => {
632+ const mcpAccessType = e . target . value as McpAccessType ;
633+ setNewApp ( {
634+ ...newApp ,
635+ mcpAccessType,
636+ allowedMcpServerNames : mcpAccessType === 'selected-mcp-servers' ? newApp . allowedMcpServerNames : [ ]
637+ } ) ;
638+ } }
639+ className = "flex h-10 w-full rounded-xl border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 shadow-sm"
640+ >
641+ < option value = "none" > No MCP Access</ option >
642+ < option value = "all-mcp-servers" > All MCP Servers</ option >
643+ < option value = "selected-mcp-servers" > Specific MCP Servers Only</ option >
644+ </ select >
645+ < p className = "text-xs text-muted-foreground" >
646+ { getMcpAccessTypeDescription ( newApp . mcpAccessType || 'none' ) }
647+ </ p >
648+ </ div >
649+
650+ { newApp . mcpAccessType === 'selected-mcp-servers' && (
651+ < div className = "space-y-3" >
652+ < Label className = "text-sm font-semibold text-foreground" > Select Allowed MCP Servers</ Label >
653+ { mcpServersLoading ? (
654+ < div className = "p-4 text-center text-muted-foreground" >
655+ < div className = "animate-spin inline-block w-4 h-4 border-2 border-border border-t-primary rounded-full mr-2" > </ div >
656+ Loading available MCP servers...
657+ </ div >
658+ ) : mcpServers . length === 0 ? (
659+ < div className = "p-4 text-center text-muted-foreground bg-muted/50 rounded-lg border border-border" >
660+ < Cpu className = "w-6 h-6 mx-auto mb-2 text-muted-foreground" />
661+ < p className = "text-sm" > No MCP servers available</ p >
662+ < p className = "text-xs text-muted-foreground" > Configure MCP servers first</ p >
663+ </ div >
664+ ) : (
665+ < div className = "space-y-2 max-h-48 overflow-y-auto p-3 bg-card rounded-lg border border-border" >
666+ { mcpServers . map ( ( server ) => (
667+ < label key = { server . name } className = "flex items-center space-x-3 p-2 hover:bg-muted/50 rounded-lg cursor-pointer" >
668+ < input
669+ type = "checkbox"
670+ checked = { ( newApp . allowedMcpServerNames || [ ] ) . includes ( server . name ) }
671+ onChange = { ( e ) => {
672+ const serverNames = e . target . checked
673+ ? [ ...( newApp . allowedMcpServerNames || [ ] ) , server . name ]
674+ : ( newApp . allowedMcpServerNames || [ ] ) . filter ( name => name !== server . name ) ;
675+ setNewApp ( { ...newApp , allowedMcpServerNames : serverNames } ) ;
676+ } }
677+ className = "w-4 h-4 text-primary border-border rounded focus:ring-ring"
678+ />
679+ < div className = "flex-1 min-w-0" >
680+ < div className = "flex items-center space-x-2" >
681+ < span className = "text-sm font-medium text-foreground truncate" > { server . name } </ span >
682+ { server . status === 'connected' ? (
683+ < CheckCircle className = "w-4 h-4 text-green-500 dark:text-green-400 flex-shrink-0" />
684+ ) : (
685+ < AlertCircle className = "w-4 h-4 text-yellow-500 dark:text-yellow-400 flex-shrink-0" />
686+ ) }
687+ </ div >
688+ < div className = "flex items-center space-x-2 text-xs text-muted-foreground" >
689+ < span className = "truncate" > { server . url || server . type } </ span >
690+ { server . toolCount !== undefined && (
691+ < Badge variant = "outline" className = "text-xs" >
692+ { server . toolCount } tools
693+ </ Badge >
694+ ) }
695+ </ div >
696+ </ div >
697+ </ label >
698+ ) ) }
699+ </ div >
700+ ) }
701+ { ( newApp . allowedMcpServerNames || [ ] ) . length > 0 && (
702+ < div className = "text-xs text-violet-600 dark:text-violet-400 bg-violet-500/10 p-2 rounded-lg border border-violet-500/20" >
703+ ✓ { ( newApp . allowedMcpServerNames || [ ] ) . length } MCP server(s) selected
704+ </ div >
705+ ) }
706+ </ div >
707+ ) }
708+
709+ { newApp . mcpAccessType === 'none' && (
710+ < div className = "p-4 bg-slate-500/10 border border-slate-500/20 rounded-lg" >
711+ < div className = "flex items-start space-x-2" >
712+ < Cpu className = "w-4 h-4 text-slate-600 dark:text-slate-400 mt-0.5 flex-shrink-0" />
713+ < div className = "text-sm" >
714+ < p className = "font-semibold text-slate-800 dark:text-slate-300 mb-1" > No MCP Access</ p >
715+ < p className = "text-slate-700 dark:text-slate-400 text-xs" >
716+ This app will not have access to any MCP servers. AI-powered features will be unavailable.
717+ </ p >
718+ </ div >
719+ </ div >
720+ </ div >
721+ ) }
722+
723+ { newApp . mcpAccessType === 'all-mcp-servers' && (
724+ < div className = "p-4 bg-violet-500/10 border border-violet-500/20 rounded-lg" >
725+ < div className = "flex items-start space-x-2" >
726+ < Cpu className = "w-4 h-4 text-violet-600 dark:text-violet-400 mt-0.5 flex-shrink-0" />
727+ < div className = "text-sm" >
728+ < p className = "font-semibold text-violet-800 dark:text-violet-300 mb-1" > Full MCP Access</ p >
729+ < p className = "text-violet-700 dark:text-violet-400 text-xs" >
730+ This app will have access to all MCP servers configured in the system, enabling full AI capabilities.
731+ </ p >
732+ </ div >
733+ </ div >
734+ </ div >
735+ ) }
736+ </ div >
737+ </ div >
738+
562739 { /* Scope Management Section */ }
563740 < div className = "space-y-6 p-6 bg-blue-500/10 rounded-xl border border-blue-500/20" >
564741 < div className = "flex items-center space-x-3" >
0 commit comments