Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pages/assets/images/docker-mark-blue.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 55 additions & 22 deletions pages/registry/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,26 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mcpm.sh - Server Registry</title>
{% include favicon.html %}

<!-- Primary Meta Tags -->
<meta name="title" content="mcpm.sh - Server Registry">
<meta name="description" content="The single open source MCP registry we all need. Discover and explore MCP servers for your projects.">
<meta name="keywords" content="MCP, server registry, AI, machine learning, open source">

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpm.sh/registry/">
<meta property="og:title" content="mcpm.sh - Server Registry">
<meta property="og:description" content="The single open source MCP registry we all need. Discover and explore MCP servers for your projects.">
<meta property="og:image" content="https://mcpm.sh/assets/images/mcpm-social.png">

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://mcpm.sh/registry/">
<meta property="twitter:title" content="mcpm.sh - Server Registry">
<meta property="twitter:description" content="The single open source MCP registry we all need. Discover and explore MCP servers for your projects.">
<meta property="twitter:image" content="https://mcpm.sh/assets/images/mcpm-social.png">

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
Expand All @@ -41,7 +41,7 @@
<style>
{% include colors.css %}
{% include common.css %}

/* Override colors with blue and pink theme */
:root {
--accent-color: #3b82f6;
Expand Down Expand Up @@ -372,7 +372,7 @@
align-items: center;
margin: 0;
}

.server-card .category:hover {
background-color: var(--accent-color);
color: white;
Expand Down Expand Up @@ -1216,12 +1216,13 @@
}

.server-card .meta .category {
background-color: var(--accent-light);
color: var(--accent-color);
background-color: var(--accent-color);
color: white;
padding: 0.1rem 0.5rem;
border-radius: 3px;
font-size: 0.7rem;
margin-left: 0.5rem;
font-weight: 600;
}

.official-badge {
Expand All @@ -1237,6 +1238,21 @@
text-transform: uppercase;
letter-spacing: 0.5px;
}

.docker-icon {
display: inline-block;
margin-left: 8px;
vertical-align: middle;
color: #2496ed;
}

.docker-icon svg {
vertical-align: middle;
}

.docker-icon:hover {
opacity: 0.8;
}
</style>
</head>

Expand Down Expand Up @@ -1635,7 +1651,7 @@ <h3>Raw Manifest:</h3>
const summary = document.createElement('summary');
summary.textContent = key;
details.appendChild(summary);

