mcp-code-indexer 3.5.5__py3-none-any.whl → 3.6.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.
mcp_code_indexer/main.py CHANGED
@@ -116,6 +116,41 @@ def parse_arguments() -> argparse.Namespace:
116
116
  help="Create local database in specified folder and migrate project data from global DB",
117
117
  )
118
118
 
119
+ # HTTP transport options
120
+ parser.add_argument(
121
+ "--http",
122
+ action="store_true",
123
+ help="Enable HTTP transport instead of stdio (requires 'http' extras)",
124
+ )
125
+
126
+ parser.add_argument(
127
+ "--host",
128
+ type=str,
129
+ default="127.0.0.1",
130
+ help="Host to bind HTTP server to (default: 127.0.0.1)",
131
+ )
132
+
133
+ parser.add_argument(
134
+ "--port",
135
+ type=int,
136
+ default=7557,
137
+ help="Port to bind HTTP server to (default: 7557)",
138
+ )
139
+
140
+ parser.add_argument(
141
+ "--auth-token",
142
+ type=str,
143
+ help="Bearer token for HTTP authentication (optional)",
144
+ )
145
+
146
+ parser.add_argument(
147
+ "--cors-origins",
148
+ type=str,
149
+ nargs="*",
150
+ default=["*"],
151
+ help="Allowed CORS origins for HTTP transport (default: allow all)",
152
+ )
153
+
119
154
  return parser.parse_args()
120
155
 
121
156
 
@@ -961,10 +996,40 @@ async def main() -> None:
961
996
  try:
962
997
  # Import and run the MCP server
963
998
  from .server.mcp_server import MCPCodeIndexServer
999
+
1000
+ # Create transport based on arguments
1001
+ transport = None
1002
+ if args.http:
1003
+ from .transport.http_transport import HTTPTransport
1004
+ transport = HTTPTransport(
1005
+ server_instance=None, # Will be set after server creation
1006
+ host=args.host,
1007
+ port=args.port,
1008
+ auth_token=args.auth_token,
1009
+ cors_origins=args.cors_origins,
1010
+ )
1011
+ logger.info(
1012
+ "HTTP transport configured",
1013
+ extra={
1014
+ "structured_data": {
1015
+ "host": args.host,
1016
+ "port": args.port,
1017
+ "auth_enabled": transport.auth_token is not None,
1018
+ "cors_origins": args.cors_origins,
1019
+ }
1020
+ },
1021
+ )
964
1022
 
965
1023
  server = MCPCodeIndexServer(
966
- token_limit=args.token_limit, db_path=db_path, cache_dir=cache_dir
1024
+ token_limit=args.token_limit,
1025
+ db_path=db_path,
1026
+ cache_dir=cache_dir,
1027
+ transport=transport,
967
1028
  )
1029
+
1030
+ # Set server instance in transport after server creation
1031
+ if transport:
1032
+ transport.server = server
968
1033
 
969
1034
  await server.run()
970
1035
 
@@ -1,7 +1,12 @@
1
1
  """
2
- Middleware components for the MCP Code Indexer.
2
+ Middleware components for MCP Code Indexer.
3
+
4
+ This module provides middleware for HTTP transport features like
5
+ logging, authentication, and security.
3
6
  """
4
7
 
5
- from .error_middleware import ToolMiddleware, AsyncTaskManager, create_tool_middleware
8
+ from .logging import HTTPLoggingMiddleware
9
+ from .auth import HTTPAuthMiddleware
10
+ from .security import HTTPSecurityMiddleware
6
11
 
