x64dbgMCP
§ 00 · Source-level Observatory

Http Sse source deep dive

一个原生 x64dbg 插件,把调试器能力通过 MCP Streamable HTTP 与 SSE 暴露给 AI 客户端。真正值得拆的是:它不是脚本壳,而是把 Win32 socket、JSON-RPC、工具 schema、x64dbg bridge 和菜单配置全部编进同一个 Zig 插件。

★ 956
GitHub Stars
5.2K
LOC
12
Files
2026-08-24
Updated
x64dbg plugin→Win32 HTTP server
MCP client→JSON-RPC tools/list + tools/call
tool handler→bridge wrapper
debugger state→event log + SSE
§ 00 · OVERVIEW

HTTP/SSE 层的最小实现

Transport 层的核心是把 Win32 socket 直接包成 MCP 服务:src/core/mcp_server.zig:179(证据 src/evidence.md:9) 启动线程,src/core/mcp_server.zig:264(证据 src/evidence.md:10) 初始化 socket,src/core/mcp_server.zig:312(证据 src/evidence.md:11) 接收连接,src/core/mcp_server.zig:358(证据 src/evidence.md:12) 解析请求。证据索引见 src/core/mcp_server.zig:439(证据 src/evidence.md:33),它说明 JSON-RPC 分发没有外部框架参与。

▸Transport 角色

它同时支持 Streamable HTTP 与 legacy SSE:src/core/mcp_server.zig:393(证据 src/evidence.md:24) 管 GET,src/core/mcp_server.zig:409(证据 src/evidence.md:25) 管 POST,src/core/mcp_server.zig:499(证据 src/evidence.md:45) 建 SSE,src/core/mcp_server.zig:530(证据 src/evidence.md:46) 处理 SSE message。实现集中,调试方便,但也把协议细节暴露给手写 parser。

§ 01 · SERVER LOOP

Socket 生命周期

▸启动与停止

