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 +6 -0
- minecode/scrappers/__init__.py +7 -0
- minecode/scrappers/minecraftwiki.py +518 -0
- minecode/scrappers/misode.py +570 -0
- minecode/scrappers/mojira.py +181 -0
- minecode/scrappers/spyglass.py +432 -0
- minecode/server.py +913 -0
- minecode_mcp-0.1.0.dist-info/METADATA +320 -0
- minecode_mcp-0.1.0.dist-info/RECORD +12 -0
- minecode_mcp-0.1.0.dist-info/WHEEL +4 -0
- minecode_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- minecode_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
minecode/__init__.py
ADDED
|
@@ -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)
|