Wednesday, December 03, 2025

Building Your Own MCP Hub: Creating a Docker-Like Registry for Model Context Protocol Servers

 




Introduction: Understanding the MCP Registry Landscape

The Model Context Protocol, developed by Anthropic, has rapidly evolved into a powerful standard for enabling large language models to interact with external tools and data sources. As the ecosystem has grown, several organizations have recognized the need for centralized registries where MCP servers can be discovered and shared. Anthropic maintains an official registry that catalogs verified MCP servers, providing a trusted source for developers seeking reliable integrations. GitHub has also emerged as a popular platform for hosting and discovering MCP servers through repository searches and topic tags.


These existing solutions serve important roles in the ecosystem. The Anthropic registry focuses on quality and verification, ensuring that listed servers meet specific standards and work reliably with Claude and other LLM systems. GitHub provides a decentralized approach where developers can publish and discover servers through the familiar repository model, leveraging existing version control and collaboration features.


However, there are compelling reasons why organizations might want to build their own MCP Hub. 


Enterprise environments often require private registries that contain proprietary MCP servers not suitable for public distribution. 

Organizations may need custom metadata schemas, domain classifications specific to their industry, or integration with existing internal tools and authentication systems. Research institutions might want specialized search capabilities tailored to academic use cases. Development teams might need a registry optimized for rapid iteration and testing of experimental servers.


This article provides a guide to building your own MCP Hub that functions similarly to Docker Hub for containers. Just as Docker Hub revolutionized container distribution by providing a centralized registry where developers can publish, discover, and pull container images, your custom MCP Hub can serve as an authoritative registry for MCP servers within your organization or community. 


The guide covers architecture, implementation details, security considerations, and extension possibilities, providing everything needed to create a production-ready registry system.


The architecture we will explore leverages advanced technologies including large language models, Retrieval Augmented Generation, and Graph RAG to understand semantic relationships between different MCP servers, their capabilities, and user requirements. The system provides both a web interface for human users and programmatic access for software systems, complete with rating mechanisms to surface the most reliable and useful servers in each domain.


Architectural Vision and Core Principles

Building an MCP Hub requires careful architectural planning to ensure the system can scale, remain maintainable, and evolve as requirements change. The architecture follows clean architecture principles, separating concerns into distinct layers that can evolve independently. At its foundation lies a sophisticated storage system that combines traditional database capabilities with modern RAG and GraphRAG technologies. This hybrid approach allows the system to handle both structured metadata like server URLs and registrant information, as well as unstructured data such as natural language descriptions and user reviews.


The architecture consists of several key layers:


The presentation layer includes both a web interface for human users and a RESTful API for programmatic access. 


The application layer contains the business logic for registration, search, rating, and domain classification. 


The domain layer models the core concepts of MCP servers, registrations, ratings, and relationships. 


Finally, the infrastructure layer handles persistence, external integrations, and the LLM-powered search capabilities.


One crucial architectural decision involves the integration of graph-based storage alongside traditional relational data. While server metadata like URLs, names, and registration dates fit naturally into relational tables, the relationships between servers, their capabilities, use cases, and domains form a rich graph structure. This graph enables sophisticated queries such as finding servers that complement each other or identifying gaps in the ecosystem where new servers would be valuable.


The system must also consider scalability from the beginning. As the number of registered servers grows, search operations must remain fast. As user traffic increases, the web interface and API must handle concurrent requests efficiently. The architecture uses caching strategically, indexes database tables appropriately, and designs for horizontal scaling where components can be distributed across multiple servers.


The Registration System: Onboarding MCP Servers

The registration system serves as the entry point for MCP servers into your hub. It must accommodate both manual registration through the web interface and programmatic registration via API calls. Each registration captures essential metadata while also allowing rich descriptions that help users understand what the server does and when to use it.


Consider the data model for an MCP server registration. Each server entry contains identifying information, technical details, and descriptive content:


from datetime import datetime

from typing import List, Optional


class MCPServerRegistration:

    """

    Represents a complete MCP server registration in the hub.

    This class encapsulates all information needed to discover,

    evaluate, and connect to an MCP server.

    """

    

    def __init__(self):

        # Unique identifier for this registration

        self.registration_id: Optional[str] = None

        

        # Core identification

        self.server_name: str = ""

        self.server_version: str = ""

        self.registrant_name: str = ""

        self.registrant_email: str = ""

        self.organization: str = ""

        

        # Technical details

        self.server_url: str = ""

        self.protocol_version: str = ""

        self.supported_capabilities: List[str] = []

        self.authentication_method: str = ""

        

        # Descriptive information

        self.short_description: str = ""

        self.detailed_description: str = ""

        self.use_cases: List[str] = []

        self.example_queries: List[str] = []

        

        # Classification

        self.primary_domain: str = ""

        self.secondary_domains: List[str] = []

        self.tags: List[str] = []

        

        # Metadata

        self.registration_date: datetime = datetime.now()

        self.last_updated: datetime = datetime.now()

        self.status: str = "pending"

        

        # Metrics

        self.total_downloads: int = 0

        self.average_rating: float = 0.0

        self.rating_count: int = 0

        

        # Embeddings for semantic search

        self.description_embedding: Optional[List[float]] = None


The registration process begins when a user or system submits server information. For manual registration through the web interface, users fill out a comprehensive form that guides them through providing all necessary details. The form 

includes validation to ensure URLs are properly formatted, descriptions are sufficiently detailed, and required fields are completed.


For programmatic registration, the API endpoint accepts JSON payloads containing the same information. This allows continuous integration systems to automatically register new versions of MCP servers as they are developed and tested:


from typing import Dict, Any


class RegistrationResult:

    """

    Encapsulates the result of a registration attempt.

    """

    

    def __init__(self, success: bool, registration_id: Optional[str] = None,

                 message: str = "", errors: Optional[List[str]] = None):

        self.success = success

        self.registration_id = registration_id

        self.message = message

        self.errors = errors or []


class RegistrationService:

    """

    Service layer handling MCP server registration logic.

    Validates submissions, enriches metadata, and persists

    registrations to the storage layer.

    """

    

    def __init__(self, storage, llm_service, validator):

        self.storage = storage

        self.llm_service = llm_service

        self.validator = validator

    

    def register_server(self, registration_data: Dict[str, Any]) -> RegistrationResult:

        """

        Process a new server registration request.

        

        This method performs validation, enrichment with LLM-generated

        insights, domain classification, and finally persists the

        registration to the storage system.

        

        Args:

            registration_data: Dictionary containing registration fields

            

        Returns:

            RegistrationResult object with success status and details

        """

        

        # Validate all required fields are present and properly formatted

        validation_result = self.validator.validate_registration(

            registration_data

        )

        

        if not validation_result.is_valid:

            return RegistrationResult(

                success=False,

                errors=validation_result.errors

            )

        

        # Create registration object from validated data

        registration = MCPServerRegistration()

        self._populate_registration(registration, registration_data)

        

        # Use LLM to analyze description and suggest domains and tags

        try:

            enrichment = self.llm_service.enrich_registration(

                description=registration.detailed_description,

                use_cases=registration.use_cases

            )

            

            # Apply suggested classifications if not already specified

            if not registration.primary_domain:

                registration.primary_domain = enrichment.suggested_domain

            

            # Add suggested tags that are not already present

            for tag in enrichment.suggested_tags:

                if tag not in registration.tags:

                    registration.tags.append(tag)

            

            # Generate embeddings for semantic search

            registration.description_embedding = (

                self.llm_service.generate_embedding(

                    registration.detailed_description

                )

            )

        except Exception as e:

            # Log error but continue with registration

            print(f"Warning: LLM enrichment failed: {str(e)}")

        

        # Persist to storage with graph relationships

        try:

            registration_id = self.storage.save_registration(registration)

            

            # Create graph nodes and relationships

            self._create_graph_relationships(registration)

            

            return RegistrationResult(

                success=True,

                registration_id=registration_id,

                message="Server registered successfully"

            )

        except Exception as e:

            return RegistrationResult(

                success=False,

                errors=[f"Storage error: {str(e)}"]

            )

    

    def _populate_registration(self, registration: MCPServerRegistration,

                              data: Dict[str, Any]) -> None:

        """

        Populate registration object from dictionary data.

        """

        registration.server_name = data.get("server_name", "")

        registration.server_version = data.get("server_version", "")

        registration.registrant_name = data.get("registrant_name", "")

        registration.registrant_email = data.get("registrant_email", "")

        registration.organization = data.get("organization", "")

        registration.server_url = data.get("server_url", "")

        registration.protocol_version = data.get("protocol_version", "")

        registration.supported_capabilities = data.get("supported_capabilities", [])

        registration.authentication_method = data.get("authentication_method", "")

        registration.short_description = data.get("short_description", "")

        registration.detailed_description = data.get("detailed_description", "")

        registration.use_cases = data.get("use_cases", [])

        registration.example_queries = data.get("example_queries", [])

        registration.primary_domain = data.get("primary_domain", "")

        registration.secondary_domains = data.get("secondary_domains", [])

        registration.tags = data.get("tags", [])

    

    def _create_graph_relationships(self, registration: MCPServerRegistration) -> None:

        """

        Create graph database nodes and relationships for this registration.

        This is called after the relational data is persisted.

        """

        # This method would interact with the graph database

        # Implementation depends on the specific graph database used

        pass


The registration service demonstrates several important patterns. First, it separates validation from business logic, making the code more maintainable and testable. Second, it leverages the LLM service to enrich registrations automatically, reducing the burden on registrants while improving discoverability. Third, it creates both traditional database records and graph relationships, enabling different types of queries. Fourth, it includes proper error handling to ensure failures are reported clearly without crashing the system.


The enrichment step deserves special attention. When a registrant provides a detailed description of their MCP server, the LLM analyzes this text to extract key concepts, identify the most appropriate domain classification, and suggest relevant tags. This automated analysis helps maintain consistency across registrations and surfaces connections that human registrants might miss.


Storage Architecture: Combining Relational, Vector, and Graph Databases

The storage layer represents one of the most sophisticated aspects of your MCP Hub. It must efficiently handle three distinct types of data and queries. Relational data includes structured metadata like server URLs, registration dates, and user ratings. Vector data consists of embeddings that enable semantic similarity search. Graph data captures the relationships between servers, domains, capabilities, and use cases.


A hybrid storage architecture addresses these requirements. PostgreSQL serves as the primary relational database, storing core metadata and providing ACID guarantees for critical operations like registration and rating submissions. The pgvector extension adds vector similarity search capabilities, allowing efficient nearest-neighbor queries on description embeddings. Neo4j or a similar graph database maintains the knowledge graph that connects servers to domains, capabilities, and related servers.


