Skip to content

Commit 5d3acdd

Browse files
committed
[front] Add skills-only MCP server setting
1 parent 1f00413 commit 5d3acdd

5 files changed

Lines changed: 152 additions & 12 deletions

File tree

‎front/components/actions/mcp/MCPServerDetails.tsx‎

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { clientFetch } from "@app/lib/egress/client";
2121
import {
2222
useMCPServer,
2323
useMCPServers,
24+
useMCPServersUsage,
25+
useMCPServerViewsFromSpaces,
2426
useMutateMCPServersViewsForAdmin,
2527
} from "@app/lib/swr/mcp_servers";
2628
import { useSpacesAsAdmin } from "@app/lib/swr/spaces";
@@ -58,11 +60,31 @@ export function MCPServerDetails({
5860
onClose,
5961
readOnly = false,
6062
}: MCPServerDetailsProps) {
61-
const { spaces } = useSpacesAsAdmin({
63+
const { spaces, isSpacesLoading, isSpacesError } = useSpacesAsAdmin({
6264
workspaceId: owner.sId,
6365
disabled: !isOpen || !isAdmin(owner),
6466
});
6567

68+
const {
69+
serverViews: directlyAvailableMCPServerViews,
70+
isLoading: isDirectAvailabilityLoading,
71+
isError: isDirectAvailabilityError,
72+
mutateServerViews: mutateDirectlyAvailableMCPServerViews,
73+
} = useMCPServerViewsFromSpaces(owner, spaces, {
74+
disabled: !isOpen || readOnly,
75+
});
76+
const isDirectAvailabilityResolved =
77+
!isSpacesLoading &&
78+
!isSpacesError &&
79+
!isDirectAvailabilityLoading &&
80+
!isDirectAvailabilityError;
81+
const isRestrictedToSkills =
82+
isDirectAvailabilityResolved &&
83+
mcpServerView !== null &&
84+
!directlyAvailableMCPServerViews.some(
85+
(view) => view.server.sId === mcpServerView.server.sId
86+
);
87+
6688
const { server: mcpServerWithViews, mutateMCPServer } = useMCPServer({
6789
owner,
6890
serverId: mcpServerView?.server.sId ?? "",
@@ -89,6 +111,10 @@ export function MCPServerDetails({
89111
owner,
90112
disabled: !isOpen || readOnly,
91113
});
114+
const { usage, mutate: mutateMCPServersUsage } = useMCPServersUsage({
115+
owner,
116+
disabled: !isOpen || readOnly,
117+
});
92118

93119
// Collect all effective view names from other servers (excluding the current one).
94120
const existingViewNames = useMemo(
@@ -105,19 +131,23 @@ export function MCPServerDetails({
105131

106132
const defaults = useMemo<MCPServerFormValues>(() => {
107133
if (mcpServerView) {
108-
return getMCPServerFormDefaults(
109-
mcpServerView,
110-
mcpServerWithViews ?? undefined,
111-
spaces
112-
);
134+
return {
135+
...getMCPServerFormDefaults(
136+
mcpServerView,
137+
mcpServerWithViews ?? undefined,
138+
spaces
139+
),
140+
isRestrictedToSkills,
141+
};
113142
}
114143
return {
115144
name: "",
116145
description: "",
146+
isRestrictedToSkills: false,
117147
toolSettings: {},
118148
sharingSettings: {},
119149
};
120-
}, [mcpServerView, mcpServerWithViews, spaces]);
150+
}, [isRestrictedToSkills, mcpServerView, mcpServerWithViews, spaces]);
121151

122152
const form = useForm<MCPServerFormValues>({
123153
values: defaults,
@@ -136,6 +166,33 @@ export function MCPServerDetails({
136166
: undefined,
137167
});
138168

169+
const confirmDirectAvailabilityChange = async (
170+
isAvailableToAgents: boolean
171+
): Promise<boolean> => {
172+
if (isAvailableToAgents || !mcpServerView) {
173+
return true;
174+
}
175+
176+
const affectedAgents = usage?.[mcpServerView.server.sId]?.agents ?? [];
177+
if (affectedAgents.length === 0) {
178+
return true;
179+
}
180+
181+
return confirm({
182+
title: "Remove this tool from agents?",
183+
message: (
184+
<div>
185+
This tool is currently used directly by {affectedAgents.length}{" "}
186+
{affectedAgents.length === 1 ? "agent" : "agents"}. Saving this change
187+
will remove it from those agents. Skills using this tool will not be
188+
affected.
189+
</div>
190+
),
191+
validateLabel: "Continue",
192+
validateVariant: "warning",
193+
});
194+
};
195+
139196
const applyToolChanges = async (
140197
toolChanges: Array<{
141198
toolName: string;
@@ -217,20 +274,22 @@ export function MCPServerDetails({
217274

218275
const applyInfoChanges = async (diff: {
219276
serverView?: { name: string; description: string };
277+
isRestrictedToSkills?: boolean;
220278
icon?: string;
221279
authSharedSecret?: string;
222280
authCustomHeaders?: any;
223281
authMeta?: Record<string, string> | null;
224282
}) => {
225283
const hasServerViewChanges = diff.serverView !== undefined;
284+
const hasSkillsOnlyChanges = diff.isRestrictedToSkills !== undefined;
226285
const hasIconChanges = diff.icon !== undefined;
227286
const hasSecretChanges = diff.authSharedSecret !== undefined;
228287
const hasHeaderChanges = diff.authCustomHeaders !== undefined;
229288
const hasMetaChanges = diff.authMeta !== undefined;
230289
const hasRemoteChanges =
231290
hasIconChanges || hasSecretChanges || hasHeaderChanges || hasMetaChanges;
232291

233-
if (!hasServerViewChanges && !hasRemoteChanges) {
292+
if (!hasServerViewChanges && !hasSkillsOnlyChanges && !hasRemoteChanges) {
234293
return;
235294
}
236295

@@ -250,6 +309,23 @@ export function MCPServerDetails({
250309
}
251310
}
252311

312+
if (hasSkillsOnlyChanges) {
313+
const response = await clientFetch(
314+
`/api/w/${owner.sId}/mcp/views/${mcpServerView?.sId}`,
315+
{
316+
method: "PATCH",
317+
headers: { "Content-Type": "application/json" },
318+
body: JSON.stringify({
319+
isRestrictedToSkills: diff.isRestrictedToSkills,
320+
}),
321+
}
322+
);
323+
if (!response.ok) {
324+
const body = await response.json();
325+
throw new Error(body.error?.message ?? "Failed to update server view");
326+
}
327+
}
328+
253329
// Patch remote server settings if needed. icon and meta use separate
254330
// requests because they are distinct discriminants in the API schema.
255331
// sharedSecret and customHeaders can be combined in one request.
@@ -357,6 +433,8 @@ export function MCPServerDetails({
357433
// Revalidate caches.
358434
await mutateMCPServersViewsForAdmin();
359435
await mutateMCPServer();
436+
await mutateMCPServersUsage();
437+
await mutateDirectlyAvailableMCPServerViews();
360438

361439
sendNotification({
362440
type: "success",
@@ -442,6 +520,8 @@ export function MCPServerDetails({
442520
spaces={spaces}
443521
readOnly={readOnly}
444522
sensitivityLabelsController={sensitivityLabelsController}
523+
confirmDirectAvailabilityChange={confirmDirectAvailabilityChange}
524+
isDirectAvailabilityResolved={isDirectAvailabilityResolved}
445525
/>
446526
</FormProvider>
447527
);

‎front/components/actions/mcp/MCPServerDetailsInfo.tsx‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ type MCPServerDetailsInfoProps = {
2424
owner: LightWorkspaceType;
2525
readOnly?: boolean;
2626
sensitivityLabelsController?: SensitivityLabelsController;
27+
confirmDirectAvailabilityChange?: (
28+
isAvailableToAgents: boolean
29+
) => Promise<boolean>;
30+
isDirectAvailabilityResolved?: boolean;
2731
};
2832

2933
export function MCPServerDetailsInfo({
3034
mcpServerView,
3135
owner,
3236
readOnly = false,
3337
sensitivityLabelsController,
38+
confirmDirectAvailabilityChange,
39+
isDirectAvailabilityResolved,
3440
}: MCPServerDetailsInfoProps) {
3541
const editedAt = useMemo(() => {
3642
const d = new Date(0);
@@ -82,7 +88,11 @@ export function MCPServerDetailsInfo({
8288
</div>
8389
)}
8490
<Separator />
85-
<MCPServerViewForm mcpServerView={mcpServerView} />
91+
<MCPServerViewForm
92+
mcpServerView={mcpServerView}
93+
confirmDirectAvailabilityChange={confirmDirectAvailabilityChange}
94+
isDirectAvailabilityResolved={isDirectAvailabilityResolved ?? false}
95+
/>
8696
<Separator />
8797
{mcpServerView.server.authorization && (
8898
<MCPServerSettings

‎front/components/actions/mcp/MCPServerDetailsSheet.tsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ interface MCPServerDetailsSheetProps {
4646
spaces: SpaceType[];
4747
readOnly?: boolean;
4848
sensitivityLabelsController?: SensitivityLabelsController;
49+
confirmDirectAvailabilityChange: (
50+
isAvailableToAgents: boolean
51+
) => Promise<boolean>;
52+
isDirectAvailabilityResolved: boolean;
4953
}
5054

5155
export function MCPServerDetailsSheet({
@@ -58,6 +62,8 @@ export function MCPServerDetailsSheet({
5862
spaces,
5963
readOnly = false,
6064
sensitivityLabelsController,
65+
confirmDirectAvailabilityChange,
66+
isDirectAvailabilityResolved,
6167
}: MCPServerDetailsSheetProps) {
6268
const [selectedTab, setSelectedTab] = useState<TabType>("info");
6369
const [prevIsOpen, setPrevIsOpen] = useState(isOpen);
@@ -208,6 +214,12 @@ export function MCPServerDetailsSheet({
208214
sensitivityLabelsController={
209215
sensitivityLabelsController
210216
}
217+
confirmDirectAvailabilityChange={
218+
confirmDirectAvailabilityChange
219+
}
220+
isDirectAvailabilityResolved={
221+
isDirectAvailabilityResolved
222+
}
211223
/>
212224
</div>
213225
)}

‎front/components/actions/mcp/create/MCPServerViewForm.tsx‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
import type { MCPServerFormValues } from "@app/components/actions/mcp/forms/mcpServerFormSchema";
22
import { getMcpServerViewDescription } from "@app/lib/actions/mcp_helper";
33
import type { MCPServerViewType } from "@app/lib/api/mcp";
4-
import { Input } from "@dust-tt/sparkle";
5-
import { useFormContext } from "react-hook-form";
4+
import { CheckBoxWithTextAndDescription, Input } from "@dust-tt/sparkle";
5+
import { useController, useFormContext } from "react-hook-form";
66

77
interface MCPServerViewFormProps {
88
mcpServerView: MCPServerViewType;
9+
confirmDirectAvailabilityChange?: (
10+
isAvailableToAgents: boolean
11+
) => Promise<boolean>;
12+
isDirectAvailabilityResolved: boolean;
913
}
1014

11-
export function MCPServerViewForm({ mcpServerView }: MCPServerViewFormProps) {
15+
export function MCPServerViewForm({
16+
mcpServerView,
17+
confirmDirectAvailabilityChange,
18+
isDirectAvailabilityResolved,
19+
}: MCPServerViewFormProps) {
1220
const form = useFormContext<MCPServerFormValues>();
21+
const { field: isRestrictedToSkillsField } = useController({
22+
name: "isRestrictedToSkills",
23+
control: form.control,
24+
});
1325

1426
return (
1527
<div className="space-y-5 text-foreground">
@@ -38,6 +50,24 @@ export function MCPServerViewForm({ mcpServerView }: MCPServerViewFormProps) {
3850
This is only for internal reference and is not shown to the model.
3951
</p>
4052
</div>
53+
54+
<CheckBoxWithTextAndDescription
55+
text="Available to agents"
56+
description="Allow agents to add and use this tool directly. Skills can always use it."
57+
checked={!isRestrictedToSkillsField.value}
58+
disabled={!isDirectAvailabilityResolved}
59+
onCheckedChange={(checked) => {
60+
const isAvailableToAgents = checked === true;
61+
void (async () => {
62+
const confirmed =
63+
!confirmDirectAvailabilityChange ||
64+
(await confirmDirectAvailabilityChange(isAvailableToAgents));
65+
if (confirmed) {
66+
isRestrictedToSkillsField.onChange(!isAvailableToAgents);
67+
}
68+
})();
69+
}}
70+
/>
4171
</div>
4272
);
4373
}

‎front/components/actions/mcp/forms/mcpServerFormSchema.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function decodeToolSettingsFromForm(
6060
export type ServerSettings = {
6161
name: string;
6262
description: string;
63+
isRestrictedToSkills: boolean;
6364
icon?: string;
6465
sharedSecret?: string;
6566
customHeaders?: HeaderRow[] | null;
@@ -161,6 +162,7 @@ export function getMCPServerFormDefaults(
161162
const defaults: MCPServerFormValues = {
162163
name: view.name ?? view.server.name,
163164
description: getMcpServerViewDescription(view),
165+
isRestrictedToSkills: false,
164166
toolSettings: encodeToolSettingsForForm(toolSettings),
165167
sharingSettings,
166168
};
@@ -209,6 +211,7 @@ export function getMCPServerFormSchema(
209211
{ message: "This name is already in use." }
210212
),
211213
description: z.string().min(1, "Description is required."),
214+
isRestrictedToSkills: z.boolean(),
212215
toolSettings: z.record(
213216
z.object({
214217
enabled: z.boolean(),
@@ -248,6 +251,7 @@ export function getMCPServerFormSchema(
248251

249252
type FormDiffType = {
250253
serverView?: { name: string; description: string };
254+
isRestrictedToSkills?: boolean;
251255
icon?: string;
252256
authSharedSecret?: string;
253257
authCustomHeaders?: HeaderRow[] | null;
@@ -287,6 +291,10 @@ export function diffMCPServerForm(
287291
};
288292
}
289293

294+
if (current.isRestrictedToSkills !== initial.isRestrictedToSkills) {
295+
out.isRestrictedToSkills = current.isRestrictedToSkills;
296+
}
297+
290298
// Check remote-specific changes.
291299
if (isRemote) {
292300
if (current.icon && current.icon !== initial.icon) {

0 commit comments

Comments
 (0)