Skip to content

Commit cabec4c

Browse files
[generate_manifest] improve registry workflow (#244)
* [generate_manifest] update workflow name * [generate_manifest] improve script
1 parent e19dfff commit cabec4c

File tree

2 files changed

+150
-70
lines changed

2 files changed

+150
-70
lines changed

.github/workflows/generate-manifest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: (TESTING) Generate MCP Manifest
1+
name: Generate MCP Manifest
22

33
on:
44
workflow_dispatch:

scripts/get_manifest.py

Lines changed: 149 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
def extract_json_from_content(content: str) -> Optional[dict]:
1919
"""Extract JSON from the API response content."""
2020
# Look for JSON code block
21-
json_match = re.search(r'```json\n(.*?)\n```', content, re.DOTALL)
21+
json_match = re.search(r"```json\n(.*?)\n```", content, re.DOTALL)
2222
if json_match:
2323
try:
2424
return json.loads(json_match.group(1))
2525
except json.JSONDecodeError as e:
2626
print(f"Error parsing JSON: {e}")
2727
return None
28-
28+
2929
# Try to find JSON without code block markers
3030
try:
3131
return json.loads(content)
@@ -37,16 +37,16 @@ def extract_json_from_content(content: str) -> Optional[dict]:
3737
def get_repo_name_from_url(repo_url: str) -> str:
3838
"""Extract repository name from URL for filename."""
3939
# Remove .git suffix if present
40-
if repo_url.endswith('.git'):
40+
if repo_url.endswith(".git"):
4141
repo_url = repo_url[:-4]
42-
42+
4343
# Extract owner/repo from URL
44-
match = re.search(r'github\.com[:/]([^/]+/[^/]+)', repo_url)
44+
match = re.search(r"github\.com[:/]([^/]+/[^/]+)", repo_url)
4545
if match:
46-
return match.group(1).replace('/', '-')
47-
46+
return match.group(1).replace("/", "-")
47+
4848
# Fallback to last part of URL
49-
return repo_url.split('/')[-1]
49+
return repo_url.split("/")[-1]
5050

5151

5252
def generate_manifest(repo_url: str) -> Optional[dict]:
@@ -55,38 +55,30 @@ def generate_manifest(repo_url: str) -> Optional[dict]:
5555
if not api_key:
5656
print("Error: ANYON_API_KEY environment variable not set")
5757
return None
58-
58+
5959
url = "https://anyon.chatxiv.org/api/v1/openai/v1/chat/completions"
60-
headers = {
61-
"Authorization": f"Bearer {api_key}",
62-
"Content-Type": "application/json"
63-
}
64-
60+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
61+
6562
payload = {
6663
"model": "x",
6764
"messages": [
6865
{
6966
"role": "user",
70-
"content": [
71-
{
72-
"type": "text",
73-
"text": f"help me generate manifest json for this repo: {repo_url}"
74-
}
75-
]
67+
"content": [{"type": "text", "text": f"help me generate manifest json for this repo: {repo_url}"}],
7668
}
77-
]
69+
],
7870
}
79-
71+
8072
try:
8173
print(f"Generating manifest for {repo_url}...")
8274
response = requests.post(url, headers=headers, json=payload)
8375
response.raise_for_status()
84-
76+
8577
data = response.json()
8678
content = data["choices"][0]["message"]["content"]
87-
79+
8880
return extract_json_from_content(content)
89-
81+
9082
except requests.RequestException as e:
9183
print(f"API request failed: {e}")
9284
return None
@@ -95,24 +87,57 @@ def generate_manifest(repo_url: str) -> Optional[dict]:
9587
return None
9688

9789

