|
6 | 6 | // |
7 | 7 | // Claude Desktop launches this CLT (packaged inside an .mcpb bundle) as a |
8 | 8 | // subprocess. It reads JSON-RPC messages from stdin, forwards each to the |
9 | | -// ES Memory app's locally-running HTTP server, and writes the response to |
10 | | -// stdout. |
| 9 | +// ES Memory app's locally-running HTTP server, and writes the response |
| 10 | +// back to stdout. |
11 | 11 | // |
12 | | -// Discovery: reads server.plist from the host's sandbox container at |
13 | | -// ~/Library/Containers/<HOST_BUNDLE_ID>/Data/Library/Application Support/ES-Memory/server.plist |
14 | | -// The plist contains the full MCP endpoint URL and the host's version. |
| 12 | +// The bridge reads zero files. The host is expected to listen at a fixed |
| 13 | +// URL (localhost:59123/mcp) — an exotic, IANA-dynamic-range port chosen to |
| 14 | +// avoid conflicts with common local services (AirPlay sits on 5000, etc.). |
15 | 15 | // |
16 | | -// If the host isn't running on startup, the bridge polls for ~5s, then enters |
17 | | -// a degraded mode that responds to MCP requests locally with a setup-help |
18 | | -// message instead of failing silently. It re-attempts discovery on every |
19 | | -// request, so it auto-recovers when the host comes up. |
| 16 | +// If the host isn't reachable, the bridge responds locally to MCP requests |
| 17 | +// with a setup-help message so Claude can surface a clear error in the |
| 18 | +// conversation. It auto-recovers on the next successful forward. |
| 19 | +// |
| 20 | +// No file IO = no TCC prompts, ever. That's the whole point of this |
| 21 | +// revision. |
20 | 22 | // |
21 | 23 |
|
22 | 24 | #import <Foundation/Foundation.h> |
23 | 25 | #include <signal.h> |
24 | 26 |
|
25 | | -// The host app's bundle ID. The bridge reads server.plist from the host's |
26 | | -// sandbox container at this ID. Change this and the bridge points at a |
27 | | -// different host app. |
28 | | -#define HOST_BUNDLE_ID @"com.elarity.es-memory-mcp" |
| 27 | +static NSString *const kServerURL = @"http://localhost:59123/mcp"; |
29 | 28 |
|
30 | 29 | static NSURL *gServerURL = nil; |
31 | | -static NSString *gServerVersion = nil; |
32 | | - |
33 | | -#pragma mark - Server Discovery |
34 | | - |
35 | | -/// Read server.plist from the host's sandbox container. |
36 | | -/// quiet=YES suppresses stderr output (used during polling so we don't spam logs). |
37 | | -static NSURL *DiscoverServerURL(BOOL quiet) { |
38 | | - NSString *plistPath = [NSString stringWithFormat: |
39 | | - @"%@/Library/Containers/%@/Data/Library/Application Support/ES-Memory/server.plist", |
40 | | - NSHomeDirectory(), HOST_BUNDLE_ID]; |
41 | | - |
42 | | - NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:plistPath]; |
43 | | - if (!info) { |
44 | | - if (!quiet) { |
45 | | - fprintf(stderr, "[es-bridge] server.plist not found at %s\n", plistPath.UTF8String); |
46 | | - fprintf(stderr, "[es-bridge] Is ES Memory MCP running?\n"); |
47 | | - } |
48 | | - return nil; |
49 | | - } |
50 | | - |
51 | | - NSString *urlString = info[@"url"]; |
52 | | - if (urlString.length == 0) { |
53 | | - if (!quiet) fprintf(stderr, "[es-bridge] server.plist missing 'url' key\n"); |
54 | | - return nil; |
55 | | - } |
56 | | - |
57 | | - NSURL *url = [NSURL URLWithString:urlString]; |
58 | | - if (!url) { |
59 | | - if (!quiet) fprintf(stderr, "[es-bridge] invalid URL in server.plist: %s\n", |
60 | | - urlString.UTF8String); |
61 | | - return nil; |
62 | | - } |
63 | | - |
64 | | - NSString *version = info[@"version"]; |
65 | | - if (version.length > 0) gServerVersion = version; |
66 | | - |
67 | | - if (!quiet) { |
68 | | - fprintf(stderr, "[es-bridge] using %s\n", plistPath.UTF8String); |
69 | | - if (gServerVersion) { |
70 | | - fprintf(stderr, "[es-bridge] ES Memory v%s\n", gServerVersion.UTF8String); |
71 | | - } |
72 | | - } |
73 | | - return url; |
74 | | -} |
75 | | - |
76 | | -/// Try discovery once verbosely; if that misses, poll every 500ms for up to 5s |
77 | | -/// (quiet, so logs aren't spammed). On a polled hit, emit the verbose diagnostic. |
78 | | -static NSURL *DiscoverServerURLWithPolling(void) { |
79 | | - NSURL *url = DiscoverServerURL(NO); |
80 | | - if (url) return url; |
81 | | - fprintf(stderr, "[es-bridge] waiting up to 5s for ES Memory...\n"); |
82 | | - for (int i = 0; i < 10; i++) { |
83 | | - [NSThread sleepForTimeInterval:0.5]; |
84 | | - url = DiscoverServerURL(YES); |
85 | | - if (url) { |
86 | | - (void)DiscoverServerURL(NO); // re-emit the path + version diagnostic |
87 | | - fprintf(stderr, "[es-bridge] connected after %dms\n", (i + 1) * 500); |
88 | | - return url; |
89 | | - } |
90 | | - } |
91 | | - return nil; |
92 | | -} |
| 30 | +static BOOL gHostReachable = YES; // optimism; flipped on first forward failure |
93 | 31 |
|
94 | 32 | #pragma mark - HTTP Forwarding |
95 | 33 |
|
@@ -201,14 +139,9 @@ int main(int argc, const char *argv[]) { |
201 | 139 | @autoreleasepool { |
202 | 140 | signal(SIGPIPE, SIG_IGN); |
203 | 141 |
|
204 | | - gServerURL = DiscoverServerURLWithPolling(); |
205 | | - if (gServerURL) { |
206 | | - fprintf(stderr, "[es-bridge] connected to %s\n", |
207 | | - gServerURL.absoluteString.UTF8String); |
208 | | - } else { |
209 | | - fprintf(stderr, "[es-bridge] entering degraded mode — will respond locally " |
210 | | - "with setup help; auto-recovers if ES Memory starts.\n"); |
211 | | - } |
| 142 | + gServerURL = [NSURL URLWithString:kServerURL]; |
| 143 | + fprintf(stderr, "[es-bridge] forwarding to %s (static URL, no discovery)\n", |
| 144 | + kServerURL.UTF8String); |
212 | 145 |
|
213 | 146 | NSFileHandle *stdinHandle = [NSFileHandle fileHandleWithStandardInput]; |
214 | 147 | NSFileHandle *stdoutHandle = [NSFileHandle fileHandleWithStandardOutput]; |
@@ -236,44 +169,29 @@ int main(int argc, const char *argv[]) { |
236 | 169 | [NSCharacterSet whitespaceCharacterSet]]; |
237 | 170 | if (line.length == 0) continue; |
238 | 171 |
|
239 | | - // Lazy retry — the host may have just launched. |
240 | | - if (!gServerURL) { |
241 | | - gServerURL = DiscoverServerURL(YES); |
242 | | - if (gServerURL) { |
243 | | - fprintf(stderr, "[es-bridge] recovered: connected to %s\n", |
244 | | - gServerURL.absoluteString.UTF8String); |
245 | | - } |
246 | | - } |
247 | | - |
| 172 | + NSError *error = nil; |
| 173 | + NSString *response = ForwardRequest(line, &error); |
248 | 174 | NSString *output = nil; |
249 | 175 |
|
250 | | - if (gServerURL) { |
251 | | - NSError *error = nil; |
252 | | - NSString *response = ForwardRequest(line, &error); |
253 | | - if (response) { |
254 | | - output = response; |
255 | | - } else if (error) { |
256 | | - // Connection failed — host probably went down. Drop the |
257 | | - // cached URL so the next request re-attempts discovery, |
258 | | - // and respond from the degraded handler for this one. |
259 | | - fprintf(stderr, "[es-bridge] forward error: %s — dropping cached URL\n", |
| 176 | + if (response) { |
| 177 | + if (!gHostReachable) { |
| 178 | + fprintf(stderr, "[es-bridge] host recovered — resuming forwarding\n"); |
| 179 | + gHostReachable = YES; |
| 180 | + } |
| 181 | + output = response; |
| 182 | + } else if (error) { |
| 183 | + if (gHostReachable) { |
| 184 | + fprintf(stderr, "[es-bridge] host unreachable: %s — degraded mode\n", |
260 | 185 | error.localizedDescription.UTF8String); |
261 | | - gServerURL = nil; |
262 | | - gServerVersion = nil; |
263 | | - NSDictionary *msg = [NSJSONSerialization JSONObjectWithData:lineData |
264 | | - options:0 error:nil]; |
265 | | - if ([msg isKindOfClass:[NSDictionary class]]) { |
266 | | - output = DegradedResponseForRequest(msg); |
267 | | - } |
| 186 | + gHostReachable = NO; |
268 | 187 | } |
269 | | - // response nil + no error → 202 ack, no output needed. |
270 | | - } else { |
271 | 188 | NSDictionary *msg = [NSJSONSerialization JSONObjectWithData:lineData |
272 | 189 | options:0 error:nil]; |
273 | 190 | if ([msg isKindOfClass:[NSDictionary class]]) { |
274 | 191 | output = DegradedResponseForRequest(msg); |
275 | 192 | } |
276 | 193 | } |
| 194 | + // response nil + no error → 202 ack from host, no output needed. |
277 | 195 |
|
278 | 196 | if (output) { |
279 | 197 | [stdoutHandle writeData:[[output stringByAppendingString:@"\n"] |
|
0 commit comments