mcppt 1.0.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.
- mcppt/__init__.py +1 -0
- mcppt/checks.py +1720 -0
- mcppt/cli.py +243 -0
- mcppt/core.py +169 -0
- mcppt/report.py +105 -0
- mcppt/server.py +254 -0
- mcppt/shell.py +508 -0
- mcppt/tui.py +160 -0
- mcppt-1.0.0.dist-info/METADATA +432 -0
- mcppt-1.0.0.dist-info/RECORD +13 -0
- mcppt-1.0.0.dist-info/WHEEL +4 -0
- mcppt-1.0.0.dist-info/entry_points.txt +2 -0
- mcppt-1.0.0.dist-info/licenses/LICENSE +21 -0
mcppt/server.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCPPT MCP Server mode — expose MCPPT itself as an MCP server.
|
|
3
|
+
|
|
4
|
+
Run: mcppt serve-mcp [--port 8899]
|
|
5
|
+
|
|
6
|
+
Any MCP client (Claude Desktop, MCP Inspector, another Claude agent)
|
|
7
|
+
can then call MCPPT tools:
|
|
8
|
+
- scan_target → run security scan, returns findings JSON
|
|
9
|
+
- list_tools → enumerate tools on a target MCP server
|
|
10
|
+
- call_tool → call a specific tool on a target MCP server
|
|
11
|
+
- get_checks → list all 16 available checks with descriptions
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import threading
|
|
17
|
+
|
|
18
|
+
from flask import Flask, request, Response
|
|
19
|
+
|
|
20
|
+
app = Flask(__name__)
|
|
21
|
+
|
|
22
|
+
MCPPT_TOOLS = [
|
|
23
|
+
{
|
|
24
|
+
"name": "scan_target",
|
|
25
|
+
"description": (
|
|
26
|
+
"Run MCPPT security scan against an MCP server. "
|
|
27
|
+
"Returns findings with severity, check name, title, and detail. "
|
|
28
|
+
"Checks: enum,auth,idor,injection,schema,ssrf,publish,rate,stored,"
|
|
29
|
+
"scope,replay,context_overflow,poison_all,tenant,session,rug_pull,all"
|
|
30
|
+
),
|
|
31
|
+
"inputSchema": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"url": {"type": "string", "description": "Target MCP server URL"},
|
|
35
|
+
"token": {"type": "string", "description": "Bearer token (optional)"},
|
|
36
|
+
"token2": {"type": "string", "description": "Second user token for IDOR/scope checks (optional)"},
|
|
37
|
+
"checks": {"type": "string", "description": "Comma-separated checks or 'all' (default: all)"},
|
|
38
|
+
"no_verify": {"type": "boolean","description": "Skip SSL cert verification (default: false)"},
|
|
39
|
+
"proxy": {"type": "string", "description": "HTTP proxy URL e.g. http://127.0.0.1:8080 (optional)"},
|
|
40
|
+
},
|
|
41
|
+
"required": ["url"],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "list_tools",
|
|
46
|
+
"description": "Enumerate tools and schemas from a target MCP server.",
|
|
47
|
+
"inputSchema": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"properties": {
|
|
50
|
+
"url": {"type": "string", "description": "Target MCP server URL"},
|
|
51
|
+
"token": {"type": "string", "description": "Bearer token (optional)"},
|
|
52
|
+
"no_verify": {"type": "boolean","description": "Skip SSL cert verification"},
|
|
53
|
+
},
|
|
54
|
+
"required": ["url"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "call_tool",
|
|
59
|
+
"description": "Call a specific tool on a target MCP server and return the response.",
|
|
60
|
+
"inputSchema": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"url": {"type": "string", "description": "Target MCP server URL"},
|
|
64
|
+
"token": {"type": "string", "description": "Bearer token (optional)"},
|
|
65
|
+
"tool_name": {"type": "string", "description": "Name of the tool to call"},
|
|
66
|
+
"args": {"type": "object", "description": "JSON arguments for the tool"},
|
|
67
|
+
"no_verify": {"type": "boolean","description": "Skip SSL cert verification"},
|
|
68
|
+
},
|
|
69
|
+
"required": ["url", "tool_name"],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "get_checks",
|
|
74
|
+
"description": "Return the list of all 16 MCPPT security checks with descriptions.",
|
|
75
|
+
"inputSchema": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"properties": {},
|
|
78
|
+
"required": [],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
CHECKS_INFO = [
|
|
84
|
+
{"id": "enum", "severity": "MEDIUM", "description": "tools/list accessible without auth"},
|
|
85
|
+
{"id": "auth", "severity": "CRITICAL", "description": "Tool calls succeed with no/invalid token"},
|
|
86
|
+
{"id": "idor", "severity": "HIGH", "description": "Cross-user resource access (needs token2)"},
|
|
87
|
+
{"id": "injection", "severity": "HIGH", "description": "Prompt injection payloads reflected"},
|
|
88
|
+
{"id": "schema", "severity": "MEDIUM", "description": "Type confusion, oversized input, null bypass"},
|
|
89
|
+
{"id": "ssrf", "severity": "CRITICAL", "description": "Cloud metadata URLs fetched via tool params"},
|
|
90
|
+
{"id": "publish", "severity": "CRITICAL", "description": "Destructive tool without confirmation gate"},
|
|
91
|
+
{"id": "rate", "severity": "LOW", "description": "No rate limiting on tool calls"},
|
|
92
|
+
{"id": "stored", "severity": "CRITICAL", "description": "Stored prompt injection: write→read unescaped"},
|
|
93
|
+
{"id": "scope", "severity": "HIGH", "description": "Read-only token reaches write tools"},
|
|
94
|
+
{"id": "replay", "severity": "HIGH", "description": "Same request accepted twice, no nonce"},
|
|
95
|
+
{"id": "context_overflow", "severity": "HIGH", "description": "100K-char payload → LLM context truncation"},
|
|
96
|
+
{"id": "poison_all", "severity": "CRITICAL", "description": "Injection in any response field (CyberArk)"},
|
|
97
|
+
{"id": "tenant", "severity": "CRITICAL", "description": "Token2 reads token1 data (isolation broken)"},
|
|
98
|
+
{"id": "session", "severity": "HIGH", "description": "Weak/sequential session IDs (CVE-2025-6515)"},
|
|
99
|
+
{"id": "rug_pull", "severity": "CRITICAL", "description": "Tool descriptions change mid-session"},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
_session_id = "mcppt-server-session-001"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _sse(body: dict) -> Response:
|
|
106
|
+
return Response(
|
|
107
|
+
f"event: message\ndata: {json.dumps(body)}\n\n",
|
|
108
|
+
mimetype="text/event-stream",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _text_result(text: str) -> dict:
|
|
113
|
+
return {"result": {"content": [{"type": "text", "text": text}]}}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── tool handlers ─────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def _handle_scan_target(args: dict, req_id: int) -> Response:
|
|
119
|
+
from .core import configure
|
|
120
|
+
from .checks import ScanState, run_scan
|
|
121
|
+
|
|
122
|
+
url = args.get("url", "")
|
|
123
|
+
token = args.get("token") or None
|
|
124
|
+
token2 = args.get("token2") or None
|
|
125
|
+
checks = [c.strip() for c in args.get("checks", "all").split(",")]
|
|
126
|
+
no_verify= bool(args.get("no_verify", False))
|
|
127
|
+
proxy = args.get("proxy") or None
|
|
128
|
+
|
|
129
|
+
configure(no_verify=no_verify, proxy=proxy)
|
|
130
|
+
state = ScanState(url=url, token=token, token2=token2)
|
|
131
|
+
|
|
132
|
+
t = threading.Thread(target=run_scan, args=(state, checks), daemon=True)
|
|
133
|
+
t.start()
|
|
134
|
+
t.join(timeout=120)
|
|
135
|
+
|
|
136
|
+
findings_data = [
|
|
137
|
+
{"check": f.check, "severity": f.severity, "title": f.title, "detail": f.detail}
|
|
138
|
+
for f in state.findings
|
|
139
|
+
]
|
|
140
|
+
from collections import Counter
|
|
141
|
+
counts = Counter(f.severity for f in state.findings)
|
|
142
|
+
result = {
|
|
143
|
+
"target": url,
|
|
144
|
+
"elapsed_seconds": round(state.elapsed, 1),
|
|
145
|
+
"summary": {
|
|
146
|
+
"CRITICAL": counts.get("CRITICAL", 0),
|
|
147
|
+
"HIGH": counts.get("HIGH", 0),
|
|
148
|
+
"MEDIUM": counts.get("MEDIUM", 0),
|
|
149
|
+
"LOW": counts.get("LOW", 0),
|
|
150
|
+
"total": len(state.findings),
|
|
151
|
+
},
|
|
152
|
+
"findings": findings_data,
|
|
153
|
+
}
|
|
154
|
+
return _sse({"jsonrpc": "2.0", "id": req_id, **_text_result(json.dumps(result, indent=2))})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _handle_list_tools(args: dict, req_id: int) -> Response:
|
|
158
|
+
from .core import configure, mcp_init, rpc
|
|
159
|
+
|
|
160
|
+
url = args.get("url", "")
|
|
161
|
+
token = args.get("token") or None
|
|
162
|
+
no_verify= bool(args.get("no_verify", False))
|
|
163
|
+
|
|
164
|
+
configure(no_verify=no_verify)
|
|
165
|
+
mcp_init(url, token)
|
|
166
|
+
r = rpc(url, "tools/list", {}, token=token)
|
|
167
|
+
tools = r["body"].get("result", {}).get("tools", []) if r["status"] == 200 else []
|
|
168
|
+
return _sse({"jsonrpc": "2.0", "id": req_id, **_text_result(json.dumps(tools, indent=2))})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _handle_call_tool(args: dict, req_id: int) -> Response:
|
|
172
|
+
from .core import configure, mcp_init, rpc
|
|
173
|
+
|
|
174
|
+
url = args.get("url", "")
|
|
175
|
+
token = args.get("token") or None
|
|
176
|
+
tool_name = args.get("tool_name", "")
|
|
177
|
+
tool_args = args.get("args", {})
|
|
178
|
+
no_verify = bool(args.get("no_verify", False))
|
|
179
|
+
|
|
180
|
+
configure(no_verify=no_verify)
|
|
181
|
+
mcp_init(url, token)
|
|
182
|
+
r = rpc(url, "tools/call", {"name": tool_name, "arguments": tool_args}, token=token)
|
|
183
|
+
return _sse({"jsonrpc": "2.0", "id": req_id, **_text_result(json.dumps(r, indent=2))})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _handle_get_checks(req_id: int) -> Response:
|
|
187
|
+
return _sse({"jsonrpc": "2.0", "id": req_id, **_text_result(json.dumps(CHECKS_INFO, indent=2))})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── MCP endpoint ──────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
@app.route("/mcp", methods=["POST"])
|
|
193
|
+
def mcp_endpoint():
|
|
194
|
+
body = request.get_json(force=True, silent=True) or {}
|
|
195
|
+
method = body.get("method", "")
|
|
196
|
+
params = body.get("params", {})
|
|
197
|
+
req_id = body.get("id", 1)
|
|
198
|
+
|
|
199
|
+
if method == "initialize":
|
|
200
|
+
resp = _sse({
|
|
201
|
+
"jsonrpc": "2.0",
|
|
202
|
+
"id": req_id,
|
|
203
|
+
"result": {
|
|
204
|
+
"protocolVersion": "2024-11-05",
|
|
205
|
+
"capabilities": {"tools": {}},
|
|
206
|
+
"serverInfo": {"name": "mcppt", "version": "2.1.0"},
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
resp.headers["mcp-session-id"] = _session_id
|
|
210
|
+
return resp
|
|
211
|
+
|
|
212
|
+
if method in ("notifications/initialized", "notifications/tools/list_changed"):
|
|
213
|
+
return _sse({"jsonrpc": "2.0", "id": req_id, "result": {}})
|
|
214
|
+
|
|
215
|
+
if method == "tools/list":
|
|
216
|
+
return _sse({"jsonrpc": "2.0", "id": req_id, "result": {"tools": MCPPT_TOOLS}})
|
|
217
|
+
|
|
218
|
+
if method == "tools/call":
|
|
219
|
+
name = params.get("name", "")
|
|
220
|
+
args = params.get("arguments", {})
|
|
221
|
+
if name == "scan_target":
|
|
222
|
+
return _handle_scan_target(args, req_id)
|
|
223
|
+
if name == "list_tools":
|
|
224
|
+
return _handle_list_tools(args, req_id)
|
|
225
|
+
if name == "call_tool":
|
|
226
|
+
return _handle_call_tool(args, req_id)
|
|
227
|
+
if name == "get_checks":
|
|
228
|
+
return _handle_get_checks(req_id)
|
|
229
|
+
return _sse({"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Unknown tool: {name}"}})
|
|
230
|
+
|
|
231
|
+
return _sse({"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Unknown method: {method}"}})
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ── launcher ──────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
def serve(port: int = 8899):
|
|
237
|
+
print("=" * 55)
|
|
238
|
+
print(" MCPPT MCP Server")
|
|
239
|
+
print(f" Endpoint: http://127.0.0.1:{port}/mcp")
|
|
240
|
+
print()
|
|
241
|
+
print(" Add to Claude Desktop config:")
|
|
242
|
+
print(' {')
|
|
243
|
+
print(' "mcpServers": {')
|
|
244
|
+
print(' "mcppt": {')
|
|
245
|
+
print(' "command": "mcppt",')
|
|
246
|
+
print(' "args": ["serve-mcp"]')
|
|
247
|
+
print(' }')
|
|
248
|
+
print(' }')
|
|
249
|
+
print(' }')
|
|
250
|
+
print()
|
|
251
|
+
print(" Or use with MCP Inspector:")
|
|
252
|
+
print(f" npx @modelcontextprotocol/inspector http://127.0.0.1:{port}/mcp")
|
|
253
|
+
print("=" * 55)
|
|
254
|
+
app.run(host="127.0.0.1", port=port, debug=False)
|