Sunday, March 01, 2026

A Guide for Implementing an LLM-based MCP Server Search Engine

 

Introduction


The Model Context Protocol (MCP), as defined by Anthropic, establishes a

framework for interaction between large language model (LLM) clients and

various data sources and services, known as MCP servers. In this ecosystem,

LLM clients, typically large language models or applications powered by them,

consume the capabilities offered by these servers, which can range from

providing real-time data feeds to executing complex computational tasks. As

the number and diversity of MCP servers grow, a critical challenge emerges:

how can an LLM client efficiently discover and select the most relevant MCP

servers for a given task or user query? This article addresses this challenge

by proposing and detailing the implementation of an LLM-based MCP Server

Search Engine. This engine empowers LLM clients to intelligently search for

MCP servers based on a user-defined domain, topic, or subject, by interacting

with server catalogs, directories, and libraries available on the internet. We

will explore all constituents and details of such an engine, providing a deep

dive into its architecture, implementation considerations, and a practical

running example.


I. Understanding the MCP Ecosystem


To appreciate the necessity and functionality of an LLM-based search engine,

it is crucial to first understand the fundamental components of the MCP

ecosystem.


A. MCP Clients


MCP clients are typically large language models or applications powered by

them. These clients are designed to perform a wide array of tasks, such as

answering questions, generating content, or automating workflows. To enhance

their capabilities, LLM clients often need to access external, up-to-date,

or specialized information and services that are beyond their internal

training data. This is precisely where MCP servers become invaluable. An LLM

client, for instance, might require current stock market data, weather

forecasts, or access to a proprietary internal database to fulfill a user's

request accurately and comprehensively.


B. MCP Servers


MCP servers are external services or data sources that expose their

functionality and data through a standardized protocol, making them

accessible and consumable by MCP clients. These servers act as specialized

"tools" or "knowledge bases" that an LLM can leverage. Examples of MCP

servers could include a financial data API providing real-time stock quotes,

a weather service offering local forecasts, a document management system for

retrieving specific enterprise documents, or a computational engine for

solving mathematical problems. Each MCP server typically offers a specific

set of functionalities and operates within a defined domain.


C. The Need for Discovery


Given the potential proliferation of diverse MCP servers, the sheer volume

and variety can make manual discovery and integration a significant hurdle.

An LLM client cannot inherently know about every available MCP server and

its specific capabilities. Therefore, a mechanism is required to enable

dynamic discovery, allowing the LLM client to identify and connect to the

most appropriate server based on the context of a user's request. Without an

efficient discovery mechanism, the full potential of the MCP ecosystem, where

LLMs can seamlessly integrate with external tools, remains largely untapped.

This is the core problem that an LLM-based MCP Server Search Engine aims to

solve, acting as a bridge between user intent and available MCP server

resources.


II. Architecture of an LLM-based MCP Server Search Engine


The design of an LLM-based MCP Server Search Engine involves several

interconnected components that work in concert to process user queries,

search for relevant servers, and present actionable results.


A. Core Components


The search engine can be conceptualized as having four primary components,

each with a distinct role in the discovery process.


1.  User Interface (Conceptual): While not part of the search engine's

    internal logic, a user interface is the entry point where a human user

    or an orchestrating LLM provides the initial domain or topic for the

    MCP server search. This interface could be a simple text prompt within

    a chat application or a more structured input form.


2.  LLM Orchestrator: This component is the "brain" of the search engine.

    It is an LLM instance specifically tasked with interpreting the user's

    query, extracting key search parameters, formulating appropriate search

    strategies, and processing the results returned by various catalogs. The

    LLM Orchestrator is responsible for understanding the semantic meaning

    of the user's request and translating it into concrete actions.


3.  Search Adapters/Connectors: These are specialized modules designed to

    interface with different MCP server catalogs, directories, or libraries.

    Each adapter understands the specific API, data format, and search

    mechanisms of a particular catalog. This abstraction layer is crucial

    for handling the heterogeneity of external information sources.


4.  MCP Server Registry/Cache (Optional but Recommended): To improve

    performance and reduce redundant calls to external catalogs, a local

    registry or cache can store metadata about previously discovered MCP

    servers. This component can also maintain an index of server

    capabilities and domains, allowing for faster lookups and potentially

    semantic search capabilities over known servers.


5.  Result Presenter: This component takes the raw search results,

    normalizes them, and presents them to the user or the requesting LLM

    client in a clear, concise, and actionable format. It might involve

    summarizing server functionalities, providing access endpoints, and

    highlighting key features.


B. Data Flow


The process of searching for an MCP server through this engine follows a

logical sequence of steps.


1.  A user or an LLM client submits a query specifying a domain or topic

    (e.g., "I need an MCP server for real-time financial market data").


2.  The query is received by the LLM Orchestrator, which analyzes the

    natural language input to understand the user's intent, identify

    keywords, and extract relevant search criteria (e.g., "financial",

    "real-time", "market data").


3.  Based on the extracted criteria, the LLM Orchestrator decides which

    Search Adapters to invoke. It might generate specific queries tailored

    to each catalog's API (e.g., a keyword search for one catalog, a

    category filter for another).


4.  The Search Adapters then communicate with their respective external MCP

    server catalogs, submitting the formulated queries. These catalogs

    respond with lists of MCP servers that match the search criteria, often

    including metadata such as server name, description, and API endpoint.


5.  The results from all invoked Search Adapters are collected by the LLM

    Orchestrator. The orchestrator then processes these raw results, which

    might involve deduplication, normalization of data formats, and further

    semantic analysis to rank servers by relevance.


6.  Finally, the Result Presenter formats the refined list of relevant MCP

    servers into a user-friendly output, which is then returned to the

    original user or LLM client. This output typically includes enough

    information for the client to decide which server to interact with.


III. Implementing the LLM Orchestrator


The LLM Orchestrator is the heart of our search engine, responsible for

intelligent query processing and result management. Its implementation

involves leveraging the capabilities of a large language model for natural

language understanding and generation.


A. User Intent Parsing


The first critical step for the LLM Orchestrator is to accurately parse the

user's intent from their natural language query. This involves identifying

the core domain, topic, and any specific requirements for the MCP server.

Prompt engineering plays a crucial role here, guiding the LLM to extract

structured information from unstructured text.


Consider a user prompt like: "I need an MCP server that provides real-time

stock prices for the energy sector." The LLM Orchestrator would need to

extract:

-   Domain: "financial data"

-   Specificity: "real-time stock prices"

-   Sector: "energy"


A simple prompt structure for the LLM could be:



"Analyze the following user request for an MCP server and extract the

primary domain, key features, and any specific sectors or data types.

Return the information as a JSON-like structure with 'domain', 'features',

and 'sector' keys. If a category is not explicitly mentioned, infer it

or leave it as 'general'.


User Request: {user_query}




import json


class LLMOrchestrator:

    """

    The LLMOrchestrator is responsible for processing user queries,

    interacting with the underlying LLM for intent parsing and query

    formulation, and managing the overall search workflow.

    """


    def __init__(self, llm_api_client):

        """

        Initializes the LLMOrchestrator with an LLM API client.

        The llm_api_client is expected to have a 'generate_response' method

        that takes a prompt and returns a text response.

        """

        self.llm_api_client = llm_api_client


    def parse_user_intent(self, user_query: str) -> dict:

        """

        Parses the user's natural language query to extract structured intent

        using the underlying LLM.


        Args:

            user_query (str): The natural language query from the user.


        Returns:

            dict: A dictionary containing the extracted domain, features, and sector.

                  Example: {"domain": "financial data", "features": ["real-time stock prices"], "sector": "energy"}

        """

        prompt = f"""

        Analyze the following user request for an MCP server and extract the

        primary domain, key features, and any specific sectors or data types.

        Return the information as a JSON object with 'domain', 'features' (list of strings),

        and 'sector' keys. If a category is not explicitly mentioned, infer it

        or use 'general' for domain/sector and an empty list for features.


        User Request: "{user_query}"


        JSON Output:

        """

        try:

            llm_response = self.llm_api_client.generate_response(prompt)

            # Attempt to parse the LLM's response as JSON

            parsed_intent = json.loads(llm_response.strip())

            return parsed_intent

        except json.JSONDecodeError as e:

            print(f"Error decoding LLM response JSON: {e}")

            print(f"LLM Response: {self.llm_api_client.generate_response(prompt)}")

            # Fallback to a default or error handling mechanism

            return {"domain": "general", "features": [], "sector": "general"}

        except Exception as e:

            print(f"An unexpected error occurred during intent parsing: {e}")

            return {"domain": "general", "features": [], "sector": "general"}




