From 846c217cf968431035f2a599696af177dee74240 Mon Sep 17 00:00:00 2001 From: Thieu Nguyen <79964592+thieung@users.noreply.github.com> Date: Mon, 2 Mar 2026 01:45:02 -0800 Subject: [PATCH] fix(store): handle NULL JSONB columns in MCP server scan (#40) * fix(store): handle NULL JSONB columns in MCP server scan Scan JSONB nullable columns (args, headers, env, settings) into *[]byte instead of directly into json.RawMessage to prevent silent scan failures when database values are SQL NULL. Also initialize result slices with make() to return empty JSON arrays instead of null. * fix(store): keep settings scanning direct since column is NOT NULL --- internal/store/pg/helpers.go | 7 +++++++ internal/store/pg/mcp_servers.go | 14 +++++++++++--- internal/store/pg/mcp_servers_access.go | 14 +++++++++----- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/internal/store/pg/helpers.go b/internal/store/pg/helpers.go index 7e2be4ab..ef964b4f 100644 --- a/internal/store/pg/helpers.go +++ b/internal/store/pg/helpers.go @@ -55,6 +55,13 @@ func derefUUID(u *uuid.UUID) uuid.UUID { return *u } +func derefBytes(b *[]byte) []byte { + if b == nil { + return nil + } + return *b +} + // --- JSON helpers --- func jsonOrEmpty(data []byte) []byte { diff --git a/internal/store/pg/mcp_servers.go b/internal/store/pg/mcp_servers.go index 2f9417a9..c1e38542 100644 --- a/internal/store/pg/mcp_servers.go +++ b/internal/store/pg/mcp_servers.go @@ -74,9 +74,10 @@ func (s *PGMCPServerStore) GetServerByName(ctx context.Context, name string) (*s func (s *PGMCPServerStore) scanServer(row *sql.Row) (*store.MCPServerData, error) { var srv store.MCPServerData var displayName, command, url, apiKey, toolPrefix *string + var args, headers, env *[]byte err := row.Scan( &srv.ID, &srv.Name, &displayName, &srv.Transport, &command, - &srv.Args, &url, &srv.Headers, &srv.Env, + &args, &url, &headers, &env, &apiKey, &toolPrefix, &srv.TimeoutSec, &srv.Settings, &srv.Enabled, &srv.CreatedBy, &srv.CreatedAt, &srv.UpdatedAt, ) @@ -87,6 +88,9 @@ func (s *PGMCPServerStore) scanServer(row *sql.Row) (*store.MCPServerData, error srv.Command = derefStr(command) srv.URL = derefStr(url) srv.ToolPrefix = derefStr(toolPrefix) + srv.Args = derefBytes(args) + srv.Headers = derefBytes(headers) + srv.Env = derefBytes(env) if apiKey != nil && *apiKey != "" && s.encKey != "" { decrypted, err := crypto.Decrypt(*apiKey, s.encKey) if err != nil { @@ -110,13 +114,14 @@ func (s *PGMCPServerStore) ListServers(ctx context.Context) ([]store.MCPServerDa } defer rows.Close() - var result []store.MCPServerData + result := make([]store.MCPServerData, 0) for rows.Next() { var srv store.MCPServerData var displayName, command, url, apiKey, toolPrefix *string + var args, headers, env *[]byte if err := rows.Scan( &srv.ID, &srv.Name, &displayName, &srv.Transport, &command, - &srv.Args, &url, &srv.Headers, &srv.Env, + &args, &url, &headers, &env, &apiKey, &toolPrefix, &srv.TimeoutSec, &srv.Settings, &srv.Enabled, &srv.CreatedBy, &srv.CreatedAt, &srv.UpdatedAt, ); err != nil { @@ -126,6 +131,9 @@ func (s *PGMCPServerStore) ListServers(ctx context.Context) ([]store.MCPServerDa srv.Command = derefStr(command) srv.URL = derefStr(url) srv.ToolPrefix = derefStr(toolPrefix) + srv.Args = derefBytes(args) + srv.Headers = derefBytes(headers) + srv.Env = derefBytes(env) if apiKey != nil && *apiKey != "" && s.encKey != "" { if decrypted, err := crypto.Decrypt(*apiKey, s.encKey); err == nil { srv.APIKey = decrypted diff --git a/internal/store/pg/mcp_servers_access.go b/internal/store/pg/mcp_servers_access.go index d4544d1f..75bce614 100644 --- a/internal/store/pg/mcp_servers_access.go +++ b/internal/store/pg/mcp_servers_access.go @@ -113,15 +113,16 @@ func (s *PGMCPServerStore) ListAccessible(ctx context.Context, agentID uuid.UUID } defer rows.Close() - var result []store.MCPAccessInfo + result := make([]store.MCPAccessInfo, 0) for rows.Next() { var srv store.MCPServerData var displayName, command, url, apiKey, toolPrefix *string - var toolAllowJSON, toolDenyJSON []byte + var args, headers, env *[]byte + var toolAllowJSON, toolDenyJSON *[]byte if err := rows.Scan( &srv.ID, &srv.Name, &displayName, &srv.Transport, &command, - &srv.Args, &url, &srv.Headers, &srv.Env, + &args, &url, &headers, &env, &apiKey, &toolPrefix, &srv.TimeoutSec, &srv.Settings, &srv.Enabled, &srv.CreatedBy, &srv.CreatedAt, &srv.UpdatedAt, &toolAllowJSON, &toolDenyJSON, @@ -132,6 +133,9 @@ func (s *PGMCPServerStore) ListAccessible(ctx context.Context, agentID uuid.UUID srv.Command = derefStr(command) srv.URL = derefStr(url) srv.ToolPrefix = derefStr(toolPrefix) + srv.Args = derefBytes(args) + srv.Headers = derefBytes(headers) + srv.Env = derefBytes(env) if apiKey != nil && *apiKey != "" && s.encKey != "" { if decrypted, err := crypto.Decrypt(*apiKey, s.encKey); err == nil { srv.APIKey = decrypted @@ -142,10 +146,10 @@ func (s *PGMCPServerStore) ListAccessible(ctx context.Context, agentID uuid.UUID info := store.MCPAccessInfo{Server: srv} if toolAllowJSON != nil { - json.Unmarshal(toolAllowJSON, &info.ToolAllow) + json.Unmarshal(*toolAllowJSON, &info.ToolAllow) } if toolDenyJSON != nil { - json.Unmarshal(toolDenyJSON, &info.ToolDeny) + json.Unmarshal(*toolDenyJSON, &info.ToolDeny) } result = append(result, info) }