Menu

Change language

GitHub 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.

Installation

$npx uipkge-ng@latest add mcp-server-registry-manager
Manifest:https://uipkge.dev/r/angular/mcp-server-registry-manager.jsonInstalls to:src/app/components/blocks/mcp-server-registry-manager/

Variants

Loading interactive previews…

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, unknown> }
  }
  sampleRpcResponse: {
    jsonrpc: '2.0'
    id: string
    result: { content: Array<{ type: 'text' | 'resource' | 'image'; text?: string }>; 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 (2)

  • src/app/components/blocks/mcp-server-registry-manager/mcp-server-registry-manager.component.ts66.1 kB
    // Anthropic Model Context Protocol (MCP) server registry manager with connection health
    // monitoring, stdio/SSE transport metrics, live tool schema explorer, parameterized
    // resource URI templates, and JSON-RPC 2.0 payload inspection drawer. Port of the React
    // block 1:1 (single-file Angular port of the Vue multi-file block).
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Activity,
      Boxes,
      Check,
      CheckCircle2,
      Copy,
      Link2,
      LucideAngularModule,
      Play,
      Plus,
      RefreshCw,
      Search,
      Server,
      ShieldCheck,
      Trash2,
      Wrench,
      X,
      Zap,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import {
      UiDialogComponent,
      UiDialogContentComponent,
      UiDialogDescriptionComponent,
      UiDialogFooterComponent,
      UiDialogHeaderComponent,
      UiDialogTitleComponent,
    } from '@/ui/dialog/dialog.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import {
      UiSheetCloseComponent,
      UiSheetComponent,
      UiSheetContentComponent,
      UiSheetDescriptionComponent,
      UiSheetFooterComponent,
      UiSheetHeaderComponent,
      UiSheetTitleComponent,
    } from '@/ui/sheet/sheet.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    import { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component'
    import { UiTextareaComponent } from '@/ui/textarea/textarea.component'
    
    export type TransportType = 'stdio' | 'sse'
    export type ServerStatus = 'healthy' | 'reconnecting' | 'degraded'
    
    export interface ToolParam {
      name: string
      type: 'string' | 'number' | 'boolean' | 'object' | 'array'
      required: boolean
      description: string
      default?: string
      enum?: string[]
    }
    
    export 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, unknown> }
      }
      sampleRpcResponse: {
        jsonrpc: '2.0'
        id: string
        result: { content: Array<{ type: 'text' | 'resource' | 'image'; text?: string }>; isError: boolean }
      }
    }
    
    export interface ResourceTemplate {
      uriTemplate: string
      name: string
      description: string
      mimeType: string
    }
    
    export 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[]
    }
    
    function rpcResult(text: string) {
      return { content: [{ type: 'text' as const, text }], isError: false }
    }
    
    export const DEFAULT_MCP_SERVERS: McpServer[] = [
      {
        id: 'server-ctx-1',
        name: 'mcp/context7-docs',
        scope: '@context7/mcp-server',
        version: 'v1.4.2',
        transport: 'stdio',
        commandOrUrl: 'npx -y @context7/mcp-server',
        status: 'healthy',
        uptime: '99.98%',
        latencyMs: 18,
        callsToday: 4120,
        description: 'Context-aware documentation retriever and code snippet indexer for public libraries and frameworks.',
        envVars: ['CONTEXT7_API_KEY=c7_live_***', 'CACHE_TTL=3600'],
        resourceTemplates: [
          {
            uriTemplate: 'docs://{library}/{version}',
            name: 'Library Documentation AST',
            description: 'Direct access to indexed AST documentation, type exports, and code snippets',
            mimeType: 'application/json',
          },
          {
            uriTemplate: 'cheatsheets://{framework}/api-reference',
            name: 'Framework Cheatsheet',
            description: 'High-density API method cheat sheets and migration summaries',
            mimeType: 'text/markdown',
          },
        ],
        tools: [
          {
            name: 'query_docs',
            displayName: 'Query Library Docs',
            description:
              'Retrieves up-to-date documentation and code examples from Context7 for any programming library or framework.',
            category: 'documentation',
            parameters: [
              {
                name: 'libraryId',
                type: 'string',
                required: true,
                description: 'Exact Context7-compatible library ID (e.g. /tailwindlabs/tailwindcss or /unovue/reka-ui).',
              },
              {
                name: 'query',
                type: 'string',
                required: true,
                description: 'What to look up in the library documentation, scoped to a single concept.',
              },
              {
                name: 'limit',
                type: 'number',
                required: false,
                default: '5',
                description: 'Maximum number of relevant code snippets to return (1-20).',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-ctx-001',
              method: 'tools/call',
              params: {
                name: 'query_docs',
                arguments: { libraryId: '/tailwindlabs/tailwindcss', query: 'CSS variables and OKLCH color palettes v4' },
              },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-ctx-001',
              result: rpcResult(
                'Tailwind CSS v4 introduces native CSS-first theme configuration using `@theme` and OKLCH color functions without javascript config files.',
              ),
            },
          },
          {
            name: 'resolve_library_id',
            displayName: 'Resolve Library ID',
            description:
              'Resolves a package or product name to a Context7-compatible library ID and returns matching ranked libraries.',
            category: 'documentation',
            parameters: [
              {
                name: 'libraryName',
                type: 'string',
                required: true,
                description: 'Library name to search for (e.g. Next.js, Reka UI, Lucide).',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-ctx-002',
              method: 'tools/call',
              params: { name: 'resolve_library_id', arguments: { libraryName: 'Tailwind CSS' } },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-ctx-002',
              result: rpcResult('Resolved library ID: /tailwindlabs/tailwindcss (trust score 9.8).'),
            },
          },
        ],
      },
      {
        id: 'server-pg-2',
        name: 'mcp/postgres-inspector',
        scope: '@modelcontextprotocol/server-postgres',
        version: 'v0.9.1',
        transport: 'stdio',
        commandOrUrl: 'npx -y @modelcontextprotocol/server-postgres postgresql://app_user:***@db.internal:5432/prod_main',
        status: 'healthy',
        uptime: '99.95%',
        latencyMs: 24,
        callsToday: 3450,
        description:
          'Direct PostgreSQL read-only query execution, EXPLAIN execution plan inspector, and live schema reflection.',
        envVars: ['PGSSLMODE=require', 'MAX_CONNECTIONS=10'],
        resourceTemplates: [
          {
            uriTemplate: 'postgres://{schema}/{table}',
            name: 'Table Schema Definition',
            description: 'PostgreSQL table column constraints, foreign keys, row count estimations, and B-Tree indexes',
            mimeType: 'application/json',
          },
          {
            uriTemplate: 'postgres://metrics/slow-queries',
            name: 'PgStatStatements Log Buffer',
            description: 'Aggregated query execution statistics and cache hit ratios',
            mimeType: 'application/json',
          },
        ],
        tools: [
          {
            name: 'execute_sql',
            displayName: 'Execute Read-Only SQL',
            description:
              'Executes read-only SQL queries against Postgres connection with safe execution timeout safeguards.',
            category: 'database',
            parameters: [
              {
                name: 'query',
                type: 'string',
                required: true,
                description: 'SQL statement (SELECT only) to execute on the cluster.',
              },
              {
                name: 'readOnly',
                type: 'boolean',
                required: false,
                default: 'true',
                description: 'Enforces transaction read-only flag before executing SQL.',
              },
              {
                name: 'timeoutMs',
                type: 'number',
                required: false,
                default: '5000',
                description: 'Statement cancellation timeout in milliseconds.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-pg-001',
              method: 'tools/call',
              params: {
                name: 'execute_sql',
                arguments: {
                  query: 'SELECT id, email, role, created_at FROM public.users WHERE is_active = true LIMIT 5;',
                  readOnly: true,
                  timeoutMs: 5000,
                },
              },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-pg-001',
              result: rpcResult(
                '{"rows":[{"id":"usr_8492","email":"[email protected]","role":"admin","created_at":"2026-02-14T08:30:00Z"}],"rowCount":1,"durationMs":14.2}',
              ),
            },
          },
          {
            name: 'explain_query',
            displayName: 'Explain Query Plan',
            description: 'Runs EXPLAIN ANALYZE on query plans to detect slow sequential scans and missing indexes.',
            category: 'database',
            parameters: [
              {
                name: 'query',
                type: 'string',
                required: true,
                description: 'SQL query to analyze execution graph and node timings.',
              },
              {
                name: 'analyze',
                type: 'boolean',
                required: false,
                default: 'true',
                description: 'Execute statement to measure actual wall-clock row counts and buffer usage.',
              },
              {
                name: 'format',
                type: 'string',
                required: false,
                default: 'json',
                enum: ['json', 'text', 'yaml'],
                description: 'Postgres execution plan output serialization format.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-pg-002',
              method: 'tools/call',
              params: {
                name: 'explain_query',
                arguments: {
                  query: 'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC;',
                  analyze: true,
                  format: 'json',
                },
              },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-pg-002',
              result: rpcResult(
                '{"Plan":{"Node Type":"Index Scan","Index Name":"idx_orders_user_id","Startup Cost":0.42,"Total Cost":8.44,"Plan Rows":1,"Actual Rows":1,"Actual Total Time":0.114}}',
              ),
            },
          },
          {
            name: 'inspect_schema',
            displayName: 'Inspect Schema Meta',
            description: 'Returns table column definitions, constraints, foreign keys, and indexes for a database schema.',
            category: 'database',
            parameters: [
              {
                name: 'schema',
                type: 'string',
                required: false,
                default: 'public',
                description: 'Target namespace schema name (e.g. public, auth, billing).',
              },
              {
                name: 'includeForeignKeys',
                type: 'boolean',
                required: false,
                default: 'true',
                description: 'Include relation cardinality graphs and ON DELETE cascade actions.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-pg-003',
              method: 'tools/call',
              params: { name: 'inspect_schema', arguments: { schema: 'public', includeForeignKeys: true } },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-pg-003',
              result: rpcResult(
                '{"schema":"public","tablesCount":18,"tables":["users","teams","orders","invoices","audit_logs"]}',
              ),
            },
          },
        ],
      },
      {
        id: 'server-gh-3',
        name: 'mcp/github-tools',
        scope: '@modelcontextprotocol/server-github',
        version: 'v2.1.0',
        transport: 'stdio',
        commandOrUrl: 'npx -y @modelcontextprotocol/server-github',
        status: 'healthy',
        uptime: '99.91%',
        latencyMs: 92,
        callsToday: 2890,
        description:
          'GitHub REST & GraphQL API bindings for pull request management, diff inspection, and GitHub Actions CI triggers.',
        envVars: ['GITHUB_PERSONAL_ACCESS_TOKEN=ghp_live_***'],
        resourceTemplates: [
          {
            uriTemplate: 'github://repos/{owner}/{repo}/pulls/{number}',
            name: 'Pull Request Snapshot',
            description: 'Structured pull request metadata, commits, CI checks, and review discussions',
            mimeType: 'application/json',
          },
          {
            uriTemplate: 'github://repos/{owner}/{repo}/actions/{runId}',
            name: 'CI Workflow Run Logs',
            description: 'GitHub Actions build step outputs and failure diagnostics',
            mimeType: 'text/plain',
          },
        ],
        tools: [
          {
            name: 'create_pr',
            displayName: 'Create Pull Request',
            description: 'Creates a GitHub pull request with title, description, branch target, and reviewer tags.',
            category: 'version-control',
            parameters: [
              { name: 'owner', type: 'string', required: true, description: 'GitHub account or organization name.' },
              { name: 'repo', type: 'string', required: true, description: 'Repository name.' },
              { name: 'title', type: 'string', required: true, description: 'Pull request header title.' },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-gh-001',
              method: 'tools/call',
              params: { name: 'create_pr', arguments: { owner: 'uipkge', repo: 'uipkge-ui', title: 'feat: add block' } },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-gh-001',
              result: rpcResult('Created PR #482: feat: add block (draft=false, checks pending).'),
            },
          },
          {
            name: 'review_diff',
            displayName: 'Review Diff',
            description: 'Fetches and analyzes pull request unified diffs against target base branch.',
            category: 'version-control',
            parameters: [
              { name: 'owner', type: 'string', required: true, description: 'Repository owner.' },
              { name: 'repo', type: 'string', required: true, description: 'Repository name.' },
              { name: 'pullNumber', type: 'number', required: true, description: 'GitHub pull request number.' },
              {
                name: 'contextLines',
                type: 'number',
                required: false,
                default: '3',
                description: 'Number of surrounding context lines to include.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-gh-002',
              method: 'tools/call',
              params: {
                name: 'review_diff',
                arguments: { owner: 'uday-a', repo: 'uipkge', pullNumber: 142, contextLines: 3 },
              },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-gh-002',
              result: rpcResult(
                'diff --git a/components/blocks/McpServerRegistryManager.tsx b/components/blocks/McpServerRegistryManager.tsx\n+export function McpServerRegistryManager() { return <div>...</div> }',
              ),
            },
          },
          {
            name: 'merge_pr',
            displayName: 'Merge Pull Request',
            description: 'Performs squash-merge or merge commit with branch auto-deletion.',
            category: 'version-control',
            parameters: [
              { name: 'owner', type: 'string', required: true, description: 'Repository owner.' },
              { name: 'repo', type: 'string', required: true, description: 'Repository name.' },
              { name: 'pullNumber', type: 'number', required: true, description: 'Pull request number.' },
              {
                name: 'mergeMethod',
                type: 'string',
                required: false,
                default: 'squash',
                enum: ['squash', 'merge', 'rebase'],
                description: 'Git merge strategy to apply.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-gh-003',
              method: 'tools/call',
              params: {
                name: 'merge_pr',
                arguments: { owner: 'uday-a', repo: 'uipkge', pullNumber: 142, mergeMethod: 'squash' },
              },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-gh-003',
              result: rpcResult(
                '{"sha":"8a7c2b3d...","merged":true,"message":"Pull Request successfully merged and closed"}',
              ),
            },
          },
          {
            name: 'get_workflow_runs',
            displayName: 'Get Workflow Runs',
            description: 'Lists GitHub Actions CI workflow run statuses and failure logs.',
            category: 'version-control',
            parameters: [
              { name: 'owner', type: 'string', required: true, description: 'Repository owner.' },
              { name: 'repo', type: 'string', required: true, description: 'Repository name.' },
              {
                name: 'status',
                type: 'string',
                required: false,
                default: 'completed',
                enum: ['completed', 'in_progress', 'queued', 'failure'],
                description: 'Filter CI runs by lifecycle status.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-gh-004',
              method: 'tools/call',
              params: {
                name: 'get_workflow_runs',
                arguments: { owner: 'uday-a', repo: 'uipkge', status: 'completed' },
              },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-gh-004',
              result: rpcResult(
                '{"total_count":24,"workflow_runs":[{"id":9812401,"name":"CI Registry Build","conclusion":"success"}]}',
              ),
            },
          },
        ],
      },
      {
        id: 'server-cf-4',
        name: 'mcp/cloudflare-deployer',
        scope: '@cloudflare/mcp-pages-worker',
        version: 'v3.0.4',
        transport: 'sse',
        commandOrUrl: 'https://mcp-gateway.cloudflare.com/sse/v1/uipkge-prod',
        status: 'healthy',
        uptime: '100.0%',
        latencyMs: 48,
        callsToday: 1180,
        description:
          'Edge runtime deployment automation, real-time Cloudflare Worker telemetry log tailing, and CDN cache invalidation.',
        envVars: ['CLOUDFLARE_API_TOKEN=cft_live_***', 'CLOUDFLARE_ACCOUNT_ID=8bb9e07a...'],
        resourceTemplates: [
          {
            uriTemplate: 'cloudflare://zones/{zoneId}/pages',
            name: 'Pages Deployment Manifest',
            description: 'Active production and preview deployment bundles with rollback pointers',
            mimeType: 'application/json',
          },
          {
            uriTemplate: 'cloudflare://workers/{scriptName}/tail',
            name: 'Live Worker Telemetry Stream',
            description: 'Real-time structured event logs, CPU execution times, and exceptions',
            mimeType: 'application/x-ndjson',
          },
        ],
        tools: [
          {
            name: 'deploy_pages',
            displayName: 'Deploy Cloudflare Pages',
            description: 'Triggers atomic Cloudflare Pages deployment bundle upload with verification.',
            category: 'deployment',
            parameters: [
              {
                name: 'projectName',
                type: 'string',
                required: true,
                description: 'Cloudflare Pages project name (uipkge).',
              },
              { name: 'branch', type: 'string', required: true, description: 'Git branch trigger (e.g. main).' },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-cf-001',
              method: 'tools/call',
              params: { name: 'deploy_pages', arguments: { projectName: 'uipkge', branch: 'main' } },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-cf-001',
              result: rpcResult('Deployment uipkge-prod-9f31 live at https://uipkge.dev (26s, verified).'),
            },
          },
          {
            name: 'purge_cache',
            displayName: 'Purge CDN Cache',
            description: 'Purges Cloudflare CDN cache zones by URL pattern or Cache-Tag headers.',
            category: 'deployment',
            parameters: [
              { name: 'zoneId', type: 'string', required: true, description: 'Cloudflare Zone identifier.' },
              {
                name: 'purgeEverything',
                type: 'boolean',
                required: false,
                default: 'false',
                description: 'Purge entire CDN edge cache.',
              },
              { name: 'tags', type: 'array', required: false, description: 'Array of Cache-Tag headers to invalidate.' },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-cf-002',
              method: 'tools/call',
              params: { name: 'purge_cache', arguments: { zoneId: '8bb9e07a', purgeEverything: true } },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-cf-002',
              result: rpcResult('{"success":true,"purged":["https://uipkge.dev/*"],"durationMs":812}'),
            },
          },
          {
            name: 'tail_worker_logs',
            displayName: 'Tail Worker Logs',
            description: 'Subscribes to real-time Cloudflare Worker telemetry logs and exception traces.',
            category: 'deployment',
            parameters: [
              { name: 'scriptName', type: 'string', required: true, description: 'Worker script identifier.' },
              {
                name: 'samplingRate',
                type: 'number',
                required: false,
                default: '1.0',
                description: 'Trace event sampling ratio (0.1 to 1.0).',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-cf-003',
              method: 'tools/call',
              params: { name: 'tail_worker_logs', arguments: { scriptName: 'uipkge-edge', samplingRate: 1.0 } },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-cf-003',
              result: rpcResult('Streaming 142 events/s from uipkge-edge (p99 CPU 3.1ms, 0 exceptions).'),
            },
          },
        ],
      },
      {
        id: 'server-bv-5',
        name: 'mcp/brave-search',
        scope: '@modelcontextprotocol/server-brave-search',
        version: 'v1.1.2',
        transport: 'sse',
        commandOrUrl: 'https://mcp-stream.brave.internal/sse?channel=research',
        status: 'reconnecting',
        uptime: '98.40%',
        latencyMs: 165,
        callsToday: 840,
        description: 'Privacy-preserving web search, index lookup, and clean markdown extraction from live URLs.',
        envVars: ['BRAVE_API_KEY=bsa_***'],
        resourceTemplates: [
          {
            uriTemplate: 'search://web/{query}',
            name: 'Real-Time Web Search Cache',
            description: 'Cached organic search result nodes with domain trust rankings',
            mimeType: 'application/json',
          },
          {
            uriTemplate: 'webpage://cached/{urlHash}',
            name: 'Normalized Page Mirror',
            description: 'Clean reader-mode markdown representation of fetched webpage',
            mimeType: 'text/markdown',
          },
        ],
        tools: [
          {
            name: 'brave_search',
            displayName: 'Brave Web Search',
            description: 'Queries Brave Search API for web results, snippets, publishing dates, and ranking scores.',
            category: 'web-search',
            parameters: [
              { name: 'query', type: 'string', required: true, description: 'Search term or technical keyword.' },
              {
                name: 'count',
                type: 'number',
                required: false,
                default: '10',
                description: 'Number of organic results (1-20).',
              },
              {
                name: 'freshness',
                type: 'string',
                required: false,
                enum: ['pd', 'pw', 'pm', 'py'],
                description: 'Filter by time horizon: past day, week, month, or year.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-bv-001',
              method: 'tools/call',
              params: {
                name: 'brave_search',
                arguments: { query: 'Reka UI Vue 3.5 release notes', count: 5, freshness: 'pm' },
              },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-bv-001',
              result: rpcResult(
                '{"results":[{"title":"Reka UI v1.4 Documentation","url":"https://reka-ui.com","snippet":"Radix-vue is now Reka UI with Vue 3.5 TypeScript improvements."}]}',
              ),
            },
          },
          {
            name: 'fetch_web_content',
            displayName: 'Fetch Web Content',
            description: 'Fetches full clean markdown representations from verified web URLs.',
            category: 'web-search',
            parameters: [
              { name: 'url', type: 'string', required: true, description: 'Fully qualified HTTP/HTTPS URL to crawl.' },
              {
                name: 'format',
                type: 'string',
                required: false,
                default: 'markdown',
                enum: ['markdown', 'text', 'html'],
                description: 'Target format of extracted webpage body content.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-bv-002',
              method: 'tools/call',
              params: { name: 'fetch_web_content', arguments: { url: 'https://reka-ui.com/docs', format: 'markdown' } },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-bv-002',
              result: rpcResult('# Reka UI — 4,210 words extracted (markdown, 0.9s).'),
            },
          },
        ],
      },
      {
        id: 'server-sn-6',
        name: 'mcp/sentry-telemetry',
        scope: '@sentry/mcp-telemetry',
        version: 'v1.8.0',
        transport: 'stdio',
        commandOrUrl: 'npx -y @sentry/mcp-telemetry --org=uipkge',
        status: 'healthy',
        uptime: '99.99%',
        latencyMs: 35,
        callsToday: 820,
        description:
          'Production crash monitoring, unhandled exceptions triage, and stacktrace source-mapping for AI agents.',
        envVars: ['SENTRY_AUTH_TOKEN=sntrys_live_***', 'SENTRY_ORG=uipkge'],
        resourceTemplates: [
          {
            uriTemplate: 'sentry://projects/{project}/issues/{issueId}',
            name: 'Sentry Issue Context',
            description: 'Resolved source maps, breadcrumbs trail, device context, and user impact rate',
            mimeType: 'application/json',
          },
          {
            uriTemplate: 'sentry://projects/{project}/stats/24h',
            name: 'Hourly Error Volume',
            description: 'Aggregated error frequency histograms across release versions',
            mimeType: 'application/json',
          },
        ],
        tools: [
          {
            name: 'query_sentry_issues',
            displayName: 'Query Sentry Issues',
            description: 'Lists unhandled exceptions and regression issues by release tag and frequency.',
            category: 'observability',
            parameters: [
              { name: 'project', type: 'string', required: true, description: 'Sentry project slug.' },
              {
                name: 'query',
                type: 'string',
                required: false,
                description: 'Sentry search query (e.g. is:unresolved).',
              },
              {
                name: 'statsPeriod',
                type: 'string',
                required: false,
                default: '24h',
                enum: ['24h', '14d', '30d'],
                description: 'Time window for issue occurrence statistics.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-sn-001',
              method: 'tools/call',
              params: {
                name: 'query_sentry_issues',
                arguments: { project: 'uipkge-astro-site', query: 'is:unresolved level:error', statsPeriod: '24h' },
              },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-sn-001',
              result: rpcResult(
                `{"issues":[{"id":"iss_991","title":"TypeError: Cannot read properties of undefined (reading 'dataset')","count":3,"lastSeen":"2026-08-21T09:12:00Z"}]}`,
              ),
            },
          },
          {
            name: 'get_issue_stacktrace',
            displayName: 'Get De-minified Stacktrace',
            description: 'Retrieves de-minified source code stacktrace and breadcrumb chain for an error ID.',
            category: 'observability',
            parameters: [
              { name: 'issueId', type: 'string', required: true, description: 'Unique Sentry issue identifier.' },
              {
                name: 'expandContext',
                type: 'boolean',
                required: false,
                default: 'true',
                description: 'Include 5 lines of source code surrounding the crashing line.',
              },
            ],
            sampleRpcRequest: {
              jsonrpc: '2.0',
              id: 'req-sn-002',
              method: 'tools/call',
              params: { name: 'get_issue_stacktrace', arguments: { issueId: 'iss_991', expandContext: true } },
            },
            sampleRpcResponse: {
              jsonrpc: '2.0',
              id: 'req-sn-002',
              result: rpcResult(
                '{"filename":"src/components/story/Story.tsx","lineno":42,"colno":14,"function":"handleCopy","contextLine":"navigator.clipboard.writeText(props.code)"}',
              ),
            },
          },
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-mcp-server-registry-manager, [ui-mcp-server-registry-manager]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiDialogComponent,
        UiDialogContentComponent,
        UiDialogDescriptionComponent,
        UiDialogFooterComponent,
        UiDialogHeaderComponent,
        UiDialogTitleComponent,
        UiInputComponent,
        UiSheetCloseComponent,
        UiSheetComponent,
        UiSheetContentComponent,
        UiSheetDescriptionComponent,
        UiSheetFooterComponent,
        UiSheetHeaderComponent,
        UiSheetTitleComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
        UiTabsComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
        UiTextareaComponent,
      ],
      template: `
        <div data-slot="mcp-server-registry-manager" [class]="rootClass">
          <!-- 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"
                >
                  <lucide-icon [img]="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">
                <span ui-badge variant="secondary" class="font-mono text-xs font-normal">
                  MCP Spec 2024-11-05 · JSON-RPC 2.0
                </span>
                <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"></span>
                  {{ 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
                ui-button
                variant="outline"
                size="sm"
                class="h-9 gap-1.5 text-xs font-medium"
                [disabled]="isHandshaking()"
                (click)="runHandshakeTest()"
              >
                <lucide-icon [img]="RefreshCw" [class]="handshakeIconClass" />
                <span>{{ isHandshaking() ? 'Handshaking...' : 'Test Tool Handshake' }}</span>
              </button>
              <button
                ui-button
                size="sm"
                class="h-9 gap-1.5 text-xs font-medium shadow-xs"
                (click)="isInstallDialogOpen.set(true)"
              >
                <lucide-icon [img]="Plus" class="size-3.5" />
                <span>Install New MCP Server</span>
              </button>
            </div>
          </header>
    
          <!-- Handshake Diagnostic Alert Banner -->
          @if (handshakeAlertVisible()) {
            <div
              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">
                <lucide-icon [img]="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
                type="button"
                aria-label="Dismiss notification"
                class="text-success hover:text-foreground rounded-md p-1 transition-colors"
                (click)="handshakeAlertVisible.set(false)"
              >
                <lucide-icon [img]="X" class="size-3.5" />
              </button>
            </div>
          }
    
          <!-- 4 MCP System KPI Cards -->
          <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  Connected Servers
                </h3>
                <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                  <lucide-icon [img]="Server" class="size-3.5" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-2xl font-bold tracking-tight">{{ servers().length }} Active Servers</div>
                <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <span class="bg-success inline-block size-1.5 rounded-full"></span>
                  <span>{{ totalConnectedServers() }} healthy</span>
                  <span>•</span>
                  <span class="text-warning">{{ servers().length - totalConnectedServers() }} reconnecting</span>
                </div>
              </div>
            </div>
    
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  Registered Tools
                </h3>
                <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                  <lucide-icon [img]="Wrench" class="size-3.5" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-2xl font-bold tracking-tight">34 Tool Functions</div>
                <div class="text-success flex items-center gap-1.5 text-xs">
                  <lucide-icon [img]="ShieldCheck" class="size-3.5" />
                  <span>100% Schema Validated</span>
                </div>
              </div>
            </div>
    
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  Resource Templates
                </h3>
                <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                  <lucide-icon [img]="Link2" class="size-3.5" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-2xl font-bold tracking-tight">18 Parameterized URIs</div>
                <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <lucide-icon [img]="Boxes" class="size-3.5" />
                  <span>Dynamic URI Subscriptions</span>
                </div>
              </div>
            </div>
    
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium tracking-wider uppercase">
                  Invocation Volume
                </h3>
                <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md">
                  <lucide-icon [img]="Zap" class="size-3.5" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-2xl font-bold tracking-tight">12,480 calls today</div>
                <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <lucide-icon [img]="Activity" class="text-success size-3.5" />
                  <span><span class="text-foreground font-medium">99.8%</span> success · 142ms avg</span>
                </div>
              </div>
            </div>
          </div>
    
          <!-- 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">
              <ui-tabs [value]="activeViewTab()" (valueChange)="setActiveViewTab($event)" class="w-full md:w-auto">
                <ui-tabs-list class="grid h-9 w-full max-w-[380px] grid-cols-3 md:w-full">
                  <button ui-tabs-trigger value="servers" class="text-xs">Servers & Tools ({{ servers().length }})</button>
                  <button ui-tabs-trigger value="handshake-logs" class="text-xs">Handshake Logs</button>
                  <button ui-tabs-trigger value="config-json" class="text-xs">Raw JSON Config</button>
                </ui-tabs-list>
              </ui-tabs>
    
              <div class="flex flex-wrap items-center gap-2">
                <div class="relative w-full sm:w-64">
                  <lucide-icon
                    [img]="Search"
                    class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
                  />
                  <ui-input
                    [value]="searchQuery()"
                    (valueChange)="searchQuery.set($event)"
                    placeholder="Search servers, tools, URIs..."
                    class="bg-background h-8.5 pl-8 text-xs"
                  />
                </div>
    
                <div class="border-border bg-muted/40 flex items-center rounded-lg border p-0.5 text-xs">
                  <button [class]="transportBtnClass('all')" (click)="transportFilter.set('all')">All</button>
                  <button [class]="transportBtnClass('stdio')" (click)="transportFilter.set('stdio')">stdio</button>
                  <button [class]="transportBtnClass('sse')" (click)="transportFilter.set('sse')">SSE Stream</button>
                </div>
              </div>
            </div>
    
            <!-- Tab View 1: Servers & Tools List -->
            @if (activeViewTab() === 'servers') {
              <div class="space-y-4">
                @if (filteredServers().length === 0) {
                  <div
                    class="border-border bg-card/40 flex flex-col items-center justify-center rounded-xl border border-dashed p-10 text-center"
                  >
                    <lucide-icon [img]="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 ui-button size="sm" variant="outline" class="mt-4 h-8 text-xs" (click)="resetFilters()">
                      Reset Filters
                    </button>
                  </div>
                }
    
                @for (server of filteredServers(); track server.id) {
                  <div ui-card class="border-border bg-card shadow-xs">
                    <div ui-card-header class="pb-2">
                      <div class="flex flex-wrap items-start justify-between gap-2">
                        <div class="space-y-1">
                          <div class="flex flex-wrap items-center gap-2">
                            <h3 ui-card-title class="font-mono text-sm font-bold">{{ server.name }}</h3>
                            <span ui-badge variant="outline" class="font-mono text-xs">{{ server.version }}</span>
                            <span ui-badge variant="outline" class="font-mono text-xs">{{ server.transport }}</span>
                            <span
                              ui-badge
                              [variant]="server.status === 'healthy' ? 'secondary' : 'destructive'"
                              class="text-xs"
                            >
                              {{ server.status }}
                            </span>
                          </div>
                          <p class="text-muted-foreground text-xs">{{ server.scope }} · {{ server.description }}</p>
                          <p class="text-muted-foreground font-mono text-xs">
                            uptime {{ server.uptime }} · {{ server.latencyMs }}ms · {{ server.callsToday }} calls today
                          </p>
                        </div>
                        <div class="flex items-center gap-1.5">
                          <button
                            ui-button
                            variant="outline"
                            size="sm"
                            class="h-8 gap-1.5 text-xs"
                            (click)="copyToClipboard(server.commandOrUrl, 'cmd-' + server.id)"
                          >
                            @if (copiedKey() === 'cmd-' + server.id) {
                              <lucide-icon [img]="Check" class="text-success size-3.5" />
                            } @else {
                              <lucide-icon [img]="Copy" class="size-3.5" />
                            }
                            <span class="max-w-[220px] truncate font-mono">{{ server.commandOrUrl }}</span>
                          </button>
                          <button
                            ui-button
                            variant="ghost"
                            size="sm"
                            class="h-8 text-xs"
                            aria-label="Restart server"
                            (click)="restartServer(server)"
                          >
                            <lucide-icon [img]="RefreshCw" class="size-3.5" />
                          </button>
                          <button
                            ui-button
                            variant="ghost"
                            size="sm"
                            class="text-destructive hover:text-destructive h-8 text-xs"
                            aria-label="Remove server"
                            (click)="removeServer(server.id)"
                          >
                            <lucide-icon [img]="Trash2" class="size-3.5" />
                          </button>
                        </div>
                      </div>
                    </div>
                    <div ui-card-content class="space-y-3">
                      <div class="flex flex-wrap items-center gap-1.5">
                        @for (tool of server.tools; track tool.name) {
                          <button
                            ui-button
                            variant="outline"
                            size="sm"
                            class="h-7 gap-1.5 font-mono text-xs"
                            (click)="openToolInspector(server, tool)"
                          >
                            <lucide-icon [img]="Wrench" class="size-3" />
                            {{ tool.name }}
                          </button>
                        }
                        <button
                          ui-button
                          variant="ghost"
                          size="sm"
                          class="h-7 text-xs"
                          (click)="openServerInspector(server)"
                        >
                          Inspect server →
                        </button>
                      </div>
                      <div class="flex flex-wrap items-center gap-1.5">
                        @for (res of server.resourceTemplates; track res.uriTemplate) {
                          <span ui-badge variant="secondary" class="font-mono text-xs font-normal">
                            {{ res.uriTemplate }}
                          </span>
                        }
                      </div>
                    </div>
                  </div>
                }
              </div>
            } @else if (activeViewTab() === 'handshake-logs') {
              <div ui-card class="border-border bg-card">
                <div ui-card-header class="pb-3">
                  <div class="flex items-center justify-between">
                    <div>
                      <h3 ui-card-title class="text-sm font-bold">JSON-RPC Handshake & Telemetry Logs</h3>
                      <p class="text-muted-foreground text-xs">Latest capability negotiation per registered server.</p>
                    </div>
                    <button
                      ui-button
                      size="sm"
                      variant="outline"
                      class="h-8 gap-1.5 text-xs"
                      [disabled]="isHandshaking()"
                      (click)="runHandshakeTest()"
                    >
                      <lucide-icon [img]="RefreshCw" [class]="handshakeIconClass" />
                      {{ isHandshaking() ? 'Probing...' : 'Re-probe' }}
                    </button>
                  </div>
                </div>
                <div ui-card-content>
                  <table ui-table>
                    <thead ui-table-header>
                      <tr ui-table-row>
                        <th ui-table-head class="text-xs">Server</th>
                        <th ui-table-head class="text-xs">Transport</th>
                        <th ui-table-head class="text-xs">Status</th>
                        <th ui-table-head class="text-xs">Latency</th>
                        <th ui-table-head class="text-xs">Uptime</th>
                      </tr>
                    </thead>
                    <tbody ui-table-body>
                      @for (server of servers(); track server.id) {
                        <tr ui-table-row>
                          <td ui-table-cell class="font-mono text-xs font-medium">{{ server.name }}</td>
                          <td ui-table-cell class="font-mono text-xs">{{ server.transport }}</td>
                          <td ui-table-cell>
                            <span
                              ui-badge
                              [variant]="server.status === 'healthy' ? 'secondary' : 'destructive'"
                              class="text-xs"
                            >
                              {{ server.status }}
                            </span>
                          </td>
                          <td ui-table-cell class="font-mono text-xs">{{ server.latencyMs }}ms</td>
                          <td ui-table-cell class="font-mono text-xs">{{ server.uptime }}</td>
                        </tr>
                      }
                    </tbody>
                  </table>
                </div>
              </div>
            } @else {
              <div ui-card class="border-border bg-card">
                <div ui-card-header class="pb-3">
                  <div class="flex items-center justify-between">
                    <div>
                      <h3 ui-card-title class="text-sm font-bold">Claude Desktop & Cursor MCP Config</h3>
                      <p class="text-muted-foreground text-xs">
                        Exportable JSON format compatible with <code class="font-mono">claude_desktop_config.json</code>.
                      </p>
                    </div>
                    <button
                      ui-button
                      size="sm"
                      variant="outline"
                      class="h-8 gap-1.5 text-xs"
                      (click)="copyToClipboard(activeMcpConfigJson(), 'mcp-config-json')"
                    >
                      @if (copiedKey() === 'mcp-config-json') {
                        <lucide-icon [img]="Check" class="text-success size-3.5" />
                      } @else {
                        <lucide-icon [img]="Copy" class="size-3.5" />
                      }
                      {{ copiedKey() === 'mcp-config-json' ? 'JSON Copied!' : 'Copy Config JSON' }}
                    </button>
                  </div>
                </div>
                <div ui-card-content>
                  <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>
                </div>
              </div>
            }
          </div>
    
          <!-- Tool Schema Inspection Drawer (Sheet) -->
          <div ui-sheet [open]="isDrawerOpen()" (openChange)="isDrawerOpen.set($event)">
            <div ui-sheet-content class="w-full space-y-6 overflow-y-auto p-6 sm:max-w-xl md:max-w-2xl">
              @if (selectedTool(); as tool) {
                <div ui-sheet-header class="space-y-2 p-0 text-left">
                  <div class="flex items-center gap-2">
                    <span ui-badge variant="outline" class="font-mono text-xs">{{ selectedServer().transport }}</span>
                    <span ui-badge variant="secondary" class="font-mono text-xs">{{ tool.category }}</span>
                  </div>
                  <h2 ui-sheet-title class="font-mono text-lg font-bold tracking-tight">{{ tool.name }}</h2>
                  <p ui-sheet-description class="text-xs">{{ selectedServer().name }} · {{ tool.displayName }}</p>
                  <p class="text-muted-foreground text-xs">{{ tool.description }}</p>
                </div>
    
                <ui-tabs [value]="inspectorTab()" (valueChange)="setInspectorTab($event)" class="w-full">
                  <ui-tabs-list class="grid h-9 w-full grid-cols-3">
                    <button ui-tabs-trigger value="request" class="text-xs">Sample Request</button>
                    <button ui-tabs-trigger value="response" class="text-xs">Sample Response</button>
                    <button ui-tabs-trigger value="schema" class="text-xs">Parameters</button>
                  </ui-tabs-list>
                </ui-tabs>
    
                @if (inspectorTab() === 'request') {
                  <pre
                    class="bg-muted/60 border-border max-h-[280px] overflow-auto rounded-lg border p-4 font-mono text-xs leading-relaxed"
                    >{{ toolRequestJson() }}</pre>
                } @else if (inspectorTab() === 'response') {
                  <pre
                    class="bg-muted/60 border-border max-h-[280px] overflow-auto rounded-lg border p-4 font-mono text-xs leading-relaxed"
                    >{{ toolResponseJson() }}</pre>
                } @else {
                  <div class="space-y-2">
                    @for (param of tool.parameters; track param.name) {
                      <div class="border-border flex items-start justify-between gap-2 rounded-lg border p-3 text-xs">
                        <div class="space-y-0.5">
                          <div class="flex items-center gap-2">
                            <code class="font-mono font-semibold">{{ param.name }}</code>
                            <span ui-badge variant="outline" class="font-mono text-xs">{{ param.type }}</span>
                            @if (param.required) {
                              <span ui-badge variant="destructive" class="text-xs">required</span>
                            }
                          </div>
                          <p class="text-muted-foreground text-xs">{{ param.description }}</p>
                        </div>
                      </div>
                    }
                  </div>
                }
    
                <div class="space-y-2">
                  <div class="flex items-center justify-between">
                    <span class="text-xs font-semibold tracking-wider uppercase">Dry Run</span>
                    <button
                      ui-button
                      size="sm"
                      variant="outline"
                      class="h-8 gap-1.5 text-xs"
                      [disabled]="isDryRunning()"
                      (click)="runDryRunTest()"
                    >
                      <lucide-icon [img]="Play" class="size-3.5" />
                      {{ isDryRunning() ? 'Running...' : 'Run Dry-Run Test' }}
                    </button>
                  </div>
                  @if (dryRunOutput()) {
                    <pre
                      class="bg-muted/60 border-border max-h-[240px] overflow-auto rounded-lg border p-4 font-mono text-xs leading-relaxed"
                      >{{ dryRunOutput() }}</pre>
                  }
                </div>
    
                <div ui-sheet-footer class="flex flex-row items-center justify-end gap-2 border-t pt-4">
                  <button
                    ui-button
                    variant="outline"
                    size="sm"
                    class="h-8 gap-1.5 text-xs"
                    (click)="copyToClipboard(toolRequestJson(), 'tool-request')"
                  >
                    @if (copiedKey() === 'tool-request') {
                      <lucide-icon [img]="Check" class="text-success size-3.5" />
                    } @else {
                      <lucide-icon [img]="Copy" class="size-3.5" />
                    }
                    Copy Request JSON
                  </button>
                  <div ui-sheet-close>
                    <button ui-button size="sm" class="h-8 text-xs">Close Inspector</button>
                  </div>
                </div>
              }
            </div>
          </div>
    
          <!-- Install New MCP Server Dialog -->
          <ui-dialog [open]="isInstallDialogOpen()" (openChange)="isInstallDialogOpen.set($event)">
            <ui-dialog-content class="sm:max-w-lg">
              <div ui-dialog-header class="space-y-1.5">
                <h2 ui-dialog-title class="flex items-center gap-2 text-base font-bold">
                  <lucide-icon [img]="Server" class="size-4" />
                  Install New MCP Server
                </h2>
                <p ui-dialog-description class="text-muted-foreground text-xs">
                  Register a stdio command or remote SSE gateway with the Anthropic runtime.
                </p>
              </div>
              <div class="space-y-4 py-2">
                <div class="space-y-1.5">
                  <label class="text-xs font-medium">Server Name</label>
                  <ui-input
                    [value]="newServerName()"
                    (valueChange)="newServerName.set($event)"
                    placeholder="e.g. mcp/my-tools"
                    class="font-mono text-xs"
                  />
                </div>
                <div class="space-y-1.5">
                  <label class="text-xs font-medium">Scope</label>
                  <ui-input
                    [value]="newServerScope()"
                    (valueChange)="newServerScope.set($event)"
                    placeholder="@custom/my-tools"
                    class="font-mono text-xs"
                  />
                </div>
                <div class="flex items-center gap-2">
                  <button type="button" [class]="installTransportClass('stdio')" (click)="newTransport.set('stdio')">
                    stdio
                  </button>
                  <button type="button" [class]="installTransportClass('sse')" (click)="newTransport.set('sse')">
                    SSE Stream
                  </button>
                </div>
                <div class="space-y-1.5">
                  <label class="text-xs font-medium">Command or SSE URL</label>
                  <ui-input
                    [value]="newCommandOrUrl()"
                    (valueChange)="newCommandOrUrl.set($event)"
                    placeholder="npx -y @custom/mcp-server"
                    class="font-mono text-xs"
                  />
                </div>
                <div class="space-y-1.5">
                  <label class="text-xs font-medium">Description</label>
                  <ui-textarea
                    [value]="newDescription()"
                    (valueChange)="newDescription.set($event)"
                    [rows]="2"
                    placeholder="What does this server expose?"
                    class="text-xs"
                  />
                </div>
              </div>
              <div ui-dialog-footer class="gap-2 sm:gap-0">
                <button ui-button variant="ghost" size="sm" (click)="isInstallDialogOpen.set(false)">Cancel</button>
                <button
                  ui-button
                  size="sm"
                  [disabled]="!newServerName().trim() || !newCommandOrUrl().trim()"
                  (click)="handleAddServer()"
                >
                  <lucide-icon [img]="Plus" class="size-3.5" />
                  Install Server
                </button>
              </div>
            </ui-dialog-content>
          </ui-dialog>
        </div>
      `,
    })
    export class UiMcpServerRegistryManagerComponent {
      protected readonly Activity = Activity
      protected readonly Boxes = Boxes
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Copy = Copy
      protected readonly Link2 = Link2
      protected readonly Play = Play
      protected readonly Plus = Plus
      protected readonly RefreshCw = RefreshCw
      protected readonly Search = Search
      protected readonly Server = Server
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Trash2 = Trash2
      protected readonly Wrench = Wrench
      protected readonly X = X
      protected readonly Zap = Zap
    
      @Input() set initialSearch(v: string | undefined) {
        this.searchQuery.set(v ?? '')
      }
      @Input() set initialTransportFilter(v: 'all' | 'stdio' | 'sse') {
        this.transportFilter.set(v ?? 'all')
      }
      @Input() set initialStatusFilter(v: 'all' | 'healthy' | 'reconnecting') {
        this.statusFilter.set(v ?? 'all')
      }
      @Input() set initialSelectedToolName(v: string | undefined) {
        if (!v) return
        for (const s of this.servers()) {
          const t = s.tools.find((tool) => tool.name === v)
          if (t) {
            this.selectedServer.set(s)
            this.selectedTool.set(t)
            break
          }
        }
      }
      @Input() set initialDrawerOpen(v: boolean) {
        this.isDrawerOpen.set(!!v)
      }
      @Input() set initialInstallDialogOpen(v: boolean) {
        this.isInstallDialogOpen.set(!!v)
      }
      @Input() set initialHandshakeAlert(v: boolean) {
        this.handshakeAlertVisible.set(!!v)
      }
      @Input() set initialServers(v: McpServer[] | undefined) {
        if (v) {
          this.servers.set([...v])
          this.selectedServer.set(v[0] ?? DEFAULT_MCP_SERVERS[0]!)
          const firstTool = v[0]?.tools[0] ?? DEFAULT_MCP_SERVERS[0]!.tools[0]!
          this.selectedTool.set(firstTool)
        }
      }
      @Input('class') className?: string
    
      readonly servers = signal<McpServer[]>(DEFAULT_MCP_SERVERS.map((s) => ({ ...s })))
      readonly searchQuery = signal('')
      readonly transportFilter = signal<'all' | 'stdio' | 'sse'>('all')
      readonly statusFilter = signal<'all' | 'healthy' | 'reconnecting'>('all')
      readonly isDrawerOpen = signal(false)
      readonly isInstallDialogOpen = signal(false)
      readonly handshakeAlertVisible = signal(false)
      readonly isHandshaking = signal(false)
      readonly copiedKey = signal<string | null>(null)
      readonly inspectorTab = signal<'request' | 'response' | 'schema'>('request')
      readonly activeViewTab = signal<'servers' | 'handshake-logs' | 'config-json'>('servers')
    
      readonly selectedServer = signal<McpServer>(DEFAULT_MCP_SERVERS[0]!)
      readonly selectedTool = signal<ToolSchema>(DEFAULT_MCP_SERVERS[0]!.tools[0]!)
    
      readonly isDryRunning = signal(false)
      readonly dryRunOutput = signal<string | null>(null)
    
      readonly newServerName = signal('')
      readonly newServerScope = signal('')
      readonly newTransport = signal<TransportType>('stdio')
      readonly newCommandOrUrl = signal('')
      readonly newDescription = signal('')
    
      readonly filteredServers = computed(() => {
        const q = this.searchQuery().trim().toLowerCase()
        return this.servers().filter((s) => {
          const matchTransport = this.transportFilter() === 'all' || s.transport === this.transportFilter()
          const matchStatus = this.statusFilter() === 'all' || s.status === this.statusFilter()
          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
        })
      })
    
      readonly totalConnectedServers = computed(() => this.servers().filter((s) => s.status === 'healthy').length)
      readonly totalRegisteredTools = computed(() => this.servers().reduce((acc, s) => acc + s.tools.length, 0))
      readonly totalResourceTemplates = computed(() =>
        this.servers().reduce((acc, s) => acc + s.resourceTemplates.length, 0),
      )
      readonly totalInvocationsToday = computed(() => this.servers().reduce((acc, s) => acc + s.callsToday, 0))
    
      readonly toolRequestJson = computed(() => JSON.stringify(this.selectedTool().sampleRpcRequest, null, 2))
      readonly toolResponseJson = computed(() => JSON.stringify(this.selectedTool().sampleRpcResponse, null, 2))
    
      readonly activeMcpConfigJson = computed(() => {
        const config: { mcpServers: Record<string, unknown> } = { mcpServers: {} }
        for (const s of this.servers()) {
          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)
      })
    
      get rootClass(): string {
        return 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',
          this.className,
        )
      }
    
      get handshakeIconClass(): string {
        return cn('size-3.5', this.isHandshaking() && 'text-primary animate-spin')
      }
    
      transportBtnClass(t: 'all' | 'stdio' | 'sse'): string {
        return cn(
          'rounded-md px-2.5 py-1 font-medium transition-colors',
          this.transportFilter() === t
            ? 'bg-background text-foreground shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      installTransportClass(t: TransportType): string {
        return cn(
          'rounded-md border px-2.5 py-1 text-xs font-medium transition-colors',
          this.newTransport() === t
            ? 'border-primary bg-primary/10 text-primary'
            : 'border-border text-muted-foreground hover:text-foreground',
        )
      }
    
      setActiveViewTab(v: string): void {
        this.activeViewTab.set(v as 'servers' | 'handshake-logs' | 'config-json')
      }
    
      setInspectorTab(v: string): void {
        this.inspectorTab.set(v as 'request' | 'response' | 'schema')
      }
    
      resetFilters(): void {
        this.searchQuery.set('')
        this.transportFilter.set('all')
        this.statusFilter.set('all')
      }
    
      openToolInspector(server: McpServer, tool: ToolSchema): void {
        this.selectedServer.set(server)
        this.selectedTool.set(tool)
        this.dryRunOutput.set(null)
        this.isDrawerOpen.set(true)
      }
    
      openServerInspector(server: McpServer): void {
        if (server.tools.length > 0) this.openToolInspector(server, server.tools[0]!)
      }
    
      copyToClipboard(text: string, key: string): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText(text)
          this.copiedKey.set(key)
          setTimeout(() => {
            if (this.copiedKey() === key) this.copiedKey.set(null)
          }, 2000)
        }
      }
    
      runHandshakeTest(): void {
        this.isHandshaking.set(true)
        this.handshakeAlertVisible.set(false)
        setTimeout(() => {
          this.isHandshaking.set(false)
          this.handshakeAlertVisible.set(true)
        }, 650)
      }
    
      handleAddServer(): void {
        if (!this.newServerName().trim() || !this.newCommandOrUrl().trim()) return
        const base = this.newServerName()
          .trim()
          .replace(/^mcp\//, '')
        const newServer: McpServer = {
          id: `server-${Date.now()}`,
          name: this.newServerName().trim(),
          scope: this.newServerScope().trim() || `@custom/${base}`,
          version: 'v1.0.0',
          transport: this.newTransport(),
          commandOrUrl: this.newCommandOrUrl().trim(),
          status: 'healthy',
          uptime: '100.0%',
          latencyMs: this.newTransport() === 'stdio' ? 22 : 64,
          callsToday: 0,
          description: this.newDescription().trim() || 'Custom registered Model Context Protocol runtime server.',
          envVars: ['MCP_ENABLED=true'],
          resourceTemplates: [
            {
              uriTemplate: `custom://${base}/{resourceId}`,
              name: 'Custom Parameterized Resource',
              description: 'Dynamic schema endpoint for custom tool payloads',
              mimeType: 'application/json',
            },
          ],
          tools: [
            {
              name: `${base.replace(/[^a-zA-Z0-9_]/g, '_')}_action`,
              displayName: 'Execute Custom Action',
              description: 'Default action handler registered for this server.',
              category: 'deployment',
              parameters: [{ name: 'payload', type: 'string', required: true, description: 'Action parameter payload.' }],
              sampleRpcRequest: {
                jsonrpc: '2.0',
                id: 'req-custom-001',
                method: 'tools/call',
                params: {
                  name: `${base.replace(/[^a-zA-Z0-9_]/g, '_')}_action`,
                  arguments: { payload: 'execute' },
                },
              },
              sampleRpcResponse: {
                jsonrpc: '2.0',
                id: 'req-custom-001',
                result: rpcResult('{"status":"success","executedAt":"2026-08-21T12:00:00Z"}'),
              },
            },
          ],
        }
        this.servers.update((prev) => [newServer, ...prev])
        this.newServerName.set('')
        this.newServerScope.set('')
        this.newCommandOrUrl.set('')
        this.newDescription.set('')
        this.isInstallDialogOpen.set(false)
      }
    
      removeServer(serverId: string): void {
        this.servers.update((prev) => prev.filter((s) => s.id !== serverId))
      }
    
      restartServer(server: McpServer): void {
        this.servers.update((prev) =>
          prev.map((s) =>
            s.id === server.id
              ? { ...s, status: 'healthy' as ServerStatus, latencyMs: Math.floor(Math.random() * 20) + 15 }
              : s,
          ),
        )
      }
    
      runDryRunTest(): void {
        const tool = this.selectedTool()
        if (!tool) return
        this.isDryRunning.set(true)
        setTimeout(() => {
          this.isDryRunning.set(false)
          this.dryRunOutput.set(JSON.stringify(tool.sampleRpcResponse.result, null, 2))
        }, 450)
      }
    }
    
  • src/app/components/blocks/mcp-server-registry-manager/index.ts0.2 kB

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