The storage abstraction layer provides a unified interface to these different systems:


import psycopg2

from typing import Optional, List


class MCPHubStorage:

    """

    Unified storage interface abstracting relational, vector,

    and graph database operations. Provides high-level methods

    for common operations while hiding implementation details.

    """

    

    def __init__(self, pg_connection, neo4j_driver):

        self.pg_conn = pg_connection

        self.graph_db = neo4j_driver

        

    def save_registration(self, registration: MCPServerRegistration) -> str:

        """

        Persist a new MCP server registration across all storage systems.

        Uses a transaction to ensure consistency across databases.

        

        Args:

            registration: MCPServerRegistration object to persist

            

        Returns:

            Generated registration_id as string

        """

        

        registration_id = None

        

        # Begin transaction in relational database

        try:

            with self.pg_conn.cursor() as cursor:

                # Insert core metadata

                cursor.execute("""

                    INSERT INTO mcp_servers (

                        server_name, server_version, registrant_name,

                        registrant_email, organization, server_url,

                        protocol_version, authentication_method,

                        short_description, detailed_description,

                        primary_domain, registration_date, status

                    ) VALUES (

                        %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s

                    )

                    RETURNING registration_id

                """, (

                    registration.server_name,

                    registration.server_version,

                    registration.registrant_name,

                    registration.registrant_email,

                    registration.organization,

                    registration.server_url,

                    registration.protocol_version,

                    registration.authentication_method,

                    registration.short_description,

                    registration.detailed_description,

                    registration.primary_domain,

                    registration.registration_date,

                    registration.status

                ))

                

                result = cursor.fetchone()

                if result:

                    registration_id = str(result[0])

                else:

                    raise Exception("Failed to retrieve registration_id")

                

                # Store description embedding for semantic search

                if registration.description_embedding:

                    # Convert embedding list to PostgreSQL array format

                    embedding_str = '[' + ','.join(

                        str(x) for x in registration.description_embedding

                    ) + ']'

                    

                    cursor.execute("""

                        INSERT INTO server_embeddings (

                            registration_id, embedding_vector

                        ) VALUES (%s, %s::vector)

                    """, (registration_id, embedding_str))

                

                # Store capabilities as separate rows

                for capability in registration.supported_capabilities:

                    cursor.execute("""

                        INSERT INTO server_capabilities (

                            registration_id, capability_name

                        ) VALUES (%s, %s)

                    """, (registration_id, capability))

                

                # Store tags

                for tag in registration.tags:

                    cursor.execute("""

                        INSERT INTO server_tags (

                            registration_id, tag_name

                        ) VALUES (%s, %s)

                    """, (registration_id, tag))

                

                # Store use cases

                for use_case in registration.use_cases:

                    cursor.execute("""

                        INSERT INTO server_use_cases (

                            registration_id, use_case_text

                        ) VALUES (%s, %s)

                    """, (registration_id, use_case))

                

                self.pg_conn.commit()

        except Exception as e:

            self.pg_conn.rollback()

            raise Exception(f"Database error during registration: {str(e)}")

        

        # Create corresponding nodes and relationships in graph database

        if registration_id:

            self._create_graph_node(registration_id, registration)

        

        return registration_id

    

    def _create_graph_node(self, registration_id: str,

                          registration: MCPServerRegistration) -> None:

        """

        Create a node in the knowledge graph representing this server

        and establish relationships to domains, capabilities, and tags.

        """

        

        try:

            with self.graph_db.session() as session:

                # Create server node

                session.run("""

                    CREATE (s:MCPServer {

                        registration_id: $reg_id,

                        name: $name,

                        version: $version,

                        url: $url

                    })

                """, {

                    "reg_id": registration_id,

                    "name": registration.server_name,

                    "version": registration.server_version,

                    "url": registration.server_url

                })

                

                # Link to primary domain

                if registration.primary_domain:

                    session.run("""

                        MATCH (s:MCPServer {registration_id: $reg_id})

                        MERGE (d:Domain {name: $domain})

                        CREATE (s)-[:BELONGS_TO {primary: true}]->(d)

                    """, {

                        "reg_id": registration_id,

                        "domain": registration.primary_domain

                    })

                

                # Link to secondary domains

                for domain in registration.secondary_domains:

                    session.run("""

                        MATCH (s:MCPServer {registration_id: $reg_id})

                        MERGE (d:Domain {name: $domain})

                        CREATE (s)-[:BELONGS_TO {primary: false}]->(d)

                    """, {

                        "reg_id": registration_id,

                        "domain": domain

                    })

                

                # Create capability nodes and relationships

                for capability in registration.supported_capabilities:

                    session.run("""

                        MATCH (s:MCPServer {registration_id: $reg_id})

                        MERGE (c:Capability {name: $capability})

                        CREATE (s)-[:PROVIDES]->(c)

                    """, {

                        "reg_id": registration_id,

                        "capability": capability

                    })

                    

                # Create tag nodes and relationships

                for tag in registration.tags:

                    session.run("""

                        MATCH (s:MCPServer {registration_id: $reg_id})

                        MERGE (t:Tag {name: $tag})

                        CREATE (s)-[:TAGGED_WITH]->(t)

                    """, {

                        "reg_id": registration_id,

                        "tag": tag

                    })

        except Exception as e:

            # Log error but do not fail the entire registration

            print(f"Warning: Graph database update failed: {str(e)}")

    

    def get_server(self, registration_id: str) -> Optional[MCPServerRegistration]:

        """

        Retrieve a server registration by its ID.

        

        Args:

            registration_id: The unique identifier of the server

            

        Returns:

            MCPServerRegistration object or None if not found

        """

        

        try:

            with self.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT 

                        registration_id, server_name, server_version,

                        registrant_name, registrant_email, organization,

                        server_url, protocol_version, authentication_method,

                        short_description, detailed_description,

                        primary_domain, registration_date, last_updated,

                        status, total_downloads, average_rating, rating_count

                    FROM mcp_servers

                    WHERE registration_id = %s

                """, (registration_id,))

                

                row = cursor.fetchone()

                if not row:

                    return None

                

                # Create registration object

                registration = MCPServerRegistration()

                registration.registration_id = str(row[0])

                registration.server_name = row[1]

                registration.server_version = row[2]

                registration.registrant_name = row[3]

                registration.registrant_email = row[4]

                registration.organization = row[5]

                registration.server_url = row[6]

                registration.protocol_version = row[7]

                registration.authentication_method = row[8]

                registration.short_description = row[9]

                registration.detailed_description = row[10]

                registration.primary_domain = row[11]

                registration.registration_date = row[12]

                registration.last_updated = row[13]

                registration.status = row[14]

                registration.total_downloads = row[15]

                registration.average_rating = float(row[16]) if row[16] else 0.0

                registration.rating_count = row[17]

                

                # Fetch capabilities

                cursor.execute("""

                    SELECT capability_name FROM server_capabilities

                    WHERE registration_id = %s

                """, (registration_id,))

                registration.supported_capabilities = [

                    row[0] for row in cursor.fetchall()

                ]

                

                # Fetch tags

                cursor.execute("""

                    SELECT tag_name FROM server_tags

                    WHERE registration_id = %s

                """, (registration_id,))

                registration.tags = [row[0] for row in cursor.fetchall()]

                

                # Fetch secondary domains

                cursor.execute("""

                    SELECT domain_name FROM server_secondary_domains

                    WHERE registration_id = %s

                """, (registration_id,))

                registration.secondary_domains = [

                    row[0] for row in cursor.fetchall()

                ]

                

                # Fetch use cases

                cursor.execute("""

                    SELECT use_case_text FROM server_use_cases

                    WHERE registration_id = %s

                """, (registration_id,))

                registration.use_cases = [row[0] for row in cursor.fetchall()]

                

                return registration

        except Exception as e:

            print(f"Error retrieving server: {str(e)}")

            return None


This storage implementation demonstrates how the system maintains consistency across multiple databases. The relational database serves as the source of truth for core metadata, while the graph database provides a rich semantic layer for discovery and relationship queries. The use of transactions ensures that partial writes cannot occur, maintaining data integrity even if one storage system fails during an operation. Error handling is comprehensive, with rollback capabilities and clear error messages.


The graph structure enables powerful queries that would be difficult or impossible with a purely relational model. For example, finding all servers that provide similar capabilities to a given server, or identifying servers that are commonly used together in the same domain, becomes straightforward with graph traversal queries.


Intelligent Search: Combining Traditional and LLM-Powered Discovery

The search system represents the core value proposition of your MCP Hub. Users need to find relevant servers quickly and accurately, whether they know exactly what they are looking for or only have a vague description of their needs. The search system must therefore support multiple query modes, each optimized for different use cases.


Exact name search provides the simplest case. When a user knows the precise name of an MCP server, the system performs a straightforward database lookup. Regular expression search extends this capability, allowing users to find servers matching patterns. For example, searching for servers with names matching the pattern "weather.*api" would return all weather-related API servers.


Domain-based browsing allows users to explore servers by category. When a user selects the "data analysis" domain, the system retrieves all servers classified under that domain, ordered by rating or popularity. This mode supports discovery when users know the general area they need but not specific server names.


The most sophisticated search mode uses natural language understanding powered by an LLM. Users describe what they need in plain language, and the system interprets this description to find relevant servers. This mode combines several techniques: embedding-based similarity search, keyword extraction, and graph traversal.


import math

from typing import List, Dict, Any


class SearchResults:

    """

    Encapsulates search results with metadata.

    """

    

    def __init__(self, query: str, results: List[Dict[str, Any]],

                 total_count: int, search_mode: str):

        self.query = query

        self.results = results

        self.total_count = total_count

        self.search_mode = search_mode

        self.servers = results


class SearchService:

    """

    Orchestrates different search strategies to find relevant MCP servers.

    Combines exact matching, pattern matching, semantic search, and

    graph-based discovery to provide comprehensive results.

    """

    

    def __init__(self, storage, llm_service, graph_db):

        self.storage = storage

        self.llm_service = llm_service

        self.graph_db = graph_db

    

    def search(self, query: str, search_mode: str = "auto",

               domain_filter: Optional[str] = None, limit: int = 20) -> SearchResults:

        """

        Execute a search query using the specified mode.

        

        The 'auto' mode analyzes the query to determine the best

        search strategy. Other modes include 'exact', 'regex',

        'semantic', and 'domain'.

        

        Args:

            query: Search query string or structured query object

            search_mode: Strategy to use for searching

            domain_filter: Optional domain to filter results

            limit: Maximum number of results to return

            

        Returns:

            SearchResults object containing ranked server matches

        """

        

        if search_mode == "auto":

            search_mode = self._determine_search_mode(query)

        

        if search_mode == "exact":

            return self._exact_search(query, domain_filter, limit)

        elif search_mode == "regex":

            return self._regex_search(query, domain_filter, limit)

        elif search_mode == "semantic":

            return self._semantic_search(query, domain_filter, limit)

        elif search_mode == "domain":

            return self._domain_search(query, limit)

        else:

            raise ValueError(f"Unknown search mode: {search_mode}")

    

    def _determine_search_mode(self, query: str) -> str:

        """

        Analyze query to determine the most appropriate search mode.

        """

        # Simple heuristics for mode detection

        if len(query.split()) > 3:

            return "semantic"

        elif '*' in query or '.' in query or '[' in query:

            return "regex"

        else:

            return "exact"

    

    def _exact_search(self, query: str, domain_filter: Optional[str],

                     limit: int) -> SearchResults:

        """

        Perform exact name matching search.

        """

        try:

            with self.storage.pg_conn.cursor() as cursor:

                if domain_filter:

                    cursor.execute("""

                        SELECT registration_id, server_name, server_url,

                               short_description, average_rating, total_downloads

                        FROM mcp_servers

                        WHERE server_name ILIKE %s

                          AND primary_domain = %s

                          AND status = 'active'

                        LIMIT %s

                    """, (f"%{query}%", domain_filter, limit))

                else:

                    cursor.execute("""

                        SELECT registration_id, server_name, server_url,

                               short_description, average_rating, total_downloads

                        FROM mcp_servers

                        WHERE server_name ILIKE %s

                          AND status = 'active'

                        LIMIT %s

                    """, (f"%{query}%", limit))

                

                results = [

                    {

                        "registration_id": row[0],

                        "name": row[1],

                        "url": row[2],

                        "description": row[3],

                        "rating": float(row[4]) if row[4] else 0.0,

                        "downloads": row[5]

                    }

                    for row in cursor.fetchall()

                ]

                

                return SearchResults(

                    query=query,

                    results=results,

                    total_count=len(results),

                    search_mode="exact"

                )

        except Exception as e:

            print(f"Search error: {str(e)}")

            return SearchResults(query, [], 0, "exact")

    

    def _semantic_search(self, query: str, domain_filter: Optional[str],

                        limit: int) -> SearchResults:

        """

        Perform semantic search using LLM-generated embeddings and

        graph-based relationship discovery.

        

        This method represents the most sophisticated search capability,

        understanding the intent behind natural language queries and

        finding servers that match that intent even if they do not

        contain the exact query terms.

        """

        

        try:

            # First, use the LLM to understand the query intent

            query_analysis = self.llm_service.analyze_search_query(query)

            

            # Extract key concepts and requirements

            extracted_concepts = query_analysis.get("concepts", [])

            required_capabilities = query_analysis.get("required_capabilities", [])

            preferred_domain = query_analysis.get("suggested_domain", "")

            

            # Generate embedding for the query

            query_embedding = self.llm_service.generate_embedding(query)

            

            # Convert embedding to PostgreSQL format

            embedding_str = '[' + ','.join(str(x) for x in query_embedding) + ']'

            

            # Find servers with similar description embeddings

            with self.storage.pg_conn.cursor() as cursor:

                if domain_filter:

                    cursor.execute("""

                        SELECT 

                            s.registration_id,

                            s.server_name,

                            s.server_url,

                            s.short_description,

                            s.average_rating,

                            s.total_downloads,

                            e.embedding_vector <-> %s::vector AS distance

                        FROM mcp_servers s

                        JOIN server_embeddings e ON s.registration_id = e.registration_id

                        WHERE s.status = 'active' AND s.primary_domain = %s

                        ORDER BY distance

                        LIMIT %s

                    """, (embedding_str, domain_filter, limit * 2))

                else:

                    cursor.execute("""

                        SELECT 

                            s.registration_id,

                            s.server_name,

                            s.server_url,

                            s.short_description,

                            s.average_rating,

                            s.total_downloads,

                            e.embedding_vector <-> %s::vector AS distance

                        FROM mcp_servers s

                        JOIN server_embeddings e ON s.registration_id = e.registration_id

                        WHERE s.status = 'active'

                        ORDER BY distance

                        LIMIT %s

                    """, (embedding_str, limit * 2))

                

                embedding_matches = cursor.fetchall()

            

            # Use graph database to find servers with required capabilities

            capability_matches = []

            if required_capabilities and len(required_capabilities) > 0:

                try:

                    with self.graph_db.session() as session:

                        min_matches = max(1, len(required_capabilities) // 2)

                        result = session.run("""

                            MATCH (s:MCPServer)-[:PROVIDES]->(c:Capability)

                            WHERE c.name IN $capabilities

                            WITH s, count(c) as matching_capabilities

                            WHERE matching_capabilities >= $min_matches

                            RETURN s.registration_id, matching_capabilities

                            ORDER BY matching_capabilities DESC

                            LIMIT $limit

                        """, {

                            "capabilities": required_capabilities,

                            "min_matches": min_matches,

                            "limit": limit

                        })

                        

                        capability_matches = [

                            (record["s.registration_id"], 

                             record["matching_capabilities"])

                            for record in result

                        ]

                except Exception as e:

                    print(f"Graph query error: {str(e)}")

            

            # Combine and rank results using a scoring function

            combined_results = self._combine_search_results(

                embedding_matches,

                capability_matches,

                preferred_domain or domain_filter,

                limit

            )

            

            return SearchResults(

                query=query,

                results=combined_results,

                total_count=len(combined_results),

                search_mode="semantic"

            )

        except Exception as e:

            print(f"Semantic search error: {str(e)}")

            return SearchResults(query, [], 0, "semantic")

    

    def _combine_search_results(self, embedding_matches: List[tuple],

                               capability_matches: List[tuple],

                               preferred_domain: Optional[str],

                               limit: int) -> List[Dict[str, Any]]:

        """

        Merge results from different search strategies and rank them

        using a composite scoring function.

        

        The scoring function considers:

        - Semantic similarity (from embedding distance)

        - Capability match count

        - Domain alignment

        - User ratings

        - Popularity (download count)

        """

        

        # Create a dictionary to accumulate scores for each server

        server_scores: Dict[str, Dict[str, Any]] = {}

        

        # Process embedding matches

        for match in embedding_matches:

            reg_id = str(match[0])

            distance = float(match[6])

            

            # Convert distance to similarity score (0 to 1)

            similarity = 1.0 / (1.0 + distance)

            

            if reg_id not in server_scores:

                server_scores[reg_id] = {

                    "registration_id": reg_id,

                    "name": match[1],

                    "url": match[2],

                    "description": match[3],

                    "rating": float(match[4]) if match[4] else 0.0,

                    "downloads": match[5],

                    "scores": {}

                }

            

            server_scores[reg_id]["scores"]["semantic"] = similarity * 0.4

        

        # Process capability matches

        for reg_id, match_count in capability_matches:

            reg_id_str = str(reg_id)

            if reg_id_str not in server_scores:

                # Fetch basic info for this server

                server_info = self._get_server_info(reg_id_str)

                if server_info:

                    server_scores[reg_id_str] = {

                        "registration_id": reg_id_str,

                        "name": server_info["name"],

                        "url": server_info["url"],

                        "description": server_info["description"],

                        "rating": server_info["rating"],

                        "downloads": server_info["downloads"],

                        "scores": {}

                    }

            

            if reg_id_str in server_scores:

                # Normalize match count to 0-1 range

                max_possible = len(capability_matches)

                capability_score = min(match_count / max(max_possible, 1), 1.0)

                server_scores[reg_id_str]["scores"]["capability"] = capability_score * 0.3

        

        # Apply domain bonus

        if preferred_domain:

            for reg_id in server_scores:

                server_info = self._get_server_info(reg_id)

                if server_info:

                    if server_info.get("primary_domain") == preferred_domain:

                        server_scores[reg_id]["scores"]["domain"] = 0.15

                    elif preferred_domain in server_info.get("secondary_domains", []):

                        server_scores[reg_id]["scores"]["domain"] = 0.08

        

        # Apply rating and popularity factors

        for reg_id in server_scores:

            rating = server_scores[reg_id]["rating"]

            downloads = server_scores[reg_id]["downloads"]

            

            # Normalize rating (0-5 scale to 0-1)

            rating_score = rating / 5.0

            server_scores[reg_id]["scores"]["rating"] = rating_score * 0.1

            

            # Logarithmic popularity score

            popularity_score = math.log10(downloads + 1) / 6.0

            server_scores[reg_id]["scores"]["popularity"] = min(popularity_score, 1.0) * 0.05

        

        # Calculate total scores and sort

        for reg_id in server_scores:

            total = sum(server_scores[reg_id]["scores"].values())

            server_scores[reg_id]["total_score"] = total

        

        sorted_results = sorted(

            server_scores.values(),

            key=lambda x: x.get("total_score", 0),

            reverse=True

        )

        

        return sorted_results[:limit]

    

    def _get_server_info(self, registration_id: str) -> Optional[Dict[str, Any]]:

        """

        Fetch basic server information for scoring purposes.

        """

        server = self.storage.get_server(registration_id)

        if server:

            return {

                "name": server.server_name,

                "url": server.server_url,

                "description": server.short_description,

                "rating": server.average_rating,

                "downloads": server.total_downloads,

                "primary_domain": server.primary_domain,

                "secondary_domains": server.secondary_domains

            }

        return None

    

    def _domain_search(self, domain: str, limit: int) -> SearchResults:

        """

        Search for all servers in a specific domain.

        """

        try:

            with self.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT registration_id, server_name, server_url,

                           short_description, average_rating, total_downloads

                    FROM mcp_servers

                    WHERE primary_domain = %s AND status = 'active'

                    ORDER BY average_rating DESC, total_downloads DESC

                    LIMIT %s

                """, (domain, limit))

                

                results = [

                    {

                        "registration_id": row[0],

                        "name": row[1],

                        "url": row[2],

                        "description": row[3],

                        "rating": float(row[4]) if row[4] else 0.0,

                        "downloads": row[5]

                    }

                    for row in cursor.fetchall()

                ]

                

                return SearchResults(

                    query=domain,

                    results=results,

                    total_count=len(results),

                    search_mode="domain"

                )

        except Exception as e:

            print(f"Domain search error: {str(e)}")

            return SearchResults(domain, [], 0, "domain")


