minecode-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
minecode/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """
2
+ MineCode - MCP Server for Minecraft Datapack Development
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __author__ = "Your Name"
@@ -0,0 +1,7 @@
1
+ # MineCode Scrappers Module
2
+ from . import mojira
3
+ from . import minecraftwiki
4
+ from . import spyglass
5
+ from . import misode
6
+
7
+ __all__ = ["mojira", "minecraftwiki", "spyglass", "misode"]
@@ -0,0 +1,518 @@
1
+ """
2
+ Minecraft.wiki API Client for Minecraft wiki pages
3
+ Uses the MediaWiki API: https://www.mediawiki.org/wiki/API:Main_page
4
+ """
5
+
6
+ import requests
7
+ from bs4 import BeautifulSoup
8
+ from dataclasses import dataclass
9
+ from typing import Optional, List, Dict, Any
10
+ import re
11
+
12
+ BASE_URL = "https://minecraft.wiki"
13
+ API_URL = "https://minecraft.wiki/api.php"
14
+ PAGE_URL = "https://minecraft.wiki/w/"
15
+
16
+
17
+ # ============================================================================
18
+ # Data Classes
19
+ # ============================================================================
20
+
21
+ @dataclass
22
+ class SearchResult:
23
+ """Represents a wiki search result"""
24
+ title: str
25
+ url: str
26
+ snippet: str = ""
27
+
28
+
29
+ @dataclass
30
+ class PageInfo:
31
+ """Represents basic page information"""
32
+ pageid: int
33
+ title: str
34
+ url: str
35
+
36
+
37
+ @dataclass
38
+ class PageContent:
39
+ """Represents parsed page content"""
40
+ title: str
41
+ pageid: int
42
+ url: str
43
+ extract: str # Plain text summary
44
+ sections: List[Dict[str, Any]]
45
+ categories: List[str]
46
+
47
+
48
+ @dataclass
49
+ class CommandInfo:
50
+ """Represents a Minecraft command"""
51
+ name: str
52
+ url: str
53
+
54
+
55
+ # ============================================================================
56
+ # API Functions
57
+ # ============================================================================
58
+
59
+ def _make_request(params: dict) -> dict:
60
+ """Make a request to the MediaWiki API"""
61
+ params["format"] = "json"
62
+ response = requests.get(API_URL, params=params, timeout=15)
63
+ response.raise_for_status()
64
+ return response.json()
65
+
66
+
67
+ def search(query: str, limit: int = 10) -> List[SearchResult]:
68
+ """
69
+ Search the Minecraft Wiki using OpenSearch protocol.
70
+
71
+ Args:
72
+ query: Search query string
73
+ limit: Maximum number of results (default 10, max 100)
74
+
75
+ Returns:
76
+ List of SearchResult objects with title and URL
77
+ """
78
+ params = {
79
+ "action": "opensearch",
80
+ "search": query,
81
+ "limit": min(limit, 100),
82
+ "namespace": 0 # Main namespace only
83
+ }
84
+
85
+ data = _make_request(params)
86
+
87
+ # OpenSearch returns: [query, [titles], [descriptions], [urls]]
88
+ results = []
89
+ if len(data) >= 4:
90
+ titles = data[1]
91
+ descriptions = data[2]
92
+ urls = data[3]
93
+
94
+ for i, title in enumerate(titles):
95
+ results.append(SearchResult(
96
+ title=title,
97
+ url=urls[i] if i < len(urls) else f"{PAGE_URL}{title.replace(' ', '_')}",
98
+ snippet=descriptions[i] if i < len(descriptions) else ""
99
+ ))
100
+
101
+ return results
102
+
103
+
104
+ def search_fulltext(query: str, limit: int = 10) -> List[SearchResult]:
105
+ """
106
+ Full-text search with snippets showing matches.
107
+
108
+ Args:
109
+ query: Search query string
110
+ limit: Maximum number of results
111
+
112
+ Returns:
113
+ List of SearchResult objects with snippets
114
+ """
115
+ params = {
116
+ "action": "query",
117
+ "list": "search",
118
+ "srsearch": query,
119
+ "srlimit": min(limit, 50),
120
+ "srprop": "snippet|titlesnippet"
121
+ }
122
+
123
+ data = _make_request(params)
124
+
125
+ results = []
126
+ for item in data.get("query", {}).get("search", []):
127
+ # Clean HTML from snippet
128
+ snippet = re.sub(r'<[^>]+>', '', item.get("snippet", ""))
129
+ results.append(SearchResult(
130
+ title=item["title"],
131
+ url=f"{PAGE_URL}{item['title'].replace(' ', '_')}",
132
+ snippet=snippet
133
+ ))
134
+
135
+ return results
136
+
137
+
138
+ def get_page_extract(title: str, sentences: int = 5) -> Optional[str]:
139
+ """
140
+ Get a plain text extract/summary of a page.
141
+
142
+ Args:
143
+ title: Page title
144
+ sentences: Number of sentences to extract
145
+
146
+ Returns:
147
+ Plain text summary or None if page not found
148
+ """
149
+ params = {
150
+ "action": "query",
151
+ "titles": title,
152
+ "prop": "extracts",
153
+ "exintro": True, # Only intro section
154
+ "explaintext": True, # Plain text, no HTML
155
+ "exsentences": sentences
156
+ }
157
+
158
+ data = _make_request(params)
159
+ pages = data.get("query", {}).get("pages", {})
160
+
161
+ for page_id, page in pages.items():
162
+ if page_id != "-1":
163
+ return page.get("extract", "")
164
+
165
+ return None
166
+
167
+
168
+ def get_page_content(title: str) -> Optional[PageContent]:
169
+ """
170
+ Get full parsed content of a page including sections and categories.
171
+
172
+ Args:
173
+ title: Page title
174
+
175
+ Returns:
176
+ PageContent object or None if page not found
177
+ """
178
+ params = {
179
+ "action": "parse",
180
+ "page": title,
181
+ "prop": "text|sections|categories",
182
+ "disabletoc": True
183
+ }
184
+
185
+ try:
186
+ data = _make_request(params)
187
+ except requests.exceptions.HTTPError:
188
+ return None
189
+
190
+ if "error" in data:
191
+ return None
192
+
193
+ parse = data.get("parse", {})
194
+
195
+ # Extract plain text from HTML
196
+ html = parse.get("text", {}).get("*", "")
197
+ soup = BeautifulSoup(html, "html.parser")
198
+
199
+ # Remove unwanted elements
200
+ for elem in soup.find_all(["script", "style", "table", "div"]):
201
+ if elem.get("class") and "infobox" in " ".join(elem.get("class", [])):
202
+ elem.decompose()
203
+
204
+ # Get text content
205
+ text = soup.get_text(separator="\n", strip=True)
206
+ # Clean up excessive whitespace
207
+ text = re.sub(r'\n{3,}', '\n\n', text)
208
+
209
+ # Extract sections
210
+ sections = [
211
+ {"index": s["index"], "level": s["level"], "name": s["line"]}
212
+ for s in parse.get("sections", [])
213
+ ]
214
+
215
+ # Extract categories
216
+ categories = [
217
+ cat["*"] for cat in parse.get("categories", [])
218
+ ]
219
+
220
+ return PageContent(
221
+ title=parse.get("title", title),
222
+ pageid=parse.get("pageid", 0),
223
+ url=f"{PAGE_URL}{title.replace(' ', '_')}",
224
+ extract=text[:5000], # Limit size
225
+ sections=sections,
226
+ categories=categories
227
+ )
228
+
229
+
230
+ def get_page_sections(title: str) -> List[Dict[str, Any]]:
231
+ """
232
+ Get the section structure of a page.
233
+
234
+ Args:
235
+ title: Page title
236
+
237
+ Returns:
238
+ List of section dictionaries with index, level, and name
239
+ """
240
+ params = {
241
+ "action": "parse",
242
+ "page": title,
243
+ "prop": "sections"
244
+ }
245
+
246
+ try:
247
+ data = _make_request(params)
248
+ except:
249
+ return []
250
+
251
+ return [
252
+ {"index": s["index"], "level": int(s["level"]), "name": s["line"]}
253
+ for s in data.get("parse", {}).get("sections", [])
254
+ ]
255
+
256
+
257
+ def get_category_members(category: str, limit: int = 50) -> List[PageInfo]:
258
+ """
259
+ Get all pages in a category.
260
+
261
+ Args:
262
+ category: Category name (with or without "Category:" prefix)
263
+ limit: Maximum number of results
264
+
265
+ Returns:
266
+ List of PageInfo objects
267
+ """
268
+ if not category.startswith("Category:"):
269
+ category = f"Category:{category}"
270
+
271
+ params = {
272
+ "action": "query",
273
+ "list": "categorymembers",
274
+ "cmtitle": category,
275
+ "cmlimit": min(limit, 500),
276
+ "cmtype": "page" # Only pages, not subcategories
277
+ }
278
+
279
+ data = _make_request(params)
280
+
281
+ return [
282
+ PageInfo(
283
+ pageid=item["pageid"],
284
+ title=item["title"],
285
+ url=f"{PAGE_URL}{item['title'].replace(' ', '_')}"
286
+ )
287
+ for item in data.get("query", {}).get("categorymembers", [])
288
+ ]
289
+
290
+
291
+ def get_commands(limit: int = 100) -> List[CommandInfo]:
292
+ """
293
+ Get list of all Minecraft commands from the wiki.
294
+
295
+ Args:
296
+ limit: Maximum number of commands to return
297
+
298
+ Returns:
299
+ List of CommandInfo objects
300
+ """
301
+ pages = get_category_members("Commands", limit=limit)
302
+
303
+ commands = []
304
+ for page in pages:
305
+ # Filter to only actual command pages
306
+ if page.title.startswith("Commands/"):
307
+ cmd_name = page.title.replace("Commands/", "")
308
+ commands.append(CommandInfo(
309
+ name=cmd_name,
310
+ url=page.url
311
+ ))
312
+
313
+ return commands
314
+
315
+
316
+ def get_command_info(command: str) -> Optional[str]:
317
+ """
318
+ Get information about a specific command.
319
+
320
+ Args:
321
+ command: Command name (e.g., "execute", "give")
322
+
323
+ Returns:
324
+ Plain text description of the command
325
+ """
326
+ # Try with Commands/ prefix first
327
+ extract = get_page_extract(f"Commands/{command}", sentences=10)
328
+ if extract:
329
+ return extract
330
+
331
+ # Try without prefix
332
+ return get_page_extract(command, sentences=10)
333
+
334
+
335
+ def get_version_info(version: str) -> Optional[str]:
336
+ """
337
+ Get information about a specific Minecraft version.
338
+
339
+ Args:
340
+ version: Version string (e.g., "1.20.1", "Java Edition 1.20")
341
+
342
+ Returns:
343
+ Plain text description of the version
344
+ """
345
+ # Try different page name formats
346
+ candidates = [
347
+ f"Java Edition {version}",
348
+ version,
349
+ f"Bedrock Edition {version}"
350
+ ]
351
+
352
+ for title in candidates:
353
+ extract = get_page_extract(title, sentences=10)
354
+ if extract:
355
+ return extract
356
+
357
+ return None
358
+
359
+
360
+ def get_block_info(block: str) -> Optional[PageContent]:
361
+ """
362
+ Get information about a specific block.
363
+
364
+ Args:
365
+ block: Block name (e.g., "Stone", "Diamond Ore")
366
+
367
+ Returns:
368
+ PageContent object with block information
369
+ """
370
+ return get_page_content(block)
371
+
372
+
373
+ def get_item_info(item: str) -> Optional[PageContent]:
374
+ """
375
+ Get information about a specific item.
376
+
377
+ Args:
378
+ item: Item name (e.g., "Diamond Sword", "Ender Pearl")
379
+
380
+ Returns:
381
+ PageContent object with item information
382
+ """
383
+ return get_page_content(item)
384
+
385
+
386
+ def get_mob_info(mob: str) -> Optional[PageContent]:
387
+ """
388
+ Get information about a specific mob/entity.
389
+
390
+ Args:
391
+ mob: Mob name (e.g., "Creeper", "Enderman")
392
+
393
+ Returns:
394
+ PageContent object with mob information
395
+ """
396
+ return get_page_content(mob)
397
+
398
+
399
+ def get_random_pages(count: int = 5) -> List[PageInfo]:
400
+ """
401
+ Get random wiki pages.
402
+
403
+ Args:
404
+ count: Number of random pages to get
405
+
406
+ Returns:
407
+ List of PageInfo objects
408
+ """
409
+ params = {
410
+ "action": "query",
411
+ "list": "random",
412
+ "rnnamespace": 0, # Main namespace
413
+ "rnlimit": min(count, 20)
414
+ }
415
+
416
+ data = _make_request(params)
417
+
418
+ return [
419
+ PageInfo(
420
+ pageid=item["id"],
421
+ title=item["title"],
422
+ url=f"{PAGE_URL}{item['title'].replace(' ', '_')}"
423
+ )
424
+ for item in data.get("query", {}).get("random", [])
425
+ ]
426
+
427
+
428
+ # ============================================================================
429
+ # Conversion Functions
430
+ # ============================================================================
431
+
432
+ def search_to_dict(results: List[SearchResult]) -> List[dict]:
433
+ """Convert SearchResult list to dict list"""
434
+ return [
435
+ {"title": r.title, "url": r.url, "snippet": r.snippet}
436
+ for r in results
437
+ ]
438
+
439
+
440
+ def page_info_to_dict(pages: List[PageInfo]) -> List[dict]:
441
+ """Convert PageInfo list to dict list"""
442
+ return [
443
+ {"pageid": p.pageid, "title": p.title, "url": p.url}
444
+ for p in pages
445
+ ]
446
+
447
+
448
+ def page_content_to_dict(content: Optional[PageContent]) -> Optional[dict]:
449
+ """Convert PageContent to dict"""
450
+ if not content:
451
+ return None
452
+ return {
453
+ "title": content.title,
454
+ "pageid": content.pageid,
455
+ "url": content.url,
456
+ "extract": content.extract,
457
+ "sections": content.sections,
458
+ "categories": content.categories
459
+ }
460
+
461
+
462
+ def commands_to_dict(commands: List[CommandInfo]) -> List[dict]:
463
+ """Convert CommandInfo list to dict list"""
464
+ return [
465
+ {"name": c.name, "url": c.url}
466
+ for c in commands
467
+ ]
468
+
469
+
470
+ # ============================================================================
471
+ # Main / Testing
472
+ # ============================================================================
473
+
474
+ if __name__ == "__main__":
475
+ print("=" * 60)
476
+ print("Minecraft Wiki API Client Tests")
477
+ print("=" * 60)
478
+
479
+ # Test 1: Search
480
+ print("\nšŸ” Test 1: Search for 'creeper'")
481
+ results = search("creeper", limit=5)
482
+ for r in results:
483
+ print(f" - {r.title}: {r.url}")
484
+
485
+ # Test 2: Full-text search
486
+ print("\nšŸ” Test 2: Full-text search for 'explosion damage'")
487
+ results = search_fulltext("explosion damage", limit=3)
488
+ for r in results:
489
+ print(f" - {r.title}")
490
+ print(f" Snippet: {r.snippet[:100]}...")
491
+
492
+ # Test 3: Get page extract
493
+ print("\nšŸ“„ Test 3: Get extract for 'Creeper'")
494
+ extract = get_page_extract("Creeper", sentences=3)
495
+ if extract:
496
+ print(f" {extract[:300]}...")
497
+
498
+ # Test 4: Get commands
499
+ print("\nāŒØļø Test 4: Get Minecraft commands")
500
+ commands = get_commands(limit=10)
501
+ for cmd in commands[:10]:
502
+ print(f" - /{cmd.name}")
503
+
504
+ # Test 5: Get category members
505
+ print("\nšŸ“ Test 5: Get blocks category (first 5)")
506
+ blocks = get_category_members("Blocks", limit=5)
507
+ for b in blocks:
508
+ print(f" - {b.title}")
509
+
510
+ # Test 6: Get version info
511
+ print("\nšŸŽ® Test 6: Get version info for '1.20'")
512
+ version = get_version_info("1.20")
513
+ if version:
514
+ print(f" {version[:200]}...")
515
+
516
+ print("\n" + "=" * 60)
517
+ print("āœ… All tests completed!")
518
+ print("=" * 60)