The `parse_user_intent` method demonstrates how the LLM Orchestrator interacts

with an underlying LLM API. The prompt is carefully crafted to instruct the

LLM to return a structured JSON output, making it easier for the orchestrator

to programmatically extract the necessary information. This structured output

is crucial for subsequent steps in the search process.


B. Query Formulation


Once the user's intent is parsed into a structured format, the LLM

Orchestrator needs to formulate specific queries for each of the available

Search Adapters. Different catalogs might have different query syntaxes or

expect different parameters. The orchestrator, potentially with further LLM

assistance or rule-based logic, translates the generic intent into

adapter-specific search requests.


For instance, if one Search Adapter connects to a public MCP server catalog

that supports keyword searches and category filters, the orchestrator would

construct a query suitable for that adapter. If another adapter connects to

an internal corporate catalog that uses product areas and specific asset types,

the orchestrator would generate a different query.



class LLMOrchestrator:

    # ... (previous __init__ and parse_user_intent methods)


    def formulate_adapter_queries(self, parsed_intent: dict, available_adapters: list) -> dict:

        """

        Formulates adapter-specific queries based on the parsed user intent.

        This method would typically use a combination of LLM capabilities

        and predefined rules for each adapter type.


        Args:

            parsed_intent (dict): The structured intent extracted from the user query.

            available_adapters (list): A list of SearchAdapter instances.


        Returns:

            dict: A dictionary where keys are adapter names and values are

                  the formulated queries for that adapter.

        """

        adapter_queries = {}

        domain = parsed_intent.get("domain", "general")

        features = parsed_intent.get("features", [])

        sector = parsed_intent.get("sector", "general")


        # Example of how an LLM might help formulate queries for various adapters

        # In a real system, this might involve more sophisticated prompt engineering

        # or a mapping layer that understands each adapter's capabilities.

        for adapter in available_adapters:

            # This is a simplified example. A more advanced system might use the LLM

            # to generate the *best* query string for each adapter based on its known schema.

            if "publiccatalogadapter" in adapter.name.lower():

                # For a generic public catalog, combine domain, features, and sector into a keyword query

                query_string = f"{domain} {' '.join(features)} {sector} MCP server"

                adapter_queries[adapter.name] = {"keywords": query_string}

            elif "corporateassetsadapter" in adapter.name.lower():

                # For a corporate asset catalog, map to specific parameters

                asset_type = "ALL" # Could be refined by LLM if intent is very specific (e.g., "video")

                collection = "ALL" # Could be refined by LLM

                corporate_query = f"{domain} {' '.join(features)} {sector}"

                adapter_queries[adapter.name] = {

                    "query": corporate_query,

                    "assetType": asset_type,

                    "collection": collection

                }

            elif "developerdocsadapter" in adapter.name.lower():

                # For developer documentation, focus on API/SDK related terms

                dev_query = f"{domain} API {' '.join(features)} {sector}"

                adapter_queries[adapter.name] = {"query": dev_query, "includeContent": "true"}

            else:

                # Default keyword search for unknown adapters

                adapter_queries[adapter.name] = {"keywords": f"{domain} {' '.join(features)} {sector}"}


        return adapter_queries




The `formulate_adapter_queries` method takes the parsed intent and a list of

available Search Adapters. It then iterates through these adapters,

generating a tailored query for each. This example shows a rule-based approach

for simplicity, but in a more sophisticated system, the LLM itself could be

tasked with generating the optimal query parameters for each adapter, given

its understanding of the adapter's capabilities (which could be learned from

its documentation or schema).


C. Result Aggregation and Ranking


After the Search Adapters have executed their queries and returned results,

the LLM Orchestrator is responsible for aggregating these diverse results

and presenting them in a coherent and ranked manner. This often involves

normalizing data formats, deduplicating entries, and applying relevance

scoring. The LLM can play a significant role in semantic ranking,

understanding which results are most pertinent to the user's original intent,

even if keywords don't perfectly match.



class LLMOrchestrator:

    # ... (previous methods)


    def aggregate_and_rank_results(self, raw_results: dict, user_query: str) -> list:

        """

        Aggregates raw search results from various adapters, deduplicates,

        normalizes, and ranks them based on relevance to the original user query.

        The LLM can be used here for semantic ranking.


        Args:

            raw_results (dict): A dictionary where keys are adapter names and

                                values are lists of MCP server results from that adapter.

            user_query (str): The original natural language query for context.


        Returns:

            list: A sorted list of dictionaries, each representing a relevant

                  MCP server, with normalized fields (e.g., 'name', 'description', 'endpoint').

        """

        all_servers = []

        unique_server_identifiers = set()


        # Step 1: Normalize and deduplicate results

        for adapter_name, results_list in raw_results.items():

            for server_data in results_list:

                # Assuming each adapter returns a dictionary with at least 'name' and 'description'

                # and potentially an 'endpoint' or 'access_info'.

                # We need to normalize these to a common format.

                normalized_server = {

                    "source_adapter": adapter_name,

                    "name": server_data.get("name", "Unknown Server"),

                    "description": server_data.get("description", "No description provided."),

                    "endpoint": server_data.get("endpoint", server_data.get("url", "N/A")),

                    "metadata": server_data # Keep original metadata for deeper inspection if needed

                }


                # Create a unique identifier for deduplication (e.g., based on name and endpoint)

                server_identifier = f"{normalized_server['name']}-{normalized_server['endpoint']}"

                if server_identifier not in unique_server_identifiers:

                    all_servers.append(normalized_server)

                    unique_server_identifiers.add(server_identifier)


        if not all_servers:

            return []


        # Step 2: Use LLM for semantic ranking

        # This prompt asks the LLM to score each server based on its description

        # and the original user query.

        ranking_prompt = f"""

        Rank the following MCP servers based on their relevance to the user's original query:

        "{user_query}"


        Provide a relevance score (0-100, where 100 is most relevant) and a brief

        reason for each server. Return the output as a JSON array of objects,

        each with 'server_name', 'score', and 'reason'.


        MCP Servers:

        """

        for i, server in enumerate(all_servers):

            ranking_prompt += f"\n{i+1}. Name: {server['name']}\n   Description: {server['description']}\n"


        ranking_prompt += "\nJSON Output:"


        try:

            llm_ranking_response = self.llm_api_client.generate_response(ranking_prompt)

            ranking_data = json.loads(llm_ranking_response.strip())


            # Map scores back to the original server objects

            scored_servers = []

            for server in all_servers:

                found_score = next((item for item in ranking_data if item.get('server_name') == server['name']), None)

                if found_score:

                    server['relevance_score'] = found_score.get('score', 0)

                    server['relevance_reason'] = found_score.get('reason', 'N/A')

                else:

                    server['relevance_score'] = 0 # Default score if LLM didn't rank it

                    server['relevance_reason'] = 'Not explicitly ranked by LLM.'

                scored_servers.append(server)


            # Sort by relevance score in descending order

            sorted_servers = sorted(scored_servers, key=lambda x: x.get('relevance_score', 0), reverse=True)

            return sorted_servers


        except json.JSONDecodeError as e:

            print(f"Error decoding LLM ranking response JSON: {e}")

            print(f"LLM Ranking Response: {self.llm_api_client.generate_response(ranking_prompt)}")

            # Fallback: if LLM ranking fails, sort by some default (e.g., alphabetically or by source)

            return sorted(all_servers, key=lambda x: x.get('name', ''))

        except Exception as e:

            print(f"An unexpected error occurred during result aggregation/ranking: {e}")

            return sorted(all_servers, key=lambda x: x.get('name', ''))




