Skip to content

Commit 875bd3b

Browse files
hyperpolymathclaude
andcommitted
fix(zig): port 9 FFI files to Zig 0.15 API; self-contained container build
- 5 build.zig files: addStaticLibrary/addSharedLibrary → addLibrary with module + linkage field (Zig 0.15 Build API) - 4 FFI sources: client.open/send/wait → client.fetch with dynamic response_storage (Zig 0.15 std.http.Client API) - container/Containerfile: Stage 1 now clones+builds V compiler from source; no local pre-build of adapter/v/boj-server required - container-publish.yml: switch to container/Containerfile (production image, 96 cartridges) and update label from 53 → 96 cartridges Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fb86412 commit 875bd3b

11 files changed

Lines changed: 147 additions & 158 deletions

File tree

.github/workflows/container-publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
5151
with:
5252
context: .
53-
file: Containerfile
53+
file: container/Containerfile
5454
push: true
5555
tags: |
5656
ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }}
@@ -59,6 +59,6 @@ jobs:
5959
${{ secrets.DOCKERHUB_USERNAME && format('docker.io/{0}/boj-server:latest', secrets.DOCKERHUB_USERNAME) || '' }}
6060
labels: |
6161
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
62-
org.opencontainers.image.description=Bundle of Joy MCP Server — 53 formally verified cartridges
62+
org.opencontainers.image.description=Bundle of Joy MCP Server — 96 formally verified cartridges
6363
org.opencontainers.image.licenses=PMPL-1.0-or-later
6464
org.opencontainers.image.version=${{ steps.meta.outputs.version }}

