From 244e0d65f5c739bc0037ba1fb5da03f56d1c851a Mon Sep 17 00:00:00 2001 From: andev0x Date: Tue, 23 Jun 2026 23:18:40 +0700 Subject: [PATCH 1/6] feat(cli): add lea tools to command - update hook.go to include new lea tools in tool detection logic - modify install/install.go to use updated tool names in instructions - adjust server.go to register lea variants of existing tools --- internal/cli/commands/hook.go | 2 +- internal/mcp/install/install.go | 8 ++++---- internal/mcp/server.go | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/cli/commands/hook.go b/internal/cli/commands/hook.go index 85cc2de..ee1c6f7 100644 --- a/internal/cli/commands/hook.go +++ b/internal/cli/commands/hook.go @@ -72,7 +72,7 @@ var hookPreToolCmd = &cobra.Command{ isLeaTool = true } else { // Fallback: check if the tool name matches one of our MCP tools - leaTools := []string{"impact", "flow", "neighbors", "violations", "symbols", "view_symbol_ast", "trace_execution_path"} + leaTools := []string{"impact", "flow", "neighbors", "violations", "symbols", "lea_view_symbol_ast", "lea_trace_execution_path", "lea_find_neighbors", "lea_trace_calls", "lea_find_architecture_violations"} for _, t := range leaTools { if lowerName == t || strings.HasSuffix(lowerName, "__"+t) || strings.HasSuffix(lowerName, "/"+t) { isLeaTool = true diff --git a/internal/mcp/install/install.go b/internal/mcp/install/install.go index fcdfca5..9283bc3 100644 --- a/internal/mcp/install/install.go +++ b/internal/mcp/install/install.go @@ -665,10 +665,10 @@ func writeTOML(path string, v map[string]any) error { func writeInstructions(path string) error { content := `# MANDATORY WORKSPACE TOOL SELECTION RULES -This repository utilizes custom AST-aware and graph-indexed tools (search_graph, resolve_symbol, view_symbol_ast, trace_execution_path). These are highly optimized for PizenLabs' internal architecture. +This repository utilizes custom AST-aware and graph-indexed tools (lynx_search_graph, lynx_resolve_symbol, lea_view_symbol_ast, lea_trace_execution_path). These are highly optimized for PizenLabs' internal architecture. ## 1. Tool Priority Queue (Strict Compliance) -1. **FIRST PRIORITY (Structure & Lookups):** Always invoke search_graph and view_symbol_ast directly when searching for code, analyzing failing tests, or viewing context. +1. **FIRST PRIORITY (Structure & Lookups):** Always invoke lynx_search_graph and lea_view_symbol_ast directly when searching for code, analyzing failing tests, or viewing context. 2. **LAST RESORT (Fallback Only):** You may only use generic fallback tools (like codebase-memory-mcp_*, grep, or glob) if and only if the primary tools yield zero results. ## 2. Enforcement @@ -688,10 +688,10 @@ func writeInstructionsFile(t target) error { path := filepath.Join(t.ConfigDir, t.InstructionFile) content := `# MANDATORY WORKSPACE TOOL SELECTION RULES -This repository utilizes custom AST-aware and graph-indexed tools (search_graph, resolve_symbol, view_symbol_ast, trace_execution_path). These are highly optimized for PizenLabs' internal architecture. +This repository utilizes custom AST-aware and graph-indexed tools (lynx_search_graph, lynx_resolve_symbol, lea_view_symbol_ast, lea_trace_execution_path). These are highly optimized for PizenLabs' internal architecture. ## 1. Tool Priority Queue (Strict Compliance) -1. **FIRST PRIORITY (Structure & Lookups):** Always invoke search_graph and view_symbol_ast directly when searching for code, analyzing failing tests, or viewing context. +1. **FIRST PRIORITY (Structure & Lookups):** Always invoke lynx_search_graph and lea_view_symbol_ast directly when searching for code, analyzing failing tests, or viewing context. 2. **LAST RESORT (Fallback Only):** You may only use generic fallback tools (like codebase-memory-mcp_*, grep, or glob) if and only if the primary tools yield zero results. ## 2. Enforcement diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 0aaa03c..b0e29a3 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -132,7 +132,7 @@ func (s *Server) registerTools(server *mcp_golang.Server) error { // registerGetSymbolContextTool registers the symbol context compiler tool. func (s *Server) registerGetSymbolContextTool(server *mcp_golang.Server) error { return server.RegisterTool( - "view_symbol_ast", + "lea_view_symbol_ast", "Generates AI-optimized markdown context for a symbol", func( ctx context.Context, @@ -152,7 +152,7 @@ func (s *Server) registerGetSymbolContextTool(server *mcp_golang.Server) error { // registerFindNeighborsTool registers the structural neighbor lookup tool. func (s *Server) registerFindNeighborsTool(server *mcp_golang.Server) error { return server.RegisterTool( - "find_neighbors", + "lea_find_neighbors", "Finds symbols directly connected to a symbol", func( ctx context.Context, @@ -172,7 +172,7 @@ func (s *Server) registerFindNeighborsTool(server *mcp_golang.Server) error { // registerTraceCallsTool registers the recursive call graph tracing tool. func (s *Server) registerTraceCallsTool(server *mcp_golang.Server) error { return server.RegisterTool( - "trace_calls", + "lea_trace_calls", "Traces the call graph starting from a symbol", func( ctx context.Context, @@ -205,7 +205,7 @@ func (s *Server) registerTraceCallsTool(server *mcp_golang.Server) error { // registerTraceExecutionPathTool registers the ordered execution flow tool. func (s *Server) registerTraceExecutionPathTool(server *mcp_golang.Server) error { return server.RegisterTool( - "trace_execution_path", + "lea_trace_execution_path", "Returns ordered control-flow traversal for a symbol", func( ctx context.Context, @@ -225,7 +225,7 @@ func (s *Server) registerTraceExecutionPathTool(server *mcp_golang.Server) error // registerArchitectureViolationsTool registers the architecture validation tool. func (s *Server) registerArchitectureViolationsTool(server *mcp_golang.Server) error { return server.RegisterTool( - "find_architecture_violations", + "lea_find_architecture_violations", "Detects architecture boundary violations", func( ctx context.Context, From bda9de1d6607df2041fa01c434966d3b1c54cf75 Mon Sep 17 00:00:00 2001 From: andev0x Date: Wed, 24 Jun 2026 03:03:53 +0700 Subject: [PATCH 2/6] build: add mcp bridge script - update .gitignore to include __pycache__ and pyc files - refactor server.go to use registration structs and closures for tool registration - add mcp-bridge.py to handle double-prefixed method names for OpenCode compatibility --- .gitignore | 2 + internal/mcp/server.go | 59 ++-- scripts/mcp-bridge.py | 116 ++++++++ scripts/mcp-bridge.sh | 95 ++++++ scripts/mcp_bridge_test_suite.py | 428 ++++++++++++++++++++++++++++ scripts/opencode-bridge-config.json | 38 +++ 6 files changed, 713 insertions(+), 25 deletions(-) create mode 100755 scripts/mcp-bridge.py create mode 100755 scripts/mcp-bridge.sh create mode 100755 scripts/mcp_bridge_test_suite.py create mode 100644 scripts/opencode-bridge-config.json diff --git a/.gitignore b/.gitignore index 5db36aa..067d048 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ bin/ lea .pi/ +**/__pycache__/ +*.pyc # OS generated files diff --git a/internal/mcp/server.go b/internal/mcp/server.go index b0e29a3..1a78757 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -106,19 +106,27 @@ func (s *Server) Start() error { select {} } -// registerTools registers every MCP tool exposed by lea. +// registerTools registers every MCP tool exposed by lea, plus aliased +// double-prefix variants for OpenCode transport compatibility. func (s *Server) registerTools(server *mcp_golang.Server) error { - tools := []func(*mcp_golang.Server) error{ - s.registerGetSymbolContextTool, - s.registerFindNeighborsTool, - s.registerTraceCallsTool, - s.registerTraceExecutionPathTool, - s.registerArchitectureViolationsTool, + type registration struct { + register func(*mcp_golang.Server, string) error + names []string } - for _, register := range tools { - if err := register(server); err != nil { - return err + regs := []registration{ + {s.registerGetSymbolContext, []string{"lea_view_symbol_ast", "lea_lea_view_symbol_ast"}}, + {s.registerFindNeighbors, []string{"lea_find_neighbors", "lea_lea_find_neighbors"}}, + {s.registerTraceCalls, []string{"lea_trace_calls", "lea_lea_trace_calls"}}, + {s.registerTraceExecutionPath, []string{"lea_trace_execution_path", "lea_lea_trace_execution_path"}}, + {s.registerArchitectureViolations, []string{"lea_find_architecture_violations", "lea_lea_find_architecture_violations"}}, + } + + for _, r := range regs { + for _, name := range r.names { + if err := r.register(server, name); err != nil { + return err + } } } @@ -129,10 +137,10 @@ func (s *Server) registerTools(server *mcp_golang.Server) error { // Tool Registration // ----------------------------------------------------------------------------- -// registerGetSymbolContextTool registers the symbol context compiler tool. -func (s *Server) registerGetSymbolContextTool(server *mcp_golang.Server) error { +// registerGetSymbolContext creates a tool registration closure for the given name. +func (s *Server) registerGetSymbolContext(server *mcp_golang.Server, toolName string) error { return server.RegisterTool( - "lea_view_symbol_ast", + toolName, "Generates AI-optimized markdown context for a symbol", func( ctx context.Context, @@ -149,10 +157,10 @@ func (s *Server) registerGetSymbolContextTool(server *mcp_golang.Server) error { ) } -// registerFindNeighborsTool registers the structural neighbor lookup tool. -func (s *Server) registerFindNeighborsTool(server *mcp_golang.Server) error { +// registerFindNeighbors creates a tool registration closure for the given name. +func (s *Server) registerFindNeighbors(server *mcp_golang.Server, toolName string) error { return server.RegisterTool( - "lea_find_neighbors", + toolName, "Finds symbols directly connected to a symbol", func( ctx context.Context, @@ -169,10 +177,10 @@ func (s *Server) registerFindNeighborsTool(server *mcp_golang.Server) error { ) } -// registerTraceCallsTool registers the recursive call graph tracing tool. -func (s *Server) registerTraceCallsTool(server *mcp_golang.Server) error { +// registerTraceCalls creates a tool registration closure for the given name. +func (s *Server) registerTraceCalls(server *mcp_golang.Server, toolName string) error { return server.RegisterTool( - "lea_trace_calls", + toolName, "Traces the call graph starting from a symbol", func( ctx context.Context, @@ -202,10 +210,10 @@ func (s *Server) registerTraceCallsTool(server *mcp_golang.Server) error { ) } -// registerTraceExecutionPathTool registers the ordered execution flow tool. -func (s *Server) registerTraceExecutionPathTool(server *mcp_golang.Server) error { +// registerTraceExecutionPath creates a tool registration closure for the given name. +func (s *Server) registerTraceExecutionPath(server *mcp_golang.Server, toolName string) error { return server.RegisterTool( - "lea_trace_execution_path", + toolName, "Returns ordered control-flow traversal for a symbol", func( ctx context.Context, @@ -222,10 +230,11 @@ func (s *Server) registerTraceExecutionPathTool(server *mcp_golang.Server) error ) } -// registerArchitectureViolationsTool registers the architecture validation tool. -func (s *Server) registerArchitectureViolationsTool(server *mcp_golang.Server) error { +// registerArchitectureViolations creates a tool registration closure for the +// given name. +func (s *Server) registerArchitectureViolations(server *mcp_golang.Server, toolName string) error { return server.RegisterTool( - "lea_find_architecture_violations", + toolName, "Detects architecture boundary violations", func( ctx context.Context, diff --git a/scripts/mcp-bridge.py b/scripts/mcp-bridge.py new file mode 100755 index 0000000..a183def --- /dev/null +++ b/scripts/mcp-bridge.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +mcp-bridge.py — MCP Transport Bridge (Option A) + +Intercepts JSON-RPC 2.0 messages between OpenCode and MCP servers (lea/lx), +remapping double-prefixed method names before forwarding to the native binary. + +Prefix mapping: + lea_lea_view_symbol_ast -> lea_view_symbol_ast + lea_lea_find_neighbors -> lea_find_neighbors + lynx_lynx_search_graph -> lynx_search_graph + lynx_lynx_resolve_symbol -> lynx_resolve_symbol + +Usage: ./mcp-bridge.py [args...] + +OpenCode config entry (~/.opencode/settings.json): + "mcp": { + "pizen-lea-bridged": { + "command": ["/path/to/mcp-bridge.py", "/path/to/lea", "mcp"], + "enabled": true + } + } +""" +import json +import os +import subprocess +import sys +import threading + + +def remap_method(method: str) -> str: + if method.startswith("lea_lea_"): + return method[4:] # lea_lea_X -> lea_X + if method.startswith("lynx_lynx_"): + return method[5:] # lynx_lynx_X -> lynx_X + if method.startswith("pizen_lea_lea_"): + return "lea_" + method[len("pizen_lea_lea_"):] + if method.startswith("pizen_lynx_lynx_"): + return "lynx_" + method[len("pizen_lynx_lynx_"):] + return method + + +def main(): + if len(sys.argv) < 2: + print("Usage: mcp-bridge.py [args...]", file=sys.stderr) + sys.exit(1) + + binary = sys.argv[1] + args = sys.argv[2:] + + if not os.access(binary, os.X_OK): + print(f"mcp-bridge: ERROR: binary not executable: {binary}", file=sys.stderr) + sys.exit(1) + + proc = subprocess.Popen( + [binary] + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=0, + ) + + def forward_stderr(): + for line in iter(proc.stderr.readline, b""): + sys.stderr.buffer.write(line) + sys.stderr.buffer.flush() + + stderr_thread = threading.Thread(target=forward_stderr, daemon=True) + stderr_thread.start() + + def pipe_stdin_to_native(): + try: + for raw_line in sys.stdin.buffer: + line = raw_line.decode("utf-8", errors="replace").strip() + if not line: + continue + + try: + msg = json.loads(line) + except json.JSONDecodeError: + proc.stdin.write(raw_line) + proc.stdin.flush() + continue + + if "method" in msg: + original = msg["method"] + remapped = remap_method(original) + if remapped != original: + msg["method"] = remapped + + proc.stdin.write((json.dumps(msg) + "\n").encode()) + proc.stdin.flush() + except BrokenPipeError: + pass + finally: + proc.stdin.close() + + stdin_thread = threading.Thread(target=pipe_stdin_to_native, daemon=True) + stdin_thread.start() + + def pipe_native_to_stdout(): + try: + for line in iter(proc.stdout.readline, b""): + sys.stdout.buffer.write(line) + sys.stdout.buffer.flush() + except BrokenPipeError: + pass + + pipe_native_to_stdout() + + stdin_thread.join(timeout=2) + proc.wait() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/mcp-bridge.sh b/scripts/mcp-bridge.sh new file mode 100755 index 0000000..7109e64 --- /dev/null +++ b/scripts/mcp-bridge.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# ============================================================================ +# mcp-bridge.sh — MCP Transport Bridge (Option A) +# +# Sits between the OpenCode client and native MCP servers (lea/lx), +# intercepting JSON-RPC 2.0 messages over stdio and remapping method names +# by stripping the redundant double-prefix before forwarding to the binary. +# +# Prefix mapping: +# lea_lea_view_symbol_ast -> lea_view_symbol_ast +# lea_lea_find_neighbors -> lea_find_neighbors +# lynx_lynx_search_graph -> lynx_search_graph +# lynx_lynx_resolve_symbol -> lynx_resolve_symbol +# +# Usage: ./mcp-bridge.sh [args...] +# +# Example (opencode.json entry): +# "pizen-lea-bridged": { +# "command": ["/path/to/mcp-bridge.sh", "/path/to/lea", "mcp"], +# "enabled": true +# } +# ============================================================================ +set -euo pipefail + +BINARY="$1" +shift + +if [ ! -x "$BINARY" ]; then + echo "mcp-bridge: ERROR: binary not found or not executable: $BINARY" >&2 + exit 1 +fi + +# Create temp directory for named pipes +TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/mcp-bridge.XXXXXXXX") +trap 'rm -rf "$TMPDIR"; kill "$NATIVE_PID" 2>/dev/null || true' EXIT INT TERM + +NATIVE_IN="$TMPDIR/native_in" +NATIVE_OUT="$TMPDIR/native_out" + +mkfifo "$NATIVE_IN" "$NATIVE_OUT" + +# Launch native binary with piped stdio +"$BINARY" "$@" < "$NATIVE_IN" > "$NATIVE_OUT" & +NATIVE_PID=$! + +# Open FIFOs for reading/writing in background +exec 3>"$NATIVE_IN" # write to native stdin +exec 4<"$NATIVE_OUT" # read from native stdout + +# --- stdin relay: remap methods and forward to native --- +stdin_relay() { + while IFS= read -r line; do + [ -z "$line" ] && continue + + # Use python3 for JSON-aware method remapping + if remapped=$(echo "$line" | python3 -c " +import json, sys +try: + d = json.loads(sys.stdin.read()) + if 'method' in d: + m = d['method'] + if m.startswith('lea_lea_'): + d['method'] = m[4:] + elif m.startswith('lynx_lynx_'): + d['method'] = m[5:] + elif m.startswith('pizen_lea_lea_'): + d['method'] = 'lea_' + m[len('pizen_lea_lea_'):] + elif m.startswith('pizen_lynx_lynx_'): + d['method'] = 'lynx_' + m[len('pizen_lynx_lynx_'):] + sys.stdout.write(json.dumps(d) + '\n') +except Exception: + sys.stdout.write(line + '\n') +" 2>/dev/null); then + echo "$remapped" >&3 + else + echo "$line" >&3 + fi + done + exec 3>&- +} + +# --- stdout relay: forward native responses to stdout --- +stdout_relay() { + while IFS= read -r line <&4; do + echo "$line" + done + exec 4<&- +} + +# Run both relays in parallel +stdin_relay & +RELAY_PID=$! +stdout_relay + +wait "$RELAY_PID" 2>/dev/null || true \ No newline at end of file diff --git a/scripts/mcp_bridge_test_suite.py b/scripts/mcp_bridge_test_suite.py new file mode 100755 index 0000000..7adb601 --- /dev/null +++ b/scripts/mcp_bridge_test_suite.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +""" +MCP Transport Bridge Diagnostic & Simulation Test Suite +======================================================== +Simulates the full OpenCode -> MCP server pipeline, detects naming +prefix mismatches, and validates the bridge fix. + +Usage: + python3 scripts/mcp_bridge_test_suite.py # run all tests + python3 scripts/mcp_bridge_test_suite.py --wrap # test via bash wrapper bridge + python3 scripts/mcp_bridge_test_suite.py --native # test native tool methods only +""" + +import io +import json +import subprocess +import sys +import time +import os +import shutil +import select +import threading + +select_func = select.select +PASS = "\033[92mPASS\033[0m" +FAIL = "\033[91mFAIL\033[0m" +SKIP = "\033[93mSKIP\033[0m" +BOLD = "\033[1m" +RESET = "\033[0m" + +LX_BIN = shutil.which("lx") or os.path.expanduser("~/.cargo/bin/lx") +LEA_BIN = shutil.which("lea") or os.path.expanduser("~/go/bin/lea") + +TESTS_RUN = 0 +TESTS_PASSED = 0 +TESTS_FAILED = 0 + + +def test(name, passed, detail=""): + global TESTS_RUN, TESTS_PASSED, TESTS_FAILED + TESTS_RUN += 1 + status = PASS if passed else FAIL + if passed: + TESTS_PASSED += 1 + else: + TESTS_FAILED += 1 + icon = "\u2713" if passed else "\u2717" + print(f" {icon} {status} | {name}" + (f" ({detail})" if detail else "")) + + +class MCPClient: + """A minimal MCP client that speaks JSON-RPC 2.0 over stdio.""" + + def __init__(self, cmd): + self.proc = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self._queue = [] + self._reader_active = True + self._reader_thread = threading.Thread(target=self._read_loop, daemon=True) + self._reader_thread.start() + + def _read_loop(self): + buf = b"" + while self._reader_active and self.proc.poll() is None: + r, _, _ = select_func([self.proc.stdout], [], [], 0.1) + if r: + chunk = os.read(self.proc.stdout.fileno(), 8192) + if not chunk: + break + buf += chunk + while b"\n" in buf: + line, buf = buf.split(b"\n", 1) + if line.strip(): + try: + self._queue.append(json.loads(line.decode())) + except json.JSONDecodeError: + self._queue.append({"raw": line.decode()}) + elif self.proc.poll() is not None: + break + + def send(self, msg): + data = json.dumps(msg) + "\n" + self.proc.stdin.write(data.encode()) + self.proc.stdin.flush() + + def recv(self, timeout=5): + start = time.time() + while time.time() - start < timeout: + if self._queue: + return self._queue.pop(0) + time.sleep(0.05) + return None + + def handshake(self): + self.send({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "mcp-test-suite", "version": "1.0"}, + }, + }) + resp = self.recv() + self.send({"jsonrpc": "2.0", "method": "notifications/initialized"}) + return bool(resp) + + def list_tools(self): + self.send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}) + resp = self.recv() + if resp and "result" in resp and isinstance(resp["result"], dict) and "tools" in resp["result"]: + return [t["name"] for t in resp["result"]["tools"]] + return [] + + def call_tool(self, method, params, rid=10, use_direct=True): + if use_direct: + self.send({"jsonrpc": "2.0", "id": rid, "method": method, "params": params}) + else: + self.send({"jsonrpc": "2.0", "id": rid, "method": "tools/call", "params": {"name": method, "arguments": params}}) + resp = self.recv() + return resp + + def close(self): + self._reader_active = False + self.proc.terminate() + self.proc.wait() + + +def section(title): + print(f"\n{BOLD}{'=' * 60}{RESET}") + print(f"{BOLD} {title}{RESET}") + print(f"{BOLD}{'=' * 60}{RESET}") + + +# ============================================================================= +# SECTION 1: Native tool method registration discovery +# ============================================================================= +def test_native_registration(server_name, bin_path, expected_tools): + section(f"Phase 1: Native Registration Discovery ({server_name})") + + if not os.path.exists(bin_path): + test(f"Binary found at {bin_path}", False, "not found") + return [] + + client = MCPClient([bin_path, "mcp"]) + client.handshake() + tools = client.list_tools() + client.close() + + test(f"tools/list returns {len(tools)} tools", len(tools) > 0) + for name in expected_tools: + test(f"Expected tool '{name}' registered", name in tools) + + for t in tools: + is_expected = t in expected_tools + test(f" Registered: '{t}'", is_expected, "expected" if is_expected else "UNEXPECTED") + + return tools + + +# ============================================================================= +# SECTION 2: Method resolution accuracy +# ============================================================================= +def test_method_resolution(server_name, bin_path, method, params, should_succeed=True, use_direct=True): + client = MCPClient([bin_path, "mcp"]) + client.handshake() + + label = f"call '{method}' on {server_name}" + resp = client.call_tool(method, params, use_direct=use_direct) + client.close() + + if resp is None: + test(label, False, "no response") + return False + + if "error" in resp: + code = resp["error"].get("code", "?") + msg = resp["error"].get("message", "?") + test(label, not should_succeed, f"error {code}: {msg}") + return False if should_succeed else True + else: + test(label, should_succeed, f"result received") + return True if should_succeed else False + + +# ============================================================================= +# SECTION 3: Simulated OpenCode transport (prefix injection / stripping) +# ============================================================================= +def test_opencode_wrapping(): + section("Phase 3: Simulated OpenCode Prefix Wrap Test") + + test_cases = [ + ("lea", "lea_view_symbol_ast", {"symbol_id": "test:sym"}), + ("lea", "lea_find_neighbors", {"symbol_id": "test:sym"}), + ("lea", "lea_trace_calls", {"symbol_id": "test:sym", "depth": 1}), + ("lea", "lea_trace_execution_path", {"symbol_id": "test:sym"}), + ("lea", "lea_find_architecture_violations", {"symbol_id": "test:sym"}), + ("lx", "lynx_search_graph", {"query": "test"}), + ("lx", "lynx_resolve_symbol", {"name": "test"}), + ("lx", "lynx_find_related", {"file": "test.go", "line": 1}), + ] + + for server_bin_name, method, params in test_cases: + bin_path = LEA_BIN if server_bin_name == "lea" else LX_BIN + if not os.path.exists(bin_path): + test(f"[{server_bin_name}] {method}", False, f"{bin_path} not found") + continue + + client = MCPClient([bin_path, "mcp"]) + client.handshake() + + use_direct = server_bin_name == "lx" + resp = client.call_tool(method, params, use_direct=use_direct) + client.close() + + if resp and "error" in resp: + test(f"[{server_bin_name}] native method '{method}'", False, + f"error: {resp['error'].get('message', '?')}") + else: + has_content = resp and resp.get("result") is not None + test(f"[{server_bin_name}] native method '{method}'", True, + "responded OK" if has_content else "responded (no error)") + + +# ============================================================================= +# SECTION 4: Bash Wrapper Bridge Test (Option A) +# ============================================================================= +def test_bash_wrapper_bridge(): + section("Phase 4: Bash Wrapper Bridge (Option A)") + + wrapper_script = shutil.which("mcp-bridge") or shutil.which("./scripts/mcp-bridge.py") + if not wrapper_script: + wrapper_script = "./scripts/mcp-bridge.py" + + if not os.path.exists(wrapper_script): + test(f"Bridge script '{wrapper_script}' found", False, "not found - skipping") + return + + for server_bin_name, method, params, expected_native in [ + ("lea", "lea_lea_view_symbol_ast", {"symbol_id": "test:sym"}, "lea_view_symbol_ast"), + ("lea", "lea_lea_find_neighbors", {"symbol_id": "test:sym"}, "lea_find_neighbors"), + ("lea", "lea_lea_trace_calls", {"symbol_id": "test:sym", "depth": 1}, "lea_trace_calls"), + ("lx", "lynx_lynx_search_graph", {"query": "test"}, "lynx_search_graph"), + ("lx", "lynx_lynx_resolve_symbol", {"name": "test"}, "lynx_resolve_symbol"), + ("lx", "lynx_lynx_find_related", {"file": "test.go", "line": 1}, "lynx_find_related"), + ]: + bin_path = LEA_BIN if server_bin_name == "lea" else LX_BIN + if not os.path.exists(bin_path): + test(f"[bridge] {method}", False, f"{bin_path} not found") + continue + + client = MCPClient([wrapper_script, bin_path, "mcp"]) + client.handshake() + + use_direct = server_bin_name == "lx" + resp = client.call_tool(method, params, use_direct=use_direct) + client.close() + + if resp is None: + test(f"[bridge] '{method}' -> '{expected_native}'", False, "no response") + elif "error" in resp: + test(f"[bridge] '{method}' -> '{expected_native}'", False, + f"error: {resp['error'].get('message', '?')}") + else: + test(f"[bridge] '{method}' -> '{expected_native}'", True) + + +# ============================================================================= +# SECTION 5: End-to-end integration matrix +# ============================================================================= +def test_integration_matrix(): + section("Phase 5: End-to-End Integration Matrix") + + scenarios = [ + {"label": "lx native: lynx_search_graph", "bin": LX_BIN, "method": "lynx_search_graph", "params": {"query": "test"}, "expect": "ok"}, + {"label": "lx native: lynx_resolve_symbol","bin": LX_BIN, "method": "lynx_resolve_symbol", "params": {"name": "test"}, "expect": "ok"}, + {"label": "lx native: lynx_find_related", "bin": LX_BIN, "method": "lynx_find_related", "params": {"file": "test.go", "line": 1}, "expect": "ok"}, + {"label": "lx wrapped: lynx_lynx_* → lynx_*","bin": LX_BIN, "method": "lynx_lynx_search_graph","params": {"query": "test"}, "expect": "error"}, + {"label": "lea native: lea_view_symbol_ast","bin": LEA_BIN, "method": "lea_view_symbol_ast", "params": {"symbol_id": "test:sym"}, "expect": "ok"}, + {"label": "lea native: lea_find_neighbors", "bin": LEA_BIN, "method": "lea_find_neighbors", "params": {"symbol_id": "test:sym"}, "expect": "ok"}, + {"label": "lea wrapped: lea_lea_* → lea_*", "bin": LEA_BIN, "method": "lea_lea_view_symbol_ast","params": {"symbol_id": "test:sym"}, "expect": "ok"}, + {"label": "lea wrapped: lea_lea_find_neighbors","bin": LEA_BIN, "method": "lea_lea_find_neighbors","params": {"symbol_id": "test:sym"}, "expect": "ok"}, + ] + + for s in scenarios: + if not os.path.exists(s["bin"]): + test(s["label"], False, f"{s['bin']} not found") + continue + client = MCPClient([s["bin"], "mcp"]) + client.handshake() + use_direct = "lx" in s["label"].lower() + resp = client.call_tool(s["method"], s["params"], use_direct=use_direct) + client.close() + + has_error = resp is None or "error" in resp + if s["expect"] == "ok": + test(s["label"], not has_error, "responded OK" if not has_error else f"ERROR: {resp.get('error',{}).get('message','?') if resp else 'no resp'}") + else: + test(s["label"], has_error, f"correctly rejected" if has_error else "should have errored") + + +# ============================================================================= +# SECTION 6: Transport channel validation +# ============================================================================= +def test_transport_validation(): + section("Phase 6: Transport Channel Validation (prefix stripping simulation)") + + prefix_map = { + "lea_lea_": "lea_", + "lynx_lynx_": "lynx_", + } + + test_payloads = [ + ("lea", '{"jsonrpc":"2.0","id":1,"method":"lea_lea_view_symbol_ast","params":{"symbol_id":"test:sym"}}', + "lea_view_symbol_ast"), + ("lx", '{"jsonrpc":"2.0","id":1,"method":"lynx_lynx_search_graph","params":{"query":"test"}}', + "lynx_search_graph"), + ] + + for server, raw_json, expected_method in test_payloads: + parsed = json.loads(raw_json) + original_method = parsed["method"] + + stripped = original_method + bin_key = "lea_" if server == "lea" else "lynx_" + if stripped.startswith(bin_key * 2): + stripped = stripped[len(bin_key):] + elif stripped.startswith(prefix_map.get(bin_key * 2, "")): + stripped = stripped[len(prefix_map.get(bin_key * 2, "")):] + + test(f"[{server}] prefix strip '{original_method}' -> '{stripped}'", + stripped == expected_method, + f"expected '{expected_method}'") + + +# ============================================================================= +# MAIN TEST RUNNER +# ============================================================================= +def main(): + print(f"{BOLD}") + print(f" ╔══════════════════════════════════════════════════════╗") + print(f" ║ MCP Transport Bridge Diagnostic & Test Suite ║") + print(f" ║ PizenLabs - lea/lx Dual Engine ║") + print(f" ╚══════════════════════════════════════════════════════╝") + print(f"{RESET}") + print(f" lx binary: {LX_BIN or 'NOT FOUND'}") + print(f" lea binary: {LEA_BIN or 'NOT FOUND'}") + print() + + run_wrap_tests = "--wrap" in sys.argv + run_native_only = "--native" in sys.argv + + # Phase 1: Discover registered tools from both servers + lea_tools = test_native_registration("lea", LEA_BIN, [ + "lea_view_symbol_ast", + "lea_find_neighbors", + "lea_trace_calls", + "lea_trace_execution_path", + "lea_find_architecture_violations", + ]) + + lx_tools = test_native_registration("lx", LX_BIN, [ + "lynx_search_graph", + "lynx_resolve_symbol", + "lynx_find_related", + ]) + + if not run_native_only: + # Phase 2: Test method resolution accuracy + section("Phase 2: Method Resolution Accuracy") + + # lea tool tests (uses standard MCP tools/call protocol) + # Note: mcp-golang library does not validate the `name` field in + # tools/call, so lea_lea_ and cross-server calls pass through. + # Validation happens at the OpenCode client layer, not MCP server. + if lea_tools: + test_method_resolution("lea", LEA_BIN, "lea_view_symbol_ast", + {"symbol_id": "func:test:main"}, should_succeed=True, use_direct=False) + test_method_resolution("lea", LEA_BIN, "lea_lea_view_symbol_ast", + {"symbol_id": "func:test:main"}, should_succeed=True, use_direct=False) + test_method_resolution("lea", LEA_BIN, "lynx_search_graph", + {"query": "test"}, should_succeed=True, use_direct=False) + + # lx tool tests (uses direct method routing) + if lx_tools: + test_method_resolution("lx", LX_BIN, "lynx_search_graph", + {"query": "main"}, should_succeed=True, use_direct=True) + test_method_resolution("lx", LX_BIN, "lynx_lynx_search_graph", + {"query": "main"}, should_succeed=False, use_direct=True) + test_method_resolution("lx", LX_BIN, "lea_view_symbol_ast", + {"symbol_id": "test"}, should_succeed=False, use_direct=True) + + # Phase 3: OpenCode transport simulation + test_opencode_wrapping() + + # Phase 5: Integration matrix + test_integration_matrix() + + # Phase 6: Transport validation + test_transport_validation() + + if run_wrap_tests: + test_bash_wrapper_bridge() + + # Summary + print(f"\n{BOLD}{'=' * 60}{RESET}") + print(f"{BOLD} RESULTS:{RESET}") + print(f" Total: {TESTS_RUN}") + print(f" Passed: {TESTS_PASSED}") + print(f" Failed: {TESTS_FAILED}") + + if TESTS_FAILED == 0: + print(f"\n {PASS} All tests passed. Transport channel diagnostics complete.") + else: + print(f"\n {FAIL} {TESTS_FAILED} test(s) failed. Review details above.") + print(f"\n RECOMMENDED FIXES:") + print(f" Option A: Run 'scripts/mcp-bridge.sh' wrapper") + print(f" Option B: Patch MCP servers (see docs for code changes)") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/opencode-bridge-config.json b/scripts/opencode-bridge-config.json new file mode 100644 index 0000000..9c5df93 --- /dev/null +++ b/scripts/opencode-bridge-config.json @@ -0,0 +1,38 @@ +# MCP Transport Bridge — OpenCode Integration Config +# ================================================== +# Add these entries to your ~/.opencode/settings.json under the "mcp" key. +# +# The bridged versions strip the double-prefix (lea_lea_ → lea_, lynx_lynx_ → lynx_) +# before forwarding to the native binary. Use them if OpenCode's tool namespacing +# causes "Method not found" (-32601) errors. +# +# You can keep both native AND bridged servers active simultaneously: +# - pizen-lea / pizen-lynx → native (works with correct OpenCode routing) +# - pizen-lea-bridged / pizen-lynx-bridged → bridged (handles double-prefix) + +{ + "mcp": { + "pizen-lea": { + "enabled": true, + "type": "local", + "command": ["/Users/anvndev/go/bin/lea", "mcp"] + }, + "pizen-lynx": { + "enabled": true, + "type": "local", + "command": ["/Users/anvndev/.cargo/bin/lx", "mcp"] + }, + "pizen-lea-bridged": { + "enabled": true, + "type": "local", + "command": ["/Users/anvndev/Documents/Project/OpenSource/PizenLabs/lea/scripts/mcp-bridge.sh", + "/Users/anvndev/go/bin/lea", "mcp"] + }, + "pizen-lynx-bridged": { + "enabled": true, + "type": "local", + "command": ["/Users/anvndev/Documents/Project/OpenSource/PizenLabs/lea/scripts/mcp-bridge.sh", + "/Users/anvndev/.cargo/bin/lx", "mcp"] + } + } +} \ No newline at end of file From 2cb5a665d9a4ad78987230efb29acd1468f61a0e Mon Sep 17 00:00:00 2001 From: andev0x Date: Wed, 24 Jun 2026 03:43:44 +0700 Subject: [PATCH 3/6] feat(test): add lea sub-tests - include additional lea tests in suite - ensure new tests are consistent with existing ones --- scripts/mcp_bridge_test_suite.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/mcp_bridge_test_suite.py b/scripts/mcp_bridge_test_suite.py index 7adb601..19d6d70 100755 --- a/scripts/mcp_bridge_test_suite.py +++ b/scripts/mcp_bridge_test_suite.py @@ -362,6 +362,11 @@ def main(): "lea_trace_calls", "lea_trace_execution_path", "lea_find_architecture_violations", + "lea_lea_view_symbol_ast", + "lea_lea_find_neighbors", + "lea_lea_trace_calls", + "lea_lea_trace_execution_path", + "lea_lea_find_architecture_violations", ]) lx_tools = test_native_registration("lx", LX_BIN, [ From 17b0c25ab9207dce010b1d5710a8cdd1fcff5a99 Mon Sep 17 00:00:00 2001 From: andev0x Date: Wed, 24 Jun 2026 13:08:17 +0700 Subject: [PATCH 4/6] style(internal/mcp/install/install.go): - refine instruction file content formatting and comments --- internal/mcp/install/install.go | 101 +++++++++++++++++++++----------- scripts/mcp-bridge.py | 31 ++++++---- 2 files changed, 85 insertions(+), 47 deletions(-) diff --git a/internal/mcp/install/install.go b/internal/mcp/install/install.go index 9283bc3..0d8829e 100644 --- a/internal/mcp/install/install.go +++ b/internal/mcp/install/install.go @@ -48,7 +48,7 @@ func installTargets(home, vscodeUserDir string) []target { {Name: "Pi Coding Agents", Path: filepath.Join(home, ".pi", "agent", "mcp.json"), Format: "json", ConfigDir: filepath.Join(home, ".pi", "agent"), InstructionFile: "AGENTS.md"}, {Name: "Gemini CLI", Path: filepath.Join(home, ".gemini", "settings.json"), Format: "json", ConfigDir: filepath.Join(home, ".gemini"), InstructionFile: "GEMINI.md"}, {Name: "Zed", Path: filepath.Join(zedDir, "settings.json"), Format: "zed", ConfigDir: zedDir, InstructionFile: "AGENTS.md"}, - {Name: "OpenCode", Path: filepath.Join(home, ".opencode", "settings.json"), Format: "opencode", ConfigDir: filepath.Join(home, ".opencode"), InstructionFile: "AGENTS.md"}, + {Name: "OpenCode", Path: filepath.Join(home, ".config", "opencode", "settings.json"), Format: "opencode", ConfigDir: filepath.Join(home, ".config", "opencode"), InstructionFile: "AGENTS.md"}, {Name: "Antigravity", Path: filepath.Join(home, ".gemini", "config", "mcp_config.json"), Format: "json", ConfigDir: filepath.Join(home, ".gemini", "config"), InstructionFile: "AGENTS.md"}, {Name: "Aider", Path: filepath.Join(home, ".aider.conf.yml"), Format: "yaml", ConfigDir: filepath.Join(home, ".aider"), InstructionFile: "AIDER.md"}, {Name: "KiloCode", Path: filepath.Join(home, ".kilocode", "settings.json"), Format: "json", ConfigDir: filepath.Join(home, ".kilocode"), InstructionFile: "AGENTS.md"}, @@ -129,8 +129,8 @@ func Run(opts ...Options) error { lxPath = resolveLXFallback() } - vscodeUserDir := vscodeUserDir(home) - allTargets := installTargets(home, vscodeUserDir) + vscDir := vscodeUserDir(home) + allTargets := installTargets(home, vscDir) detected := detectTargets(allTargets) if len(detected) == 0 { @@ -343,7 +343,7 @@ func selectTargets(targets []target) ([]target, error) { func resolveLXFallback() string { p, err := exec.LookPath("lx") if err != nil { - return filepath.Join(homeDir(), ".cargo", "bin", "lx") // return default even if missing + return filepath.Join(homeDir(), ".cargo", "bin", "lx") } return p } @@ -369,7 +369,7 @@ func configureTarget(t target, leaPath, lxPath string) error { case "codex_toml": err = injectCodexTOML(t.Path, leaPath, lxPath) case "instructions": - err = writeInstructions(t.Path) + return writeInstructions(t.Path) default: return fmt.Errorf("unsupported format: %s", t.Format) } @@ -400,7 +400,6 @@ func injectJSON(path, leaPath, lxPath string) error { raw = make(map[string]any) } - // Standard mcpServers injection servers, ok := raw["mcpServers"].(map[string]any) if !ok || servers == nil { servers = make(map[string]any) @@ -453,7 +452,6 @@ func injectZedJSON(path, leaPath, lxPath string) error { } // injectOpenCodeJSON handles OpenCode's MCP config format under root "mcp" key. -// OpenCode uses: { "enabled": true, "type": "local", "command": ["path", "mcp"] } func injectOpenCodeJSON(path, leaPath, lxPath string) error { data := readOrEmpty(path) @@ -501,12 +499,6 @@ func injectYAML(path, leaPath, lxPath string) error { raw = make(map[string]any) } - // YAML mcp list format for aider: - // mcp: - // - name: pizen-lea - // cmd: mcp - // - name: pizen-lynx - // cmd: mcp mcpList, ok := raw["mcp"].([]any) if !ok { mcpList = []any{} @@ -555,8 +547,6 @@ func injectTOML(path, leaPath, lxPath string) error { raw = make(map[string]any) } - // Kiro Agent TOML format expects external tools under [[tool]] or similar. - // Inject under a generic `[[external_tools]]` array or merge into existing. tools, ok := raw["external_tools"].([]any) if !ok { tools = []any{} @@ -593,8 +583,7 @@ func upsertTOMLTool(list []any, name, command string, args []string) []any { return list } -// injectCodexTOML reads or creates a TOML file and injects pizen entries -// using the Codex CLI format: [mcpServers] key with inline table values. +// injectCodexTOML reads or creates a TOML file and injects pizen entries. func injectCodexTOML(path, leaPath, lxPath string) error { data := readOrEmpty(path) @@ -663,17 +652,38 @@ func writeTOML(path string, v map[string]any) error { // writeInstructions writes the global Pizen ecosystem instructions file. func writeInstructions(path string) error { - content := `# MANDATORY WORKSPACE TOOL SELECTION RULES - -This repository utilizes custom AST-aware and graph-indexed tools (lynx_search_graph, lynx_resolve_symbol, lea_view_symbol_ast, lea_trace_execution_path). These are highly optimized for PizenLabs' internal architecture. - + rawContent := ` ## 1. Tool Priority Queue (Strict Compliance) -1. **FIRST PRIORITY (Structure & Lookups):** Always invoke lynx_search_graph and lea_view_symbol_ast directly when searching for code, analyzing failing tests, or viewing context. -2. **LAST RESORT (Fallback Only):** You may only use generic fallback tools (like codebase-memory-mcp_*, grep, or glob) if and only if the primary tools yield zero results. - -## 2. Enforcement -Before executing any tool call, verify that you are leveraging the custom AST-aware commands first. Guessing file paths via generic tools before attempting a structured lookup is penalized. +1. **FIRST PRIORITY:** Always invoke the custom AST-aware commands directly when searching for code or viewing context. + * Code Search: Use [BT]lynx_search_graph[BT]. + * Symbol Definition: Use [BT]lynx_resolve_symbol[BT]. + * AST Inspection: Use [BT]lea_view_symbol_ast[BT]. + +2. **LAST RESORT FALLBACK:** If (and only if) the primary tools above return connection errors, a method-not-found exception, or an empty index result, you are strictly allowed to fallback to grep or glob to locate the plain-text references. + +## 2. Strict Parameter Schema Mapping & Identifier Rules +* **[BT]lea_view_symbol_ast[BT]**: Requires [BT]symbol_id[BT] parameter. + - For Functions: {"symbol_id": "func:package/path:FunctionName"} + - For Structs/Types: {"symbol_id": "type:package/path:StructName"} + - For Methods: {"symbol_id": "func:package/path:ReceiverStruct.MethodName"} + +* **[BT]lynx_search_graph[BT]**: Requires [BT]query[BT] parameter. + - Example: {"query": "userRepository"} + +* **[BT]lynx_resolve_symbol[BT]**: Requires [BT]name[BT] parameter. + - Example: {"name": "userRepository"} + +## 3. Expected Thinking Pattern & Examples +When tasked to inspect or locate a struct, type, or function without an explicit package path, follow this internal reflection loop to save tokens and avoid failures: +1. **Bad Thought:** "Let's run grep, ripgrep, or glob to find userRepository." -> (STRICT VIOLATION) +2. **Good Thought:** "I must look up the structural definition first. I will call [BT]lynx_lynx_resolve_symbol[BT] with name='userRepository' or [BT]lynx_lynx_search_graph[BT] with query='userRepository' to discover its exact Go package path." +3. **Execution:** Once the package path is resolved (e.g., [BT]internal/user/impl[BT]), I will construct the canonical [BT]symbol_id[BT] (e.g., [BT]type:internal/user/impl:userRepository[BT]) and invoke [BT]lea_lea_view_symbol_ast[BT] directly. + +## CRITICAL NOTE ON GO SYMBOLS: +The second component in the [BT]symbol_id[BT] MUST ALWAYS be the exact Go package path (e.g., internal/user/impl), NEVER a standard Go filename or physical file path. Using file paths will result in immediate "symbol not found" errors. + ` + content := strings.ReplaceAll(rawContent, "[BT]", "`") return os.WriteFile(path, []byte(content), 0644) } @@ -681,22 +691,43 @@ func writeInstructionsFile(t target) error { if t.InstructionFile == "" { return nil } - // Make sure config directory exists if err := os.MkdirAll(t.ConfigDir, 0755); err != nil { return err } path := filepath.Join(t.ConfigDir, t.InstructionFile) - content := `# MANDATORY WORKSPACE TOOL SELECTION RULES - -This repository utilizes custom AST-aware and graph-indexed tools (lynx_search_graph, lynx_resolve_symbol, lea_view_symbol_ast, lea_trace_execution_path). These are highly optimized for PizenLabs' internal architecture. + rawContent := ` ## 1. Tool Priority Queue (Strict Compliance) -1. **FIRST PRIORITY (Structure & Lookups):** Always invoke lynx_search_graph and lea_view_symbol_ast directly when searching for code, analyzing failing tests, or viewing context. -2. **LAST RESORT (Fallback Only):** You may only use generic fallback tools (like codebase-memory-mcp_*, grep, or glob) if and only if the primary tools yield zero results. - -## 2. Enforcement -Before executing any tool call, verify that you are leveraging the custom AST-aware commands first. Guessing file paths via generic tools before attempting a structured lookup is penalized. +1. **FIRST PRIORITY:** Always invoke the custom AST-aware commands directly when searching for code or viewing context. + * Code Search: Use [BT]lynx_search_graph[BT]. + * Symbol Definition: Use [BT]lynx_resolve_symbol[BT]. + * AST Inspection: Use [BT]lea_view_symbol_ast[BT]. + +2. **LAST RESORT FALLBACK:** If (and only if) the primary tools above return connection errors, a method-not-found exception, or an empty index result, you are strictly allowed to fallback to grep or glob to locate the plain-text references. + +## 2. Strict Parameter Schema Mapping & Identifier Rules +* **[BT]lea_view_symbol_ast[BT]**: Requires [BT]symbol_id[BT] parameter. + - For Functions: {"symbol_id": "func:package/path:FunctionName"} + - For Structs/Types: {"symbol_id": "type:package/path:StructName"} + - For Methods: {"symbol_id": "func:package/path:ReceiverStruct.MethodName"} + +* **[BT]lynx_search_graph[BT]**: Requires [BT]query[BT] parameter. + - Example: {"query": "userRepository"} + +* **[BT]lynx_resolve_symbol[BT]**: Requires [BT]name[BT] parameter. + - Example: {"name": "userRepository"} + +## 3. Expected Thinking Pattern & Examples +When tasked to inspect or locate a struct, type, or function without an explicit package path, follow this internal reflection loop to save tokens and avoid failures: +1. **Bad Thought:** "Let's run grep, ripgrep, or glob to find userRepository." -> (STRICT VIOLATION) +2. **Good Thought:** "I must look up the structural definition first. I will call [BT]lynx_lynx_resolve_symbol[BT] with name='userRepository' or [BT]lynx_lynx_search_graph[BT] with query='userRepository' to discover its exact Go package path." +3. **Execution:** Once the package path is resolved (e.g., [BT]internal/user/impl[BT]), I will construct the canonical [BT]symbol_id[BT] (e.g., [BT]type:internal/user/impl:userRepository[BT]) and invoke [BT]lea_lea_view_symbol_ast[BT] directly. + +## CRITICAL NOTE ON GO SYMBOLS: +The second component in the [BT]symbol_id[BT] MUST ALWAYS be the exact Go package path (e.g., internal/user/impl), NEVER a standard Go filename or physical file path. Using file paths will result in immediate "symbol not found" errors. + ` + content := strings.ReplaceAll(rawContent, "[BT]", "`") return os.WriteFile(path, []byte(content), 0644) } diff --git a/scripts/mcp-bridge.py b/scripts/mcp-bridge.py index a183def..bb754fa 100755 --- a/scripts/mcp-bridge.py +++ b/scripts/mcp-bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -mcp-bridge.py — MCP Transport Bridge (Option A) +mcp-bridge.py — MCP Transport Bridge Intercepts JSON-RPC 2.0 messages between OpenCode and MCP servers (lea/lx), remapping double-prefixed method names before forwarding to the native binary. @@ -12,14 +12,6 @@ lynx_lynx_resolve_symbol -> lynx_resolve_symbol Usage: ./mcp-bridge.py [args...] - -OpenCode config entry (~/.opencode/settings.json): - "mcp": { - "pizen-lea-bridged": { - "command": ["/path/to/mcp-bridge.py", "/path/to/lea", "mcp"], - "enabled": true - } - } """ import json import os @@ -29,14 +21,24 @@ def remap_method(method: str) -> str: + """ + Removes the duplicate client-side prefix boggled by OpenCode encapsulation, + ensuring the native MCP server handles valid method identifiers. + """ + # If client sends 'lea_lea_view_symbol_ast' -> slice off the first 'lea_' (4 chars) if method.startswith("lea_lea_"): - return method[4:] # lea_lea_X -> lea_X + return method[4:] + + # If client sends 'lynx_lynx_search_graph' -> slice off the first 'lynx_' (5 chars) if method.startswith("lynx_lynx_"): - return method[5:] # lynx_lynx_X -> lynx_X + return method[5:] + + # Handle system extended ecosystem prefixes if applicable if method.startswith("pizen_lea_lea_"): return "lea_" + method[len("pizen_lea_lea_"):] if method.startswith("pizen_lynx_lynx_"): return "lynx_" + method[len("pizen_lynx_lynx_"):] + return method @@ -52,6 +54,7 @@ def main(): print(f"mcp-bridge: ERROR: binary not executable: {binary}", file=sys.stderr) sys.exit(1) + # Spawn the underlying native MCP server process (lea or lx) proc = subprocess.Popen( [binary] + args, stdin=subprocess.PIPE, @@ -60,6 +63,7 @@ def main(): bufsize=0, ) + # Separate thread to handle standard error forwarding def forward_stderr(): for line in iter(proc.stderr.readline, b""): sys.stderr.buffer.write(line) @@ -68,6 +72,7 @@ def forward_stderr(): stderr_thread = threading.Thread(target=forward_stderr, daemon=True) stderr_thread.start() + # Intercept and clean incoming requests from OpenCode client before piping to native server def pipe_stdin_to_native(): try: for raw_line in sys.stdin.buffer: @@ -78,6 +83,7 @@ def pipe_stdin_to_native(): try: msg = json.loads(line) except json.JSONDecodeError: + # Pass through raw non-JSON payloads safely proc.stdin.write(raw_line) proc.stdin.flush() continue @@ -98,6 +104,7 @@ def pipe_stdin_to_native(): stdin_thread = threading.Thread(target=pipe_stdin_to_native, daemon=True) stdin_thread.start() + # Pass native stdout server responses back to OpenCode client unmodified def pipe_native_to_stdout(): try: for line in iter(proc.stdout.readline, b""): @@ -113,4 +120,4 @@ def pipe_native_to_stdout(): if __name__ == "__main__": - main() \ No newline at end of file + main() From fb64079c33ab7ed970e9264e16c8efb22ed4c92b Mon Sep 17 00:00:00 2001 From: andev0x Date: Wed, 24 Jun 2026 13:18:17 +0700 Subject: [PATCH 5/6] fix(mcp/install): correct opencode path - apply semantic repository updates --- internal/mcp/install/install.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/mcp/install/install.go b/internal/mcp/install/install.go index 0d8829e..ac60521 100644 --- a/internal/mcp/install/install.go +++ b/internal/mcp/install/install.go @@ -48,7 +48,7 @@ func installTargets(home, vscodeUserDir string) []target { {Name: "Pi Coding Agents", Path: filepath.Join(home, ".pi", "agent", "mcp.json"), Format: "json", ConfigDir: filepath.Join(home, ".pi", "agent"), InstructionFile: "AGENTS.md"}, {Name: "Gemini CLI", Path: filepath.Join(home, ".gemini", "settings.json"), Format: "json", ConfigDir: filepath.Join(home, ".gemini"), InstructionFile: "GEMINI.md"}, {Name: "Zed", Path: filepath.Join(zedDir, "settings.json"), Format: "zed", ConfigDir: zedDir, InstructionFile: "AGENTS.md"}, - {Name: "OpenCode", Path: filepath.Join(home, ".config", "opencode", "settings.json"), Format: "opencode", ConfigDir: filepath.Join(home, ".config", "opencode"), InstructionFile: "AGENTS.md"}, + {Name: "OpenCode", Path: filepath.Join(home, ".config", "opencode", "opencode.json"), Format: "opencode", ConfigDir: filepath.Join(home, ".config", "opencode"), InstructionFile: "AGENTS.md"}, {Name: "Antigravity", Path: filepath.Join(home, ".gemini", "config", "mcp_config.json"), Format: "json", ConfigDir: filepath.Join(home, ".gemini", "config"), InstructionFile: "AGENTS.md"}, {Name: "Aider", Path: filepath.Join(home, ".aider.conf.yml"), Format: "yaml", ConfigDir: filepath.Join(home, ".aider"), InstructionFile: "AIDER.md"}, {Name: "KiloCode", Path: filepath.Join(home, ".kilocode", "settings.json"), Format: "json", ConfigDir: filepath.Join(home, ".kilocode"), InstructionFile: "AGENTS.md"}, @@ -469,12 +469,12 @@ func injectOpenCodeJSON(path, leaPath, lxPath string) error { mcp = make(map[string]any) } - mcp["pizen-lea"] = map[string]any{ + mcp["lea"] = map[string]any{ "enabled": true, "type": "local", "command": []any{leaPath, "mcp"}, } - mcp["pizen-lynx"] = map[string]any{ + mcp["lynx"] = map[string]any{ "enabled": true, "type": "local", "command": []any{lxPath, "mcp"}, From 2b0f661b029cd4d2a56bc8cc740117e6ef06a92d Mon Sep 17 00:00:00 2001 From: andev0x Date: Wed, 24 Jun 2026 14:10:25 +0700 Subject: [PATCH 6/6] fix(install): remove unnecessary hook injection - simplify json configuration process - improve code readability --- internal/mcp/install/install.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/mcp/install/install.go b/internal/mcp/install/install.go index ac60521..3767405 100644 --- a/internal/mcp/install/install.go +++ b/internal/mcp/install/install.go @@ -481,8 +481,6 @@ func injectOpenCodeJSON(path, leaPath, lxPath string) error { } raw["mcp"] = mcp - injectHooksJSON(raw, leaPath) - return writeJSON(path, raw) }