File size: 4,835 Bytes
3573b21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""MCP config validator — paste an MCP client config, get back issues.

Validates against the common shape used by Claude Desktop, Cursor, Cline,
Windsurf, and Zed. Doesn't try to BE all of those — just a quick sanity check.
"""

import json
import gradio as gr


REQUIRED_TOP = {"mcpServers"}
SERVER_REQUIRED = {"command"}
SERVER_OPTIONAL = {"args", "env", "cwd", "transport", "url"}


def validate(config_text: str):
    if not config_text.strip():
        return "_Paste a config to validate._"

    try:
        cfg = json.loads(config_text)
    except json.JSONDecodeError as e:
        return f"❌ **Invalid JSON:** {e.msg} at line {e.lineno}, col {e.colno}"

    issues = []
    suggestions = []

    if not isinstance(cfg, dict):
        return "❌ Config must be a JSON object at the top level."

    if "mcpServers" not in cfg:
        issues.append("Missing top-level `mcpServers` key.")
        return _format(issues, suggestions, cfg)

    servers = cfg["mcpServers"]
    if not isinstance(servers, dict):
        issues.append("`mcpServers` must be an object mapping server name → server config.")
        return _format(issues, suggestions, cfg)

    if not servers:
        issues.append("`mcpServers` is empty — no servers will be loaded.")

    for name, scfg in servers.items():
        if not isinstance(scfg, dict):
            issues.append(f"Server `{name}` is not an object.")
            continue
        # Either command-based (stdio) or url-based (sse/http) — must have one
        if "command" not in scfg and "url" not in scfg:
            issues.append(f"Server `{name}` has neither `command` (stdio) nor `url` (sse/http).")
        if "command" in scfg and "url" in scfg:
            suggestions.append(f"Server `{name}` has both `command` and `url`. Most clients use one.")
        if "args" in scfg and not isinstance(scfg["args"], list):
            issues.append(f"Server `{name}`: `args` must be a list of strings.")
        if "env" in scfg and not isinstance(scfg["env"], dict):
            issues.append(f"Server `{name}`: `env` must be an object of string→string.")
        unknown = set(scfg.keys()) - SERVER_REQUIRED - SERVER_OPTIONAL
        if unknown:
            suggestions.append(f"Server `{name}` has unknown keys: {sorted(unknown)} (might be client-specific).")

    return _format(issues, suggestions, cfg)


def _format(issues, suggestions, cfg):
    n_servers = len(cfg.get("mcpServers", {})) if isinstance(cfg.get("mcpServers"), dict) else 0
    rows = []
    if not issues:
        rows.append(f"✅ **Looks good** — {n_servers} server(s) configured.")
    else:
        rows.append(f"❌ **{len(issues)} issue(s):**")
        for i in issues:
            rows.append(f"- {i}")
    if suggestions:
        rows.append("")
        rows.append("💡 **Suggestions:**")
        for s in suggestions:
            rows.append(f"- {s}")
    return "\n".join(rows)


EXAMPLES = [
    ['{"mcpServers": {"filesystem": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me"]}}}'],
    ['{"mcpServers": {"agentfit": {"command": "npx", "args": ["-y", "@mukundakatta/agentfit-mcp"]}}}'],
    ['{"mcpServers": {"broken": {"args": ["only-args"]}}}'],
    ['{"mcpServers": {}}'],
    ['{}'],
    ['not even json'],
]


with gr.Blocks(title="MCP Config Validator", theme=gr.themes.Soft()) as demo:
    gr.Markdown(
        """
        # MCP Config Validator

        Paste your MCP client config (Claude Desktop / Cursor / Cline / Windsurf / Zed style) and get a quick sanity check.

        Doesn't replace your client's own validator — just catches the common mistakes (missing `command`, wrong types, empty servers map) before you restart the client.

        Sample configs from [`mcp-config-examples`](https://huggingface.co/datasets/mukunda1729/mcp-config-examples).
        """
    )

    with gr.Row():
        with gr.Column():
            txt = gr.Code(
                value=json.dumps({
                    "mcpServers": {
                        "filesystem": {
                            "command": "npx",
                            "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me"],
                        }
                    }
                }, indent=2),
                language="json",
                label="MCP config",
                lines=14,
            )
            btn = gr.Button("Validate", variant="primary")
        out = gr.Markdown()
    btn.click(validate, inputs=txt, outputs=out)

    gr.Examples(examples=EXAMPLES, inputs=txt)

    gr.Markdown(
        """
        ---
        Part of [The Agent Reliability Stack](https://mukundakatta.github.io/agent-stack/) · MIT licensed
        """
    )


if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)