The semantic search implementation demonstrates the power of combining multiple signals to produce highly relevant results. The embedding-based similarity captures semantic meaning, while capability matching ensures functional requirements are met. Domain alignment and user ratings provide additional quality signals. The composite scoring function weights these different factors to produce a final ranking that balances relevance, functionality, and quality.


The LLM plays a crucial role in query understanding. When a user submits a query like "I need to analyze customer sentiment from social media posts," the LLM extracts key concepts such as sentiment analysis, social media, and text processing. It identifies required capabilities like natural language processing and data analysis. It suggests the appropriate domain, perhaps "data science" or "text analytics." This structured understanding of the query enables much more accurate matching than simple keyword search.


The Rating System: Crowdsourcing Quality Signals


User ratings provide essential quality signals that help others evaluate MCP servers. However, implementing a fair and abuse-resistant rating system requires careful design. The system must prevent users from rating the same server multiple times, handle rating updates gracefully, and aggregate ratings in a way that reflects both quality and confidence.


Each rating consists of a numerical score from one to five stars and an optional text review. The system tracks which user submitted each rating to enforce the one-rating-per-user-per-server rule. When calculating average ratings, the system uses a weighted average that accounts for the number of ratings, preventing servers with only one or two five-star ratings from appearing more highly rated than servers with hundreds of four-star ratings.


