gopher-mcp 0.2.1__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.
gopher_mcp/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Gopher MCP - A Model Context Protocol server for Gopher and Gemini protocols.
2
+
3
+ This package provides a cross-platform MCP server that allows LLMs to browse
4
+ Gopher and Gemini resources safely and efficiently.
5
+ """
6
+
7
+ __version__ = "0.2.1"
8
+ __author__ = "Gopher MCP Team"
9
+ __email__ = "team@gopher-mcp.dev"
10
+ __license__ = "MIT"
11
+
12
+ from .server import gopher_fetch, gemini_fetch, mcp
13
+
14
+ __all__ = [
15
+ "mcp",
16
+ "gopher_fetch",
17
+ "gemini_fetch",
18
+ ]
gopher_mcp/__main__.py ADDED
@@ -0,0 +1,23 @@
1
+ """Main entry point for the Gopher MCP server."""
2
+
3
+ import sys
4
+
5
+ from .server import mcp
6
+
7
+
8
+ def main() -> None:
9
+ """Run the main entry point."""
10
+ try:
11
+ if len(sys.argv) > 1 and sys.argv[1] == "--http":
12
+ # HTTP transport not yet implemented in FastMCP
13
+ print("HTTP transport not yet supported", file=sys.stderr)
14
+ sys.exit(1)
15
+ else:
16
+ # FastMCP handles its own event loop
17
+ mcp.run(transport="stdio")
18
+ except KeyboardInterrupt:
19
+ pass
20
+
21
+
22
+ if __name__ == "__main__":
23
+ main()
@@ -0,0 +1,562 @@
1
+ """Certificate validation error handling for Gemini protocol."""
2
+
3
+ import ssl
4
+ import time
5
+ from enum import Enum
6
+ from typing import Dict, List, Optional, Any
7
+ from dataclasses import dataclass
8
+
9
+ import structlog
10
+
11
+ logger = structlog.get_logger(__name__)
12
+
13
+
14
+ class CertificateErrorType(Enum):
15
+ """Types of certificate validation errors."""
16
+
17
+ # TLS handshake errors
18
+ TLS_HANDSHAKE_FAILED = "tls_handshake_failed"
19
+ TLS_VERSION_NOT_SUPPORTED = "tls_version_not_supported"
20
+ TLS_CIPHER_NOT_SUPPORTED = "tls_cipher_not_supported"
21
+ TLS_SNI_FAILED = "tls_sni_failed"
22
+ TLS_CONNECTION_TIMEOUT = "tls_connection_timeout"
23
+
24
+ # Certificate validation errors
25
+ CERTIFICATE_EXPIRED = "certificate_expired"
26
+ CERTIFICATE_NOT_YET_VALID = "certificate_not_yet_valid"
27
+ CERTIFICATE_REVOKED = "certificate_revoked"
28
+ CERTIFICATE_INVALID_SIGNATURE = "certificate_invalid_signature"
29
+ CERTIFICATE_UNTRUSTED_ROOT = "certificate_untrusted_root"
30
+ CERTIFICATE_HOSTNAME_MISMATCH = "certificate_hostname_mismatch"
31
+ CERTIFICATE_CHAIN_INVALID = "certificate_chain_invalid"
32
+
33
+ # TOFU-specific errors
34
+ TOFU_CERTIFICATE_CHANGED = "tofu_certificate_changed"
35
+ TOFU_FINGERPRINT_MISMATCH = "tofu_fingerprint_mismatch"
36
+ TOFU_STORAGE_ERROR = "tofu_storage_error"
37
+ TOFU_VALIDATION_FAILED = "tofu_validation_failed"
38
+
39
+ # Client certificate errors
40
+ CLIENT_CERT_NOT_FOUND = "client_cert_not_found"
41
+ CLIENT_CERT_EXPIRED = "client_cert_expired"
42
+ CLIENT_CERT_INVALID = "client_cert_invalid"
43
+ CLIENT_CERT_KEY_MISMATCH = "client_cert_key_mismatch"
44
+ CLIENT_CERT_REJECTED = "client_cert_rejected"
45
+
46
+ # General errors
47
+ CERTIFICATE_PARSE_ERROR = "certificate_parse_error"
48
+ CERTIFICATE_ENCODING_ERROR = "certificate_encoding_error"
49
+ UNKNOWN_CERTIFICATE_ERROR = "unknown_certificate_error"
50
+
51
+
52
+ class CertificateErrorSeverity(Enum):
53
+ """Severity levels for certificate errors."""
54
+
55
+ LOW = "low"
56
+ MEDIUM = "medium"
57
+ HIGH = "high"
58
+ CRITICAL = "critical"
59
+
60
+
61
+ @dataclass
62
+ class CertificateError:
63
+ """Represents a certificate validation error."""
64
+
65
+ error_type: CertificateErrorType
66
+ message: str
67
+ severity: CertificateErrorSeverity
68
+ host: str
69
+ port: int
70
+ details: Dict[str, Any]
71
+ timestamp: float
72
+ recoverable: bool = False
73
+ recovery_suggestions: Optional[List[str]] = None
74
+
75
+ def __post_init__(self) -> None:
76
+ """Initialize recovery suggestions if not provided."""
77
+ if self.recovery_suggestions is None:
78
+ self.recovery_suggestions = []
79
+
80
+
81
+ class CertificateErrorHandler:
82
+ """Handles certificate validation errors and provides recovery suggestions."""
83
+
84
+ def __init__(self) -> None:
85
+ """Initialize error handler."""
86
+ self._error_counts: Dict[str, int] = {}
87
+ self._last_errors: Dict[str, CertificateError] = {}
88
+
89
+ def handle_ssl_error(
90
+ self,
91
+ error: ssl.SSLError,
92
+ host: str,
93
+ port: int,
94
+ context: Optional[Dict[str, Any]] = None,
95
+ ) -> CertificateError:
96
+ """Handle SSL/TLS errors and convert to CertificateError.
97
+
98
+ Args:
99
+ error: SSL error from Python ssl module
100
+ host: Target hostname
101
+ port: Target port
102
+ context: Additional context information
103
+
104
+ Returns:
105
+ CertificateError with appropriate type and recovery suggestions
106
+ """
107
+ context = context or {}
108
+ error_str = str(error).lower()
109
+
110
+ # Determine error type based on SSL error
111
+ if "certificate verify failed" in error_str:
112
+ if "certificate has expired" in error_str:
113
+ error_type = CertificateErrorType.CERTIFICATE_EXPIRED
114
+ severity = CertificateErrorSeverity.HIGH
115
+ recoverable = True
116
+ suggestions = [
117
+ "Check if the server certificate has expired",
118
+ "Contact the server administrator",
119
+ "Verify system clock is correct",
120
+ "Consider using TOFU mode if appropriate",
121
+ ]
122
+ elif "hostname" in error_str or "name" in error_str:
123
+ error_type = CertificateErrorType.CERTIFICATE_HOSTNAME_MISMATCH
124
+ severity = CertificateErrorSeverity.HIGH
125
+ recoverable = True
126
+ suggestions = [
127
+ f"Verify you're connecting to the correct hostname: {host}",
128
+ "Check for typos in the hostname",
129
+ "Verify DNS resolution is correct",
130
+ "Check if the server uses a different certificate name",
131
+ ]
132
+ elif "self signed" in error_str or "untrusted" in error_str:
133
+ error_type = CertificateErrorType.CERTIFICATE_UNTRUSTED_ROOT
134
+ severity = CertificateErrorSeverity.MEDIUM
135
+ recoverable = True
136
+ suggestions = [
137
+ "This is common in Gemini - consider using TOFU validation",
138
+ "Verify the certificate fingerprint manually",
139
+ "Check if this is the expected self-signed certificate",
140
+ "Contact the server administrator for certificate details",
141
+ ]
142
+ else:
143
+ error_type = CertificateErrorType.CERTIFICATE_INVALID_SIGNATURE
144
+ severity = CertificateErrorSeverity.HIGH
145
+ recoverable = False
146
+ suggestions = [
147
+ "Certificate signature validation failed",
148
+ "This may indicate a security issue",
149
+ "Do not proceed without verification",
150
+ "Contact the server administrator",
151
+ ]
152
+
153
+ elif "timeout" in error_str or "timed out" in error_str:
154
+ error_type = CertificateErrorType.TLS_CONNECTION_TIMEOUT
155
+ severity = CertificateErrorSeverity.MEDIUM
156
+ recoverable = True
157
+ suggestions = [
158
+ "Connection timed out during TLS handshake",
159
+ "Check network connectivity",
160
+ "Try increasing connection timeout",
161
+ "Verify the server is responding",
162
+ ]
163
+
164
+ elif "handshake failure" in error_str or "handshake" in error_str:
165
+ error_type = CertificateErrorType.TLS_HANDSHAKE_FAILED
166
+ severity = CertificateErrorSeverity.HIGH
167
+ recoverable = True
168
+ suggestions = [
169
+ "TLS handshake failed - check TLS version compatibility",
170
+ "Verify cipher suite compatibility",
171
+ "Check if client certificate is required",
172
+ "Try with different TLS configuration",
173
+ ]
174
+
175
+ elif "protocol" in error_str or "version" in error_str:
176
+ error_type = CertificateErrorType.TLS_VERSION_NOT_SUPPORTED
177
+ severity = CertificateErrorSeverity.HIGH
178
+ recoverable = True
179
+ suggestions = [
180
+ "TLS version not supported by server",
181
+ "Try with TLS 1.2 or 1.3",
182
+ "Check server TLS configuration",
183
+ "Update TLS client configuration",
184
+ ]
185
+
186
+ else:
187
+ error_type = CertificateErrorType.UNKNOWN_CERTIFICATE_ERROR
188
+ severity = CertificateErrorSeverity.MEDIUM
189
+ recoverable = False
190
+ suggestions = [
191
+ "Unknown SSL/TLS error occurred",
192
+ "Check server logs for more details",
193
+ "Verify network connectivity",
194
+ "Contact support if issue persists",
195
+ ]
196
+
197
+ cert_error = CertificateError(
198
+ error_type=error_type,
199
+ message=f"SSL/TLS error: {str(error)}",
200
+ severity=severity,
201
+ host=host,
202
+ port=port,
203
+ details={
204
+ "ssl_error": str(error),
205
+ "ssl_error_type": type(error).__name__,
206
+ "context": context,
207
+ },
208
+ timestamp=time.time(),
209
+ recoverable=recoverable,
210
+ recovery_suggestions=suggestions,
211
+ )
212
+
213
+ self._record_error(cert_error)
214
+ return cert_error
215
+
216
+ def handle_tofu_error(
217
+ self,
218
+ error_type: CertificateErrorType,
219
+ message: str,
220
+ host: str,
221
+ port: int,
222
+ details: Optional[Dict[str, Any]] = None,
223
+ ) -> CertificateError:
224
+ """Handle TOFU-specific errors.
225
+
226
+ Args:
227
+ error_type: Type of TOFU error
228
+ message: Error message
229
+ host: Target hostname
230
+ port: Target port
231
+ details: Additional error details
232
+
233
+ Returns:
234
+ CertificateError with TOFU-specific recovery suggestions
235
+ """
236
+ details = details or {}
237
+
238
+ if error_type == CertificateErrorType.TOFU_CERTIFICATE_CHANGED:
239
+ severity = CertificateErrorSeverity.CRITICAL
240
+ recoverable = True
241
+ suggestions = [
242
+ "Certificate has changed since first connection",
243
+ "This could indicate a security issue or server update",
244
+ "Verify the change is legitimate with the server administrator",
245
+ "Check the new certificate fingerprint manually",
246
+ "Only accept if you trust the new certificate",
247
+ ]
248
+
249
+ elif error_type == CertificateErrorType.TOFU_FINGERPRINT_MISMATCH:
250
+ severity = CertificateErrorSeverity.HIGH
251
+ recoverable = True
252
+ suggestions = [
253
+ "Certificate fingerprint does not match stored value",
254
+ "Verify the certificate fingerprint manually",
255
+ "Check if the server certificate was updated",
256
+ "Contact the server administrator for verification",
257
+ ]
258
+
259
+ elif error_type == CertificateErrorType.TOFU_STORAGE_ERROR:
260
+ severity = CertificateErrorSeverity.MEDIUM
261
+ recoverable = True
262
+ suggestions = [
263
+ "Error accessing TOFU certificate storage",
264
+ "Check file permissions for certificate store",
265
+ "Verify disk space availability",
266
+ "Check certificate store path configuration",
267
+ ]
268
+
269
+ else:
270
+ severity = CertificateErrorSeverity.MEDIUM
271
+ recoverable = True
272
+ suggestions = [
273
+ "TOFU validation failed",
274
+ "Check TOFU configuration",
275
+ "Verify certificate storage is accessible",
276
+ "Try clearing TOFU cache if appropriate",
277
+ ]
278
+
279
+ cert_error = CertificateError(
280
+ error_type=error_type,
281
+ message=message,
282
+ severity=severity,
283
+ host=host,
284
+ port=port,
285
+ details=details,
286
+ timestamp=time.time(),
287
+ recoverable=recoverable,
288
+ recovery_suggestions=suggestions,
289
+ )
290
+
291
+ self._record_error(cert_error)
292
+ return cert_error
293
+
294
+ def handle_client_certificate_error(
295
+ self,
296
+ error_type: CertificateErrorType,
297
+ message: str,
298
+ host: str,
299
+ port: int,
300
+ cert_path: Optional[str] = None,
301
+ details: Optional[Dict[str, Any]] = None,
302
+ ) -> CertificateError:
303
+ """Handle client certificate errors.
304
+
305
+ Args:
306
+ error_type: Type of client certificate error
307
+ message: Error message
308
+ host: Target hostname
309
+ port: Target port
310
+ cert_path: Path to client certificate
311
+ details: Additional error details
312
+
313
+ Returns:
314
+ CertificateError with client certificate recovery suggestions
315
+ """
316
+ details = details or {}
317
+ if cert_path:
318
+ details["cert_path"] = cert_path
319
+
320
+ if error_type == CertificateErrorType.CLIENT_CERT_NOT_FOUND:
321
+ severity = CertificateErrorSeverity.MEDIUM
322
+ recoverable = True
323
+ suggestions = [
324
+ "Client certificate not found",
325
+ "Check certificate file path",
326
+ "Generate a new client certificate if needed",
327
+ "Verify certificate file permissions",
328
+ ]
329
+
330
+ elif error_type == CertificateErrorType.CLIENT_CERT_EXPIRED:
331
+ severity = CertificateErrorSeverity.MEDIUM
332
+ recoverable = True
333
+ suggestions = [
334
+ "Client certificate has expired",
335
+ "Generate a new client certificate",
336
+ "Check certificate validity period",
337
+ "Update certificate before expiry",
338
+ ]
339
+
340
+ elif error_type == CertificateErrorType.CLIENT_CERT_REJECTED:
341
+ severity = CertificateErrorSeverity.HIGH
342
+ recoverable = True
343
+ suggestions = [
344
+ "Server rejected the client certificate",
345
+ "Verify certificate is valid for this server",
346
+ "Check certificate scope and permissions",
347
+ "Contact server administrator for certificate requirements",
348
+ ]
349
+
350
+ else:
351
+ severity = CertificateErrorSeverity.MEDIUM
352
+ recoverable = True
353
+ suggestions = [
354
+ "Client certificate error",
355
+ "Check certificate file format",
356
+ "Verify certificate and key match",
357
+ "Generate a new certificate if needed",
358
+ ]
359
+
360
+ cert_error = CertificateError(
361
+ error_type=error_type,
362
+ message=message,
363
+ severity=severity,
364
+ host=host,
365
+ port=port,
366
+ details=details,
367
+ timestamp=time.time(),
368
+ recoverable=recoverable,
369
+ recovery_suggestions=suggestions,
370
+ )
371
+
372
+ self._record_error(cert_error)
373
+ return cert_error
374
+
375
+ def handle_certificate_expiry(
376
+ self,
377
+ host: str,
378
+ port: int,
379
+ not_before: str,
380
+ not_after: str,
381
+ current_time: Optional[float] = None,
382
+ ) -> CertificateError:
383
+ """Handle certificate expiry errors.
384
+
385
+ Args:
386
+ host: Target hostname
387
+ port: Target port
388
+ not_before: Certificate validity start
389
+ not_after: Certificate validity end
390
+ current_time: Current timestamp (defaults to now)
391
+
392
+ Returns:
393
+ CertificateError for expiry issue
394
+ """
395
+ current_time = current_time or time.time()
396
+
397
+ try:
398
+ # Parse certificate dates (assuming ISO format or timestamp)
399
+ if not_before.isdigit():
400
+ not_before_ts = float(not_before)
401
+ else:
402
+ # Try to parse as timestamp, if that fails, raise error
403
+ raise ValueError(f"Invalid date format: {not_before}")
404
+
405
+ if not_after.isdigit():
406
+ not_after_ts = float(not_after)
407
+ # Check if certificate has expired
408
+ if current_time > not_after_ts:
409
+ # Certificate has expired - create error immediately
410
+ cert_error = CertificateError(
411
+ error_type=CertificateErrorType.CERTIFICATE_EXPIRED,
412
+ message=f"Certificate expired (expired on {not_after})",
413
+ severity=CertificateErrorSeverity.HIGH,
414
+ host=host,
415
+ port=port,
416
+ details={
417
+ "not_before": not_before,
418
+ "not_after": not_after,
419
+ "current_time": current_time,
420
+ },
421
+ timestamp=time.time(),
422
+ recoverable=True,
423
+ recovery_suggestions=[
424
+ "Certificate has expired",
425
+ "Contact server administrator for certificate renewal",
426
+ "Check if server has updated certificate",
427
+ "Verify system clock is correct",
428
+ ],
429
+ )
430
+ self._record_error(cert_error)
431
+ return cert_error
432
+ else:
433
+ # Try to parse as timestamp, if that fails, raise error
434
+ raise ValueError(f"Invalid date format: {not_after}")
435
+
436
+ if current_time < not_before_ts:
437
+ error_type = CertificateErrorType.CERTIFICATE_NOT_YET_VALID
438
+ message = f"Certificate not yet valid (valid from {not_before})"
439
+ suggestions = [
440
+ "Certificate is not yet valid",
441
+ "Check system clock is correct",
442
+ "Wait until certificate becomes valid",
443
+ "Contact server administrator if clock is correct",
444
+ ]
445
+ else:
446
+ error_type = CertificateErrorType.CERTIFICATE_EXPIRED
447
+ message = f"Certificate expired (expired on {not_after})"
448
+ suggestions = [
449
+ "Certificate has expired",
450
+ "Contact server administrator for certificate renewal",
451
+ "Check if server has updated certificate",
452
+ "Verify system clock is correct",
453
+ ]
454
+
455
+ except (ValueError, TypeError):
456
+ error_type = CertificateErrorType.CERTIFICATE_PARSE_ERROR
457
+ message = "Could not parse certificate validity dates"
458
+ suggestions = [
459
+ "Certificate date parsing failed",
460
+ "Certificate may be malformed",
461
+ "Contact server administrator",
462
+ "Check certificate format",
463
+ ]
464
+
465
+ cert_error = CertificateError(
466
+ error_type=error_type,
467
+ message=message,
468
+ severity=CertificateErrorSeverity.HIGH,
469
+ host=host,
470
+ port=port,
471
+ details={
472
+ "not_before": not_before,
473
+ "not_after": not_after,
474
+ "current_time": current_time,
475
+ },
476
+ timestamp=time.time(),
477
+ recoverable=True,
478
+ recovery_suggestions=suggestions,
479
+ )
480
+
481
+ self._record_error(cert_error)
482
+ return cert_error
483
+
484
+ def _record_error(self, error: CertificateError) -> None:
485
+ """Record error for tracking and analysis.
486
+
487
+ Args:
488
+ error: Certificate error to record
489
+ """
490
+ host_key = f"{error.host}:{error.port}"
491
+
492
+ # Update error count
493
+ self._error_counts[host_key] = self._error_counts.get(host_key, 0) + 1
494
+
495
+ # Store last error
496
+ self._last_errors[host_key] = error
497
+
498
+ # Log error
499
+ logger.error(
500
+ "Certificate validation error",
501
+ error_type=error.error_type.value,
502
+ severity=error.severity.value,
503
+ host=error.host,
504
+ port=error.port,
505
+ message=error.message,
506
+ recoverable=error.recoverable,
507
+ error_count=self._error_counts[host_key],
508
+ )
509
+
510
+ def get_error_statistics(self) -> Dict[str, Any]:
511
+ """Get error statistics for monitoring.
512
+
513
+ Returns:
514
+ Dictionary with error statistics
515
+ """
516
+ total_errors = sum(self._error_counts.values())
517
+ error_types: Dict[str, int] = {}
518
+
519
+ for error in self._last_errors.values():
520
+ error_type = error.error_type.value
521
+ error_types[error_type] = error_types.get(error_type, 0) + 1
522
+
523
+ return {
524
+ "total_errors": total_errors,
525
+ "unique_hosts": len(self._error_counts),
526
+ "error_types": error_types,
527
+ "hosts_with_errors": list(self._error_counts.keys()),
528
+ }
529
+
530
+ def get_last_error(self, host: str, port: int) -> Optional[CertificateError]:
531
+ """Get the last error for a specific host.
532
+
533
+ Args:
534
+ host: Target hostname
535
+ port: Target port
536
+
537
+ Returns:
538
+ Last CertificateError for the host or None
539
+ """
540
+ host_key = f"{host}:{port}"
541
+ return self._last_errors.get(host_key)
542
+
543
+ def clear_errors(
544
+ self, host: Optional[str] = None, port: Optional[int] = None
545
+ ) -> None:
546
+ """Clear error records.
547
+
548
+ Args:
549
+ host: Specific host to clear (clears all if None)
550
+ port: Specific port to clear (clears all if None)
551
+ """
552
+ if host and port:
553
+ host_key = f"{host}:{port}"
554
+ self._error_counts.pop(host_key, None)
555
+ self._last_errors.pop(host_key, None)
556
+ else:
557
+ self._error_counts.clear()
558
+ self._last_errors.clear()
559
+
560
+
561
+ # Global error handler instance
562
+ certificate_error_handler = CertificateErrorHandler()