The `aggregate_and_rank_results` method first normalizes and deduplicates

the results received from various adapters. It then constructs a prompt for

the LLM to semantically rank these servers based on their descriptions and

the original user query. This allows for a more intelligent ranking than

simple keyword matching. The LLM's response, expected in JSON, is then

parsed and used to sort the final list of servers, providing the most

relevant ones at the top.


IV. Search Adapters/Connectors


Search Adapters are the bridge between our LLM Orchestrator and the diverse

world of MCP server catalogs. Each adapter is responsible for knowing how to

query a specific catalog, parse its responses, and return results in a

standardized format. This modular design ensures that new catalogs can be

integrated by simply developing a new adapter without modifying the core

orchestrator logic.


A. Adapter Interface


To ensure consistency and ease of integration, all Search Adapters should

adhere to a common interface. This interface defines the methods that the

LLM Orchestrator will call to interact with any adapter.



from abc import ABC, abstractmethod


class SearchAdapter(ABC):

    """

    Abstract base class for all Search Adapters.

    Defines the common interface for interacting with MCP server catalogs.

    """

    def __init__(self, name: str):

        self.name = name


    @abstractmethod

    def search(self, query_params: dict) -> list:

        """

        Executes a search query against the specific MCP server catalog

        this adapter is responsible for.


        Args:

            query_params (dict): A dictionary of parameters specific to

                                 this adapter's catalog API.


        Returns:

            list: A list of dictionaries, where each dictionary represents

                  an MCP server and contains its normalized metadata.

                  Example: [{"name": "Server A", "description": "...", "endpoint": "..."}]

        """

        pass


    @abstractmethod

    def get_capabilities(self) -> dict:

        """

        Returns a dictionary describing the capabilities and expected

        query parameters of this adapter. This can be used by the LLM

        Orchestrator for more intelligent query formulation.

        """

        pass




The `SearchAdapter` abstract base class mandates two methods: `search` and

`get_capabilities`. The `search` method is where the actual interaction with

the external catalog happens, while `get_capabilities` allows the orchestrator

to understand what kind of queries an adapter can handle.


B. Concrete Adapter Implementations


Let's consider concrete implementations for a generic public catalog and

a corporate-specific catalog, leveraging a hypothetical external API.


1. Generic Public Catalog Adapter


This adapter would simulate interaction with a hypothetical public catalog

of MCP servers. In a real-world scenario, this might involve web scraping

or interacting with a public API. For our running example, we will simulate

a simple in-memory catalog.



class PublicCatalogAdapter(SearchAdapter):

    """

    A concrete SearchAdapter for a hypothetical public MCP server catalog.

    Simulates an in-memory catalog for demonstration purposes.

    """

    def __init__(self, name="PublicCatalogAdapter"):

        super().__init__(name)

        # Simulate a simple in-memory catalog of MCP servers

        self._catalog_data = [

            {

                "name": "Global Stock Data API",

                "description": "Provides real-time and historical stock prices for global markets.",

                "endpoint": "https://api.globalstockdata.com/v1/stocks",

                "tags": ["financial", "real-time", "stocks", "market data"]

            },

            {

                "name": "Weather Forecast Service",

                "description": "Offers current weather conditions and forecasts worldwide.",

                "endpoint": "https://api.weatherforecast.com/v2/forecast",

                "tags": ["weather", "environmental", "real-time"]

            },

            {

                "name": "Energy Market Insights",

                "description": "Provides analytics and real-time data for the energy sector.",

                "endpoint": "https://api.energymarketinsights.com/data",

                "tags": ["financial", "energy", "market data", "analytics"]

            },

            {

                "name": "Currency Exchange Rates",

                "description": "Offers real-time currency conversion rates.",

                "endpoint": "https://api.currencyexchange.com/rates",

                "tags": ["financial", "currency", "real-time"]

            }

        ]


    def search(self, query_params: dict) -> list:

        """

        Searches the simulated public catalog based on keywords.

        """

        keywords = query_params.get("keywords", "").lower().split()

        if not keywords:

            return []


        results = []

        for server in self._catalog_data:

            server_text = f"{server['name'].lower()} {server['description'].lower()} {' '.join(server['tags'])}"

            if any(keyword in server_text for keyword in keywords):

                results.append(server)

        return results


    def get_capabilities(self) -> dict:

        """

        Describes the search capabilities of this adapter.

        """

        return {

            "search_type": "keyword_search",

            "parameters": {"keywords": "string - space-separated keywords for searching"}

        }



The `PublicCatalogAdapter` implements the `search` method by performing a

simple keyword match against its internal `_catalog_data`. This simulates

how an adapter would query an external service and filter results.


2. Corporate Assets Adapter


This adapter will leverage a hypothetical `external_api_client` to search

for corporate digital assets, which can be considered a type of MCP server

for content.



# Assume an external_api_client object is available in the environment

# from the tool context, or mocked for the running example.


class CorporateAssetsAdapter(SearchAdapter):

    """

    A concrete SearchAdapter for corporate digital assets, utilizing a

    hypothetical external API client.

    """

    def __init__(self, name="CorporateAssetsAdapter", external_api_client=None):

        super().__init__(name)

        self.external_api_client = external_api_client # This will be the mock client in the example


    def search(self, query_params: dict) -> list:

        """

        Executes a search against corporate digital assets.

        Expected query_params: {'query': str, 'assetType': str, 'collection': str}

        """

        if not self.external_api_client:

            print("External API client not initialized for CorporateAssetsAdapter.")

            return []


        query = query_params.get("query")

        asset_type = query_params.get("assetType", "ALL")

        collection = query_params.get("collection", "ALL")

        max_results = query_params.get("maxResults", "10") # Default max results


        if not query:

            return []


        try:

            # This calls the mocked external tool function

            response = self.external_api_client.get_corporate_assets(

                query=query,

                assetType=asset_type,

                collection=collection,

                maxResults=max_results

            )


            # Normalize the response for consistency

            normalized_results = []

            if response and "assets" in response:

                for asset in response["assets"]:

                    normalized_results.append({

                        "name": asset.get("title", "Corporate Asset"),

                        "description": asset.get("description", "Corporate digital asset."),

                        "endpoint": asset.get("primaryDownloadUrl", "N/A"), # Or a more specific asset URL

                        "asset_id": asset.get("id"),

                        "asset_type": asset.get("assetType"),

                        "tags": asset.get("tags", [])

                    })

            return normalized_results

        except Exception as e:

            print(f"Error calling Corporate Assets API: {e}")

            return []


    def get_capabilities(self) -> dict:

        """

        Describes the search capabilities of this adapter for Corporate Assets.

        """

        return {

            "search_type": "corporate_assets_search",

            "parameters": {

                "query": "string - keyword search for assets",

                "assetType": "string - IMAGES, VIDEOS, DOCUMENTS, AUDIO, ICONS, ALL",

                "collection": "string - GENERAL, MARKETING, INTERNAL, ALL",

                "maxResults": "string - Maximum number of results (5-50)"

            }

        }




The `CorporateAssetsAdapter` demonstrates how to integrate with a specific

