mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
feat(ui): add OpenAPI MCP server support with popular API quick-picker
- New `openapi_registry.json` with 10 well-known APIs (GitHub, Atlassian, Figma, Google, Stripe, HubSpot, Notion, Slack, Shopify, Snowflake) — each with validated spec URLs and OAuth 2.0 endpoints - Backend endpoint `GET /v1/mcp/registry.json` to serve the registry (reads fresh from disk) - `OpenAPIQuickPicker` component: logo grid for popular APIs with letter fallback for broken images - `OpenAPIFormSection` component: encapsulates picker + spec URL input as a clean unit - When selecting a preset, spec URL and OAuth fields are pre-filled automatically - Fixed `useTestMCPConnection`: for OpenAPI transport, tools load from the spec as soon as the URL is set — no auth type or OAuth token required - Validated all spec URLs are reachable; removed Linear (GraphQL-only, no REST spec)
This commit is contained in:
parent
90b6cc2a4c
commit
2b2e0de2af
8 changed files with 472 additions and 27 deletions
|
|
@ -11,6 +11,7 @@ Endpoints here:
|
|||
- GET `/v1/mcp/tools - lists all the tools available for a key
|
||||
- GET `/v1/mcp/access_groups` - lists all available MCP access groups
|
||||
- GET `/v1/mcp/discover` - Returns curated list of well-known MCP servers for discovery UI
|
||||
- GET `/v1/mcp/openapi-registry` - Returns well-known OpenAPI APIs with OAuth 2.0 metadata
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -1361,3 +1362,40 @@ if MCP_AVAILABLE:
|
|||
"servers": servers,
|
||||
"categories": categories,
|
||||
}
|
||||
|
||||
# --- OpenAPI Registry ---
|
||||
|
||||
_OPENAPI_REGISTRY_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"openapi_registry.json",
|
||||
)
|
||||
|
||||
def _load_openapi_registry() -> Dict[str, Any]:
|
||||
try:
|
||||
with open(_OPENAPI_REGISTRY_PATH, "r") as f:
|
||||
data: Dict[str, Any] = json.load(f)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(
|
||||
f"Failed to load OpenAPI registry from {_OPENAPI_REGISTRY_PATH}: {e}"
|
||||
)
|
||||
data = {"apis": []}
|
||||
return data
|
||||
|
||||
@router.get(
|
||||
"/openapi-registry",
|
||||
description="Returns well-known OpenAPI APIs with OAuth 2.0 metadata for the OpenAPI MCP picker",
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_openapi_registry(
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only proxy admins can access the OpenAPI registry. Your role={}".format(
|
||||
user_api_key_dict.user_role
|
||||
)
|
||||
},
|
||||
)
|
||||
return _load_openapi_registry()
|
||||
|
|
|
|||
134
litellm/proxy/openapi_registry.json
Normal file
134
litellm/proxy/openapi_registry.json
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
{
|
||||
"apis": [
|
||||
{
|
||||
"name": "github",
|
||||
"title": "GitHub",
|
||||
"description": "Repos, issues, PRs, and workflow automation via the GitHub REST API",
|
||||
"icon_url": "https://cdn.simpleicons.org/github",
|
||||
"spec_url": "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json",
|
||||
"oauth": {
|
||||
"authorization_url": "https://github.com/login/oauth/authorize",
|
||||
"token_url": "https://github.com/login/oauth/access_token",
|
||||
"pkce": false,
|
||||
"docs_url": "https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "atlassian",
|
||||
"title": "Atlassian",
|
||||
"description": "Jira issues, Confluence pages, and project management",
|
||||
"icon_url": "https://cdn.simpleicons.org/atlassian",
|
||||
"spec_url": "https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json",
|
||||
"oauth": {
|
||||
"authorization_url": "https://auth.atlassian.com/authorize",
|
||||
"token_url": "https://auth.atlassian.com/oauth/token",
|
||||
"pkce": true,
|
||||
"docs_url": "https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "figma",
|
||||
"title": "Figma",
|
||||
"description": "Design files, components, prototypes, and comments",
|
||||
"icon_url": "https://cdn.simpleicons.org/figma",
|
||||
"spec_url": "https://raw.githubusercontent.com/figma/rest-api-spec/main/openapi/openapi.yaml",
|
||||
"oauth": {
|
||||
"authorization_url": "https://www.figma.com/oauth",
|
||||
"token_url": "https://www.figma.com/api/oauth/token",
|
||||
"pkce": false,
|
||||
"docs_url": "https://www.figma.com/developers/api#oauth2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "google",
|
||||
"title": "Google APIs",
|
||||
"description": "Gmail, Calendar, Drive, and other Google services",
|
||||
"icon_url": "https://cdn.simpleicons.org/google",
|
||||
"spec_url": "https://www.googleapis.com/discovery/v1/apis",
|
||||
"oauth": {
|
||||
"authorization_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"token_url": "https://oauth2.googleapis.com/token",
|
||||
"pkce": true,
|
||||
"docs_url": "https://developers.google.com/identity/protocols/oauth2/web-server"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "stripe",
|
||||
"title": "Stripe",
|
||||
"description": "Payments, customers, subscriptions, and billing",
|
||||
"icon_url": "https://cdn.simpleicons.org/stripe",
|
||||
"spec_url": "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
|
||||
"oauth": {
|
||||
"authorization_url": "https://connect.stripe.com/oauth/authorize",
|
||||
"token_url": "https://connect.stripe.com/oauth/token",
|
||||
"pkce": false,
|
||||
"docs_url": "https://stripe.com/docs/connect/oauth-reference"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "hubspot",
|
||||
"title": "HubSpot",
|
||||
"description": "CRM contacts, deals, companies, and marketing automation",
|
||||
"icon_url": "https://cdn.simpleicons.org/hubspot",
|
||||
"spec_url": "https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json",
|
||||
"oauth": {
|
||||
"authorization_url": "https://app.hubspot.com/oauth/authorize",
|
||||
"token_url": "https://api.hubspot.com/oauth/v1/token",
|
||||
"pkce": false,
|
||||
"docs_url": "https://developers.hubspot.com/docs/api/oauth-quickstart-guide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "notion",
|
||||
"title": "Notion",
|
||||
"description": "Pages, databases, and workspace content in Notion",
|
||||
"icon_url": "https://cdn.simpleicons.org/notion",
|
||||
"spec_url": "https://raw.githubusercontent.com/TakashiSasaki/notion-openapi/main/Page/notion_api_page_management_v1.yaml",
|
||||
"oauth": {
|
||||
"authorization_url": "https://api.notion.com/v1/oauth/authorize",
|
||||
"token_url": "https://api.notion.com/v1/oauth/token",
|
||||
"pkce": false,
|
||||
"docs_url": "https://developers.notion.com/docs/authorization"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slack",
|
||||
"title": "Slack",
|
||||
"description": "Channels, messages, users, and workspace management",
|
||||
"icon_url": "https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/slack.svg",
|
||||
"spec_url": "https://raw.githubusercontent.com/slackapi/slack-api-specs/master/web-api/slack_web_openapi_v2.json",
|
||||
"oauth": {
|
||||
"authorization_url": "https://slack.com/oauth/v2/authorize",
|
||||
"token_url": "https://slack.com/api/oauth.v2.access",
|
||||
"pkce": false,
|
||||
"docs_url": "https://api.slack.com/authentication/oauth-v2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "shopify",
|
||||
"title": "Shopify",
|
||||
"description": "Products, orders, customers, and store management",
|
||||
"icon_url": "https://cdn.simpleicons.org/shopify",
|
||||
"spec_url": "https://raw.githubusercontent.com/allengrant/shopify_openapi/master/shopify_openapi.json",
|
||||
"oauth": {
|
||||
"authorization_url": "https://{shop}.myshopify.com/admin/oauth/authorize",
|
||||
"token_url": "https://{shop}.myshopify.com/admin/oauth/access_token",
|
||||
"pkce": false,
|
||||
"docs_url": "https://shopify.dev/docs/apps/auth/get-access-tokens/authorization-code-grant"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "snowflake",
|
||||
"title": "Snowflake",
|
||||
"description": "Data warehouse queries, database operations, and analytics via the Snowflake SQL API",
|
||||
"icon_url": "https://cdn.simpleicons.org/snowflake",
|
||||
"spec_url": "https://raw.githubusercontent.com/snowflakedb/snowflake-rest-api-specs/refs/heads/main/specifications/sqlapi.yaml",
|
||||
"oauth": {
|
||||
"authorization_url": "https://<account>.snowflakecomputing.com/oauth/authorize",
|
||||
"token_url": "https://<account>.snowflakecomputing.com/oauth/token-request",
|
||||
"pkce": false,
|
||||
"docs_url": "https://docs.snowflake.com/en/user-guide/oauth-custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import { fileURLToPath } from "url";
|
|||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const DEV_PROXY_TARGET = process.env.LITELLM_BACKEND_URL || "http://localhost:4000";
|
||||
|
||||
const nextConfig = {
|
||||
output: "export",
|
||||
basePath: "",
|
||||
|
|
@ -13,6 +15,67 @@ const nextConfig = {
|
|||
// Must be absolute; "." is no longer allowed
|
||||
root: __dirname,
|
||||
},
|
||||
// Dev-only: proxy API calls to the litellm backend to avoid CORS issues
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/v1/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/v1/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/v2/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/v2/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/key/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/key/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/team/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/team/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/user/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/user/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/model/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/model/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/health/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/health/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/callbacks/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/callbacks/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/config/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/config/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/mcp-rest/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/mcp-rest/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/sso/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/sso/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/.well-known/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/.well-known/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/litellm/.well-known/:path*",
|
||||
destination: `${DEV_PROXY_TARGET}/litellm/.well-known/:path*`,
|
||||
},
|
||||
{
|
||||
source: "/in_product_nudges",
|
||||
destination: `${DEV_PROXY_TARGET}/in_product_nudges`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import React, { useState } from "react";
|
||||
import { Form, Input, Tooltip } from "antd";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { FormInstance } from "antd/es/form";
|
||||
import { AUTH_TYPE } from "./types";
|
||||
import OpenAPIQuickPicker, { OpenAPIRegistryEntry } from "./OpenAPIQuickPicker";
|
||||
|
||||
interface OpenAPIFormSectionProps {
|
||||
form: FormInstance;
|
||||
accessToken: string | null;
|
||||
/** Called when a preset is selected so the parent can sync its formValues state. */
|
||||
onValuesChange: (updates: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates all OpenAPI-specific form fields:
|
||||
* - popular API quick-picker (logos)
|
||||
* - spec URL input
|
||||
*
|
||||
* When a preset is selected, it pre-fills the spec URL and OAuth 2.0 fields
|
||||
* directly on the Ant Design form instance, then notifies the parent so it
|
||||
* can keep its own formValues state in sync (since setFieldsValue bypasses
|
||||
* the Form onValuesChange callback).
|
||||
*/
|
||||
const OpenAPIFormSection: React.FC<OpenAPIFormSectionProps> = ({
|
||||
form,
|
||||
accessToken,
|
||||
onValuesChange,
|
||||
}) => {
|
||||
const [selectedPreset, setSelectedPreset] = useState<string | null>(null);
|
||||
|
||||
const handlePresetSelect = (entry: OpenAPIRegistryEntry) => {
|
||||
setSelectedPreset(entry.name);
|
||||
const updates = {
|
||||
spec_path: entry.spec_url,
|
||||
auth_type: AUTH_TYPE.OAUTH2,
|
||||
credentials: {
|
||||
authorization_url: entry.oauth.authorization_url,
|
||||
token_url: entry.oauth.token_url,
|
||||
},
|
||||
};
|
||||
form.setFieldsValue(updates);
|
||||
onValuesChange(updates);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<OpenAPIQuickPicker
|
||||
accessToken={accessToken}
|
||||
selectedName={selectedPreset}
|
||||
onSelect={handlePresetSelect}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenAPIFormSection;
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Spin } from "antd";
|
||||
import { fetchOpenAPIRegistry } from "../networking";
|
||||
|
||||
export interface OpenAPIRegistryEntry {
|
||||
name: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon_url: string;
|
||||
spec_url: string;
|
||||
oauth: {
|
||||
authorization_url: string;
|
||||
token_url: string;
|
||||
pkce: boolean;
|
||||
docs_url: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OpenAPIQuickPickerProps {
|
||||
accessToken: string | null;
|
||||
selectedName: string | null;
|
||||
onSelect: (entry: OpenAPIRegistryEntry) => void;
|
||||
}
|
||||
|
||||
const OpenAPIQuickPicker: React.FC<OpenAPIQuickPickerProps> = ({
|
||||
accessToken,
|
||||
selectedName,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [apis, setApis] = useState<OpenAPIRegistryEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [imgErrors, setImgErrors] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken) return;
|
||||
setLoading(true);
|
||||
fetchOpenAPIRegistry(accessToken)
|
||||
.then((data) => setApis(data.apis ?? []))
|
||||
.catch(() => setApis([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, [accessToken]);
|
||||
|
||||
const handleImgError = (name: string) => {
|
||||
setImgErrors((prev) => new Set(prev).add(name));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<span className="text-sm font-medium text-gray-700">Popular APIs</span>
|
||||
<div className="flex justify-center py-6">
|
||||
<Spin size="small" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (apis.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<span className="text-sm font-medium text-gray-700 block mb-2">Popular APIs</span>
|
||||
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{apis.map((api) => {
|
||||
const isSelected = selectedName === api.name;
|
||||
const imgFailed = imgErrors.has(api.name);
|
||||
return (
|
||||
<button
|
||||
key={api.name}
|
||||
type="button"
|
||||
title={api.description}
|
||||
onClick={() => onSelect(api)}
|
||||
className={`flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all cursor-pointer
|
||||
${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50 shadow-sm"
|
||||
: "border-gray-200 hover:border-blue-300 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
{imgFailed ? (
|
||||
<span className="w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center text-sm font-bold text-gray-600">
|
||||
{api.title.charAt(0)}
|
||||
</span>
|
||||
) : (
|
||||
<img
|
||||
src={api.icon_url}
|
||||
alt={api.title}
|
||||
className="w-7 h-7 object-contain"
|
||||
onError={() => handleImgError(api.name)}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-gray-600 text-center leading-tight font-medium">
|
||||
{api.title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
Select an API to pre-fill the spec URL and OAuth 2.0 settings, or enter
|
||||
your own spec URL below.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenAPIQuickPicker;
|
||||
|
|
@ -10,6 +10,7 @@ import MCPConnectionStatus from "./mcp_connection_status";
|
|||
import MCPToolConfiguration from "./mcp_tool_configuration";
|
||||
import StdioConfiguration from "./StdioConfiguration";
|
||||
import MCPPermissionManagement from "./MCPPermissionManagement";
|
||||
import OpenAPIFormSection from "./OpenAPIFormSection";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import { validateMCPServerUrl, validateMCPServerName } from "./utils";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
|
@ -603,25 +604,15 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* OpenAPI Spec URL - only show for OpenAPI transport */}
|
||||
{/* OpenAPI: logo picker + spec URL input */}
|
||||
{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>
|
||||
<OpenAPIFormSection
|
||||
form={form}
|
||||
accessToken={accessToken}
|
||||
onValuesChange={(updates) =>
|
||||
setFormValues((prev) => ({ ...prev, ...updates }))
|
||||
}
|
||||
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>
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* BYOK toggle - only for OpenAPI */}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,12 @@ import { jsonFields } from "./common_components/check_openapi_schema";
|
|||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
const defaultProxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||
// In dev, if NEXT_PUBLIC_USE_REWRITES=true the Next.js dev server proxies API calls
|
||||
// to the backend — use relative URLs (null) so rewrites can intercept them.
|
||||
const defaultProxyBaseUrl =
|
||||
isLocal && process.env.NEXT_PUBLIC_USE_REWRITES !== "true"
|
||||
? "http://localhost:4000"
|
||||
: null;
|
||||
const defaultServerRootPath = "/";
|
||||
export let serverRootPath = defaultServerRootPath;
|
||||
export let proxyBaseUrl = defaultProxyBaseUrl;
|
||||
|
|
@ -98,7 +103,10 @@ const updateProxyBaseUrl = (serverRootPath: string, receivedProxyBaseUrl: string
|
|||
* Special function for updating the proxy base url. Should only be called by getUiConfig.
|
||||
*/
|
||||
const browserLocation = getWindowLocation();
|
||||
const resolvedDefaultProxyBaseUrl = isLocal ? "http://localhost:4000" : browserLocation?.origin ?? null;
|
||||
const resolvedDefaultProxyBaseUrl =
|
||||
isLocal && process.env.NEXT_PUBLIC_USE_REWRITES !== "true"
|
||||
? "http://localhost:4000"
|
||||
: browserLocation?.origin ?? null;
|
||||
let initialProxyBaseUrl = receivedProxyBaseUrl || resolvedDefaultProxyBaseUrl;
|
||||
console.log("proxyBaseUrl:", proxyBaseUrl);
|
||||
console.log("serverRootPath:", serverRootPath);
|
||||
|
|
@ -6243,6 +6251,27 @@ export const updateInternalUserSettings = async (accessToken: string, settings:
|
|||
}
|
||||
};
|
||||
|
||||
export const fetchOpenAPIRegistry = async (accessToken: string) => {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/v1/mcp/openapi-registry`
|
||||
: `/v1/mcp/openapi-registry`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: HTTP_REQUEST.GET,
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(deriveErrorMessage(errorData));
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const fetchDiscoverableMCPServers = async (accessToken: string) => {
|
||||
try {
|
||||
const url = proxyBaseUrl
|
||||
|
|
|
|||
|
|
@ -58,13 +58,18 @@ export const useTestMCPConnection = ({
|
|||
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 = !!(
|
||||
hasEndpoint &&
|
||||
formValues.transport &&
|
||||
formValues.auth_type &&
|
||||
accessToken &&
|
||||
(!requiresOAuthToken || oauthAccessToken)
|
||||
);
|
||||
|
||||
// For OpenAPI: tools are derived from the spec itself — no auth needed to load them.
|
||||
// For other transports: auth_type is required, and OAuth interactive flows need a token.
|
||||
const canFetchTools = isOpenAPITransport
|
||||
? !!(hasEndpoint && accessToken)
|
||||
: !!(
|
||||
hasEndpoint &&
|
||||
formValues.transport &&
|
||||
formValues.auth_type &&
|
||||
accessToken &&
|
||||
(!requiresOAuthToken || oauthAccessToken)
|
||||
);
|
||||
|
||||
const staticHeadersKey = JSON.stringify(formValues.static_headers ?? {});
|
||||
const credentialsKey = JSON.stringify(formValues.credentials ?? {});
|
||||
|
|
@ -74,7 +79,8 @@ export const useTestMCPConnection = ({
|
|||
return;
|
||||
}
|
||||
|
||||
if (requiresOAuthToken && !oauthAccessToken) {
|
||||
// For OpenAPI transport, tools are derived from the spec — no OAuth token needed.
|
||||
if (requiresOAuthToken && !oauthAccessToken && !isOpenAPITransport) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue