Mcp Server Registry Manager
blockaiAnthropic 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
$pnpm dlx uipkge-ng@latest add mcp-server-registry-manager$npx uipkge-ng@latest add mcp-server-registry-manager$yarn dlx uipkge-ng@latest add mcp-server-registry-manager$bunx uipkge-ng@latest add mcp-server-registry-managerManifest:
https://uipkge.dev/r/angular/mcp-server-registry-manager.jsonInstalls to:src/app/components/blocks/mcp-server-registry-manager/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ToolParaminterface ToolParam {
name: string
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
required: boolean
description: string
default?: string
enum?: string[]
}ToolSchemainterface 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 }
}
}ResourceTemplateinterface ResourceTemplate {
uriTemplate: string
name: string
description: string
mimeType: string
}McpServerinterface 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
export { UiMcpServerRegistryManagerComponent, DEFAULT_MCP_SERVERS, type McpServer, type ResourceTemplate, type ServerStatus, type ToolParam, type ToolSchema, type TransportType, } from './mcp-server-registry-manager.component'
Raw manifest:https://uipkge.dev/r/angular/mcp-server-registry-manager.json