UIPackage
Menu

Framework

Change language

Boilerplate repo

Mcp Server Registry Manager

blockai

Anthropic Model Context Protocol (MCP) server registry manager with connection health monitoring, stdio/SSE stream transport metrics, live tool schema explorer, parameterized resource URI templates, and JSON-RPC 2.0 payload inspection drawer.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/mcp-server-registry-manager.json
Named registry:npx shadcn-vue@latest add @uipkge/mcp-server-registry-managerInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
classHTMLAttributes['class']optional
initialSearchstring''optional
initialTransportFilter
'all''stdio''sse'
'all'optional
initialStatusFilter
'all''healthy''reconnecting'
'all'optional
initialSelectedToolNamestringoptional
initialDrawerOpenbooleanfalseoptional
initialInstallDialogOpenbooleanfalseoptional
initialHandshakeAlertbooleanfalseoptional
initialServersMcpServer[]optional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ToolParam
interface ToolParam {
  name: string
  type: 'string' | 'number' | 'boolean' | 'object' | 'array'
  required: boolean
  description: string
  default?: string
  enum?: string[]
}
ToolSchema
interface ToolSchema {
  name: string
  displayName: string
  description: string
  category: 'documentation' | 'database' | 'version-control' | 'deployment' | 'web-search' | 'observability'
  parameters: ToolParam[]
  sampleRpcRequest: {
    jsonrpc: '2.0'
    id: string
    method: 'tools/call'
    params: {
      name: string
      arguments: Record<string, any>
    }
  }
  sampleRpcResponse: {
    jsonrpc: '2.0'
    id: string
    result: {
      content: Array<{
        type: 'text' | 'resource' | 'image'
        text?: string
        resource?: any
      }>
      isError: boolean
    }
  }
}
ResourceTemplate
interface ResourceTemplate {
  uriTemplate: string
  name: string
  description: string
  mimeType: string
}
McpServer
interface McpServer {
  id: string
  name: string
  scope: string
  version: string
  transport: TransportType
  commandOrUrl: string
  status: ServerStatus
  uptime: string
  latencyMs: number
  callsToday: number
  description: string
  envVars: string[]
  resourceTemplates: ResourceTemplate[]
  tools: ToolSchema[]
}