external tool. It translates the generic `query_params` into the arguments

expected by the `get_corporate_assets` function and then normalizes

the tool's response into the standard server metadata format.


V. MCP Server Registry/Cache


For performance and efficiency, especially in scenarios with frequent

queries or slow external catalog APIs, an MCP Server Registry or Cache is

highly beneficial. This component acts as a local store for metadata about

previously discovered MCP servers, reducing the need to repeatedly query

external sources.


A. Purpose and Benefits


1.  Reduced Latency: By serving results from a local cache, the search

    engine can respond much faster to queries for commonly requested server

    types.

2.  Reduced API Calls: This minimizes the number of calls to external catalog

    APIs, which can be rate-limited or incur costs.

3.  Offline Capability: In some cases, a limited search capability might

    even be possible if external catalogs are temporarily unavailable.

4.  Enhanced Search: The local registry can be indexed and enriched with

    additional metadata, allowing for more sophisticated internal search

    capabilities (e.g., semantic search over cached descriptions).


B. Implementation Considerations


The registry can be implemented using a simple in-memory dictionary for small

scale, a persistent database (like SQLite, PostgreSQL) for larger deployments,

or a dedicated caching system (like Redis). Key design choices include:


-   Data Structure: How to store server metadata (e.g., JSON documents,

    relational tables).

-   Indexing: How to efficiently search the cached data (e.g., full-text

    indexing, tag-based indexing).

-   Invalidation Strategy: How to ensure cached data remains fresh and

    reflects changes in external catalogs (e.g., time-to-live (TTL),

    webhook-based updates).


For our running example, we will use a simple in-memory cache that can be

extended to a persistent store.



import time


class MCPServerRegistry:

    """

    A simple in-memory registry/cache for MCP server metadata.

    This can be extended to use a persistent database for production.

    """

    def __init__(self, cache_ttl_seconds=3600):

        self._cache = {} # Stores server_identifier -> {"server_data": {}, "timestamp": float}

        self.cache_ttl_seconds = cache_ttl_seconds


    def add_server(self, server_data: dict):

        """

        Adds or updates an MCP server's metadata in the cache.

        A unique identifier is generated for each server.

        """

        # Assuming 'name' and 'endpoint' uniquely identify a server

        server_identifier = f"{server_data.get('name')}-{server_data.get('endpoint')}"

        if server_identifier not in self._cache or self._is_stale(server_identifier):

            self._cache[server_identifier] = {

                "server_data": server_data,

                "timestamp": time.time()

            }

            return True

        return False # Server already exists and is fresh


    def get_server(self, server_identifier: str) -> dict or None:

        """

        Retrieves a server's metadata from the cache by its identifier.

        Returns None if not found or if the entry is stale.

        """

        if server_identifier in self._cache and not self._is_stale(server_identifier):

            return self._cache[server_identifier]["server_data"]

        return None


    def search_registry(self, keywords: list) -> list:

        """

        Performs a simple keyword search within the cached server descriptions.

        This is a basic example; a real registry might use full-text search.

        """

        matching_servers = []

        for identifier, entry in self._cache.items():

            if not self._is_stale(identifier):

                server = entry["server_data"]

                server_text = f"{server.get('name', '').lower()} {server.get('description', '').lower()}"

                if any(keyword.lower() in server_text for keyword in keywords):

                    matching_servers.append(server)

        return matching_servers


    def _is_stale(self, server_identifier: str) -> bool:

        """

        Checks if a cached entry is stale based on its timestamp and TTL.

        """

        if server_identifier not in self._cache:

            return True

        return (time.time() - self._cache[server_identifier]["timestamp"]) > self.cache_ttl_seconds


    def clear_stale_entries(self):

        """

        Removes all stale entries from the cache.

        """

        stale_identifiers = [

            identifier for identifier in self._cache

            if self._is_stale(identifier)

        ]

        for identifier in stale_identifiers:

            del self._cache[identifier]

        print(f"Cleared {len(stale_identifiers)} stale entries from registry.")




The `MCPServerRegistry` provides basic `add_server`, `get_server`, and

`search_registry` functionalities. It includes a simple time-to-live (TTL)

mechanism to manage cache freshness. In a production environment, this

would be backed by a more robust and scalable data store.


VI. Result Presenter


The final component of our search engine is the Result Presenter. Its role

is to take the ranked list of MCP servers from the LLM Orchestrator and

format it into a human-readable or LLM-consumable output. The presentation

should be clear, concise, and provide enough information for the user or

client LLM to make an informed decision about which server to use.


A. Formatting for Readability


The presentation should include key details such as the server's name, a

brief description, its access endpoint, and the calculated relevance score.

Additional metadata, if relevant, can also be included.



class ResultPresenter:

    """

    Formats and presents the aggregated and ranked MCP server search results.

    """

    def present_results(self, ranked_servers: list) -> str:

        """

        Formats the list of ranked MCP servers into a human-readable string.


        Args:

            ranked_servers (list): A list of dictionaries, each representing

                                   a relevant MCP server, sorted by relevance.


        Returns:

            str: A formatted string presenting the search results.

        """

        if not ranked_servers:

            return "No MCP servers found matching your criteria."


        output = "Found the following MCP servers, ranked by relevance:\n"

        output += "---------------------------------------------------\n"


        for i, server in enumerate(ranked_servers):

            output += f"\n{i+1}. Server Name: {server.get('name', 'N/A')}\n"

            output += f"   Description: {server.get('description', 'N/A')}\n"

            output += f"   Endpoint:    {server.get('endpoint', 'N/A')}\n"

            output += f"   Relevance Score: {server.get('relevance_score', 0)}/100\n"

            output += f"   Reason for Relevance: {server.get('relevance_reason', 'N/A')}\n"

            output += f"   Source Adapter: {server.get('source_adapter', 'N/A')}\n"

            # Optionally include more metadata if desired

            # output += f"   Tags: {', '.join(server.get('metadata', {}).get('tags', []))}\n"

            output += "---------------------------------------------------\n"


        return output


    def present_results_for_llm(self, ranked_servers: list) -> str:

        """

        Formats the list of ranked MCP servers into a structured JSON string

        suitable for consumption by another LLM client.


        Args:

            ranked_servers (list): A list of dictionaries, each representing

                                   a relevant MCP server, sorted by relevance.


        Returns:

            str: A JSON string representing the search results.

        """

        if not ranked_servers:

            return json.dumps({"message": "No MCP servers found matching your criteria.", "servers": []})


        # Create a simplified structure for LLM consumption

        llm_output_servers = []

        for server in ranked_servers:

            llm_output_servers.append({

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

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

                "endpoint": server.get('endpoint'),

                "relevance_score": server.get('relevance_score')

            })

        return json.dumps({"servers": llm_output_servers})




The `ResultPresenter` offers two methods: `present_results` for human-readable

output and `present_results_for_llm` for structured JSON output, allowing

the search engine to serve both human users and other LLM clients effectively.


VII. Full Running Example: MCP Server Search Engine


This section provides a complete, runnable example integrating all the

components discussed. It simulates the LLM API client and the external API

client to make the example self-contained and executable.


A. Mock LLM API Client


To run the example without a live LLM API, we'll create a mock client that

simulates LLM responses based on predefined patterns.



