sol-mcp 0.2.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.
solana_mcp/server.py ADDED
@@ -0,0 +1,746 @@
1
+ """MCP server for Solana runtime and SIMDs.
2
+
3
+ Exposes tools for searching and analyzing Solana protocol code.
4
+ """
5
+
6
+
7
+ import logging
8
+ from pathlib import Path
9
+
10
+ from mcp.server import Server
11
+ from mcp.server.stdio import stdio_server
12
+ from mcp.types import TextContent, Tool
13
+ from pydantic import ValidationError
14
+
15
+ from .expert.guidance import get_expert_guidance, list_guidance_topics
16
+ from .indexer.compiler import lookup_constant, lookup_function
17
+ from .indexer.downloader import DEFAULT_DATA_DIR, list_downloaded_repos
18
+ from .indexer.embedder import DEPS_AVAILABLE, get_index_stats, search
19
+ from .models import (
20
+ ClientLookupInput,
21
+ ConstantLookupInput,
22
+ FunctionLookupInput,
23
+ GuidanceInput,
24
+ SearchInput,
25
+ )
26
+ from .versions import (
27
+ get_client,
28
+ get_client_diversity,
29
+ get_consensus_status,
30
+ get_current_version,
31
+ list_clients,
32
+ list_feature_gates,
33
+ list_versions,
34
+ )
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # Initialize server
39
+ server = Server("solana-mcp")
40
+
41
+ # Data directory
42
+ DATA_DIR = DEFAULT_DATA_DIR
43
+
44
+
45
+ def _safe_path(base: Path, user_path: str) -> Path:
46
+ """
47
+ Resolve a user-provided path safely within a base directory.
48
+ Prevents path traversal attacks (e.g., ../../../etc/passwd).
49
+
50
+ Args:
51
+ base: The base directory that all paths must stay within
52
+ user_path: User-provided relative path
53
+
54
+ Returns:
55
+ Resolved path within base directory
56
+
57
+ Raises:
58
+ ValueError: If path would escape base directory
59
+ """
60
+ # Resolve the full path
61
+ resolved = (base / user_path).resolve()
62
+
63
+ # Ensure it's still within the base directory
64
+ try:
65
+ resolved.relative_to(base.resolve())
66
+ except ValueError:
67
+ logger.warning("Path traversal attempt blocked: %s", user_path)
68
+ raise ValueError(f"Invalid path: {user_path}")
69
+
70
+ return resolved
71
+
72
+
73
+ @server.list_tools()
74
+ async def list_tools() -> list[Tool]:
75
+ """List available tools."""
76
+ return [
77
+ Tool(
78
+ name="sol_search",
79
+ description=(
80
+ "Search across Solana runtime code and SIMDs. "
81
+ "Use for questions about how Solana works, finding functions, "
82
+ "understanding stake/vote/consensus mechanics."
83
+ ),
84
+ inputSchema={
85
+ "type": "object",
86
+ "properties": {
87
+ "query": {
88
+ "type": "string",
89
+ "description": "Search query (e.g., 'stake warmup period')",
90
+ },
91
+ "limit": {
92
+ "type": "integer",
93
+ "description": "Max results (default: 5)",
94
+ "default": 5,
95
+ },
96
+ },
97
+ "required": ["query"],
98
+ },
99
+ ),
100
+ Tool(
101
+ name="sol_search_runtime",
102
+ description=(
103
+ "Search only Rust runtime code (no SIMDs). "
104
+ "Use when you specifically want implementation details."
105
+ ),
106
+ inputSchema={
107
+ "type": "object",
108
+ "properties": {
109
+ "query": {
110
+ "type": "string",
111
+ "description": "Search query",
112
+ },
113
+ "limit": {
114
+ "type": "integer",
115
+ "description": "Max results (default: 5)",
116
+ "default": 5,
117
+ },
118
+ },
119
+ "required": ["query"],
120
+ },
121
+ ),
122
+ Tool(
123
+ name="sol_search_simd",
124
+ description=(
125
+ "Search only SIMDs (Solana Improvement Documents). "
126
+ "Use for protocol proposals, specifications, and governance."
127
+ ),
128
+ inputSchema={
129
+ "type": "object",
130
+ "properties": {
131
+ "query": {
132
+ "type": "string",
133
+ "description": "Search query",
134
+ },
135
+ "limit": {
136
+ "type": "integer",
137
+ "description": "Max results (default: 5)",
138
+ "default": 5,
139
+ },
140
+ },
141
+ "required": ["query"],
142
+ },
143
+ ),
144
+ Tool(
145
+ name="sol_grep_constant",
146
+ description=(
147
+ "Look up a specific constant by name. "
148
+ "Fast exact-match lookup for constants like LAMPORTS_PER_SOL, "
149
+ "MAX_LOCKOUT_HISTORY, etc."
150
+ ),
151
+ inputSchema={
152
+ "type": "object",
153
+ "properties": {
154
+ "name": {
155
+ "type": "string",
156
+ "description": "Constant name (e.g., 'LAMPORTS_PER_SOL')",
157
+ },
158
+ },
159
+ "required": ["name"],
160
+ },
161
+ ),
162
+ Tool(
163
+ name="sol_analyze_function",
164
+ description=(
165
+ "Get the full Rust implementation of a function. "
166
+ "Use to understand exactly how something works in the runtime."
167
+ ),
168
+ inputSchema={
169
+ "type": "object",
170
+ "properties": {
171
+ "name": {
172
+ "type": "string",
173
+ "description": "Function name (e.g., 'process_stake_instruction')",
174
+ },
175
+ },
176
+ "required": ["name"],
177
+ },
178
+ ),
179
+ Tool(
180
+ name="sol_expert_guidance",
181
+ description=(
182
+ "Get curated expert guidance on Solana topics. "
183
+ "Covers: staking, voting, slashing, alpenglow, poh, accounts, svm, turbine, towerbft. "
184
+ "Includes gotchas and nuances not obvious from the code."
185
+ ),
186
+ inputSchema={
187
+ "type": "object",
188
+ "properties": {
189
+ "topic": {
190
+ "type": "string",
191
+ "description": "Topic (e.g., 'staking', 'towerbft', 'alpenglow')",
192
+ },
193
+ },
194
+ "required": ["topic"],
195
+ },
196
+ ),
197
+ Tool(
198
+ name="sol_list_guidance_topics",
199
+ description="List all available expert guidance topics.",
200
+ inputSchema={
201
+ "type": "object",
202
+ "properties": {},
203
+ },
204
+ ),
205
+ Tool(
206
+ name="sol_get_status",
207
+ description="Get status of the Solana MCP index.",
208
+ inputSchema={
209
+ "type": "object",
210
+ "properties": {},
211
+ },
212
+ ),
213
+ Tool(
214
+ name="sol_get_current_version",
215
+ description=(
216
+ "Get the current Solana mainnet version. "
217
+ "Returns version number, release date, and key features."
218
+ ),
219
+ inputSchema={
220
+ "type": "object",
221
+ "properties": {},
222
+ },
223
+ ),
224
+ Tool(
225
+ name="sol_list_versions",
226
+ description=(
227
+ "List all major Solana versions with their features and release dates. "
228
+ "Includes historical versions and planned future versions."
229
+ ),
230
+ inputSchema={
231
+ "type": "object",
232
+ "properties": {},
233
+ },
234
+ ),
235
+ Tool(
236
+ name="sol_get_consensus_status",
237
+ description=(
238
+ "Get current consensus mechanism status. "
239
+ "Shows TowerBFT (current) vs Alpenglow (future) status, finality times, etc."
240
+ ),
241
+ inputSchema={
242
+ "type": "object",
243
+ "properties": {},
244
+ },
245
+ ),
246
+ Tool(
247
+ name="sol_list_feature_gates",
248
+ description=(
249
+ "List Solana feature gates (protocol feature flags). "
250
+ "Shows which features are activated and when."
251
+ ),
252
+ inputSchema={
253
+ "type": "object",
254
+ "properties": {
255
+ "activated_only": {
256
+ "type": "boolean",
257
+ "description": "Only show activated features",
258
+ "default": False,
259
+ },
260
+ },
261
+ },
262
+ ),
263
+ Tool(
264
+ name="sol_list_clients",
265
+ description=(
266
+ "List Solana validator client implementations. "
267
+ "Includes Agave, Jito-Agave, Firedancer, Frankendancer, Sig."
268
+ ),
269
+ inputSchema={
270
+ "type": "object",
271
+ "properties": {
272
+ "production_only": {
273
+ "type": "boolean",
274
+ "description": "Only show production-ready clients",
275
+ "default": False,
276
+ },
277
+ },
278
+ },
279
+ ),
280
+ Tool(
281
+ name="sol_get_client",
282
+ description=(
283
+ "Get details about a specific Solana client. "
284
+ "E.g., 'firedancer', 'jito', 'agave', 'frankendancer', 'sig'."
285
+ ),
286
+ inputSchema={
287
+ "type": "object",
288
+ "properties": {
289
+ "name": {
290
+ "type": "string",
291
+ "description": "Client name",
292
+ },
293
+ },
294
+ "required": ["name"],
295
+ },
296
+ ),
297
+ Tool(
298
+ name="sol_get_client_diversity",
299
+ description=(
300
+ "Get Solana client diversity statistics. "
301
+ "Shows stake distribution across clients and diversity concerns."
302
+ ),
303
+ inputSchema={
304
+ "type": "object",
305
+ "properties": {},
306
+ },
307
+ ),
308
+ ]
309
+
310
+
311
+ @server.call_tool()
312
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
313
+ """Handle tool calls."""
314
+
315
+ if name == "sol_search":
316
+ try:
317
+ validated = SearchInput(
318
+ query=arguments["query"],
319
+ limit=arguments.get("limit", 5),
320
+ source_type=None,
321
+ )
322
+ except ValidationError as e:
323
+ return [TextContent(type="text", text=f"Validation error: {e}")]
324
+ return await handle_search(
325
+ validated.query,
326
+ validated.limit,
327
+ source_type=validated.source_type,
328
+ )
329
+
330
+ elif name == "sol_search_runtime":
331
+ try:
332
+ validated = SearchInput(
333
+ query=arguments["query"],
334
+ limit=arguments.get("limit", 5),
335
+ source_type="rust",
336
+ )
337
+ except ValidationError as e:
338
+ return [TextContent(type="text", text=f"Validation error: {e}")]
339
+ return await handle_search(
340
+ validated.query,
341
+ validated.limit,
342
+ source_type=validated.source_type,
343
+ )
344
+
345
+ elif name == "sol_search_simd":
346
+ try:
347
+ validated = SearchInput(
348
+ query=arguments["query"],
349
+ limit=arguments.get("limit", 5),
350
+ source_type="simd",
351
+ )
352
+ except ValidationError as e:
353
+ return [TextContent(type="text", text=f"Validation error: {e}")]
354
+ return await handle_search(
355
+ validated.query,
356
+ validated.limit,
357
+ source_type=validated.source_type,
358
+ )
359
+
360
+ elif name == "sol_grep_constant":
361
+ try:
362
+ validated = ConstantLookupInput(name=arguments["name"])
363
+ except ValidationError as e:
364
+ return [TextContent(type="text", text=f"Validation error: {e}")]
365
+ return await handle_grep_constant(validated.name)
366
+
367
+ elif name == "sol_analyze_function":
368
+ try:
369
+ validated = FunctionLookupInput(name=arguments["name"])
370
+ except ValidationError as e:
371
+ return [TextContent(type="text", text=f"Validation error: {e}")]
372
+ return await handle_analyze_function(validated.name)
373
+
374
+ elif name == "sol_expert_guidance":
375
+ try:
376
+ validated = GuidanceInput(topic=arguments["topic"])
377
+ except ValidationError as e:
378
+ return [TextContent(type="text", text=f"Validation error: {e}")]
379
+ return await handle_expert_guidance(validated.topic)
380
+
381
+ elif name == "sol_list_guidance_topics":
382
+ topics = list_guidance_topics()
383
+ return [TextContent(
384
+ type="text",
385
+ text="Available guidance topics:\n" + "\n".join(f" - {t}" for t in topics),
386
+ )]
387
+
388
+ elif name == "sol_get_status":
389
+ return await handle_status()
390
+
391
+ elif name == "sol_get_current_version":
392
+ return await handle_current_version()
393
+
394
+ elif name == "sol_list_versions":
395
+ return await handle_list_versions()
396
+
397
+ elif name == "sol_get_consensus_status":
398
+ return await handle_consensus_status()
399
+
400
+ elif name == "sol_list_feature_gates":
401
+ return await handle_feature_gates(arguments.get("activated_only", False))
402
+
403
+ elif name == "sol_list_clients":
404
+ return await handle_list_clients(arguments.get("production_only", False))
405
+
406
+ elif name == "sol_get_client":
407
+ try:
408
+ validated = ClientLookupInput(name=arguments["name"])
409
+ except ValidationError as e:
410
+ return [TextContent(type="text", text=f"Validation error: {e}")]
411
+ return await handle_get_client(validated.name)
412
+
413
+ elif name == "sol_get_client_diversity":
414
+ return await handle_client_diversity()
415
+
416
+ else:
417
+ return [TextContent(
418
+ type="text",
419
+ text=f"Unknown tool: {name}",
420
+ )]
421
+
422
+
423
+ async def handle_search(
424
+ query: str,
425
+ limit: int,
426
+ source_type: str | None,
427
+ ) -> list[TextContent]:
428
+ """Handle search requests."""
429
+ if not DEPS_AVAILABLE:
430
+ return [TextContent(
431
+ type="text",
432
+ text="Search dependencies not installed. Run: pip install lancedb sentence-transformers",
433
+ )]
434
+
435
+ results = search(
436
+ query,
437
+ data_dir=DATA_DIR,
438
+ limit=limit,
439
+ source_type=source_type,
440
+ )
441
+
442
+ if not results:
443
+ return [TextContent(
444
+ type="text",
445
+ text=f"No results found for: {query}",
446
+ )]
447
+
448
+ # Format results
449
+ output = [f"Search results for: {query}\n"]
450
+
451
+ for i, result in enumerate(results):
452
+ score = 1 - result["score"] # Convert distance to similarity
453
+ output.append(
454
+ f"\n{i + 1}. [{result['source_type']}] {result['source_name']} "
455
+ f"(score: {score:.0%})"
456
+ )
457
+ output.append(f" File: {result['source_file']}:{result['line_number']}")
458
+
459
+ # Include content
460
+ content = result["content"]
461
+ if len(content) > 500:
462
+ content = content[:500] + "\n... (truncated)"
463
+ output.append(f"\n```\n{content}\n```")
464
+
465
+ return [TextContent(type="text", text="\n".join(output))]
466
+
467
+
468
+ async def handle_grep_constant(name: str) -> list[TextContent]:
469
+ """Handle constant lookup."""
470
+ compiled_dir = DATA_DIR / "compiled"
471
+
472
+ # Search all compiled directories
473
+ for subdir in compiled_dir.glob("**"):
474
+ if (subdir / "index.json").exists():
475
+ result = lookup_constant(name, subdir)
476
+ if result:
477
+ output = [
478
+ f"# {result.name}",
479
+ f"Value: {result.value}",
480
+ ]
481
+ if result.type_annotation:
482
+ output.append(f"Type: {result.type_annotation}")
483
+ output.append(f"File: {result.file_path}:{result.line_number}")
484
+ if result.doc_comment:
485
+ output.append(f"\nDoc: {result.doc_comment}")
486
+
487
+ return [TextContent(type="text", text="\n".join(output))]
488
+
489
+ return [TextContent(
490
+ type="text",
491
+ text=f"Constant '{name}' not found in index.",
492
+ )]
493
+
494
+
495
+ async def handle_analyze_function(name: str) -> list[TextContent]:
496
+ """Handle function lookup."""
497
+ compiled_dir = DATA_DIR / "compiled"
498
+
499
+ for subdir in compiled_dir.glob("**"):
500
+ if (subdir / "index.json").exists():
501
+ result = lookup_function(name, subdir)
502
+ if result:
503
+ output = [
504
+ f"# {result.signature}",
505
+ f"File: {result.file_path}:{result.line_number}",
506
+ ]
507
+ if result.doc_comment:
508
+ output.append(f"\n/// {result.doc_comment}\n")
509
+ output.append(f"\n```rust\n{result.body}\n```")
510
+
511
+ return [TextContent(type="text", text="\n".join(output))]
512
+
513
+ return [TextContent(
514
+ type="text",
515
+ text=f"Function '{name}' not found in index.",
516
+ )]
517
+
518
+
519
+ async def handle_expert_guidance(topic: str) -> list[TextContent]:
520
+ """Handle expert guidance lookup."""
521
+ guidance = get_expert_guidance(topic)
522
+
523
+ if not guidance:
524
+ topics = list_guidance_topics()
525
+ return [TextContent(
526
+ type="text",
527
+ text=f"No guidance found for '{topic}'.\n\nAvailable topics:\n"
528
+ + "\n".join(f" - {t}" for t in topics),
529
+ )]
530
+
531
+ output = [
532
+ f"# {guidance.topic}",
533
+ f"\n{guidance.summary}\n",
534
+ "## Key Points",
535
+ ]
536
+ for point in guidance.key_points:
537
+ output.append(f"- {point}")
538
+
539
+ output.append("\n## Gotchas")
540
+ for gotcha in guidance.gotchas:
541
+ output.append(f"- {gotcha}")
542
+
543
+ output.append("\n## References")
544
+ for ref in guidance.references:
545
+ output.append(f"- {ref}")
546
+
547
+ return [TextContent(type="text", text="\n".join(output))]
548
+
549
+
550
+ async def handle_status() -> list[TextContent]:
551
+ """Handle status check."""
552
+ output = ["# Solana MCP Status\n"]
553
+
554
+ # Repos
555
+ output.append("## Repositories")
556
+ repos = list_downloaded_repos(DATA_DIR)
557
+ for name, info in repos.items():
558
+ status = "✓" if info["exists"] else "✗"
559
+ version = info.get("version", "not downloaded")
560
+ output.append(f" {status} {name}: {version}")
561
+
562
+ # Index
563
+ output.append("\n## Index")
564
+ if DEPS_AVAILABLE:
565
+ stats = get_index_stats(DATA_DIR)
566
+ if stats and "error" not in stats:
567
+ output.append(f" Total chunks: {stats['total_chunks']}")
568
+ for source_type, count in stats.get("by_source_type", {}).items():
569
+ output.append(f" {source_type}: {count}")
570
+ else:
571
+ output.append(" Not indexed")
572
+ else:
573
+ output.append(" Dependencies not installed")
574
+
575
+ return [TextContent(type="text", text="\n".join(output))]
576
+
577
+
578
+ async def handle_current_version() -> list[TextContent]:
579
+ """Handle current version request."""
580
+ version = get_current_version()
581
+
582
+ output = [
583
+ f"# Solana {version.version}",
584
+ f"Release: {version.release_date}",
585
+ f"\n{version.description}",
586
+ "\n## Key Features",
587
+ ]
588
+ for feature in version.key_features:
589
+ output.append(f"- {feature}")
590
+
591
+ if version.breaking_changes:
592
+ output.append("\n## Breaking Changes")
593
+ for change in version.breaking_changes:
594
+ output.append(f"- {change}")
595
+
596
+ return [TextContent(type="text", text="\n".join(output))]
597
+
598
+
599
+ async def handle_list_versions() -> list[TextContent]:
600
+ """Handle list versions request."""
601
+ versions = list_versions()
602
+
603
+ output = ["# Solana Version History\n"]
604
+
605
+ for v in versions:
606
+ current = " (CURRENT)" if v.current else ""
607
+ future = " (NOT YET LIVE)" if "expected" in v.release_date.lower() else ""
608
+ output.append(f"## {v.version}{current}{future}")
609
+ output.append(f"Release: {v.release_date}")
610
+ output.append(f"{v.description}\n")
611
+
612
+ return [TextContent(type="text", text="\n".join(output))]
613
+
614
+
615
+ async def handle_consensus_status() -> list[TextContent]:
616
+ """Handle consensus status request."""
617
+ status = get_consensus_status()
618
+
619
+ output = [
620
+ "# Solana Consensus Status",
621
+ "",
622
+ "## Current (Mainnet)",
623
+ f"**{status['current']}**",
624
+ f"{status['current_description']}",
625
+ f"- Finality: {status['finality']}",
626
+ f"- Optimistic confirmation: {status['optimistic_confirmation']}",
627
+ "",
628
+ "## Proof of History",
629
+ f"Status: {status['poh_status']}",
630
+ "",
631
+ "## Future: Alpenglow",
632
+ f"Status: {status['future_status']}",
633
+ f"- Expected finality: {status['future_finality']}",
634
+ "",
635
+ "**Note:** TowerBFT is the CURRENT consensus. Alpenglow is NOT YET LIVE.",
636
+ ]
637
+
638
+ return [TextContent(type="text", text="\n".join(output))]
639
+
640
+
641
+ async def handle_feature_gates(activated_only: bool) -> list[TextContent]:
642
+ """Handle feature gates request."""
643
+ features = list_feature_gates(activated_only)
644
+
645
+ output = ["# Solana Feature Gates\n"]
646
+
647
+ for f in features:
648
+ status = "✓ Activated" if f.activated_slot else "○ Pending"
649
+ output.append(f"## {f.name}")
650
+ output.append(f"Status: {status}")
651
+ output.append(f"Introduced: {f.version_introduced}")
652
+ if f.activated_slot:
653
+ output.append(f"Activated: slot {f.activated_slot} ({f.activated_date})")
654
+ output.append(f"Description: {f.description}")
655
+ output.append(f"Feature ID: `{f.feature_id}`\n")
656
+
657
+ return [TextContent(type="text", text="\n".join(output))]
658
+
659
+
660
+ async def handle_list_clients(production_only: bool) -> list[TextContent]:
661
+ """Handle list clients request."""
662
+ clients = list_clients(production_only)
663
+
664
+ output = ["# Solana Validator Clients\n"]
665
+
666
+ for c in clients:
667
+ stake_str = f" (~{c.stake_percentage}% stake)" if c.stake_percentage else ""
668
+ output.append(f"## {c.name}{stake_str}")
669
+ output.append(f"**{c.organization}** | {c.language} | {c.mainnet_status}")
670
+ output.append(f"\n{c.description}")
671
+ output.append(f"\nRepo: {c.repo}\n")
672
+
673
+ return [TextContent(type="text", text="\n".join(output))]
674
+
675
+
676
+ async def handle_get_client(name: str) -> list[TextContent]:
677
+ """Handle get client request."""
678
+ client = get_client(name)
679
+
680
+ if not client:
681
+ clients = list_clients()
682
+ names = [c.name for c in clients]
683
+ return [TextContent(
684
+ type="text",
685
+ text=f"Client '{name}' not found.\n\nAvailable clients: {', '.join(names)}",
686
+ )]
687
+
688
+ stake_str = f" (~{client.stake_percentage}% stake)" if client.stake_percentage else ""
689
+ output = [
690
+ f"# {client.name}{stake_str}",
691
+ f"Organization: {client.organization}",
692
+ f"Language: {client.language}",
693
+ f"Status: {client.mainnet_status}",
694
+ f"Repo: {client.repo}",
695
+ f"\n{client.description}",
696
+ "\n## Key Differentiators",
697
+ ]
698
+ for diff in client.key_differentiators:
699
+ output.append(f"- {diff}")
700
+
701
+ output.append("\n## Notes")
702
+ for note in client.notes:
703
+ output.append(f"- {note}")
704
+
705
+ return [TextContent(type="text", text="\n".join(output))]
706
+
707
+
708
+ async def handle_client_diversity() -> list[TextContent]:
709
+ """Handle client diversity request."""
710
+ diversity = get_client_diversity()
711
+
712
+ output = [
713
+ "# Solana Client Diversity",
714
+ f"\nTotal clients: {diversity['total_clients']}",
715
+ f"Production clients: {diversity['production_clients']}",
716
+ "\n## Stake Distribution (Oct 2025)",
717
+ ]
718
+
719
+ for client, info in diversity["client_breakdown"].items():
720
+ output.append(f"- **{client}**: {info}")
721
+
722
+ output.append("\n## Diversity Notes")
723
+ for note in diversity["diversity_notes"]:
724
+ output.append(f"- {note}")
725
+
726
+ return [TextContent(type="text", text="\n".join(output))]
727
+
728
+
729
+ async def main():
730
+ """Run the MCP server."""
731
+ async with stdio_server() as (read_stream, write_stream):
732
+ await server.run(
733
+ read_stream,
734
+ write_stream,
735
+ server.create_initialization_options(),
736
+ )
737
+
738
+
739
+ def run():
740
+ """Entry point for the MCP server."""
741
+ import asyncio
742
+ asyncio.run(main())
743
+
744
+
745
+ if __name__ == "__main__":
746
+ run()