cartridges/burble-admin-mcp/ffi/build.zig

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ pub fn build(b: *std.Build) void {
55
const target = b.standardTargetOptions(.{});
66
const optimize = b.standardOptimizeOption(.{});
77

8-
const lib = b.addStaticLibrary(.{
9-
.name = "burble_admin_ffi",
8+
const ffi_mod = b.addModule("burble_admin_ffi", .{
109
.root_source_file = b.path("burble_admin_ffi.zig"),
1110
.target = target,
1211
.optimize = optimize,
1312
});
13+
const lib = b.addLibrary(.{
14+
.name = "burble_admin_ffi",
15+
.root_module = ffi_mod,
16+
.linkage = .static,
17+
});
1418
b.installArtifact(lib);
1519

1620
const tests = b.addTest(.{
17-
.root_source_file = b.path("burble_admin_ffi.zig"),
18-
.target = target,
19-
.optimize = optimize,
21+
.root_module = ffi_mod,
2022
});
2123
const run_tests = b.addRunArtifact(tests);
2224
const test_step = b.step("test", "Run FFI tests");

cartridges/cloudflare-mcp/ffi/build.zig

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@ pub fn build(b: *std.Build) void {
77
const target = b.standardTargetOptions(.{});
88
const optimize = b.standardOptimizeOption(.{});
99

10-
const lib = b.addSharedLibrary(.{
11-
.name = "cloudflare_mcp_ffi",
10+
const ffi_mod = b.addModule("cloudflare_mcp_ffi", .{
1211
.root_source_file = b.path("cloudflare_mcp_ffi.zig"),
13-
.target = target,
12+
.target = target,
1413
.optimize = optimize,
1514
});
15+
const lib = b.addLibrary(.{
16+
.name = "cloudflare_mcp_ffi",
17+
.root_module = ffi_mod,
18+
.linkage = .dynamic,
19+
});
1620

1721
b.installArtifact(lib);
1822

1923
const unit_tests = b.addTest(.{
20-
.root_source_file = b.path("cloudflare_mcp_ffi.zig"),
21-
.target = target,
22-
.optimize = optimize,
24+
.root_module = ffi_mod,
2325
});
2426
const run_tests = b.addRunArtifact(unit_tests);
2527
const test_step = b.step("test", "Run FFI unit tests");

cartridges/game-admin-mcp/ffi/build.zig

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ pub fn build(b: *std.Build) void {
55
const target = b.standardTargetOptions(.{});
66
const optimize = b.standardOptimizeOption(.{});
77

8-
const lib = b.addStaticLibrary(.{
9-
.name = "game_admin_ffi",
8+
const ffi_mod = b.addModule("game_admin_ffi", .{
109
.root_source_file = b.path("game_admin_ffi.zig"),
1110
.target = target,
1211
.optimize = optimize,
1312
});
13+
const lib = b.addLibrary(.{
14+
.name = "game_admin_ffi",
15+
.root_module = ffi_mod,
16+
.linkage = .static,
17+
});
1418
b.installArtifact(lib);
1519

1620
const tests = b.addTest(.{
17-
.root_source_file = b.path("game_admin_ffi.zig"),
18-
.target = target,
19-
.optimize = optimize,
21+
.root_module = ffi_mod,
2022
});
2123
const run_tests = b.addRunArtifact(tests);
2224
const test_step = b.step("test", "Run FFI tests");

cartridges/github-api-mcp/ffi/github_api_mcp_ffi.zig

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -376,52 +376,35 @@ fn doHttpRequest(
376376

377377
const http_method = parseHttpMethod(method);
378378

379-
// Open the request
380-
var server_header_buffer: [16384]u8 = undefined;
381-
var req = client.open(http_method, uri, .{
382-
.server_header_buffer = &server_header_buffer,
379+
// Fetch the request (Zig 0.15 API — replaces open/send/wait)
380+
var response_storage = std.ArrayList(u8).init(allocator);
381+
defer response_storage.deinit();
382+
383+
const fetch_result = client.fetch(.{
384+
.method = http_method,
385+
.location = .{ .uri = uri },
383386
.extra_headers = &headers_buf,
387+
.payload = body,
388+
.response_storage = .{ .dynamic = &response_storage },
384389
}) catch return -5;
385-
defer req.deinit();
386-
387-
// Set Content-Type and body for methods that have a body
388-
if (body) |b| {
389-
if (b.len > 0) {
390-
req.transfer_encoding = .{ .content_length = b.len };
391-
}
392-
}
393-
394-
// Send the request
395-
req.send() catch return -5;
396-
397-
// Write body if present
398-
if (body) |b| {
399-
if (b.len > 0) {
400-
req.writer().writeAll(b) catch return -5;
401-
}
402-
}
403-
404-
// Finish sending and wait for response
405-
req.finish() catch return -5;
406-
req.wait() catch return -5;
407390

408391
// Handle rate limiting (HTTP 429 or 403 with depleted budget)
409-
const status_code = @intFromEnum(req.response.status);
392+
const status_code = @intFromEnum(fetch_result.status);
410393
if (status_code == 429 or (status_code == 403 and slot.rate_limit.remaining == 0)) {
411394
slot.state = .rate_limited;
412395
return -3;
413396
}
414397

415-
// Read the response body into the caller's output buffer
416-
const response_body = req.reader().readAll(out_buf[0..out_cap]) catch return -5;
417-
418398
// Transition to error on server errors (5xx)
419399
if (status_code >= 500) {
420400
slot.state = .err;
421401
return -5;
422402
}
423403

424-
return @intCast(response_body);
404+
// Copy response body into the caller's output buffer
405+
const to_copy = @min(response_storage.items.len, out_cap);
406+
@memcpy(out_buf[0..to_copy], response_storage.items[0..to_copy]);
407+
return @intCast(to_copy);
425408
}
426409

427410
// ---------------------------------------------------------------------------

cartridges/gitlab-api-mcp/ffi/gitlab_api_mcp_ffi.zig

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -329,46 +329,36 @@ pub export fn gitlab_api_mcp_request(
329329
break :blk body[0..blen];
330330
} else null;
331331

332-
// Open request
333-
var server_header_buffer: [16384]u8 = undefined;
334-
var req = client.open(http_method, uri, .{
335-
.server_header_buffer = &server_header_buffer,
332+
// Fetch the request (Zig 0.15 API — replaces open/send/wait)
333+
var response_storage = std.ArrayList(u8).init(allocator);
334+
defer response_storage.deinit();
335+
336+
const fetch_result = client.fetch(.{
337+
.method = http_method,
338+
.location = .{ .uri = uri },
336339
.extra_headers = &headers_buf,
340+
.payload = body_slice,
341+
.response_storage = .{ .dynamic = &response_storage },
337342
}) catch return -4;
338-
defer req.deinit();
339-
340-
if (body_slice) |b| {
341-
req.transfer_encoding = .{ .content_length = b.len };
342-
}
343-
344-
req.send() catch return -4;
345-
346-
if (body_slice) |b| {
347-
req.writer().writeAll(b) catch return -4;
348-
}
349-
350-
req.finish() catch return -4;
351-
req.wait() catch return -4;
352343

353344
// Handle rate limiting (HTTP 429)
354-
const status_code = @intFromEnum(req.response.status);
345+
const status_code = @intFromEnum(fetch_result.status);
355346
if (status_code == 429) {
356347
slot.state = .rate_limited;
357348
slot.rate_limit_remaining = 0;
358349
return -3;
359350
}
360351

361-
// Read response body
362-
const bytes_read = req.reader().readAll(out_buf[0..cap]) catch return -4;
363-
364352
// Handle server errors
353+
const bytes_read = @min(response_storage.items.len, cap);
354+
@memcpy(out_buf[0..bytes_read], response_storage.items[0..bytes_read]);
355+
out_len.* = @intCast(bytes_read);
356+
365357
if (status_code >= 500) {
366358
slot.state = .err;
367-
out_len.* = @intCast(bytes_read);
368359
return -4;
369360
}
370361

371-
out_len.* = @intCast(bytes_read);
372362
return 0;
373363
}
374364

@@ -445,27 +435,27 @@ pub export fn gitlab_api_mcp_graphql(
445435
.{ .name = "User-Agent", .value = "boj-server/1.0 (gitlab-api-mcp cartridge)" },
446436
};
447437

448-
var server_header_buffer: [16384]u8 = undefined;
449-
var req = client.open(.POST, uri, .{
450-
.server_header_buffer = &server_header_buffer,
438+
// Fetch the request (Zig 0.15 API — replaces open/send/wait)
439+
var response_storage = std.ArrayList(u8).init(allocator);
440+
defer response_storage.deinit();
441+
442+
const fetch_result = client.fetch(.{
443+
.method = .POST,
444+
.location = .{ .uri = uri },
451445
.extra_headers = &headers_buf,
446+
.payload = gql_body,
447+
.response_storage = .{ .dynamic = &response_storage },
452448
}) catch return -4;
453-
defer req.deinit();
454449

455-
req.transfer_encoding = .{ .content_length = gql_body.len };
456-
req.send() catch return -4;
457-
req.writer().writeAll(gql_body) catch return -4;
458-
req.finish() catch return -4;
459-
req.wait() catch return -4;
460-
461-
const status_code = @intFromEnum(req.response.status);
450+
const status_code = @intFromEnum(fetch_result.status);
462451
if (status_code == 429) {
463452
slot.state = .rate_limited;
464453
slot.rate_limit_remaining = 0;
465454
return -3;
466455
}
467456

468-
const bytes_read = req.reader().readAll(out_buf[0..cap]) catch return -5;
457+
const bytes_read = @min(response_storage.items.len, cap);
458+
@memcpy(out_buf[0..bytes_read], response_storage.items[0..bytes_read]);
469459
out_len.* = @intCast(bytes_read);
470460
return 0;
471461
}
@@ -537,27 +527,27 @@ pub export fn gitlab_api_mcp_setup_mirror(
537527
.{ .name = "User-Agent", .value = "boj-server/1.0 (gitlab-api-mcp cartridge)" },
538528
};
539529

540-
var server_header_buffer: [16384]u8 = undefined;
541-
var req = client.open(.POST, uri, .{
542-
.server_header_buffer = &server_header_buffer,
530+
// Fetch the request (Zig 0.15 API — replaces open/send/wait)
531+
var response_storage = std.ArrayList(u8).init(allocator);
532+
defer response_storage.deinit();
533+
534+
const fetch_result = client.fetch(.{
535+
.method = .POST,
536+
.location = .{ .uri = uri },
543537
.extra_headers = &headers_buf_mirror,
538+
.payload = mirror_body,
539+
.response_storage = .{ .dynamic = &response_storage },
544540
}) catch return -4;
545-
defer req.deinit();
546-
547-
req.transfer_encoding = .{ .content_length = mirror_body.len };
548-
req.send() catch return -4;
549-
req.writer().writeAll(mirror_body) catch return -4;
550-
req.finish() catch return -4;
551-
req.wait() catch return -4;
552541

553-
const status_code = @intFromEnum(req.response.status);
542+
const status_code = @intFromEnum(fetch_result.status);
554543
if (status_code == 429) {
555544
slot.state = .rate_limited;
556545
slot.rate_limit_remaining = 0;
557546
return -3;
558547
}
559548

560-
const bytes_read = req.reader().readAll(out_buf[0..cap]) catch return -5;
549+
const bytes_read = @min(response_storage.items.len, cap);
550+
@memcpy(out_buf[0..bytes_read], response_storage.items[0..bytes_read]);
561551
out_len.* = @intCast(bytes_read);
562552
return 0;
563553
}

cartridges/idaptik-admin-mcp/ffi/build.zig

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ pub fn build(b: *std.Build) void {
55
const target = b.standardTargetOptions(.{});
66
const optimize = b.standardOptimizeOption(.{});
77

8-
const lib = b.addStaticLibrary(.{
9-
.name = "idaptik_admin_ffi",
8+
const ffi_mod = b.addModule("idaptik_admin_ffi", .{
109
.root_source_file = b.path("idaptik_admin_ffi.zig"),
1110
.target = target,
1211
.optimize = optimize,
1312
});
13+
const lib = b.addLibrary(.{
14+
.name = "idaptik_admin_ffi",
15+
.root_module = ffi_mod,
16+
.linkage = .static,
17+
});
1418
b.installArtifact(lib);
1519

1620
const tests = b.addTest(.{
17-
.root_source_file = b.path("idaptik_admin_ffi.zig"),
18-
.target = target,
19-
.optimize = optimize,
21+
.root_module = ffi_mod,
2022
});
2123
const run_tests = b.addRunArtifact(tests);
2224
const test_step = b.step("test", "Run FFI tests");

cartridges/jira-mcp/ffi/jira_mcp_ffi.zig

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -249,38 +249,31 @@ fn doJiraApiCall(slot: *SessionSlot, endpoint: []const u8, http_method: []const
249249
else
250250
.GET;
251251

252-
var server_header_buffer: [16384]u8 = undefined;
253-
var req = client.open(method, uri, .{
254-
.server_header_buffer = &server_header_buffer,
255-
.extra_headers = &headers_buf,
256-
}) catch return 0;
257-
defer req.deinit();
258-
259-
// Set body for POST/PUT
252+
// Fetch the request (Zig 0.15 API — replaces open/send/wait)
260253
const body = params_json orelse "";
261-
if (body.len > 0 and (method == .POST or method == .PUT)) {
262-
req.transfer_encoding = .{ .content_length = body.len };
263-
}
264-
265-
req.send() catch return 0;
254+
const payload: ?[]const u8 = if (body.len > 0 and (method == .POST or method == .PUT)) body else null;
255+
var response_storage = std.ArrayList(u8).init(allocator);
256+
defer response_storage.deinit();
266257

267-
if (body.len > 0 and (method == .POST or method == .PUT)) {
268-
req.writer().writeAll(body) catch return 0;
269-
}
270-
271-
req.finish() catch return 0;
272-
req.wait() catch return 0;
258+
const fetch_result = client.fetch(.{
259+
.method = method,
260+
.location = .{ .uri = uri },
261+
.extra_headers = &headers_buf,
262+
.payload = payload,
263+
.response_storage = .{ .dynamic = &response_storage },
264+
}) catch return 0;
273265

274266
// Check for rate limiting (HTTP 429)
275-
const status_code = @intFromEnum(req.response.status);
267+
const status_code = @intFromEnum(fetch_result.status);
276268
if (status_code == 429) {
277269
slot.state = .rate_limited;
278270
return 0;
279271
}
280272

281-
// Read response body
282-
const bytes_read = req.reader().readAll(out_buf) catch return 0;
283-
return bytes_read;
273+
// Copy response body into the caller's output buffer
274+
const to_copy = @min(response_storage.items.len, out_buf.len);
275+
@memcpy(out_buf[0..to_copy], response_storage.items[0..to_copy]);
276+
return to_copy;
284277
}
285278

286279
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)