From b07159894d70f094e880105f6952b75fd67cede4 Mon Sep 17 00:00:00 2001 From: Thieu Nguyen <79964592+thieung@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:03:57 -0800 Subject: [PATCH] fix(mcp): use json.RawMessage for JSONB fields to prevent base64 serialization (#25) []byte fields were base64-encoded during JSON serialization, causing frontend TypeError when calling .join() on non-array values. Added Array.isArray() guards in UI as defensive fallback. --- internal/http/mcp.go | 8 +++---- internal/store/mcp_store.go | 25 +++++++++++----------- ui/web/src/pages/mcp/mcp-form-dialog.tsx | 2 +- ui/web/src/pages/mcp/mcp-grants-dialog.tsx | 4 ++-- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/internal/http/mcp.go b/internal/http/mcp.go index c64483a9..3321e6e9 100644 --- a/internal/http/mcp.go +++ b/internal/http/mcp.go @@ -176,8 +176,8 @@ func (h *MCPHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request) { var req struct { AgentID string `json:"agent_id"` - ToolAllow []byte `json:"tool_allow,omitempty"` - ToolDeny []byte `json:"tool_deny,omitempty"` + ToolAllow json.RawMessage `json:"tool_allow,omitempty"` + ToolDeny json.RawMessage `json:"tool_deny,omitempty"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) @@ -258,8 +258,8 @@ func (h *MCPHandler) handleGrantUser(w http.ResponseWriter, r *http.Request) { var req struct { UserID string `json:"user_id"` - ToolAllow []byte `json:"tool_allow,omitempty"` - ToolDeny []byte `json:"tool_deny,omitempty"` + ToolAllow json.RawMessage `json:"tool_allow,omitempty"` + ToolDeny json.RawMessage `json:"tool_deny,omitempty"` } if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"}) diff --git a/internal/store/mcp_store.go b/internal/store/mcp_store.go index e5266860..8cd1f6c0 100644 --- a/internal/store/mcp_store.go +++ b/internal/store/mcp_store.go @@ -2,6 +2,7 @@ package store import ( "context" + "encoding/json" "time" "github.com/google/uuid" @@ -14,14 +15,14 @@ type MCPServerData struct { DisplayName string `json:"display_name,omitempty"` Transport string `json:"transport"` // "stdio", "sse", "streamable-http" Command string `json:"command,omitempty"` // stdio - Args []byte `json:"args,omitempty"` // JSONB + Args json.RawMessage `json:"args,omitempty"` // JSONB URL string `json:"url,omitempty"` // sse/http - Headers []byte `json:"headers,omitempty"` // JSONB - Env []byte `json:"env,omitempty"` // JSONB (stdio) + Headers json.RawMessage `json:"headers,omitempty"` // JSONB + Env json.RawMessage `json:"env,omitempty"` // JSONB (stdio) APIKey string `json:"api_key,omitempty"` // encrypted ToolPrefix string `json:"tool_prefix,omitempty"` TimeoutSec int `json:"timeout_sec"` - Settings []byte `json:"settings,omitempty"` // JSONB + Settings json.RawMessage `json:"settings,omitempty"` // JSONB Enabled bool `json:"enabled"` CreatedBy string `json:"created_by"` } @@ -32,9 +33,9 @@ type MCPAgentGrant struct { ServerID uuid.UUID `json:"server_id"` AgentID uuid.UUID `json:"agent_id"` Enabled bool `json:"enabled"` - ToolAllow []byte `json:"tool_allow,omitempty"` // JSONB - ToolDeny []byte `json:"tool_deny,omitempty"` // JSONB - ConfigOverrides []byte `json:"config_overrides,omitempty"` // JSONB + ToolAllow json.RawMessage `json:"tool_allow,omitempty"` // JSONB + ToolDeny json.RawMessage `json:"tool_deny,omitempty"` // JSONB + ConfigOverrides json.RawMessage `json:"config_overrides,omitempty"` // JSONB GrantedBy string `json:"granted_by"` CreatedAt time.Time `json:"created_at"` } @@ -45,10 +46,10 @@ type MCPUserGrant struct { ServerID uuid.UUID `json:"server_id"` UserID string `json:"user_id"` Enabled bool `json:"enabled"` - ToolAllow []byte `json:"tool_allow,omitempty"` // JSONB - ToolDeny []byte `json:"tool_deny,omitempty"` // JSONB - GrantedBy string `json:"granted_by"` - CreatedAt time.Time `json:"created_at"` + ToolAllow json.RawMessage `json:"tool_allow,omitempty"` // JSONB + ToolDeny json.RawMessage `json:"tool_deny,omitempty"` // JSONB + GrantedBy string `json:"granted_by"` + CreatedAt time.Time `json:"created_at"` } // MCPAccessRequest represents a request for MCP server access. @@ -60,7 +61,7 @@ type MCPAccessRequest struct { Scope string `json:"scope"` // "agent" or "user" Status string `json:"status"` // "pending", "approved", "rejected" Reason string `json:"reason,omitempty"` - ToolAllow []byte `json:"tool_allow,omitempty"` // JSONB + ToolAllow json.RawMessage `json:"tool_allow,omitempty"` // JSONB RequestedBy string `json:"requested_by"` ReviewedBy string `json:"reviewed_by,omitempty"` ReviewedAt *time.Time `json:"reviewed_at,omitempty"` diff --git a/ui/web/src/pages/mcp/mcp-form-dialog.tsx b/ui/web/src/pages/mcp/mcp-form-dialog.tsx index c9f05321..e4202017 100644 --- a/ui/web/src/pages/mcp/mcp-form-dialog.tsx +++ b/ui/web/src/pages/mcp/mcp-form-dialog.tsx @@ -46,7 +46,7 @@ export function MCPFormDialog({ open, onOpenChange, server, onSubmit }: MCPFormD setDisplayName(server?.display_name ?? ""); setTransport(server?.transport ?? "stdio"); setCommand(server?.command ?? ""); - setArgs(server?.args?.join(", ") ?? ""); + setArgs(Array.isArray(server?.args) ? server.args.join(", ") : ""); setUrl(server?.url ?? ""); setHeaders(server?.headers ? JSON.stringify(server.headers, null, 2) : ""); setToolPrefix(server?.tool_prefix ?? ""); diff --git a/ui/web/src/pages/mcp/mcp-grants-dialog.tsx b/ui/web/src/pages/mcp/mcp-grants-dialog.tsx index 381ba972..2618a694 100644 --- a/ui/web/src/pages/mcp/mcp-grants-dialog.tsx +++ b/ui/web/src/pages/mcp/mcp-grants-dialog.tsx @@ -107,10 +107,10 @@ export function MCPGrantsDialog({
{grant.agent_id} - {grant.tool_allow && ( + {Array.isArray(grant.tool_allow) && grant.tool_allow.length > 0 && ( allow: {grant.tool_allow.join(", ")} )} - {grant.tool_deny && ( + {Array.isArray(grant.tool_deny) && grant.tool_deny.length > 0 && ( deny: {grant.tool_deny.join(", ")} )}