src/core/mcp_server.zig · L179-250Zig
179pub fn start() void {
180    if (server_running) {
181        bridge.logPuts("[x64dbg-MCP Server] Server already running.\x00");
182        return;
183    }
184
185    server_running = true;
186
187    server_handle = CreateThread(null, 0, serverThreadProc, null, 0, null);
188    if (server_handle == null) {
189        server_running = false;
190        bridge.logPuts("[x64dbg-MCP Server] Failed to create server thread.\x00");
191        return;
192    }
193
194    {
195        var buf: [192]u8 = undefined;
196        const prefix = "[x64dbg-MCP Server] Starting MCP server on ";
197        @memcpy(buf[0..prefix.len], prefix);
198        var pos: usize = prefix.len;
199        @memcpy(buf[pos .. pos + server_ip_len], server_ip[0..server_ip_len]);
200        pos += server_ip_len;
201        buf[pos] = ':';
202        pos += 1;
203        pos += fmtPort(server_port, buf[pos..]);
204        const suffix = "...";
205        @memcpy(buf[pos .. pos + suffix.len], suffix);
206        pos += suffix.len;
207        buf[pos] = 0;
208        bridge.logPuts(@ptrCast(&buf));
209    }
210}
211
212pub fn stop() void {
213    if (!server_running) return;
214    server_running = false;
215
216    // Close all SSE client sockets to unblock their keepalive loops
217    for (&sse_clients) |*slot| {
218        if (slot.* != INVALID_SOCKET) {
219            _ = ws2.closesocket(slot.*);
220            slot.* = INVALID_SOCKET;
221        }
222    }
223
224    // Close listen socket to unblock accept()
225    if (listen_sock != INVALID_SOCKET) {
226        _ = ws2.closesocket(listen_sock);
227        listen_sock = INVALID_SOCKET;
228    }
229
230    // Wait for server thread to exit (up to 5s)
231    if (server_handle) |h| {
232        _ = WaitForSingleObject(h, 5000);
233        _ = CloseHandle(h);
234        server_handle = null;
235    }
236    {
237        var buf: [192]u8 = undefined;
238        const prefix = "[x64dbg-MCP Server] MCP server stopped (was on ";
239        @memcpy(buf[0..prefix.len], prefix);
240        var pos: usize = prefix.len;
241        @memcpy(buf[pos .. pos + server_ip_len], server_ip[0..server_ip_len]);
242        pos += server_ip_len;
243        buf[pos] = ':';
244        pos += 1;
245        pos += fmtPort(server_port, buf[pos..]);
246        buf[pos] = ')';
247        pos += 1;
248        buf[pos] = 0;
249        bridge.logPuts(@ptrCast(&buf));
250    }

src/core/mcp_server.zig:179(证据 src/evidence.md:9) 和 src/core/mcp_server.zig:212(证据 src/evidence.md:19) 是成对设计:启动只允许一次,停止时关闭 SSE clients 和 listen socket。由于 src/core/mcp_server.zig:190(证据 src/evidence.md:20) 用 Win32 thread,serverLoop 中的资源释放必须完整,否则会影响宿主调试器。

§ 02 · ROUTING

请求分流

▸handleClient

src/core/mcp_server.zig · L358-431Zig
358fn handleClient(sock: SOCKET) void {
359    const recv_result = wsRecv(sock);
360    if (recv_result.len == 0) {
361        _ = ws2.closesocket(sock);
362        return;
363    }
364
365    const request = recv_result.buf[0..recv_result.len];
366    const method_line = getFirstLine(request);
367
368    if (std.mem.startsWith(u8, method_line, "OPTIONS ")) {
369        wsSend(sock, "HTTP/1.1 204 No Content\r\n" ++ CORS_HEADERS ++ "\r\n");
370        _ = ws2.closesocket(sock);
371        return;
372    }
373
374    {
375        const headers_slice = if (findHeaderEnd(request)) |he| request[0..he] else request;
376        const tok = parseHeaderValue(headers_slice, "Authorization");
377        var authorized = false;
378        if (tok) |t| {
379            if (t.len > 7 and std.mem.eql(u8, t[0..7], "Bearer ")) {
380                const bearer = t[7..];
381                if (auth_token_len > 0 and bearer.len == auth_token_len and std.mem.eql(u8, bearer, auth_token[0..auth_token_len])) {
382                    authorized = true;
383                }
384            }
385        }
386        if (!authorized) {
387            wsSend(sock, "HTTP/1.1 401 Unauthorized\r\n" ++ CORS_HEADERS ++ "Content-Length: 24\r\n\r\n{\"error\":\"Unauthorized\"}");
388            _ = ws2.closesocket(sock);
389            return;
390        }
391    }
392
393    if (std.mem.startsWith(u8, method_line, "GET ")) {
394        const path = getPath(method_line);
395        if (std.mem.indexOf(u8, path, ".well-known") != null) {
396            wsSend(sock, "HTTP/1.1 404 Not Found\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n");
397            _ = ws2.closesocket(sock);
398            return;
399        }
400        if (std.mem.eql(u8, path, "/sse")) {
401            handleSseConnect(sock);
402            return;
403        }
404        sendServerInfo(sock);
405        _ = ws2.closesocket(sock);
406        return;
407    }
408
409    if (std.mem.startsWith(u8, method_line, "POST ")) {
410        const path = getPath(method_line);
411        if (std.mem.indexOf(u8, path, ".well-known") != null) {
412            wsSend(sock, "HTTP/1.1 404 Not Found\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n");
413            _ = ws2.closesocket(sock);
414            return;
415        }
416
417        if (findHeaderEnd(request)) |header_end| {
418            const body = request[header_end + 4 ..];
419            const headers = request[0..header_end];
420
421            if (std.mem.eql(u8, path, "/messages") or std.mem.startsWith(u8, path, "/messages?")) {
422                const session_id = parseQueryParam(path, "sessionId") orelse
423                    parseHeaderValue(headers, "Mcp-Session-Id");
424                handleSseMessage(sock, body, session_id);
425            } else {
426                handleJsonRpc(sock, body);
427            }
428        } else {
429            wsSend(sock, "HTTP/1.1 400 Bad Request\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n");
430        }
431        _ = ws2.closesocket(sock);

认证检查在 src/core/mcp_server.zig:374(证据 src/evidence.md:22) 到 src/core/mcp_server.zig:388(证据 src/evidence.md:23) 完成;随后 src/core/mcp_server.zig:393(证据 src/evidence.md:24) 选择 GET 路径,src/core/mcp_server.zig:409(证据 src/evidence.md:25) 选择 POST 路径。这里没有路径级权限概念,token 一旦通过就能到达工具分发层。

§ 03 · JSON-RPC

MCP 方法分派

▸方法白名单

src/core/mcp_server.zig · L439-469Zig
439fn handleJsonRpc(sock: SOCKET, body: []const u8) void {
440    const parsed = std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, body, .{}) catch {
441        sendJsonRpcError(sock, null, -32700, "Parse error");
442        return;
443    };
444    defer parsed.deinit();
445
446    const root = parsed.value;
447    const method_val = json_mod.getStringField(root, "method") orelse {
448        sendJsonRpcError(sock, null, -32600, "Invalid request: missing method");
449        return;
450    };
451    const id = json_mod.getObjectField(root, "id");
452
453    if (id == null or std.mem.startsWith(u8, method_val, "notifications/")) {
454        wsSend(sock, "HTTP/1.1 202 Accepted\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n");
455        return;
456    }
457
458    if (std.mem.eql(u8, method_val, "initialize")) {
459        handleInitialize(sock, id);
460    } else if (std.mem.eql(u8, method_val, "tools/list")) {
461        handleToolsList(sock, id);
462    } else if (std.mem.eql(u8, method_val, "tools/call")) {
463        handleToolsCall(sock, id, root);
464    } else if (std.mem.eql(u8, method_val, "ping")) {
465        sendJsonRpcResult(sock, id, "{}");
466    } else {
467        sendJsonRpcError(sock, id, -32601, "Method not found");
468    }
469}

src/core/mcp_server.zig:439(证据 src/evidence.md:33) 只认 initialize、tools/list、tools/call、ping;src/core/mcp_server.zig:588(证据 src/evidence.md:57) 生成初始化结果,src/core/mcp_server.zig:621(证据 src/evidence.md:15) 生成工具清单,src/core/mcp_server.zig:650(证据 src/evidence.md:16) 处理调用结果。这是 MCP 兼容的最小闭环。

§ 04 · SSE

Legacy 客户端桥

▸SSE client 管理

src/core/mcp_server.zig · L499-586Zig
499fn handleSseConnect(sock: SOCKET) void {
500    const idx = sseAddClient(sock) orelse {
501        wsSend(sock, "HTTP/1.1 503 Service Unavailable\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n");
502        _ = ws2.closesocket(sock);
503        return;
504    };
505
506    const session_id = sse_session_ids[idx][0..sse_session_lens[idx]];
507
508    wsSend(sock, "HTTP/1.1 200 OK\r\n" ++ CORS_HEADERS ++
509        "Content-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: keep-alive\r\n\r\n");
510
511    var endpoint_buf: [128]u8 = undefined;
512    const endpoint = std.fmt.bufPrint(&endpoint_buf, "/messages?sessionId={s}", .{session_id}) catch "/messages";
513    sseSendEvent(sock, "endpoint", endpoint);
514
515    bridge.logPuts("[x64dbg-MCP Server] SSE client connected.\x00");
516
517    // Keep connection alive until client disconnects or server stops
518    while (server_running) {
519        Sleep(5000);
520        // Send keep-alive comment
521        const n = ws2.send(sock, ": keepalive\n\n", 13, 0);
522        if (n <= 0) break;
523    }
524
525    sseRemoveClient(idx);
526    _ = ws2.closesocket(sock);
527    bridge.logPuts("[x64dbg-MCP Server] SSE client disconnected.\x00");
528}
529
530fn handleSseMessage(sock: SOCKET, body: []const u8, session_id: ?[]const u8) void {
531    const parsed = std.json.parseFromSlice(std.json.Value, std.heap.page_allocator, body, .{}) catch {
532        sendJsonRpcError(sock, null, -32700, "Parse error");
533        return;
534    };
535    defer parsed.deinit();
536
537    const root = parsed.value;
538    const method_val = json_mod.getStringField(root, "method") orelse {
539        sendJsonRpcError(sock, null, -32600, "Invalid request: missing method");
540        return;
541    };
542    const id = json_mod.getObjectField(root, "id");
543
544    // Notifications get 202
545    if (id == null or std.mem.startsWith(u8, method_val, "notifications/")) {
546        wsSend(sock, "HTTP/1.1 202 Accepted\r\n" ++ CORS_HEADERS ++ "Content-Length: 0\r\n\r\n");
547        return;
548    }
549
550    // Process the RPC call and build the result
551    var result_buf: [131072]u8 = undefined;
552    var rw = JsonWriter.init(&result_buf);
553
554    if (std.mem.eql(u8, method_val, "initialize")) {
555        buildInitializeResult(&rw);
556    } else if (std.mem.eql(u8, method_val, "tools/list")) {
557        buildToolsListResult(&rw);
558    } else if (std.mem.eql(u8, method_val, "tools/call")) {
559        buildToolsCallResult(&rw, root);
560    } else if (std.mem.eql(u8, method_val, "ping")) {
561        rw.raw("{}");
562    } else {
563        // Method not found — respond directly on POST socket
564        sendJsonRpcError(sock, id, -32601, "Method not found");
565        return;
566    }
567
568    // Build full JSON-RPC response
569    var resp_buf: [131072]u8 = undefined;
570    var w = JsonWriter.init(&resp_buf);
571    w.raw("{\"jsonrpc\":\"2.0\",\"id\":");
572    writeJsonValue(&w, id);
573    w.raw(",\"result\":");
574    w.raw(rw.slice());
575    w.raw("}");
576    const resp_json = w.slice();
577
578    // Try to push response via SSE channel
579    const sse_idx = if (session_id) |sid| sseFindBySessionId(sid) else null;
580    if (sse_idx) |idx| {
581        sseSendEvent(sse_clients[idx], "message", resp_json);
582    }
583
584    // Also respond on the POST connection
585    wsSend(sock, "HTTP/1.1 200 OK\r\n" ++ CORS_HEADERS ++ "Content-Type: application/json\r\nContent-Length: 0\r\n\r\n");
586}

SSE 链路在 src/core/mcp_server.zig:499(证据 src/evidence.md:45) 分配 client slot,在 src/core/mcp_server.zig:530(证据 src/evidence.md:46) 收到消息后把 JSON-RPC 结果推回 stream。这个兼容路径有价值,但最好配合短 token、loopback bind 和外层防火墙。

§ 05 · RISKS

Transport 风险

▸默认开放地址

src/core/mcp_server.zig:37(证据 src/evidence.md:52) 的默认 IP 是 0.0.0.0,src/core/config.zig:203(证据 src/evidence.md:38) 也会把它作为初始配置。虽然 src/core/config.zig:406(证据 src/evidence.md:58) 的 UI 文案解释了 127.0.0.1 与 0.0.0.0,默认开放仍值得在生产使用前改成 loopback。