Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.

23 changes: 16 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 @@ -342,6 +346,9 @@ async def get_available_tool_servers(
is_archived=tool.properties.get("is_archived", False),
)
)

# Sort the result and put archived tools at the end
results.sort(key=lambda x: x.is_archived)
return results

@app.get("/api/projects/{project_id}/kiln_task_tools")
Expand Down Expand Up @@ -492,6 +499,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 +559,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 @@ -26,6 +26,7 @@
let env_vars: McpServerKeyValuePair[] = []
let description = ""
let installation_instruction = ""
let is_archived = false
// Form state
let error: KilnError | null = null
let submitting = false
Expand Down Expand Up @@ -70,6 +71,9 @@

// We should fix the server type, so it's not "never" and we don't need a cast
const props = editing_tool_server.properties as Record<string, unknown>
if (typeof props.is_archived === "boolean") {
is_archived = props.is_archived
}

if (props.command && typeof props.command === "string") {
command = props.command
Expand Down Expand Up @@ -181,6 +185,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: is_archived,
}

let server_id: string | null | undefined = undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
let name = ""
let server_url = ""
let description = ""
let is_archived = false

let headers: McpServerKeyValuePair[] = []

Expand Down Expand Up @@ -58,6 +59,9 @@

// We should fix the server type, so it's not "never" and we don't need a cast
const props = editing_tool_server.properties as Record<string, unknown>
if (typeof props.is_archived === "boolean") {
is_archived = props.is_archived
}

if (props.server_url && typeof props.server_url === "string") {
server_url = props.server_url
Expand Down Expand Up @@ -150,6 +154,7 @@
headers: headersData.headersObj,
secret_header_keys: headersData.secret_header_keys,
description: description || null,
is_archived: is_archived,
}

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}
/>
Loading
Loading