ida-pro-mcp-xjoker 1.0.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.
Files changed (45) hide show
  1. ida_pro_mcp/__init__.py +0 -0
  2. ida_pro_mcp/__main__.py +6 -0
  3. ida_pro_mcp/ida_mcp/__init__.py +68 -0
  4. ida_pro_mcp/ida_mcp/api_analysis.py +1296 -0
  5. ida_pro_mcp/ida_mcp/api_core.py +337 -0
  6. ida_pro_mcp/ida_mcp/api_debug.py +617 -0
  7. ida_pro_mcp/ida_mcp/api_memory.py +304 -0
  8. ida_pro_mcp/ida_mcp/api_modify.py +406 -0
  9. ida_pro_mcp/ida_mcp/api_python.py +179 -0
  10. ida_pro_mcp/ida_mcp/api_resources.py +295 -0
  11. ida_pro_mcp/ida_mcp/api_stack.py +167 -0
  12. ida_pro_mcp/ida_mcp/api_types.py +480 -0
  13. ida_pro_mcp/ida_mcp/auth.py +166 -0
  14. ida_pro_mcp/ida_mcp/cache.py +232 -0
  15. ida_pro_mcp/ida_mcp/config.py +228 -0
  16. ida_pro_mcp/ida_mcp/framework.py +547 -0
  17. ida_pro_mcp/ida_mcp/http.py +859 -0
  18. ida_pro_mcp/ida_mcp/port_utils.py +104 -0
  19. ida_pro_mcp/ida_mcp/rpc.py +187 -0
  20. ida_pro_mcp/ida_mcp/server_manager.py +339 -0
  21. ida_pro_mcp/ida_mcp/sync.py +233 -0
  22. ida_pro_mcp/ida_mcp/tests/__init__.py +14 -0
  23. ida_pro_mcp/ida_mcp/tests/test_api_analysis.py +336 -0
  24. ida_pro_mcp/ida_mcp/tests/test_api_core.py +237 -0
  25. ida_pro_mcp/ida_mcp/tests/test_api_memory.py +207 -0
  26. ida_pro_mcp/ida_mcp/tests/test_api_modify.py +123 -0
  27. ida_pro_mcp/ida_mcp/tests/test_api_resources.py +199 -0
  28. ida_pro_mcp/ida_mcp/tests/test_api_stack.py +77 -0
  29. ida_pro_mcp/ida_mcp/tests/test_api_types.py +249 -0
  30. ida_pro_mcp/ida_mcp/ui.py +357 -0
  31. ida_pro_mcp/ida_mcp/utils.py +1186 -0
  32. ida_pro_mcp/ida_mcp/zeromcp/__init__.py +5 -0
  33. ida_pro_mcp/ida_mcp/zeromcp/jsonrpc.py +384 -0
  34. ida_pro_mcp/ida_mcp/zeromcp/mcp.py +883 -0
  35. ida_pro_mcp/ida_mcp.py +186 -0
  36. ida_pro_mcp/idalib_server.py +354 -0
  37. ida_pro_mcp/idalib_session_manager.py +259 -0
  38. ida_pro_mcp/server.py +1060 -0
  39. ida_pro_mcp/test.py +170 -0
  40. ida_pro_mcp_xjoker-1.0.1.dist-info/METADATA +405 -0
  41. ida_pro_mcp_xjoker-1.0.1.dist-info/RECORD +45 -0
  42. ida_pro_mcp_xjoker-1.0.1.dist-info/WHEEL +5 -0
  43. ida_pro_mcp_xjoker-1.0.1.dist-info/entry_points.txt +4 -0
  44. ida_pro_mcp_xjoker-1.0.1.dist-info/licenses/LICENSE +21 -0
  45. ida_pro_mcp_xjoker-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ # NOTE: Vendored from zeromcp 1.3.0