class RatingResult:

    """

    Encapsulates the result of a rating submission.

    """

    

    def __init__(self, success: bool, rating_id: Optional[str] = None,

                 action: str = "", error: str = ""):

        self.success = success

        self.rating_id = rating_id

        self.action = action

        self.error = error


class RatingService:

    """

    Manages user ratings for MCP servers, including submission,

    validation, aggregation, and anti-abuse measures.

    """

    

    def __init__(self, storage, user_service):

        self.storage = storage

        self.user_service = user_service

    

    def submit_rating(self, user_id: str, registration_id: str,

                     stars: int, review_text: Optional[str] = None) -> RatingResult:

        """

        Submit or update a rating for an MCP server.

        

        Enforces the rule that each user can rate each server only once.

        If the user has already rated this server, their previous rating

        is updated rather than creating a duplicate.

        

        Args:

            user_id: Identifier of the user submitting the rating

            registration_id: Server being rated

            stars: Rating value from 1 to 5

            review_text: Optional text review

            

        Returns:

            RatingResult indicating success or failure with details

        """

        

        # Validate star rating is in acceptable range

        if not 1 <= stars <= 5:

            return RatingResult(

                success=False,

                error="Star rating must be between 1 and 5"

            )

        

        # Verify user is authenticated and authorized

        if not self.user_service.is_authenticated(user_id):

            return RatingResult(

                success=False,

                error="User must be authenticated to submit ratings"

            )

        

        # Check if server exists and is active

        server = self.storage.get_server(registration_id)

        if not server or server.status != "active":

            return RatingResult(

                success=False,

                error="Server not found or not active"

            )

        

        # Check for existing rating from this user

        existing_rating = self._get_user_rating_internal(user_id, registration_id)

        

        rating_id = None

        action = ""

        

        try:

            with self.storage.pg_conn.cursor() as cursor:

                if existing_rating:

                    # Update existing rating

                    cursor.execute("""

                        UPDATE server_ratings

                        SET stars = %s,

                            review_text = %s,

                            updated_at = CURRENT_TIMESTAMP

                        WHERE user_id = %s AND registration_id = %s

                        RETURNING rating_id

                    """, (stars, review_text, user_id, registration_id))

                    

                    result = cursor.fetchone()

                    rating_id = str(result[0]) if result else None

                    action = "updated"

                else:

                    # Insert new rating

                    cursor.execute("""

                        INSERT INTO server_ratings (

                            user_id, registration_id, stars, review_text,

                            created_at, updated_at

                        ) VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)

                        RETURNING rating_id

                    """, (user_id, registration_id, stars, review_text))

                    

                    result = cursor.fetchone()

                    rating_id = str(result[0]) if result else None

                    action = "created"

                

                self.storage.pg_conn.commit()

        except Exception as e:

            self.storage.pg_conn.rollback()

            return RatingResult(

                success=False,

                error=f"Database error: {str(e)}"

            )

        

        # Recalculate aggregate rating for this server

        self._update_aggregate_rating(registration_id)

        

        return RatingResult(

            success=True,

            rating_id=rating_id,

            action=action

        )

    

    def _get_user_rating_internal(self, user_id: str,

                                  registration_id: str) -> Optional[Dict[str, Any]]:

        """

        Internal method to check if user has already rated a server.

        """

        try:

            with self.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT rating_id, stars, review_text, created_at

                    FROM server_ratings

                    WHERE user_id = %s AND registration_id = %s

                """, (user_id, registration_id))

                

                row = cursor.fetchone()

                if row:

                    return {

                        "rating_id": str(row[0]),

                        "stars": row[1],

                        "review_text": row[2],

                        "created_at": row[3]

                    }

                return None

        except Exception as e:

            print(f"Error checking existing rating: {str(e)}")

            return None

    

    def get_user_rating(self, user_id: str,

                       registration_id: str) -> Optional[Dict[str, Any]]:

        """

        Public method to retrieve a user's rating for a server.

        """

        return self._get_user_rating_internal(user_id, registration_id)

    

    def _update_aggregate_rating(self, registration_id: str) -> None:

        """

        Recalculate the aggregate rating statistics for a server.

        

        Uses a Bayesian average to prevent servers with few ratings

        from dominating rankings. The formula adds virtual ratings

        at the global average to smooth out extremes.

        """

        

        try:

            with self.storage.pg_conn.cursor() as cursor:

                # Get all ratings for this server

                cursor.execute("""

                    SELECT stars FROM server_ratings

                    WHERE registration_id = %s

                """, (registration_id,))

                

                ratings = [row[0] for row in cursor.fetchall()]

                

                if not ratings or len(ratings) == 0:

                    # No ratings yet

                    avg_rating = 0.0

                    rating_count = 0

                else:

                    # Get global average rating across all servers

                    cursor.execute("""

                        SELECT AVG(average_rating) FROM mcp_servers

                        WHERE rating_count > 0

                    """)

                    

                    global_avg_row = cursor.fetchone()

                    global_avg = float(global_avg_row[0]) if global_avg_row[0] else 3.0

                    

                    # Bayesian average with confidence parameter

                    confidence = 10

                    bayesian_avg = (

                        (confidence * global_avg + sum(ratings)) /

                        (confidence + len(ratings))

                    )

                    

                    avg_rating = bayesian_avg

                    rating_count = len(ratings)

                

                # Update server record with new aggregate statistics

                cursor.execute("""

                    UPDATE mcp_servers

                    SET average_rating = %s,

                        rating_count = %s,

                        last_updated = CURRENT_TIMESTAMP

                    WHERE registration_id = %s

                """, (avg_rating, rating_count, registration_id))

                

                self.storage.pg_conn.commit()

        except Exception as e:

            self.storage.pg_conn.rollback()

            print(f"Error updating aggregate rating: {str(e)}")

    

    def get_server_ratings(self, registration_id: str,

                          limit: int = 20) -> List[Dict[str, Any]]:

        """

        Retrieve recent ratings and reviews for a server.

        

        Args:

            registration_id: The server to get ratings for

            limit: Maximum number of ratings to return

            

        Returns:

            List of rating dictionaries with user info and review text

        """

        try:

            with self.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT r.rating_id, r.user_id, r.stars, r.review_text,

                           r.created_at, u.username

                    FROM server_ratings r

                    JOIN users u ON r.user_id = u.user_id

                    WHERE r.registration_id = %s

                    ORDER BY r.created_at DESC

                    LIMIT %s

                """, (registration_id, limit))

                

                return [

                    {

                        "rating_id": str(row[0]),

                        "user_id": str(row[1]),

                        "stars": row[2],

                        "review_text": row[3],

                        "created_at": row[4],

                        "username": row[5]

                    }

                    for row in cursor.fetchall()

                ]

        except Exception as e:

            print(f"Error retrieving ratings: {str(e)}")

            return []


The Bayesian average approach prevents gaming of the rating system. A server with one five-star rating will not appear above a server with a hundred four-star ratings. The confidence parameter determines how many virtual ratings at the global average are added to each server's actual ratings. This smoothing effect is strongest for servers with few ratings and diminishes as the number of real ratings increases.


The rating system also integrates with the search ranking algorithm. Servers with higher ratings receive a boost in search results, creating a positive feedback loop where quality servers become more discoverable, leading to more usage and potentially more ratings.


Domain Classification and Organization

Organizing MCP servers into domains serves multiple purposes. It enables browsing by category, helps users discover related servers, and provides context for search results. However, domain classification presents challenges. A single server might legitimately belong to multiple domains, and the boundaries between domains can be fuzzy.


Your MCP Hub addresses these challenges through a flexible classification system that allows both primary and secondary domain assignments. Each server must have one primary domain that represents its main purpose, but it can also be associated with multiple secondary domains. For example, a weather data API might have "environmental data" as its primary domain but also belong to "geospatial services" and "time series data" as secondary domains.


The domain taxonomy itself requires careful design. It must be comprehensive enough to cover the diverse range of MCP servers while remaining navigable and understandable. A hierarchical structure helps manage complexity:


from typing import Dict, List, Optional