90+
def validate_installation_entry(install_type: str, entry: dict) -> bool:
91+
"""Validate a single installation entry against MCP registry schema."""
92+
# Required fields for all installation types
93+
required_fields = {"type", "command", "args"}
94+
95+
# Check all required fields exist
96+
if not all(field in entry for field in required_fields):
97+
return False
98+
99+
# Type must match install_type
100+
if entry.get("type") != install_type:
101+
return False
102+
103+
# Type-specific validation based on MCP registry patterns
104+
if install_type == "npm":
105+
# npm type should use npx command with -y flag
106+
if entry.get("command") != "npx":
107+
return False
108+
args = entry.get("args", [])
109+
if not args or args[0] != "-y":
110+
return False
111+
elif install_type == "uvx":
112+
# uvx type should use uvx command
113+
if entry.get("command") != "uvx":
114+
return False
115+
elif install_type == "docker":
116+
# docker type should use docker command
117+
if entry.get("command") != "docker":
118+
return False
119+
args = entry.get("args", [])
120+
if not args or args[0] != "run":
121+
return False
122+
123+
return True
124+
125+
98126
def validate_installations(manifest: dict, repo_url: str) -> Optional[dict]:
99127
"""Validate and correct the installations field by having API check the original README."""
100128
if not manifest:
101129
return manifest
102-
130+
103131
api_key = os.getenv("ANYON_API_KEY")
104132
if not api_key:
105133
print("Error: ANYON_API_KEY environment variable not set, skipping validation")
106134
return manifest
107-
135+
108136
url = "https://anyon.chatxiv.org/api/v1/openai/v1/chat/completions"
109-
headers = {
110-
"Authorization": f"Bearer {api_key}",
111-
"Content-Type": "application/json"
112-
}
113-
137+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
138+
114139
current_installations = manifest.get("installations", {})
115-
140+
116141
payload = {
117142
"model": "x",
118143
"messages": [
@@ -121,50 +146,105 @@ def validate_installations(manifest: dict, repo_url: str) -> Optional[dict]:
121146
"content": [
122147
{
123148
"type": "text",
124-
"text": f"""Please carefully validate and correct the installations field in this manifest by checking the original README.md from the repository.
125-
126-
Repository: {repo_url}
149+
"text": f"""Please read the README.md from {repo_url} and create accurate installation entries following the MCP registry schema.
127150
128-
Current manifest installations:
151+
Current installations:
129152
{json.dumps(current_installations, indent=2)}
130153
131-
IMPORTANT INSTRUCTIONS:
132-
1. Access the README.md from the repository URL: {repo_url}
133-
2. Compare the current installations against the exact commands and configurations shown in the README.md
134-
3. Ensure the command, args, and env variables exactly match what's documented in the README. Remove the installation methods which are not mentioned in README.
135-
4. Pay special attention to:
136-
- Exact command names (npx, uvx, docker, python, etc.)
137-
- Correct package names and arguments (e.g., for npx command, it should usually be "-y [package_name]"
138-
- Proper environment variable names and formats
139-
- Installation type matching the command used
140-
5. Fix any discrepancies between the manifest and the README
141-
6. Return ONLY a valid JSON object with the corrected installations field
142-
7. The response should be in this exact format: {{"installations": {{...}}}}
143-
144-
Focus on accuracy - the installations must work exactly as documented in the README. If the README shows different installation methods, include all valid ones."""
154+
TASK: Find installation instructions in the README and convert them to the exact schema format used in the MCP registry.
155+
156+
INSTALLATION SCHEMA EXAMPLES:
157+
158+
1. NPX INSTALLATIONS (most common):
159+
```json
160+
"npm": {{
161+
"type": "npm",
162+
"command": "npx",
163+
"args": ["-y", "@package/name"],
164+
"description": "Install with npx"
165+
}}
166+
```
167+
168+
2. UVX INSTALLATIONS:
169+
```json
170+
"uvx": {{
171+
"type": "uvx",
172+
"command": "uvx",
173+
"args": ["package-name"],
174+
"description": "Run with Claude Desktop"
175+
}}
176+
```
177+
OR with git URL:
178+
```json
179+
"uvx": {{
180+
"type": "uvx",
181+
"command": "uvx",
182+
"args": ["--from", "git+https://github.com/user/repo", "command-name"],
183+
"description": "Install from git"
184+
}}
185+
```
186+
187+
3. DOCKER INSTALLATIONS:
188+
```json
189+
"docker": {{
190+
"type": "docker",
191+
"command": "docker",
192+
"args": ["run", "-i", "--rm", "image-name"],
193+
"description": "Run using Docker"
194+
}}
195+
```
196+
197+
PROCESS:
198+
1. Read the README.md from {repo_url}
199+
2. Find installation sections (Installation, Setup, Usage, Getting Started, etc.)
200+
3. For each installation method found:
201+
- If README shows "npx package-name" → create npm entry with npx command
202+
- If README shows "uvx package-name" → create uvx entry
203+
- If README shows "docker run ..." → create docker entry
204+
- Copy the EXACT package names and arguments from README
205+
206+
CRITICAL RULES:
207+
- Use exact package names from README (don't guess or modify)
208+
- Match the schema format exactly as shown in examples
209+
- Include ALL installation methods mentioned in README
210+
- Remove installation methods NOT mentioned in README
211+
- For npx: always use type "npm" with command "npx" and args ["-y", "package-name"]
212+
213+
Return ONLY: {{"installations": {{...}}}}""",
145214
}
146-
]
215+
],
147216
}
148-
]
217+
],
149218
}
150-
219+
151220
try:
152221
print("Validating installations against README...")
153222
response = requests.post(url, headers=headers, json=payload)
154223
response.raise_for_status()
155-
224+
156225
data = response.json()
157226
content = data["choices"][0]["message"]["content"]
158-
227+
159228
validated_data = extract_json_from_content(content)
160229
if validated_data and "installations" in validated_data:
161-
print("✓ Installations validated and corrected")
162-
manifest["installations"] = validated_data["installations"]
230+
# Additional validation of each installation entry
231+
cleaned_installations = {}
232+
for install_type, entry in validated_data["installations"].items():
233+
if validate_installation_entry(install_type, entry):
234+
cleaned_installations[install_type] = entry
235+
else:
236+
print(f"⚠ Removing invalid {install_type} installation entry")
237+
238+
if cleaned_installations:
239+
print("✓ Installations validated and corrected")
240+
manifest["installations"] = cleaned_installations
241+
else:
242+
print("⚠ No valid installations found, keeping original")
163243
return manifest
164244
else:
165245
print("⚠ Validation failed, keeping original installations")
166246
return manifest
167-
247+
168248
except Exception as e:
169249
print(f"Error validating installations: {e}")
170250
return manifest
@@ -175,19 +255,19 @@ def save_manifest(manifest: dict, repo_url: str) -> bool:
175255
# Create directory if it doesn't exist
176256
servers_dir = Path("mcp-registry/servers")
177257
servers_dir.mkdir(parents=True, exist_ok=True)
178-
258+
179259
# Generate filename from repo URL
180260
repo_name = get_repo_name_from_url(repo_url)
181261
filename = f"{repo_name}.json"
182262
filepath = servers_dir / filename
183-
263+
184264
try:
185-
with open(filepath, 'w', encoding='utf-8') as f:
265+
with open(filepath, "w", encoding="utf-8") as f:
186266
json.dump(manifest, f, indent=2, ensure_ascii=False)
187-
267+
188268
print(f"Manifest saved to {filepath}")
189269
return True
190-
270+
191271
except IOError as e:
192272
print(f"Failed to save manifest: {e}")
193273
return False
@@ -196,26 +276,26 @@ def save_manifest(manifest: dict, repo_url: str) -> bool:
196276
def main():
197277
parser = argparse.ArgumentParser(description="Generate MCP manifest JSON from repository URL")
198278
parser.add_argument("repo_url", help="Repository URL to generate manifest for")
199-
279+
200280
args = parser.parse_args()
201-
281+
202282
# Step 1: Generate initial manifest
203283
print("Step 1: Generating initial manifest...")
204284
manifest = generate_manifest(args.repo_url)
205285
if not manifest:
206286
print("Failed to generate manifest")
207287
sys.exit(1)
208-
288+
209289
# Step 2: Validate and correct installations
210290
print("Step 2: Validating installations against README...")
211291
manifest = validate_installations(manifest, args.repo_url)
212-
292+
213293
# Step 3: Save manifest
214294
print("Step 3: Saving manifest...")
215295
if not save_manifest(manifest, args.repo_url):
216296
print("Failed to save manifest")
217297
sys.exit(1)
218-
298+
219299
print("✓ Manifest generation completed successfully!")
220300

221301

0 commit comments

Comments
 (0)