Files installed (8)

  • app/components/blocks/McpServerRegistryManager.vue16.3 kB
    <script setup lang="ts">
    import { computed, ref } from 'vue'
    import type { HTMLAttributes } from 'vue'
    import { Check, CheckCircle2, Copy, Plus, Radio, RefreshCw, Search, Server, Terminal, X } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
    import { Input } from '@/components/ui/input'
    import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
    
    import { defaultServers } from './mcp-server-registry-data'
    import type { McpServer, ToolSchema } from './mcp-server-registry-types'
    import McpHandshakeLogsTable from './McpHandshakeLogsTable.vue'
    import McpInstallServerDialog from './McpInstallServerDialog.vue'
    import McpKpiCards from './McpKpiCards.vue'
    import McpServerCard from './McpServerCard.vue'
    import McpToolInspectorDrawer from './McpToolInspectorDrawer.vue'
    
    export type {
      McpServer,
      ResourceTemplate,
      ServerStatus,
      ToolParam,
      ToolSchema,
      TransportType,
    } from './mcp-server-registry-types'
    
    interface Props {
      class?: HTMLAttributes['class']
      initialSearch?: string
      initialTransportFilter?: 'all' | 'stdio' | 'sse'
      initialStatusFilter?: 'all' | 'healthy' | 'reconnecting'
      initialSelectedToolName?: string
      initialDrawerOpen?: boolean
      initialInstallDialogOpen?: boolean
      initialHandshakeAlert?: boolean
      initialServers?: McpServer[]
    }
    
    const props = withDefaults(defineProps<Props>(), {
      initialSearch: '',
      initialTransportFilter: 'all',
      initialStatusFilter: 'all',
      initialDrawerOpen: false,
      initialInstallDialogOpen: false,
      initialHandshakeAlert: false,
    })
    
    // State Management
    const servers = ref<McpServer[]>(props.initialServers ? [...props.initialServers] : [...defaultServers])
    const searchQuery = ref(props.initialSearch)
    const transportFilter = ref<'all' | 'stdio' | 'sse'>(props.initialTransportFilter)
    const statusFilter = ref<'all' | 'healthy' | 'reconnecting'>(props.initialStatusFilter)
    const isDrawerOpen = ref(props.initialDrawerOpen)
    const isInstallDialogOpen = ref(props.initialInstallDialogOpen)
    const handshakeAlertVisible = ref(props.initialHandshakeAlert)
    const isHandshaking = ref(false)
    const copiedKey = ref<string | null>(null)
    const inspectorTab = ref<'request' | 'response' | 'schema'>('request')
    const activeViewTab = ref<'servers' | 'handshake-logs' | 'config-json'>('servers')
    
    // Inspector state
    const selectedServer = ref<McpServer>(servers.value[0] ?? defaultServers[0])
    const selectedTool = ref<ToolSchema>(servers.value[0]?.tools[0] ?? defaultServers[0].tools[0])
    
    // Initialize selected tool if provided
    if (props.initialSelectedToolName) {
      for (const s of servers.value) {
        const t = s.tools.find((tool) => tool.name === props.initialSelectedToolName)
        if (t) {
          selectedServer.value = s
          selectedTool.value = t
          break
        }
      }
    }
    
    // Dry Run state
    const isDryRunning = ref(false)
    const dryRunOutput = ref<string | null>(null)
    
    function resetFilters() {
      searchQuery.value = ''
      transportFilter.value = 'all'
      statusFilter.value = 'all'
    }
    
    // Filtering Logic
    const filteredServers = computed(() => {
      const q = searchQuery.value.trim().toLowerCase()
      return servers.value.filter((s) => {
        const matchTransport = transportFilter.value === 'all' || s.transport === transportFilter.value
        const matchStatus = statusFilter.value === 'all' || s.status === statusFilter.value
        if (!matchTransport || !matchStatus) return false
    
        if (!q) return true
        const inName = s.name.toLowerCase().includes(q)
        const inScope = s.scope.toLowerCase().includes(q)
        const inDesc = s.description.toLowerCase().includes(q)
        const inCmd = s.commandOrUrl.toLowerCase().includes(q)
        const inTools = s.tools.some((t) => t.name.toLowerCase().includes(q) || t.displayName.toLowerCase().includes(q))
        const inUris = s.resourceTemplates.some((r) => r.uriTemplate.toLowerCase().includes(q))
        return inName || inScope || inDesc || inCmd || inTools || inUris
      })
    })
    
    // Counts & Metrics
    const totalConnectedServers = computed(() => servers.value.filter((s) => s.status === 'healthy').length)
    const totalRegisteredTools = computed(() => servers.value.reduce((acc, s) => acc + s.tools.length, 0))
    const totalResourceTemplates = computed(() => servers.value.reduce((acc, s) => acc + s.resourceTemplates.length, 0))
    const totalInvocationsToday = computed(() => servers.value.reduce((acc, s) => acc + s.callsToday, 0))
    
    function openToolInspector(server: McpServer, tool: ToolSchema) {
      selectedServer.value = server
      selectedTool.value = tool
      dryRunOutput.value = null
      isDrawerOpen.value = true
    }
    
    function openServerInspector(server: McpServer) {
      if (server.tools.length > 0) {
        openToolInspector(server, server.tools[0])
      }
    }
    
    function copyToClipboard(text: string, key: string) {
      if (typeof navigator !== 'undefined' && navigator.clipboard) {
        navigator.clipboard.writeText(text)
        copiedKey.value = key
        setTimeout(() => {
          if (copiedKey.value === key) copiedKey.value = null
        }, 2000)
      }
    }
    
    function runHandshakeTest() {
      isHandshaking.value = true
      handshakeAlertVisible.value = false
      setTimeout(() => {
        isHandshaking.value = false
        handshakeAlertVisible.value = true
      }, 650)
    }
    
    function onServerInstalled(newServer: McpServer) {
      servers.value.unshift(newServer)
    }
    
    function removeServer(serverId: string) {
      servers.value = servers.value.filter((s) => s.id !== serverId)
    }
    
    function restartServer(server: McpServer) {
      server.status = 'healthy'
      server.latencyMs = Math.floor(Math.random() * 20) + 15
    }
    
    function runDryRunTest() {
      if (!selectedTool.value) return
      isDryRunning.value = true
      setTimeout(() => {
        isDryRunning.value = false
        dryRunOutput.value = JSON.stringify(selectedTool.value.sampleRpcResponse.result, null, 2)
      }, 450)
    }
    
    const activeMcpConfigJson = computed(() => {
      const config: { mcpServers: Record<string, any> } = { mcpServers: {} }
      for (const s of servers.value) {
        const key = s.name.replace(/^mcp\//, '')
        if (s.transport === 'stdio') {
          const parts = s.commandOrUrl.split(' ')
          config.mcpServers[key] = {
            command: parts[0],
            args: parts.slice(1),
            env: s.envVars.reduce((acc: Record<string, string>, ev) => {
              const [k, v] = ev.split('=')
              if (k && v) acc[k] = v
              return acc
            }, {}),
          }
        } else {
          config.mcpServers[key] = {
            transport: 'sse',
            url: s.commandOrUrl,
          }
        }
      }
      return JSON.stringify(config, null, 2)
    })
    </script>
    
    <template>
      <div
        data-slot="mcp-server-registry-manager"
        :class="
          cn(
            'bg-background text-foreground border-border w-full space-y-6 overflow-hidden rounded-xl border p-4 shadow-xs sm:p-6 md:p-8',
            props.class,
          )
        "
      >
        <!-- Top System Header -->
        <header class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
          <div class="space-y-1.5">
            <div class="flex flex-wrap items-center gap-2.5">
              <div
                class="bg-primary/10 text-primary border-primary/20 flex size-9 items-center justify-center rounded-lg border"
              >
                <Server class="size-4.5" />
              </div>
              <h1 class="text-xl font-bold tracking-tight sm:text-2xl">Model Context Protocol (MCP) Server Manager</h1>
            </div>
            <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
              <Badge variant="secondary" class="font-mono text-xs font-normal"> MCP Spec 2024-11-05 · JSON-RPC 2.0 </Badge>
              <span></span>
              <span class="text-success inline-flex items-center gap-1.5 font-medium">
                <span class="bg-success flex size-2 animate-pulse rounded-full" />
                {{ totalConnectedServers }} Connected · {{ totalRegisteredTools }} Tools Active
              </span>
              <span class="hidden sm:inline"></span>
              <span class="hidden sm:inline">Anthropic Runtime & Agent Registry</span>
            </div>
          </div>
    
          <!-- Action CTA Buttons -->
          <div class="flex flex-wrap items-center gap-2.5">
            <Button
              variant="outline"
              size="sm"
              class="h-9 gap-1.5 text-xs font-medium"
              :disabled="isHandshaking"
              @click="runHandshakeTest"
            >
              <RefreshCw :class="cn('size-3.5', isHandshaking && 'text-primary animate-spin')" />
              <span>{{ isHandshaking ? 'Handshaking...' : 'Test Tool Handshake' }}</span>
            </Button>
    
            <Button size="sm" class="h-9 gap-1.5 text-xs font-medium shadow-xs" @click="isInstallDialogOpen = true">
              <Plus class="size-3.5" />
              <span>Install New MCP Server</span>
            </Button>
          </div>
        </header>
    
        <!-- Handshake Diagnostic Alert Banner -->
        <div
          v-if="handshakeAlertVisible"
          class="border-success/30 bg-success/10 text-foreground dark:text-foreground flex items-start justify-between rounded-lg border p-3.5 text-xs transition-colors"
        >
          <div class="flex items-start gap-2.5">
            <CheckCircle2 class="text-success mt-0.5 size-4 shrink-0" />
            <div class="space-y-0.5">
              <p class="text-success font-semibold">Handshake Verified · All 6 MCP Servers Responded (Avg 42ms)</p>
              <p class="text-success">
                34 tool schemas, 18 resource templates, and prompt interfaces validated against MCP Specification
                2024-11-05.
              </p>
            </div>
          </div>
          <button
            aria-label="Dismiss notification"
            class="text-success hover:text-foreground rounded-md p-1 transition-colors"
            @click="handshakeAlertVisible = false"
          >
            <X class="size-3.5" />
          </button>
        </div>
    
        <!-- 4 MCP System KPI Cards -->
        <McpKpiCards
          :servers-count="servers.length"
          :healthy-count="totalConnectedServers"
          :reconnecting-count="servers.length - totalConnectedServers"
          :tools-count="totalRegisteredTools"
          :resource-templates-count="totalResourceTemplates"
          :invocations-today="totalInvocationsToday"
        />
    
        <!-- Main Content Navigation & Filter Bar -->
        <div class="space-y-4">
          <div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
            <!-- View Tabs -->
            <Tabs v-model="activeViewTab" class="w-full md:w-auto">
              <TabsList class="grid h-9 w-full max-w-[380px] grid-cols-3 md:w-full">
                <TabsTrigger value="servers" class="text-xs"> Servers & Tools ({{ servers.length }}) </TabsTrigger>
                <TabsTrigger value="handshake-logs" class="text-xs"> Handshake Logs </TabsTrigger>
                <TabsTrigger value="config-json" class="text-xs"> Raw JSON Config </TabsTrigger>
              </TabsList>
            </Tabs>
    
            <!-- Search Bar and Transport Filters -->
            <div class="flex flex-wrap items-center gap-2">
              <div class="relative w-full sm:w-64">
                <Search class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
                <Input
                  v-model="searchQuery"
                  placeholder="Search servers, tools, URIs..."
                  class="bg-background h-8.5 pl-8 text-xs"
                />
              </div>
    
              <!-- Transport Filter Buttons -->
              <div class="border-border bg-muted/40 flex items-center rounded-lg border p-0.5 text-xs">
                <button
                  :class="
                    cn(
                      'rounded-md px-2.5 py-1 font-medium transition-colors',
                      transportFilter === 'all'
                        ? 'bg-background text-foreground shadow-xs'
                        : 'text-muted-foreground hover:text-foreground',
                    )
                  "
                  @click="transportFilter = 'all'"
                >
                  All
                </button>
                <button
                  :class="
                    cn(
                      'inline-flex items-center gap-1 rounded-md px-2.5 py-1 font-medium transition-colors',
                      transportFilter === 'stdio'
                        ? 'bg-background text-foreground shadow-xs'
                        : 'text-muted-foreground hover:text-foreground',
                    )
                  "
                  @click="transportFilter = 'stdio'"
                >
                  <Terminal class="size-3" />
                  stdio
                </button>
                <button
                  :class="
                    cn(
                      'inline-flex items-center gap-1 rounded-md px-2.5 py-1 font-medium transition-colors',
                      transportFilter === 'sse'
                        ? 'bg-background text-foreground shadow-xs'
                        : 'text-muted-foreground hover:text-foreground',
                    )
                  "
                  @click="transportFilter = 'sse'"
                >
                  <Radio class="size-3" />
                  SSE Stream
                </button>
              </div>
            </div>
          </div>
    
          <!-- Tab View 1: Servers & Tools List -->
          <div v-if="activeViewTab === 'servers'" class="space-y-4">
            <!-- Empty State -->
            <div
              v-if="filteredServers.length === 0"
              class="border-border bg-card/40 flex flex-col items-center justify-center rounded-xl border border-dashed p-10 text-center"
            >
              <Server class="text-muted-foreground/60 mb-3 size-10" />
              <h3 class="text-sm font-semibold">No MCP Servers Found</h3>
              <p class="text-muted-foreground mt-1 max-w-sm text-xs">
                No servers match your current search query or filter criteria. Clear filters or add a new MCP server
                connection.
              </p>
              <Button size="sm" variant="outline" class="mt-4 h-8 text-xs" @click="resetFilters"> Reset Filters </Button>
            </div>
    
            <!-- Servers Cards -->
            <McpServerCard
              v-for="server in filteredServers"
              :key="server.id"
              :server="server"
              :copied-key="copiedKey"
              @open-tool-inspector="openToolInspector(server, $event)"
              @open-server-inspector="openServerInspector(server)"
              @restart-server="restartServer(server)"
              @remove-server="removeServer(server.id)"
              @copy="copyToClipboard"
            />
          </div>
    
          <!-- Tab View 2: Handshake & Telemetry Logs -->
          <McpHandshakeLogsTable
            v-else-if="activeViewTab === 'handshake-logs'"
            :servers="servers"
            :is-handshaking="isHandshaking"
            @re-probe="runHandshakeTest"
          />
    
          <!-- Tab View 3: Raw JSON Configuration -->
          <div v-else-if="activeViewTab === 'config-json'" class="space-y-4">
            <Card class="border-border bg-card">
              <CardHeader class="pb-3">
                <div class="flex items-center justify-between">
                  <div>
                    <CardTitle class="text-sm font-bold">Claude Desktop & Cursor MCP Config</CardTitle>
                    <CardDescription class="text-xs">
                      Exportable JSON format compatible with <code class="font-mono">claude_desktop_config.json</code>.
                    </CardDescription>
                  </div>
                  <Button
                    size="sm"
                    variant="outline"
                    class="h-8 gap-1.5 text-xs"
                    @click="copyToClipboard(activeMcpConfigJson, 'mcp-config-json')"
                  >
                    <Check v-if="copiedKey === 'mcp-config-json'" class="text-success size-3.5" />
                    <Copy v-else class="size-3.5" />
                    {{ copiedKey === 'mcp-config-json' ? 'JSON Copied!' : 'Copy Config JSON' }}
                  </Button>
                </div>
              </CardHeader>
              <CardContent>
                <pre
                  class="bg-muted/60 border-border text-foreground max-h-[420px] overflow-x-auto rounded-lg border p-4 font-mono text-xs leading-relaxed"
                  >{{ activeMcpConfigJson }}</pre
                >
              </CardContent>
            </Card>
          </div>
        </div>
    
        <!-- Tool Schema Inspection Drawer (Sheet) -->
        <McpToolInspectorDrawer
          :open="isDrawerOpen"
          :server="selectedServer"
          :tool="selectedTool"
          :inspector-tab="inspectorTab"
          :copied-key="copiedKey"
          :is-dry-running="isDryRunning"
          :dry-run-output="dryRunOutput"
          @update:open="isDrawerOpen = $event"
          @update:inspector-tab="inspectorTab = $event"
          @copy="copyToClipboard"
          @run-dry-run="runDryRunTest"
        />
    
        <!-- Install New MCP Server Dialog -->
        <McpInstallServerDialog v-model:open="isInstallDialogOpen" @install="onServerInstalled" />
      </div>
    </template>
    
  • app/components/blocks/McpHandshakeLogsTable.vue3.4 kB
  • app/components/blocks/McpInstallServerDialog.vue7 kB
  • app/components/blocks/McpKpiCards.vue4 kB
  • app/components/blocks/McpServerCard.vue7.1 kB
  • app/components/blocks/McpToolInspectorDrawer.vue9.2 kB
  • app/components/blocks/mcp-server-registry-data.ts30.9 kB
  • app/components/blocks/mcp-server-registry-types.ts1.3 kB

Raw manifest:https://uipkge.dev/r/vue/mcp-server-registry-manager.json