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,237 @@
1
+ """Tests for api_core API functions."""
2
+
3
+ # Import test framework from parent
4
+ from ..framework import (
5
+ test,
6
+ assert_has_keys,
7
+ assert_is_list,
8
+ assert_all_have_keys,
9
+ get_any_function,
10
+ get_data_address,
11
+ )
12
+
13
+ # Import functions under test
14
+ from ..api_core import (
15
+ lookup_funcs,
16
+ int_convert,
17
+ list_funcs,
18
+ list_globals,
19
+ imports,
20
+ find_regex,
21
+ )
22
+
23
+ # Import sync module for IDAError
24
+
25
+
26
+ # ============================================================================
27
+ # Tests for lookup_funcs
28
+ # ============================================================================
29
+
30
+
31
+ @test()
32
+ def test_lookup_funcs_by_address():
33
+ """lookup_funcs can find function by address"""
34
+ fn_addr = get_any_function()
35
+ if not fn_addr:
36
+ return # Skip if no functions
37
+
38
+ result = lookup_funcs(fn_addr)
39
+ assert_is_list(result, min_length=1)
40
+ assert result[0]["fn"] is not None
41
+ assert result[0]["error"] is None
42
+ assert_has_keys(result[0]["fn"], "addr", "name", "size")
43
+
44
+
45
+ @test()
46
+ def test_lookup_funcs_invalid():
47
+ """lookup_funcs returns error for invalid address"""
48
+ # Use an address that's unlikely to be a valid function
49
+ result = lookup_funcs("0xDEADBEEFDEADBEEF")
50
+ assert_is_list(result, min_length=1)
51
+ assert result[0]["fn"] is None
52
+ assert result[0]["error"] is not None
53
+
54
+
55
+ @test()
56
+ def test_lookup_funcs_wildcard():
57
+ """lookup_funcs with '*' returns all functions"""
58
+ result = lookup_funcs("*")
59
+ assert_is_list(result, min_length=1)
60
+ # All results should have query="*" and a function
61
+ for r in result:
62
+ assert r["query"] == "*"
63
+ assert r["fn"] is not None
64
+
65
+
66
+ @test()
67
+ def test_lookup_funcs_empty():
68
+ """lookup_funcs with empty string returns all functions"""
69
+ result = lookup_funcs("")
70
+ assert_is_list(result, min_length=1)
71
+ assert result[0]["query"] == "*"
72
+
73
+
74
+ @test()
75
+ def test_lookup_funcs_malformed_hex():
76
+ """lookup_funcs handles malformed hex address"""
77
+ # This looks like an address but isn't valid hex
78
+ result = lookup_funcs("0xZZZZ")
79
+ assert_is_list(result, min_length=1)
80
+ # Should return error since it's not a valid address or name
81
+ assert result[0]["error"] is not None
82
+
83
+
84
+ @test()
85
+ def test_lookup_funcs_data_address():
86
+ """lookup_funcs with valid address but not a function"""
87
+ data_addr = get_data_address()
88
+ if not data_addr:
89
+ return # Skip if no data segments
90
+
91
+ result = lookup_funcs(data_addr)
92
+ assert_is_list(result, min_length=1)
93
+ # Should return "Not a function" error
94
+ assert result[0]["fn"] is None
95
+ assert "Not a function" in str(result[0]["error"]) or "Not found" in str(
96
+ result[0]["error"]
97
+ )
98
+
99
+
100
+ # ============================================================================
101
+ # Tests for int_convert
102
+ # ============================================================================
103
+
104
+
105
+ @test()
106
+ def test_int_convert():
107
+ """int_convert properly converts numbers"""
108
+ result = int_convert({"text": "0x41"})
109
+ assert_is_list(result, min_length=1)
110
+ assert result[0]["error"] is None
111
+ assert result[0]["result"] is not None
112
+ conv = result[0]["result"]
113
+ assert_has_keys(conv, "decimal", "hexadecimal", "bytes", "binary")
114
+ assert conv["decimal"] == "65"
115
+ assert conv["hexadecimal"] == "0x41"
116
+ assert conv["ascii"] == "A"
117
+
118
+
119
+ @test()
120
+ def test_int_convert_invalid_text():
121
+ """int_convert handles invalid number text"""
122
+ result = int_convert({"text": "not_a_number"})
123
+ assert_is_list(result, min_length=1)
124
+ assert result[0]["result"] is None
125
+ assert result[0]["error"] is not None
126
+ assert "Invalid number" in result[0]["error"]
127
+
128
+
129
+ @test()
130
+ def test_int_convert_overflow():
131
+ """int_convert handles overflow with small size"""
132
+ # Try to fit a large number into 1 byte
133
+ result = int_convert({"text": "0xFFFF", "size": 1})
134
+ assert_is_list(result, min_length=1)
135
+ assert result[0]["result"] is None
136
+ assert result[0]["error"] is not None
137
+ assert "too big" in result[0]["error"]
138
+
139
+
140
+ @test()
141
+ def test_int_convert_non_ascii():
142
+ """int_convert handles non-ASCII bytes"""
143
+ # 0x01 is not a printable ASCII character (control char)
144
+ result = int_convert({"text": "0x01"})
145
+ assert_is_list(result, min_length=1)
146
+ assert result[0]["error"] is None
147
+ # ascii should be None for non-printable bytes
148
+ assert result[0]["result"]["ascii"] is None
149
+
150
+
151
+ # ============================================================================
152
+ # Tests for list_funcs
153
+ # ============================================================================
154
+
155
+
156
+ @test()
157
+ def test_list_funcs():
158
+ """list_funcs returns functions with proper structure"""
159
+ result = list_funcs({})
160
+ assert_is_list(result, min_length=1)
161
+ page = result[0]
162
+ assert_has_keys(page, "data", "offset", "count", "total")
163
+ if page["data"]:
164
+ assert_all_have_keys(page["data"], "addr", "name", "size")
165
+
166
+
167
+ @test()
168
+ def test_list_funcs_pagination():
169
+ """list_funcs respects pagination parameters"""
170
+ result = list_funcs({"offset": 0, "count": 5})
171
+ assert_is_list(result, min_length=1)
172
+ page = result[0]
173
+ assert page["offset"] == 0
174
+ assert len(page["data"]) <= 5
175
+
176
+
177
+ # ============================================================================
178
+ # Tests for list_globals
179
+ # ============================================================================
180
+
181
+
182
+ @test()
183
+ def test_list_globals():
184
+ """list_globals returns globals with proper structure"""
185
+ result = list_globals({})
186
+ assert_is_list(result, min_length=1)
187
+ page = result[0]
188
+ assert_has_keys(page, "data", "offset", "count", "total")
189
+
190
+
191
+ @test()
192
+ def test_list_globals_pagination():
193
+ """list_globals respects pagination parameters"""
194
+ result = list_globals({"offset": 0, "count": 5})
195
+ assert_is_list(result, min_length=1)
196
+ page = result[0]
197
+ assert page["offset"] == 0
198
+ assert len(page["data"]) <= 5
199
+
200
+
201
+ # ============================================================================
202
+ # Tests for imports
203
+ # ============================================================================
204
+
205
+
206
+ @test()
207
+ def test_imports():
208
+ """imports returns import list with proper structure"""
209
+ result = imports({})
210
+ assert_is_list(result, min_length=1)
211
+ page = result[0]
212
+ assert_has_keys(page, "data", "offset", "count", "total")
213
+
214
+
215
+ @test()
216
+ def test_imports_pagination():
217
+ """imports respects pagination parameters"""
218
+ result = imports({"offset": 0, "count": 5})
219
+ assert_is_list(result, min_length=1)
220
+ page = result[0]
221
+ assert page["offset"] == 0
222
+ assert len(page["data"]) <= 5
223
+
224
+
225
+ # ============================================================================
226
+ # Tests for find_regex
227
+ # ============================================================================
228
+
229
+
230
+ @test()
231
+ def test_find_regex():
232
+ """find_regex can search for patterns"""
233
+ # Search for a common pattern that should exist in most binaries
234
+ result = find_regex({"pattern": ".*"})
235
+ assert_is_list(result, min_length=1)
236
+ # Result structure should have matches
237
+ assert_has_keys(result[0], "query", "matches", "error")
@@ -0,0 +1,207 @@
1
+ """Tests for api_memory API functions."""
2
+
3
+ # Import test framework from parent
4
+ from ..framework import (
5
+ test,
6
+ assert_has_keys,
7
+ assert_is_list,
8
+ get_any_string,
9
+ get_first_segment,
10
+ get_data_address,
11
+ get_unmapped_address,
12
+ )
13
+
14
+ # Import functions under test
15
+ from ..api_memory import (
16
+ get_bytes,
17
+ get_int,
18
+ get_string,
19
+ get_global_value,
20
+ patch,
21
+ )
22
+
23
+ # Import sync module for IDAError
24
+
25
+
26
+ # ============================================================================
27
+ # Tests for get_bytes
28
+ # ============================================================================
29
+
30
+
31
+ @test()
32
+ def test_get_bytes():
33
+ """get_bytes reads bytes from a valid address"""
34
+ seg = get_first_segment()
35
+ if not seg:
36
+ return
37
+
38
+ start_addr, _ = seg
39
+ result = get_bytes({"addr": start_addr, "size": 16})
40
+ assert_is_list(result, min_length=1)
41
+ r = result[0]
42
+ assert_has_keys(r, "addr", "hex", "error")
43
+ if r["error"] is None:
44
+ assert r["hex"] is not None
45
+
46
+
47
+ @test()
48
+ def test_get_bytes_invalid():
49
+ """get_bytes handles invalid address"""
50
+ result = get_bytes({"addr": get_unmapped_address(), "size": 16})
51
+ assert_is_list(result, min_length=1)
52
+ r = result[0]
53
+ # Should have error or empty data
54
+ assert r.get("error") is not None or r.get("hex") == ""
55
+
56
+
57
+ # ============================================================================
58
+ # Tests for get_int
59
+ # ============================================================================
60
+
61
+
62
+ @test()
63
+ def test_get_int_u8():
64
+ """get_int reads 8-bit unsigned integer"""
65
+ seg = get_first_segment()
66
+ if not seg:
67
+ return
68
+
69
+ start_addr, _ = seg
70
+ result = get_int({"addr": start_addr, "size": 1})
71
+ assert_is_list(result, min_length=1)
72
+ r = result[0]
73
+ assert_has_keys(r, "addr", "value", "error")
74
+
75
+
76
+ @test()
77
+ def test_get_int_u16():
78
+ """get_int reads 16-bit unsigned integer"""
79
+ seg = get_first_segment()
80
+ if not seg:
81
+ return
82
+
83
+ start_addr, _ = seg
84
+ result = get_int({"addr": start_addr, "size": 2})
85
+ assert_is_list(result, min_length=1)
86
+ r = result[0]
87
+ assert_has_keys(r, "addr", "value", "error")
88
+
89
+
90
+ @test()
91
+ def test_get_int_u32():
92
+ """get_int reads 32-bit unsigned integer"""
93
+ seg = get_first_segment()
94
+ if not seg:
95
+ return
96
+
97
+ start_addr, _ = seg
98
+ result = get_int({"addr": start_addr, "size": 4})
99
+ assert_is_list(result, min_length=1)
100
+ r = result[0]
101
+ assert_has_keys(r, "addr", "value", "error")
102
+
103
+
104
+ @test()
105
+ def test_get_int_u64():
106
+ """get_int reads 64-bit unsigned integer"""
107
+ seg = get_first_segment()
108
+ if not seg:
109
+ return
110
+
111
+ start_addr, _ = seg
112
+ result = get_int({"addr": start_addr, "size": 8})
113
+ assert_is_list(result, min_length=1)
114
+ r = result[0]
115
+ assert_has_keys(r, "addr", "value", "error")
116
+
117
+
118
+ # ============================================================================
119
+ # Tests for get_string
120
+ # ============================================================================
121
+
122
+
123
+ @test()
124
+ def test_get_string():
125
+ """get_string reads string from a valid address"""
126
+ str_addr = get_any_string()
127
+ if not str_addr:
128
+ return
129
+
130
+ result = get_string(str_addr)
131
+ assert_is_list(result, min_length=1)
132
+ r = result[0]
133
+ assert_has_keys(r, "addr", "value", "error")
134
+
135
+
136
+ # ============================================================================
137
+ # Tests for get_global_value
138
+ # ============================================================================
139
+
140
+
141
+ @test()
142
+ def test_get_global_value():
143
+ """get_global_value retrieves global variable value"""
144
+ # Try to get value at a data address
145
+ data_addr = get_data_address()
146
+ if not data_addr:
147
+ seg = get_first_segment()
148
+ if not seg:
149
+ return
150
+ data_addr = seg[0]
151
+
152
+ result = get_global_value(data_addr)
153
+ assert_is_list(result, min_length=1)
154
+ r = result[0]
155
+ assert_has_keys(r, "addr", "error")
156
+
157
+
158
+ # ============================================================================
159
+ # Tests for patch
160
+ # ============================================================================
161
+
162
+
163
+ @test(skip=True) # Skip by default as it modifies the database
164
+ def test_patch():
165
+ """patch writes bytes to address"""
166
+ seg = get_first_segment()
167
+ if not seg:
168
+ return
169
+
170
+ start_addr, _ = seg
171
+ # Read original bytes first
172
+ original = get_bytes({"addr": start_addr, "size": 4})
173
+
174
+ try:
175
+ result = patch({"addr": start_addr, "hex": "90909090"})
176
+ assert_is_list(result, min_length=1)
177
+ r = result[0]
178
+ assert_has_keys(r, "addr", "error")
179
+ finally:
180
+ # Restore original bytes
181
+ if original and original[0].get("hex"):
182
+ patch({"addr": start_addr, "hex": original[0]["hex"]})
183
+
184
+
185
+ @test()
186
+ def test_patch_invalid_address():
187
+ """patch handles invalid address"""
188
+ result = patch({"addr": get_unmapped_address(), "hex": "90"})
189
+ assert_is_list(result, min_length=1)
190
+ r = result[0]
191
+ # Should have error
192
+ assert r.get("error") is not None
193
+
194
+
195
+ @test()
196
+ def test_patch_invalid_hex_data():
197
+ """patch handles invalid hex data"""
198
+ seg = get_first_segment()
199
+ if not seg:
200
+ return
201
+
202
+ start_addr, _ = seg
203
+ result = patch({"addr": start_addr, "hex": "ZZZZ"})
204
+ assert_is_list(result, min_length=1)
205
+ r = result[0]
206
+ # Should have error for invalid hex
207
+ assert r.get("error") is not None
@@ -0,0 +1,123 @@
1
+ """Tests for api_modify API functions."""
2
+
3
+ # Import test framework from parent
4
+ from ..framework import (
5
+ test,
6
+ assert_has_keys,
7
+ assert_is_list,
8
+ get_any_function,
9
+ get_data_address,
10
+ )
11
+
12
+ # Import functions under test
13
+ from ..api_modify import (
14
+ set_comments,
15
+ patch_asm,
16
+ rename,
17
+ )
18
+
19
+ # Import sync module for IDAError
20
+
21
+
22
+ # ============================================================================
23
+ # Tests for set_comments
24
+ # ============================================================================
25
+
26
+
27
+ @test()
28
+ def test_set_comment_roundtrip():
29
+ """set_comments can add and remove comments"""
30
+ fn_addr = get_any_function()
31
+ if not fn_addr:
32
+ return
33
+
34
+ # Add a comment
35
+ result = set_comments({"addr": fn_addr, "comment": "__TEST_COMMENT__"})
36
+ assert_is_list(result, min_length=1)
37
+
38
+ # Clear the comment
39
+ result = set_comments({"addr": fn_addr, "comment": ""})
40
+ assert_is_list(result, min_length=1)
41
+
42
+
43
+ # ============================================================================
44
+ # Tests for patch_asm
45
+ # ============================================================================
46
+
47
+
48
+ @test(skip=True) # Skip by default as it modifies the database
49
+ def test_patch_asm():
50
+ """patch_asm can patch assembly"""
51
+ fn_addr = get_any_function()
52
+ if not fn_addr:
53
+ return
54
+
55
+ # This is a risky test - patching assembly could corrupt the binary
56
+ result = patch_asm({"addr": fn_addr, "asm": "nop"})
57
+ assert_is_list(result, min_length=1)
58
+ r = result[0]
59
+ assert_has_keys(r, "addr", "error")
60
+
61
+
62
+ # ============================================================================
63
+ # Tests for rename
64
+ # ============================================================================
65
+
66
+
67
+ @test()
68
+ def test_rename_function_roundtrip():
69
+ """rename function works and can be undone"""
70
+ fn_addr = get_any_function()
71
+ if not fn_addr:
72
+ return
73
+
74
+ # Import to get original name
75
+ from ..api_core import lookup_funcs
76
+
77
+ # Get original name
78
+ lookup_result = lookup_funcs(fn_addr)
79
+ if not lookup_result or not lookup_result[0].get("fn"):
80
+ return
81
+
82
+ original_name = lookup_result[0]["fn"]["name"]
83
+
84
+ try:
85
+ # Rename
86
+ result = rename({"func": [{"addr": fn_addr, "name": "__test_rename__"}]})
87
+ assert isinstance(result, dict)
88
+
89
+ # Verify rename worked
90
+ lookup_result = lookup_funcs(fn_addr)
91
+ new_name = lookup_result[0]["fn"]["name"]
92
+ assert new_name == "__test_rename__"
93
+ finally:
94
+ # Restore
95
+ rename({"func": [{"addr": fn_addr, "name": original_name}]})
96
+
97
+
98
+ @test()
99
+ def test_rename_global_roundtrip():
100
+ """rename global variable works"""
101
+ data_addr = get_data_address()
102
+ if not data_addr:
103
+ return
104
+
105
+ try:
106
+ result = rename({"global": [{"addr": data_addr, "name": "__test_global__"}]})
107
+ assert isinstance(result, dict)
108
+ except Exception:
109
+ pass # May fail if no suitable global exists
110
+
111
+
112
+ @test(skip=True) # Local variable renaming requires decompilation
113
+ def test_rename_local_roundtrip():
114
+ """rename local variable works"""
115
+ fn_addr = get_any_function()
116
+ if not fn_addr:
117
+ return
118
+
119
+ # This requires the function to be decompilable and have local variables
120
+ result = rename(
121
+ {"local": [{"func": fn_addr, "name": "old_var", "new_name": "__test_local__"}]}
122
+ )
123
+ assert isinstance(result, dict)