feat(mcp): support mcp resources (#2215)

* feat(mcp): support mcp resources

* feat(web): split MCP resources into tab

* docs: add MCP resources PR review

* feat(mcp): productionize resource support

* feat(mcp): scope local agent tools and resources

* fix(web): gate space embedding models behind login

* fix(web): prevent clipped space model CTA

* test: update preproc resource tool expectations

* fix(web): expose skill authoring tools in selector

---------

Co-authored-by: yang.xiang <yang.xiang@advancegroup.com>
Co-authored-by: Hyu <chenhyu@proton.me>
Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
advancer-young
2026-06-30 19:16:30 +08:00
committed by GitHub
parent 2618e06492
commit 096ec1a8ce
39 changed files with 4103 additions and 201 deletions
@@ -12,16 +12,40 @@ import {
DialogFooter,
} from '@/components/ui/dialog';
import { Checkbox } from '@/components/ui/checkbox';
import { Plus, X, Server, Wrench, Sparkles } from 'lucide-react';
import { CircleHelp, Plus, X, Server, Wrench, Sparkles } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Plugin } from '@/app/infra/entities/plugin';
import { MCPServer, Skill } from '@/app/infra/entities/api';
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice';
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
function InfoTooltip({ label }: { label: string }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
aria-label={label}
>
<CircleHelp className="size-3.5" />
</button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[280px]">
{label}
</TooltipContent>
</Tooltip>
);
}
export default function PipelineExtension({
pipelineId,
}: {
@@ -418,9 +442,14 @@ export default function PipelineExtension({
{/* MCP Servers Section */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-foreground">
{t('pipelines.extensions.mcpServersTitle')}
</h3>
<div className="flex items-center gap-1.5">
<h3 className="text-sm font-semibold text-foreground">
{t('pipelines.extensions.mcpServersTitle')}
</h3>
<InfoTooltip
label={t('pipelines.extensions.mcpServersScopeTooltip')}
/>
</div>
<div className="flex items-center gap-2">
<Label
htmlFor="enable-all-mcp-servers"
@@ -428,6 +457,9 @@ export default function PipelineExtension({
>
{t('pipelines.extensions.enableAllMCPServers')}
</Label>
<InfoTooltip
label={t('pipelines.extensions.enableAllMCPServersTooltip')}
/>
<Switch
id="enable-all-mcp-servers"
checked={enableAllMCPServers}
@@ -427,13 +427,14 @@ export default function PipelineFormComponent({
const forcedBoxTemplate =
systemInfo.limitation?.force_box_session_id_template || '';
const boxScopeForced = !!forcedBoxTemplate;
const stageSystemContext =
stage.name === 'local-agent'
? {
box_available: boxAvailable,
box_scope_editable: boxAvailable && !boxScopeForced,
}
: undefined;
const isLocalAgentStage = formName === 'ai' && stage.name === 'local-agent';
const stageSystemContext = isLocalAgentStage
? {
box_available: boxAvailable,
box_scope_editable: boxAvailable && !boxScopeForced,
pipeline_id: pipelineId,
}
: undefined;
// When the deployment pins every pipeline to a fixed sandbox scope (SaaS
// ``force_box_session_id_template``), the Sandbox Scope selector is locked.
@@ -446,12 +447,26 @@ export default function PipelineFormComponent({
const stageInitialValues: Record<string, any> =
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
const effectiveInitialValues =
stage.name === 'local-agent' && boxScopeForced
isLocalAgentStage && boxScopeForced
? {
...stageInitialValues,
'box-session-id-template': forcedBoxTemplate,
}
: stageInitialValues;
const emitStageValues = (values: object) => {
if (!isLocalAgentStage) {
handleDynamicFormEmit(formName, stage.name, values);
return;
}
const latestStageValues =
((form.getValues(formName) as Record<string, any>) || {})[stage.name] ||
{};
handleDynamicFormEmit(formName, stage.name, {
...latestStageValues,
...values,
});
};
return (
<Card key={stage.name}>
@@ -467,9 +482,7 @@ export default function PipelineFormComponent({
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={effectiveInitialValues}
onSubmit={(values) => {
handleDynamicFormEmit(formName, stage.name, values);
}}
onSubmit={emitStageValues}
systemContext={stageSystemContext}
/>
</CardContent>