+ )
+}
diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/McpServerItemControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/McpServerItemControl.tsx
new file mode 100644
index 0000000000..7eea7e82ba
--- /dev/null
+++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/McpServerItemControl.tsx
@@ -0,0 +1,139 @@
+/**
+ * McpServerItemControl
+ *
+ * Schema-driven control for one declared MCP server on the agent config. MCP servers are a
+ * sibling of `tools` (not a tool variant): a server names a transport (stdio command/args/env
+ * or a remote url), an optional tool allowlist, and the vault secret names the backend resolves
+ * into its env at run time. The shape is open enough that a JSON editor is the pragmatic v1 —
+ * the same approach ToolItemControl takes for tool definitions — with a name header and a delete
+ * control. The typed shape lives in the `agent_config` catalog type (AgentConfigSchema /
+ * McpServer in the SDK); this control just edits one entry of the `mcp_servers` array.
+ */
+import {memo, useCallback, useEffect, useRef, useState} from "react"
+
+import {isPlainObject, safeStringify} from "@agenta/shared/utils"
+import {useDrillInUI} from "@agenta/ui/drill-in"
+import {MinusCircle} from "@phosphor-icons/react"
+import {Button, Tooltip, Typography} from "antd"
+import clsx from "clsx"
+
+export interface McpServerItemControlProps {
+ /** MCP server value (object or JSON string) */
+ value: unknown
+ /** Called when the server value changes (only on valid JSON) */
+ onChange?: (value: Record) => void
+ /** Called when the server should be removed */
+ onDelete?: () => void
+ /** Whether the control is read-only */
+ disabled?: boolean
+ /** Additional CSS classes */
+ className?: string
+}
+
+function toServerObj(value: unknown): Record {
+ try {
+ if (typeof value === "string") {
+ const parsed = value ? JSON.parse(value) : {}
+ return isPlainObject(parsed) ? parsed : {}
+ }
+ if (isPlainObject(value)) return value
+ } catch {
+ // fall through to empty object
+ }
+ return {}
+}
+
+export const McpServerItemControl = memo(function McpServerItemControl({
+ value,
+ onChange,
+ onDelete,
+ disabled = false,
+ className,
+}: McpServerItemControlProps) {
+ const {SharedEditor} = useDrillInUI()
+ const serverObj = toServerObj(value)
+ const name =
+ typeof serverObj.name === "string" && serverObj.name ? serverObj.name : "MCP server"
+
+ const [editorText, setEditorText] = useState(() => safeStringify(serverObj ?? {}))
+
+ // Reset the editor text when the value changes from outside (add/remove/reorder).
+ const lastExternalRef = useRef(safeStringify(serverObj ?? {}))
+ useEffect(() => {
+ const next = safeStringify(toServerObj(value) ?? {})
+ if (next !== lastExternalRef.current) {
+ lastExternalRef.current = next
+ setEditorText(next)
+ }
+ }, [value])
+
+ const handleEditorChange = useCallback(
+ (text: string) => {
+ if (disabled) return
+ setEditorText(text)
+ try {
+ const parsed = text ? JSON.parse(text) : {}
+ if (!isPlainObject(parsed)) return
+ lastExternalRef.current = safeStringify(parsed)
+ onChange?.(parsed)
+ } catch {
+ // Keep the invalid text in the editor; don't propagate until it parses.
+ }
+ },
+ [disabled, onChange],
+ )
+
+ const header = (
+