├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── bridge_mcp_ghidra.py
├── images
├── enable_plugin.png
└── new_plugins_found.png
├── lib
└── .gitignore
├── pom.xml
├── requirements.txt
└── src
├── assembly
└── ghidra-extension.xml
├── main
├── java
│ └── com
│ │ └── lauriewired
│ │ └── GhidraMCPPlugin.java
└── resources
│ ├── META-INF
│ └── MANIFEST.MF
│ ├── Module.manifest
│ └── extension.properties
└── test
└── java
└── com
└── lauriewired
└── AppTest.java
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build with Maven
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | env:
13 | GHIDRA_VERSION: 11.3.1
14 | GHIDRA_DATE: 20250219
15 | GHIDRA_LIBS: >-
16 | Features/Base/lib/Base.jar
17 | Features/Decompiler/lib/Decompiler.jar
18 | Framework/Docking/lib/Docking.jar
19 | Framework/Generic/lib/Generic.jar
20 | Framework/Project/lib/Project.jar
21 | Framework/SoftwareModeling/lib/SoftwareModeling.jar
22 | Framework/Utility/lib/Utility.jar
23 | Framework/Gui/lib/Gui.jar
24 |
25 | steps:
26 | - uses: actions/checkout@v4
27 | - name: Set up JDK 21
28 | uses: actions/setup-java@v4
29 | with:
30 | java-version: '21'
31 | distribution: 'temurin'
32 | cache: maven
33 |
34 | - name: Download Ghidra
35 | run: |
36 | wget --no-verbose -O ghidra.zip https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${{ env.GHIDRA_VERSION }}_build/ghidra_${{ env.GHIDRA_VERSION }}_PUBLIC_${{ env.GHIDRA_DATE }}.zip
37 | 7z x -bd ghidra.zip
38 |
39 | - name: Copy Ghidra libs
40 | run: |
41 | mkdir -p ./lib
42 | for libfile in ${{ env.GHIDRA_LIBS }}
43 | do echo "Copying ${libfile} to lib/"
44 | cp ghidra_${{ env.GHIDRA_VERSION }}_PUBLIC/Ghidra/${libfile} ./lib/
45 | done
46 |
47 | - name: Build with Maven
48 | run: mvn clean package assembly:single
49 |
50 | - name: Assemble release directory
51 | run: |
52 | mkdir release
53 | cp target/GhidraMCP-*-SNAPSHOT.zip release/
54 | cp bridge_mcp_ghidra.py release/
55 |
56 | - name: Upload artifact
57 | uses: actions/upload-artifact@v4
58 | with:
59 | name: GhidraMCP-artifact
60 | path: |
61 | release/*
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Maven target directory
2 | /target/
3 |
4 | # Compiled class files
5 | *.class
6 |
7 | # Logs
8 | *.log
9 |
10 | # IDE files
11 | # IntelliJ
12 | .idea/
13 | *.iml
14 | *.iws
15 | out/
16 |
17 | # Eclipse
18 | .project
19 | .classpath
20 | .settings/
21 | bin/
22 |
23 | # VS Code
24 | .vscode/
25 |
26 | # macOS
27 | .DS_Store
28 | .AppleDouble
29 | .LSOverride
30 |
31 | # Thumbnails
32 | ._*
33 |
34 | # macOS metadata files
35 | .Spotlight-V100
36 | .Trashes
37 |
38 | # Maven Wrapper
39 | .mvn/
40 | !/.mvn/wrapper/maven-wrapper.jar
41 | mvnw
42 | mvnw.cmd
43 |
44 | # Environment files
45 | .env
46 | .env.*
47 |
48 | # Java crash logs (if any)
49 | hs_err_pid*
50 | replay_pid*
51 |
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.apache.org/licenses/LICENSE-2.0)
2 | [](https://github.com/LaurieWired/GhidraMCP/releases)
3 | [](https://github.com/LaurieWired/GhidraMCP/stargazers)
4 | [](https://github.com/LaurieWired/GhidraMCP/network/members)
5 | [](https://github.com/LaurieWired/GhidraMCP/graphs/contributors)
6 | [](https://twitter.com/lauriewired)
7 |
8 | 
9 |
10 |
11 | # ghidraMCP
12 | ghidraMCP is an Model Context Protocol server for allowing LLMs to autonomously reverse engineer applications. It exposes numerous tools from core Ghidra functionality to MCP clients.
13 |
14 | https://github.com/user-attachments/assets/36080514-f227-44bd-af84-78e29ee1d7f9
15 |
16 |
17 | # Features
18 | MCP Server + Ghidra Plugin
19 |
20 | - Decompile and analyze binaries in Ghidra
21 | - Automatically rename methods and data
22 | - List methods, classes, imports, and exports
23 |
24 | # Installation
25 |
26 | ## Prerequisites
27 | - Install [Ghidra](https://ghidra-sre.org)
28 | - Python3
29 | - MCP [SDK](https://github.com/modelcontextprotocol/python-sdk)
30 |
31 | ## Ghidra
32 | First, download the latest [release](https://github.com/LaurieWired/GhidraMCP/releases) from this repository. This contains the Ghidra plugin and Python MCP client. Then, you can directly import the plugin into Ghidra.
33 |
34 | 1. Run Ghidra
35 | 2. Select `File` -> `Install Extensions`
36 | 3. Click the `+` button
37 | 4. Select the `GhidraMCP-1-2.zip` (or your chosen version) from the downloaded release
38 | 5. Restart Ghidra
39 | 6. Make sure the GhidraMCPPlugin is enabled in `File` -> `Configure` -> `Developer`
40 | 7. *Optional*: Configure the port in Ghidra with `Edit` -> `Tool Options` -> `GhidraMCP HTTP Server`
41 |
42 | Video Installation Guide:
43 |
44 |
45 | https://github.com/user-attachments/assets/75f0c176-6da1-48dc-ad96-c182eb4648c3
46 |
47 |
48 |
49 | ## MCP Clients
50 |
51 | Theoretically, any MCP client should work with ghidraMCP. Three examples are given below.
52 |
53 | ## Example 1: Claude Desktop
54 | To set up Claude Desktop as a Ghidra MCP client, go to `Claude` -> `Settings` -> `Developer` -> `Edit Config` -> `claude_desktop_config.json` and add the following:
55 |
56 | ```json
57 | {
58 | "mcpServers": {
59 | "ghidra": {
60 | "command": "python",
61 | "args": [
62 | "/ABSOLUTE_PATH_TO/bridge_mcp_ghidra.py",
63 | "--ghidra-server",
64 | "http://127.0.0.1:8080/"
65 | ]
66 | }
67 | }
68 | }
69 | ```
70 |
71 | Alternatively, edit this file directly:
72 | ```
73 | /Users/YOUR_USER/Library/Application Support/Claude/claude_desktop_config.json
74 | ```
75 |
76 | The server IP and port are configurable and should be set to point to the target Ghidra instance. If not set, both will default to localhost:8080.
77 |
78 | ## Example 2: Cline
79 | To use GhidraMCP with [Cline](https://cline.bot), this requires manually running the MCP server as well. First run the following command:
80 |
81 | ```
82 | python bridge_mcp_ghidra.py --transport sse --mcp-host 127.0.0.1 --mcp-port 8081 --ghidra-server http://127.0.0.1:8080/
83 | ```
84 |
85 | The only *required* argument is the transport. If all other arguments are unspecified, they will default to the above. Once the MCP server is running, open up Cline and select `MCP Servers` at the top.
86 |
87 | 
88 |
89 | Then select `Remote Servers` and add the following, ensuring that the url matches the MCP host and port:
90 |
91 | 1. Server Name: GhidraMCP
92 | 2. Server URL: `http://127.0.0.1:8081/sse`
93 |
94 | ## Example 3: 5ire
95 | Another MCP client that supports multiple models on the backend is [5ire](https://github.com/nanbingxyz/5ire). To set up GhidraMCP, open 5ire and go to `Tools` -> `New` and set the following configurations:
96 |
97 | 1. Tool Key: ghidra
98 | 2. Name: GhidraMCP
99 | 3. Command: `python /ABSOLUTE_PATH_TO/bridge_mcp_ghidra.py`
100 |
101 | # Building from Source
102 | 1. Copy the following files from your Ghidra directory to this project's `lib/` directory:
103 | - `Ghidra/Features/Base/lib/Base.jar`
104 | - `Ghidra/Features/Decompiler/lib/Decompiler.jar`
105 | - `Ghidra/Framework/Docking/lib/Docking.jar`
106 | - `Ghidra/Framework/Generic/lib/Generic.jar`
107 | - `Ghidra/Framework/Project/lib/Project.jar`
108 | - `Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar`
109 | - `Ghidra/Framework/Utility/lib/Utility.jar`
110 | - `Ghidra/Framework/Gui/lib/Gui.jar`
111 | 2. Build with Maven by running:
112 |
113 | `mvn clean package assembly:single`
114 |
115 | The generated zip file includes the built Ghidra plugin and its resources. These files are required for Ghidra to recognize the new extension.
116 |
117 | - lib/GhidraMCP.jar
118 | - extensions.properties
119 | - Module.manifest
120 |
--------------------------------------------------------------------------------
/bridge_mcp_ghidra.py:
--------------------------------------------------------------------------------
1 | # /// script
2 | # requires-python = ">=3.10"
3 | # dependencies = [
4 | # "requests>=2,<3",
5 | # "mcp>=1.2.0,<2",
6 | # ]
7 | # ///
8 |
9 | import sys
10 | import requests
11 | import argparse
12 | import logging
13 | from urllib.parse import urljoin
14 |
15 | from mcp.server.fastmcp import FastMCP
16 |
17 | DEFAULT_GHIDRA_SERVER = "http://127.0.0.1:8080/"
18 |
19 | logger = logging.getLogger(__name__)
20 |
21 | mcp = FastMCP("ghidra-mcp")
22 |
23 | # Initialize ghidra_server_url with default value
24 | ghidra_server_url = DEFAULT_GHIDRA_SERVER
25 |
26 | def safe_get(endpoint: str, params: dict = None) -> list:
27 | """
28 | Perform a GET request with optional query parameters.
29 | """
30 | if params is None:
31 | params = {}
32 |
33 | url = urljoin(ghidra_server_url, endpoint)
34 |
35 | try:
36 | response = requests.get(url, params=params, timeout=5)
37 | response.encoding = 'utf-8'
38 | if response.ok:
39 | return response.text.splitlines()
40 | else:
41 | return [f"Error {response.status_code}: {response.text.strip()}"]
42 | except Exception as e:
43 | return [f"Request failed: {str(e)}"]
44 |
45 | def safe_post(endpoint: str, data: dict | str) -> str:
46 | try:
47 | url = urljoin(ghidra_server_url, endpoint)
48 | if isinstance(data, dict):
49 | response = requests.post(url, data=data, timeout=5)
50 | else:
51 | response = requests.post(url, data=data.encode("utf-8"), timeout=5)
52 | response.encoding = 'utf-8'
53 | if response.ok:
54 | return response.text.strip()
55 | else:
56 | return f"Error {response.status_code}: {response.text.strip()}"
57 | except Exception as e:
58 | return f"Request failed: {str(e)}"
59 |
60 | @mcp.tool()
61 | def list_methods(offset: int = 0, limit: int = 100) -> list:
62 | """
63 | List all function names in the program with pagination.
64 | """
65 | return safe_get("methods", {"offset": offset, "limit": limit})
66 |
67 | @mcp.tool()
68 | def list_classes(offset: int = 0, limit: int = 100) -> list:
69 | """
70 | List all namespace/class names in the program with pagination.
71 | """
72 | return safe_get("classes", {"offset": offset, "limit": limit})
73 |
74 | @mcp.tool()
75 | def decompile_function(name: str) -> str:
76 | """
77 | Decompile a specific function by name and return the decompiled C code.
78 | """
79 | return safe_post("decompile", name)
80 |
81 | @mcp.tool()
82 | def rename_function(old_name: str, new_name: str) -> str:
83 | """
84 | Rename a function by its current name to a new user-defined name.
85 | """
86 | return safe_post("renameFunction", {"oldName": old_name, "newName": new_name})
87 |
88 | @mcp.tool()
89 | def rename_data(address: str, new_name: str) -> str:
90 | """
91 | Rename a data label at the specified address.
92 | """
93 | return safe_post("renameData", {"address": address, "newName": new_name})
94 |
95 | @mcp.tool()
96 | def list_segments(offset: int = 0, limit: int = 100) -> list:
97 | """
98 | List all memory segments in the program with pagination.
99 | """
100 | return safe_get("segments", {"offset": offset, "limit": limit})
101 |
102 | @mcp.tool()
103 | def list_imports(offset: int = 0, limit: int = 100) -> list:
104 | """
105 | List imported symbols in the program with pagination.
106 | """
107 | return safe_get("imports", {"offset": offset, "limit": limit})
108 |
109 | @mcp.tool()
110 | def list_exports(offset: int = 0, limit: int = 100) -> list:
111 | """
112 | List exported functions/symbols with pagination.
113 | """
114 | return safe_get("exports", {"offset": offset, "limit": limit})
115 |
116 | @mcp.tool()
117 | def list_namespaces(offset: int = 0, limit: int = 100) -> list:
118 | """
119 | List all non-global namespaces in the program with pagination.
120 | """
121 | return safe_get("namespaces", {"offset": offset, "limit": limit})
122 |
123 | @mcp.tool()
124 | def list_data_items(offset: int = 0, limit: int = 100) -> list:
125 | """
126 | List defined data labels and their values with pagination.
127 | """
128 | return safe_get("data", {"offset": offset, "limit": limit})
129 |
130 | @mcp.tool()
131 | def search_functions_by_name(query: str, offset: int = 0, limit: int = 100) -> list:
132 | """
133 | Search for functions whose name contains the given substring.
134 | """
135 | if not query:
136 | return ["Error: query string is required"]
137 | return safe_get("searchFunctions", {"query": query, "offset": offset, "limit": limit})
138 |
139 | @mcp.tool()
140 | def rename_variable(function_name: str, old_name: str, new_name: str) -> str:
141 | """
142 | Rename a local variable within a function.
143 | """
144 | return safe_post("renameVariable", {
145 | "functionName": function_name,
146 | "oldName": old_name,
147 | "newName": new_name
148 | })
149 |
150 | @mcp.tool()
151 | def get_function_by_address(address: str) -> str:
152 | """
153 | Get a function by its address.
154 | """
155 | return "\n".join(safe_get("get_function_by_address", {"address": address}))
156 |
157 | @mcp.tool()
158 | def get_current_address() -> str:
159 | """
160 | Get the address currently selected by the user.
161 | """
162 | return "\n".join(safe_get("get_current_address"))
163 |
164 | @mcp.tool()
165 | def get_current_function() -> str:
166 | """
167 | Get the function currently selected by the user.
168 | """
169 | return "\n".join(safe_get("get_current_function"))
170 |
171 | @mcp.tool()
172 | def list_functions() -> list:
173 | """
174 | List all functions in the database.
175 | """
176 | return safe_get("list_functions")
177 |
178 | @mcp.tool()
179 | def decompile_function_by_address(address: str) -> str:
180 | """
181 | Decompile a function at the given address.
182 | """
183 | return "\n".join(safe_get("decompile_function", {"address": address}))
184 |
185 | @mcp.tool()
186 | def disassemble_function(address: str) -> list:
187 | """
188 | Get assembly code (address: instruction; comment) for a function.
189 | """
190 | return safe_get("disassemble_function", {"address": address})
191 |
192 | @mcp.tool()
193 | def set_decompiler_comment(address: str, comment: str) -> str:
194 | """
195 | Set a comment for a given address in the function pseudocode.
196 | """
197 | return safe_post("set_decompiler_comment", {"address": address, "comment": comment})
198 |
199 | @mcp.tool()
200 | def set_disassembly_comment(address: str, comment: str) -> str:
201 | """
202 | Set a comment for a given address in the function disassembly.
203 | """
204 | return safe_post("set_disassembly_comment", {"address": address, "comment": comment})
205 |
206 | @mcp.tool()
207 | def rename_function_by_address(function_address: str, new_name: str) -> str:
208 | """
209 | Rename a function by its address.
210 | """
211 | return safe_post("rename_function_by_address", {"function_address": function_address, "new_name": new_name})
212 |
213 | @mcp.tool()
214 | def set_function_prototype(function_address: str, prototype: str) -> str:
215 | """
216 | Set a function's prototype.
217 | """
218 | return safe_post("set_function_prototype", {"function_address": function_address, "prototype": prototype})
219 |
220 | @mcp.tool()
221 | def set_local_variable_type(function_address: str, variable_name: str, new_type: str) -> str:
222 | """
223 | Set a local variable's type.
224 | """
225 | return safe_post("set_local_variable_type", {"function_address": function_address, "variable_name": variable_name, "new_type": new_type})
226 |
227 | @mcp.tool()
228 | def get_xrefs_to(address: str, offset: int = 0, limit: int = 100) -> list:
229 | """
230 | Get all references to the specified address (xref to).
231 |
232 | Args:
233 | address: Target address in hex format (e.g. "0x1400010a0")
234 | offset: Pagination offset (default: 0)
235 | limit: Maximum number of references to return (default: 100)
236 |
237 | Returns:
238 | List of references to the specified address
239 | """
240 | return safe_get("xrefs_to", {"address": address, "offset": offset, "limit": limit})
241 |
242 | @mcp.tool()
243 | def get_xrefs_from(address: str, offset: int = 0, limit: int = 100) -> list:
244 | """
245 | Get all references from the specified address (xref from).
246 |
247 | Args:
248 | address: Source address in hex format (e.g. "0x1400010a0")
249 | offset: Pagination offset (default: 0)
250 | limit: Maximum number of references to return (default: 100)
251 |
252 | Returns:
253 | List of references from the specified address
254 | """
255 | return safe_get("xrefs_from", {"address": address, "offset": offset, "limit": limit})
256 |
257 | @mcp.tool()
258 | def get_function_xrefs(name: str, offset: int = 0, limit: int = 100) -> list:
259 | """
260 | Get all references to the specified function by name.
261 |
262 | Args:
263 | name: Function name to search for
264 | offset: Pagination offset (default: 0)
265 | limit: Maximum number of references to return (default: 100)
266 |
267 | Returns:
268 | List of references to the specified function
269 | """
270 | return safe_get("function_xrefs", {"name": name, "offset": offset, "limit": limit})
271 |
272 | @mcp.tool()
273 | def list_strings(offset: int = 0, limit: int = 2000, filter: str = None) -> list:
274 | """
275 | List all defined strings in the program with their addresses.
276 |
277 | Args:
278 | offset: Pagination offset (default: 0)
279 | limit: Maximum number of strings to return (default: 2000)
280 | filter: Optional filter to match within string content
281 |
282 | Returns:
283 | List of strings with their addresses
284 | """
285 | params = {"offset": offset, "limit": limit}
286 | if filter:
287 | params["filter"] = filter
288 | return safe_get("strings", params)
289 |
290 | def main():
291 | parser = argparse.ArgumentParser(description="MCP server for Ghidra")
292 | parser.add_argument("--ghidra-server", type=str, default=DEFAULT_GHIDRA_SERVER,
293 | help=f"Ghidra server URL, default: {DEFAULT_GHIDRA_SERVER}")
294 | parser.add_argument("--mcp-host", type=str, default="127.0.0.1",
295 | help="Host to run MCP server on (only used for sse), default: 127.0.0.1")
296 | parser.add_argument("--mcp-port", type=int,
297 | help="Port to run MCP server on (only used for sse), default: 8081")
298 | parser.add_argument("--transport", type=str, default="stdio", choices=["stdio", "sse"],
299 | help="Transport protocol for MCP, default: stdio")
300 | args = parser.parse_args()
301 |
302 | # Use the global variable to ensure it's properly updated
303 | global ghidra_server_url
304 | if args.ghidra_server:
305 | ghidra_server_url = args.ghidra_server
306 |
307 | if args.transport == "sse":
308 | try:
309 | # Set up logging
310 | log_level = logging.INFO
311 | logging.basicConfig(level=log_level)
312 | logging.getLogger().setLevel(log_level)
313 |
314 | # Configure MCP settings
315 | mcp.settings.log_level = "INFO"
316 | if args.mcp_host:
317 | mcp.settings.host = args.mcp_host
318 | else:
319 | mcp.settings.host = "127.0.0.1"
320 |
321 | if args.mcp_port:
322 | mcp.settings.port = args.mcp_port
323 | else:
324 | mcp.settings.port = 8081
325 |
326 | logger.info(f"Connecting to Ghidra server at {ghidra_server_url}")
327 | logger.info(f"Starting MCP server on http://{mcp.settings.host}:{mcp.settings.port}/sse")
328 | logger.info(f"Using transport: {args.transport}")
329 |
330 | mcp.run(transport="sse")
331 | except KeyboardInterrupt:
332 | logger.info("Server stopped by user")
333 | else:
334 | mcp.run()
335 |
336 | if __name__ == "__main__":
337 | main()
338 |
339 |
--------------------------------------------------------------------------------
/images/enable_plugin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaurieWired/GhidraMCP/3b876ddbf761a9b41c6c4c42697e8fb5a3cedcdb/images/enable_plugin.png
--------------------------------------------------------------------------------
/images/new_plugins_found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaurieWired/GhidraMCP/3b876ddbf761a9b41c6c4c42697e8fb5a3cedcdb/images/new_plugins_found.png
--------------------------------------------------------------------------------
/lib/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 | 4.0.0
6 | com.lauriewired
7 | GhidraMCP
8 | jar
9 | 1.0-SNAPSHOT
10 | GhidraMCP
11 | http://maven.apache.org
12 |
13 |
14 |
15 |
16 | ghidra
17 | Generic
18 | 11.3.1
19 | system
20 | ${project.basedir}/lib/Generic.jar
21 |
22 |
23 | ghidra
24 | SoftwareModeling
25 | 11.3.1
26 | system
27 | ${project.basedir}/lib/SoftwareModeling.jar
28 |
29 |
30 | ghidra
31 | Project
32 | 11.3.1
33 | system
34 | ${project.basedir}/lib/Project.jar
35 |
36 |
37 | ghidra
38 | Docking
39 | 11.3.1
40 | system
41 | ${project.basedir}/lib/Docking.jar
42 |
43 |
44 | ghidra
45 | Decompiler
46 | 11.3.1
47 | system
48 | ${project.basedir}/lib/Decompiler.jar
49 |
50 |
51 | ghidra
52 | Utility
53 | 11.3.1
54 | system
55 | ${project.basedir}/lib/Utility.jar
56 |
57 |
58 | ghidra
59 | Base
60 | 11.3.1
61 | system
62 | ${project.basedir}/lib/Base.jar
63 |
64 |
65 | ghidra
66 | Gui
67 | 11.3.1
68 | system
69 | ${project.basedir}/lib/Gui.jar
70 |
71 |
72 |
73 |
74 | junit
75 | junit
76 | 3.8.1
77 | test
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | maven-jar-plugin
86 | 3.2.2
87 |
88 |
89 | src/main/resources/META-INF/MANIFEST.MF
90 |
91 |
92 | GhidraMCP
93 |
94 |
95 | **/App.class
96 |
97 |
98 | ${project.build.directory}
99 |
100 |
101 |
102 |
103 |
104 | org.apache.maven.plugins
105 | maven-assembly-plugin
106 | 3.3.0
107 |
108 |
109 |
110 | src/assembly/ghidra-extension.xml
111 |
112 |
113 |
114 | GhidraMCP-${project.version}
115 |
116 |
117 | false
118 |
119 |
120 |
121 |
122 | make-assembly
123 | package
124 |
125 | single
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | org.apache.maven.plugins
134 | maven-dependency-plugin
135 | 3.1.2
136 |
137 |
138 | copy-dependencies
139 | prepare-package
140 |
141 | copy-dependencies
142 |
143 |
144 | ${project.build.directory}/lib
145 | runtime
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | mcp==1.5.0
2 | requests==2.32.3
3 |
--------------------------------------------------------------------------------
/src/assembly/ghidra-extension.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | ghidra-extension
9 |
10 |
11 |
12 | zip
13 |
14 |
15 |
16 | false
17 |
18 |
19 |
21 |
22 | src/main/resources
23 |
24 | extension.properties
25 | Module.manifest
26 |
27 | GhidraMCP
28 |
29 |
30 |
31 |
32 | ${project.build.directory}
33 |
34 |
35 | GhidraMCP.jar
36 |
37 | GhidraMCP/lib
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/main/java/com/lauriewired/GhidraMCPPlugin.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired;
2 |
3 | import ghidra.framework.plugintool.Plugin;
4 | import ghidra.framework.plugintool.PluginTool;
5 | import ghidra.program.model.address.Address;
6 | import ghidra.program.model.address.GlobalNamespace;
7 | import ghidra.program.model.listing.*;
8 | import ghidra.program.model.mem.MemoryBlock;
9 | import ghidra.program.model.symbol.*;
10 | import ghidra.program.model.symbol.ReferenceManager;
11 | import ghidra.program.model.symbol.Reference;
12 | import ghidra.program.model.symbol.ReferenceIterator;
13 | import ghidra.program.model.symbol.RefType;
14 | import ghidra.program.model.pcode.HighFunction;
15 | import ghidra.program.model.pcode.HighSymbol;
16 | import ghidra.program.model.pcode.LocalSymbolMap;
17 | import ghidra.program.model.pcode.HighFunctionDBUtil;
18 | import ghidra.program.model.pcode.HighFunctionDBUtil.ReturnCommitOption;
19 | import ghidra.app.decompiler.DecompInterface;
20 | import ghidra.app.decompiler.DecompileResults;
21 | import ghidra.app.plugin.PluginCategoryNames;
22 | import ghidra.app.services.CodeViewerService;
23 | import ghidra.app.services.ProgramManager;
24 | import ghidra.app.util.PseudoDisassembler;
25 | import ghidra.app.cmd.function.SetVariableNameCmd;
26 | import ghidra.program.model.symbol.SourceType;
27 | import ghidra.program.model.listing.LocalVariableImpl;
28 | import ghidra.program.model.listing.ParameterImpl;
29 | import ghidra.util.exception.DuplicateNameException;
30 | import ghidra.util.exception.InvalidInputException;
31 | import ghidra.framework.plugintool.PluginInfo;
32 | import ghidra.framework.plugintool.util.PluginStatus;
33 | import ghidra.program.util.ProgramLocation;
34 | import ghidra.util.Msg;
35 | import ghidra.util.task.ConsoleTaskMonitor;
36 | import ghidra.util.task.TaskMonitor;
37 | import ghidra.program.model.pcode.HighVariable;
38 | import ghidra.program.model.pcode.Varnode;
39 | import ghidra.program.model.data.DataType;
40 | import ghidra.program.model.data.DataTypeManager;
41 | import ghidra.program.model.data.PointerDataType;
42 | import ghidra.program.model.data.Undefined1DataType;
43 | import ghidra.program.model.listing.Variable;
44 | import ghidra.app.decompiler.component.DecompilerUtils;
45 | import ghidra.app.decompiler.ClangToken;
46 | import ghidra.framework.options.Options;
47 |
48 | import com.sun.net.httpserver.HttpExchange;
49 | import com.sun.net.httpserver.HttpServer;
50 |
51 | import javax.swing.SwingUtilities;
52 | import java.io.IOException;
53 | import java.io.OutputStream;
54 | import java.lang.reflect.InvocationTargetException;
55 | import java.net.InetSocketAddress;
56 | import java.net.URLDecoder;
57 | import java.nio.charset.StandardCharsets;
58 | import java.util.*;
59 | import java.util.concurrent.atomic.AtomicBoolean;
60 |
61 | @PluginInfo(
62 | status = PluginStatus.RELEASED,
63 | packageName = ghidra.app.DeveloperPluginPackage.NAME,
64 | category = PluginCategoryNames.ANALYSIS,
65 | shortDescription = "HTTP server plugin",
66 | description = "Starts an embedded HTTP server to expose program data. Port configurable via Tool Options."
67 | )
68 | public class GhidraMCPPlugin extends Plugin {
69 |
70 | private HttpServer server;
71 | private static final String OPTION_CATEGORY_NAME = "GhidraMCP HTTP Server";
72 | private static final String PORT_OPTION_NAME = "Server Port";
73 | private static final int DEFAULT_PORT = 8080;
74 |
75 | public GhidraMCPPlugin(PluginTool tool) {
76 | super(tool);
77 | Msg.info(this, "GhidraMCPPlugin loading...");
78 |
79 | // Register the configuration option
80 | Options options = tool.getOptions(OPTION_CATEGORY_NAME);
81 | options.registerOption(PORT_OPTION_NAME, DEFAULT_PORT,
82 | null, // No help location for now
83 | "The network port number the embedded HTTP server will listen on. " +
84 | "Requires Ghidra restart or plugin reload to take effect after changing.");
85 |
86 | try {
87 | startServer();
88 | }
89 | catch (IOException e) {
90 | Msg.error(this, "Failed to start HTTP server", e);
91 | }
92 | Msg.info(this, "GhidraMCPPlugin loaded!");
93 | }
94 |
95 | private void startServer() throws IOException {
96 | // Read the configured port
97 | Options options = tool.getOptions(OPTION_CATEGORY_NAME);
98 | int port = options.getInt(PORT_OPTION_NAME, DEFAULT_PORT);
99 |
100 | // Stop existing server if running (e.g., if plugin is reloaded)
101 | if (server != null) {
102 | Msg.info(this, "Stopping existing HTTP server before starting new one.");
103 | server.stop(0);
104 | server = null;
105 | }
106 |
107 | server = HttpServer.create(new InetSocketAddress(port), 0);
108 |
109 | // Each listing endpoint uses offset & limit from query params:
110 | server.createContext("/methods", exchange -> {
111 | Map qparams = parseQueryParams(exchange);
112 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
113 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
114 | sendResponse(exchange, getAllFunctionNames(offset, limit));
115 | });
116 |
117 | server.createContext("/classes", exchange -> {
118 | Map qparams = parseQueryParams(exchange);
119 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
120 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
121 | sendResponse(exchange, getAllClassNames(offset, limit));
122 | });
123 |
124 | server.createContext("/decompile", exchange -> {
125 | String name = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
126 | sendResponse(exchange, decompileFunctionByName(name));
127 | });
128 |
129 | server.createContext("/renameFunction", exchange -> {
130 | Map params = parsePostParams(exchange);
131 | String response = renameFunction(params.get("oldName"), params.get("newName"))
132 | ? "Renamed successfully" : "Rename failed";
133 | sendResponse(exchange, response);
134 | });
135 |
136 | server.createContext("/renameData", exchange -> {
137 | Map params = parsePostParams(exchange);
138 | renameDataAtAddress(params.get("address"), params.get("newName"));
139 | sendResponse(exchange, "Rename data attempted");
140 | });
141 |
142 | server.createContext("/renameVariable", exchange -> {
143 | Map params = parsePostParams(exchange);
144 | String functionName = params.get("functionName");
145 | String oldName = params.get("oldName");
146 | String newName = params.get("newName");
147 | String result = renameVariableInFunction(functionName, oldName, newName);
148 | sendResponse(exchange, result);
149 | });
150 |
151 | server.createContext("/segments", exchange -> {
152 | Map qparams = parseQueryParams(exchange);
153 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
154 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
155 | sendResponse(exchange, listSegments(offset, limit));
156 | });
157 |
158 | server.createContext("/imports", exchange -> {
159 | Map qparams = parseQueryParams(exchange);
160 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
161 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
162 | sendResponse(exchange, listImports(offset, limit));
163 | });
164 |
165 | server.createContext("/exports", exchange -> {
166 | Map qparams = parseQueryParams(exchange);
167 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
168 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
169 | sendResponse(exchange, listExports(offset, limit));
170 | });
171 |
172 | server.createContext("/namespaces", exchange -> {
173 | Map qparams = parseQueryParams(exchange);
174 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
175 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
176 | sendResponse(exchange, listNamespaces(offset, limit));
177 | });
178 |
179 | server.createContext("/data", exchange -> {
180 | Map qparams = parseQueryParams(exchange);
181 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
182 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
183 | sendResponse(exchange, listDefinedData(offset, limit));
184 | });
185 |
186 | server.createContext("/searchFunctions", exchange -> {
187 | Map qparams = parseQueryParams(exchange);
188 | String searchTerm = qparams.get("query");
189 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
190 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
191 | sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit));
192 | });
193 |
194 | // New API endpoints based on requirements
195 |
196 | server.createContext("/get_function_by_address", exchange -> {
197 | Map qparams = parseQueryParams(exchange);
198 | String address = qparams.get("address");
199 | sendResponse(exchange, getFunctionByAddress(address));
200 | });
201 |
202 | server.createContext("/get_current_address", exchange -> {
203 | sendResponse(exchange, getCurrentAddress());
204 | });
205 |
206 | server.createContext("/get_current_function", exchange -> {
207 | sendResponse(exchange, getCurrentFunction());
208 | });
209 |
210 | server.createContext("/list_functions", exchange -> {
211 | sendResponse(exchange, listFunctions());
212 | });
213 |
214 | server.createContext("/decompile_function", exchange -> {
215 | Map qparams = parseQueryParams(exchange);
216 | String address = qparams.get("address");
217 | sendResponse(exchange, decompileFunctionByAddress(address));
218 | });
219 |
220 | server.createContext("/disassemble_function", exchange -> {
221 | Map qparams = parseQueryParams(exchange);
222 | String address = qparams.get("address");
223 | sendResponse(exchange, disassembleFunction(address));
224 | });
225 |
226 | server.createContext("/set_decompiler_comment", exchange -> {
227 | Map params = parsePostParams(exchange);
228 | String address = params.get("address");
229 | String comment = params.get("comment");
230 | boolean success = setDecompilerComment(address, comment);
231 | sendResponse(exchange, success ? "Comment set successfully" : "Failed to set comment");
232 | });
233 |
234 | server.createContext("/set_disassembly_comment", exchange -> {
235 | Map params = parsePostParams(exchange);
236 | String address = params.get("address");
237 | String comment = params.get("comment");
238 | boolean success = setDisassemblyComment(address, comment);
239 | sendResponse(exchange, success ? "Comment set successfully" : "Failed to set comment");
240 | });
241 |
242 | server.createContext("/rename_function_by_address", exchange -> {
243 | Map params = parsePostParams(exchange);
244 | String functionAddress = params.get("function_address");
245 | String newName = params.get("new_name");
246 | boolean success = renameFunctionByAddress(functionAddress, newName);
247 | sendResponse(exchange, success ? "Function renamed successfully" : "Failed to rename function");
248 | });
249 |
250 | server.createContext("/set_function_prototype", exchange -> {
251 | Map params = parsePostParams(exchange);
252 | String functionAddress = params.get("function_address");
253 | String prototype = params.get("prototype");
254 |
255 | // Call the set prototype function and get detailed result
256 | PrototypeResult result = setFunctionPrototype(functionAddress, prototype);
257 |
258 | if (result.isSuccess()) {
259 | // Even with successful operations, include any warning messages for debugging
260 | String successMsg = "Function prototype set successfully";
261 | if (!result.getErrorMessage().isEmpty()) {
262 | successMsg += "\n\nWarnings/Debug Info:\n" + result.getErrorMessage();
263 | }
264 | sendResponse(exchange, successMsg);
265 | } else {
266 | // Return the detailed error message to the client
267 | sendResponse(exchange, "Failed to set function prototype: " + result.getErrorMessage());
268 | }
269 | });
270 |
271 | server.createContext("/set_local_variable_type", exchange -> {
272 | Map params = parsePostParams(exchange);
273 | String functionAddress = params.get("function_address");
274 | String variableName = params.get("variable_name");
275 | String newType = params.get("new_type");
276 |
277 | // Capture detailed information about setting the type
278 | StringBuilder responseMsg = new StringBuilder();
279 | responseMsg.append("Setting variable type: ").append(variableName)
280 | .append(" to ").append(newType)
281 | .append(" in function at ").append(functionAddress).append("\n\n");
282 |
283 | // Attempt to find the data type in various categories
284 | Program program = getCurrentProgram();
285 | if (program != null) {
286 | DataTypeManager dtm = program.getDataTypeManager();
287 | DataType directType = findDataTypeByNameInAllCategories(dtm, newType);
288 | if (directType != null) {
289 | responseMsg.append("Found type: ").append(directType.getPathName()).append("\n");
290 | } else if (newType.startsWith("P") && newType.length() > 1) {
291 | String baseTypeName = newType.substring(1);
292 | DataType baseType = findDataTypeByNameInAllCategories(dtm, baseTypeName);
293 | if (baseType != null) {
294 | responseMsg.append("Found base type for pointer: ").append(baseType.getPathName()).append("\n");
295 | } else {
296 | responseMsg.append("Base type not found for pointer: ").append(baseTypeName).append("\n");
297 | }
298 | } else {
299 | responseMsg.append("Type not found directly: ").append(newType).append("\n");
300 | }
301 | }
302 |
303 | // Try to set the type
304 | boolean success = setLocalVariableType(functionAddress, variableName, newType);
305 |
306 | String successMsg = success ? "Variable type set successfully" : "Failed to set variable type";
307 | responseMsg.append("\nResult: ").append(successMsg);
308 |
309 | sendResponse(exchange, responseMsg.toString());
310 | });
311 |
312 | server.createContext("/xrefs_to", exchange -> {
313 | Map qparams = parseQueryParams(exchange);
314 | String address = qparams.get("address");
315 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
316 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
317 | sendResponse(exchange, getXrefsTo(address, offset, limit));
318 | });
319 |
320 | server.createContext("/xrefs_from", exchange -> {
321 | Map qparams = parseQueryParams(exchange);
322 | String address = qparams.get("address");
323 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
324 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
325 | sendResponse(exchange, getXrefsFrom(address, offset, limit));
326 | });
327 |
328 | server.createContext("/function_xrefs", exchange -> {
329 | Map qparams = parseQueryParams(exchange);
330 | String name = qparams.get("name");
331 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
332 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
333 | sendResponse(exchange, getFunctionXrefs(name, offset, limit));
334 | });
335 |
336 | server.createContext("/strings", exchange -> {
337 | Map qparams = parseQueryParams(exchange);
338 | int offset = parseIntOrDefault(qparams.get("offset"), 0);
339 | int limit = parseIntOrDefault(qparams.get("limit"), 100);
340 | String filter = qparams.get("filter");
341 | sendResponse(exchange, listDefinedStrings(offset, limit, filter));
342 | });
343 |
344 | server.setExecutor(null);
345 | new Thread(() -> {
346 | try {
347 | server.start();
348 | Msg.info(this, "GhidraMCP HTTP server started on port " + port);
349 | } catch (Exception e) {
350 | Msg.error(this, "Failed to start HTTP server on port " + port + ". Port might be in use.", e);
351 | server = null; // Ensure server isn't considered running
352 | }
353 | }, "GhidraMCP-HTTP-Server").start();
354 | }
355 |
356 | // ----------------------------------------------------------------------------------
357 | // Pagination-aware listing methods
358 | // ----------------------------------------------------------------------------------
359 |
360 | private String getAllFunctionNames(int offset, int limit) {
361 | Program program = getCurrentProgram();
362 | if (program == null) return "No program loaded";
363 |
364 | List names = new ArrayList<>();
365 | for (Function f : program.getFunctionManager().getFunctions(true)) {
366 | names.add(f.getName());
367 | }
368 | return paginateList(names, offset, limit);
369 | }
370 |
371 | private String getAllClassNames(int offset, int limit) {
372 | Program program = getCurrentProgram();
373 | if (program == null) return "No program loaded";
374 |
375 | Set classNames = new HashSet<>();
376 | for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) {
377 | Namespace ns = symbol.getParentNamespace();
378 | if (ns != null && !ns.isGlobal()) {
379 | classNames.add(ns.getName());
380 | }
381 | }
382 | // Convert set to list for pagination
383 | List sorted = new ArrayList<>(classNames);
384 | Collections.sort(sorted);
385 | return paginateList(sorted, offset, limit);
386 | }
387 |
388 | private String listSegments(int offset, int limit) {
389 | Program program = getCurrentProgram();
390 | if (program == null) return "No program loaded";
391 |
392 | List lines = new ArrayList<>();
393 | for (MemoryBlock block : program.getMemory().getBlocks()) {
394 | lines.add(String.format("%s: %s - %s", block.getName(), block.getStart(), block.getEnd()));
395 | }
396 | return paginateList(lines, offset, limit);
397 | }
398 |
399 | private String listImports(int offset, int limit) {
400 | Program program = getCurrentProgram();
401 | if (program == null) return "No program loaded";
402 |
403 | List lines = new ArrayList<>();
404 | for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) {
405 | lines.add(symbol.getName() + " -> " + symbol.getAddress());
406 | }
407 | return paginateList(lines, offset, limit);
408 | }
409 |
410 | private String listExports(int offset, int limit) {
411 | Program program = getCurrentProgram();
412 | if (program == null) return "No program loaded";
413 |
414 | SymbolTable table = program.getSymbolTable();
415 | SymbolIterator it = table.getAllSymbols(true);
416 |
417 | List lines = new ArrayList<>();
418 | while (it.hasNext()) {
419 | Symbol s = it.next();
420 | // On older Ghidra, "export" is recognized via isExternalEntryPoint()
421 | if (s.isExternalEntryPoint()) {
422 | lines.add(s.getName() + " -> " + s.getAddress());
423 | }
424 | }
425 | return paginateList(lines, offset, limit);
426 | }
427 |
428 | private String listNamespaces(int offset, int limit) {
429 | Program program = getCurrentProgram();
430 | if (program == null) return "No program loaded";
431 |
432 | Set namespaces = new HashSet<>();
433 | for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) {
434 | Namespace ns = symbol.getParentNamespace();
435 | if (ns != null && !(ns instanceof GlobalNamespace)) {
436 | namespaces.add(ns.getName());
437 | }
438 | }
439 | List sorted = new ArrayList<>(namespaces);
440 | Collections.sort(sorted);
441 | return paginateList(sorted, offset, limit);
442 | }
443 |
444 | private String listDefinedData(int offset, int limit) {
445 | Program program = getCurrentProgram();
446 | if (program == null) return "No program loaded";
447 |
448 | List lines = new ArrayList<>();
449 | for (MemoryBlock block : program.getMemory().getBlocks()) {
450 | DataIterator it = program.getListing().getDefinedData(block.getStart(), true);
451 | while (it.hasNext()) {
452 | Data data = it.next();
453 | if (block.contains(data.getAddress())) {
454 | String label = data.getLabel() != null ? data.getLabel() : "(unnamed)";
455 | String valRepr = data.getDefaultValueRepresentation();
456 | lines.add(String.format("%s: %s = %s",
457 | data.getAddress(),
458 | escapeNonAscii(label),
459 | escapeNonAscii(valRepr)
460 | ));
461 | }
462 | }
463 | }
464 | return paginateList(lines, offset, limit);
465 | }
466 |
467 | private String searchFunctionsByName(String searchTerm, int offset, int limit) {
468 | Program program = getCurrentProgram();
469 | if (program == null) return "No program loaded";
470 | if (searchTerm == null || searchTerm.isEmpty()) return "Search term is required";
471 |
472 | List matches = new ArrayList<>();
473 | for (Function func : program.getFunctionManager().getFunctions(true)) {
474 | String name = func.getName();
475 | // simple substring match
476 | if (name.toLowerCase().contains(searchTerm.toLowerCase())) {
477 | matches.add(String.format("%s @ %s", name, func.getEntryPoint()));
478 | }
479 | }
480 |
481 | Collections.sort(matches);
482 |
483 | if (matches.isEmpty()) {
484 | return "No functions matching '" + searchTerm + "'";
485 | }
486 | return paginateList(matches, offset, limit);
487 | }
488 |
489 | // ----------------------------------------------------------------------------------
490 | // Logic for rename, decompile, etc.
491 | // ----------------------------------------------------------------------------------
492 |
493 | private String decompileFunctionByName(String name) {
494 | Program program = getCurrentProgram();
495 | if (program == null) return "No program loaded";
496 | DecompInterface decomp = new DecompInterface();
497 | decomp.openProgram(program);
498 | for (Function func : program.getFunctionManager().getFunctions(true)) {
499 | if (func.getName().equals(name)) {
500 | DecompileResults result =
501 | decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
502 | if (result != null && result.decompileCompleted()) {
503 | return result.getDecompiledFunction().getC();
504 | } else {
505 | return "Decompilation failed";
506 | }
507 | }
508 | }
509 | return "Function not found";
510 | }
511 |
512 | private boolean renameFunction(String oldName, String newName) {
513 | Program program = getCurrentProgram();
514 | if (program == null) return false;
515 |
516 | AtomicBoolean successFlag = new AtomicBoolean(false);
517 | try {
518 | SwingUtilities.invokeAndWait(() -> {
519 | int tx = program.startTransaction("Rename function via HTTP");
520 | try {
521 | for (Function func : program.getFunctionManager().getFunctions(true)) {
522 | if (func.getName().equals(oldName)) {
523 | func.setName(newName, SourceType.USER_DEFINED);
524 | successFlag.set(true);
525 | break;
526 | }
527 | }
528 | }
529 | catch (Exception e) {
530 | Msg.error(this, "Error renaming function", e);
531 | }
532 | finally {
533 | program.endTransaction(tx, successFlag.get());
534 | }
535 | });
536 | }
537 | catch (InterruptedException | InvocationTargetException e) {
538 | Msg.error(this, "Failed to execute rename on Swing thread", e);
539 | }
540 | return successFlag.get();
541 | }
542 |
543 | private void renameDataAtAddress(String addressStr, String newName) {
544 | Program program = getCurrentProgram();
545 | if (program == null) return;
546 |
547 | try {
548 | SwingUtilities.invokeAndWait(() -> {
549 | int tx = program.startTransaction("Rename data");
550 | try {
551 | Address addr = program.getAddressFactory().getAddress(addressStr);
552 | Listing listing = program.getListing();
553 | Data data = listing.getDefinedDataAt(addr);
554 | if (data != null) {
555 | SymbolTable symTable = program.getSymbolTable();
556 | Symbol symbol = symTable.getPrimarySymbol(addr);
557 | if (symbol != null) {
558 | symbol.setName(newName, SourceType.USER_DEFINED);
559 | } else {
560 | symTable.createLabel(addr, newName, SourceType.USER_DEFINED);
561 | }
562 | }
563 | }
564 | catch (Exception e) {
565 | Msg.error(this, "Rename data error", e);
566 | }
567 | finally {
568 | program.endTransaction(tx, true);
569 | }
570 | });
571 | }
572 | catch (InterruptedException | InvocationTargetException e) {
573 | Msg.error(this, "Failed to execute rename data on Swing thread", e);
574 | }
575 | }
576 |
577 | private String renameVariableInFunction(String functionName, String oldVarName, String newVarName) {
578 | Program program = getCurrentProgram();
579 | if (program == null) return "No program loaded";
580 |
581 | DecompInterface decomp = new DecompInterface();
582 | decomp.openProgram(program);
583 |
584 | Function func = null;
585 | for (Function f : program.getFunctionManager().getFunctions(true)) {
586 | if (f.getName().equals(functionName)) {
587 | func = f;
588 | break;
589 | }
590 | }
591 |
592 | if (func == null) {
593 | return "Function not found";
594 | }
595 |
596 | DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
597 | if (result == null || !result.decompileCompleted()) {
598 | return "Decompilation failed";
599 | }
600 |
601 | HighFunction highFunction = result.getHighFunction();
602 | if (highFunction == null) {
603 | return "Decompilation failed (no high function)";
604 | }
605 |
606 | LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap();
607 | if (localSymbolMap == null) {
608 | return "Decompilation failed (no local symbol map)";
609 | }
610 |
611 | HighSymbol highSymbol = null;
612 | Iterator symbols = localSymbolMap.getSymbols();
613 | while (symbols.hasNext()) {
614 | HighSymbol symbol = symbols.next();
615 | String symbolName = symbol.getName();
616 |
617 | if (symbolName.equals(oldVarName)) {
618 | highSymbol = symbol;
619 | }
620 | if (symbolName.equals(newVarName)) {
621 | return "Error: A variable with name '" + newVarName + "' already exists in this function";
622 | }
623 | }
624 |
625 | if (highSymbol == null) {
626 | return "Variable not found";
627 | }
628 |
629 | boolean commitRequired = checkFullCommit(highSymbol, highFunction);
630 |
631 | final HighSymbol finalHighSymbol = highSymbol;
632 | final Function finalFunction = func;
633 | AtomicBoolean successFlag = new AtomicBoolean(false);
634 |
635 | try {
636 | SwingUtilities.invokeAndWait(() -> {
637 | int tx = program.startTransaction("Rename variable");
638 | try {
639 | if (commitRequired) {
640 | HighFunctionDBUtil.commitParamsToDatabase(highFunction, false,
641 | ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource());
642 | }
643 | HighFunctionDBUtil.updateDBVariable(
644 | finalHighSymbol,
645 | newVarName,
646 | null,
647 | SourceType.USER_DEFINED
648 | );
649 | successFlag.set(true);
650 | }
651 | catch (Exception e) {
652 | Msg.error(this, "Failed to rename variable", e);
653 | }
654 | finally {
655 | program.endTransaction(tx, true);
656 | }
657 | });
658 | } catch (InterruptedException | InvocationTargetException e) {
659 | String errorMsg = "Failed to execute rename on Swing thread: " + e.getMessage();
660 | Msg.error(this, errorMsg, e);
661 | return errorMsg;
662 | }
663 | return successFlag.get() ? "Variable renamed" : "Failed to rename variable";
664 | }
665 |
666 | /**
667 | * Copied from AbstractDecompilerAction.checkFullCommit, it's protected.
668 | * Compare the given HighFunction's idea of the prototype with the Function's idea.
669 | * Return true if there is a difference. If a specific symbol is being changed,
670 | * it can be passed in to check whether or not the prototype is being affected.
671 | * @param highSymbol (if not null) is the symbol being modified
672 | * @param hfunction is the given HighFunction
673 | * @return true if there is a difference (and a full commit is required)
674 | */
675 | protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) {
676 | if (highSymbol != null && !highSymbol.isParameter()) {
677 | return false;
678 | }
679 | Function function = hfunction.getFunction();
680 | Parameter[] parameters = function.getParameters();
681 | LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap();
682 | int numParams = localSymbolMap.getNumParams();
683 | if (numParams != parameters.length) {
684 | return true;
685 | }
686 |
687 | for (int i = 0; i < numParams; i++) {
688 | HighSymbol param = localSymbolMap.getParamSymbol(i);
689 | if (param.getCategoryIndex() != i) {
690 | return true;
691 | }
692 | VariableStorage storage = param.getStorage();
693 | // Don't compare using the equals method so that DynamicVariableStorage can match
694 | if (0 != storage.compareTo(parameters[i].getVariableStorage())) {
695 | return true;
696 | }
697 | }
698 |
699 | return false;
700 | }
701 |
702 | // ----------------------------------------------------------------------------------
703 | // New methods to implement the new functionalities
704 | // ----------------------------------------------------------------------------------
705 |
706 | /**
707 | * Get function by address
708 | */
709 | private String getFunctionByAddress(String addressStr) {
710 | Program program = getCurrentProgram();
711 | if (program == null) return "No program loaded";
712 | if (addressStr == null || addressStr.isEmpty()) return "Address is required";
713 |
714 | try {
715 | Address addr = program.getAddressFactory().getAddress(addressStr);
716 | Function func = program.getFunctionManager().getFunctionAt(addr);
717 |
718 | if (func == null) return "No function found at address " + addressStr;
719 |
720 | return String.format("Function: %s at %s\nSignature: %s\nEntry: %s\nBody: %s - %s",
721 | func.getName(),
722 | func.getEntryPoint(),
723 | func.getSignature(),
724 | func.getEntryPoint(),
725 | func.getBody().getMinAddress(),
726 | func.getBody().getMaxAddress());
727 | } catch (Exception e) {
728 | return "Error getting function: " + e.getMessage();
729 | }
730 | }
731 |
732 | /**
733 | * Get current address selected in Ghidra GUI
734 | */
735 | private String getCurrentAddress() {
736 | CodeViewerService service = tool.getService(CodeViewerService.class);
737 | if (service == null) return "Code viewer service not available";
738 |
739 | ProgramLocation location = service.getCurrentLocation();
740 | return (location != null) ? location.getAddress().toString() : "No current location";
741 | }
742 |
743 | /**
744 | * Get current function selected in Ghidra GUI
745 | */
746 | private String getCurrentFunction() {
747 | CodeViewerService service = tool.getService(CodeViewerService.class);
748 | if (service == null) return "Code viewer service not available";
749 |
750 | ProgramLocation location = service.getCurrentLocation();
751 | if (location == null) return "No current location";
752 |
753 | Program program = getCurrentProgram();
754 | if (program == null) return "No program loaded";
755 |
756 | Function func = program.getFunctionManager().getFunctionContaining(location.getAddress());
757 | if (func == null) return "No function at current location: " + location.getAddress();
758 |
759 | return String.format("Function: %s at %s\nSignature: %s",
760 | func.getName(),
761 | func.getEntryPoint(),
762 | func.getSignature());
763 | }
764 |
765 | /**
766 | * List all functions in the database
767 | */
768 | private String listFunctions() {
769 | Program program = getCurrentProgram();
770 | if (program == null) return "No program loaded";
771 |
772 | StringBuilder result = new StringBuilder();
773 | for (Function func : program.getFunctionManager().getFunctions(true)) {
774 | result.append(String.format("%s at %s\n",
775 | func.getName(),
776 | func.getEntryPoint()));
777 | }
778 |
779 | return result.toString();
780 | }
781 |
782 | /**
783 | * Gets a function at the given address or containing the address
784 | * @return the function or null if not found
785 | */
786 | private Function getFunctionForAddress(Program program, Address addr) {
787 | Function func = program.getFunctionManager().getFunctionAt(addr);
788 | if (func == null) {
789 | func = program.getFunctionManager().getFunctionContaining(addr);
790 | }
791 | return func;
792 | }
793 |
794 | /**
795 | * Decompile a function at the given address
796 | */
797 | private String decompileFunctionByAddress(String addressStr) {
798 | Program program = getCurrentProgram();
799 | if (program == null) return "No program loaded";
800 | if (addressStr == null || addressStr.isEmpty()) return "Address is required";
801 |
802 | try {
803 | Address addr = program.getAddressFactory().getAddress(addressStr);
804 | Function func = getFunctionForAddress(program, addr);
805 | if (func == null) return "No function found at or containing address " + addressStr;
806 |
807 | DecompInterface decomp = new DecompInterface();
808 | decomp.openProgram(program);
809 | DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
810 |
811 | return (result != null && result.decompileCompleted())
812 | ? result.getDecompiledFunction().getC()
813 | : "Decompilation failed";
814 | } catch (Exception e) {
815 | return "Error decompiling function: " + e.getMessage();
816 | }
817 | }
818 |
819 | /**
820 | * Get assembly code for a function
821 | */
822 | private String disassembleFunction(String addressStr) {
823 | Program program = getCurrentProgram();
824 | if (program == null) return "No program loaded";
825 | if (addressStr == null || addressStr.isEmpty()) return "Address is required";
826 |
827 | try {
828 | Address addr = program.getAddressFactory().getAddress(addressStr);
829 | Function func = getFunctionForAddress(program, addr);
830 | if (func == null) return "No function found at or containing address " + addressStr;
831 |
832 | StringBuilder result = new StringBuilder();
833 | Listing listing = program.getListing();
834 | Address start = func.getEntryPoint();
835 | Address end = func.getBody().getMaxAddress();
836 |
837 | InstructionIterator instructions = listing.getInstructions(start, true);
838 | while (instructions.hasNext()) {
839 | Instruction instr = instructions.next();
840 | if (instr.getAddress().compareTo(end) > 0) {
841 | break; // Stop if we've gone past the end of the function
842 | }
843 | String comment = listing.getComment(CodeUnit.EOL_COMMENT, instr.getAddress());
844 | comment = (comment != null) ? "; " + comment : "";
845 |
846 | result.append(String.format("%s: %s %s\n",
847 | instr.getAddress(),
848 | instr.toString(),
849 | comment));
850 | }
851 |
852 | return result.toString();
853 | } catch (Exception e) {
854 | return "Error disassembling function: " + e.getMessage();
855 | }
856 | }
857 |
858 | /**
859 | * Set a comment using the specified comment type (PRE_COMMENT or EOL_COMMENT)
860 | */
861 | private boolean setCommentAtAddress(String addressStr, String comment, int commentType, String transactionName) {
862 | Program program = getCurrentProgram();
863 | if (program == null) return false;
864 | if (addressStr == null || addressStr.isEmpty() || comment == null) return false;
865 |
866 | AtomicBoolean success = new AtomicBoolean(false);
867 |
868 | try {
869 | SwingUtilities.invokeAndWait(() -> {
870 | int tx = program.startTransaction(transactionName);
871 | try {
872 | Address addr = program.getAddressFactory().getAddress(addressStr);
873 | program.getListing().setComment(addr, commentType, comment);
874 | success.set(true);
875 | } catch (Exception e) {
876 | Msg.error(this, "Error setting " + transactionName.toLowerCase(), e);
877 | } finally {
878 | program.endTransaction(tx, success.get());
879 | }
880 | });
881 | } catch (InterruptedException | InvocationTargetException e) {
882 | Msg.error(this, "Failed to execute " + transactionName.toLowerCase() + " on Swing thread", e);
883 | }
884 |
885 | return success.get();
886 | }
887 |
888 | /**
889 | * Set a comment for a given address in the function pseudocode
890 | */
891 | private boolean setDecompilerComment(String addressStr, String comment) {
892 | return setCommentAtAddress(addressStr, comment, CodeUnit.PRE_COMMENT, "Set decompiler comment");
893 | }
894 |
895 | /**
896 | * Set a comment for a given address in the function disassembly
897 | */
898 | private boolean setDisassemblyComment(String addressStr, String comment) {
899 | return setCommentAtAddress(addressStr, comment, CodeUnit.EOL_COMMENT, "Set disassembly comment");
900 | }
901 |
902 | /**
903 | * Class to hold the result of a prototype setting operation
904 | */
905 | private static class PrototypeResult {
906 | private final boolean success;
907 | private final String errorMessage;
908 |
909 | public PrototypeResult(boolean success, String errorMessage) {
910 | this.success = success;
911 | this.errorMessage = errorMessage;
912 | }
913 |
914 | public boolean isSuccess() {
915 | return success;
916 | }
917 |
918 | public String getErrorMessage() {
919 | return errorMessage;
920 | }
921 | }
922 |
923 | /**
924 | * Rename a function by its address
925 | */
926 | private boolean renameFunctionByAddress(String functionAddrStr, String newName) {
927 | Program program = getCurrentProgram();
928 | if (program == null) return false;
929 | if (functionAddrStr == null || functionAddrStr.isEmpty() ||
930 | newName == null || newName.isEmpty()) {
931 | return false;
932 | }
933 |
934 | AtomicBoolean success = new AtomicBoolean(false);
935 |
936 | try {
937 | SwingUtilities.invokeAndWait(() -> {
938 | performFunctionRename(program, functionAddrStr, newName, success);
939 | });
940 | } catch (InterruptedException | InvocationTargetException e) {
941 | Msg.error(this, "Failed to execute rename function on Swing thread", e);
942 | }
943 |
944 | return success.get();
945 | }
946 |
947 | /**
948 | * Helper method to perform the actual function rename within a transaction
949 | */
950 | private void performFunctionRename(Program program, String functionAddrStr, String newName, AtomicBoolean success) {
951 | int tx = program.startTransaction("Rename function by address");
952 | try {
953 | Address addr = program.getAddressFactory().getAddress(functionAddrStr);
954 | Function func = getFunctionForAddress(program, addr);
955 |
956 | if (func == null) {
957 | Msg.error(this, "Could not find function at address: " + functionAddrStr);
958 | return;
959 | }
960 |
961 | func.setName(newName, SourceType.USER_DEFINED);
962 | success.set(true);
963 | } catch (Exception e) {
964 | Msg.error(this, "Error renaming function by address", e);
965 | } finally {
966 | program.endTransaction(tx, success.get());
967 | }
968 | }
969 |
970 | /**
971 | * Set a function's prototype with proper error handling using ApplyFunctionSignatureCmd
972 | */
973 | private PrototypeResult setFunctionPrototype(String functionAddrStr, String prototype) {
974 | // Input validation
975 | Program program = getCurrentProgram();
976 | if (program == null) return new PrototypeResult(false, "No program loaded");
977 | if (functionAddrStr == null || functionAddrStr.isEmpty()) {
978 | return new PrototypeResult(false, "Function address is required");
979 | }
980 | if (prototype == null || prototype.isEmpty()) {
981 | return new PrototypeResult(false, "Function prototype is required");
982 | }
983 |
984 | final StringBuilder errorMessage = new StringBuilder();
985 | final AtomicBoolean success = new AtomicBoolean(false);
986 |
987 | try {
988 | SwingUtilities.invokeAndWait(() ->
989 | applyFunctionPrototype(program, functionAddrStr, prototype, success, errorMessage));
990 | } catch (InterruptedException | InvocationTargetException e) {
991 | String msg = "Failed to set function prototype on Swing thread: " + e.getMessage();
992 | errorMessage.append(msg);
993 | Msg.error(this, msg, e);
994 | }
995 |
996 | return new PrototypeResult(success.get(), errorMessage.toString());
997 | }
998 |
999 | /**
1000 | * Helper method that applies the function prototype within a transaction
1001 | */
1002 | private void applyFunctionPrototype(Program program, String functionAddrStr, String prototype,
1003 | AtomicBoolean success, StringBuilder errorMessage) {
1004 | try {
1005 | // Get the address and function
1006 | Address addr = program.getAddressFactory().getAddress(functionAddrStr);
1007 | Function func = getFunctionForAddress(program, addr);
1008 |
1009 | if (func == null) {
1010 | String msg = "Could not find function at address: " + functionAddrStr;
1011 | errorMessage.append(msg);
1012 | Msg.error(this, msg);
1013 | return;
1014 | }
1015 |
1016 | Msg.info(this, "Setting prototype for function " + func.getName() + ": " + prototype);
1017 |
1018 | // Store original prototype as a comment for reference
1019 | addPrototypeComment(program, func, prototype);
1020 |
1021 | // Use ApplyFunctionSignatureCmd to parse and apply the signature
1022 | parseFunctionSignatureAndApply(program, addr, prototype, success, errorMessage);
1023 |
1024 | } catch (Exception e) {
1025 | String msg = "Error setting function prototype: " + e.getMessage();
1026 | errorMessage.append(msg);
1027 | Msg.error(this, msg, e);
1028 | }
1029 | }
1030 |
1031 | /**
1032 | * Add a comment showing the prototype being set
1033 | */
1034 | private void addPrototypeComment(Program program, Function func, String prototype) {
1035 | int txComment = program.startTransaction("Add prototype comment");
1036 | try {
1037 | program.getListing().setComment(
1038 | func.getEntryPoint(),
1039 | CodeUnit.PLATE_COMMENT,
1040 | "Setting prototype: " + prototype
1041 | );
1042 | } finally {
1043 | program.endTransaction(txComment, true);
1044 | }
1045 | }
1046 |
1047 | /**
1048 | * Parse and apply the function signature with error handling
1049 | */
1050 | private void parseFunctionSignatureAndApply(Program program, Address addr, String prototype,
1051 | AtomicBoolean success, StringBuilder errorMessage) {
1052 | // Use ApplyFunctionSignatureCmd to parse and apply the signature
1053 | int txProto = program.startTransaction("Set function prototype");
1054 | try {
1055 | // Get data type manager
1056 | DataTypeManager dtm = program.getDataTypeManager();
1057 |
1058 | // Get data type manager service
1059 | ghidra.app.services.DataTypeManagerService dtms =
1060 | tool.getService(ghidra.app.services.DataTypeManagerService.class);
1061 |
1062 | // Create function signature parser
1063 | ghidra.app.util.parser.FunctionSignatureParser parser =
1064 | new ghidra.app.util.parser.FunctionSignatureParser(dtm, dtms);
1065 |
1066 | // Parse the prototype into a function signature
1067 | ghidra.program.model.data.FunctionDefinitionDataType sig = parser.parse(null, prototype);
1068 |
1069 | if (sig == null) {
1070 | String msg = "Failed to parse function prototype";
1071 | errorMessage.append(msg);
1072 | Msg.error(this, msg);
1073 | return;
1074 | }
1075 |
1076 | // Create and apply the command
1077 | ghidra.app.cmd.function.ApplyFunctionSignatureCmd cmd =
1078 | new ghidra.app.cmd.function.ApplyFunctionSignatureCmd(
1079 | addr, sig, SourceType.USER_DEFINED);
1080 |
1081 | // Apply the command to the program
1082 | boolean cmdResult = cmd.applyTo(program, new ConsoleTaskMonitor());
1083 |
1084 | if (cmdResult) {
1085 | success.set(true);
1086 | Msg.info(this, "Successfully applied function signature");
1087 | } else {
1088 | String msg = "Command failed: " + cmd.getStatusMsg();
1089 | errorMessage.append(msg);
1090 | Msg.error(this, msg);
1091 | }
1092 | } catch (Exception e) {
1093 | String msg = "Error applying function signature: " + e.getMessage();
1094 | errorMessage.append(msg);
1095 | Msg.error(this, msg, e);
1096 | } finally {
1097 | program.endTransaction(txProto, success.get());
1098 | }
1099 | }
1100 |
1101 | /**
1102 | * Set a local variable's type using HighFunctionDBUtil.updateDBVariable
1103 | */
1104 | private boolean setLocalVariableType(String functionAddrStr, String variableName, String newType) {
1105 | // Input validation
1106 | Program program = getCurrentProgram();
1107 | if (program == null) return false;
1108 | if (functionAddrStr == null || functionAddrStr.isEmpty() ||
1109 | variableName == null || variableName.isEmpty() ||
1110 | newType == null || newType.isEmpty()) {
1111 | return false;
1112 | }
1113 |
1114 | AtomicBoolean success = new AtomicBoolean(false);
1115 |
1116 | try {
1117 | SwingUtilities.invokeAndWait(() ->
1118 | applyVariableType(program, functionAddrStr, variableName, newType, success));
1119 | } catch (InterruptedException | InvocationTargetException e) {
1120 | Msg.error(this, "Failed to execute set variable type on Swing thread", e);
1121 | }
1122 |
1123 | return success.get();
1124 | }
1125 |
1126 | /**
1127 | * Helper method that performs the actual variable type change
1128 | */
1129 | private void applyVariableType(Program program, String functionAddrStr,
1130 | String variableName, String newType, AtomicBoolean success) {
1131 | try {
1132 | // Find the function
1133 | Address addr = program.getAddressFactory().getAddress(functionAddrStr);
1134 | Function func = getFunctionForAddress(program, addr);
1135 |
1136 | if (func == null) {
1137 | Msg.error(this, "Could not find function at address: " + functionAddrStr);
1138 | return;
1139 | }
1140 |
1141 | DecompileResults results = decompileFunction(func, program);
1142 | if (results == null || !results.decompileCompleted()) {
1143 | return;
1144 | }
1145 |
1146 | ghidra.program.model.pcode.HighFunction highFunction = results.getHighFunction();
1147 | if (highFunction == null) {
1148 | Msg.error(this, "No high function available");
1149 | return;
1150 | }
1151 |
1152 | // Find the symbol by name
1153 | HighSymbol symbol = findSymbolByName(highFunction, variableName);
1154 | if (symbol == null) {
1155 | Msg.error(this, "Could not find variable '" + variableName + "' in decompiled function");
1156 | return;
1157 | }
1158 |
1159 | // Get high variable
1160 | HighVariable highVar = symbol.getHighVariable();
1161 | if (highVar == null) {
1162 | Msg.error(this, "No HighVariable found for symbol: " + variableName);
1163 | return;
1164 | }
1165 |
1166 | Msg.info(this, "Found high variable for: " + variableName +
1167 | " with current type " + highVar.getDataType().getName());
1168 |
1169 | // Find the data type
1170 | DataTypeManager dtm = program.getDataTypeManager();
1171 | DataType dataType = resolveDataType(dtm, newType);
1172 |
1173 | if (dataType == null) {
1174 | Msg.error(this, "Could not resolve data type: " + newType);
1175 | return;
1176 | }
1177 |
1178 | Msg.info(this, "Using data type: " + dataType.getName() + " for variable " + variableName);
1179 |
1180 | // Apply the type change in a transaction
1181 | updateVariableType(program, symbol, dataType, success);
1182 |
1183 | } catch (Exception e) {
1184 | Msg.error(this, "Error setting variable type: " + e.getMessage());
1185 | }
1186 | }
1187 |
1188 | /**
1189 | * Find a high symbol by name in the given high function
1190 | */
1191 | private HighSymbol findSymbolByName(ghidra.program.model.pcode.HighFunction highFunction, String variableName) {
1192 | Iterator symbols = highFunction.getLocalSymbolMap().getSymbols();
1193 | while (symbols.hasNext()) {
1194 | HighSymbol s = symbols.next();
1195 | if (s.getName().equals(variableName)) {
1196 | return s;
1197 | }
1198 | }
1199 | return null;
1200 | }
1201 |
1202 | /**
1203 | * Decompile a function and return the results
1204 | */
1205 | private DecompileResults decompileFunction(Function func, Program program) {
1206 | // Set up decompiler for accessing the decompiled function
1207 | DecompInterface decomp = new DecompInterface();
1208 | decomp.openProgram(program);
1209 | decomp.setSimplificationStyle("decompile"); // Full decompilation
1210 |
1211 | // Decompile the function
1212 | DecompileResults results = decomp.decompileFunction(func, 60, new ConsoleTaskMonitor());
1213 |
1214 | if (!results.decompileCompleted()) {
1215 | Msg.error(this, "Could not decompile function: " + results.getErrorMessage());
1216 | return null;
1217 | }
1218 |
1219 | return results;
1220 | }
1221 |
1222 | /**
1223 | * Apply the type update in a transaction
1224 | */
1225 | private void updateVariableType(Program program, HighSymbol symbol, DataType dataType, AtomicBoolean success) {
1226 | int tx = program.startTransaction("Set variable type");
1227 | try {
1228 | // Use HighFunctionDBUtil to update the variable with the new type
1229 | HighFunctionDBUtil.updateDBVariable(
1230 | symbol, // The high symbol to modify
1231 | symbol.getName(), // Keep original name
1232 | dataType, // The new data type
1233 | SourceType.USER_DEFINED // Mark as user-defined
1234 | );
1235 |
1236 | success.set(true);
1237 | Msg.info(this, "Successfully set variable type using HighFunctionDBUtil");
1238 | } catch (Exception e) {
1239 | Msg.error(this, "Error setting variable type: " + e.getMessage());
1240 | } finally {
1241 | program.endTransaction(tx, success.get());
1242 | }
1243 | }
1244 |
1245 | /**
1246 | * Get all references to a specific address (xref to)
1247 | */
1248 | private String getXrefsTo(String addressStr, int offset, int limit) {
1249 | Program program = getCurrentProgram();
1250 | if (program == null) return "No program loaded";
1251 | if (addressStr == null || addressStr.isEmpty()) return "Address is required";
1252 |
1253 | try {
1254 | Address addr = program.getAddressFactory().getAddress(addressStr);
1255 | ReferenceManager refManager = program.getReferenceManager();
1256 |
1257 | ReferenceIterator refIter = refManager.getReferencesTo(addr);
1258 |
1259 | List refs = new ArrayList<>();
1260 | while (refIter.hasNext()) {
1261 | Reference ref = refIter.next();
1262 | Address fromAddr = ref.getFromAddress();
1263 | RefType refType = ref.getReferenceType();
1264 |
1265 | Function fromFunc = program.getFunctionManager().getFunctionContaining(fromAddr);
1266 | String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : "";
1267 |
1268 | refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName()));
1269 | }
1270 |
1271 | return paginateList(refs, offset, limit);
1272 | } catch (Exception e) {
1273 | return "Error getting references to address: " + e.getMessage();
1274 | }
1275 | }
1276 |
1277 | /**
1278 | * Get all references from a specific address (xref from)
1279 | */
1280 | private String getXrefsFrom(String addressStr, int offset, int limit) {
1281 | Program program = getCurrentProgram();
1282 | if (program == null) return "No program loaded";
1283 | if (addressStr == null || addressStr.isEmpty()) return "Address is required";
1284 |
1285 | try {
1286 | Address addr = program.getAddressFactory().getAddress(addressStr);
1287 | ReferenceManager refManager = program.getReferenceManager();
1288 |
1289 | Reference[] references = refManager.getReferencesFrom(addr);
1290 |
1291 | List refs = new ArrayList<>();
1292 | for (Reference ref : references) {
1293 | Address toAddr = ref.getToAddress();
1294 | RefType refType = ref.getReferenceType();
1295 |
1296 | String targetInfo = "";
1297 | Function toFunc = program.getFunctionManager().getFunctionAt(toAddr);
1298 | if (toFunc != null) {
1299 | targetInfo = " to function " + toFunc.getName();
1300 | } else {
1301 | Data data = program.getListing().getDataAt(toAddr);
1302 | if (data != null) {
1303 | targetInfo = " to data " + (data.getLabel() != null ? data.getLabel() : data.getPathName());
1304 | }
1305 | }
1306 |
1307 | refs.add(String.format("To %s%s [%s]", toAddr, targetInfo, refType.getName()));
1308 | }
1309 |
1310 | return paginateList(refs, offset, limit);
1311 | } catch (Exception e) {
1312 | return "Error getting references from address: " + e.getMessage();
1313 | }
1314 | }
1315 |
1316 | /**
1317 | * Get all references to a specific function by name
1318 | */
1319 | private String getFunctionXrefs(String functionName, int offset, int limit) {
1320 | Program program = getCurrentProgram();
1321 | if (program == null) return "No program loaded";
1322 | if (functionName == null || functionName.isEmpty()) return "Function name is required";
1323 |
1324 | try {
1325 | List refs = new ArrayList<>();
1326 | FunctionManager funcManager = program.getFunctionManager();
1327 | for (Function function : funcManager.getFunctions(true)) {
1328 | if (function.getName().equals(functionName)) {
1329 | Address entryPoint = function.getEntryPoint();
1330 | ReferenceIterator refIter = program.getReferenceManager().getReferencesTo(entryPoint);
1331 |
1332 | while (refIter.hasNext()) {
1333 | Reference ref = refIter.next();
1334 | Address fromAddr = ref.getFromAddress();
1335 | RefType refType = ref.getReferenceType();
1336 |
1337 | Function fromFunc = funcManager.getFunctionContaining(fromAddr);
1338 | String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : "";
1339 |
1340 | refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName()));
1341 | }
1342 | }
1343 | }
1344 |
1345 | if (refs.isEmpty()) {
1346 | return "No references found to function: " + functionName;
1347 | }
1348 |
1349 | return paginateList(refs, offset, limit);
1350 | } catch (Exception e) {
1351 | return "Error getting function references: " + e.getMessage();
1352 | }
1353 | }
1354 |
1355 | /**
1356 | * List all defined strings in the program with their addresses
1357 | */
1358 | private String listDefinedStrings(int offset, int limit, String filter) {
1359 | Program program = getCurrentProgram();
1360 | if (program == null) return "No program loaded";
1361 |
1362 | List lines = new ArrayList<>();
1363 | DataIterator dataIt = program.getListing().getDefinedData(true);
1364 |
1365 | while (dataIt.hasNext()) {
1366 | Data data = dataIt.next();
1367 |
1368 | if (data != null && isStringData(data)) {
1369 | String value = data.getValue() != null ? data.getValue().toString() : "";
1370 |
1371 | if (filter == null || value.toLowerCase().contains(filter.toLowerCase())) {
1372 | String escapedValue = escapeString(value);
1373 | lines.add(String.format("%s: \"%s\"", data.getAddress(), escapedValue));
1374 | }
1375 | }
1376 | }
1377 |
1378 | return paginateList(lines, offset, limit);
1379 | }
1380 |
1381 | /**
1382 | * Check if the given data is a string type
1383 | */
1384 | private boolean isStringData(Data data) {
1385 | if (data == null) return false;
1386 |
1387 | DataType dt = data.getDataType();
1388 | String typeName = dt.getName().toLowerCase();
1389 | return typeName.contains("string") || typeName.contains("char") || typeName.equals("unicode");
1390 | }
1391 |
1392 | /**
1393 | * Escape special characters in a string for display
1394 | */
1395 | private String escapeString(String input) {
1396 | if (input == null) return "";
1397 |
1398 | StringBuilder sb = new StringBuilder();
1399 | for (int i = 0; i < input.length(); i++) {
1400 | char c = input.charAt(i);
1401 | if (c >= 32 && c < 127) {
1402 | sb.append(c);
1403 | } else if (c == '\n') {
1404 | sb.append("\\n");
1405 | } else if (c == '\r') {
1406 | sb.append("\\r");
1407 | } else if (c == '\t') {
1408 | sb.append("\\t");
1409 | } else {
1410 | sb.append(String.format("\\x%02x", (int)c & 0xFF));
1411 | }
1412 | }
1413 | return sb.toString();
1414 | }
1415 |
1416 | /**
1417 | * Resolves a data type by name, handling common types and pointer types
1418 | * @param dtm The data type manager
1419 | * @param typeName The type name to resolve
1420 | * @return The resolved DataType, or null if not found
1421 | */
1422 | private DataType resolveDataType(DataTypeManager dtm, String typeName) {
1423 | // First try to find exact match in all categories
1424 | DataType dataType = findDataTypeByNameInAllCategories(dtm, typeName);
1425 | if (dataType != null) {
1426 | Msg.info(this, "Found exact data type match: " + dataType.getPathName());
1427 | return dataType;
1428 | }
1429 |
1430 | // Check for Windows-style pointer types (PXXX)
1431 | if (typeName.startsWith("P") && typeName.length() > 1) {
1432 | String baseTypeName = typeName.substring(1);
1433 |
1434 | // Special case for PVOID
1435 | if (baseTypeName.equals("VOID")) {
1436 | return new PointerDataType(dtm.getDataType("/void"));
1437 | }
1438 |
1439 | // Try to find the base type
1440 | DataType baseType = findDataTypeByNameInAllCategories(dtm, baseTypeName);
1441 | if (baseType != null) {
1442 | return new PointerDataType(baseType);
1443 | }
1444 |
1445 | Msg.warn(this, "Base type not found for " + typeName + ", defaulting to void*");
1446 | return new PointerDataType(dtm.getDataType("/void"));
1447 | }
1448 |
1449 | // Handle common built-in types
1450 | switch (typeName.toLowerCase()) {
1451 | case "int":
1452 | case "long":
1453 | return dtm.getDataType("/int");
1454 | case "uint":
1455 | case "unsigned int":
1456 | case "unsigned long":
1457 | case "dword":
1458 | return dtm.getDataType("/uint");
1459 | case "short":
1460 | return dtm.getDataType("/short");
1461 | case "ushort":
1462 | case "unsigned short":
1463 | case "word":
1464 | return dtm.getDataType("/ushort");
1465 | case "char":
1466 | case "byte":
1467 | return dtm.getDataType("/char");
1468 | case "uchar":
1469 | case "unsigned char":
1470 | return dtm.getDataType("/uchar");
1471 | case "longlong":
1472 | case "__int64":
1473 | return dtm.getDataType("/longlong");
1474 | case "ulonglong":
1475 | case "unsigned __int64":
1476 | return dtm.getDataType("/ulonglong");
1477 | case "bool":
1478 | case "boolean":
1479 | return dtm.getDataType("/bool");
1480 | case "void":
1481 | return dtm.getDataType("/void");
1482 | default:
1483 | // Try as a direct path
1484 | DataType directType = dtm.getDataType("/" + typeName);
1485 | if (directType != null) {
1486 | return directType;
1487 | }
1488 |
1489 | // Fallback to int if we couldn't find it
1490 | Msg.warn(this, "Unknown type: " + typeName + ", defaulting to int");
1491 | return dtm.getDataType("/int");
1492 | }
1493 | }
1494 |
1495 | /**
1496 | * Find a data type by name in all categories/folders of the data type manager
1497 | * This searches through all categories rather than just the root
1498 | */
1499 | private DataType findDataTypeByNameInAllCategories(DataTypeManager dtm, String typeName) {
1500 | // Try exact match first
1501 | DataType result = searchByNameInAllCategories(dtm, typeName);
1502 | if (result != null) {
1503 | return result;
1504 | }
1505 |
1506 | // Try lowercase
1507 | return searchByNameInAllCategories(dtm, typeName.toLowerCase());
1508 | }
1509 |
1510 | /**
1511 | * Helper method to search for a data type by name in all categories
1512 | */
1513 | private DataType searchByNameInAllCategories(DataTypeManager dtm, String name) {
1514 | // Get all data types from the manager
1515 | Iterator allTypes = dtm.getAllDataTypes();
1516 | while (allTypes.hasNext()) {
1517 | DataType dt = allTypes.next();
1518 | // Check if the name matches exactly (case-sensitive)
1519 | if (dt.getName().equals(name)) {
1520 | return dt;
1521 | }
1522 | // For case-insensitive, we want an exact match except for case
1523 | if (dt.getName().equalsIgnoreCase(name)) {
1524 | return dt;
1525 | }
1526 | }
1527 | return null;
1528 | }
1529 |
1530 | // ----------------------------------------------------------------------------------
1531 | // Utility: parse query params, parse post params, pagination, etc.
1532 | // ----------------------------------------------------------------------------------
1533 |
1534 | /**
1535 | * Parse query parameters from the URL, e.g. ?offset=10&limit=100
1536 | */
1537 | private Map parseQueryParams(HttpExchange exchange) {
1538 | Map result = new HashMap<>();
1539 | String query = exchange.getRequestURI().getQuery(); // e.g. offset=10&limit=100
1540 | if (query != null) {
1541 | String[] pairs = query.split("&");
1542 | for (String p : pairs) {
1543 | String[] kv = p.split("=");
1544 | if (kv.length == 2) {
1545 | // URL decode parameter values
1546 | try {
1547 | String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8);
1548 | String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8);
1549 | result.put(key, value);
1550 | } catch (Exception e) {
1551 | Msg.error(this, "Error decoding URL parameter", e);
1552 | }
1553 | }
1554 | }
1555 | }
1556 | return result;
1557 | }
1558 |
1559 | /**
1560 | * Parse post body form params, e.g. oldName=foo&newName=bar
1561 | */
1562 | private Map parsePostParams(HttpExchange exchange) throws IOException {
1563 | byte[] body = exchange.getRequestBody().readAllBytes();
1564 | String bodyStr = new String(body, StandardCharsets.UTF_8);
1565 | Map params = new HashMap<>();
1566 | for (String pair : bodyStr.split("&")) {
1567 | String[] kv = pair.split("=");
1568 | if (kv.length == 2) {
1569 | // URL decode parameter values
1570 | try {
1571 | String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8);
1572 | String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8);
1573 | params.put(key, value);
1574 | } catch (Exception e) {
1575 | Msg.error(this, "Error decoding URL parameter", e);
1576 | }
1577 | }
1578 | }
1579 | return params;
1580 | }
1581 |
1582 | /**
1583 | * Convert a list of strings into one big newline-delimited string, applying offset & limit.
1584 | */
1585 | private String paginateList(List items, int offset, int limit) {
1586 | int start = Math.max(0, offset);
1587 | int end = Math.min(items.size(), offset + limit);
1588 |
1589 | if (start >= items.size()) {
1590 | return ""; // no items in range
1591 | }
1592 | List sub = items.subList(start, end);
1593 | return String.join("\n", sub);
1594 | }
1595 |
1596 | /**
1597 | * Parse an integer from a string, or return defaultValue if null/invalid.
1598 | */
1599 | private int parseIntOrDefault(String val, int defaultValue) {
1600 | if (val == null) return defaultValue;
1601 | try {
1602 | return Integer.parseInt(val);
1603 | }
1604 | catch (NumberFormatException e) {
1605 | return defaultValue;
1606 | }
1607 | }
1608 |
1609 | /**
1610 | * Escape non-ASCII chars to avoid potential decode issues.
1611 | */
1612 | private String escapeNonAscii(String input) {
1613 | if (input == null) return "";
1614 | StringBuilder sb = new StringBuilder();
1615 | for (char c : input.toCharArray()) {
1616 | if (c >= 32 && c < 127) {
1617 | sb.append(c);
1618 | }
1619 | else {
1620 | sb.append("\\x");
1621 | sb.append(Integer.toHexString(c & 0xFF));
1622 | }
1623 | }
1624 | return sb.toString();
1625 | }
1626 |
1627 | public Program getCurrentProgram() {
1628 | ProgramManager pm = tool.getService(ProgramManager.class);
1629 | return pm != null ? pm.getCurrentProgram() : null;
1630 | }
1631 |
1632 | private void sendResponse(HttpExchange exchange, String response) throws IOException {
1633 | byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
1634 | exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
1635 | exchange.sendResponseHeaders(200, bytes.length);
1636 | try (OutputStream os = exchange.getResponseBody()) {
1637 | os.write(bytes);
1638 | }
1639 | }
1640 |
1641 | @Override
1642 | public void dispose() {
1643 | if (server != null) {
1644 | Msg.info(this, "Stopping GhidraMCP HTTP server...");
1645 | server.stop(1); // Stop with a small delay (e.g., 1 second) for connections to finish
1646 | server = null; // Nullify the reference
1647 | Msg.info(this, "GhidraMCP HTTP server stopped.");
1648 | }
1649 | super.dispose();
1650 | }
1651 | }
1652 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/MANIFEST.MF:
--------------------------------------------------------------------------------
1 | Manifest-Version: 1.0
2 | Plugin-Class: com.lauriewired.GhidraMCP
3 | Plugin-Name: GhidraMCP
4 | Plugin-Version: 1.0
5 | Plugin-Author: LaurieWired
6 | Plugin-Description: A custom plugin by LaurieWired
7 |
--------------------------------------------------------------------------------
/src/main/resources/Module.manifest:
--------------------------------------------------------------------------------
1 | GHIDRA_MODULE_NAME=GhidraMCP
2 | GHIDRA_MODULE_DESC=An HTTP server plugin for Ghidra
3 |
--------------------------------------------------------------------------------
/src/main/resources/extension.properties:
--------------------------------------------------------------------------------
1 | name=GhidraMCP
2 | description=A plugin that runs an embedded HTTP server to expose program data.
3 | author=LaurieWired
4 | createdOn=2025-03-22
5 | version=11.3.1
6 | ghidraVersion=11.3.1
--------------------------------------------------------------------------------
/src/test/java/com/lauriewired/AppTest.java:
--------------------------------------------------------------------------------
1 | package com.lauriewired;
2 |
3 | import junit.framework.Test;
4 | import junit.framework.TestCase;
5 | import junit.framework.TestSuite;
6 |
7 | /**
8 | * Unit test for simple App.
9 | */
10 | public class AppTest
11 | extends TestCase
12 | {
13 | /**
14 | * Create the test case
15 | *
16 | * @param testName name of the test case
17 | */
18 | public AppTest( String testName )
19 | {
20 | super( testName );
21 | }
22 |
23 | /**
24 | * @return the suite of tests being tested
25 | */
26 | public static Test suite()
27 | {
28 | return new TestSuite( AppTest.class );
29 | }
30 |
31 | /**
32 | * Rigourous Test :-)
33 | */
34 | public void testApp()
35 | {
36 | assertTrue( true );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------