7
- __all__ = ["ToolMiddleware", "AsyncTaskManager", "create_tool_middleware"]
12
+ __all__ = ["HTTPLoggingMiddleware", "HTTPAuthMiddleware", "HTTPSecurityMiddleware"]
@@ -0,0 +1,228 @@
1
+ """
2
+ HTTP authentication middleware for MCP Code Indexer.
3
+
4
+ Provides Bearer token authentication for HTTP transport.
5
+ """
6
+
7
+ import logging
8
+ from typing import Optional
9
+
10
+ try:
11
+ from fastapi import HTTPException, Request
12
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
13
+ from starlette.middleware.base import BaseHTTPMiddleware
14
+ except ImportError as e:
15
+ raise ImportError(
16
+ "HTTP middleware dependencies not installed. "
17
+ "This should not happen as they are now required dependencies. "
18
+ "Please reinstall mcp-code-indexer."
19
+ ) from e
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class HTTPAuthMiddleware(BaseHTTPMiddleware):
25
+ """
26
+ HTTP authentication middleware using Bearer tokens.
27
+
28
+ Validates Bearer tokens for protected endpoints while allowing
29
+ public endpoints to pass through.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ app,
35
+ auth_token: Optional[str] = None,
36
+ public_paths: Optional[list] = None,
37
+ ):
38
+ """
39
+ Initialize HTTP authentication middleware.
40
+
41
+ Args:
42
+ app: FastAPI application instance
43
+ auth_token: Expected Bearer token for authentication
44
+ public_paths: List of paths that don't require authentication
45
+ """
46
+ super().__init__(app)
47
+ self.auth_token = auth_token
48
+ self.public_paths = public_paths or ["/health", "/docs", "/openapi.json"]
49
+ self.logger = logger.getChild("http_auth")
50
+
51
+ # Only enable auth if token is provided
52
+ self.auth_enabled = auth_token is not None
53
+
54
+ if self.auth_enabled:
55
+ self.logger.info("HTTP authentication enabled")
56
+ else:
57
+ self.logger.info("HTTP authentication disabled")
58
+
59
+ async def dispatch(self, request: Request, call_next):
60
+ """
61
+ Process HTTP request and validate authentication.
62
+
63
+ Args:
64
+ request: FastAPI request object
65
+ call_next: Next middleware in chain
66
+
67
+ Returns:
68
+ HTTP response
69
+
70
+ Raises:
71
+ HTTPException: If authentication fails
72
+ """
73
+ # Skip authentication for public paths
74
+ if not self.auth_enabled or request.url.path in self.public_paths:
75
+ return await call_next(request)
76
+
77
+ # Extract Authorization header
78
+ auth_header = request.headers.get("authorization")
79
+
80
+ if not auth_header:
81
+ self.logger.warning(
82
+ f"Missing authorization header for {request.url.path}",
83
+ extra={
84
+ "structured_data": {
85
+ "path": request.url.path,
86
+ "client_ip": self._get_client_ip(request),
87
+ "user_agent": request.headers.get("user-agent", ""),
88
+ }
89
+ },
90
+ )
91
+ raise HTTPException(
92
+ status_code=401,
93
+ detail="Authorization header required",
94
+ headers={"WWW-Authenticate": "Bearer"},
95
+ )
96
+
97
+ # Validate Bearer token format
98
+ if not auth_header.startswith("Bearer "):
99
+ self.logger.warning(
100
+ f"Invalid authorization format for {request.url.path}",
101
+ extra={
102
+ "structured_data": {
103
+ "path": request.url.path,
104
+ "auth_format": auth_header.split(" ")[0] if " " in auth_header else auth_header,
105
+ "client_ip": self._get_client_ip(request),
106
+ }
107
+ },
108
+ )
109
+ raise HTTPException(
110
+ status_code=401,
111
+ detail="Invalid authorization format. Use: Bearer <token>",
112
+ headers={"WWW-Authenticate": "Bearer"},
113
+ )
114
+
115
+ # Extract and validate token
116
+ token = auth_header[7:] # Remove "Bearer " prefix
117
+
118
+ if token != self.auth_token:
119
+ self.logger.warning(
120
+ f"Invalid token for {request.url.path}",
121
+ extra={
122
+ "structured_data": {
123
+ "path": request.url.path,
124
+ "token_prefix": token[:8] + "..." if len(token) > 8 else token,
125
+ "client_ip": self._get_client_ip(request),
126
+ }
127
+ },
128
+ )
129
+ raise HTTPException(
130
+ status_code=401,
131
+ detail="Invalid authentication token",
132
+ headers={"WWW-Authenticate": "Bearer"},
133
+ )
134
+
135
+ # Token is valid, proceed with request
136
+ self.logger.debug(
137
+ f"Authentication successful for {request.url.path}",
138
+ extra={
139
+ "structured_data": {
140
+ "path": request.url.path,
141
+ "client_ip": self._get_client_ip(request),
142
+ }
143
+ },
144
+ )
145
+
146
+ return await call_next(request)
147
+
148
+ def _get_client_ip(self, request: Request) -> str:
149
+ """Extract client IP address from request."""
150
+ # Check for forwarded headers
151
+ forwarded_for = request.headers.get("x-forwarded-for")
152
+ if forwarded_for:
153
+ return forwarded_for.split(",")[0].strip()
154
+
155
+ real_ip = request.headers.get("x-real-ip")
156
+ if real_ip:
157
+ return real_ip
158
+
159
+ # Fall back to direct client IP
160
+ if hasattr(request.client, "host"):
161
+ return request.client.host
162
+
163
+ return "unknown"
164
+
165
+
166
+ class TokenValidator:
167
+ """
168
+ Utility class for token validation logic.
169
+
170
+ Provides methods for validating different token formats
171
+ and managing token-based authentication.
172
+ """
173
+
174
+ @staticmethod
175
+ def validate_bearer_token(token: str, expected_token: str) -> bool:
176
+ """
177
+ Validate Bearer token against expected value.
178
+
179
+ Args:
180
+ token: Token to validate
181
+ expected_token: Expected token value
182
+
183
+ Returns:
184
+ True if token is valid, False otherwise
185
+ """
186
+ if not token or not expected_token:
187
+ return False
188
+
189
+ # Simple string comparison for now
190
+ # In production, consider using constant-time comparison
191
+ return token == expected_token
192
+
193
+ @staticmethod
194
+ def generate_token(length: int = 32) -> str:
195
+ """
196
+ Generate a random token for authentication.
197
+
198
+ Args:
199
+ length: Length of token to generate
200
+
201
+ Returns:
202
+ Random token string
203
+ """
204
+ import secrets
205
+ import string
206
+
207
+ alphabet = string.ascii_letters + string.digits
208
+ return ''.join(secrets.choice(alphabet) for _ in range(length))
209
+
210
+ @staticmethod
211
+ def mask_token(token: str, visible_chars: int = 8) -> str:
212
+ """
213
+ Mask token for logging purposes.
214
+
215
+ Args:
216
+ token: Token to mask
217
+ visible_chars: Number of characters to show
218
+
219
+ Returns:
220
+ Masked token string
221
+ """
222
+ if not token:
223
+ return ""
224
+
225
+ if len(token) <= visible_chars:
226
+ return "*" * len(token)
227
+
228
+ return token[:visible_chars] + "..." + "*" * (len(token) - visible_chars)
@@ -0,0 +1,274 @@
1
+ """
2
+ HTTP logging middleware for MCP Code Indexer.
3
+
4
+ Provides request/response logging and monitoring for HTTP transport.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import time
10
+ from typing import Any, Dict, Optional
11
+
12
+ try:
13
+ from fastapi import Request, Response
14
+ from fastapi.responses import StreamingResponse
15
+ from starlette.middleware.base import BaseHTTPMiddleware
16
+ except ImportError as e:
17
+ raise ImportError(
18
+ "HTTP middleware dependencies not installed. "
19
+ "This should not happen as they are now required dependencies. "
20
+ "Please reinstall mcp-code-indexer."
21
+ ) from e
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class HTTPLoggingMiddleware(BaseHTTPMiddleware):
27
+ """
28
+ HTTP request/response logging middleware.
29
+
30
+ Logs HTTP requests and responses with performance metrics
31
+ and structured data for monitoring.
32
+ """
33
+
34
+ def __init__(self, app: Any, log_level: str = "INFO"):
35
+ """
36
+ Initialize HTTP logging middleware.
37
+
38
+ Args:
39
+ app: FastAPI application instance
40
+ log_level: Logging level for HTTP requests
41
+ """
42
+ super().__init__(app)
43
+ self.log_level = getattr(logging, log_level.upper())
44
+ self.logger = logger.getChild("http_access")
45
+ self.logger.setLevel(self.log_level)
46
+
47
+ async def dispatch(self, request: Request, call_next) -> Response:
48
+ """
49
+ Process HTTP request and log access information.
50
+
51
+ Args:
52
+ request: FastAPI request object
53
+ call_next: Next middleware in chain
54
+
55
+ Returns:
56
+ HTTP response
57
+ """
58
+ start_time = time.time()
59
+ request_id = self._generate_request_id(request)
60
+
61
+ # Extract client information
62
+ client_ip = self._get_client_ip(request)
63
+ user_agent = request.headers.get("user-agent", "")
64
+
65
+ # Log incoming request
66
+ self.logger.info(
67
+ f"HTTP {request.method} {request.url.path}",
68
+ extra={
69
+ "structured_data": {
70
+ "event_type": "http_request",
71
+ "request_id": request_id,
72
+ "method": request.method,
73
+ "path": request.url.path,
74
+ "query_params": dict(request.query_params),
75
+ "client_ip": client_ip,
76
+ "user_agent": user_agent,
77
+ "content_length": request.headers.get("content-length"),
78
+ "content_type": request.headers.get("content-type"),
79
+ }
80
+ },
81
+ )
82
+
83
+ # Add request ID to request state for use in handlers
84
+ request.state.request_id = request_id
85
+
86
+ try:
87
+ # Process request
88
+ response = await call_next(request)
89
+
90
+ # Calculate processing time
91
+ process_time = time.time() - start_time
92
+
93
+ # Log response
94
+ self._log_response(
95
+ request_id=request_id,
96
+ method=request.method,
97
+ path=request.url.path,
98
+ status_code=response.status_code,
99
+ process_time=process_time,
100
+ response=response,
101
+ )
102
+
103
+ # Add performance headers
104
+ response.headers["X-Process-Time"] = str(process_time)
105
+ response.headers["X-Request-ID"] = request_id
106
+
107
+ return response
108
+
109
+ except Exception as e:
110
+ # Log error
111
+ process_time = time.time() - start_time
112
+
113
+ self.logger.error(
114
+ f"HTTP {request.method} {request.url.path} - ERROR",
115
+ extra={
116
+ "structured_data": {
117
+ "event_type": "http_error",
118
+ "request_id": request_id,
119
+ "method": request.method,
120
+ "path": request.url.path,
121
+ "error_type": type(e).__name__,
122
+ "error_message": str(e),
123
+ "process_time": process_time,
124
+ }
125
+ },
126
+ )
127
+
128
+ raise
129
+
130
+ def _generate_request_id(self, request: Request) -> str:
131
+ """Generate unique request ID."""
132
+ import uuid
133
+ return str(uuid.uuid4())[:8]
134
+
135
+ def _get_client_ip(self, request: Request) -> str:
136
+ """Extract client IP address from request."""
137
+ # Check for forwarded headers
138
+ forwarded_for = request.headers.get("x-forwarded-for")
139
+ if forwarded_for:
140
+ return forwarded_for.split(",")[0].strip()
141
+
142
+ real_ip = request.headers.get("x-real-ip")
143
+ if real_ip:
144
+ return real_ip
145
+
146
+ # Fall back to direct client IP
147
+ if hasattr(request.client, "host"):
148
+ return request.client.host
149
+
150
+ return "unknown"
151
+
152
+ def _log_response(
153
+ self,
154
+ request_id: str,
155
+ method: str,
156
+ path: str,
157
+ status_code: int,
158
+ process_time: float,
159
+ response: Response,
160
+ ) -> None:
161
+ """Log HTTP response information."""
162
+ # Determine log level based on status code
163
+ if status_code >= 500:
164
+ log_level = logging.ERROR
165
+ elif status_code >= 400:
166
+ log_level = logging.WARNING
167
+ else:
168
+ log_level = self.log_level
169
+
170
+ # Extract response information
171
+ content_length = response.headers.get("content-length")
172
+ content_type = response.headers.get("content-type")
173
+
174
+ # Check if this is a streaming response
175
+ is_streaming = isinstance(response, StreamingResponse)
176
+
177
+ self.logger.log(
178
+ log_level,
179
+ f"HTTP {method} {path} - {status_code}",
180
+ extra={
181
+ "structured_data": {
182
+ "event_type": "http_response",
183
+ "request_id": request_id,
184
+ "method": method,
185
+ "path": path,
186
+ "status_code": status_code,
187
+ "process_time": process_time,
188
+ "content_length": content_length,
189
+ "content_type": content_type,
190
+ "is_streaming": is_streaming,
191
+ "performance_category": self._categorize_performance(process_time),
192
+ }
193
+ },
194
+ )
195
+
196
+ def _categorize_performance(self, process_time: float) -> str:
197
+ """Categorize request performance."""
198
+ if process_time < 0.1:
199
+ return "fast"
200
+ elif process_time < 1.0:
201
+ return "normal"
202
+ elif process_time < 5.0:
203
+ return "slow"
204
+ else:
205
+ return "very_slow"
206
+
207
+
208
+ class HTTPMetricsCollector:
209
+ """
210
+ Collect HTTP metrics for monitoring.
211
+
212
+ Tracks request counts, response times, and error rates.
213
+ """
214
+
215
+ def __init__(self):
216
+ """Initialize metrics collector."""
217
+ self.request_count = 0
218
+ self.error_count = 0
219
+ self.total_response_time = 0.0
220
+ self.response_times = []
221
+ self.max_response_times = 1000 # Keep last 1000 response times
222
+
223
+ def record_request(
224
+ self,
225
+ method: str,
226
+ path: str,
227
+ status_code: int,
228
+ response_time: float,
229
+ ) -> None:
230
+ """Record HTTP request metrics."""
231
+ self.request_count += 1
232
+ self.total_response_time += response_time
233
+
234
+ # Track error rates
235
+ if status_code >= 400:
236
+ self.error_count += 1
237
+
238
+ # Track response times
239
+ self.response_times.append(response_time)
240
+ if len(self.response_times) > self.max_response_times:
241
+ self.response_times.pop(0)
242
+
243
+ def get_metrics(self) -> Dict[str, Any]:
244
+ """Get current metrics summary."""
245
+ if not self.request_count:
246
+ return {
247
+ "request_count": 0,
248
+ "error_count": 0,
249
+ "error_rate": 0.0,
250
+ "avg_response_time": 0.0,
251
+ "p95_response_time": 0.0,
252
+ "p99_response_time": 0.0,
253
+ }
254
+
255
+ # Calculate percentiles
256
+ sorted_times = sorted(self.response_times)
257
+ p95_index = int(0.95 * len(sorted_times))
258
+ p99_index = int(0.99 * len(sorted_times))
259
+
260
+ return {
261
+ "request_count": self.request_count,
262
+ "error_count": self.error_count,
263
+ "error_rate": self.error_count / self.request_count,
264
+ "avg_response_time": self.total_response_time / self.request_count,
265
+ "p95_response_time": sorted_times[p95_index] if sorted_times else 0.0,
266
+ "p99_response_time": sorted_times[p99_index] if sorted_times else 0.0,
267
+ }
268
+
269
+ def reset_metrics(self) -> None:
270
+ """Reset all metrics."""
271
+ self.request_count = 0
272
+ self.error_count = 0
273
+ self.total_response_time = 0.0
274
+ self.response_times.clear()