Skip to content

Commit 96b27ca

Browse files
committed
Merge branch 'dev/mcp-stuff' into develop
2 parents 28388fb + 9d681cf commit 96b27ca

5 files changed

Lines changed: 240 additions & 5 deletions

File tree

.github/workflows/create-pr.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ on:
1010
- "ai/test/*"
1111
- "ai/checklist/*"
1212

13+
permissions:
14+
contents: write
15+
pull-requests: write
16+
1317
jobs:
1418
pr-to-develop:
1519
name: Create/Update PR from dev/* → develop

backend/src/schemas/admin/smart-apps.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,15 @@ export const SmartApp = t.Object({
4848

4949
// PKCE and offline access
5050
requirePkce: t.Optional(t.Boolean({ description: 'Require PKCE for public clients' })),
51-
allowOfflineAccess: t.Optional(t.Boolean({ description: 'Allow offline access (refresh tokens)' }))
51+
allowOfflineAccess: t.Optional(t.Boolean({ description: 'Allow offline access (refresh tokens)' })),
52+
53+
// MCP server access control
54+
mcpAccessType: t.Optional(t.Union([
55+
t.Literal('none'),
56+
t.Literal('all-mcp-servers'),
57+
t.Literal('selected-mcp-servers')
58+
], { description: 'MCP server access control type' })),
59+
allowedMcpServerNames: t.Optional(t.Array(t.String(), { description: 'List of allowed MCP server names (when mcpAccessType is selected-mcp-servers)' }))
5260
}, { title: 'SmartApp' })
5361

5462
export const CreateSmartAppRequest = t.Object({
@@ -89,7 +97,15 @@ export const CreateSmartAppRequest = t.Object({
8997

9098
// PKCE and offline access
9199
requirePkce: t.Optional(t.Boolean({ description: 'Require Proof Key for Code Exchange (PKCE) for public clients' })),
92-
allowOfflineAccess: t.Optional(t.Boolean({ description: 'Allow offline access (refresh tokens)' }))
100+
allowOfflineAccess: t.Optional(t.Boolean({ description: 'Allow offline access (refresh tokens)' })),
101+
102+
// MCP server access control
103+
mcpAccessType: t.Optional(t.Union([
104+
t.Literal('none'),
105+
t.Literal('all-mcp-servers'),
106+
t.Literal('selected-mcp-servers')
107+
], { description: 'MCP server access control type (none = no MCP access, all-mcp-servers = access all, selected-mcp-servers = specific servers only)' })),
108+
allowedMcpServerNames: t.Optional(t.Array(t.String(), { description: 'List of allowed MCP server names (when mcpAccessType is selected-mcp-servers)' }))
93109
}, { title: 'CreateSmartAppRequest' })
94110

95111
export const UpdateSmartAppRequest = t.Object({
@@ -130,7 +146,15 @@ export const UpdateSmartAppRequest = t.Object({
130146

131147
// PKCE and offline access
132148
requirePkce: t.Optional(t.Boolean({ description: 'Require PKCE for public clients' })),
133-
allowOfflineAccess: t.Optional(t.Boolean({ description: 'Allow offline access (refresh tokens)' }))
149+
allowOfflineAccess: t.Optional(t.Boolean({ description: 'Allow offline access (refresh tokens)' })),
150+
151+
// MCP server access control
152+
mcpAccessType: t.Optional(t.Union([
153+
t.Literal('none'),
154+
t.Literal('all-mcp-servers'),
155+
t.Literal('selected-mcp-servers')
156+
], { description: 'MCP server access control type' })),
157+
allowedMcpServerNames: t.Optional(t.Array(t.String(), { description: 'List of allowed MCP server names' }))
134158
}, { title: 'UpdateSmartAppRequest' })
135159

136160
export const ClientIdParam = t.Object({

ui/src/components/SmartAppsManager/SmartAppAddForm.tsx

Lines changed: 179 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect, useCallback } from 'react';
22
import { Button } from '@/components/ui/button';
33
import { Badge } from '@/components/ui/badge';
44
import { 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';
1516
import { useFhirServers } from '@/stores/smartStore';
17+
import { useAuth } from '@/stores/authStore';
1618
import type { ScopeSet, SmartAppFormData } from '@/lib/types/api';
19+
import type { GetAdminMcpServers200ResponseServersInner } from '@/lib/api-client';
1720

1821
type SmartAppType = 'backend-service' | 'standalone-app' | 'ehr-launch' | 'agent';
1922
type AuthenticationType = 'asymmetric' | 'symmetric' | 'none';
2023
type ServerAccessType = 'all-servers' | 'selected-servers' | 'user-person-servers';
24+
type McpAccessType = 'none' | 'all-mcp-servers' | 'selected-mcp-servers';
2125

2226
interface SmartAppAddFormProps {
2327
open: boolean;
@@ -28,6 +32,31 @@ interface SmartAppAddFormProps {
2832

2933
export 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">

ui/src/components/SmartAppsManager/SmartAppsManager.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,33 @@ export function SmartAppsManager() {
803803
</CardContent>
804804
</Card>
805805

806+
{/* MCP Access Section */}
807+
<Card>
808+
<CardHeader>
809+
<CardTitle className="text-sm">MCP Server Access (AI Capabilities)</CardTitle>
810+
</CardHeader>
811+
<CardContent>
812+
<div className="space-y-2">
813+
<div>
814+
<div className="text-xs font-medium mb-1">Access Type:</div>
815+
<Badge variant="outline" className="text-xs">
816+
{(editingApp as SmartApp & { mcpAccessType?: string }).mcpAccessType || 'none'}
817+
</Badge>
818+
</div>
819+
{(editingApp as SmartApp & { mcpAccessType?: string; allowedMcpServerNames?: string[] }).mcpAccessType === 'selected-mcp-servers' && (
820+
<div>
821+
<div className="text-xs font-medium mb-1">Allowed MCP Servers:</div>
822+
<div className="flex flex-wrap gap-1">
823+
{(editingApp as SmartApp & { allowedMcpServerNames?: string[] }).allowedMcpServerNames?.map((name, i) => (
824+
<Badge key={i} variant="outline" className="text-xs bg-violet-500/10 text-violet-700 dark:text-violet-400 border-violet-500/20">{name}</Badge>
825+
)) || <span className="text-sm text-muted-foreground">None</span>}
826+
</div>
827+
</div>
828+
)}
829+
</div>
830+
</CardContent>
831+
</Card>
832+
806833
<div className="flex justify-end pt-4">
807834
<Button onClick={() => setShowConfigDialog(false)}>Close</Button>
808835
</div>

ui/src/lib/types/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ export interface HealthcareUserFormData extends CreateHealthcareUserRequest {
6464
export interface SmartAppFormData extends CreateSmartAppRequest {
6565
authenticationType?: 'asymmetric' | 'symmetric' | 'none'; // UI-only field for form UX
6666
secret?: string; // Temporary fix until TypeScript client is regenerated
67+
// MCP server access control (added for Issue #189)
68+
mcpAccessType?: 'none' | 'all-mcp-servers' | 'selected-mcp-servers';
69+
allowedMcpServerNames?: string[];
6770
}
6871

6972
export interface ScopeSet {

0 commit comments

Comments
 (0)