File size: 4,619 Bytes
db14e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"""Sandbox testability verifier for MatrixHub catalog entries.

The public Matrix Hub API exposes *metadata only* (id, name, type, source_url,
…) — it does NOT return a runnable build (no ``packages`` / ``mcp_registration``
/ exec command). The no-secrets sandbox therefore can only trial servers for
which we have a known, packaged, credential-free run command.

This module is the single source of truth for "can this entry be tested in our
sandbox?". It is mirrored in the matrixhub UI by ``lib/sandboxEligibility.ts``
(keep the two registries in sync). See
``matrixhub/docs/SANDBOX_TESTABILITY.md`` for the full design + reproduction.

Verdict:
    {
      "testable": bool,
      "reason": str,            # human-readable, shown as the UI tag tooltip
      "runtime": "node|python", # only when testable
      "transport": "stdio",     # only when testable
      "start_command": "...",   # only when testable
    }
"""
from __future__ import annotations

import re
from typing import Any


# --- Curated registry of sandbox-runnable MCP servers ----------------------
# These are packaged, credential-free MCP servers verified to start in the
# no-secrets sandbox (npx for Node, uvx for Python). Matched against a
# haystack of id + name + source_url. A match requires EITHER a strict
# ``pattern`` (explicit ``server-<key>`` / ``mcp-server-<key>`` token) OR the
# official ``modelcontextprotocol`` repo plus the ``word``. This keeps the
# "testable" tag conservative — we never mislabel an unrelated server.
# Order matters (first match wins).
SANDBOX_REGISTRY: list[dict[str, Any]] = [
    {"key": "filesystem", "pattern": r"(?:mcp[-_])?server[-_]filesystem", "word": r"\bfilesystem\b",
     "runtime": "node", "start_command": "npx -y @modelcontextprotocol/server-filesystem /tmp"},
    {"key": "everything", "pattern": r"(?:mcp[-_])?server[-_]everything", "word": r"\beverything\b",
     "runtime": "node", "start_command": "npx -y @modelcontextprotocol/server-everything"},
    {"key": "memory", "pattern": r"(?:mcp[-_])?server[-_]memory", "word": r"\bmemory\b",
     "runtime": "node", "start_command": "npx -y @modelcontextprotocol/server-memory"},
    {"key": "sequentialthinking", "pattern": r"sequential[-_]?thinking", "word": r"sequential[-_]?thinking",
     "runtime": "node", "start_command": "npx -y @modelcontextprotocol/server-sequential-thinking"},
    {"key": "time", "pattern": r"(?:mcp[-_])?server[-_]time", "word": r"\btime\b",
     "runtime": "python", "start_command": "uvx mcp-server-time"},
    {"key": "fetch", "pattern": r"(?:mcp[-_])?server[-_]fetch", "word": r"\bfetch\b",
     "runtime": "python", "start_command": "uvx mcp-server-fetch"},
    {"key": "git", "pattern": r"(?:mcp[-_])?server[-_]git\b", "word": r"\bgit\b",
     "runtime": "python", "start_command": "uvx mcp-server-git"},
]

OFFICIAL_RE = r"modelcontextprotocol"

# Signals that a server CANNOT run in a no-secrets sandbox even if packaged:
# SaaS connectors / anything that needs real API credentials to do anything.
BLOCKERS: list[dict[str, str]] = [
    {"pattern": r"retell|talentlms|microsoft|\boffice\b|openai|anthropic|slack|"
                r"salesforce|notion|gmail|google|stripe|twilio|hubspot|jira|"
                r"confluence|aws|azure|\bgcp\b|databricks|snowflake|watsonx|"
                r"sendgrid|sentry|datadog|pagerduty|zendesk|shopify",
     "reason": "SaaS connector — requires API credentials"},
]

NOT_PACKAGED_REASON = (
    "No packaged sandbox build available "
    "(the public catalog exposes no credential-free run command)"
)


def _haystack(entity: dict[str, Any]) -> str:
    return " ".join(str(entity.get(k) or "") for k in ("id", "name", "source_url", "homepage")).lower()


def verify(entity: dict[str, Any]) -> dict[str, Any]:
    """Return a sandbox-testability verdict for a catalog entity dict."""
    hay = _haystack(entity)

    for b in BLOCKERS:
        if re.search(b["pattern"], hay):
            return {"testable": False, "reason": b["reason"]}

    official = bool(re.search(OFFICIAL_RE, hay))
    for entry in SANDBOX_REGISTRY:
        if re.search(entry["pattern"], hay) or (official and re.search(entry["word"], hay)):
            return {
                "testable": True,
                "reason": f"packaged reference server ({entry['key']})",
                "runtime": entry["runtime"],
                "transport": "stdio",
                "start_command": entry["start_command"],
            }

    return {"testable": False, "reason": NOT_PACKAGED_REASON}


def is_testable(entity: dict[str, Any]) -> bool:
    return verify(entity)["testable"]