const subUl = document.createElement('ul');
Object.entries(value).forEach(([subKey, subValue]) => {
const subLi = document.createElement('li');
Expand All @@ -1660,7 +1676,7 @@ <h3>Raw Manifest:</h3>
const summary = document.createElement('summary');
summary.textContent = tool.name || `Tool ${index + 1}`;
details.appendChild(summary);

const subUl = document.createElement('ul');
Object.entries(tool).forEach(([key, value]) => {
const subLi = document.createElement('li');
Expand All @@ -1685,7 +1701,7 @@ <h3>Raw Manifest:</h3>
const summary = document.createElement('summary');
summary.textContent = resource.name || `Resource ${index + 1}`;
details.appendChild(summary);

const subUl = document.createElement('ul');
Object.entries(resource).forEach(([key, value]) => {
const subLi = document.createElement('li');
Expand All @@ -1710,7 +1726,7 @@ <h3>Raw Manifest:</h3>
const summary = document.createElement('summary');
summary.textContent = prompt.name || `Prompt ${index + 1}`;
details.appendChild(summary);

const subUl = document.createElement('ul');
Object.entries(prompt).forEach(([key, value]) => {
const subLi = document.createElement('li');
Expand Down Expand Up @@ -1863,15 +1879,32 @@ <h3>Raw Manifest:</h3>

const heading = document.createElement('h3');
heading.textContent = server.display_name || server.name;


if (server.docker_url) {
const dockerIcon = document.createElement('span');
dockerIcon.className = 'docker-icon';
dockerIcon.innerHTML = '<img src="/assets/images/docker-mark-blue.svg" alt="Docker" width="16" height="16">';
dockerIcon.style.cursor = 'pointer';
dockerIcon.title = 'Docker';

// Open Docker Hub link in new tab
dockerIcon.addEventListener('click', (event) => {
event.stopPropagation();
window.open(server.docker_url, '_blank');
});

// Add Docker icon to heading
heading.appendChild(dockerIcon);
}

// Add official badge if the server is marked as official
if (server.is_official) {
const officialBadge = document.createElement('span');
officialBadge.className = 'official-badge';
officialBadge.textContent = 'Official';
heading.appendChild(officialBadge);
}

serverHeader.appendChild(heading);

// Add placeholder for GitHub stars in the header
Expand All @@ -1887,11 +1920,11 @@ <h3>Raw Manifest:</h3>
description.className = 'description';
description.textContent = server.description;
serverContent.appendChild(description);

// Create tags container
const tagsContainer = document.createElement('div');
tagsContainer.className = 'tags';

// Add regular tags (if any)
if (server.tags && server.tags.length > 0) {
server.tags.slice(0, 4).forEach(tag => {
Expand All @@ -1901,28 +1934,28 @@ <h3>Raw Manifest:</h3>
tagsContainer.appendChild(tagSpan);
});
}

// Append tags container to server content
serverContent.appendChild(tagsContainer);

const meta = document.createElement('div');
meta.className = 'meta';

// Add placeholder for author that will be populated later
const authorMeta = document.createElement('span');
authorMeta.className = 'author author-placeholder';
authorMeta.textContent = 'Loading author...';
authorMeta.style.opacity = '0';
meta.appendChild(authorMeta);

// Add categories if they exist
if (server.categories && server.categories.length > 0) {
const categorySpan = document.createElement('span');
categorySpan.className = 'category';
categorySpan.textContent = server.categories[0]; // Display first category
meta.appendChild(categorySpan);
}

// Update author information directly from server data (no need to fetch)
if (server.author && server.author.name) {
// Add user icon before author name
Expand Down Expand Up @@ -2188,4 +2221,4 @@ <h3>Raw Manifest:</h3>
</script>
</body>

</html>
</html>
10 changes: 9 additions & 1 deletion scripts/get_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
import requests
from categorization import CategorizationAgent
from openai import OpenAI
from utils import McpClient, validate_arguments_in_installation
from utils import McpClient, inspect_docker_repo, validate_arguments_in_installation

dotenv.load_dotenv()
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

_OUTPUT_DIR = "mcp-registry/servers/"
DOCKER_MCP_REPO_URL = "https://hub.docker.com/r"


class ManifestGenerator:
Expand Down Expand Up @@ -781,6 +782,13 @@ def generate_manifest(self, repo_url: str, server_name: Optional[str] = None) ->
except Exception as e:
logger.error(f"Failed to extract capabilities: {e}")

# docker url if docker command in installation
installations = manifest.get("installations", {})
if "docker" in installations:
docker_repo_name = inspect_docker_repo(installations["docker"])
if docker_repo_name:
manifest["docker_url"] = f"{DOCKER_MCP_REPO_URL}/{docker_repo_name}"

return manifest

except Exception as e:
Expand Down
33 changes: 32 additions & 1 deletion scripts/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import re
from contextlib import AsyncExitStack
from datetime import timedelta
from typing import Any
from typing import Any, Optional

import requests
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.shared.exceptions import McpError
from mcp.types import ListPromptsResult, ListResourcesResult, ListToolsResult

DOCKER_HUB_REPO_URL = "https://hub.docker.com/v2/repositories/"

class McpClient:
session: ClientSession
Expand Down Expand Up @@ -148,3 +150,32 @@ def validate_arguments_in_installation(
installation["env"] = env

return installation, replacement


def validate_docker_url(docker_url: str) -> bool:
try:
response = requests.get(docker_url)
# if success with status code 200, the repo should be a valid and registered one
return response.status_code == 200
except Exception:
return False


def inspect_docker_repo(installation: dict[str, Any]) -> Optional[str]:
""" inspect the docker url from docker installation args, the args should pattern as {namespace}/{repo_name} where namespace=mcp
Example
if args = ["run", "-i", "--rm", "-e", "PERPLEXITY_API_KEY", "mcp/perplexity-ask"]
return "mcp/perplexity-ask"
"""
repo_name = None
if "args" in installation and installation["args"]:
args = installation["args"]
for arg in args:
# namespace/repo(:tag)
repo_match = re.match(r"^(mcp/[\w-]+)(?::[\w.\-]+)?$", arg)
if repo_match:
repo_name = repo_match.group(1)
if validate_docker_url(DOCKER_HUB_REPO_URL + repo_name):
return repo_name # namespace/repo without tag

return None