sirchmunk 0.0.1__py3-none-any.whl → 0.0.2__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.
Files changed (43) hide show
  1. sirchmunk/api/__init__.py +1 -0
  2. sirchmunk/api/chat.py +1123 -0
  3. sirchmunk/api/components/__init__.py +0 -0
  4. sirchmunk/api/components/history_storage.py +402 -0
  5. sirchmunk/api/components/monitor_tracker.py +518 -0
  6. sirchmunk/api/components/settings_storage.py +353 -0
  7. sirchmunk/api/history.py +254 -0
  8. sirchmunk/api/knowledge.py +411 -0
  9. sirchmunk/api/main.py +120 -0
  10. sirchmunk/api/monitor.py +219 -0
  11. sirchmunk/api/run_server.py +54 -0
  12. sirchmunk/api/search.py +230 -0
  13. sirchmunk/api/settings.py +309 -0
  14. sirchmunk/api/tools.py +315 -0
  15. sirchmunk/cli/__init__.py +11 -0
  16. sirchmunk/cli/cli.py +789 -0
  17. sirchmunk/learnings/knowledge_base.py +5 -2
  18. sirchmunk/llm/prompts.py +12 -1
  19. sirchmunk/retrieve/text_retriever.py +186 -2
  20. sirchmunk/scan/file_scanner.py +2 -2
  21. sirchmunk/schema/knowledge.py +119 -35
  22. sirchmunk/search.py +384 -26
  23. sirchmunk/storage/__init__.py +2 -2
  24. sirchmunk/storage/{knowledge_manager.py → knowledge_storage.py} +265 -60
  25. sirchmunk/utils/constants.py +7 -5
  26. sirchmunk/utils/embedding_util.py +217 -0
  27. sirchmunk/utils/tokenizer_util.py +36 -1
  28. sirchmunk/version.py +1 -1
  29. {sirchmunk-0.0.1.dist-info → sirchmunk-0.0.2.dist-info}/METADATA +196 -14
  30. sirchmunk-0.0.2.dist-info/RECORD +69 -0
  31. {sirchmunk-0.0.1.dist-info → sirchmunk-0.0.2.dist-info}/WHEEL +1 -1
  32. sirchmunk-0.0.2.dist-info/top_level.txt +2 -0
  33. sirchmunk_mcp/__init__.py +25 -0
  34. sirchmunk_mcp/cli.py +478 -0
  35. sirchmunk_mcp/config.py +276 -0
  36. sirchmunk_mcp/server.py +355 -0
  37. sirchmunk_mcp/service.py +327 -0
  38. sirchmunk_mcp/setup.py +15 -0
  39. sirchmunk_mcp/tools.py +410 -0
  40. sirchmunk-0.0.1.dist-info/RECORD +0 -45
  41. sirchmunk-0.0.1.dist-info/top_level.txt +0 -1
  42. {sirchmunk-0.0.1.dist-info → sirchmunk-0.0.2.dist-info}/entry_points.txt +0 -0
  43. {sirchmunk-0.0.1.dist-info → sirchmunk-0.0.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,276 @@
1
+ # Copyright (c) ModelScope Contributors. All rights reserved.
2
+ """
3
+ Configuration management for Sirchmunk MCP Server.
4
+
5
+ Handles loading and validation of configuration from environment variables,
6
+ configuration files, and default values.
7
+ """
8
+
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Optional
12
+
13
+ from dotenv import load_dotenv
14
+ from pydantic import BaseModel, Field, field_validator
15
+
16
+
17
+ class LLMConfig(BaseModel):
18
+ """Configuration for LLM service."""
19
+
20
+ base_url: str = Field(
21
+ default="https://api.openai.com/v1",
22
+ description="LLM API base URL"
23
+ )
24
+ api_key: str = Field(
25
+ description="LLM API key (required)"
26
+ )
27
+ model_name: str = Field(
28
+ default="gpt-5.2",
29
+ description="LLM model name"
30
+ )
31
+ timeout: float = Field(
32
+ default=60.0,
33
+ description="Request timeout in seconds",
34
+ gt=0
35
+ )
36
+
37
+ @field_validator("api_key")
38
+ @classmethod
39
+ def validate_api_key(cls, v: str) -> str:
40
+ """Validate API key is not empty."""
41
+ if not v or v.strip() == "":
42
+ raise ValueError("LLM API key cannot be empty")
43
+ return v
44
+
45
+
46
+ class ClusterSimilarityConfig(BaseModel):
47
+ """Configuration for cluster similarity search."""
48
+
49
+ threshold: float = Field(
50
+ default=0.85,
51
+ description="Similarity threshold for cluster reuse",
52
+ ge=0.0,
53
+ le=1.0
54
+ )
55
+ top_k: int = Field(
56
+ default=3,
57
+ description="Number of similar clusters to retrieve",
58
+ ge=1,
59
+ le=10
60
+ )
61
+
62
+
63
+ class SearchDefaultsConfig(BaseModel):
64
+ """Default configuration for search operations."""
65
+
66
+ max_depth: int = Field(
67
+ default=5,
68
+ description="Maximum directory depth to search",
69
+ ge=1,
70
+ le=20
71
+ )
72
+ top_k_files: int = Field(
73
+ default=3,
74
+ description="Number of top files to return",
75
+ ge=1,
76
+ le=20
77
+ )
78
+ keyword_levels: int = Field(
79
+ default=3,
80
+ description="Number of keyword granularity levels",
81
+ ge=1,
82
+ le=5
83
+ )
84
+ grep_timeout: float = Field(
85
+ default=60.0,
86
+ description="Timeout for grep operations in seconds",
87
+ gt=0
88
+ )
89
+ max_queries_per_cluster: int = Field(
90
+ default=5,
91
+ description="Maximum number of queries to keep per cluster (FIFO)",
92
+ ge=1,
93
+ le=20
94
+ )
95
+
96
+
97
+ class SirchmunkConfig(BaseModel):
98
+ """Configuration for Sirchmunk service."""
99
+
100
+ work_path: Path = Field(
101
+ default_factory=lambda: Path.home() / ".sirchmunk",
102
+ description="Working directory for Sirchmunk data"
103
+ )
104
+ verbose: bool = Field(
105
+ default=False,
106
+ description="Enable verbose logging"
107
+ )
108
+ enable_cluster_reuse: bool = Field(
109
+ default=True,
110
+ description="Enable knowledge cluster reuse with embeddings"
111
+ )
112
+ cluster_similarity: ClusterSimilarityConfig = Field(
113
+ default_factory=ClusterSimilarityConfig
114
+ )
115
+ search_defaults: SearchDefaultsConfig = Field(
116
+ default_factory=SearchDefaultsConfig
117
+ )
118
+
119
+ @field_validator("work_path")
120
+ @classmethod
121
+ def validate_work_path(cls, v: Path) -> Path:
122
+ """Ensure work path is absolute and exists."""
123
+ v = v.expanduser().resolve()
124
+ v.mkdir(parents=True, exist_ok=True)
125
+ return v
126
+
127
+
128
+ class MCPServerConfig(BaseModel):
129
+ """Configuration for MCP server."""
130
+
131
+ server_name: str = Field(
132
+ default="sirchmunk",
133
+ description="MCP server name"
134
+ )
135
+ server_version: str = Field(
136
+ default="0.1.0",
137
+ description="MCP server version"
138
+ )
139
+ log_level: str = Field(
140
+ default="INFO",
141
+ description="Logging level"
142
+ )
143
+ transport: str = Field(
144
+ default="stdio",
145
+ description="MCP transport protocol (stdio or http)"
146
+ )
147
+
148
+ # HTTP-specific settings
149
+ host: str = Field(
150
+ default="localhost",
151
+ description="Host for HTTP transport"
152
+ )
153
+ port: int = Field(
154
+ default=8080,
155
+ description="Port for HTTP transport",
156
+ ge=1024,
157
+ le=65535
158
+ )
159
+
160
+ @field_validator("transport")
161
+ @classmethod
162
+ def validate_transport(cls, v: str) -> str:
163
+ """Validate transport protocol."""
164
+ v = v.lower()
165
+ if v not in ("stdio", "http"):
166
+ raise ValueError(f"Invalid transport: {v}. Must be 'stdio' or 'http'")
167
+ return v
168
+
169
+ @field_validator("log_level")
170
+ @classmethod
171
+ def validate_log_level(cls, v: str) -> str:
172
+ """Validate log level."""
173
+ v = v.upper()
174
+ valid_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
175
+ if v not in valid_levels:
176
+ raise ValueError(f"Invalid log level: {v}. Must be one of {valid_levels}")
177
+ return v
178
+
179
+
180
+ class Config(BaseModel):
181
+ """Master configuration for Sirchmunk MCP Server."""
182
+
183
+ llm: LLMConfig
184
+ sirchmunk: SirchmunkConfig = Field(default_factory=SirchmunkConfig)
185
+ mcp: MCPServerConfig = Field(default_factory=MCPServerConfig)
186
+
187
+ @classmethod
188
+ def from_env(cls) -> "Config":
189
+ """Load configuration from environment variables.
190
+
191
+ Automatically loads .mcp_env file from work_path (~/.sirchmunk/.mcp_env by default).
192
+
193
+ Environment variables:
194
+ LLM_BASE_URL: LLM API base URL
195
+ LLM_API_KEY: LLM API key (required)
196
+ LLM_MODEL_NAME: LLM model name
197
+ SIRCHMUNK_WORK_PATH: Sirchmunk working directory
198
+ SIRCHMUNK_VERBOSE: Enable verbose logging
199
+ SIRCHMUNK_ENABLE_CLUSTER_REUSE: Enable cluster reuse
200
+ CLUSTER_SIM_THRESHOLD: Similarity threshold
201
+ CLUSTER_SIM_TOP_K: Top-K similar clusters
202
+ DEFAULT_MAX_DEPTH: Default max directory depth
203
+ DEFAULT_TOP_K_FILES: Default top-K files
204
+ DEFAULT_KEYWORD_LEVELS: Default keyword levels
205
+ GREP_TIMEOUT: Grep operation timeout
206
+ MAX_QUERIES_PER_CLUSTER: Max queries per cluster
207
+ MCP_SERVER_NAME: MCP server name
208
+ MCP_SERVER_VERSION: MCP server version
209
+ MCP_LOG_LEVEL: Logging level
210
+ MCP_TRANSPORT: MCP transport protocol
211
+ MCP_HOST: MCP server host (HTTP mode)
212
+ MCP_PORT: MCP server port (HTTP mode)
213
+
214
+ Returns:
215
+ Config: Loaded configuration
216
+
217
+ Raises:
218
+ ValueError: If required configuration is missing or invalid
219
+ """
220
+ # Load .mcp_env from work_path (default: ~/.sirchmunk/.mcp_env)
221
+ work_path = Path(os.getenv("SIRCHMUNK_WORK_PATH", str(Path.home() / ".sirchmunk")))
222
+ work_path = work_path.expanduser().resolve()
223
+ env_file = work_path / ".mcp_env"
224
+
225
+ if env_file.exists():
226
+ load_dotenv(env_file, override=False)
227
+
228
+ # LLM configuration
229
+ llm_config = LLMConfig(
230
+ base_url=os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
231
+ api_key=os.getenv("LLM_API_KEY", ""),
232
+ model_name=os.getenv("LLM_MODEL_NAME", "gpt-5.2"),
233
+ timeout=float(os.getenv("LLM_TIMEOUT", "60.0")),
234
+ )
235
+
236
+ # Sirchmunk configuration
237
+ sirchmunk_config = SirchmunkConfig(
238
+ work_path=Path(os.getenv("SIRCHMUNK_WORK_PATH", str(Path.home() / ".sirchmunk"))),
239
+ verbose=os.getenv("SIRCHMUNK_VERBOSE", "false").lower() == "true",
240
+ enable_cluster_reuse=os.getenv("SIRCHMUNK_ENABLE_CLUSTER_REUSE", "true").lower() == "true",
241
+ cluster_similarity=ClusterSimilarityConfig(
242
+ threshold=float(os.getenv("CLUSTER_SIM_THRESHOLD", "0.85")),
243
+ top_k=int(os.getenv("CLUSTER_SIM_TOP_K", "3")),
244
+ ),
245
+ search_defaults=SearchDefaultsConfig(
246
+ max_depth=int(os.getenv("DEFAULT_MAX_DEPTH", "5")),
247
+ top_k_files=int(os.getenv("DEFAULT_TOP_K_FILES", "3")),
248
+ keyword_levels=int(os.getenv("DEFAULT_KEYWORD_LEVELS", "3")),
249
+ grep_timeout=float(os.getenv("GREP_TIMEOUT", "60.0")),
250
+ max_queries_per_cluster=int(os.getenv("MAX_QUERIES_PER_CLUSTER", "5")),
251
+ ),
252
+ )
253
+
254
+ # MCP server configuration
255
+ mcp_config = MCPServerConfig(
256
+ server_name=os.getenv("MCP_SERVER_NAME", "sirchmunk"),
257
+ server_version=os.getenv("MCP_SERVER_VERSION", "0.1.0"),
258
+ log_level=os.getenv("MCP_LOG_LEVEL", "INFO"),
259
+ transport=os.getenv("MCP_TRANSPORT", "stdio"),
260
+ host=os.getenv("MCP_HOST", "localhost"),
261
+ port=int(os.getenv("MCP_PORT", "8080")),
262
+ )
263
+
264
+ return cls(
265
+ llm=llm_config,
266
+ sirchmunk=sirchmunk_config,
267
+ mcp=mcp_config,
268
+ )
269
+
270
+ def to_dict(self) -> Dict[str, Any]:
271
+ """Convert configuration to dictionary.
272
+
273
+ Returns:
274
+ Dict[str, Any]: Configuration dictionary
275
+ """
276
+ return self.model_dump()
@@ -0,0 +1,355 @@
1
+ # Copyright (c) ModelScope Contributors. All rights reserved.
2
+ """
3
+ MCP Server implementation for Sirchmunk using FastMCP.
4
+
5
+ Provides the main MCP server that exposes Sirchmunk functionality
6
+ as MCP tools following the Model Context Protocol specification.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from mcp.server.fastmcp import FastMCP
14
+
15
+ from .config import Config
16
+ from .service import SirchmunkService
17
+
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Global service instance (initialized when server starts)
22
+ _service: Optional[SirchmunkService] = None
23
+
24
+
25
+ def create_server(config: Config) -> FastMCP:
26
+ """Create and configure FastMCP server instance.
27
+
28
+ Args:
29
+ config: Configuration object
30
+
31
+ Returns:
32
+ Configured FastMCP server instance
33
+ """
34
+ global _service
35
+
36
+ # Initialize service
37
+ _service = SirchmunkService(config)
38
+
39
+ # Create FastMCP server
40
+ mcp = FastMCP(
41
+ name=config.mcp.server_name,
42
+ )
43
+
44
+ logger.info(
45
+ f"Creating MCP server: {config.mcp.server_name} v{config.mcp.server_version}"
46
+ )
47
+
48
+ # Register tools using decorators
49
+ @mcp.tool()
50
+ async def sirchmunk_search(
51
+ query: str,
52
+ search_paths: List[str],
53
+ mode: str = "DEEP",
54
+ max_depth: int = 5,
55
+ top_k_files: int = 3,
56
+ keyword_levels: int = 3,
57
+ include: Optional[List[str]] = None,
58
+ exclude: Optional[List[str]] = None,
59
+ return_cluster: bool = False,
60
+ ) -> str:
61
+ """Intelligent code and document search with multi-mode support.
62
+
63
+ DEEP mode provides comprehensive knowledge extraction with full context analysis.
64
+ FILENAME_ONLY mode performs fast filename pattern matching without content search.
65
+
66
+ Args:
67
+ query: Search query or question (e.g., 'How does authentication work?')
68
+ search_paths: Paths to search in (files or directories)
69
+ mode: Search mode - DEEP (comprehensive, 10-30s) or FILENAME_ONLY (fast, <1s)
70
+ max_depth: Maximum directory depth to search (1-20, default: 5)
71
+ top_k_files: Number of top files to return (1-20, default: 3)
72
+ keyword_levels: Keyword granularity levels for DEEP mode (1-5, default: 3)
73
+ include: File patterns to include (glob, e.g., ['*.py', '*.md'])
74
+ exclude: File patterns to exclude (glob, e.g., ['*.pyc', '*.log'])
75
+ return_cluster: Return full KnowledgeCluster object (DEEP mode only)
76
+
77
+ Returns:
78
+ Search results as formatted text
79
+ """
80
+ if _service is None:
81
+ return "Error: Service not initialized"
82
+
83
+ logger.info(f"sirchmunk_search: mode={mode}, query='{query[:50]}...'")
84
+
85
+ try:
86
+ result = await _service.searcher.search(
87
+ query=query,
88
+ search_paths=search_paths,
89
+ mode=mode,
90
+ max_depth=max_depth,
91
+ top_k_files=top_k_files,
92
+ keyword_levels=keyword_levels,
93
+ include=include,
94
+ exclude=exclude,
95
+ return_cluster=return_cluster,
96
+ )
97
+
98
+ if result is None:
99
+ return f"No results found for query: {query}"
100
+
101
+ if isinstance(result, str):
102
+ return result
103
+
104
+ if isinstance(result, list):
105
+ # FILENAME_ONLY mode returns list of file matches
106
+ return _format_filename_results(result, query)
107
+
108
+ if hasattr(result, "__str__"):
109
+ return str(result)
110
+
111
+ return str(result)
112
+
113
+ except Exception as e:
114
+ logger.error(f"Search failed: {e}", exc_info=True)
115
+ return f"Search failed: {str(e)}"
116
+
117
+ @mcp.tool()
118
+ async def sirchmunk_get_cluster(cluster_id: str) -> str:
119
+ """Retrieve a previously saved knowledge cluster by its ID.
120
+
121
+ Knowledge clusters are automatically saved during DEEP mode searches
122
+ and contain rich information including evidences, patterns, and constraints.
123
+
124
+ Args:
125
+ cluster_id: Knowledge cluster ID (e.g., 'C1007')
126
+
127
+ Returns:
128
+ Full cluster information or error message
129
+ """
130
+ if _service is None:
131
+ return "Error: Service not initialized"
132
+
133
+ logger.info(f"sirchmunk_get_cluster: cluster_id={cluster_id}")
134
+
135
+ try:
136
+ cluster = await _service.get_cluster(cluster_id)
137
+
138
+ if cluster is None:
139
+ return f"Cluster not found: {cluster_id}"
140
+
141
+ return str(cluster)
142
+
143
+ except Exception as e:
144
+ logger.error(f"Get cluster failed: {e}", exc_info=True)
145
+ return f"Failed to retrieve cluster: {str(e)}"
146
+
147
+ @mcp.tool()
148
+ async def sirchmunk_list_clusters(
149
+ limit: int = 10,
150
+ sort_by: str = "last_modified",
151
+ ) -> str:
152
+ """List all saved knowledge clusters with optional filtering and sorting.
153
+
154
+ Useful for discovering previously searched topics and reusing knowledge.
155
+
156
+ Args:
157
+ limit: Maximum number of clusters to return (1-100, default: 10)
158
+ sort_by: Sort field - hotness, confidence, or last_modified (default)
159
+
160
+ Returns:
161
+ List of cluster metadata
162
+ """
163
+ if _service is None:
164
+ return "Error: Service not initialized"
165
+
166
+ logger.info(f"sirchmunk_list_clusters: limit={limit}, sort_by={sort_by}")
167
+
168
+ try:
169
+ clusters = await _service.list_clusters(limit=limit, sort_by=sort_by)
170
+
171
+ if not clusters:
172
+ return "No knowledge clusters found."
173
+
174
+ return _format_cluster_list(clusters, sort_by)
175
+
176
+ except Exception as e:
177
+ logger.error(f"List clusters failed: {e}", exc_info=True)
178
+ return f"Failed to list clusters: {str(e)}"
179
+
180
+ return mcp
181
+
182
+
183
+ def _format_filename_results(results: List[Dict[str, Any]], query: str) -> str:
184
+ """Format FILENAME_ONLY mode results.
185
+
186
+ Args:
187
+ results: List of filename match dictionaries
188
+ query: Original query
189
+
190
+ Returns:
191
+ Formatted string representation
192
+ """
193
+ lines = [
194
+ f"# Filename Search Results",
195
+ f"",
196
+ f"**Query**: `{query}`",
197
+ f"**Found**: {len(results)} matching file(s)",
198
+ f"",
199
+ ]
200
+
201
+ for i, result in enumerate(results, 1):
202
+ lines.append(f"## {i}. {result.get('filename', 'unknown')}")
203
+ lines.append(f"- **Path**: `{result.get('path', 'unknown')}`")
204
+ if 'match_score' in result:
205
+ lines.append(f"- **Relevance**: {result['match_score']:.2f}")
206
+ if "matched_pattern" in result:
207
+ lines.append(f"- **Pattern**: `{result['matched_pattern']}`")
208
+ lines.append("")
209
+
210
+ return "\n".join(lines)
211
+
212
+
213
+ def _format_cluster_list(clusters: List[Dict[str, Any]], sort_by: str) -> str:
214
+ """Format cluster list.
215
+
216
+ Args:
217
+ clusters: List of cluster metadata dictionaries
218
+ sort_by: Sort field used
219
+
220
+ Returns:
221
+ Formatted string representation
222
+ """
223
+ lines = [
224
+ f"# Knowledge Clusters",
225
+ f"",
226
+ f"**Total**: {len(clusters)} cluster(s)",
227
+ f"**Sorted by**: {sort_by}",
228
+ f"",
229
+ ]
230
+
231
+ for i, cluster in enumerate(clusters, 1):
232
+ lines.append(f"## {i}. {cluster.get('name', 'Unnamed')}")
233
+ lines.append(f"- **ID**: `{cluster.get('id', 'unknown')}`")
234
+ lines.append(f"- **Lifecycle**: {cluster.get('lifecycle', 'unknown')}")
235
+ lines.append(f"- **Version**: {cluster.get('version', 0)}")
236
+
237
+ if cluster.get('confidence') is not None:
238
+ lines.append(f"- **Confidence**: {cluster['confidence']:.2f}")
239
+
240
+ if cluster.get('hotness') is not None:
241
+ lines.append(f"- **Hotness**: {cluster['hotness']:.2f}")
242
+
243
+ if cluster.get('last_modified'):
244
+ lines.append(f"- **Last Modified**: {cluster['last_modified']}")
245
+
246
+ if cluster.get('queries'):
247
+ queries_preview = ", ".join(f'"{q}"' for q in cluster['queries'][:3])
248
+ if len(cluster['queries']) > 3:
249
+ queries_preview += f" (+{len(cluster['queries']) - 3} more)"
250
+ lines.append(f"- **Related Queries**: {queries_preview}")
251
+
252
+ lines.append(f"- **Evidences**: {cluster.get('evidences_count', 0)}")
253
+ lines.append("")
254
+
255
+ return "\n".join(lines)
256
+
257
+
258
+ async def run_stdio_server(config: Config) -> None:
259
+ """Run MCP server with stdio transport.
260
+
261
+ This is the default transport mode for Claude Desktop and other
262
+ MCP clients that communicate via standard input/output.
263
+
264
+ Args:
265
+ config: Configuration object
266
+
267
+ Note:
268
+ This mode should be launched by an MCP client, not run directly
269
+ in an interactive terminal. Manual terminal input will cause
270
+ JSON parsing errors.
271
+ """
272
+ logger.info("Starting MCP server with stdio transport")
273
+
274
+ # Create server
275
+ mcp = create_server(config)
276
+
277
+ # Run with stdio transport
278
+ logger.info("MCP server listening on stdio")
279
+ logger.info("Waiting for MCP client connection...")
280
+
281
+ await mcp.run_stdio_async()
282
+
283
+
284
+ async def run_http_server(config: Config) -> None:
285
+ """Run MCP server with Streamable HTTP transport.
286
+
287
+ This transport mode runs an HTTP server that communicates via
288
+ HTTP with streaming support, suitable for web-based clients.
289
+
290
+ Args:
291
+ config: Configuration object
292
+
293
+ Note:
294
+ HTTP transport requires uvicorn to be installed.
295
+ """
296
+ logger.info(
297
+ f"Starting MCP server with HTTP transport on {config.mcp.host}:{config.mcp.port}"
298
+ )
299
+
300
+ # Create server
301
+ mcp = create_server(config)
302
+
303
+ # Run with HTTP transport using uvicorn
304
+ try:
305
+ import uvicorn
306
+ uvicorn.run(
307
+ mcp.sse_app(),
308
+ host=config.mcp.host,
309
+ port=config.mcp.port,
310
+ log_level="info",
311
+ )
312
+ except ImportError:
313
+ raise RuntimeError(
314
+ "HTTP transport requires uvicorn. Install with: pip install uvicorn"
315
+ )
316
+
317
+
318
+ async def main() -> None:
319
+ """Main entry point for MCP server.
320
+
321
+ Loads configuration and starts the appropriate transport server.
322
+ """
323
+ # Configure logging
324
+ logging.basicConfig(
325
+ level=logging.INFO,
326
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
327
+ )
328
+
329
+ try:
330
+ # Load configuration from environment
331
+ config = Config.from_env()
332
+
333
+ # Set log level from config
334
+ logging.getLogger().setLevel(config.mcp.log_level)
335
+
336
+ logger.info(f"Loaded configuration: transport={config.mcp.transport}")
337
+
338
+ # Start appropriate transport server
339
+ if config.mcp.transport == "stdio":
340
+ await run_stdio_server(config)
341
+ elif config.mcp.transport == "http":
342
+ await run_http_server(config)
343
+ else:
344
+ raise ValueError(f"Unknown transport: {config.mcp.transport}")
345
+
346
+ except KeyboardInterrupt:
347
+ logger.info("Received interrupt signal, shutting down")
348
+
349
+ except Exception as e:
350
+ logger.error(f"Server error: {e}", exc_info=True)
351
+ raise
352
+
353
+
354
+ if __name__ == "__main__":
355
+ asyncio.run(main())