More MCP work
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,3 +51,4 @@ GPF-output/**
|
|||||||
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.clangd-query.pid
|
.clangd-query.pid
|
||||||
|
.clangd-query/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "UObject/Interface.h"
|
||||||
#include "UObject/Object.h"
|
#include "UObject/Object.h"
|
||||||
#include "Dom/JsonObject.h"
|
#include "Dom/JsonObject.h"
|
||||||
#include "MCPHandler.generated.h"
|
#include "MCPHandler.generated.h"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "Async/Future.h"
|
||||||
#include "Dom/JsonObject.h"
|
#include "Dom/JsonObject.h"
|
||||||
#include "MCPUtils.h"
|
#include "MCPUtils.h"
|
||||||
class FSocket;
|
class FSocket;
|
||||||
|
|||||||
10
build.py
10
build.py
@@ -323,6 +323,7 @@ def build_compile_commands_from_integration():
|
|||||||
if len(clangs) != 1: sys.exit("Couldn't identify correct clang++ compiler in UnrealEngine thirdparty directory")
|
if len(clangs) != 1: sys.exit("Couldn't identify correct clang++ compiler in UnrealEngine thirdparty directory")
|
||||||
clang = str(clangs[0])
|
clang = str(clangs[0])
|
||||||
# Build the table of source files and RSP files.
|
# Build the table of source files and RSP files.
|
||||||
|
# First, scan unity Module.*.o.rsp files and expand their #included .cpp files.
|
||||||
mods1 = Path(f"{INTEGRATION}/Intermediate/Build/{OS}").rglob(f"UnrealEditor/{DEBUG}/**/Module.*.o.rsp")
|
mods1 = Path(f"{INTEGRATION}/Intermediate/Build/{OS}").rglob(f"UnrealEditor/{DEBUG}/**/Module.*.o.rsp")
|
||||||
mods1p = Path(f"{INTEGRATION}/Plugins").rglob(f"Intermediate/Build/{OS}/x64/UnrealEditor/{DEBUG}/**/Module.*.o.rsp")
|
mods1p = Path(f"{INTEGRATION}/Plugins").rglob(f"Intermediate/Build/{OS}/x64/UnrealEditor/{DEBUG}/**/Module.*.o.rsp")
|
||||||
mods2 = Path(f"{UNREALENGINE}/Engine/Intermediate/Build/{OS}").rglob("UnrealEditor/Development/**/Module.*.o.rsp")
|
mods2 = Path(f"{UNREALENGINE}/Engine/Intermediate/Build/{OS}").rglob("UnrealEditor/Development/**/Module.*.o.rsp")
|
||||||
@@ -334,6 +335,15 @@ def build_compile_commands_from_integration():
|
|||||||
for subfile in cpp_files_included_by(cpp):
|
for subfile in cpp_files_included_by(cpp):
|
||||||
abs = os.path.abspath(os.path.join(f"{UNREALENGINE}/Engine/Source", subfile))
|
abs = os.path.abspath(os.path.join(f"{UNREALENGINE}/Engine/Source", subfile))
|
||||||
cpp_to_rsp[abs] = rsp
|
cpp_to_rsp[abs] = rsp
|
||||||
|
# Also pick up non-unity .cpp files that have their own .o.rsp (e.g. files
|
||||||
|
# excluded from unity builds). Only include if the .cpp file actually exists.
|
||||||
|
standalone1 = Path(f"{INTEGRATION}/Intermediate/Build/{OS}").rglob(f"UnrealEditor/{DEBUG}/**/*.cpp.o.rsp")
|
||||||
|
standalone1p = Path(f"{INTEGRATION}/Plugins").rglob(f"Intermediate/Build/{OS}/x64/UnrealEditor/{DEBUG}/**/*.cpp.o.rsp")
|
||||||
|
for rsp_path in itertools.chain(standalone1, standalone1p):
|
||||||
|
rsp = str(rsp_path)
|
||||||
|
cpp = os.path.abspath(rsp.removesuffix(".o.rsp"))
|
||||||
|
if cpp not in cpp_to_rsp and os.path.exists(cpp):
|
||||||
|
cpp_to_rsp[cpp] = rsp
|
||||||
# Generate compile commands.
|
# Generate compile commands.
|
||||||
entries = []
|
entries = []
|
||||||
ccdir = f"{UNREALENGINE}/Engine/Source"
|
ccdir = f"{UNREALENGINE}/Engine/Source"
|
||||||
|
|||||||
@@ -149,6 +149,22 @@ class ClangdDaemon:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
clangd_path, clangd_args = read_workspace_config()
|
clangd_path, clangd_args = read_workspace_config()
|
||||||
|
|
||||||
|
# Give the daemon its own compile-commands dir so it gets a separate
|
||||||
|
# clangd index cache (clangd stores .cache/ next to compile_commands.json).
|
||||||
|
daemon_cc_dir = os.path.join(PROJECT_DIR, ".clangd-query")
|
||||||
|
os.makedirs(daemon_cc_dir, exist_ok=True)
|
||||||
|
cc_link = os.path.join(daemon_cc_dir, "compile_commands.json")
|
||||||
|
cc_src = os.path.join(PROJECT_DIR, ".vscode", "compile_commands.json")
|
||||||
|
if os.path.islink(cc_link) or os.path.exists(cc_link):
|
||||||
|
os.unlink(cc_link)
|
||||||
|
os.symlink(cc_src, cc_link)
|
||||||
|
|
||||||
|
# Replace --compile-commands-dir in args to point to our dir
|
||||||
|
clangd_args = [
|
||||||
|
f"--compile-commands-dir={daemon_cc_dir}" if a.startswith("--compile-commands-dir") else a
|
||||||
|
for a in clangd_args
|
||||||
|
]
|
||||||
cmd = [clangd_path] + clangd_args + ["-j=4"]
|
cmd = [clangd_path] + clangd_args + ["-j=4"]
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
@@ -164,6 +180,8 @@ class ClangdDaemon:
|
|||||||
self.events = {}
|
self.events = {}
|
||||||
self.index_loaded = threading.Event()
|
self.index_loaded = threading.Event()
|
||||||
self.opened_files = set()
|
self.opened_files = set()
|
||||||
|
self.diagnostics = {} # uri -> list of diagnostics
|
||||||
|
self.diag_events = {} # uri -> threading.Event
|
||||||
|
|
||||||
threading.Thread(target=self._stdout_reader, daemon=True).start()
|
threading.Thread(target=self._stdout_reader, daemon=True).start()
|
||||||
threading.Thread(target=self._stderr_reader, daemon=True).start()
|
threading.Thread(target=self._stderr_reader, daemon=True).start()
|
||||||
@@ -173,6 +191,14 @@ class ClangdDaemon:
|
|||||||
msg = read_lsp_message(self.proc.stdout)
|
msg = read_lsp_message(self.proc.stdout)
|
||||||
if msg is None:
|
if msg is None:
|
||||||
break
|
break
|
||||||
|
# Handle server-initiated diagnostics notifications
|
||||||
|
if msg.get("method") == "textDocument/publishDiagnostics":
|
||||||
|
uri = msg["params"]["uri"]
|
||||||
|
with self.lock:
|
||||||
|
self.diagnostics[uri] = msg["params"]["diagnostics"]
|
||||||
|
if uri in self.diag_events:
|
||||||
|
self.diag_events[uri].set()
|
||||||
|
continue
|
||||||
msg_id = msg.get("id")
|
msg_id = msg.get("id")
|
||||||
if msg_id is not None and msg_id in self.pending:
|
if msg_id is not None and msg_id in self.pending:
|
||||||
with self.lock:
|
with self.lock:
|
||||||
@@ -241,6 +267,20 @@ class ClangdDaemon:
|
|||||||
resp = self.send_request("workspace/symbol", {"query": args[0]})
|
resp = self.send_request("workspace/symbol", {"query": args[0]})
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
if cmd == "diagnostics":
|
||||||
|
filepath = args[0]
|
||||||
|
abs_path = os.path.abspath(filepath)
|
||||||
|
uri = f"file://{abs_path}"
|
||||||
|
# Set up event before opening so we don't miss the notification
|
||||||
|
with self.lock:
|
||||||
|
self.diag_events[uri] = threading.Event()
|
||||||
|
self.open_file(abs_path)
|
||||||
|
self.diag_events[uri].wait(timeout=30)
|
||||||
|
with self.lock:
|
||||||
|
diags = self.diagnostics.get(uri, [])
|
||||||
|
self.diag_events.pop(uri, None)
|
||||||
|
return {"result": diags}
|
||||||
|
|
||||||
if cmd == "definition":
|
if cmd == "definition":
|
||||||
filepath, line, col = args[0], int(args[1]), int(args[2])
|
filepath, line, col = args[0], int(args[1]), int(args[2])
|
||||||
abs_path = os.path.abspath(filepath)
|
abs_path = os.path.abspath(filepath)
|
||||||
@@ -408,15 +448,27 @@ def start_daemon():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def send_query(query):
|
def send_query(query, retries=3):
|
||||||
"""Send a query to the daemon and return the response."""
|
"""Send a query to the daemon and return the response."""
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
for attempt in range(retries):
|
||||||
sock.settimeout(60)
|
try:
|
||||||
sock.connect(SOCKET_PATH)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
sock_send(sock, query)
|
sock.settimeout(60)
|
||||||
result = sock_recv(sock)
|
sock.connect(SOCKET_PATH)
|
||||||
sock.close()
|
sock_send(sock, query)
|
||||||
return result
|
result = sock_recv(sock)
|
||||||
|
sock.close()
|
||||||
|
return result
|
||||||
|
except (ConnectionResetError, ConnectionRefusedError, BrokenPipeError, OSError):
|
||||||
|
sock.close()
|
||||||
|
if attempt < retries - 1:
|
||||||
|
import time
|
||||||
|
time.sleep(2)
|
||||||
|
# Daemon may have died — restart it
|
||||||
|
if not daemon_is_alive():
|
||||||
|
start_daemon()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def ensure_daemon():
|
def ensure_daemon():
|
||||||
@@ -444,6 +496,22 @@ def format_symbol_results(resp):
|
|||||||
print(f"{path}:{line} [{kind}] {qualified}")
|
print(f"{path}:{line} [{kind}] {qualified}")
|
||||||
|
|
||||||
|
|
||||||
|
def format_diagnostics(resp):
|
||||||
|
if not resp or "result" not in resp:
|
||||||
|
print("No diagnostics (or clangd error).", file=sys.stderr)
|
||||||
|
return
|
||||||
|
diags = resp["result"]
|
||||||
|
if not diags:
|
||||||
|
print("No problems found.")
|
||||||
|
return
|
||||||
|
for d in diags:
|
||||||
|
line = d.get("range", {}).get("start", {}).get("line", 0) + 1
|
||||||
|
col = d.get("range", {}).get("start", {}).get("character", 0) + 1
|
||||||
|
severity = {1: "error", 2: "warning", 3: "info", 4: "hint"}.get(d.get("severity", 0), "unknown")
|
||||||
|
msg = d.get("message", "")
|
||||||
|
print(f" {line}:{col} [{severity}] {msg}")
|
||||||
|
|
||||||
|
|
||||||
def format_location_results(resp, label="results"):
|
def format_location_results(resp, label="results"):
|
||||||
if not resp or "result" not in resp:
|
if not resp or "result" not in resp:
|
||||||
print(f"No {label} (or clangd error).", file=sys.stderr)
|
print(f"No {label} (or clangd error).", file=sys.stderr)
|
||||||
@@ -485,6 +553,10 @@ def main():
|
|||||||
if len(args) < 1:
|
if len(args) < 1:
|
||||||
print("Usage: clangd-query.py symbol <name>", file=sys.stderr)
|
print("Usage: clangd-query.py symbol <name>", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
elif command == "diagnostics":
|
||||||
|
if len(args) < 1:
|
||||||
|
print("Usage: clangd-query.py diagnostics <file>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
elif command in ("definition", "references"):
|
elif command in ("definition", "references"):
|
||||||
if len(args) < 3:
|
if len(args) < 3:
|
||||||
print(f"Usage: clangd-query.py {command} <file> <line> <col>",
|
print(f"Usage: clangd-query.py {command} <file> <line> <col>",
|
||||||
@@ -500,6 +572,8 @@ def main():
|
|||||||
|
|
||||||
if command == "symbol":
|
if command == "symbol":
|
||||||
format_symbol_results(resp)
|
format_symbol_results(resp)
|
||||||
|
elif command == "diagnostics":
|
||||||
|
format_diagnostics(resp)
|
||||||
elif command == "definition":
|
elif command == "definition":
|
||||||
format_location_results(resp, "definition")
|
format_location_results(resp, "definition")
|
||||||
elif command == "references":
|
elif command == "references":
|
||||||
|
|||||||
Reference in New Issue
Block a user