class DomainTaxonomy:

    """

    Represents the hierarchical organization of domains within

    the MCP Hub. Supports both top-level categories and more

    specific subcategories.

    """

    

    def __init__(self):

        # Define the domain hierarchy

        self.taxonomy: Dict[str, Dict[str, Any]] = {

            "data_access": {

                "name": "Data Access and Integration",

                "description": "Servers that provide access to data sources",

                "subcategories": {

                    "databases": "Database connectivity and querying",

                    "apis": "External API integration",

                    "file_systems": "File and document access",

                    "streaming": "Real-time data streams"

                }

            },

            "data_processing": {

                "name": "Data Processing and Analysis",

                "description": "Servers that transform and analyze data",

                "subcategories": {

                    "analytics": "Statistical analysis and reporting",

                    "transformation": "Data transformation and ETL",

                    "machine_learning": "ML model inference and training",

                    "visualization": "Data visualization and charting"

                }

            },

            "communication": {

                "name": "Communication and Collaboration",

                "description": "Servers enabling communication capabilities",

                "subcategories": {

                    "messaging": "Chat and messaging platforms",

                    "email": "Email services",

                    "notifications": "Push notifications and alerts",

                    "video": "Video conferencing and streaming"

                }

            },

            "productivity": {

                "name": "Productivity and Automation",

                "description": "Servers that enhance productivity",

                "subcategories": {

                    "scheduling": "Calendar and scheduling",

                    "task_management": "Task and project management",

                    "document_processing": "Document creation and editing",

                    "workflow": "Workflow automation"

                }

            },

            "development": {

                "name": "Development Tools",

                "description": "Servers supporting software development",

                "subcategories": {

                    "version_control": "Git and version control",

                    "ci_cd": "Continuous integration and deployment",

                    "testing": "Testing and quality assurance",

                    "monitoring": "Application monitoring and logging"

                }

            }

        }

    

    def get_domain_info(self, domain_key: str) -> Optional[Dict[str, Any]]:

        """

        Retrieve information about a specific domain.

        

        Args:

            domain_key: String identifier for the domain

            

        Returns:

            Dictionary containing domain metadata or None if not found

        """

        

        # Check if this is a top-level domain

        if domain_key in self.taxonomy:

            return self.taxonomy[domain_key]

        

        # Check if it is a subcategory

        for top_level in self.taxonomy.values():

            if "subcategories" in top_level:

                if domain_key in top_level["subcategories"]:

                    return {

                        "name": domain_key,

                        "description": top_level["subcategories"][domain_key],

                        "parent": top_level["name"]

                    }

        

        return None

    

    def suggest_domain(self, server_description: str,

                      capabilities: List[str]) -> List[Dict[str, Any]]:

        """

        Use heuristics and keyword matching to suggest appropriate

        domains for a server based on its description and capabilities.

        

        This method provides initial suggestions that can be refined

        by the LLM-based classification system.

        

        Args:

            server_description: Text description of the server

            capabilities: List of capability names

            

        Returns:

            List of domain suggestions with confidence scores

        """

        

        description_lower = server_description.lower()

        suggestions: List[Dict[str, Any]] = []

        

        # Define keyword patterns for each domain

        domain_keywords = {

            "data_access": ["database", "query", "api", "fetch", "retrieve", "connect"],

            "data_processing": ["analyze", "transform", "process", "compute", "calculate"],

            "communication": ["message", "email", "chat", "notify", "send"],

            "productivity": ["schedule", "task", "calendar", "document", "organize"],

            "development": ["git", "deploy", "test", "monitor", "build", "debug"]

        }

        

        # Score each domain based on keyword matches

        for domain, keywords in domain_keywords.items():

            score = sum(

                1 for keyword in keywords 

                if keyword in description_lower

            )

            

            if score > 0:

                suggestions.append({

                    "domain": domain,

                    "confidence": score / len(keywords)

                })

        

        # Sort by confidence descending

        suggestions.sort(key=lambda x: x["confidence"], reverse=True)

        

        return suggestions

    

    def get_all_domains(self) -> List[str]:

        """

        Get a list of all top-level domain keys.

        """

        return list(self.taxonomy.keys())

    

    def get_subcategories(self, domain_key: str) -> Dict[str, str]:

        """

        Get all subcategories for a given domain.

        

        Args:

            domain_key: The top-level domain

            

        Returns:

            Dictionary mapping subcategory keys to descriptions

        """

        if domain_key in self.taxonomy:

            return self.taxonomy[domain_key].get("subcategories", {})

        return {}


The domain taxonomy provides structure while remaining flexible enough to accommodate new categories as the ecosystem evolves. The suggestion mechanism helps registrants choose appropriate domains, but the final decision remains with the registrant or can be refined by the LLM-based classification system.


When users browse by domain, they see servers organized by their primary domain, with options to filter by subcategories. The interface also shows related domains, helping users discover servers they might not have initially considered. 


For example, someone browsing data processing servers might benefit from seeing related data access servers that could provide input for their processing tasks.


The Web Interface: Making Discovery Accessible

The web interface serves as the primary interaction point for most users. It must be intuitive, responsive, and provide clear pathways to both registration and discovery. The landing page showcases recently registered servers, highly rated servers, and popular servers across different domains. This gives visitors an immediate sense of the ecosystem's breadth and highlights quality servers.


The registration page guides users through the process of adding their MCP server to the hub. It uses progressive disclosure to avoid overwhelming users with too many fields at once. 


Required information appears first, followed by optional but recommended fields, and finally advanced options for users who want fine-grained control over their server's metadata.


class WebInterface:

    """

    Handles HTTP requests and renders web pages for the MCP Hub.

    Implements the presentation layer of the clean architecture.

    """

    

    def __init__(self, search_service, registration_service, 

                 rating_service, domain_taxonomy):

        self.search_service = search_service

        self.registration_service = registration_service

        self.rating_service = rating_service

        self.domain_taxonomy = domain_taxonomy

    

    def render_landing_page(self, request) -> str:

        """

        Generate the landing page showing featured servers,

        recent additions, and domain categories.

        

        Args:

            request: HTTP request object

            

        Returns:

            HTML content for the landing page

        """

        

        # Fetch recently registered servers

        recent_servers = self._get_recent_servers(limit=10)

        

        # Fetch top-rated servers

        top_rated = self._get_top_rated_servers(limit=10)

        

        # Fetch most downloaded servers

        popular_servers = self._get_popular_servers(limit=10)

        

        # Get server counts by domain

        domain_stats = self._get_domain_statistics()

        

        # Render template with data

        return self._render_template("landing.html", {

            "recent_servers": recent_servers,

            "top_rated": top_rated,

            "popular_servers": popular_servers,

            "domain_stats": domain_stats,

            "total_servers": sum(domain_stats.values())

        })

    

    def _get_recent_servers(self, limit: int) -> List[Dict[str, Any]]:

        """

        Fetch recently registered servers.

        """

        try:

            with self.search_service.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT registration_id, server_name, server_url,

                           short_description, average_rating, registration_date

                    FROM mcp_servers

                    WHERE status = 'active'

                    ORDER BY registration_date DESC

                    LIMIT %s

                """, (limit,))

                

                return [

                    {

                        "registration_id": str(row[0]),

                        "name": row[1],

                        "url": row[2],

                        "description": row[3],

                        "rating": float(row[4]) if row[4] else 0.0,

                        "date": row[5]

                    }

                    for row in cursor.fetchall()

                ]

        except Exception as e:

            print(f"Error fetching recent servers: {str(e)}")

            return []

    

    def _get_top_rated_servers(self, limit: int) -> List[Dict[str, Any]]:

        """

        Fetch highest rated servers with minimum rating count.

        """

        try:

            with self.search_service.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT registration_id, server_name, server_url,

                           short_description, average_rating, rating_count

                    FROM mcp_servers

                    WHERE status = 'active' AND rating_count >= 5

                    ORDER BY average_rating DESC, rating_count DESC

                    LIMIT %s

                """, (limit,))

                

                return [

                    {

                        "registration_id": str(row[0]),

                        "name": row[1],

                        "url": row[2],

                        "description": row[3],

                        "rating": float(row[4]) if row[4] else 0.0,

                        "rating_count": row[5]

                    }

                    for row in cursor.fetchall()

                ]

        except Exception as e:

            print(f"Error fetching top rated servers: {str(e)}")

            return []

    

    def _get_popular_servers(self, limit: int) -> List[Dict[str, Any]]:

        """

        Fetch most downloaded servers.

        """

        try:

            with self.search_service.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT registration_id, server_name, server_url,

                           short_description, total_downloads, average_rating

                    FROM mcp_servers

                    WHERE status = 'active'

                    ORDER BY total_downloads DESC

                    LIMIT %s

                """, (limit,))

                

                return [

                    {

                        "registration_id": str(row[0]),

                        "name": row[1],

                        "url": row[2],

                        "description": row[3],

                        "downloads": row[4],

                        "rating": float(row[5]) if row[5] else 0.0

                    }

                    for row in cursor.fetchall()

                ]

        except Exception as e:

            print(f"Error fetching popular servers: {str(e)}")

            return []

    

    def _get_domain_statistics(self) -> Dict[str, int]:

        """

        Get count of servers in each domain.

        """

        try:

            with self.search_service.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT primary_domain, COUNT(*) as server_count

                    FROM mcp_servers

                    WHERE status = 'active'

                    GROUP BY primary_domain

                """)

                

                return {row[0]: row[1] for row in cursor.fetchall()}

        except Exception as e:

            print(f"Error fetching domain statistics: {str(e)}")

            return {}

    

    def render_search_page(self, request) -> str:

        """

        Handle search requests and display results.

        

        Supports multiple search modes and provides filtering

        and sorting options.

        

        Args:

            request: HTTP request object with query parameters

            

        Returns:

            HTML content for search results page

        """

        

        query = request.get_parameter("q", "")

        search_mode = request.get_parameter("mode", "auto")

        domain_filter = request.get_parameter("domain", None)

        sort_by = request.get_parameter("sort", "relevance")

        

        if not query and not domain_filter:

            # Show search interface without results

            return self._render_template("search.html", {

                "domains": self.domain_taxonomy.taxonomy,

                "query": "",

                "results": None

            })

        

        # Execute search

        results = self.search_service.search(

            query=query if query else domain_filter,

            search_mode=search_mode if query else "domain",

            domain_filter=domain_filter

        )

        

        # Apply sorting if not using relevance

        if sort_by == "rating":

            results.results.sort(key=lambda x: x.get("rating", 0), reverse=True)

        elif sort_by == "downloads":

            results.results.sort(key=lambda x: x.get("downloads", 0), reverse=True)

        elif sort_by == "recent":

            # Would need to fetch registration dates for this

            pass

        

        return self._render_template("search.html", {

            "query": query,

            "results": results,

            "domains": self.domain_taxonomy.taxonomy,

            "current_domain": domain_filter,

            "current_sort": sort_by

        })

    

    def render_server_detail_page(self, request, registration_id: str) -> str:

        """

        Display detailed information about a specific MCP server.

        

        Includes full description, capabilities, usage examples,

        ratings, and related servers.

        

        Args:

            request: HTTP request object

            registration_id: ID of server to display

            

        Returns:

            HTML content for server detail page

        """

        

        # Fetch server details

        server = self.search_service.storage.get_server(registration_id)

        

        if not server:

            return self._render_error_page(404, "Server not found")

        

        # Fetch ratings and reviews

        ratings = self.rating_service.get_server_ratings(

            registration_id,

            limit=20

        )

        

        # Find related servers using graph relationships

        related_servers = self._find_related_servers(registration_id, limit=5)

        

        # Check if current user has rated this server

        user_rating = None

        if hasattr(request, 'user') and request.user:

            user_rating = self.rating_service.get_user_rating(

                request.user.id,

                registration_id

            )

        

        return self._render_template("server_detail.html", {

            "server": server,

            "ratings": ratings,

            "related_servers": related_servers,

            "user_rating": user_rating,

            "can_rate": hasattr(request, 'user') and request.user is not None

        })

    

    def _find_related_servers(self, registration_id: str,

                             limit: int) -> List[Dict[str, Any]]:

        """

        Find servers related to the given server based on shared

        capabilities, domains, or tags.

        """

        try:

            with self.search_service.graph_db.session() as session:

                result = session.run("""

                    MATCH (s1:MCPServer {registration_id: $reg_id})

                    MATCH (s1)-[:PROVIDES]->(c:Capability)<-[:PROVIDES]-(s2:MCPServer)

                    WHERE s1 <> s2

                    WITH s2, count(c) as shared_capabilities

                    RETURN s2.registration_id, s2.name, shared_capabilities

                    ORDER BY shared_capabilities DESC

                    LIMIT $limit

                """, {"reg_id": registration_id, "limit": limit})

                

                related = []

                for record in result:

                    server_id = record["s2.registration_id"]

                    server = self.search_service.storage.get_server(server_id)

                    if server:

                        related.append({

                            "registration_id": server_id,

                            "name": record["s2.name"],

                            "description": server.short_description,

                            "rating": server.average_rating

                        })

                

                return related

        except Exception as e:

            print(f"Error finding related servers: {str(e)}")

            return []

    

    def render_registration_page(self, request) -> str:

        """

        Display the server registration form.

        

        Provides guidance and validation to help users submit

        complete and accurate registrations.

        

        Args:

            request: HTTP request object

            

        Returns:

            HTML content for registration page

        """

        

        if request.method == "POST":

            # Process registration submission

            registration_data = self._extract_registration_data(request)

            result = self.registration_service.register_server(

                registration_data

            )

            

            if result.success:

                # Redirect to the newly registered server's detail page

                return self._redirect(

                    f"/servers/{result.registration_id}"

                )

            else:

                # Show form again with error messages

                return self._render_template("register.html", {

                    "errors": result.errors,

                    "form_data": registration_data,

                    "domains": self.domain_taxonomy.taxonomy

                })

        else:

            # Show empty registration form

            return self._render_template("register.html", {

                "domains": self.domain_taxonomy.taxonomy,

                "errors": [],

                "form_data": {}

            })

    

    def _extract_registration_data(self, request) -> Dict[str, Any]:

        """

        Extract registration data from HTTP request form data.

        """

        return {

            "server_name": request.get_form_value("server_name", ""),

            "server_version": request.get_form_value("server_version", ""),

            "server_url": request.get_form_value("server_url", ""),

            "short_description": request.get_form_value("short_description", ""),

            "detailed_description": request.get_form_value("detailed_description", ""),

            "primary_domain": request.get_form_value("primary_domain", ""),

            "supported_capabilities": request.get_form_list("capabilities"),

            "tags": request.get_form_list("tags"),

            "authentication_method": request.get_form_value("auth_method", ""),

            "protocol_version": request.get_form_value("protocol_version", "1.0")

        }

    

    def _render_template(self, template_name: str,

                        context: Dict[str, Any]) -> str:

        """

        Render an HTML template with the given context.

        This would integrate with your templating engine.

        """

        # Implementation depends on your templating system

        pass

    

    def _render_error_page(self, status_code: int, message: str) -> str:

        """

        Render an error page with appropriate status code.

        """

        return self._render_template("error.html", {

            "status_code": status_code,

            "message": message

        })

    

    def _redirect(self, url: str) -> str:

        """

        Generate a redirect response to the given URL.

        """

        # Implementation depends on your web framework

        pass


