Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 101 additions & 7 deletions app/desktop/studio_server/test_tool_api.py

Large diffs are not rendered by default.

20 changes: 13 additions & 7 deletions app/desktop/studio_server/tool_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class ExternalToolServerCreationRequest(BaseModel):
server_url: str
headers: Dict[str, str] = Field(default_factory=dict)
secret_header_keys: List[str] = Field(default_factory=list)
is_archived: bool


class LocalToolServerCreationRequest(BaseModel):
Expand All @@ -72,6 +73,7 @@ class LocalToolServerCreationRequest(BaseModel):
args: List[str]
env_vars: Dict[str, str] = Field(default_factory=dict)
secret_env_var_keys: List[str] = Field(default_factory=list)
is_archived: bool


class KilnTaskToolServerCreationRequest(BaseModel):
Expand Down Expand Up @@ -247,6 +249,9 @@ async def get_available_tools(
task_tools = []
mcp_tool_sets = []
for server in project.external_tool_servers(readonly=True):
if server.properties.get("is_archived", False):
continue

server_tools = []
match server.type:
case ToolServerType.remote_mcp | ToolServerType.local_mcp:
Expand All @@ -256,14 +261,13 @@ async def get_available_tools(
# Skip the tool when we can't connect to the server
continue
case ToolServerType.kiln_task:
if not server.properties.get("is_archived", False):
task_tools.append(
ToolApiDescription(
id=build_kiln_task_tool_id(server.id),
name=server.properties.get("name") or "",
description=server.properties.get("description") or "",
)
task_tools.append(
ToolApiDescription(
id=build_kiln_task_tool_id(server.id),
name=server.properties.get("name") or "",
description=server.properties.get("description") or "",
)
)
case _:
raise_exhaustive_enum_error(server.type)

Expand Down Expand Up @@ -492,6 +496,7 @@ def _remote_tool_server_properties(
"server_url": tool_data.server_url,
"headers": tool_data.headers,
"secret_header_keys": tool_data.secret_header_keys,
"is_archived": tool_data.is_archived,
}

@app.post("/api/projects/{project_id}/connect_local_mcp")
Expand Down Expand Up @@ -551,6 +556,7 @@ def _local_tool_server_properties(
"args": tool_data.args,
"env_vars": tool_data.env_vars,
"secret_env_var_keys": tool_data.secret_env_var_keys,
"is_archived": tool_data.is_archived,
}

def _validate_kiln_task_tool_task_and_run_config(
Expand Down
8 changes: 8 additions & 0 deletions app/web_ui/src/lib/api_schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3227,6 +3227,8 @@ export interface components {
};
/** Secret Header Keys */
secret_header_keys?: string[];
/** Is Archived */
is_archived: boolean;
};
/** ExtractionProgress */
ExtractionProgress: {
Expand Down Expand Up @@ -3757,6 +3759,8 @@ export interface components {
};
/** Secret Env Var Keys */
secret_env_var_keys?: string[];
/** Is Archived */
is_archived: boolean;
};
/** LocalToolServerCreationRequest */
LocalToolServerCreationRequest: {
Expand All @@ -3774,6 +3778,8 @@ export interface components {
};
/** Secret Env Var Keys */
secret_env_var_keys?: string[];
/** Is Archived */
is_archived: boolean;
};
/** LogMessage */
LogMessage: {
Expand Down Expand Up @@ -4321,6 +4327,8 @@ export interface components {
};
/** Secret Header Keys */
secret_header_keys?: string[];
/** Is Archived */
is_archived: boolean;
};
/** RepairRunPost */
RepairRunPost: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@
args: args.trim() ? args.trim().split(/\s+/) : [], // Split into argv list; empty -> []
env_vars: envVarsData.envVarsObj,
secret_env_var_keys: envVarsData.secret_env_var_keys,
is_archived: false,
}
let server_id: string | null | undefined = undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
headers: headersData.headersObj,
secret_header_keys: headersData.secret_header_keys,
description: description || null,
is_archived: false,
}
let server_id: string | null | undefined = undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@
isToolType,
} from "$lib/types"
import { toolServerTypeToString } from "$lib/utils/formatters"
import DeleteDialog from "$lib/ui/delete_dialog.svelte"
import type { UiProperty } from "$lib/ui/property_list"
import { uncache_available_tools } from "$lib/stores"
import { load_available_tools } from "$lib/stores"
import Warning from "$lib/ui/warning.svelte"

$: project_id = $page.params.project_id
$: tool_server_id = $page.params.tool_server_id
$: is_archived = tool_server?.properties?.is_archived ?? false

let tool_server: ExternalToolServerApiDescription | null = null
let loading = true
let error: KilnError | null = null
let delete_dialog: DeleteDialog | null = null
$: delete_url = `/api/projects/${project_id}/tool_servers/${tool_server_id}`
let loading_error: KilnError | null = null
let archive_error: KilnError | null = null
let unarchive_error: KilnError | null = null

