mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 14:06:18 +00:00
refactor(capabilities): simplify labels and connection validation (#5580)
This commit is contained in:
parent
5051709343
commit
075f4a3607
@ -54,6 +54,37 @@ class CapabilityAdapter(Protocol):
|
||||
async def install(self, context: AdapterContext, manifest: PluginManifest, name: str, configuration: dict[str, Any]) -> None: ...
|
||||
|
||||
|
||||
def validate_mcp_connection(configuration: dict[str, Any]) -> None:
|
||||
"""Validate the normalized transport definition, not manifest form fields."""
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
transport = configuration.get("type", configuration.get("transport", "stdio"))
|
||||
if not isinstance(transport, str):
|
||||
raise HTTPException(422, "Supply a supported MCP transport")
|
||||
if transport in {"http", "sse"}:
|
||||
url = configuration.get("url")
|
||||
valid = False
|
||||
try:
|
||||
if isinstance(url, str):
|
||||
parsed = urlsplit(url)
|
||||
_ = parsed.port # Validate malformed ports too.
|
||||
has_http_scheme = parsed.scheme in {"https", "http"}
|
||||
has_host = bool(parsed.hostname)
|
||||
has_credentials = parsed.username is not None or parsed.password is not None
|
||||
has_whitespace = any(c.isspace() for c in url)
|
||||
valid = has_http_scheme and has_host and not has_credentials and not parsed.fragment and not has_whitespace
|
||||
except ValueError:
|
||||
valid = False
|
||||
if not valid:
|
||||
raise HTTPException(422, "Supply an HTTP(S) MCP server URL without embedded credentials")
|
||||
return
|
||||
if transport != "stdio":
|
||||
raise HTTPException(422, "Supply a supported MCP transport and its required connection fields")
|
||||
command = configuration.get("command")
|
||||
if not isinstance(command, str) or not command.strip():
|
||||
raise HTTPException(422, "Supply a supported MCP transport and its required connection fields")
|
||||
|
||||
|
||||
class MCPAdapter:
|
||||
async def list_installations(self, context: AdapterContext) -> list[CapabilityInstallation]:
|
||||
servers = await asyncio.to_thread(mcp._load_raw_mcp_server_responses)
|
||||
@ -97,26 +128,7 @@ class MCPAdapter:
|
||||
if not name.strip():
|
||||
raise HTTPException(422, "Installation name is required")
|
||||
if manifest.adapter == "mcp":
|
||||
# The manifest form is normalized to a transport definition by the
|
||||
# UI. Validate that wire contract, not form-only name/auth fields.
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
transport = configuration.get("type", configuration.get("transport", "stdio"))
|
||||
if not isinstance(transport, str):
|
||||
raise HTTPException(422, "Supply a supported MCP transport")
|
||||
if transport in {"http", "sse"}:
|
||||
url = configuration.get("url")
|
||||
try:
|
||||
parsed = urlsplit(url) if isinstance(url, str) else None
|
||||
valid = parsed is not None and parsed.scheme in {"https", "http"} and bool(parsed.hostname) and parsed.username is None and parsed.password is None and not parsed.fragment and not any(c.isspace() for c in url)
|
||||
if parsed is not None:
|
||||
_ = parsed.port # Validate malformed ports too.
|
||||
except ValueError:
|
||||
valid = False
|
||||
if not valid:
|
||||
raise HTTPException(422, "Supply an HTTP(S) MCP server URL without embedded credentials")
|
||||
elif transport != "stdio" or not isinstance(configuration.get("command"), str) or not configuration["command"].strip():
|
||||
raise HTTPException(422, "Supply a supported MCP transport and its required connection fields")
|
||||
validate_mcp_connection(configuration)
|
||||
definition = {**configuration, "capability": {"id": str(uuid4()), "plugin_id": manifest.id, "version": manifest.version}}
|
||||
try:
|
||||
body = mcp.McpConfigUpdateRequest(mcp_servers={name: mcp.McpServerConfigResponse.model_validate(definition)})
|
||||
|
||||
@ -99,11 +99,7 @@ function MCPServerList({
|
||||
const previewEntry = previewEntries[0];
|
||||
const previewName =
|
||||
editor?.mode === "edit" ? editor.name : (previewEntry?.[0] ?? "");
|
||||
const previewMetadata = catalogForServer(
|
||||
previewName,
|
||||
previewEntry?.[1],
|
||||
definitions,
|
||||
);
|
||||
const previewMetadata = catalogForServer(previewEntry?.[1], definitions);
|
||||
const previewIcon =
|
||||
draftIcon === undefined && previewEntry
|
||||
? readPluginIcon(previewEntry[1])
|
||||
@ -276,13 +272,13 @@ function MCPServerList({
|
||||
...catalog.filter(
|
||||
(item) =>
|
||||
!entries.some(
|
||||
([name, server]) =>
|
||||
catalogForServer(name, server, definitions)?.id === item.id,
|
||||
([, server]) =>
|
||||
catalogForServer(server, definitions)?.id === item.id,
|
||||
),
|
||||
),
|
||||
...entries.map(([name, config]): PluginDirectoryEntry => {
|
||||
const displayName = displayServerName(name);
|
||||
const metadata = catalogForServer(name, config, definitions);
|
||||
const metadata = catalogForServer(config, definitions);
|
||||
return {
|
||||
id: `mcp:${name}`,
|
||||
category: metadata?.category ?? "custom",
|
||||
|
||||
@ -15,7 +15,6 @@ export function catalogText(text: LocalizedText, locale: string) {
|
||||
}
|
||||
/** Identity is explicit metadata. A display name never claims an official provider. */
|
||||
export function catalogForServer(
|
||||
_name: string,
|
||||
config?: MCPServerConfig,
|
||||
catalog: PluginManifest[] = [],
|
||||
) {
|
||||
|
||||
@ -20,7 +20,9 @@ import {
|
||||
installationQuery,
|
||||
useCapabilityCatalog,
|
||||
} from "@/core/capabilities/hooks";
|
||||
import type { CapabilityInstallation } from "@/core/capabilities/types";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import type { Translations } from "@/core/i18n/locales/types";
|
||||
import { isStaticWebsiteOnly } from "@/core/static-mode";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@ -38,6 +40,37 @@ import {
|
||||
} from "./plugin-directory";
|
||||
import { PluginIcon } from "./plugin-icon";
|
||||
|
||||
function getPluginStatusLabel(
|
||||
adapter: string,
|
||||
status: CapabilityInstallation | undefined,
|
||||
unavailable: boolean | undefined,
|
||||
t: Translations,
|
||||
labels: ReturnType<typeof capabilityCopy>,
|
||||
) {
|
||||
if (unavailable) return labels.adapterError;
|
||||
if (status) {
|
||||
if (status.auth_status === "connected") return labels.connected;
|
||||
if (status.auth_status === "required") return labels.required;
|
||||
if (status.auth_status === "configured") return labels.configured;
|
||||
return labels.installed;
|
||||
}
|
||||
if (adapter === "guide") return t.capabilities.directory.candidate;
|
||||
if (adapter === "lark") return t.capabilities.notInstalled;
|
||||
return labels.notConfigured;
|
||||
}
|
||||
|
||||
function getPluginActionLabel(
|
||||
adapter: string,
|
||||
installed: boolean,
|
||||
canManage: boolean,
|
||||
t: Translations,
|
||||
) {
|
||||
if (installed) return t.capabilities.manage;
|
||||
if (adapter === "guide" || !canManage) return t.capabilities.directory.view;
|
||||
if (adapter === "lark") return t.common.install;
|
||||
return t.capabilities.configure;
|
||||
}
|
||||
|
||||
export function PluginGallery({ query }: { query: string }) {
|
||||
const { t, locale } = useI18n();
|
||||
const copy = t.capabilities.directory;
|
||||
@ -109,23 +142,13 @@ export function PluginGallery({ query }: { query: string }) {
|
||||
capabilityId={plugin.id}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
unavailable
|
||||
? labels.adapterError
|
||||
: status
|
||||
? status.auth_status === "connected"
|
||||
? labels.connected
|
||||
: status.auth_status === "required"
|
||||
? labels.required
|
||||
: status.auth_status === "configured"
|
||||
? labels.configured
|
||||
: labels.installed
|
||||
: plugin.adapter === "guide"
|
||||
? copy.candidate
|
||||
: plugin.adapter === "lark"
|
||||
? t.capabilities.notInstalled
|
||||
: labels.notConfigured
|
||||
}
|
||||
label={getPluginStatusLabel(
|
||||
plugin.adapter,
|
||||
status,
|
||||
unavailable,
|
||||
t,
|
||||
labels,
|
||||
)}
|
||||
onDetails={() => setSelectedId(plugin.id)}
|
||||
detailsLabel={`${t.capabilities.details} ${catalogText(plugin.name, locale)}`}
|
||||
>
|
||||
@ -136,15 +159,7 @@ export function PluginGallery({ query }: { query: string }) {
|
||||
aria-label={`${plugin.adapter === "guide" ? copy.guide : t.capabilities.configure} ${catalogText(plugin.name, locale)}`}
|
||||
onClick={() => setSelectedId(plugin.id)}
|
||||
>
|
||||
{status
|
||||
? t.capabilities.manage
|
||||
: plugin.adapter === "guide"
|
||||
? copy.view
|
||||
: canManage
|
||||
? plugin.adapter === "lark"
|
||||
? t.common.install
|
||||
: t.capabilities.configure
|
||||
: copy.view}
|
||||
{getPluginActionLabel(plugin.adapter, !!status, canManage, t)}
|
||||
</Button>
|
||||
</PluginRow>
|
||||
),
|
||||
|
||||
@ -30,11 +30,7 @@ export function useCapabilityCatalog() {
|
||||
});
|
||||
}
|
||||
export function useCapabilityInstallations(adapter: string) {
|
||||
return useQuery({
|
||||
queryKey: ["capabilities", "installations", adapter],
|
||||
queryFn: () =>
|
||||
request<InstallationList>(`installations/${encodeURIComponent(adapter)}`),
|
||||
});
|
||||
return useQuery(installationQuery(adapter));
|
||||
}
|
||||
export function useInstallCapability() {
|
||||
const client = useQueryClient();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user