Skip to content

Fix: MCP servers JSON editor loses server IDs (data integrity issue) #317

@juniorcammel

Description

@juniorcammel

Bug Description

The JSON editor for MCP servers exports configuration as an object keyed by server name (losing server IDs), but the backend requires IDs to locate servers in storage. After editing and saving JSON, the backend cannot find servers because the IDs are missing.

Impact

  • Severity: High - Data integrity issue affecting server persistence
  • User Experience: Editing via JSON breaks server functionality
  • Affected Operations: "Edit JSON" feature, backend server lookups
  • Data Loss Risk: Server IDs regenerated on every JSON edit → breaks references

Affected Code

File: apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts

Affected Functions:

  • Lines 606-639: handleOpenGlobalJsonEdit - exports to JSON
  • Lines 641-828: handleSaveGlobalJsonEdit - imports from JSON

Current Behavior (Incorrect)

Export (Object Format - Loses IDs)

const handleOpenGlobalJsonEdit = () => {
  const exportData: Record<string, Record<string, unknown>> = {};

  for (const server of mcpServers) {
    const serverConfig = { type: server.type, command: server.command, ... };
    exportData[server.name] = serverConfig; // ❌ Uses name as key, loses ID
  }

  setGlobalJsonValue(JSON.stringify({ mcpServers: exportData }, null, 2));
};

Exported JSON (Current - BAD):

{
  "mcpServers": {
    "shadcn": {
      "type": "stdio",
      "command": "cmd",
      "args": ["/c", "npx", "shadcn@latest", "mcp"]
    }
  }
}

Problem: Server ID mcp-1767024860547-90lvecvle is lost!

Import (No ID Handling)

const handleSaveGlobalJsonEdit = async () => {
  const parsed = JSON.parse(globalJsonValue);
  const servers = parsed.mcpServers || parsed;

  // Uses name to match existing servers, but backend needs ID
  const existing = existingByName.get(name);
  if (existing) {
    updateMCPServer(existing.id, serverData); // ✅ Has ID (if matched by name)
  } else {
    addMCPServer(serverData); // ❌ Creates NEW ID (even if editing existing)
  }
};

Result: Backend calls like POST /api/mcp/${serverId}/test fail because IDs don't match storage.

Expected Behavior

  1. Export: Include server IDs in JSON format
  2. Import: Preserve IDs when editing existing servers
  3. Backward Compatibility: Support legacy object format (Claude Desktop compatible)

Proposed Solution

1. Change Export Format to Array with IDs

const handleOpenGlobalJsonEdit = () => {
  const serversArray = mcpServers.map((server) => ({
    id: server.id,          // ✅ Preserve ID
    name: server.name,      // ✅ Preserve name
    type: server.type || 'stdio',
    // ... other fields
  }));

  setGlobalJsonValue(JSON.stringify({ mcpServers: serversArray }, null, 2));
};

New Export Format (Array - GOOD):

{
  "mcpServers": [
    {
      "id": "mcp-1767024860547-90lvecvle",
      "name": "shadcn",
      "type": "stdio",
      "command": "cmd",
      "args": ["/c", "npx", "shadcn@latest", "mcp"]
    }
  ]
}

IDs preserved!

2. Support Both Formats on Import

const handleSaveGlobalJsonEdit = async () => {
  const parsed = JSON.parse(globalJsonValue);
  const servers = parsed.mcpServers || parsed;

  if (Array.isArray(servers)) {
    // ✅ New format: array with IDs
    await handleSaveGlobalJsonArray(servers);
  } else if (typeof servers === 'object' && servers !== null) {
    // ✅ Legacy format: object (Claude Desktop compatible)
    await handleSaveGlobalJsonObject(servers);
  } else {
    toast.error('Invalid format');
  }
};

3. Match by ID First, Then Name

const handleSaveGlobalJsonArray = async (serversArray: unknown[]) => {
  for (const config of serversArray) {
    const id = serverConfig.id as string | undefined;
    const name = serverConfig.name as string;

    // ✅ Try to match by ID first, then by name
    const existing = id ? existingById.get(id) : existingByName.get(name);
    if (existing) {
      updateMCPServer(existing.id, serverData); // Uses original ID
    } else {
      addMCPServer(serverData); // Creates new server with new ID
    }
  }
};

Testing

Before Fix:

  1. Add MCP server (gets ID mcp-123)
  2. Open JSON editor → ID not visible
  3. Edit and save → New ID generated (mcp-456)
  4. Backend test fails: "Server mcp-123 not found"

After Fix:

  1. Add MCP server (gets ID mcp-123)
  2. Open JSON editor → ID visible in array format
  3. Edit and save → Same ID preserved (mcp-123)
  4. Backend test succeeds: Server found with correct ID
  5. Legacy object format still imports correctly (Claude Desktop compatibility)

Dependencies

  • Requires: Race condition fix (see related issue) for correct sync timing
  • Requires: HTTP error handling fix (see related issue) for proper error messages
  • Enables: Reliable backend operations (test, execute, delete)

Additional Context

  • Breaking Change: No - supports both formats
  • Claude Desktop Compatibility: Yes - object format still supported for import
  • Default Format: Array (new format with IDs)
  • Migration: Automatic - next JSON edit converts to array format
  • Performance: Minimal impact (~1-2ms for array iteration)

Example: Full Working Configuration

{
  "mcpServers": [
    {
      "id": "mcp-1767024860547-90lvecvle",
      "name": "shadcn",
      "type": "stdio",
      "description": "Shadcn UI components",
      "command": "cmd",
      "args": ["/c", "npx", "shadcn@latest", "mcp"],
      "enabled": true
    },
    {
      "id": "mcp-1767024918008-pvxmpgulq",
      "name": "playwright",
      "type": "stdio",
      "description": "Playwright testing",
      "command": "cmd",
      "args": ["/c", "npx", "@playwright/mcp@latest"],
      "enabled": true
    },
    {
      "id": "mcp-1767025842728-oknc6liiu",
      "name": "context7",
      "type": "http",
      "description": "Context7 AI assistant",
      "url": "https://mcp.context7.com/mcp",
      "headers": {
        "CONTEXT7_API_KEY": "ctx7sk-xxx"
      },
      "enabled": true
    }
  ]
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions