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
4 changes: 3 additions & 1 deletion internal/tools/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ func init() {

// ToolsetsFor reports which named toolsets include a tool.
func ToolsetsFor(name string) []string {
var out []string
// Keep this non-nil so JSON clients receive [] rather than null for dynamic
// tools (for example MCP tools) that are not members of a named preset.
out := []string{}
for set, members := range Toolsets {
if set == "all" {
continue
Expand Down
20 changes: 20 additions & 0 deletions internal/tools/register_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tools

import (
"encoding/json"
"testing"
)

func TestToolsetsForUnknownToolEncodesAsEmptyArray(t *testing.T) {
sets := ToolsetsFor("mcp__dynamic__not-in-a-preset")
if sets == nil || len(sets) != 0 {
t.Fatalf("ToolsetsFor unknown tool = %#v, want non-nil empty slice", sets)
}
got, err := json.Marshal(sets)
if err != nil {
t.Fatal(err)
}
if string(got) != "[]" {
t.Fatalf("JSON = %s, want []", got)
}
}
15 changes: 15 additions & 0 deletions web/src/lib/toolPayload.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, expect, test } from 'bun:test'
import { toolsetsOrEmpty } from './toolPayload.ts'

describe('tool payload normalization', () => {
test('turns null or missing toolsets into an empty array', () => {
expect(toolsetsOrEmpty(null)).toEqual([])
expect(toolsetsOrEmpty(undefined)).toEqual([])
})

test('keeps valid toolset arrays', () => {
const sets = ['default', 'coding']
expect(toolsetsOrEmpty(sets)).toBe(sets)
expect(toolsetsOrEmpty(sets).slice(0, 4)).toEqual(sets)
})
})
3 changes: 3 additions & 0 deletions web/src/lib/toolPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function toolsetsOrEmpty(value: string[] | null | undefined): string[] {
return Array.isArray(value) ? value : []
}
13 changes: 7 additions & 6 deletions web/src/pages/ToolsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ import { PageLayout } from '@/components/layout/PageLayout'
import { Badge, EmptyState, Input, Switch } from '@/components/ui/primitives'
import { SkeletonList } from '@/components/ui/skeleton'
import { useI18n } from '@/lib/i18n'
import { toolsetsOrEmpty } from '@/lib/toolPayload'

interface ToolInfo {
name: string
description: string
enabled: boolean
requires_approval: boolean
toolsets: string[]
toolsets: string[] | null
}

interface ToolsResponse {
toolset: string
toolsets: string[]
toolsets: string[] | null
tools: ToolInfo[]
}

Expand Down Expand Up @@ -71,7 +72,7 @@ export default function ToolsPage() {
{t('tools.activeToolset')}
</p>
<div className="flex flex-wrap gap-1.5">
{data.toolsets.map((preset) => (
{toolsetsOrEmpty(data.toolsets).map((preset) => (
<button
key={preset}
disabled={!!busy}
Expand Down Expand Up @@ -158,13 +159,13 @@ export default function ToolsPage() {
{t('tools.mutates')}
</Badge>
) : null}
{item.toolsets.slice(0, 4).map((ts) => (
{toolsetsOrEmpty(item.toolsets).slice(0, 4).map((ts) => (
<Badge key={ts} variant="outline">
{ts}
</Badge>
))}
{item.toolsets.length > 4 ? (
<Badge variant="outline">+{item.toolsets.length - 4}</Badge>
{toolsetsOrEmpty(item.toolsets).length > 4 ? (
<Badge variant="outline">+{toolsetsOrEmpty(item.toolsets).length - 4}</Badge>
) : null}
</div>
</div>
Expand Down