The web interface implementation separates presentation logic from business logic. The web layer calls service methods to perform operations and retrieve data, then renders that data into HTML templates. This separation makes the code more testable and allows the business logic to be reused in other contexts, such as the API layer.


The server detail page deserves special attention as it serves as the primary information source for users evaluating whether to use a particular MCP server. It displays comprehensive information including the full description, supported capabilities, authentication requirements, usage examples, and user ratings. The page also shows related servers, helping users discover alternatives or complementary servers.


The API Layer: Enabling Programmatic Access

While the web interface serves human users, the API layer enables programmatic access for software systems. Development tools, continuous integration pipelines, and other automated systems need to interact with your MCP Hub without human intervention. The API provides RESTful endpoints for all major operations: registration, search, rating, and retrieval.


The API design follows REST principles, using appropriate HTTP methods for different operations and returning structured JSON responses. Authentication uses API keys for programmatic access, allowing systems to act on behalf of registered users or organizations.


import json

from typing import Any


class APIController:

    """

    Implements RESTful API endpoints for programmatic access

    to MCP Hub functionality.

    """

    

    def __init__(self, search_service, registration_service,

                 rating_service, auth_service):

        self.search_service = search_service

        self.registration_service = registration_service

        self.rating_service = rating_service

        self.auth_service = auth_service

    

    def handle_search_request(self, request) -> str:

        """

        API endpoint for searching MCP servers.

        

        GET /api/v1/search?q=query&mode=semantic&domain=data_access

        

        Returns JSON array of matching servers with metadata.

        

        Args:

            request: HTTP request object

            

        Returns:

            JSON response string

        """

        

        query = request.get_parameter("q")

        mode = request.get_parameter("mode", "auto")

        domain = request.get_parameter("domain")

        limit_str = request.get_parameter("limit", "20")

        

        try:

            limit = int(limit_str)

        except ValueError:

            return self._json_error(400, "Invalid limit parameter")

        

        if not query and not domain:

            return self._json_error(

                400,

                "Either query or domain parameter is required"

            )

        

        try:

            results = self.search_service.search(

                query=query if query else domain,

                search_mode=mode if query else "domain",

                domain_filter=domain,

                limit=limit

            )

            

            # Convert results to JSON-serializable format

            response_data = {

                "query": query,

                "mode": mode,

                "total_results": results.total_count,

                "servers": [

                    {

                        "registration_id": str(server.get("registration_id", "")),

                        "name": server.get("name", ""),

                        "version": server.get("version", ""),

                        "url": server.get("url", ""),

                        "description": server.get("description", ""),

                        "domain": server.get("domain", ""),

                        "rating": float(server.get("rating", 0.0)),

                        "rating_count": server.get("rating_count", 0),

                        "capabilities": server.get("capabilities", [])

                    }

                    for server in results.servers

                ]

            }

            

            return self._json_response(200, response_data)

        except Exception as e:

            return self._json_error(500, f"Search failed: {str(e)}")

    

    def handle_registration_request(self, request) -> str:

        """

        API endpoint for registering new MCP servers.

        

        POST /api/v1/servers

        Content-Type: application/json

        

        Requires authentication via API key.

        

        Args:

            request: HTTP request object

            

        Returns:

            JSON response string

        """

        

        # Verify authentication

        api_key = request.get_header("X-API-Key")

        user = self.auth_service.authenticate_api_key(api_key)

        

        if not user:

            return self._json_error(401, "Invalid or missing API key")

        

        # Parse request body

        try:

            registration_data = json.loads(request.get_body())

        except (ValueError, json.JSONDecodeError) as e:

            return self._json_error(400, f"Invalid JSON: {str(e)}")

        

        # Add authenticated user information

        registration_data["registrant_email"] = user.get("email", "")

        registration_data["registrant_name"] = user.get("name", "")

        

        # Process registration

        try:

            result = self.registration_service.register_server(

                registration_data

            )

            

            if result.success:

                response_data = {

                    "success": True,

                    "registration_id": result.registration_id,

                    "message": result.message,

                    "server_url": f"/api/v1/servers/{result.registration_id}"

                }

                return self._json_response(201, response_data)

            else:

                return self._json_error(400, result.errors)

        except Exception as e:

            return self._json_error(500, f"Registration failed: {str(e)}")

    

    def handle_get_server_request(self, request, registration_id: str) -> str:

        """

        API endpoint for retrieving detailed server information.

        

        GET /api/v1/servers/{registration_id}

        

        Returns complete server metadata and statistics.

        

        Args:

            request: HTTP request object

            registration_id: Server ID to retrieve

            

        Returns:

            JSON response string

        """

        

        try:

            server = self.search_service.storage.get_server(registration_id)

            

            if not server:

                return self._json_error(404, "Server not found")

            

            response_data = {

                "registration_id": str(server.registration_id),

                "name": server.server_name,

                "version": server.server_version,

                "url": server.server_url,

                "registrant": {

                    "name": server.registrant_name,

                    "organization": server.organization

                },

                "description": {

                    "short": server.short_description,

                    "detailed": server.detailed_description

                },

                "classification": {

                    "primary_domain": server.primary_domain,

                    "secondary_domains": server.secondary_domains,

                    "tags": server.tags

                },

                "capabilities": server.supported_capabilities,

                "authentication": server.authentication_method,

                "statistics": {

                    "average_rating": float(server.average_rating),

                    "rating_count": server.rating_count,

                    "total_downloads": server.total_downloads

                },

                "metadata": {

                    "registration_date": server.registration_date.isoformat(),

                    "last_updated": server.last_updated.isoformat(),

                    "status": server.status

                }

            }

            

            return self._json_response(200, response_data)

        except Exception as e:

            return self._json_error(500, f"Retrieval failed: {str(e)}")

    

    def handle_submit_rating_request(self, request, registration_id: str) -> str:

        """

        API endpoint for submitting server ratings.

        

        POST /api/v1/servers/{registration_id}/ratings

        Content-Type: application/json

        

        Body: {"stars": 5, "review": "Excellent server!"}

        

        Args:

            request: HTTP request object

            registration_id: Server to rate

            

        Returns:

            JSON response string

        """

        

        # Verify authentication

        api_key = request.get_header("X-API-Key")

        user = self.auth_service.authenticate_api_key(api_key)

        

        if not user:

            return self._json_error(401, "Invalid or missing API key")

        

        # Parse request body

        try:

            rating_data = json.loads(request.get_body())

        except (ValueError, json.JSONDecodeError) as e:

            return self._json_error(400, f"Invalid JSON: {str(e)}")

        

        stars = rating_data.get("stars")

        review = rating_data.get("review")

        

        if stars is None:

            return self._json_error(400, "Stars rating is required")

        

        # Submit rating

        try:

            result = self.rating_service.submit_rating(

                user_id=user.get("id", ""),

                registration_id=registration_id,

                stars=int(stars),

                review_text=review

            )

            

            if result.success:

                response_data = {

                    "success": True,

                    "rating_id": result.rating_id,

                    "action": result.action

                }

                return self._json_response(200, response_data)

            else:

                return self._json_error(400, result.error)

        except Exception as e:

            return self._json_error(500, f"Rating submission failed: {str(e)}")

    

    def _json_response(self, status_code: int, data: Any) -> str:

        """

        Create a JSON response with the given status code and data.

        

        Args:

            status_code: HTTP status code

            data: Data to serialize to JSON

            

        Returns:

            JSON response string

        """

        # This would set appropriate headers and status code

        # Implementation depends on your web framework

        return json.dumps(data, indent=2)

    

    def _json_error(self, status_code: int, error_message: Any) -> str:

        """

        Create a JSON error response.

        

        Args:

            status_code: HTTP error status code

            error_message: Error message or list of errors

            

        Returns:

            JSON error response string

        """

        if isinstance(error_message, list):

            error_data = {"success": False, "errors": error_message}

        else:

            error_data = {"success": False, "error": str(error_message)}

        

        return json.dumps(error_data, indent=2)


