feat/enh: external tool server manual JSON spec

This commit is contained in:
Timothy Jaeryang Baek 2025-09-26 22:02:48 -05:00
parent a05dab6298
commit bad7d69a58
2 changed files with 141 additions and 51 deletions

View file

@ -588,28 +588,20 @@ async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
error = str(err) error = str(err)
raise Exception(error) raise Exception(error)
data = { log.debug(f"Fetched data: {res}")
"openapi": res, return res
"info": res.get("info", {}),
"specs": convert_openapi_to_tool_payload(res),
}
log.info(f"Fetched data: {data}")
return data
async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]: async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
# Prepare list of enabled servers along with their original index # Prepare list of enabled servers along with their original index
tasks = []
server_entries = [] server_entries = []
for idx, server in enumerate(servers): for idx, server in enumerate(servers):
if ( if (
server.get("config", {}).get("enable") server.get("config", {}).get("enable")
and server.get("type", "openapi") == "openapi" and server.get("type", "openapi") == "openapi"
): ):
# Path (to OpenAPI spec URL) can be either a full URL or a path to append to the base URL
openapi_path = server.get("path", "openapi.json")
full_url = get_tool_server_url(server.get("url"), openapi_path)
info = server.get("info", {}) info = server.get("info", {})
auth_type = server.get("auth_type", "bearer") auth_type = server.get("auth_type", "bearer")
@ -625,12 +617,34 @@ async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str,
if not id: if not id:
id = str(idx) id = str(idx)
server_entries.append((id, idx, server, full_url, info, token)) server_url = server.get("url")
spec_type = server.get("spec_type", "url")
# Create async tasks to fetch data # Create async tasks to fetch data
tasks = [ task = None
get_tool_server_data(token, url) for (_, _, _, url, _, token) in server_entries if spec_type == "url":
] # Path (to OpenAPI spec URL) can be either a full URL or a path to append to the base URL
openapi_path = server.get("path", "openapi.json")
spec_url = get_tool_server_url(server_url, openapi_path)
# Fetch from URL
task = get_tool_server_data(token, spec_url)
elif spec_type == "json" and server.get("spec", ""):
# Use provided JSON spec
spec_json = None
try:
spec_json = json.loads(server.get("spec", ""))
except Exception as e:
log.error(f"Error parsing JSON spec for tool server {id}: {e}")
if spec_json:
task = asyncio.sleep(
0,
result=spec_json,
)
if task:
tasks.append(task)
server_entries.append((id, idx, server, server_url, info, token))
# Execute tasks concurrently # Execute tasks concurrently
responses = await asyncio.gather(*tasks, return_exceptions=True) responses = await asyncio.gather(*tasks, return_exceptions=True)
@ -642,8 +656,13 @@ async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str,
log.error(f"Failed to connect to {url} OpenAPI tool server") log.error(f"Failed to connect to {url} OpenAPI tool server")
continue continue
openapi_data = response.get("openapi", {}) response = {
"openapi": response,
"info": response.get("info", {}),
"specs": convert_openapi_to_tool_payload(response),
}
openapi_data = response.get("openapi", {})
if info and isinstance(openapi_data, dict): if info and isinstance(openapi_data, dict):
openapi_data["info"] = openapi_data.get("info", {}) openapi_data["info"] = openapi_data.get("info", {})

View file

