-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Add MCP section to config web UI #2770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
5de68cb
4fcb826
a6a13b7
6e8e346
3a96d8b
f1737f7
bc8c08d
7c989ce
17b24e8
d7f77e7
c1442a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,16 +22,19 @@ import { | |
| DevicesSection, | ||
| ExecSection, | ||
| LauncherSection, | ||
| MCPSection, | ||
| RuntimeSection, | ||
| } from "@/components/config/config-sections" | ||
| import { | ||
| type CoreConfigForm, | ||
| EMPTY_FORM, | ||
| EMPTY_LAUNCHER_FORM, | ||
| type LauncherForm, | ||
| type MCPServerForm, | ||
| buildFormFromConfig, | ||
| parseCIDRText, | ||
| parseIntField, | ||
| parseJSONObjectField, | ||
| parseMultilineList, | ||
| } from "@/components/config/form-model" | ||
| import { PageHeader } from "@/components/page-header" | ||
|
|
@@ -143,6 +146,42 @@ export function ConfigPage() { | |
| setLauncherForm((prev) => ({ ...prev, [key]: value })) | ||
| } | ||
|
|
||
| const handleMCPServerAdd = () => { | ||
| const nextIndex = form.mcpServers.length + 1 | ||
| const server: MCPServerForm = { | ||
| id: `mcp-${Date.now()}-${nextIndex}`, | ||
| name: "", | ||
| enabled: true, | ||
| type: "stdio", | ||
| url: "", | ||
| command: "", | ||
| argsText: "", | ||
| envText: "{}", | ||
| headersText: "{}", | ||
| } | ||
| updateField("mcpServers", [...form.mcpServers, server]) | ||
| } | ||
|
|
||
| const handleMCPServerRemove = (id: string) => { | ||
| updateField( | ||
| "mcpServers", | ||
| form.mcpServers.filter((server) => server.id !== id), | ||
| ) | ||
| } | ||
|
|
||
| const handleMCPServerFieldChange = <K extends keyof MCPServerForm>( | ||
| id: string, | ||
| key: K, | ||
| value: MCPServerForm[K], | ||
| ) => { | ||
| updateField( | ||
| "mcpServers", | ||
| form.mcpServers.map((server) => | ||
| server.id === id ? { ...server, [key]: value } : server, | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| const handleReset = () => { | ||
| setForm(baseline) | ||
| setLauncherForm(launcherBaseline) | ||
|
|
@@ -178,6 +217,17 @@ export function ConfigPage() { | |
| throw new Error("Session scope is required.") | ||
| } | ||
|
|
||
| if ( | ||
| form.mcpEnabled && | ||
| form.mcpDiscoveryEnabled && | ||
| !form.mcpDiscoveryUseBM25 && | ||
| !form.mcpDiscoveryUseRegex | ||
| ) { | ||
| throw new Error( | ||
| "MCP discovery requires at least one search method (BM25 or regex).", | ||
| ) | ||
| } | ||
|
|
||
| const maxTokens = parseIntField(form.maxTokens, "Max tokens", { | ||
| min: 1, | ||
| }) | ||
|
|
@@ -214,10 +264,113 @@ export function ConfigPage() { | |
| "Cron exec timeout", | ||
| { min: 0 }, | ||
| ) | ||
| const mcpDiscoveryTTL = parseIntField( | ||
| form.mcpDiscoveryTTL, | ||
| "MCP discovery ttl", | ||
| { min: 0 }, | ||
| ) | ||
| const mcpDiscoveryMaxSearchResults = parseIntField( | ||
| form.mcpDiscoveryMaxSearchResults, | ||
| "MCP discovery max search results", | ||
| { min: 0 }, | ||
| ) | ||
|
Gabrielsv01 marked this conversation as resolved.
Outdated
|
||
| const execConfigPatch: Record<string, unknown> = { | ||
| enabled: form.execEnabled, | ||
| } | ||
|
|
||
| const baselineServerNames = new Set( | ||
| baseline.mcpServers | ||
| .map((server) => server.name.trim()) | ||
| .filter((name) => name !== ""), | ||
| ) | ||
|
|
||
| const normalizedServers = form.mcpServers | ||
| .map((server) => ({ | ||
| ...server, | ||
| name: server.name.trim(), | ||
| url: server.url.trim(), | ||
| command: server.command.trim(), | ||
| })) | ||
| .filter((server) => server.name !== "") | ||
|
|
||
| const serverNameCounts = new Map<string, number>() | ||
| for (const server of normalizedServers) { | ||
| serverNameCounts.set( | ||
| server.name, | ||
| (serverNameCounts.get(server.name) ?? 0) + 1, | ||
| ) | ||
| } | ||
|
|
||
| const duplicateNames = Array.from(serverNameCounts.entries()) | ||
| .filter(([, count]) => count > 1) | ||
| .map(([name]) => name) | ||
| .sort((a, b) => a.localeCompare(b)) | ||
|
|
||
| if (duplicateNames.length > 0) { | ||
| throw new Error( | ||
| `MCP server names must be unique. Duplicates: ${duplicateNames.join(", ")}.`, | ||
| ) | ||
| } | ||
|
|
||
| const currentServerNames = new Set( | ||
| normalizedServers.map((server) => server.name), | ||
| ) | ||
|
|
||
| const removedServerEntries = Array.from(baselineServerNames) | ||
| .filter((name) => !currentServerNames.has(name)) | ||
| .map((name) => [name, null] as const) | ||
|
|
||
| const upsertServerEntries = normalizedServers.map((server) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Please either keep the server editor accessible when MCP is disabled, or only require |
||
| if (server.type !== "stdio") { | ||
| if (server.url === "") { | ||
| throw new Error(`MCP server ${server.name} requires a URL.`) | ||
| } | ||
|
|
||
| return [ | ||
| server.name, | ||
| { | ||
| enabled: server.enabled, | ||
| type: server.type, | ||
| url: server.url, | ||
| headers: parseJSONObjectField( | ||
| server.headersText, | ||
| `MCP server ${server.name} headers`, | ||
|
Gabrielsv01 marked this conversation as resolved.
Outdated
|
||
| ), | ||
| command: null, | ||
| args: null, | ||
| env: null, | ||
| env_file: null, | ||
| }, | ||
| ] as const | ||
| } | ||
|
|
||
| if (server.command === "") { | ||
| throw new Error(`MCP server ${server.name} requires a command.`) | ||
| } | ||
|
|
||
| return [ | ||
| server.name, | ||
| { | ||
| enabled: server.enabled, | ||
| type: "stdio", | ||
| command: server.command, | ||
| args: parseMultilineList(server.argsText), | ||
| env: parseJSONObjectField( | ||
| server.envText, | ||
| `MCP server ${server.name} env`, | ||
| ), | ||
|
Gabrielsv01 marked this conversation as resolved.
|
||
| url: null, | ||
| headers: null, | ||
| env_file: null, | ||
|
Gabrielsv01 marked this conversation as resolved.
Outdated
Gabrielsv01 marked this conversation as resolved.
Outdated
|
||
| }, | ||
| ] as const | ||
|
Gabrielsv01 marked this conversation as resolved.
|
||
| }) | ||
|
|
||
| const mcpServersPatch = Object.fromEntries([ | ||
| ...upsertServerEntries, | ||
| ...removedServerEntries, | ||
| ]) | ||
|
Gabrielsv01 marked this conversation as resolved.
Comment on lines
+444
to
+447
|
||
|
|
||
| if (form.execEnabled) { | ||
| execConfigPatch.allow_remote = form.allowRemote | ||
| execConfigPatch.enable_deny_patterns = form.enableDenyPatterns | ||
|
|
@@ -264,6 +417,17 @@ export function ConfigPage() { | |
| exec_timeout_minutes: cronExecTimeoutMinutes, | ||
| }, | ||
| exec: execConfigPatch, | ||
| mcp: { | ||
| enabled: form.mcpEnabled, | ||
| discovery: { | ||
| enabled: form.mcpDiscoveryEnabled, | ||
| ttl: mcpDiscoveryTTL, | ||
| max_search_results: mcpDiscoveryMaxSearchResults, | ||
| use_bm25: form.mcpDiscoveryUseBM25, | ||
| use_regex: form.mcpDiscoveryUseRegex, | ||
| }, | ||
|
Gabrielsv01 marked this conversation as resolved.
Outdated
|
||
| servers: mcpServersPatch, | ||
| }, | ||
| }, | ||
| heartbeat: { | ||
| enabled: form.heartbeatEnabled, | ||
|
|
@@ -414,6 +578,14 @@ export function ConfigPage() { | |
|
|
||
| <RuntimeSection form={form} onFieldChange={updateField} /> | ||
|
|
||
| <MCPSection | ||
| form={form} | ||
| onFieldChange={updateField} | ||
| onAddServer={handleMCPServerAdd} | ||
| onRemoveServer={handleMCPServerRemove} | ||
| onServerFieldChange={handleMCPServerFieldChange} | ||
| /> | ||
|
|
||
| <ExecSection form={form} onFieldChange={updateField} /> | ||
|
|
||
| <CronSection form={form} onFieldChange={updateField} /> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.