The API design includes versioning in the URL path, allowing the API to evolve without breaking existing integrations. All endpoints return consistent JSON structures with clear success and error responses. Rate limiting and authentication ensure that the API cannot be abused while remaining accessible for legitimate use cases.


Documentation for the API should include detailed examples showing how to perform common operations. For instance, a continuous integration system might automatically register a new version of an MCP server after successful tests:


import requests

import json

import os


def register_mcp_server_from_ci():

    """

    Example function showing how a CI/CD pipeline might

    automatically register an MCP server after building

    and testing it.

    

    This function would be called from your CI/CD configuration

    after successful build and test stages.

    """

    

    # Configuration from environment variables

    api_key = os.environ.get("MCP_HUB_API_KEY")

    server_url = os.environ.get("MCP_SERVER_URL")

    server_version = os.environ.get("BUILD_VERSION")

    hub_url = os.environ.get("MCP_HUB_URL", "https://mcphub.example.com")

    

    if not all([api_key, server_url, server_version]):

        raise ValueError("Missing required environment variables")

    

    # Prepare registration data

    registration_data = {

        "server_name": "WeatherDataAPI",

        "server_version": server_version,

        "server_url": server_url,

        "protocol_version": "1.0",

        "authentication_method": "api_key",

        "short_description": "Real-time weather data from global sources",

        "detailed_description": """

This MCP server provides access to real-time weather data

from multiple global sources. It supports queries by location,

historical data retrieval, and weather forecasting.


Key features include current conditions for any location,

seven-day forecasts, historical weather data, severe weather

alerts, and multiple data sources for reliability.

        """.strip(),

        "supported_capabilities": [

            "weather_current",

            "weather_forecast",

            "weather_historical",

            "weather_alerts"

        ],

        "primary_domain": "data_access",

        "secondary_domains": ["environmental_data", "geospatial"],

        "tags": ["weather", "climate", "forecasting", "real-time"]

    }

    

    # Submit registration

    response = requests.post(

        f"{hub_url}/api/v1/servers",

        headers={

            "X-API-Key": api_key,

            "Content-Type": "application/json"

        },

        data=json.dumps(registration_data)

    )

    

    if response.status_code == 201:

        result = response.json()

        print(f"Successfully registered server: {result['registration_id']}")

        print(f"Server URL: {result['server_url']}")

        return result

    else:

        print(f"Registration failed with status {response.status_code}")

        print(f"Error: {response.text}")

        raise Exception("Failed to register MCP server")


if __name__ == "__main__":

    register_mcp_server_from_ci()


This example demonstrates how the API enables automation while maintaining data quality through validation and authentication requirements. The CI/CD integration ensures that every successful build results in an updated registry entry, keeping users informed about the latest versions.


Security and Governance Considerations

Operating a registry for MCP servers introduces significant security and governance challenges. Your hub must protect against malicious registrations, prevent abuse of the rating system, ensure that registered servers meet minimum quality standards, and handle takedown requests for servers that violate policies or contain malicious code.


Authentication and authorization form the first line of defense. All registration and rating operations require authenticated users. The system maintains an audit log of all operations, recording who performed each action and when. This audit trail enables investigation of suspicious activity and provides accountability.


Server validation includes both automated and manual components. Automated validation checks that URLs are reachable, that the server responds to MCP protocol requests, and that the provided metadata is complete and properly formatted. Manual review by hub administrators can verify that servers meet quality standards and do not violate policies before they become publicly visible.


import requests

import re

from urllib.parse import urlparse


class ValidationResult:

    """

    Encapsulates validation results.

    """

    

    def __init__(self, is_valid: bool = True,

                 errors: Optional[List[str]] = None,

                 warnings: Optional[List[str]] = None):

        self.is_valid = is_valid

        self.errors = errors or []

        self.warnings = warnings or []