@ -27,11 +27,14 @@
export let direct = false; export let direct = false;
export let connection = null; export let connection = null;
let url = '';
let path = 'openapi.json';
let type = 'openapi'; // 'openapi', 'mcp' let type = 'openapi'; // 'openapi', 'mcp'
let url = '';
let spec_type = 'url'; // 'url', 'json'
let spec = ''; // used when spec_type is 'json'
let path = 'openapi.json';
let auth_type = 'bearer'; let auth_type = 'bearer';
let key = ''; let key = '';
@ -149,10 +152,26 @@
return; return;
} }
// validate spec
if (spec_type === 'json') {
try {
const specJSON = JSON.parse(spec);
spec = JSON.stringify(specJSON, null, 2);
} catch (e) {
toast.error($i18n.t('Please enter a valid JSON spec'));
loading = false;
return;
}
}
const connection = { const connection = {
url,
path,
type, type,
url,
spec_type,
spec,
path,
auth_type, auth_type,
key, key,
config: { config: {
@ -173,9 +192,12 @@
show = false; show = false;
// reset form // reset form
url = '';
path = 'openapi.json';
type = 'openapi'; type = 'openapi';
url = '';
spec_type = 'url';
spec = '';
path = 'openapi.json';
key = ''; key = '';
auth_type = 'bearer'; auth_type = 'bearer';
@ -191,10 +213,13 @@
const init = () => { const init = () => {
if (connection) { if (connection) {
type = connection?.type ?? 'openapi';
url = connection.url; url = connection.url;
spec_type = connection?.spec_type ?? 'url';
spec = connection?.spec ?? '';
path = connection?.path ?? 'openapi.json'; path = connection?.path ?? 'openapi.json';
type = connection?.type ?? 'openapi';
auth_type = connection?.auth_type ?? 'bearer'; auth_type = connection?.auth_type ?? 'bearer';
key = connection?.key ?? ''; key = connection?.key ?? '';
@ -326,35 +351,81 @@
<Switch bind:state={enable} /> <Switch bind:state={enable} />
</Tooltip> </Tooltip>
</div> </div>
{#if ['', 'openapi'].includes(type)}
<div class="flex-1 flex items-center">
<label for="url-or-path" class="sr-only"
>{$i18n.t('openapi.json URL or Path')}</label
>
<input
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="text"
id="url-or-path"
bind:value={path}
placeholder={$i18n.t('openapi.json URL or Path')}
autocomplete="off"
required
/>
</div>
{/if}
</div> </div>
</div> </div>
{#if ['', 'openapi'].includes(type)} {#if ['', 'openapi'].includes(type)}
<div <div class="flex gap-2 mt-2">
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`} <div class="flex flex-col w-full">
> <div class="flex justify-between items-center mb-0.5">
{$i18n.t(`WebUI will make requests to "{{url}}"`, { <div class="flex gap-2 items-center">
url: path.includes('://') <div
? path for="select-bearer-or-session"
: `${url}${path.startsWith('/') ? '' : '/'}${path}` class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
})} >
{$i18n.t('OpenAPI Spec')}
</div>
</div>
</div>
<div class="flex gap-2">
<div class="flex-shrink-0 self-start">
<select
id="select-bearer-or-session"
class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
bind:value={spec_type}
>
<option value="url">{$i18n.t('URL')}</option>
<option value="json">{$i18n.t('JSON')}</option>
</select>
</div>
<div class="flex flex-1 items-center">
{#if spec_type === 'url'}
<div class="flex-1 flex items-center">
<label for="url-or-path" class="sr-only"
>{$i18n.t('openapi.json URL or Path')}</label
>
<input
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
type="text"
id="url-or-path"
bind:value={path}
placeholder={$i18n.t('openapi.json URL or Path')}
autocomplete="off"
required
/>
</div>
{:else if spec_type === 'json'}
<div
class={`text-xs w-full self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
<label for="url-or-path" class="sr-only">{$i18n.t('JSON Spec')}</label>
<textarea
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-black dark:text-white'}`}
bind:value={spec}
placeholder={$i18n.t('JSON Spec')}
autocomplete="off"
required
rows="5"
/>
</div>
{/if}
</div>
</div>
{#if ['', 'url'].includes(spec_type)}
<div
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
url: path.includes('://')
? path
: `${url}${path.startsWith('/') ? '' : '/'}${path}`
})}
</div>
{/if}
</div>
</div> </div>
{/if} {/if}