class MockLLMAPIClient:

    """

    A mock LLM API client for demonstration purposes.

    Simulates responses for intent parsing and result ranking.

    """

    def generate_response(self, prompt: str) -> str:

        """

        Simulates an LLM response based on the prompt content.

        """

        prompt_lower = prompt.lower()


        if "analyze the following user request" in prompt_lower:

            if "real-time stock prices for the energy sector" in prompt_lower:

                return '{"domain": "financial data", "features": ["real-time stock prices"], "sector": "energy"}'

            elif "industrial automation solutions" in prompt_lower:

                return '{"domain": "industrial automation", "features": ["solutions"], "sector": "general"}'

            elif "documents about plc programming" in prompt_lower:

                return '{"domain": "documentation", "features": ["PLC programming"], "sector": "industrial"}'

            elif "corporate financial api" in prompt_lower:

                return '{"domain": "financial data", "features": ["API"], "sector": "general"}'

            else:

                return '{"domain": "general", "features": [], "sector": "general"}'

        elif "rank the following mcp servers" in prompt_lower:

            # Simple mock ranking logic based on keywords

            ranking_output = []

            server_lines = prompt.split("MCP Servers:")[1].split("\n")

            server_data = []

            current_server = {}

            for line in server_lines:

                line = line.strip()

                if line.startswith(f"{len(server_data) + 1}. Name:"):

                    if current_server:

                        server_data.append(current_server)

                    current_server = {"server_name": line.replace(f"{len(server_data) + 1}. Name:", "").strip()}

                elif line.startswith("Description:") and current_server:

                    current_server["description"] = line.replace("Description:", "").strip()

            if current_server:

                server_data.append(current_server)


            for server in server_data:

                score = 0

                reason = "General relevance."

                if "energy" in prompt_lower and "energy" in server.get("description", "").lower():

                    score += 30

                    reason = "Matches energy sector."

                if "stock" in prompt_lower and "stock" in server.get("description", "").lower():

                    score += 40

                    reason = "Matches stock data."

                if "real-time" in prompt_lower and "real-time" in server.get("description", "").lower():

                    score += 20

                    reason = "Matches real-time feature."

                if "corporate" in prompt_lower and "corporate" in server.get("description", "").lower():

                    score += 50

                    reason = "Specifically requested corporate content."

                if "plc programming" in prompt_lower and "plc" in server.get("description", "").lower():

                    score += 60

                    reason = "Matches PLC programming."

                if "api" in prompt_lower and "api" in server.get("description", "").lower():

                    score += 35

                    reason = "Matches API feature."


                score = min(100, score) # Cap score at 100

                ranking_output.append({"server_name": server["server_name"], "score": score, "reason": reason})

            return json.dumps(ranking_output)

        return ""




B. Mock External API Client


Similarly, we'll create a mock for the external API client to simulate its

responses.



class MockExternalAPIClient:

    """

    A mock client for external API tools, specifically for corporate assets and developer docs.

    """

    def get_corporate_assets(self, query: str, assetType: str = "ALL", collection: str = "ALL", maxResults: str = "10") -> dict:

        query_lower = query.lower()

        mock_assets = []


        if "industrial automation solutions" in query_lower:

            mock_assets.append({

                "id": "corp-ia-solution-1",

                "title": "Advanced Automation System Series 7000",

                "description": "Comprehensive automation solution for various industrial applications.",

                "assetType": "DOCUMENT",

                "primaryDownloadUrl": "https://corporate.com/docs/automation-series7000.pdf",

                "tags": ["automation", "industrial", "control", "solution"]

            })

            mock_assets.append({

                "id": "corp-ia-solution-2",

                "title": "Edge Computing Devices Overview for Industry",

                "description": "Edge computing solutions for industrial environments.",

                "assetType": "VIDEO",

                "primaryDownloadUrl": "https://corporate.com/videos/industrial-edge.mp4",

                "tags": ["industrial", "edge", "computing", "solution"]

            })

        elif "plc programming" in query_lower:

             mock_assets.append({

                "id": "corp-plc-guide-1",

                "title": "Unified Portal Programming Guide for Logic Controllers",

                "description": "Detailed guide for programming industrial logic controllers using Unified Portal.",

                "assetType": "DOCUMENT",

                "primaryDownloadUrl": "https://corporate.com/docs/unified-portal-programming.pdf",

                "tags": ["PLC", "programming", "logic controller"]

            })

        elif "energy" in query_lower:

            mock_assets.append({

                "id": "corp-energy-asset-1",

                "title": "Corporate Energy Management Solutions Brochure",

                "description": "Solutions for efficient energy consumption and distribution.",

                "assetType": "DOCUMENT",

                "primaryDownloadUrl": "https://corporate.com/docs/energy-management.pdf",

                "tags": ["energy", "management", "solution"]

            })

        elif "financial" in query_lower and "api" in query_lower:

            mock_assets.append({

                "id": "corp-finance-api-doc-1",

                "title": "Corporate Financial Services API Documentation",

                "description": "Documentation for integrating with Corporate Financial Services APIs.",

                "assetType": "DOCUMENT",

                "primaryDownloadUrl": "https://corporate.com/docs/cfs-api.pdf",

                "tags": ["financial", "API", "services"]

            })


        return {"assets": mock_assets[:int(maxResults)]}


    def get_developer_docs(self, query: str, includeContent: str = "false", limit: str = "10") -> dict:

        query_lower = query.lower()

        mock_dev_docs = []


        if "api" in query_lower and "financial" in query_lower:

            mock_dev_docs.append({

                "title": "Financial Data API Integration Guide",

                "description": "Guide on integrating corporate financial data APIs.",

                "url": "https://developer.corporate.com/docs/finance-api-guide",

                "content_snippet": "This guide covers authentication and data retrieval for our financial APIs.",

                "type": "API_DOC"

            })

        elif "industrial automation" in query_lower or "plc" in query_lower:

            mock_dev_docs.append({

                "title": "Industrial Edge API Reference",

                "description": "API reference for developing applications on industrial edge platforms.",

                "url": "https://developer.corporate.com/docs/industrial-edge-api",

                "content_snippet": "Explore the APIs for device management and data processing on the edge.",

                "type": "API_REFERENCE"

            })

            mock_dev_docs.append({

                "title": "Logic Controller SDK Documentation",

                "description": "Software Development Kit for industrial logic controllers.",

                "url": "https://developer.corporate.com/docs/logic-controller-sdk",

                "content_snippet": "Develop custom applications interacting with industrial PLCs.",

                "type": "SDK_DOC"

            })

        return {"results": mock_dev_docs[:int(limit)]}




C. Main Application Logic


This is the main script that orchestrates the entire search process using

the components defined above.



# Import all necessary classes

# For a self-contained running example, we'll redefine the classes here

# to avoid circular imports or complex module structures for this single file.


import json

import time

from abc import ABC, abstractmethod


# --- Mock LLM API Client ---

class MockLLMAPIClient:

    """

    A mock LLM API client for demonstration purposes.

    Simulates responses for intent parsing and result ranking.

    """

    def generate_response(self, prompt: str) -> str:

        """

        Simulates an LLM response based on the prompt content.

        """

        prompt_lower = prompt.lower()


        if "analyze the following user request" in prompt_lower:

            if "real-time stock prices for the energy sector" in prompt_lower:

                return '{"domain": "financial data", "features": ["real-time stock prices"], "sector": "energy"}'

            elif "industrial automation solutions" in prompt_lower:

                return '{"domain": "industrial automation", "features": ["solutions"], "sector": "general"}'

            elif "documents about plc programming" in prompt_lower:

                return '{"domain": "documentation", "features": ["PLC programming"], "sector": "industrial"}'

            elif "corporate financial api" in prompt_lower:

                return '{"domain": "financial data", "features": ["API"], "sector": "general"}'

            else:

                return '{"domain": "general", "features": [], "sector": "general"}'

        elif "rank the following mcp servers" in prompt_lower:

            ranking_output = []

            server_lines = prompt.split("MCP Servers:")[1].split("\n")

            server_data = []

            current_server = {}

            for line in server_lines:

                line = line.strip()

                if line.startswith(f"{len(server_data) + 1}. Name:"):

                    if current_server:

                        server_data.append(current_server)

                    current_server = {"server_name": line.replace(f"{len(server_data) + 1}. Name:", "").strip()}

                elif line.startswith("Description:") and current_server:

                    current_server["description"] = line.replace("Description:", "").strip()

            if current_server:

                server_data.append(current_server)


            for server in server_data:

                score = 0

                reason = "General relevance."

                if "energy" in prompt_lower and "energy" in server.get("description", "").lower():

                    score += 30

                    reason += " Matches energy sector."

                if "stock" in prompt_lower and "stock" in server.get("description", "").lower():

                    score += 40

                    reason += " Matches stock data."

                if "real-time" in prompt_lower and "real-time" in server.get("description", "").lower():

                    score += 20

                    reason += " Matches real-time feature."

                if "corporate" in prompt_lower and "corporate" in server.get("description", "").lower():

                    score += 50

                    reason += " Specifically requested corporate content."

                if "plc programming" in prompt_lower and "plc" in server.get("description", "").lower():

                    score += 60

                    reason += " Matches PLC programming."

                if "api" in prompt_lower and "api" in server.get("description", "").lower():

                    score += 35

                    reason += " Matches API feature."


                score = min(100, score)

                ranking_output.append({"server_name": server["server_name"], "score": score, "reason": reason.strip()})

            return json.dumps(ranking_output)

        return ""


