diff --git a/bridged/src/main/java/dev/ltms/bridged/Bridged.java b/bridged/src/main/java/dev/ltms/bridged/Bridged.java
index 1f10f10..fc1131f 100644
--- a/bridged/src/main/java/dev/ltms/bridged/Bridged.java
+++ b/bridged/src/main/java/dev/ltms/bridged/Bridged.java
@@ -69,7 +69,7 @@ public final class Bridged {
// MCP server face (CB-105): bridge_send/bridge_reply/bridge_status, mounted at /mcp.
// Caller identity is resolved from the connection (peer PID → herdr pane), not arguments.
ConnectionIdentity identity = new ConnectionIdentity(new PaneLocator(herdr), new LsofPeerPidLookup());
- BridgeMcp mcp = new BridgeMcp(messages, rendezvous, identity);
+ BridgeMcp mcp = new BridgeMcp(messages, rendezvous, workers, identity);
Runtime.getRuntime().addShutdownHook(new Thread(mcp::close));
Javalin app = new BridgedApp(herdr, workers, messages, rendezvous, mcp.servlet()).build();
app.start(cfg.bind().host(), cfg.bind().port());
diff --git a/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java b/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java
index db19a7b..fb0548e 100644
--- a/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java
+++ b/bridged/src/main/java/dev/ltms/bridged/mcp/BridgeMcp.java
@@ -1,8 +1,11 @@
package dev.ltms.bridged.mcp;
+import dev.ltms.bridged.guard.GuardException;
+import dev.ltms.bridged.herdr.Agent;
import dev.ltms.bridged.herdr.HerdrException;
import dev.ltms.bridged.msg.MessageService;
import dev.ltms.bridged.msg.Rendezvous;
+import dev.ltms.bridged.worker.WorkerService;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.jackson3.JacksonMcpJsonMapperSupplier;
@@ -11,8 +14,10 @@ import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
+import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServlet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -22,6 +27,10 @@ import java.util.Map;
* validated by parity, not by re-implementing behaviour. The primary Opus calls {@code bridge_send}
* / {@code bridge_status}; the worker calls {@code bridge_reply}.
*
+ *
Beyond delegation the primary also manages the fleet here (CB-108): {@code bridge_spawn} /
+ * {@code bridge_list} / {@code bridge_stop} adapt {@link WorkerService} so a worker's whole lifecycle
+ * is driven through MCP, with the subscription boundary still enforced inside {@code WorkerService}.
+ *
*
The tool logic lives in package-private static methods returning a
* {@link McpSchema.CallToolResult}, so it is unit-testable without standing up the HTTP transport;
* the SDK owns the wire protocol. Mount {@link #servlet()} at {@code /mcp} on the daemon's Jetty.
@@ -30,6 +39,7 @@ public final class BridgeMcp {
private static final long DEFAULT_TIMEOUT_MS = 25_000;
private static final long MAX_TIMEOUT_MS = 120_000;
+ private static final ObjectMapper MAPPER = new ObjectMapper(); // worker-view JSON projections
/** Transport-context key under which the extractor stashes the resolved caller identity. */
static final String CALLER_TERMINAL = "callerTerminal";
@@ -37,7 +47,8 @@ public final class BridgeMcp {
private final HttpServletStreamableServerTransportProvider transport;
private final McpSyncServer server;
- public BridgeMcp(MessageService messages, Rendezvous rendezvous, ConnectionIdentity identity) {
+ public BridgeMcp(MessageService messages, Rendezvous rendezvous, WorkerService workers,
+ ConnectionIdentity identity) {
McpJsonMapper json = new JacksonMcpJsonMapperSupplier().get();
this.transport = HttpServletStreamableServerTransportProvider.builder()
.jsonMapper(json)
@@ -64,6 +75,10 @@ public final class BridgeMcp {
status(messages, str(req.arguments(), "sessionId")))
.toolCall(pollTool(), (_, req) ->
poll(messages, str(req.arguments(), "ticket")))
+ // Fleet management (CB-108): spawn/list/stop over WorkerService.
+ .toolCall(spawnTool(), (_, _) -> spawn(workers))
+ .toolCall(listTool(), (_, _) -> listWorkers(workers))
+ .toolCall(stopTool(), (_, req) -> stop(workers, str(req.arguments(), "paneId")))
.build();
}
@@ -174,6 +189,59 @@ public final class BridgeMcp {
}
}
+ // --- fleet management logic (CB-108) -------------------------------------------------------
+
+ /** {@code bridge_spawn}: launch a guard-checked worker and return its session id + pane id. */
+ static McpSchema.CallToolResult spawn(WorkerService workers) {
+ try {
+ return text(json(workerView(workers.spawn())));
+ } catch (GuardException e) {
+ return error("subscription boundary: " + e.getMessage());
+ } catch (HerdrException e) {
+ return error("herdr error spawning worker: " + e.getMessage());
+ }
+ }
+
+ /** {@code bridge_list}: every worker herdr tracks (session id, pane, status). */
+ static McpSchema.CallToolResult listWorkers(WorkerService workers) {
+ try {
+ List