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 +34 -0
- mcpscore/cli.py +64 -0
- mcpscore/enums.py +44 -0
- mcpscore/mcp_auditor.py +224 -0
- mcpscore/mcp_client.py +408 -0
- mcpscore/py.typed +0 -0
- mcpscore/rules/__init__.py +89 -0
- mcpscore/rules/base.py +229 -0
- mcpscore/rules/capabilities.py +398 -0
- mcpscore/rules/protocol_version.py +187 -0
- mcpscore/rules/registry.py +105 -0
- mcpscore/rules/security.py +277 -0
- mcpscore/rules/server_info.py +181 -0
- mcpscore/rules/tools.py +472 -0
- mcpscore/rules/transport.py +77 -0
- mcpscore-0.3.0.dist-info/METADATA +150 -0
- mcpscore-0.3.0.dist-info/RECORD +20 -0
- mcpscore-0.3.0.dist-info/WHEEL +4 -0
- mcpscore-0.3.0.dist-info/entry_points.txt +2 -0
- mcpscore-0.3.0.dist-info/licenses/LICENSE +21 -0
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."""
|
mcpscore/mcp_auditor.py
ADDED
|
@@ -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
|
+
}
|