# --- Mock External API Client ---

class MockExternalAPIClient:

    """

    A mock client for external API tools, specifically for corporate assets and developer docs.

    """

    def get_corporate_assets(self, query: str, assetType: str = "ALL", collection: str = "ALL", maxResults: str = "10") -> dict:

        query_lower = query.lower()

        mock_assets = []


        if "industrial automation solutions" in query_lower:

            mock_assets.append({

                "id": "corp-ia-solution-1",

                "title": "Advanced Automation System Series 7000",

                "description": "Comprehensive automation solution for various industrial applications.",

                "assetType": "DOCUMENT",

                "primaryDownloadUrl": "https://corporate.com/docs/automation-series7000.pdf",

                "tags": ["automation", "industrial", "control", "solution"]

            })

            mock_assets.append({

                "id": "corp-ia-solution-2",

                "title": "Edge Computing Devices Overview for Industry",

                "description": "Edge computing solutions for industrial environments.",

                "assetType": "VIDEO",

                "primaryDownloadUrl": "https://corporate.com/videos/industrial-edge.mp4",

                "tags": ["industrial", "edge", "computing", "solution"]

            })

        elif "plc programming" in query_lower:

             mock_assets.append({

                "id": "corp-plc-guide-1",

                "title": "Unified Portal Programming Guide for Logic Controllers",

                "description": "Detailed guide for programming industrial logic controllers using Unified Portal.",

                "assetType": "DOCUMENT",

                "primaryDownloadUrl": "https://corporate.com/docs/unified-portal-programming.pdf",

                "tags": ["PLC", "programming", "logic controller"]

            })

        elif "energy" in query_lower:

            mock_assets.append({

                "id": "corp-energy-asset-1",

                "title": "Corporate Energy Management Solutions Brochure",

                "description": "Solutions for efficient energy consumption and distribution.",

                "assetType": "DOCUMENT",

                "primaryDownloadUrl": "https://corporate.com/docs/energy-management.pdf",

                "tags": ["energy", "management", "solution"]

            })

        elif "financial" in query_lower and "api" in query_lower:

            mock_assets.append({

                "id": "corp-finance-api-doc-1",

                "title": "Corporate Financial Services API Documentation",

                "description": "Documentation for integrating with Corporate Financial Services APIs.",

                "assetType": "DOCUMENT",

                "primaryDownloadUrl": "https://corporate.com/docs/cfs-api.pdf",

                "tags": ["financial", "API", "services"]

            })


        return {"assets": mock_assets[:int(maxResults)]}


    def get_developer_docs(self, query: str, includeContent: str = "false", limit: str = "10") -> dict:

        query_lower = query.lower()

        mock_dev_docs = []


        if "api" in query_lower and "financial" in query_lower:

            mock_dev_docs.append({

                "title": "Financial Data API Integration Guide",

                "description": "Guide on integrating corporate financial data APIs.",

                "url": "https://developer.corporate.com/docs/finance-api-guide",

                "content_snippet": "This guide covers authentication and data retrieval for our financial APIs.",

                "type": "API_DOC"

            })

        elif "industrial automation" in query_lower or "plc" in query_lower:

            mock_dev_docs.append({

                "title": "Industrial Edge API Reference",

                "description": "API reference for developing applications on industrial edge platforms.",

                "url": "https://developer.corporate.com/docs/industrial-edge-api",

                "content_snippet": "Explore the APIs for device management and data processing on the edge.",

                "type": "API_REFERENCE"

            })

            mock_dev_docs.append({

                "title": "Logic Controller SDK Documentation",

                "description": "Software Development Kit for industrial logic controllers.",

                "url": "https://developer.corporate.com/docs/logic-controller-sdk",

                "content_snippet": "Develop custom applications interacting with industrial PLCs.",

                "type": "SDK_DOC"

            })

        return {"results": mock_dev_docs[:int(limit)]}



# --- LLMOrchestrator Class ---

class LLMOrchestrator:

    def __init__(self, llm_api_client):

        self.llm_api_client = llm_api_client


    def parse_user_intent(self, user_query: str) -> dict:

        prompt = f"""

        Analyze the following user request for an MCP server and extract the

        primary domain, key features, and any specific sectors or data types.

        Return the information as a JSON object with 'domain', 'features' (list of strings),

        and 'sector' keys. If a category is not explicitly mentioned, infer it

        or use 'general' for domain/sector and an empty list for features.


        User Request: "{user_query}"


        JSON Output:

        """

        try:

            llm_response = self.llm_api_client.generate_response(prompt)

            parsed_intent = json.loads(llm_response.strip())

            return parsed_intent

        except json.JSONDecodeError as e:

            print(f"Error decoding LLM response JSON: {e}")

            return {"domain": "general", "features": [], "sector": "general"}

        except Exception as e:

            print(f"An unexpected error occurred during intent parsing: {e}")

            return {"domain": "general", "features": [], "sector": "general"}


    def formulate_adapter_queries(self, parsed_intent: dict, available_adapters: list) -> dict:

        adapter_queries = {}

        domain = parsed_intent.get("domain", "general")

        features = parsed_intent.get("features", [])

        sector = parsed_intent.get("sector", "general")


        for adapter in available_adapters:

            if "publiccatalogadapter" in adapter.name.lower():

                query_string = f"{domain} {' '.join(features)} {sector} MCP server"

                adapter_queries[adapter.name] = {"keywords": query_string}

            elif "corporateassetsadapter" in adapter.name.lower():

                asset_type = "ALL"

                collection = "ALL"

                corporate_query = f"{domain} {' '.join(features)} {sector}"

                adapter_queries[adapter.name] = {

                    "query": corporate_query,

                    "assetType": asset_type,

                    "collection": collection

                }

            elif "developerdocsadapter" in adapter.name.lower():

                dev_query = f"{domain} API {' '.join(features)} {sector}"

                adapter_queries[adapter.name] = {"query": dev_query, "includeContent": "true"}

            else:

                adapter_queries[adapter.name] = {"keywords": f"{domain} {' '.join(features)} {sector}"}

        return adapter_queries


    def aggregate_and_rank_results(self, raw_results: dict, user_query: str) -> list:

        all_servers = []

        unique_server_identifiers = set()


        for adapter_name, results_list in raw_results.items():

            for server_data in results_list:

                normalized_server = {

                    "source_adapter": adapter_name,

                    "name": server_data.get("name", server_data.get("title", "Unknown Server")),

                    "description": server_data.get("description", "No description provided."),

                    "endpoint": server_data.get("endpoint", server_data.get("primaryDownloadUrl", server_data.get("url", "N/A"))),

                    "metadata": server_data

                }

                server_identifier = f"{normalized_server['name']}-{normalized_server['endpoint']}"

                if server_identifier not in unique_server_identifiers:

                    all_servers.append(normalized_server)

                    unique_server_identifiers.add(server_identifier)


        if not all_servers:

            return []


        ranking_prompt = f"""

        Rank the following MCP servers based on their relevance to the user's original query:

        "{user_query}"


        Provide a relevance score (0-100, where 100 is most relevant) and a brief

        reason for each server. Return the output as a JSON array of objects,

        each with 'server_name', 'score', and 'reason'.


        MCP Servers:

        """

        for i, server in enumerate(all_servers):

            ranking_prompt += f"\n{i+1}. Name: {server['name']}\n   Description: {server['description']}\n"


        ranking_prompt += "\nJSON Output:"


        try:

            llm_ranking_response = self.llm_api_client.generate_response(ranking_prompt)

            ranking_data = json.loads(llm_ranking_response.strip())


            scored_servers = []

            for server in all_servers:

                found_score = next((item for item in ranking_data if item.get('server_name') == server['name']), None)

                if found_score:

                    server['relevance_score'] = found_score.get('score', 0)

                    server['relevance_reason'] = found_score.get('reason', 'N/A')

                else:

                    server['relevance_score'] = 0

                    server['relevance_reason'] = 'Not explicitly ranked by LLM.'

                scored_servers.append(server)


            sorted_servers = sorted(scored_servers, key=lambda x: x.get('relevance_score', 0), reverse=True)

            return sorted_servers


        except json.JSONDecodeError as e:

            print(f"Error decoding LLM ranking response JSON: {e}")

            return sorted(all_servers, key=lambda x: x.get('name', ''))

        except Exception as e:

            print(f"An unexpected error occurred during result aggregation/ranking: {e}")

            return sorted(all_servers, key=lambda x: x.get('name', ''))


