1818def 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]:
3737def 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
5252def 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+
98126def 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:
196276def 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