diff --git a/packages/browseros-agent/apps/agent/entrypoints/app/connect-mcp/AddCustomMCPDialog.tsx b/packages/browseros-agent/apps/agent/entrypoints/app/connect-mcp/AddCustomMCPDialog.tsx index 381873a70..f0bb18b9d 100644 --- a/packages/browseros-agent/apps/agent/entrypoints/app/connect-mcp/AddCustomMCPDialog.tsx +++ b/packages/browseros-agent/apps/agent/entrypoints/app/connect-mcp/AddCustomMCPDialog.tsx @@ -27,24 +27,111 @@ import { FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' -const formSchema = z.object({ - name: z.string().min(1, 'Server name is required'), - url: z.string().url('Please enter a valid URL'), - description: z.string().optional(), -}) +const ARGUMENTS_TEXT = '-y\nanythingllm-mcp-server@2.0.0' +const ENV_TEXT = + 'ANYTHINGLLM_BASE_URL=http://localhost:3001\nANYTHINGLLM_API_KEY=' + +const isValidUrl = (value: string) => { + try { + new URL(value) + return true + } catch { + return false + } +} + +const parseArgsText = (value?: string) => + value + ?.split('\n') + .map((line) => line.trim()) + .filter(Boolean) ?? [] + +const parseEnvText = (value?: string): Record | undefined => { + const env: Record = {} + for (const rawLine of value?.split('\n') ?? []) { + const line = rawLine.trim() + if (!line) continue + const separatorIndex = line.indexOf('=') + if (separatorIndex <= 0) return undefined + const key = line.slice(0, separatorIndex).trim() + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return undefined + env[key] = line.slice(separatorIndex + 1) + } + return Object.keys(env).length ? env : undefined +} + +const formSchema = z + .object({ + name: z.string().min(1, 'Server name is required'), + type: z.enum(['http', 'process']), + url: z.string().optional(), + command: z.string().optional(), + argsText: z.string().optional(), + envText: z.string().optional(), + cwd: z.string().optional(), + description: z.string().optional(), + }) + .superRefine((values, ctx) => { + if (values.type === 'http') { + if (!values.url?.trim() || !isValidUrl(values.url.trim())) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['url'], + message: 'Please enter a valid URL', + }) + } + return + } + + if (!values.command?.trim()) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['command'], + message: 'Command is required', + }) + } + + if (values.envText?.trim() && parseEnvText(values.envText) === undefined) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['envText'], + message: 'Use KEY=value lines', + }) + } + }) type FormValues = z.infer +type CustomMcpConfig = + | { + name: string + type: 'http' + url: string + description: string + } + | { + name: string + type: 'process' + command: string + args?: string[] + env?: Record + cwd?: string + description: string + } + interface AddCustomMCPDialogProps { open: boolean onOpenChange: (open: boolean) => void - onAddServer: (config: { - name: string - url: string - description: string - }) => void + onAddServer: (config: CustomMcpConfig) => void } export const AddCustomMCPDialog: FC = ({ @@ -56,10 +143,16 @@ export const AddCustomMCPDialog: FC = ({ resolver: zodResolver(formSchema), defaultValues: { name: '', + type: 'http', url: '', + command: '', + argsText: '', + envText: '', + cwd: '', description: '', }, }) + const connectionType = form.watch('type') const handleOpenChange = (isOpen: boolean) => { if (!isOpen) { @@ -69,18 +162,45 @@ export const AddCustomMCPDialog: FC = ({ } const onSubmit = (values: FormValues) => { - onAddServer({ - name: values.name, - url: values.url, - description: values.description ?? '', - }) + if (values.type === 'process') { + const args = parseArgsText(values.argsText) + onAddServer({ + name: values.name, + type: 'process', + command: values.command?.trim() ?? '', + args: args.length ? args : undefined, + env: parseEnvText(values.envText), + cwd: values.cwd?.trim() || undefined, + description: values.description ?? '', + }) + } else { + onAddServer({ + name: values.name, + type: 'http', + url: values.url?.trim() ?? '', + description: values.description ?? '', + }) + } form.reset() onOpenChange(false) } + const applyAnythingLlmPreset = () => { + form.reset({ + name: 'AnythingLLM', + type: 'process', + url: '', + command: 'npx', + argsText: ARGUMENTS_TEXT, + envText: ENV_TEXT, + cwd: '', + description: 'Local AnythingLLM MCP agent', + }) + } + return ( - + Add Custom App @@ -90,6 +210,23 @@ export const AddCustomMCPDialog: FC = ({
+
+
+

AnythingLLM

+

+ Local stdio server via npx +

+
+ +
+ = ({ ( - MCP Server URL - (only supports HTTP) - - - + Connection Type + )} /> + {connectionType === 'http' ? ( + ( + + MCP Server URL + Streamable HTTP or SSE + + + + + + )} + /> + ) : ( + <> + ( + + Command + + + + + + )} + /> + + ( + + Arguments + One argument per line + +