Skip to content

Commit 3e78f5a

Browse files
hyperpolymathclaude
andcommitted
feat(adr-0006): migrate 5 cartridges to 5-symbol ABI (Pattern A batch 3)
Pattern A migration (git-mcp, iac-mcp, k8s-mcp, lang-mcp, local-coord-mcp). boj_cartridge_invoke dispatch added via cartridge_shim. Tests passing per cartridge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 650d8dd commit 3e78f5a

10 files changed

Lines changed: 451 additions & 0 deletions

File tree

cartridges/git-mcp/ffi/build.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,26 @@ pub fn build(b: *std.Build) void {
99
const target = b.standardTargetOptions(.{});
1010
const optimize = b.standardOptimizeOption(.{});
1111

12+
// Shared ADR-0006 invoke-shim module (relative path up to boj-server trunk).
13+
14+
const shim_mod = b.addModule("cartridge_shim", .{
15+
16+
.root_source_file = b.path("../../../ffi/zig/src/cartridge_shim.zig"),
17+
18+
.target = target,
19+
20+
.optimize = optimize,
21+
22+
});
23+
1224
const git_mod = b.addModule("git_ffi", .{
1325
.root_source_file = b.path("git_ffi.zig"),
1426
.target = target,
1527
.optimize = optimize,
1628
});
1729

30+
git_mod.addImport("cartridge_shim", shim_mod);
31+
1832
// ── Tests ────────────────────────────────────────────────────────
1933
const git_tests = b.addTest(.{
2034
.root_module = git_mod,

cartridges/git-mcp/ffi/git_ffi.zig

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,43 @@ pub export fn boj_cartridge_version() [*:0]const u8 {
206206
return "0.1.0";
207207
}
208208

209+
// ═══════════════════════════════════════════════════════════════════════
210+
// ADR-0006 dispatch (boj_cartridge_invoke, 5th standard symbol)
211+
// ═══════════════════════════════════════════════════════════════════════
212+
213+
const shim = @import("cartridge_shim");
214+
215+
/// Dispatch the cartridge.json MCP tools. Grade D Alpha — each arm
216+
/// returns a stub JSON body shaped to the tool's intended response.
217+
export fn boj_cartridge_invoke(
218+
tool_name: [*c]const u8,
219+
json_args: [*c]const u8,
220+
out_buf: [*c]u8,
221+
in_out_len: [*c]usize,
222+
) callconv(.c) i32 {
223+
_ = json_args;
224+
if (shim.invokeArgsNull(tool_name, out_buf, in_out_len)) return shim.RC_BAD_ARGS;
225+
226+
const body: []const u8 = if (shim.toolIs(tool_name, "git_authenticate"))
227+
"{\"result\":{\"status\":\"stub\"}}"
228+
else if (shim.toolIs(tool_name, "git_select_repo"))
229+
"{\"result\":{\"status\":\"stub\"}}"
230+
else if (shim.toolIs(tool_name, "git_status"))
231+
"{\"result\":{\"metadata\":{},\"status\":\"stub\"}}"
232+
else if (shim.toolIs(tool_name, "git_log"))
233+
"{\"result\":{\"status\":\"stub\"}}"
234+
else if (shim.toolIs(tool_name, "git_diff"))
235+
"{\"result\":{\"status\":\"stub\"}}"
236+
else if (shim.toolIs(tool_name, "git_create_branch"))
237+
"{\"result\":{\"status\":\"stub\"}}"
238+
else if (shim.toolIs(tool_name, "git_push"))
239+
"{\"result\":{\"status\":\"stub\"}}"
240+
else
241+
return shim.RC_UNKNOWN_TOOL;
242+
243+
return shim.writeResult(out_buf, in_out_len, body);
244+
}
245+
209246
// ═══════════════════════════════════════════════════════════════════════
210247
// Tests
211248
// ═══════════════════════════════════════════════════════════════════════
@@ -270,3 +307,41 @@ test "state transition validation" {
270307
try std.testing.expectEqual(@as(c_int, 0), git_can_transition(0, 3)); // unauth -> operating
271308
try std.testing.expectEqual(@as(c_int, 0), git_can_transition(3, 0)); // operating -> unauth
272309
}
310+
311+
// ═══════════════════════════════════════════════════════════════════════
312+
// ADR-0006 invoke dispatch tests
313+
// ═══════════════════════════════════════════════════════════════════════
314+
315+
test "invoke: each declared tool succeeds" {
316+
var buf: [256]u8 = undefined;
317+
const tools = [_][]const u8{
318+
"git_authenticate",
319+
"git_select_repo",
320+
"git_status",
321+
"git_log",
322+
"git_diff",
323+
"git_create_branch",
324+
"git_push",
325+
};
326+
for (tools) |t| {
327+
var len: usize = buf.len;
328+
const rc = boj_cartridge_invoke(t.ptr, "{}", &buf, &len);
329+
try std.testing.expectEqual(@as(i32, 0), rc);
330+
try std.testing.expect(std.mem.indexOf(u8, buf[0..len], "result") != null);
331+
}
332+
}
333+
334+
test "invoke: unknown tool returns -1" {
335+
var buf: [64]u8 = undefined;
336+
var len: usize = buf.len;
337+
const rc = boj_cartridge_invoke("nope", "{}", &buf, &len);
338+
try std.testing.expectEqual(@as(i32, -1), rc);
339+
}
340+
341+
test "invoke: buffer too small returns -3" {
342+
var buf: [4]u8 = undefined;
343+
var len: usize = buf.len;
344+
const rc = boj_cartridge_invoke("git_authenticate", "{}", &buf, &len);
345+
try std.testing.expectEqual(@as(i32, -3), rc);
346+
try std.testing.expect(len > 4);
347+
}

cartridges/iac-mcp/ffi/build.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,26 @@ pub fn build(b: *std.Build) void {
99
const target = b.standardTargetOptions(.{});
1010
const optimize = b.standardOptimizeOption(.{});
1111

12+
// Shared ADR-0006 invoke-shim module (relative path up to boj-server trunk).
13+
14+
const shim_mod = b.addModule("cartridge_shim", .{
15+
16+
.root_source_file = b.path("../../../ffi/zig/src/cartridge_shim.zig"),
17+
18+
.target = target,
19+
20+
.optimize = optimize,
21+
22+
});
23+
1224
const iac_mod = b.addModule("iac_ffi", .{
1325
.root_source_file = b.path("iac_ffi.zig"),
1426
.target = target,
1527
.optimize = optimize,
1628
});
1729

30+
iac_mod.addImport("cartridge_shim", shim_mod);
31+
1832
// ── Tests ────────────────────────────────────────────────────────
1933
const iac_tests = b.addTest(.{
2034
.root_module = iac_mod,

cartridges/iac-mcp/ffi/iac_ffi.zig

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,43 @@ pub export fn boj_cartridge_version() [*:0]const u8 {
194194
return "0.1.0";
195195
}
196196

197+
// ═══════════════════════════════════════════════════════════════════════
198+
// ADR-0006 dispatch (boj_cartridge_invoke, 5th standard symbol)
199+
// ═══════════════════════════════════════════════════════════════════════
200+
201+
const shim = @import("cartridge_shim");
202+
203+
/// Dispatch the cartridge.json MCP tools. Grade D Alpha — each arm
204+
/// returns a stub JSON body shaped to the tool's intended response.
205+
export fn boj_cartridge_invoke(
206+
tool_name: [*c]const u8,
207+
json_args: [*c]const u8,
208+
out_buf: [*c]u8,
209+
in_out_len: [*c]usize,
210+
) callconv(.c) i32 {
211+
_ = json_args;
212+
if (shim.invokeArgsNull(tool_name, out_buf, in_out_len)) return shim.RC_BAD_ARGS;
213+
214+
const body: []const u8 = if (shim.toolIs(tool_name, "iac_init"))
215+
"{\"result\":{\"status\":\"stub\"}}"
216+
else if (shim.toolIs(tool_name, "iac_plan"))
217+
"{\"result\":{\"status\":\"stub\"}}"
218+
else if (shim.toolIs(tool_name, "iac_apply"))
219+
"{\"result\":{\"status\":\"stub\"}}"
220+
else if (shim.toolIs(tool_name, "iac_destroy"))
221+
"{\"result\":{\"status\":\"stub\"}}"
222+
else if (shim.toolIs(tool_name, "iac_state"))
223+
"{\"result\":{\"status\":\"stub\"}}"
224+
else if (shim.toolIs(tool_name, "iac_output"))
225+
"{\"result\":{\"status\":\"stub\"}}"
226+
else if (shim.toolIs(tool_name, "iac_release"))
227+
"{\"result\":{\"status\":\"stub\"}}"
228+
else
229+
return shim.RC_UNKNOWN_TOOL;
230+
231+
return shim.writeResult(out_buf, in_out_len, body);
232+
}
233+
197234
// ═══════════════════════════════════════════════════════════════════════
198235
// Tests
199236
// ═══════════════════════════════════════════════════════════════════════
@@ -262,3 +299,41 @@ test "state transition validation" {
262299
try std.testing.expectEqual(@as(c_int, 0), iac_can_transition(0, 2)); // uninit -> planned
263300
try std.testing.expectEqual(@as(c_int, 0), iac_can_transition(2, 0)); // planned -> uninit
264301
}
302+
303+
// ═══════════════════════════════════════════════════════════════════════
304+
// ADR-0006 invoke dispatch tests
305+
// ═══════════════════════════════════════════════════════════════════════
306+
307+
test "invoke: each declared tool succeeds" {
308+
var buf: [256]u8 = undefined;
309+
const tools = [_][]const u8{
310+
"iac_init",
311+
"iac_plan",
312+
"iac_apply",
313+
"iac_destroy",
314+
"iac_state",
315+
"iac_output",
316+
"iac_release",
317+
};
318+
for (tools) |t| {
319+
var len: usize = buf.len;
320+
const rc = boj_cartridge_invoke(t.ptr, "{}", &buf, &len);
321+
try std.testing.expectEqual(@as(i32, 0), rc);
322+
try std.testing.expect(std.mem.indexOf(u8, buf[0..len], "result") != null);
323+
}
324+
}
325+
326+
test "invoke: unknown tool returns -1" {
327+
var buf: [64]u8 = undefined;
328+
var len: usize = buf.len;
329+
const rc = boj_cartridge_invoke("nope", "{}", &buf, &len);
330+
try std.testing.expectEqual(@as(i32, -1), rc);
331+
}
332+
333+
test "invoke: buffer too small returns -3" {
334+
var buf: [4]u8 = undefined;
335+
var len: usize = buf.len;
336+
const rc = boj_cartridge_invoke("iac_init", "{}", &buf, &len);
337+
try std.testing.expectEqual(@as(i32, -3), rc);
338+
try std.testing.expect(len > 4);
339+
}

cartridges/k8s-mcp/ffi/build.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,26 @@ pub fn build(b: *std.Build) void {
99
const target = b.standardTargetOptions(.{});
1010
const optimize = b.standardOptimizeOption(.{});
1111

12+
// Shared ADR-0006 invoke-shim module (relative path up to boj-server trunk).
13+
14+
const shim_mod = b.addModule("cartridge_shim", .{
15+
16+
.root_source_file = b.path("../../../ffi/zig/src/cartridge_shim.zig"),
17+
18+
.target = target,
19+
20+
.optimize = optimize,
21+
22+
});
23+
1224
const k8s_mod = b.addModule("k8s_ffi", .{
1325
.root_source_file = b.path("k8s_ffi.zig"),
1426
.target = target,
1527
.optimize = optimize,
1628
});
1729

30+
k8s_mod.addImport("cartridge_shim", shim_mod);
31+
1832
// ── Tests ────────────────────────────────────────────────────────
1933
const k8s_tests = b.addTest(.{
2034
.root_module = k8s_mod,

cartridges/k8s-mcp/ffi/k8s_ffi.zig

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,45 @@ pub export fn boj_cartridge_version() [*:0]const u8 {
205205
return "0.1.0";
206206
}
207207

208+
// ═══════════════════════════════════════════════════════════════════════
209+
// ADR-0006 dispatch (boj_cartridge_invoke, 5th standard symbol)
210+
// ═══════════════════════════════════════════════════════════════════════
211+
212+
const shim = @import("cartridge_shim");
213+
214+
/// Dispatch the cartridge.json MCP tools. Grade D Alpha — each arm
215+
/// returns a stub JSON body shaped to the tool's intended response.
216+
export fn boj_cartridge_invoke(
217+
tool_name: [*c]const u8,
218+
json_args: [*c]const u8,
219+
out_buf: [*c]u8,
220+
in_out_len: [*c]usize,
221+
) callconv(.c) i32 {
222+
_ = json_args;
223+
if (shim.invokeArgsNull(tool_name, out_buf, in_out_len)) return shim.RC_BAD_ARGS;
224+
225+
const body: []const u8 = if (shim.toolIs(tool_name, "k8s_connect"))
226+
"{\"result\":{\"status\":\"stub\"}}"
227+
else if (shim.toolIs(tool_name, "k8s_list_pods"))
228+
"{\"result\":{\"items\":[],\"count\":0,\"status\":\"stub\"}}"
229+
else if (shim.toolIs(tool_name, "k8s_get_pod"))
230+
"{\"result\":{\"metadata\":{},\"status\":\"stub\"}}"
231+
else if (shim.toolIs(tool_name, "k8s_list_deployments"))
232+
"{\"result\":{\"items\":[],\"count\":0,\"status\":\"stub\"}}"
233+
else if (shim.toolIs(tool_name, "k8s_apply"))
234+
"{\"result\":{\"status\":\"stub\"}}"
235+
else if (shim.toolIs(tool_name, "k8s_delete"))
236+
"{\"result\":{\"status\":\"stub\"}}"
237+
else if (shim.toolIs(tool_name, "k8s_logs"))
238+
"{\"result\":{\"status\":\"stub\"}}"
239+
else if (shim.toolIs(tool_name, "k8s_disconnect"))
240+
"{\"result\":{\"status\":\"stub\"}}"
241+
else
242+
return shim.RC_UNKNOWN_TOOL;
243+
244+
return shim.writeResult(out_buf, in_out_len, body);
245+
}
246+
208247
// ═══════════════════════════════════════════════════════════════════════
209248
// Tests
210249
// ═══════════════════════════════════════════════════════════════════════
@@ -297,3 +336,42 @@ test "max clusters enforced" {
297336
// Next connect should fail
298337
try std.testing.expectEqual(@as(c_int, -1), k8s_connect(@intFromEnum(K8sTool.kubectl)));
299338
}
339+
340+
// ═══════════════════════════════════════════════════════════════════════
341+
// ADR-0006 invoke dispatch tests
342+
// ═══════════════════════════════════════════════════════════════════════
343+
344+
test "invoke: each declared tool succeeds" {
345+
var buf: [256]u8 = undefined;
346+
const tools = [_][]const u8{
347+
"k8s_connect",
348+
"k8s_list_pods",
349+
"k8s_get_pod",
350+
"k8s_list_deployments",
351+
"k8s_apply",
352+
"k8s_delete",
353+
"k8s_logs",
354+
"k8s_disconnect",
355+
};
356+
for (tools) |t| {
357+
var len: usize = buf.len;
358+
const rc = boj_cartridge_invoke(t.ptr, "{}", &buf, &len);
359+
try std.testing.expectEqual(@as(i32, 0), rc);
360+
try std.testing.expect(std.mem.indexOf(u8, buf[0..len], "result") != null);
361+
}
362+
}
363+
364+
test "invoke: unknown tool returns -1" {
365+
var buf: [64]u8 = undefined;
366+
var len: usize = buf.len;
367+
const rc = boj_cartridge_invoke("nope", "{}", &buf, &len);
368+
try std.testing.expectEqual(@as(i32, -1), rc);
369+
}
370+
371+
test "invoke: buffer too small returns -3" {
372+
var buf: [4]u8 = undefined;
373+
var len: usize = buf.len;
374+
const rc = boj_cartridge_invoke("k8s_connect", "{}", &buf, &len);
375+
try std.testing.expectEqual(@as(i32, -3), rc);
376+
try std.testing.expect(len > 4);
377+
}

cartridges/lang-mcp/ffi/build.zig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,26 @@ pub fn build(b: *std.Build) void {
99
const target = b.standardTargetOptions(.{});
1010
const optimize = b.standardOptimizeOption(.{});
1111

12+
// Shared ADR-0006 invoke-shim module (relative path up to boj-server trunk).
13+
14+
const shim_mod = b.addModule("cartridge_shim", .{
15+
16+
.root_source_file = b.path("../../../ffi/zig/src/cartridge_shim.zig"),
17+
18+
.target = target,
19+
20+
.optimize = optimize,
21+
22+
});
23+
1224
const lang_mod = b.addModule("lang_ffi", .{
1325
.root_source_file = b.path("lang_ffi.zig"),
1426
.target = target,
1527
.optimize = optimize,
1628
});
1729

30+
lang_mod.addImport("cartridge_shim", shim_mod);
31+
1832
const lang_tests = b.addTest(.{ .root_module = lang_mod });
1933
const run_tests = b.addRunArtifact(lang_tests);
2034
const test_step = b.step("test", "Run lang-mcp FFI tests");

0 commit comments

Comments
 (0)