Skip to content
tnsaiDocs
GitHub

MCP registry

UnifiedMcpRegistry (in com.tnsai.mcp.registry) searches public MCP server catalogs and merges the answers into one list. Use it to find a server's endpoint; connect to the server with a transport and the client.

Example

package com.example.tnsai.docs;

import com.tnsai.mcp.registry.McpRegistrySource;
import com.tnsai.mcp.registry.McpServerEntry;
import com.tnsai.mcp.registry.OfficialRegistryClient;
import com.tnsai.mcp.registry.SmitheryClient;
import com.tnsai.mcp.registry.UnifiedMcpRegistry;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

/** Compilation contract for mcp/registry.md. */
public final class McpRegistryExample {

    private McpRegistryExample() {
    }

    static Optional<String> findEndpoint() {
        UnifiedMcpRegistry registry = new UnifiedMcpRegistry();          // all four public sources
        List<McpServerEntry> hits = registry.search("filesystem");
        hits.forEach(e -> System.out.println(e.name() + " (" + e.source() + "): " + e.endpoint()));
        return registry.getServer("filesystem").map(McpServerEntry::endpoint);
    }

    public static final class InternalCatalog implements McpRegistrySource {

        private final List<McpServerEntry> entries = List.of(new McpServerEntry(
            "billing", "Billing tools", "2.1.0", null, "streamable-http",
            "https://mcp.internal.example.com/billing", "internal", 0.0, Map.of("team", "payments")));

        @Override
        public String getSourceName() {
            return "internal";
        }

        @Override
        public List<McpServerEntry> search(String query) {
            String q = query == null ? "" : query.toLowerCase(Locale.ROOT);
            return entries.stream().filter(e -> e.name().contains(q)).toList();
        }

        @Override
        public Optional<McpServerEntry> getServer(String name) {
            return entries.stream().filter(e -> e.name().equals(name)).findFirst();
        }

        @Override
        public List<McpServerEntry> listAll() {
            return entries;
        }
    }

    static UnifiedMcpRegistry customSources() {
        UnifiedMcpRegistry registry = new UnifiedMcpRegistry(
            List.of(new OfficialRegistryClient(), new SmitheryClient()),
            Duration.ofMinutes(5));
        registry.addSource(new InternalCatalog());
        registry.removeSource("smithery");
        registry.clearCache();                    // the next search asks the sources again
        return registry;
    }
}

Sources

new UnifiedMcpRegistry() uses the four built-in sources with a 10-minute cache. new UnifiedMcpRegistry(sources, cacheTtl) takes your own list; a null TTL means 10 minutes.

ClassSource nameHostPriority
OfficialRegistryClientofficialregistry.modelcontextprotocol.io4
CamelMcpHubClientcamelmcp.camel-ai.org3
SmitheryClientsmitherysmithery.ai2
McpServersOrgClientmcpservers.orgmcpservers.org1

Each built-in client sends a GET with a 15-second timeout and takes an optional HttpClient in its constructor. Any other source name has priority 0.

addSource(source) and removeSource(name) change the list at run time; getSources() returns a copy of it. To add your own catalog, implement McpRegistrySource: getSourceName(), search(query), getServer(name) and listAll().

search(query) asks every source in turn.

  • A source that throws IOException is logged and skipped. A built-in source also skips entries it cannot parse.
  • A null or blank query asks each built-in source for its unfiltered list.
  • Results are merged by name, ignoring case. When two sources list the same name, the higher-priority source wins.
  • The list is sorted by source priority, then by rating, highest first.

getServer(name) asks the sources one at a time, highest priority first, and returns the first match. A built-in source answers empty for any error, including an unknown name.

Both methods drop entries whose protocolVersion is set and is neither 2024-11-05 nor 2025-11-05. The built-in sources leave it null, so they are never dropped.

Cache

Answers are cached per lowercased query or name for the TTL. A search is cached even when it is empty, including when every source failed, so call clearCache() to ask again before the TTL ends. getServer caches only a match. getCacheSize() returns the number of cached answers.

Server entries

McpServerEntry is a record.

FieldMeaning
nameThe server name; required, not blank
description, versionAs the source lists them
protocolVersionThe MCP version, or null when unknown
transportFor example stdio, sse or streamable-http; the built-in sources default to stdio
endpointA URL or a command; the built-in sources read endpoint, then url, then command
sourceThe source name
rating0 to 5; a value outside that range throws IllegalArgumentException
metadataExtra values: the source's metadata object plus author and homepage when present, and repository from the official and Smithery sources

A registry entry is unverified catalog data. Check the endpoint or command before you run it.