Skip to content

Commit c463b35

Browse files
committed
Prevent MCP page crash on empty tool lists
Re-apply the missing fix: Go's append(nil, empty...) encodes tools as JSON null, and the MCP page called .length on it. Return a non-nil empty slice and normalize tools on the client with mcpToolsOrEmpty.
1 parent bf3ee7c commit c463b35

5 files changed

Lines changed: 47 additions & 3 deletions

File tree

internal/mcp/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ func (c *Client) refreshTools(ctx context.Context) error {
205205
func (c *Client) Tools() []ToolDef {
206206
c.mu.RLock()
207207
defer c.mu.RUnlock()
208-
return append([]ToolDef(nil), c.tools...)
208+
return append([]ToolDef{}, c.tools...)
209209
}
210210

211211
// Name returns the configured server name.

internal/mcp/client_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,23 @@ func TestStdioReportsToolErrors(t *testing.T) {
6868
}
6969
}
7070

71+
func TestEmptyToolListEncodesAsArray(t *testing.T) {
72+
client := &Client{}
73+
got := client.Tools()
74+
if got == nil {
75+
t.Fatal("Tools() returned nil, want a non-nil empty slice")
76+
}
77+
payload, err := json.Marshal(struct {
78+
Tools []ToolDef `json:"tools"`
79+
}{Tools: got})
80+
if err != nil {
81+
t.Fatal(err)
82+
}
83+
if string(payload) != `{"tools":[]}` {
84+
t.Fatalf("empty tool payload = %s, want tools encoded as []", payload)
85+
}
86+
}
87+
7188
func TestUnknownTransport(t *testing.T) {
7289
if _, err := Connect(context.Background(), "x", ServerConfig{Transport: "carrier-pigeon"}); err == nil {
7390
t.Fatal("expected an unknown transport to fail")

web/src/lib/mcpPayload.test.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { describe, expect, test } from 'bun:test'
2+
import { mcpToolsOrEmpty } from './mcpPayload.ts'
3+
4+
describe('MCP payload normalization', () => {
5+
test('turns null or missing tools into an empty array', () => {
6+
expect(mcpToolsOrEmpty(null)).toEqual([])
7+
expect(mcpToolsOrEmpty(undefined)).toEqual([])
8+
expect(mcpToolsOrEmpty(null).length).toBe(0)
9+
})
10+
11+
test('keeps valid tool arrays', () => {
12+
const tools = [{ name: 'server_health', description: 'Read server health' }]
13+
expect(mcpToolsOrEmpty(tools)).toBe(tools)
14+
})
15+
})

web/src/lib/mcpPayload.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface McpPayloadTool {
2+
name: string
3+
description: string
4+
}
5+
6+
export function mcpToolsOrEmpty<T extends McpPayloadTool>(value: T[] | null | undefined): T[] {
7+
return Array.isArray(value) ? value : []
8+
}

web/src/pages/McpPage.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { del, post } from '@/lib/api'
1212
import { useApi } from '@/lib/hooks'
1313
import { useI18n } from '@/lib/i18n'
14+
import { mcpToolsOrEmpty } from '@/lib/mcpPayload'
1415
import { cn } from '@/lib/utils'
1516
import { PageLayout } from '@/components/layout/PageLayout'
1617
import {
@@ -51,7 +52,7 @@ interface McpServer {
5152
name: string
5253
connected: boolean
5354
error?: string
54-
tools: McpTool[]
55+
tools: McpTool[] | null
5556
}
5657

5758
export default function McpPage() {
@@ -93,7 +94,10 @@ export default function McpPage() {
9394

9495
if (loading && !data) return <SkeletonList count={3} />
9596

96-
const servers = data?.servers ?? []
97+
const servers = (data?.servers ?? []).map((server) => ({
98+
...server,
99+
tools: mcpToolsOrEmpty(server.tools),
100+
}))
97101

98102
const header = (
99103
<Tabs value={tab} onValueChange={(v) => setTab(v as 'servers' | 'docs')}>

0 commit comments

Comments
 (0)