Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
mcp_bridge.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
72#include <poll.h>
73#include <sys/socket.h>
74#include <sys/un.h>
75#include <unistd.h>
76
77#include <cerrno>
78#include <cstdio>
79#include <cstring>
80#include <filesystem>
81#include <string>
82
83namespace entropic {
84// Forward-declared from include/entropic/mcp/mcp_json_discovery.h; pulled in
85// via libentropic-mcp at link time so the CLI does not have to include the
86// mcp internal header (which transitively pulls nlohmann/json_fwd).
87std::filesystem::path compute_socket_path(
88 const std::filesystem::path& project_dir);
89} // namespace entropic
90
91namespace entropic::cli {
92
93namespace {
94
95constexpr size_t kRelayBufSize = 8192;
96
106std::string parse_flag(int argc, char* argv[], const char* flag)
107{
108 for (int i = 1; i < argc - 1; ++i) {
109 if (std::strcmp(argv[i], flag) == 0) {
110 return argv[i + 1];
111 }
112 }
113 return {};
114}
115
123int connect_unix_socket(const std::string& path)
124{
125 int fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
126 bool ok = fd >= 0 && path.size() < sizeof(sockaddr_un::sun_path);
127 int saved = ok ? 0 : (fd < 0 ? errno : ENAMETOOLONG);
128 if (ok) {
129 struct sockaddr_un addr {};
130 addr.sun_family = AF_UNIX;
131 std::strncpy(addr.sun_path, path.c_str(),
132 sizeof(addr.sun_path) - 1);
133 ok = ::connect(fd, reinterpret_cast<sockaddr*>(&addr),
134 sizeof(addr)) == 0;
135 if (!ok) { saved = errno; }
136 }
137 if (!ok) {
138 if (fd >= 0) { ::close(fd); }
139 errno = saved;
140 fd = -1;
141 }
142 return fd;
143}
144
161int emit_no_engine_error(
162 const std::filesystem::path& requested,
163 const std::filesystem::path& canonical,
164 const std::filesystem::path& socket,
165 const char* why)
166{
167 std::fprintf(stderr,
168 "entropic mcp-bridge: no running engine.\n"
169 " project_dir (requested): %s\n"
170 " project_dir (canonical): %s\n"
171 " expected socket: %s\n"
172 " connect failed: %s\n"
173 "\n"
174 "An engine must be running for this project. Start one via the\n"
175 "engine host that owns the model (TUI, consumer app, or headless\n"
176 "server). mcp-bridge is a relay only — it does not load the\n"
177 "model itself.\n",
178 requested.c_str(), canonical.c_str(),
179 socket.c_str(), why ? why : "unknown");
180 return 1;
181}
182
191bool pump_once(int src_fd, int dst_fd)
192{
193 char buf[kRelayBufSize];
194 ssize_t n = ::read(src_fd, buf, sizeof(buf));
195 if (n <= 0) { return false; }
196 ssize_t off = 0;
197 while (off < n) {
198 ssize_t w = ::write(dst_fd, buf + off, n - off);
199 if (w <= 0) { return false; }
200 off += w;
201 }
202 return true;
203}
204
210void service_revents(struct pollfd* fds, int sock_fd,
211 bool& stdin_open, bool& sock_open)
212{
213 if (stdin_open && (fds[0].revents & (POLLIN | POLLHUP))) {
214 if (!pump_once(STDIN_FILENO, sock_fd)) {
215 stdin_open = false;
216 ::shutdown(sock_fd, SHUT_WR);
217 }
218 }
219 if (sock_open && (fds[1].revents & (POLLIN | POLLHUP))) {
220 if (!pump_once(sock_fd, STDOUT_FILENO)) {
221 sock_open = false;
222 }
223 }
224}
225
241void relay_loop(int sock_fd)
242{
243 struct pollfd fds[2] = {};
244 fds[0].fd = STDIN_FILENO;
245 fds[1].fd = sock_fd;
246
247 bool stdin_open = true;
248 bool sock_open = true;
249 while (stdin_open || sock_open) {
250 fds[0].events = stdin_open ? POLLIN : 0;
251 fds[1].events = sock_open ? POLLIN : 0;
252 int rc = ::poll(fds, 2, -1);
253 if (rc < 0 && errno != EINTR) { break; }
254 if (rc > 0) {
255 service_revents(fds, sock_fd, stdin_open, sock_open);
256 }
257 }
258}
259
260} // namespace
261
275int run_mcp_bridge(int argc, char* argv[])
276{
277 // --socket PATH overrides discovery. Primary uses: deterministic
278 // tests, and users whose engine is configured with a non-default
279 // socket path. Absent the override, the bridge derives the path
280 // from the canonicalized project_dir hash — same scheme as the
281 // engine's ExternalBridge.
282 std::string explicit_socket = parse_flag(argc, argv, "--socket");
283 std::string requested = parse_flag(argc, argv, "--project-dir");
284 std::filesystem::path requested_path = requested.empty()
285 ? std::filesystem::current_path()
286 : std::filesystem::path(requested);
287
288 std::error_code ec;
289 auto canonical = std::filesystem::weakly_canonical(requested_path, ec);
290 if (ec) { canonical = requested_path; }
291
292 std::filesystem::path socket_path = explicit_socket.empty()
294 : std::filesystem::path(explicit_socket);
295
296 int fd = connect_unix_socket(socket_path.string());
297 if (fd < 0) {
298 return emit_no_engine_error(
299 requested_path, canonical, socket_path,
300 std::strerror(errno));
301 }
302
303 relay_loop(fd);
304 ::close(fd);
305 return 0;
306}
307
308} // namespace entropic::cli
int run_mcp_bridge(int argc, char *argv[])
Entry point for the mcp-bridge subcommand.
Activate model on GPU (WARM → ACTIVE).
std::filesystem::path compute_socket_path(const std::filesystem::path &project_dir)
Compute project-unique Unix socket path for self-detection.
@ ok
Tool dispatched, returned non-empty content.