# --- SearchAdapter Abstract Base Class ---

class SearchAdapter(ABC):

    def __init__(self, name: str):

        self.name = name


    @abstractmethod

    def search(self, query_params: dict) -> list:

        pass


    @abstractmethod

    def get_capabilities(self) -> dict:

        pass


# --- PublicCatalogAdapter ---

class PublicCatalogAdapter(SearchAdapter):

    def __init__(self, name="PublicCatalogAdapter"):

        super().__init__(name)

        self._catalog_data = [

            {

                "name": "Global Stock Data API",

                "description": "Provides real-time and historical stock prices for global markets.",

                "endpoint": "https://api.globalstockdata.com/v1/stocks",

                "tags": ["financial", "real-time", "stocks", "market data"]

            },

            {

                "name": "Weather Forecast Service",

                "description": "Offers current weather conditions and forecasts worldwide.",

                "endpoint": "https://api.weatherforecast.com/v2/forecast",

                "tags": ["weather", "environmental", "real-time"]

            },

            {

                "name": "Energy Market Insights",

                "description": "Provides analytics and real-time data for the energy sector.",

                "endpoint": "https://api.energymarketinsights.com/data",

                "tags": ["financial", "energy", "market data", "analytics"]

            },

            {

                "name": "Currency Exchange Rates",

                "description": "Offers real-time currency conversion rates.",

                "endpoint": "https://api.currencyexchange.com/rates",

                "tags": ["financial", "currency", "real-time"]

            }

        ]


    def search(self, query_params: dict) -> list:

        keywords = query_params.get("keywords", "").lower().split()

        if not keywords:

            return []


        results = []

        for server in self._catalog_data:

            server_text = f"{server['name'].lower()} {server['description'].lower()} {' '.join(server['tags'])}"

            if any(keyword in server_text for keyword in keywords):

                results.append(server)

        return results


    def get_capabilities(self) -> dict:

        return {

            "search_type": "keyword_search",

            "parameters": {"keywords": "string - space-separated keywords for searching"}

        }


# --- CorporateAssetsAdapter ---

class CorporateAssetsAdapter(SearchAdapter):

    def __init__(self, name="CorporateAssetsAdapter", external_api_client=None):

        super().__init__(name)

        self.external_api_client = external_api_client


    def search(self, query_params: dict) -> list:

        if not self.external_api_client:

            print("External API client not initialized for CorporateAssetsAdapter.")

            return []


        query = query_params.get("query")

        asset_type = query_params.get("assetType", "ALL")

        collection = query_params.get("collection", "ALL")

        max_results = query_params.get("maxResults", "10")


        if not query:

            return []


        try:

            response = self.external_api_client.get_corporate_assets(

                query=query,

                assetType=asset_type,

                collection=collection,

                maxResults=max_results

            )


            normalized_results = []

            if response and "assets" in response:

                for asset in response["assets"]:

                    normalized_results.append({

                        "name": asset.get("title", "Corporate Asset"),

                        "description": asset.get("description", "Corporate digital asset."),

                        "endpoint": asset.get("primaryDownloadUrl", "N/A"),

                        "asset_id": asset.get("id"),

                        "asset_type": asset.get("assetType"),

                        "tags": asset.get("tags", [])

                    })

            return normalized_results

        except Exception as e:

            print(f"Error calling Corporate Assets API: {e}")

            return []


    def get_capabilities(self) -> dict:

        return {

            "search_type": "corporate_assets_search",

            "parameters": {

                "query": "string - keyword search for assets",

                "assetType": "string - IMAGES, VIDEOS, DOCUMENTS, AUDIO, ICONS, ALL",

                "collection": "string - GENERAL, MARKETING, INTERNAL, ALL",

                "maxResults": "string - Maximum number of results (5-50)"

            }

        }


# --- DeveloperDocsAdapter (New Adapter) ---

class DeveloperDocsAdapter(SearchAdapter):

    """

    A concrete SearchAdapter for developer documentation, utilizing a

    hypothetical external API client.

    """

    def __init__(self, name="DeveloperDocsAdapter", external_api_client=None):

        super().__init__(name)

        self.external_api_client = external_api_client


    def search(self, query_params: dict) -> list:

        if not self.external_api_client:

            print("External API client not initialized for DeveloperDocsAdapter.")

            return []


        query = query_params.get("query")

        include_content = query_params.get("includeContent", "false")

        limit = query_params.get("limit", "10")


        if not query:

            return []


        try:

            response = self.external_api_client.get_developer_docs(

                query=query,

                includeContent=include_content,

                limit=limit

            )


            normalized_results = []

            if response and "results" in response:

                for doc in response["results"]:

                    normalized_results.append({

                        "name": doc.get("title", "Developer Doc"),

                        "description": doc.get("description", "Developer documentation."),

                        "endpoint": doc.get("url", "N/A"),

                        "doc_type": doc.get("type"),

                        "content_snippet": doc.get("content_snippet", "")

                    })

            return normalized_results

        except Exception as e:

            print(f"Error calling Developer Docs API: {e}")

            return []


    def get_capabilities(self) -> dict:

        return {

            "search_type": "developer_docs_search",

            "parameters": {

                "query": "string - search query for developer documentation",

                "includeContent": "string - 'true' or 'false' to include content snippets",

                "limit": "string - maximum number of results"

            }

        }


# --- MCPServerRegistry ---