class SecurityService:

    """

    Handles security-related operations including authentication,

    authorization, validation, and abuse prevention.

    """

    

    def __init__(self, storage, audit_log):

        self.storage = storage

        self.audit_log = audit_log

    

    def validate_server_registration(self,

                                    registration_data: Dict[str, Any]) -> ValidationResult:

        """

        Perform security validation on a server registration.

        

        Checks include URL accessibility and protocol compliance,

        metadata completeness and format, duplicate detection,

        and malicious content screening.

        

        Args:

            registration_data: Dictionary containing registration fields

            

        Returns:

            ValidationResult with validation status and messages

        """

        

        validation_results = ValidationResult()

        

        # Verify required fields are present

        required_fields = [

            "server_name", "server_url", "short_description",

            "detailed_description", "primary_domain"

        ]

        

        for field in required_fields:

            if not registration_data.get(field):

                validation_results.is_valid = False

                validation_results.errors.append(

                    f"Required field missing: {field}"

                )

        

        # Validate server URL format

        server_url = registration_data.get("server_url", "")

        if server_url:

            if not self._is_valid_url(server_url):

                validation_results.is_valid = False

                validation_results.errors.append(

                    f"Invalid URL format: {server_url}"

                )

            else:

                # Verify server URL is accessible

                if not self._verify_url_accessible(server_url):

                    validation_results.is_valid = False

                    validation_results.errors.append(

                        f"Server URL {server_url} is not accessible"

                    )

                else:

                    # Check if server responds to MCP protocol

                    if not self._verify_mcp_protocol(server_url):

                        validation_results.warnings.append(

                            "Server may not respond to MCP protocol requests"

                        )

        

        # Check for duplicate registrations

        if server_url:

            existing = self._find_server_by_url(server_url)

            if existing:

                validation_results.warnings.append(

                    f"A server at this URL is already registered: {existing.get('name', 'Unknown')}"

                )

        

        # Scan description for prohibited content

        description = registration_data.get("detailed_description", "")

        if description and self._contains_prohibited_content(description):

            validation_results.is_valid = False

            validation_results.errors.append(

                "Description contains prohibited content"

            )

        

        # Validate email format

        email = registration_data.get("registrant_email", "")

        if email and not self._is_valid_email(email):

            validation_results.is_valid = False

            validation_results.errors.append(

                f"Invalid email format: {email}"

            )

        

        return validation_results

    

    def _is_valid_url(self, url: str) -> bool:

        """

        Check if URL has valid format.

        """

        try:

            result = urlparse(url)

            return all([result.scheme, result.netloc])

        except Exception:

            return False

    

    def _is_valid_email(self, email: str) -> bool:

        """

        Check if email has valid format.

        """

        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

        return re.match(pattern, email) is not None

    

    def _verify_url_accessible(self, url: str) -> bool:

        """

        Check if URL is accessible via HTTP request.

        """

        try:

            response = requests.head(url, timeout=5, allow_redirects=True)

            return response.status_code < 500

        except requests.RequestException:

            return False

    

    def _verify_mcp_protocol(self, url: str) -> bool:

        """

        Check if server responds to MCP protocol requests.

        This is a simplified check - real implementation would

        test actual MCP protocol compliance.

        """

        try:

            # Send a test MCP request

            response = requests.get(url, timeout=5)

            # Check for MCP-specific headers or response format

            return response.status_code == 200

        except requests.RequestException:

            return False

    

    def _find_server_by_url(self, url: str) -> Optional[Dict[str, Any]]:

        """

        Check if a server with this URL already exists.

        """

        try:

            with self.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT registration_id, server_name

                    FROM mcp_servers

                    WHERE server_url = %s

                """, (url,))

                

                row = cursor.fetchone()

                if row:

                    return {

                        "registration_id": str(row[0]),

                        "name": row[1]

                    }

                return None

        except Exception as e:

            print(f"Error checking for duplicate: {str(e)}")

            return None

    

    def _contains_prohibited_content(self, text: str) -> bool:

        """

        Scan text for prohibited content patterns.

        This is a simplified implementation - production systems

        would use more sophisticated content filtering.

        """

        prohibited_patterns = [

            r'<script[^>]*>',

            r'javascript:',

            r'onclick=',

            r'onerror='

        ]

        

        text_lower = text.lower()

        for pattern in prohibited_patterns:

            if re.search(pattern, text_lower):

                return True

        

        return False

    

    def detect_rating_abuse(self, user_id: str,

                           registration_id: str) -> Dict[str, Any]:

        """

        Analyze rating patterns to detect potential abuse.

        

        Looks for suspicious patterns such as rapid submission

        of multiple ratings, coordinated rating campaigns, and

        ratings from newly created accounts.

        

        Args:

            user_id: User submitting the rating

            registration_id: Server being rated

            

        Returns:

            Dictionary with suspicious flag and reason

        """

        

        # Check user account age

        user = self._get_user(user_id)

        if user:

            account_age_days = (

                datetime.now() - user.get("created_at", datetime.now())

            ).days

            

            if account_age_days < 7:

                self.audit_log.log_warning(

                    "rating_from_new_account",

                    user_id=user_id,

                    registration_id=registration_id,

                    account_age_days=account_age_days

                )

        

        # Check for rapid rating submissions

        recent_ratings = self._get_user_recent_ratings(user_id, hours=24)

        

        if len(recent_ratings) > 10:

            self.audit_log.log_warning(

                "rapid_rating_submission",

                user_id=user_id,

                rating_count=len(recent_ratings)

            )

            

            return {

                "suspicious": True,

                "reason": "Unusually high rating activity"

            }

        

        # Check for coordinated rating patterns

        server_recent_ratings = self._get_server_recent_ratings(

            registration_id,

            hours=24

        )

        

        if len(server_recent_ratings) > 20:

            # Analyze rating distribution

            rating_values = [r.get("stars", 0) for r in server_recent_ratings]

            if rating_values:

                avg_rating = sum(rating_values) / len(rating_values)

                

                # If all recent ratings are 5 stars, flag as suspicious

                if avg_rating > 4.9:

                    self.audit_log.log_warning(

                        "coordinated_rating_campaign",

                        registration_id=registration_id,

                        recent_rating_count=len(server_recent_ratings)

                    )

                    

                    return {

                        "suspicious": True,

                        "reason": "Potential coordinated rating campaign"

                    }

        

        return {"suspicious": False}

    

    def _get_user(self, user_id: str) -> Optional[Dict[str, Any]]:

        """

        Retrieve user information.

        """

        try:

            with self.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT user_id, username, email, created_at

                    FROM users

                    WHERE user_id = %s

                """, (user_id,))

                

                row = cursor.fetchone()

                if row:

                    return {

                        "id": str(row[0]),

                        "username": row[1],

                        "email": row[2],

                        "created_at": row[3]

                    }

                return None

        except Exception as e:

            print(f"Error retrieving user: {str(e)}")

            return None

    

    def _get_user_recent_ratings(self, user_id: str,

                                 hours: int) -> List[Dict[str, Any]]:

        """

        Get ratings submitted by user in recent time period.

        """

        try:

            with self.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT rating_id, registration_id, stars, created_at

                    FROM server_ratings

                    WHERE user_id = %s

                      AND created_at > NOW() - INTERVAL '%s hours'

                """, (user_id, hours))

                

                return [

                    {

                        "rating_id": str(row[0]),

                        "registration_id": str(row[1]),

                        "stars": row[2],

                        "created_at": row[3]

                    }

                    for row in cursor.fetchall()

                ]

        except Exception as e:

            print(f"Error retrieving recent ratings: {str(e)}")

            return []

    

    def _get_server_recent_ratings(self, registration_id: str,

                                   hours: int) -> List[Dict[str, Any]]:

        """

        Get ratings for a server in recent time period.

        """

        try:

            with self.storage.pg_conn.cursor() as cursor:

                cursor.execute("""

                    SELECT rating_id, user_id, stars, created_at

                    FROM server_ratings

                    WHERE registration_id = %s

                      AND created_at > NOW() - INTERVAL '%s hours'

                """, (registration_id, hours))

                

                return [

                    {

                        "rating_id": str(row[0]),

                        "user_id": str(row[1]),

                        "stars": row[2],

                        "created_at": row[3]

                    }

                    for row in cursor.fetchall()

                ]

        except Exception as e:

            print(f"Error retrieving server recent ratings: {str(e)}")

            return []


The security service demonstrates defense in depth, using multiple validation layers to protect the hub's integrity. URL verification ensures that registered servers actually exist and respond to requests. Protocol validation confirms that servers implement the MCP specification correctly. Content screening prevents malicious or inappropriate descriptions. Abuse detection identifies suspicious rating patterns that might indicate manipulation attempts.


Governance policies define acceptable use of your hub and consequences for violations. Servers that become unavailable, violate the terms of service, or receive consistent negative feedback can be marked as deprecated or removed from the registry. The system maintains transparency by logging all such actions and providing appeals processes for affected registrants.


Use Cases and Workflows

Understanding how different users interact with your MCP Hub helps clarify its value proposition. Consider several common scenarios that illustrate the hub's capabilities and benefits.


A developer building a data analysis application needs to access weather data. They visit your MCP Hub and search for "weather data API" using the natural language search. The system returns several relevant servers, ranked by rating and relevance. The developer reviews the top results, reading descriptions and checking ratings. They select a highly-rated weather server, copy its URL, and integrate it into their application. After using the server successfully, they return to submit a five-star rating with a review describing their positive experience.


A DevOps engineer setting up continuous integration for an MCP server project configures the pipeline to automatically register new versions with your hub. Each time the build succeeds and tests pass, the pipeline calls the registration API with updated version information and the deployment URL. This ensures that users always have access to the latest stable version without manual intervention.


A data scientist exploring available tools browses your hub by domain, selecting "data processing" and then "machine learning" as a subcategory. They see a list of ML-related MCP servers sorted by rating. They discover a server for sentiment analysis that they had not previously known about. Reading the detailed description and example queries, they realize this server could enhance their current project. They integrate it and later rate it based on their experience.


An organization managing multiple internal MCP servers uses your hub as a central catalog. They register all their servers, marking them with organizational tags. Employees can search the hub to discover internal tools, reducing duplication of effort and improving knowledge sharing across teams. The rating system helps identify which internal servers are most useful and which might need improvement.


These scenarios demonstrate how your hub serves different user types and use cases, from discovery and evaluation to integration and feedback. The combination of web interface and API ensures that both human users and 

automated systems can benefit from the centralized registry.


Future Extensions and Evolution

Your MCP Hub provides a foundation that can evolve in numerous directions. Several extensions would enhance its value and capabilities.


Dependency management could track relationships between servers. Some MCP servers might depend on or complement others. Your hub could model these dependencies, helping users discover complete solution stacks rather than individual servers. For example, a data visualization server might work best with specific data processing servers, and the hub could recommend these combinations. The graph database already in place provides an excellent foundation for modeling these dependency relationships.


Version management could become more sophisticated, allowing servers to register multiple versions simultaneously. Users could specify version constraints when searching, ensuring compatibility with their systems. Your hub could track which versions are stable, which are deprecated, and which represent the latest development. The registration system could be extended to support version ranges and compatibility declarations.


Analytics and insights could help registrants understand how their servers are being used. Your hub could provide dashboards showing search impressions, detail page views, download counts, and rating trends over time. This feedback helps server maintainers improve their offerings and understand user needs. Privacy-preserving analytics ensure that individual user behavior remains confidential while providing valuable aggregate insights.


Integration with development tools could streamline workflows. IDE plugins could search your hub directly, allowing developers to discover and integrate MCP servers without leaving their development environment. Package managers could use your hub as a source for MCP server installations. Command-line tools could query the hub and automatically configure local environments.


Community features could foster collaboration and knowledge sharing. Discussion forums for each server would allow users to ask questions, share tips, and report issues. Your hub could host documentation, tutorials, and example code contributed by the community. A wiki-style knowledge base could accumulate best practices and integration patterns.


Quality badges could recognize exceptional servers. Automated testing could verify that servers meet quality standards, awarding badges for criteria like uptime, response time, documentation quality, and test coverage. These badges would help users identify reliable servers quickly. The validation system already includes basic quality checks that could be expanded into a comprehensive quality assurance framework.


Private sub-registries represent another powerful extension. Organizations often need to maintain private collections of MCP servers that should not be publicly visible. Your hub could support the creation of private sub-registries with their own access controls, domain taxonomies, and governance policies. Each sub-registry would function as an isolated namespace within the larger hub, with its own set of registered servers, users, and permissions.


The private sub-registry feature would work as follows. An organization creates a sub-registry with a unique identifier and configures access controls specifying which users can view, register, and rate servers within that sub-registry. The organization can define custom domain taxonomies specific to their industry or use case, extending or replacing the default taxonomy. Servers registered in a private sub-registry remain invisible to users outside the authorized group.


Search operations could span multiple sub-registries based on user permissions. A user with access to both the public registry and their organization's private sub-registry would see results from both when searching. The API would support sub-registry selection through request parameters or headers, allowing programmatic clients to specify which registries to query.


The storage layer would need extensions to support sub-registries. Each server registration would include a sub-registry identifier, with the public registry treated as the default. Database queries would filter by sub-registry based on user permissions. The graph database would maintain separate subgraphs for each sub-registry while potentially allowing cross-registry relationships for users with appropriate permissions.


Authentication and authorization become more complex with private sub-registries. The system would need role-based access control allowing fine-grained permissions like read-only access, registration rights, and administrative privileges within each sub-registry. API keys could be scoped to specific sub-registries, ensuring that automated systems can only access authorized resources.


This sub-registry capability would make your hub suitable for enterprise deployments where organizations need both public discovery of community servers and private management of proprietary servers. The architecture's clean separation of concerns makes adding this feature straightforward, as the core business logic remains unchanged while the storage and authorization layers extend to support multi-tenancy.


Conclusion

Building your own MCP Hub represents a significant undertaking, but the benefits justify the effort. By creating a centralized registry for Model Context Protocol servers, you provide essential infrastructure that accelerates development, promotes quality, and fosters community growth. While existing hubs from Anthropic and GitHub serve important roles, a custom hub allows you to tailor the system to your specific needs, whether those involve enterprise requirements, specialized domains, or unique integration patterns.


The architecture presented in this guide balances multiple concerns. Ease of use for human users through the web interface ensures broad accessibility. Programmatic access through the API enables automation and integration with existing tools. Sophisticated search capabilities powered by LLM integration help users find what they need even with vague requirements. Data integrity through validation and security measures maintains trust in the registry.


As the MCP ecosystem continues to grow, registries become increasingly valuable. They reduce friction in discovering and integrating servers, promote quality through ratings and reviews, and provide visibility for server developers. Your hub serves as infrastructure that benefits your entire organization or community, similar to how package registries have accelerated software development in other domains.


The technical implementation combines proven technologies in novel ways. Relational databases provide reliable storage for structured metadata. Vector embeddings enable semantic search that understands meaning rather than just matching keywords. Graph databases capture the rich relationships between servers, domains, and capabilities. Large language models tie these technologies together, interpreting natural language queries and enriching registrations with automatically extracted metadata.


Building such a hub requires careful attention to user experience, data quality, security, and scalability. The architecture presented here provides a solid foundation that can evolve as your ecosystem grows and new requirements emerge. By following clean architecture principles and separating concerns into distinct layers, the system remains maintainable and extensible.


Your MCP Hub vision extends beyond simple cataloging to create an intelligent discovery system that helps users find exactly what they need, whether they know precisely what they are looking for or only have a vague description of their requirements. This capability, powered by modern AI technologies, represents the future of software component discovery and integration. The combination of traditional database technologies, vector search, graph relationships, and LLM-powered understanding creates a registry system that is both powerful and accessible.


The journey from concept to production involves many decisions and tradeoffs. This guide provides a comprehensive blueprint, but your specific implementation will necessarily differ based on your requirements, constraints, and preferences. The core principles remain constant: provide value through discovery, maintain quality through validation and ratings, enable automation through APIs, and build for evolution through clean architecture. With these principles as your foundation, you can create an MCP Hub that serves your community effectively for years to come.

No comments: