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.
This commit is contained in:
Thieu Nguyen authored and GitHub committed 2026-03-01 15:03:57 +07:00
1 parent 27f73cde6a
commit b07159894d
4 files changed
+20 -19

No files matched your search

+4 -4
View File
@@ -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"})
+13 -12
View File
@@ -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"`
+1 -1
View File
@@ -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 ?? "");
+2 -2
View File
@@ -107,10 +107,10 @@ export function MCPGrantsDialog({
<div key={grant.id} className="flex items-center justify-between border-b px-3 py-2 last:border-0">
<div className="flex items-center gap-2">
<Badge variant="outline" className="font-mono text-xs">{grant.agent_id}</Badge>
{grant.tool_allow && (
{Array.isArray(grant.tool_allow) && grant.tool_allow.length > 0 && (
<span className="text-xs text-muted-foreground">allow: {grant.tool_allow.join(", ")}</span>
)}
{grant.tool_deny && (
{Array.isArray(grant.tool_deny) && grant.tool_deny.length > 0 && (
<span className="text-xs text-muted-foreground">deny: {grant.tool_deny.join(", ")}</span>
)}
</div>