2
+
3
+ from .mcp import McpRpcRegistry, McpToolError, McpServer, McpHttpRequestHandler
4
+
5
+ __all__ = ["McpRpcRegistry", "McpToolError", "McpServer", "McpHttpRequestHandler"]
@@ -0,0 +1,384 @@
1
+ import json
2
+ import inspect
3
+ import os
4
+ import threading
5
+ import time
6
+ import traceback
7
+ from typing import Any, Callable, get_type_hints, get_origin, get_args, Union, TypedDict, TypeAlias, NotRequired, is_typeddict
8
+ from types import UnionType
9
+
10
+ JsonRpcId: TypeAlias = str | int | float | None
11
+
12
+ # Thread-local storage for current request context (ID + cancel event)
13
+ _current_request = threading.local()
14
+
15
+ # Global pending requests for cancellation
16
+ _pending_requests_lock = threading.Lock()
17
+ _pending_requests: dict[int | str, threading.Event] = {}
18
+
19
+
20
+ def get_current_request_id() -> JsonRpcId:
21
+ """Get the JSON-RPC request ID of the currently executing request."""
22
+ return getattr(_current_request, "id", None)
23
+
24
+
25
+ def get_current_cancel_event() -> threading.Event | None:
26
+ """Get the cancel event for the currently executing request."""
27
+ return getattr(_current_request, "cancel_event", None)
28
+
29
+
30
+ def register_pending_request(request_id: int | str) -> threading.Event:
31
+ """Register a request as pending and return its cancel event."""
32
+ event = threading.Event()
33
+ with _pending_requests_lock:
34
+ _pending_requests[request_id] = event
35
+ _current_request.cancel_event = event
36
+ return event
37
+
38
+
39
+ def unregister_pending_request(request_id: int | str) -> None:
40
+ """Unregister a pending request."""
41
+ with _pending_requests_lock:
42
+ _pending_requests.pop(request_id, None)
43
+ _current_request.cancel_event = None
44
+
45
+
46
+ def cancel_request(request_id: int | str) -> bool:
47
+ """Signal cancellation for a pending request. Returns True if request was found."""
48
+ with _pending_requests_lock:
49
+ event = _pending_requests.get(request_id)
50
+ if event:
51
+ event.set()
52
+ return True
53
+ return False
54
+
55
+
56
+ def _parse_bool_env(name: str, default: bool) -> bool:
57
+ value = os.getenv(name)
58
+ if value is None:
59
+ return default
60
+ value = value.strip().lower()
61
+ if value in ("1", "true", "yes", "on"):
62
+ return True
63
+ if value in ("0", "false", "no", "off"):
64
+ return False
65
+ return default
66
+
67
+
68
+ _LOG_REQUESTS = _parse_bool_env("IDA_MCP_LOG_REQUESTS", True)
69
+ _LOG_SKIP_METHODS = {
70
+ m.strip()
71
+ for m in os.getenv("IDA_MCP_LOG_SKIP_METHODS", "tools/call").split(",")
72
+ if m.strip()
73
+ }
74
+ JsonRpcParams: TypeAlias = dict[str, Any] | list[Any] | None
75
+
76
+ class JsonRpcRequest(TypedDict):
77
+ jsonrpc: str
78
+ method: str
79
+ params: NotRequired[JsonRpcParams]
80
+ id: NotRequired[JsonRpcId]
81
+
82
+ class JsonRpcError(TypedDict):
83
+ code: int
84
+ message: str
85
+ data: NotRequired[Any]
86
+
87
+ class JsonRpcResponse(TypedDict):
88
+ jsonrpc: str
89
+ result: NotRequired[Any]
90
+ error: NotRequired[JsonRpcError]
91
+ id: JsonRpcId
92
+
93
+ class JsonRpcException(Exception):
94
+ def __init__(self, code: int, message: str, data: Any = None):
95
+ self.code = code
96
+ self.message = message
97
+ self.data = data
98
+
99
+
100
+ class RequestCancelledError(Exception):
101
+ """Base class for request cancellation errors (LSP error code -32800)."""
102
+ pass
103
+
104
+ class JsonRpcRegistry:
105
+ def __init__(self):
106
+ self.methods: dict[str, Callable] = {}
107
+ self._cache: dict[Callable, tuple[inspect.Signature, dict, list[str]]] = {}
108
+ self.redact_exceptions = False
109
+
110
+ def method(self, func: Callable, name: str | None = None) -> Callable:
111
+ self.methods[name or func.__name__] = func # type: ignore
112
+ return func
113
+
114
+ def dispatch(self, request: dict | str | bytes | bytearray) -> JsonRpcResponse | None:
115
+ try:
116
+ if not isinstance(request, dict):
117
+ request = json.loads(request)
118
+ if not isinstance(request, dict):
119
+ return self._error(None, -32600, "Invalid request: must be a JSON object")
120
+ except Exception as e:
121
+ return self._error(None, -32700, "JSON parse error", str(e))
122
+
123
+ if request.get("jsonrpc") != "2.0":
124
+ return self._error(None, -32600, "Invalid request: 'jsonrpc' must be '2.0'")
125
+
126
+ method = request.get("method")
127
+ if method is None:
128
+ return self._error(None, -32600, "Invalid request: 'method' is required")
129
+ if not isinstance(method, str):
130
+ return self._error(None, -32600, "Invalid request: 'method' must be a string")
131
+
132
+ request_id: JsonRpcId = request.get("id")
133
+ is_notification = "id" not in request
134
+ params: JsonRpcParams = request.get("params")
135
+
136
+ log_method = _LOG_REQUESTS and method not in _LOG_SKIP_METHODS
137
+ if log_method:
138
+ params_str = json.dumps(params, default=str)
139
+ if len(params_str) > 200:
140
+ params_str = params_str[:200] + "..."
141
+ print(f"[MCP] >> {method}({params_str})")
142
+
143
+ # Set current request ID in thread-local for cancellation tracking
144
+ _current_request.id = request_id
145
+ start_time = time.perf_counter()
146
+ try:
147
+ result = self._call(method, params)
148
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
149
+ if log_method:
150
+ result_str = json.dumps(result, default=str)
151
+ if len(result_str) > 200:
152
+ result_str = result_str[:200] + "..."
153
+ print(f"[MCP] << {method} ({elapsed_ms:.1f}ms) {result_str}")
154
+ if is_notification:
155
+ return None
156
+ return {
157
+ "jsonrpc": "2.0",
158
+ "result": result,
159
+ "id": request_id,
160
+ }
161
+ except JsonRpcException as e:
162
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
163
+ if log_method:
164
+ print(f"[MCP] << {method} ({elapsed_ms:.1f}ms) ERROR: {e.message}")
165
+ if is_notification:
166
+ return None
167
+ return self._error(request_id, e.code, e.message, e.data)
168
+ except RequestCancelledError as e:
169
+ # LSP error code -32800: Request cancelled
170
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
171
+ if log_method:
172
+ print(f"[MCP] << {method} ({elapsed_ms:.1f}ms) CANCELLED")
173
+ if is_notification:
174
+ return None
175
+ return self._error(request_id, -32800, str(e) or "Request cancelled")
176
+ except Exception as e:
177
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
178
+ if log_method:
179
+ print(f"[MCP] << {method} ({elapsed_ms:.1f}ms) EXCEPTION: {e}")
180
+ if is_notification:
181
+ return None
182
+ error = self.map_exception(e)
183
+ return self._error(request_id, error["code"], error["message"], error.get("data"))
184
+ finally:
185
+ _current_request.id = None
186
+
187
+ def map_exception(self, e: Exception) -> JsonRpcError:
188
+ if self.redact_exceptions:
189
+ return {
190
+ "code": -32603,
191
+ "message": f"Internal Error: {str(e)}",
192
+ }
193
+ return {
194
+ "code": -32603,
195
+ "message": "\n".join(traceback.format_exception(e)).strip() + "\n\nPlease report a bug!",
196
+ }
197
+
198
+ def _call(self, method: str, params: Any) -> Any:
199
+ if method not in self.methods:
200
+ raise JsonRpcException(-32601, f"Method '{method}' not found")
201
+
202
+ func = self.methods[method]
203
+
204
+ # Check for cached reflection data
205
+ if func not in self._cache:
206
+ sig = inspect.signature(func)
207
+ hints = get_type_hints(func)
208
+ hints.pop("return", None)
209
+
210
+ # Determine required vs optional parameters
211
+ required_params = []
212
+ for param_name, param in sig.parameters.items():
213
+ if param.default is inspect.Parameter.empty:
214
+ required_params.append(param_name)
215
+
216
+ self._cache[func] = (sig, hints, required_params)
217
+
218
+ sig, hints, required_params = self._cache[func]
219
+
220
+ # Handle None params
221
+ if params is None:
222
+ if len(required_params) == 0:
223
+ return func()
224
+ else:
225
+ raise JsonRpcException(-32602, "Missing required params")
226
+
227
+ # Convert list params to dict by parameter names
228
+ if isinstance(params, list):
229
+ if len(params) < len(required_params):
230
+ raise JsonRpcException(
231
+ -32602,
232
+ f"Invalid params: expected at least {len(required_params)} arguments, got {len(params)}"
233
+ )
234
+ if len(params) > len(sig.parameters):
235
+ raise JsonRpcException(
236
+ -32602,
237
+ f"Invalid params: expected at most {len(sig.parameters)} arguments, got {len(params)}"
238
+ )
239
+ params = dict(zip(sig.parameters.keys(), params))
240
+
241
+ # Validate dict params
242
+ if isinstance(params, dict):
243
+ # Check all required params are present
244
+ missing = set(required_params) - set(params.keys())
245
+ if missing:
246
+ raise JsonRpcException(
247
+ -32602,
248
+ f"Invalid params: missing required parameters: {list(missing)}"
249
+ )
250
+
251
+ # Check no extra params
252
+ extra = set(params.keys()) - set(sig.parameters.keys())
253
+ if extra:
254
+ raise JsonRpcException(
255
+ -32602,
256
+ f"Invalid params: unexpected parameters: {list(extra)}"
257
+ )
258
+
259
+ validated_params = {}
260
+ for param_name, value in params.items():
261
+ # If no type hint, pass through without validation
262
+ if param_name not in hints:
263
+ validated_params[param_name] = value
264
+ continue
265
+
266
+ # Has type hint, validate
267
+ expected_type = hints[param_name]
268
+
269
+ # Inline type validation
270
+ origin = get_origin(expected_type)
271
+ args = get_args(expected_type)
272
+
273
+ # Handle None/null
274
+ if value is None:
275
+ if expected_type is not type(None):
276
+ # Check if None is allowed in a Union
277
+ if not (origin in (Union, UnionType) and type(None) in args):
278
+ raise JsonRpcException(-32602, f"Invalid params: {param_name} cannot be null")
279
+ validated_params[param_name] = None
280
+ continue
281
+
282
+ # Handle Union types (int | str, Optional[int], etc.)
283
+ if origin in (Union, UnionType):
284
+ type_matched = False
285
+
286
+ # HACK: Try to parse str as JSON for non-str unions
287
+ #
288
+ # When JSON schema says one field is "object", Claude Code
289
+ # (and maybe other MCP clients) can't (or won't) detect
290
+ # that the field is actually a dict/list. Instead, they
291
+ # treat the field as a string containing JSON object.
292
+ #
293
+ # To work around this, if the expected type is a Union
294
+ # that does not include str, and the provided value is
295
+ # a str, we try to parse it as JSON first.
296
+ if type(str) not in args and isinstance(value, str):
297
+ try:
298
+ value = json.loads(value)
299
+ except json.JSONDecodeError:
300
+ pass
301
+
302
+ for arg_type in args:
303
+ if arg_type is type(None):
304
+ continue
305
+
306
+ arg_origin = get_origin(arg_type)
307
+ check_type = arg_origin if arg_origin is not None else arg_type
308
+
309
+ # TypedDict cannot be used with isinstance - check for dict instead
310
+ if is_typeddict(arg_type):
311
+ check_type = dict
312
+
313
+ if isinstance(value, check_type):
314
+ type_matched = True
315
+ break
316
+
317
+ if not type_matched:
318
+ raise JsonRpcException(-32602, "Invalid params: expected {} for {}, got {}".format(
319
+ " | ".join(
320
+ t.__name__ if isinstance(t, type) else str(t)
321
+ for t in args
322
+ ),
323
+ param_name,
324
+ type(value).__name__
325
+ ))
326
+ validated_params[param_name] = value
327
+ continue
328
+
329
+ # Handle generic types (list[X], dict[K,V])
330
+ if origin is not None:
331
+ if not isinstance(value, origin):
332
+ raise JsonRpcException(
333
+ -32602,
334
+ f"Invalid params: {param_name} expected {origin.__name__}, got {type(value).__name__}"
335
+ )
336
+ validated_params[param_name] = value
337
+ continue
338
+
339
+ # Handle TypedDict (must check before basic types)
340
+ if is_typeddict(expected_type):
341
+ if not isinstance(value, dict):
342
+ raise JsonRpcException(
343
+ -32602,
344
+ f"Invalid params: {param_name} expected dict, got {type(value).__name__}"
345
+ )
346
+ validated_params[param_name] = value
347
+ continue
348
+
349
+ # Handle Any
350
+ if expected_type is Any:
351
+ validated_params[param_name] = value
352
+ continue
353
+
354
+ # Handle basic types
355
+ if isinstance(expected_type, type):
356
+ # Allow int -> float conversion
357
+ if expected_type is float and isinstance(value, int):
358
+ validated_params[param_name] = float(value)
359
+ continue
360
+ if not isinstance(value, expected_type):
361
+ raise JsonRpcException(
362
+ -32602,
363
+ f"Invalid params: {param_name} expected {expected_type.__name__}, got {type(value).__name__}"
364
+ )
365
+ validated_params[param_name] = value
366
+ continue
367
+
368
+ return func(**validated_params)
369
+
370
+ else:
371
+ raise JsonRpcException(-32602, "Invalid params: must be array or object")
372
+
373
+ def _error(self, request_id: JsonRpcId, code: int, message: str, data: Any = None) -> JsonRpcResponse | None:
374
+ error: JsonRpcError = {
375
+ "code": code,
376
+ "message": message,
377
+ }
378
+ if data is not None:
379
+ error["data"] = data
380
+ return {
381
+ "jsonrpc": "2.0",
382
+ "error": error,
383
+ "id": request_id,
384
+ }