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/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)