onMount(async () => {
await fetch_tool_server()
Expand All @@ -32,7 +33,7 @@
async function fetch_tool_server() {
try {
loading = true
error = null
loading_error = null

if (!project_id) {
throw new Error("No project ID provided")
Expand Down Expand Up @@ -61,7 +62,7 @@

tool_server = data as ExternalToolServerApiDescription
} catch (err) {
error = createKilnError(err)
loading_error = createKilnError(err)
} finally {
loading = false
}
Expand Down Expand Up @@ -249,11 +250,95 @@
return args
}

function afterDelete() {
// Delete the project_id from the available_tools, so next reload it loads the updated list.
uncache_available_tools(project_id)
async function archive() {
update_archive(true)
}

goto(`/settings/manage_tools/${project_id}`)
async function unarchive() {
update_archive(false)
}

async function update_archive(is_archived: boolean) {
if (!tool_server) {
return
}

try {
archive_error = null
unarchive_error = null

switch (tool_server.type) {
case "remote_mcp": {
toolIsType(tool_server, tool_server.type)
await client.PATCH(
"/api/projects/{project_id}/edit_remote_mcp/{tool_server_id}",
{
params: {
path: {
project_id,
tool_server_id,
},
},
body: {
name: tool_server.name,
description: tool_server.description ?? null,
server_url: tool_server.properties.server_url,
headers: tool_server.properties.headers || {},
secret_header_keys:
tool_server.properties.secret_header_keys || [],
is_archived: is_archived,
},
},
)
break
}
case "local_mcp": {
toolIsType(tool_server, tool_server.type)
await client.PATCH(
"/api/projects/{project_id}/edit_local_mcp/{tool_server_id}",
{
params: {
path: {
project_id,
tool_server_id,
},
},
body: {
name: tool_server.name,
description: tool_server.description ?? null,
command: tool_server.properties.command,
args: tool_server.properties.args || [],
env_vars: tool_server.properties.env_vars || {},
secret_env_var_keys:
tool_server.properties.secret_env_var_keys || [],
is_archived: is_archived,
},
},
)
break
}
case "kiln_task": {
// Kiln task tools is handled by /settings/manage_tools/[project_id]/kiln_task/[tool_server_id] page
break
}
default: {
const exhaustiveCheck: never = tool_server.type
console.warn(`Unhandled toolType: ${exhaustiveCheck}`)
break
}
}
} catch (e) {
if (is_archived) {
archive_error = createKilnError(e)
} else {
unarchive_error = createKilnError(e)
}
} finally {
fetch_tool_server()
if (project_id) {
load_available_tools(project_id, true)
}
}
}
</script>

Expand All @@ -277,22 +362,48 @@
href: `/settings/manage_tools/${project_id}/edit_tool_server/${tool_server?.id}`,
},
{
icon: "/images/delete.svg",
handler: () => delete_dialog?.show(),
label: is_archived ? "Unarchive" : "Archive",
handler: is_archived ? unarchive : archive,
},
]}
>
{#if archive_error}
<Warning
warning_message={archive_error.getMessage() ||
"An unknown error occurred"}
large_icon={true}
warning_color="error"
outline={true}
/>
{/if}
{#if unarchive_error}
<Warning
warning_message={unarchive_error.getMessage() ||
"An unknown error occurred"}
large_icon={true}
warning_color="error"
outline={true}
/>
{/if}
{#if is_archived}
<Warning
warning_message="This tool server is archived. You may unarchive it to use it again."
large_icon={true}
warning_color="warning"
outline={true}
/>
{/if}
{#if loading}
<div class="w-full min-h-[50vh] flex justify-center items-center">
<div class="loading loading-spinner loading-lg"></div>
</div>
{:else if error}
{:else if loading_error}
<div
class="w-full min-h-[50vh] flex flex-col justify-center items-center gap-2"
>
<div class="font-medium">Error Loading Tool</div>
<div class="text-error text-sm">
{error.getMessage() || "An unknown error occurred"}
{loading_error.getMessage() || "An unknown error occurred"}
</div>
<button class="btn btn-primary mt-4" on:click={goBack}>
Back to Tools
Expand Down Expand Up @@ -418,10 +529,3 @@
{/if}
</AppPage>
</div>

<DeleteDialog
name="Tool Server"
bind:this={delete_dialog}
{delete_url}
after_delete={afterDelete}
/>
14 changes: 14 additions & 0 deletions libs/core/kiln_ai/datamodel/external_tool_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ class LocalServerProperties(TypedDict, total=True):
args: NotRequired[list[str]]
env_vars: NotRequired[dict[str, str]]
secret_env_var_keys: NotRequired[list[str]]
is_archived: bool


class RemoteServerProperties(TypedDict, total=True):
server_url: str
headers: NotRequired[dict[str, str]]
secret_header_keys: NotRequired[list[str]]
is_archived: bool


class KilnTaskServerProperties(TypedDict, total=True):
Expand Down Expand Up @@ -212,6 +214,18 @@ def type_from_data(cls, data: dict) -> ToolServerType:
valid_types = ", ".join(type.value for type in ToolServerType)
raise ValueError(f"type must be one of: {valid_types}")

@model_validator(mode="before")
def upgrade_old_properties(cls, data: dict) -> dict:
"""
Upgrade properties for backwards compatibility.
"""
properties = data.get("properties")
if properties is not None and "is_archived" not in properties:
# Add is_archived field with default value back to data
properties["is_archived"] = False
data["properties"] = properties
return data

@model_validator(mode="before")
def validate_required_fields(cls, data: dict) -> dict:
"""Validate that each tool type has the required configuration."""
Expand Down
Loading
Loading