mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
Add OpenAPI-to-MCP support via API and UI (#21575)
* add spec_path column to LiteLLM_MCPServerTable schema * add spec_path to MCP request types and table model * wire spec_path through build_mcp_server_from_table * add openapi transport type constant * add OpenAPI Spec as first-class transport option in create form * add OpenAPI transport support to edit form with auto-detection * support spec_path in connection status component * support spec_path in tool configuration component * support OpenAPI transport in test connection hook * register OpenAPI tools on server add/update/reload * preview OpenAPI tools in test/tools/list endpoint
This commit is contained in:
parent
e9a07347dc
commit
cd95e54c10
10 changed files with 186 additions and 32 deletions
|
|
@ -71,7 +71,9 @@ try:
|
|||
from mcp.shared.tool_name_validation import (
|
||||
validate_tool_name, # pyright: ignore[reportAssignmentType]
|
||||
)
|
||||
from mcp.shared.tool_name_validation import SEP_986_URL
|
||||
from mcp.shared.tool_name_validation import (
|
||||
SEP_986_URL,
|
||||
)
|
||||
except ImportError:
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -608,6 +610,7 @@ class MCPServerManager:
|
|||
alias=getattr(mcp_server, "alias", None),
|
||||
server_name=getattr(mcp_server, "server_name", None),
|
||||
url=mcp_server.url,
|
||||
spec_path=getattr(mcp_server, "spec_path", None),
|
||||
transport=cast(MCPTransportType, mcp_server.transport),
|
||||
auth_type=auth_type,
|
||||
authentication_token=auth_value,
|
||||
|
|
@ -638,11 +641,25 @@ class MCPServerManager:
|
|||
)
|
||||
return new_server
|
||||
|
||||
async def _maybe_register_openapi_tools(self, server: MCPServer):
|
||||
"""Register OpenAPI tools if the server has a spec_path configured."""
|
||||
if server.spec_path:
|
||||
verbose_logger.info(
|
||||
f"Loading OpenAPI spec from {server.spec_path} for server {server.name}"
|
||||
)
|
||||
await self._register_openapi_tools(
|
||||
spec_path=server.spec_path,
|
||||
server=server,
|
||||
base_url=server.url or "",
|
||||
)
|
||||
self.initialize_tool_name_to_mcp_server_name_mapping()
|
||||
|
||||
async def add_server(self, mcp_server: LiteLLM_MCPServerTable):
|
||||
try:
|
||||
if mcp_server.server_id not in self.registry:
|
||||
new_server = await self.build_mcp_server_from_table(mcp_server)
|
||||
self.registry[mcp_server.server_id] = new_server
|
||||
await self._maybe_register_openapi_tools(new_server)
|
||||
verbose_logger.debug(f"Added MCP Server: {new_server.name}")
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -654,6 +671,7 @@ class MCPServerManager:
|
|||
if mcp_server.server_id in self.registry:
|
||||
new_server = await self.build_mcp_server_from_table(mcp_server)
|
||||
self.registry[mcp_server.server_id] = new_server
|
||||
await self._maybe_register_openapi_tools(new_server)
|
||||
verbose_logger.debug(f"Updated MCP Server: {new_server.name}")
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -2242,9 +2260,9 @@ class MCPServerManager:
|
|||
verbose_logger.debug(
|
||||
f"Building server from DB: {server.server_id} ({server.server_name})"
|
||||
)
|
||||
new_registry[server.server_id] = await self.build_mcp_server_from_table(
|
||||
server
|
||||
)
|
||||
new_server = await self.build_mcp_server_from_table(server)
|
||||
new_registry[server.server_id] = new_server
|
||||
await self._maybe_register_openapi_tools(new_server)
|
||||
|
||||
self.registry = new_registry
|
||||
|
||||
|
|
|
|||
|
|
@ -625,6 +625,46 @@ if MCP_AVAILABLE:
|
|||
"message": "Failed to connect to MCP server. Check proxy logs for details.",
|
||||
}
|
||||
|
||||
async def _preview_openapi_tools(spec_path: str) -> dict:
|
||||
"""Generate tool previews from an OpenAPI spec without creating a server."""
|
||||
from litellm.proxy._experimental.mcp_server.openapi_to_mcp_generator import (
|
||||
build_input_schema,
|
||||
load_openapi_spec_async,
|
||||
)
|
||||
|
||||
try:
|
||||
spec = await load_openapi_spec_async(spec_path)
|
||||
paths = spec.get("paths", {})
|
||||
tools: List[dict] = []
|
||||
for path, path_item in paths.items():
|
||||
for method in ("get", "post", "put", "patch", "delete"):
|
||||
operation = path_item.get(method)
|
||||
if operation is None:
|
||||
continue
|
||||
op_id = operation.get("operationId", f"{method}_{path}")
|
||||
summary = operation.get("summary", "")
|
||||
description = operation.get("description", summary)
|
||||
input_schema = build_input_schema(operation)
|
||||
tools.append(
|
||||
{
|
||||
"name": op_id,
|
||||
"description": description or summary or f"{method.upper()} {path}",
|
||||
"inputSchema": input_schema,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"tools": tools,
|
||||
"error": None,
|
||||
"message": f"Found {len(tools)} tools from OpenAPI spec",
|
||||
}
|
||||
except Exception as e:
|
||||
verbose_logger.error("Error previewing OpenAPI tools: %s", e, exc_info=True)
|
||||
return {
|
||||
"tools": [],
|
||||
"error": True,
|
||||
"message": f"Failed to load OpenAPI spec: {e}",
|
||||
}
|
||||
|
||||
@router.post("/test/connection", dependencies=[Depends(user_api_key_auth)])
|
||||
async def test_connection(
|
||||
request: Request,
|
||||
|
|
@ -657,6 +697,10 @@ if MCP_AVAILABLE:
|
|||
"""
|
||||
Preview tools available from MCP server before adding it
|
||||
"""
|
||||
# For OpenAPI spec servers, generate tools from the spec directly
|
||||
if new_mcp_server_request.spec_path:
|
||||
return await _preview_openapi_tools(new_mcp_server_request.spec_path)
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import (
|
||||
MCPRequestHandler,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1077,6 +1077,7 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase):
|
|||
auth_type: Optional[MCPAuthType] = None
|
||||
credentials: Optional[MCPCredentials] = None
|
||||
url: Optional[str] = None
|
||||
spec_path: Optional[str] = None
|
||||
mcp_info: Optional[MCPInfo] = None
|
||||
mcp_access_groups: List[str] = Field(default_factory=list)
|
||||
allowed_tools: Optional[List[str]] = None
|
||||
|
|
@ -1103,8 +1104,8 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase):
|
|||
if not values.get("args"):
|
||||
raise ValueError("args is required for stdio transport")
|
||||
elif transport in [MCPTransport.http, MCPTransport.sse]:
|
||||
if not values.get("url"):
|
||||
raise ValueError("url is required for HTTP/SSE transport")
|
||||
if not values.get("url") and not values.get("spec_path"):
|
||||
raise ValueError("url or spec_path is required for HTTP/SSE transport")
|
||||
return values
|
||||
|
||||
@model_validator(mode="before")
|
||||
|
|
@ -1139,6 +1140,7 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase):
|
|||
auth_type: Optional[MCPAuthType] = None
|
||||
credentials: Optional[MCPCredentials] = None
|
||||
url: Optional[str] = None
|
||||
spec_path: Optional[str] = None
|
||||
mcp_info: Optional[MCPInfo] = None
|
||||
mcp_access_groups: List[str] = Field(default_factory=list)
|
||||
allowed_tools: Optional[List[str]] = None
|
||||
|
|
@ -1165,8 +1167,8 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase):
|
|||
if not values.get("args"):
|
||||
raise ValueError("args is required for stdio transport")
|
||||
elif transport in [MCPTransport.http, MCPTransport.sse]:
|
||||
if not values.get("url"):
|
||||
raise ValueError("url is required for HTTP/SSE transport")
|
||||
if not values.get("url") and not values.get("spec_path"):
|
||||
raise ValueError("url or spec_path is required for HTTP/SSE transport")
|
||||
return values
|
||||
|
||||
|
||||
|
|
@ -1178,6 +1180,7 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase):
|
|||
alias: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
spec_path: Optional[str] = None
|
||||
transport: MCPTransportType
|
||||
auth_type: Optional[MCPAuthType] = None
|
||||
credentials: Optional[MCPCredentials] = None
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ model LiteLLM_MCPServerTable {
|
|||
alias String?
|
||||
description String?
|
||||
url String?
|
||||
spec_path String?
|
||||
transport String @default("sse")
|
||||
auth_type String?
|
||||
credentials Json? @default("{}")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Modal, Tooltip, Form, Select, Input } from "antd";
|
|||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, TextInput } from "@tremor/react";
|
||||
import { createMCPServer } from "../networking";
|
||||
import { AUTH_TYPE, DiscoverableMCPServer, OAUTH_FLOW, MCPServer, MCPServerCostInfo } from "./types";
|
||||
import { AUTH_TYPE, DiscoverableMCPServer, OAUTH_FLOW, MCPServer, MCPServerCostInfo, TRANSPORT } from "./types";
|
||||
import OAuthFormFields from "./OAuthFormFields";
|
||||
import MCPServerCostConfig from "./mcp_server_cost_config";
|
||||
import MCPConnectionStatus from "./mcp_connection_status";
|
||||
|
|
@ -316,6 +316,11 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
// Map "openapi" transport to "http" for the backend
|
||||
if (restValues.transport === TRANSPORT.OPENAPI) {
|
||||
restValues.transport = "http";
|
||||
}
|
||||
|
||||
// Prepare the payload with cost configuration and allowed tools
|
||||
const payload: Record<string, any> = {
|
||||
...restValues,
|
||||
|
|
@ -377,9 +382,11 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
setTransportType(value);
|
||||
// Clear fields that are not relevant for the selected transport
|
||||
if (value === "stdio") {
|
||||
form.setFieldsValue({ url: undefined, auth_type: undefined, credentials: undefined });
|
||||
form.setFieldsValue({ url: undefined, spec_path: undefined, auth_type: undefined, credentials: undefined });
|
||||
} else if (value === TRANSPORT.OPENAPI) {
|
||||
form.setFieldsValue({ url: undefined, command: undefined, args: undefined, env: undefined });
|
||||
} else {
|
||||
form.setFieldsValue({ command: undefined, args: undefined, env: undefined });
|
||||
form.setFieldsValue({ spec_path: undefined, command: undefined, args: undefined, env: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -555,11 +562,12 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
<Select.Option value="http">Streamable HTTP (Recommended)</Select.Option>
|
||||
<Select.Option value="sse">Server-Sent Events (SSE)</Select.Option>
|
||||
<Select.Option value="stdio">Standard Input/Output (stdio)</Select.Option>
|
||||
<Select.Option value={TRANSPORT.OPENAPI}>OpenAPI Spec</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* URL field - only show for HTTP and SSE */}
|
||||
{transportType !== "stdio" && (
|
||||
{(transportType === "http" || transportType === "sse") && (
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">MCP Server URL</span>}
|
||||
name="url"
|
||||
|
|
@ -575,8 +583,29 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* Authentication - only show for HTTP and SSE */}
|
||||
{transportType !== "stdio" && (
|
||||
{/* OpenAPI Spec URL - only show for OpenAPI transport */}
|
||||
{transportType === TRANSPORT.OPENAPI && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
OpenAPI Spec URL
|
||||
<Tooltip title="URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="spec_path"
|
||||
rules={[{ required: true, message: "Please enter an OpenAPI spec URL" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="https://petstore3.swagger.io/api/v3/openapi.json"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* Authentication - show for HTTP, SSE, and OpenAPI */}
|
||||
{transportType !== "stdio" && transportType !== "" && (
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">Authentication</span>}
|
||||
name="auth_type"
|
||||
|
|
@ -592,7 +621,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
</Form.Item>
|
||||
)}
|
||||
|
||||
{transportType !== "stdio" && shouldShowAuthValueField && (
|
||||
{transportType !== "stdio" && transportType !== "" && shouldShowAuthValueField && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
|
|
@ -613,7 +642,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
</Form.Item>
|
||||
)}
|
||||
|
||||
{transportType !== "stdio" && isOAuthAuthType && (
|
||||
{transportType !== "stdio" && transportType !== "" && isOAuthAuthType && (
|
||||
<OAuthFormFields
|
||||
isM2M={isM2MFlow}
|
||||
initialFlowType={OAUTH_FLOW.INTERACTIVE}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const MCPConnectionStatus: React.FC<MCPConnectionStatusProps> = ({ accessToken,
|
|||
}, [tools, onToolsLoaded]);
|
||||
|
||||
// Don't show anything if required fields aren't filled
|
||||
if (!canFetchTools && !formValues.url) {
|
||||
if (!canFetchTools && !formValues.url && !formValues.spec_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ const MCPConnectionStatus: React.FC<MCPConnectionStatusProps> = ({ accessToken,
|
|||
<Title>Connection Status</Title>
|
||||
</div>
|
||||
|
||||
{!canFetchTools && formValues.url && (
|
||||
{!canFetchTools && (formValues.url || formValues.spec_path) && (
|
||||
<div className="text-center py-6 text-gray-400 border rounded-lg border-dashed">
|
||||
<ToolOutlined className="text-2xl mb-2" />
|
||||
<Text>Complete required fields to test connection</Text>
|
||||
|
|
@ -60,7 +60,7 @@ const MCPConnectionStatus: React.FC<MCPConnectionStatusProps> = ({ accessToken,
|
|||
: "Ready to test connection"}
|
||||
</Text>
|
||||
<br />
|
||||
<Text className="text-gray-500 text-sm">Server: {formValues.url}</Text>
|
||||
<Text className="text-gray-500 text-sm">Server: {formValues.url || formValues.spec_path}</Text>
|
||||
</div>
|
||||
|
||||
{isLoadingTools && (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
|
|||
import { Form, Select, Button as AntdButton, Tooltip, Input } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react";
|
||||
import { AUTH_TYPE, OAUTH_FLOW, MCPServer, MCPServerCostInfo } from "./types";
|
||||
import { AUTH_TYPE, OAUTH_FLOW, MCPServer, MCPServerCostInfo, TRANSPORT } from "./types";
|
||||
import { updateMCPServer, testMCPToolsListRequest } from "../networking";
|
||||
import MCPServerCostConfig from "./mcp_server_cost_config";
|
||||
import MCPPermissionManagement from "./MCPPermissionManagement";
|
||||
|
|
@ -42,6 +42,8 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
const authType = Form.useWatch("auth_type", form) as string | undefined;
|
||||
const transportType = Form.useWatch("transport", form) as string | undefined;
|
||||
const isStdioTransport = transportType === "stdio";
|
||||
const isOpenAPITransport = transportType === TRANSPORT.OPENAPI;
|
||||
const isMCPTransport = !isStdioTransport && !isOpenAPITransport;
|
||||
const shouldShowAuthValueField = authType ? AUTH_TYPES_REQUIRING_AUTH_VALUE.includes(authType) : false;
|
||||
const isOAuthAuthType = authType === AUTH_TYPE.OAUTH2;
|
||||
const oauthFlowTypeValue = Form.useWatch("oauth_flow_type", form) as string | undefined;
|
||||
|
|
@ -142,13 +144,22 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
}, [mcpServer.env]);
|
||||
|
||||
|
||||
// If server has spec_path and no url, show it as "openapi" transport in the UI
|
||||
const effectiveTransport = React.useMemo(() => {
|
||||
if (mcpServer.spec_path && !mcpServer.url && mcpServer.transport !== "stdio") {
|
||||
return TRANSPORT.OPENAPI;
|
||||
}
|
||||
return mcpServer.transport;
|
||||
}, [mcpServer]);
|
||||
|
||||
const initialValues = React.useMemo(
|
||||
() => ({
|
||||
...mcpServer,
|
||||
transport: effectiveTransport,
|
||||
static_headers: initialStaticHeaders,
|
||||
oauth_flow_type: mcpServer.token_url ? OAUTH_FLOW.M2M : OAUTH_FLOW.INTERACTIVE,
|
||||
}),
|
||||
[mcpServer, initialStaticHeaders, initialEnvJson],
|
||||
[mcpServer, effectiveTransport, initialStaticHeaders, initialEnvJson],
|
||||
);
|
||||
|
||||
// Initialize cost config from existing server data
|
||||
|
|
@ -231,8 +242,8 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
const fetchTools = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
// HTTP/SSE requires a URL; stdio does not.
|
||||
if (mcpServer.transport !== "stdio" && !mcpServer.url) return;
|
||||
// HTTP/SSE requires a URL (unless spec_path is set); stdio does not.
|
||||
if (mcpServer.transport !== "stdio" && !mcpServer.url && !mcpServer.spec_path) return;
|
||||
|
||||
const isM2M = mcpServer.auth_type === AUTH_TYPE.OAUTH2 && !!mcpServer.token_url;
|
||||
if (mcpServer.auth_type === AUTH_TYPE.OAUTH2 && !isM2M && !oauthAccessToken) {
|
||||
|
|
@ -311,14 +322,24 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
if (value === "stdio") {
|
||||
form.setFieldsValue({
|
||||
url: undefined,
|
||||
spec_path: undefined,
|
||||
auth_type: undefined,
|
||||
credentials: undefined,
|
||||
authorization_url: undefined,
|
||||
token_url: undefined,
|
||||
registration_url: undefined,
|
||||
});
|
||||
} else if (value === TRANSPORT.OPENAPI) {
|
||||
form.setFieldsValue({
|
||||
url: undefined,
|
||||
command: undefined,
|
||||
args: undefined,
|
||||
env_json: undefined,
|
||||
stdio_config: undefined,
|
||||
});
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
spec_path: undefined,
|
||||
command: undefined,
|
||||
args: undefined,
|
||||
env_json: undefined,
|
||||
|
|
@ -457,6 +478,11 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
// Map "openapi" transport to "http" for the backend
|
||||
if (restValues.transport === TRANSPORT.OPENAPI) {
|
||||
restValues.transport = "http";
|
||||
}
|
||||
|
||||
// Prepare the payload with cost configuration and permission fields
|
||||
const mcpInfoServerName =
|
||||
restValues.server_name ||
|
||||
|
|
@ -543,14 +569,15 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
</Form.Item>
|
||||
<Form.Item label="Transport Type" name="transport" rules={[{ required: true }]}>
|
||||
<Select onChange={handleTransportChange}>
|
||||
<Select.Option value="sse">Server-Sent Events (SSE)</Select.Option>
|
||||
<Select.Option value="http">Streamable HTTP (Recommended)</Select.Option>
|
||||
<Select.Option value="sse">Server-Sent Events (SSE)</Select.Option>
|
||||
<Select.Option value="stdio">Standard Input/Output (stdio)</Select.Option>
|
||||
<Select.Option value={TRANSPORT.OPENAPI}>OpenAPI Spec</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* URL/Auth fields are only applicable for HTTP/SSE */}
|
||||
{!isStdioTransport && (
|
||||
{/* URL field - only for HTTP/SSE */}
|
||||
{isMCPTransport && (
|
||||
<Form.Item
|
||||
label="MCP Server URL"
|
||||
name="url"
|
||||
|
|
@ -566,6 +593,28 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* OpenAPI Spec URL - only for OpenAPI transport */}
|
||||
{isOpenAPITransport && (
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-sm font-medium text-gray-700 flex items-center">
|
||||
OpenAPI Spec URL
|
||||
<Tooltip title="URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.">
|
||||
<InfoCircleOutlined className="ml-2 text-blue-400 hover:text-blue-600 cursor-help" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="spec_path"
|
||||
rules={[{ required: true, message: "Please enter an OpenAPI spec URL" }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="https://petstore3.swagger.io/api/v3/openapi.json"
|
||||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* Authentication - for HTTP, SSE, and OpenAPI */}
|
||||
{!isStdioTransport && (
|
||||
<Form.Item label="Authentication" name="auth_type" rules={[{ required: true }]}>
|
||||
<Select>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ const MCPToolConfiguration: React.FC<MCPToolConfigurationProps> = ({
|
|||
};
|
||||
|
||||
// Don't show anything if required fields aren't filled
|
||||
if (!canFetchTools && !formValues.url) {
|
||||
if (!canFetchTools && !formValues.url && !formValues.spec_path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ const MCPToolConfiguration: React.FC<MCPToolConfigurationProps> = ({
|
|||
)}
|
||||
|
||||
{/* Incomplete form state */}
|
||||
{!canFetchTools && formValues.url && (
|
||||
{!canFetchTools && (formValues.url || formValues.spec_path) && (
|
||||
<div className="text-center py-6 text-gray-400 border rounded-lg border-dashed">
|
||||
<ToolOutlined className="text-2xl mb-2" />
|
||||
<Text>Complete required fields to configure tools</Text>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export const TRANSPORT = {
|
|||
SSE: "sse",
|
||||
HTTP: "http",
|
||||
STDIO: "stdio",
|
||||
OPENAPI: "openapi",
|
||||
};
|
||||
|
||||
export const handleTransport = (transport?: string | null): string => {
|
||||
|
|
@ -143,6 +144,7 @@ export interface MCPServer {
|
|||
* For `stdio`, the backend can return null/undefined.
|
||||
*/
|
||||
url?: string | null;
|
||||
spec_path?: string | null;
|
||||
transport?: string | null;
|
||||
auth_type?: string | null;
|
||||
authorization_url?: string | null;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { testMCPToolsListRequest } from "../components/networking";
|
||||
import { AUTH_TYPE, OAUTH_FLOW } from "@/components/mcp_tools/types";
|
||||
import { AUTH_TYPE, OAUTH_FLOW, TRANSPORT } from "@/components/mcp_tools/types";
|
||||
|
||||
interface MCPServerConfig {
|
||||
server_id?: string;
|
||||
server_name?: string;
|
||||
url?: string;
|
||||
spec_path?: string;
|
||||
transport?: string;
|
||||
auth_type?: string;
|
||||
authorization_url?: string;
|
||||
|
|
@ -55,8 +56,10 @@ export const useTestMCPConnection = ({
|
|||
const isM2MOAuth = formValues.auth_type === AUTH_TYPE.OAUTH2
|
||||
&& formValues.oauth_flow_type === OAUTH_FLOW.M2M;
|
||||
const requiresOAuthToken = formValues.auth_type === AUTH_TYPE.OAUTH2 && !isM2MOAuth;
|
||||
const isOpenAPITransport = formValues.transport === TRANSPORT.OPENAPI;
|
||||
const hasEndpoint = isOpenAPITransport ? !!formValues.spec_path : !!formValues.url;
|
||||
const canFetchTools = !!(
|
||||
formValues.url &&
|
||||
hasEndpoint &&
|
||||
formValues.transport &&
|
||||
formValues.auth_type &&
|
||||
accessToken &&
|
||||
|
|
@ -67,7 +70,7 @@ export const useTestMCPConnection = ({
|
|||
const credentialsKey = JSON.stringify(formValues.credentials ?? {});
|
||||
|
||||
const fetchTools = async () => {
|
||||
if (!accessToken || !formValues.url) {
|
||||
if (!accessToken || (!formValues.url && !formValues.spec_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +128,15 @@ export const useTestMCPConnection = ({
|
|||
)
|
||||
: undefined;
|
||||
|
||||
// For OpenAPI transport, map to "http" for backend compatibility
|
||||
const effectiveTransport = formValues.transport === TRANSPORT.OPENAPI ? "http" : formValues.transport;
|
||||
|
||||
const mcpServerConfig: MCPServerConfig = {
|
||||
server_id: formValues.server_id || "",
|
||||
server_name: formValues.server_name || "",
|
||||
url: formValues.url,
|
||||
transport: formValues.transport,
|
||||
spec_path: formValues.spec_path,
|
||||
transport: effectiveTransport,
|
||||
auth_type: formValues.auth_type,
|
||||
authorization_url: formValues.authorization_url,
|
||||
token_url: formValues.token_url,
|
||||
|
|
@ -190,6 +197,7 @@ export const useTestMCPConnection = ({
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
formValues.url,
|
||||
formValues.spec_path,
|
||||
formValues.transport,
|
||||
formValues.auth_type,
|
||||
accessToken,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue