mcpscore 0.3.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.
mcpscore/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ """MCPDoctor - A comprehensive auditing tool for MCP (Model Context Protocol) servers.
2
+
3
+ This package provides tools for auditing MCP servers to ensure compliance with
4
+ protocol standards and best practices. It includes:
5
+
6
+ - MCPClient: For connecting to and communicating with MCP servers
7
+ - MCPDoctor: For orchestrating the audit process
8
+ - Rule system: Extensible framework for implementing audit checks
9
+ - Enums: Protocol versions and transport types
10
+
11
+ The audit system uses a rule-based approach where each rule checks specific
12
+ aspects of MCP compliance and contributes to an overall audit score.
13
+ """
14
+
15
+ from .enums import MCPProtocolVersion, MCPTransportType
16
+ from .mcp_auditor import MCPAuditor
17
+ from .mcp_client import MCPClient
18
+ from .rules import (
19
+ AuditData,
20
+ BaseRule,
21
+ RuleResult,
22
+ RuleSeverity,
23
+ )
24
+
25
+ __all__ = (
26
+ "AuditData",
27
+ "BaseRule",
28
+ "MCPAuditor",
29
+ "MCPClient",
30
+ "MCPProtocolVersion",
31
+ "MCPTransportType",
32
+ "RuleResult",
33
+ "RuleSeverity",
34
+ )
mcpscore/cli.py ADDED
@@ -0,0 +1,64 @@
1
+ """Command-line interface for MCPScore."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import sys
6
+
7
+ from mcpscore import MCPAuditor, MCPClient
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ async def async_main() -> None:
13
+ """Execute the main entry point for the MCPScore CLI application.
14
+
15
+ Orchestrates the audit process by:
16
+ 1. Parsing command line arguments for the server path or URL
17
+ 2. Creating MCP client and auditor instances
18
+ 3. Auto-detecting transport and connecting to the MCP server
19
+ 4. Running the audit process and displaying results
20
+ 5. Cleaning up resources
21
+
22
+ Supports local servers (.py, .js) via STDIO and remote servers via
23
+ Streamable HTTP or SSE (auto-detected).
24
+
25
+ Exits with code 1 if no server path is provided, or code 2 if connection fails.
26
+ """
27
+ logger.info("Welcome to MCPScore!")
28
+
29
+ if len(sys.argv) < 2:
30
+ logger.error("Usage: mcpscore <server_path_or_url>")
31
+ sys.exit(1)
32
+
33
+ target: str = sys.argv[1]
34
+ client: MCPClient = MCPClient()
35
+ doctor: MCPAuditor = MCPAuditor()
36
+
37
+ success, transport = await client.detect_and_connect(target)
38
+
39
+ if success:
40
+ logger.info("Connected to the MCP server: %s", target)
41
+ logger.info("Transport: %s", transport)
42
+ else:
43
+ logger.error("Error connecting to the MCP server: %s", target)
44
+ sys.exit(2)
45
+
46
+ logger.info("Starting the audit...")
47
+ final_score, max_score = await doctor.audit(client)
48
+ logger.info("Audit finished. Final score: %s/%s", final_score, max_score)
49
+
50
+ await client.cleanup()
51
+
52
+
53
+ def main() -> None:
54
+ """Entry point for the mcpscore CLI command.
55
+
56
+ This function is called when running `mcpscore` from the command line.
57
+ It sets up logging and runs the async main function.
58
+ """
59
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
60
+ asyncio.run(async_main())
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
mcpscore/enums.py ADDED
@@ -0,0 +1,44 @@
1
+ """Enumerations and constants for MCP (Model Context Protocol) auditing.
2
+
3
+ This module defines the core enumerations used throughout the MCPDoctor system:
4
+
5
+ - MCPTransportType: Supported transport methods for MCP communication
6
+ - MCPProtocolVersion: Supported versions of the MCP protocol
7
+
8
+ These enums provide type safety and ensure consistent usage of protocol
9
+ versions and transport types across the audit system.
10
+ """
11
+
12
+ from enum import StrEnum
13
+
14
+
15
+ class MCPTransportType(StrEnum):
16
+ """Transport types supported by MCP (Model Context Protocol)."""
17
+
18
+ STDIO = "stdio"
19
+ """Standard input/output transport for local processes."""
20
+
21
+ STREAMABLE_HTTP = "streamable-http"
22
+ """HTTP-based transport with streaming capabilities."""
23
+
24
+ SSE = "sse"
25
+ """Server-Sent Events transport for real-time communication."""
26
+
27
+ WEBSOCKET = "websocket"
28
+ """WebSocket transport for bidirectional communication."""
29
+
30
+
31
+ class MCPProtocolVersion(StrEnum):
32
+ """Supported versions of the MCP (Model Context Protocol)."""
33
+
34
+ v2024_11_05 = "2024-11-05"
35
+ """MCP protocol version from November 5, 2024."""
36
+
37
+ v2025_03_26 = "2025-03-26"
38
+ """MCP protocol version from March 26, 2025."""
39
+
40
+ v2025_06_18 = "2025-06-18"
41
+ """Latest MCP protocol version (June 18, 2025)."""
42
+
43
+ Latest = v2025_06_18
44
+ """Alias for the latest protocol version."""
@@ -0,0 +1,224 @@
1
+ import logging
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from mcp.types import InitializeResult, Prompt, Resource, Tool
6
+
7
+ from .mcp_client import MCPClient
8
+ from .rules import AuditData, BaseRule, RuleResult, RuleSeverity, create_all_rules
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class MCPAuditor:
14
+ """Orchestrates the MCP server audit process.
15
+
16
+ This class manages the complete audit workflow:
17
+ - Collects initialization data from the MCP server
18
+ - Executes all registered audit rules
19
+ - Tracks audit results and scoring
20
+ - Provides audit summary and reporting
21
+
22
+ The doctor uses a rule-based system where each rule checks specific
23
+ aspects of MCP compliance and contributes to an overall audit score.
24
+ """
25
+
26
+ def __init__(self) -> None:
27
+ """Initialize a new MCPDoctor instance.
28
+
29
+ Sets up the doctor with:
30
+ - Empty audit data container
31
+ - All registered audit rules
32
+ - Zero initial score
33
+ - Empty results list
34
+ """
35
+ super().__init__()
36
+ self.mcp_client: MCPClient | None = None
37
+ self.audit_data: AuditData = AuditData()
38
+ self.score: int = 0
39
+ self.max_score: int = 0
40
+ self.rules: list[BaseRule] = list(create_all_rules())
41
+ self.results: list[RuleResult] = []
42
+
43
+ async def audit(self, client: MCPClient) -> tuple[int, int]:
44
+ """Execute the complete audit process for an MCP server.
45
+
46
+ Args:
47
+ client: Connected MCPClient instance to audit
48
+
49
+ Returns:
50
+ Final audit score (positive for passed rules, negative for failed rules)
51
+
52
+ The audit process:
53
+ 1. Collects server initialization data
54
+ 2. Runs all registered audit rules
55
+ 3. Calculates and returns the final score
56
+
57
+ """
58
+ self.mcp_client = client
59
+ self.score = 0
60
+ self.max_score = 0
61
+
62
+ await self._collect_transport_metadata()
63
+ await self._collect_init_result()
64
+ if self.audit_data.capabilities is not None:
65
+ if self.audit_data.capabilities.tools is not None:
66
+ await self._collect_tools()
67
+ if self.audit_data.capabilities.resources is not None:
68
+ await self._collect_resources()
69
+ if self.audit_data.capabilities.prompts is not None:
70
+ await self._collect_prompts()
71
+ await self._run_all_rules()
72
+
73
+ return self.score, self.max_score
74
+
75
+ async def _run_all_rules(self) -> None:
76
+ """Execute all registered audit rules and update the audit score.
77
+
78
+ Iterates through all rules, executes each one, logs the results,
79
+ and updates the overall audit score based on rule severity and pass/fail status.
80
+ """
81
+ for rule in sorted(self.rules, key=lambda r: r.sort_order):
82
+ res: RuleResult = rule.check(self.audit_data)
83
+ logger.info(res.message)
84
+
85
+ self.max_score += res.severity.value
86
+ if res.passed:
87
+ self.score += res.severity.value
88
+
89
+ self.results.append(res)
90
+
91
+ async def _collect_transport_metadata(self) -> None:
92
+ """Collect transport and connection metadata from the MCP client.
93
+
94
+ Populates audit data with:
95
+ - Transport type (STDIO, HTTP, SSE)
96
+ - URL (for HTTP/SSE connections)
97
+ - TLS information (for HTTPS connections)
98
+ - Connection timing
99
+
100
+ This data is used by security and transport audit rules.
101
+ """
102
+ if self.mcp_client is None:
103
+ logger.error("No MCP client to audit")
104
+ return
105
+
106
+ # Collect transport metadata from client
107
+ self.audit_data.transport_type = self.mcp_client.transport_type
108
+ self.audit_data.url = self.mcp_client.url
109
+ self.audit_data.connection_time_ms = self.mcp_client.connection_time_ms
110
+
111
+ # For HTTPS connections, check TLS
112
+ if self.mcp_client.url and self.mcp_client.url.startswith("https://"):
113
+ # If we successfully connected via HTTPS, assume TLS is verified
114
+ # (httpx would have failed the connection if cert validation failed)
115
+ self.audit_data.tls_verified = True
116
+ self.audit_data.tls_version = "TLSv1.3" # Modern default, could be probed more precisely
117
+ elif self.mcp_client.url and self.mcp_client.url.startswith("http://"):
118
+ self.audit_data.tls_verified = False
119
+ self.audit_data.tls_version = None
120
+
121
+ async def _collect_init_result(self) -> None:
122
+ """Collect initialization data from the MCP server.
123
+
124
+ Retrieves the server's initialization result and populates the audit data
125
+ with protocol version, server info, capabilities, and instructions.
126
+
127
+ This data is then used by all audit rules to perform their checks.
128
+ """
129
+ if self.mcp_client is None:
130
+ logger.error("No MCP client to audit")
131
+ return
132
+
133
+ init_result: InitializeResult | None = await self.mcp_client.initialize()
134
+ if init_result is None:
135
+ logger.error("No Init Result to audit")
136
+ return
137
+ else:
138
+ self.audit_data.protocol_version = str(init_result.protocolVersion)
139
+ self.audit_data.server_info = init_result.serverInfo
140
+ self.audit_data.capabilities = init_result.capabilities
141
+ self.audit_data.instructions = init_result.instructions
142
+
143
+ async def _collect_tools(self) -> None:
144
+ """Collect the list of Tools from the MCP server.
145
+
146
+ Retrieves the server's Tools and populates the audit data with
147
+ information about them.
148
+
149
+ This data is then used by all audit rules to perform their checks.
150
+ """
151
+ if self.mcp_client is None:
152
+ logger.error("No MCP client to audit")
153
+ return
154
+
155
+ tools: list[Tool] | None = await self.mcp_client.list_tools()
156
+ if tools is None:
157
+ logger.error("No Tools to audit")
158
+ return
159
+ else:
160
+ self.audit_data.tools = tools
161
+
162
+ async def _collect_resources(self) -> None:
163
+ """Collect the list of Resources from the MCP server.
164
+
165
+ Retrieves the server's Resources and populates the audit data with
166
+ information about them.
167
+
168
+ This data is then used by all audit rules to perform their checks.
169
+ """
170
+ if self.mcp_client is None:
171
+ logger.error("No MCP client to audit")
172
+ return
173
+
174
+ resources: list[Resource] | None = await self.mcp_client.list_resources()
175
+ if resources is None:
176
+ logger.error("No Resources to audit")
177
+ return
178
+ else:
179
+ self.audit_data.resources = resources
180
+
181
+ async def _collect_prompts(self) -> None:
182
+ """Collect the list of Prompts from the MCP server.
183
+
184
+ Retrieves the server's Prompts and populates the audit data with
185
+ information about them.
186
+
187
+ This data is then used by all audit rules to perform their checks.
188
+ """
189
+ if self.mcp_client is None:
190
+ logger.error("No MCP client to audit")
191
+ return
192
+
193
+ prompts: list[Prompt] | None = await self.mcp_client.list_prompts()
194
+ if prompts is None:
195
+ logger.error("No prompts to audit")
196
+ return
197
+ else:
198
+ self.audit_data.prompts = prompts
199
+
200
+ def get_audit_summary(self) -> dict:
201
+ """Generate a comprehensive summary of the audit results.
202
+
203
+ Returns:
204
+ Dictionary containing:
205
+ - total: Total number of rules executed
206
+ - passed: Number of rules that passed
207
+ - failed: Number of rules that failed
208
+ - by_severity: Breakdown by severity level (CRITICAL, HIGH, MEDIUM, LOW)
209
+ with counts for total, passed, and failed rules in each category
210
+
211
+ """
212
+ return {
213
+ "total": len(self.results),
214
+ "passed": sum(1 for r in self.results if r.passed),
215
+ "failed": sum(1 for r in self.results if not r.passed),
216
+ "by_severity": {
217
+ severity.value: {
218
+ "total": sum(1 for r in self.results if r.severity == severity),
219
+ "passed": sum(1 for r in self.results if r.severity == severity and r.passed),
220
+ "failed": sum(1 for r in self.results if r.severity == severity and not r.passed),
221
+ }
222
+ for severity in RuleSeverity
223
+ },
224
+ }