class MCPServerRegistry:

    def __init__(self, cache_ttl_seconds=3600):

        self._cache = {}

        self.cache_ttl_seconds = cache_ttl_seconds


    def add_server(self, server_data: dict):

        server_identifier = f"{server_data.get('name')}-{server_data.get('endpoint')}"

        if server_identifier not in self._cache or self._is_stale(server_identifier):

            self._cache[server_identifier] = {

                "server_data": server_data,

                "timestamp": time.time()

            }

            return True

        return False


    def get_server(self, server_identifier: str) -> dict or None:

        if server_identifier in self._cache and not self._is_stale(server_identifier):

            return self._cache[server_identifier]["server_data"]

        return None


    def search_registry(self, keywords: list) -> list:

        matching_servers = []

        for identifier, entry in self._cache.items():

            if not self._is_stale(identifier):

                server = entry["server_data"]

                server_text = f"{server.get('name', '').lower()} {server.get('description', '').lower()}"

                if any(keyword.lower() in server_text for keyword in keywords):

                    matching_servers.append(server)

        return matching_servers


    def _is_stale(self, server_identifier: str) -> bool:

        if server_identifier not in self._cache:

            return True

        return (time.time() - self._cache[server_identifier]["timestamp"]) > self.cache_ttl_seconds


    def clear_stale_entries(self):

        stale_identifiers = [

            identifier for identifier in self._cache

            if self._is_stale(identifier)

        ]

        for identifier in stale_identifiers:

            del self._cache[identifier]

        print(f"Cleared {len(stale_identifiers)} stale entries from registry.")


# --- ResultPresenter ---

class ResultPresenter:

    def present_results(self, ranked_servers: list) -> str:

        if not ranked_servers:

            return "No MCP servers found matching your criteria."


        output = "Found the following MCP servers, ranked by relevance:\n"

        output += "---------------------------------------------------\n"


        for i, server in enumerate(ranked_servers):

            output += f"\n{i+1}. Server Name: {server.get('name', 'N/A')}\n"

            output += f"   Description: {server.get('description', 'N/A')}\n"

            output += f"   Endpoint:    {server.get('endpoint', 'N/A')}\n"

            output += f"   Relevance Score: {server.get('relevance_score', 0)}/100\n"

            output += f"   Reason for Relevance: {server.get('relevance_reason', 'N/A')}\n"

            output += f"   Source Adapter: {server.get('source_adapter', 'N/A')}\n"

            output += "---------------------------------------------------\n"


        return output


    def present_results_for_llm(self, ranked_servers: list) -> str:

        if not ranked_servers:

            return json.dumps({"message": "No MCP servers found matching your criteria.", "servers": []})


        llm_output_servers = []

        for server in ranked_servers:

            llm_output_servers.append({

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

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

                "endpoint": server.get('endpoint'),

                "relevance_score": server.get('relevance_score')

            })

        return json.dumps({"servers": llm_output_servers})



# --- Main Application Class ---

class MCPServerSearchEngine:

    """

    The main class that orchestrates the MCP Server Search Engine.

    """

    def __init__(self, llm_api_client, external_api_client):

        self.llm_orchestrator = LLMOrchestrator(llm_api_client)

        self.registry = MCPServerRegistry()

        self.result_presenter = ResultPresenter()


        # Initialize all available Search Adapters

        self.adapters = [

            PublicCatalogAdapter(),

            CorporateAssetsAdapter(external_api_client=external_api_client),

            DeveloperDocsAdapter(external_api_client=external_api_client)

            # Add more adapters here as needed

        ]


    def search_mcp_servers(self, user_query: str) -> str:

        """

        Executes a full search for MCP servers based on the user's query.


        Args:

            user_query (str): The natural language query from the user.


        Returns:

            str: A formatted string of search results.

        """

        print(f"\n--- Processing User Query: '{user_query}' ---")


        # 1. Parse User Intent

        print("Step 1: Parsing user intent...")

        parsed_intent = self.llm_orchestrator.parse_user_intent(user_query)

        print(f"   Parsed Intent: {json.dumps(parsed_intent, indent=2)}")


        # 2. Formulate Adapter Queries

        print("Step 2: Formulating adapter-specific queries...")

        adapter_queries = self.llm_orchestrator.formulate_adapter_queries(parsed_intent, self.adapters)

        print(f"   Adapter Queries: {json.dumps(adapter_queries, indent=2)}")


        # 3. Execute Searches via Adapters and populate Registry

        print("Step 3: Executing searches via adapters and populating registry...")

        raw_results = {}

        for adapter in self.adapters:

            adapter_name = adapter.name

            query_params = adapter_queries.get(adapter_name)

            if query_params:

                print(f"   Searching with {adapter_name} using params: {query_params}")

                adapter_results = adapter.search(query_params)

                raw_results[adapter_name] = adapter_results

                # Add results to registry

                for server in adapter_results:

                    self.registry.add_server(server)

            else:

                print(f"   No specific query formulated for {adapter_name}.")


        # Also search the local registry for cached results

        registry_keywords = []

        if parsed_intent.get("domain"):

            registry_keywords.append(parsed_intent["domain"])

        registry_keywords.extend(parsed_intent.get("features", []))

        if parsed_intent.get("sector"):

            registry_keywords.append(parsed_intent["sector"])


        if registry_keywords:

            print(f"   Searching local registry with keywords: {registry_keywords}")

            cached_results = self.registry.search_registry(registry_keywords)

            if "Registry" not in raw_results:

                raw_results["Registry"] = []

            raw_results["Registry"].extend(cached_results)

        else:

            print("   No keywords for registry search.")


        # 4. Aggregate and Rank Results

        print("Step 4: Aggregating and ranking results...")

        ranked_servers = self.llm_orchestrator.aggregate_and_rank_results(raw_results, user_query)


        # 5. Present Results

        print("Step 5: Presenting results...")

        final_output = self.result_presenter.present_results(ranked_servers)

        return final_output


# --- Main Execution Block ---

if __name__ == "__main__":

    # Initialize mock clients

    mock_llm_client = MockLLMAPIClient()

    mock_external_api_client = MockExternalAPIClient()


    # Create the search engine instance

    search_engine = MCPServerSearchEngine(mock_llm_client, mock_external_api_client)


    # Example User Queries

    queries = [

        "I need an MCP server that provides real-time stock prices for the energy sector.",

        "Find industrial automation solutions.",

        "I'm looking for documents about PLC programming.",

        "Show me any corporate financial APIs."

    ]


    for query in queries:

        results = search_engine.search_mcp_servers(query)

        print(results)

        print("\n" + "="*80 + "\n") # Separator for queries


    # Demonstrate cache behavior (run a query again)

    print("\n--- Rerunning a query to demonstrate registry caching ---")

    results_cached = search_engine.search_mcp_servers(queries[0])

    print(results_cached)

    print("\n" + "="*80 + "\n")


    # Clear stale entries (example of maintenance)

    search_engine.registry.clear_stale_entries()




This comprehensive running example illustrates the full workflow of the

LLM-based MCP Server Search Engine. It starts by initializing mock clients

for the LLM and external APIs, then sets up the `MCPServerSearchEngine` with

its orchestrator, registry, and adapters. It processes several example user

queries, demonstrating how intent is parsed, queries are formulated for

different adapters, results are collected and ranked, and finally presented.

The example also includes a demonstration of the registry's caching behavior.


Conclusion


The implementation of an LLM-based MCP Server Search Engine represents a significant step towards unlocking the full potential of the Model Context

Protocol ecosystem. By leveraging the natural language understanding and

generation capabilities of large language models, this engine provides an

intelligent and adaptive mechanism for discovering relevant MCP servers.

The modular architecture, comprising an LLM Orchestrator, flexible Search

Adapters, an efficient MCP Server Registry, and a clear Result Presenter,

ensures scalability, maintainability, and extensibility. As the landscape

of MCP servers continues to evolve, such a search engine will be

indispensable for LLM clients to seamlessly integrate with and utilize the

vast array of external services and data sources, ultimately enhancing their

utility and responsiveness to complex user demands. This guide has provided

a deep dive into the conceptual and practical aspects of building such a

system, complete with a runnable example to illustrate its core functionalities.