feat(gemini): plexum-host MCP exposure + real consume mirror
Same architecture as the codex sister change:
1. mcp-host subcommand routes argv[1] to mcpbridge.Run, reading
PLEXUM_MCP_SOCKET / PLEXUM_MCP_AGENT_ID from gemini-cli's
`gemini mcp add --env` env baking.
2. EnsureGeminiMCPRegistered handles per-agent stable registration
under `-s user --trust` so gemini auto-approves the tool surface.
3. Post-turn ParseGeminiToolCalls + EmitGeminiToolCalls scans the
chat JSONL at ~/.gemini/tmp/<ws>/chats/session-*.jsonl. Pairs
gemini's nested `toolCalls[].id` with the matching
`functionResponse.id` from later user lines.
4. MutateGeminiSession rewrites the chat JSONL: toolCalls[].args
→ {} (heavy), functionResponse.response.output → marker.
E2E verified: gemini call exec via plexum-host → dynamic.jsonl
records the call → dynamic-tool-clear consumes → gemini session
mirrored → resume gemini sees consumed marker not the original.
This commit is contained in:
70
internal/runner/mcp_register.go
Normal file
70
internal/runner/mcp_register.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// MCP server registration. gemini's `gemini mcp add -s user` writes
|
||||
// into the user's settings.json; we use a per-agent stable name so
|
||||
// multiple Plexum agents using gemini don't collide. Registration is
|
||||
// idempotent and performed lazily before the first turn that needs it.
|
||||
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var registered sync.Map // agentID → bool (registration done this process)
|
||||
|
||||
// EnsureGeminiMCPRegistered makes sure gemini's user settings has an
|
||||
// MCP server entry for this agent named plexum-host-<sanitized>.
|
||||
// Socket path is the stable StableSocketPath(agentID); same path the
|
||||
// per-turn bridge listens on.
|
||||
func EnsureGeminiMCPRegistered(ctx context.Context, geminiBinary, pluginBinary, agentID string) (string, error) {
|
||||
name := geminiServerName(agentID)
|
||||
if _, ok := registered.Load(name); ok {
|
||||
return name, nil
|
||||
}
|
||||
// Remove first so we don't double-register; ignore error.
|
||||
_ = exec.CommandContext(ctx, geminiBinary, "mcp", "remove", "-s", "user", name).Run()
|
||||
|
||||
args := []string{
|
||||
"mcp", "add",
|
||||
"-s", "user", "--trust",
|
||||
"-e", "PLEXUM_MCP_SOCKET=" + StableSocketPath(agentID),
|
||||
"-e", "PLEXUM_MCP_AGENT_ID=" + agentID,
|
||||
name, pluginBinary, "mcp-host",
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, geminiBinary, args...)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return "", fmt.Errorf("gemini mcp add: %w: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
registered.Store(name, true)
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// StableSocketPath returns the per-agent unix socket path the bridge
|
||||
// listens on AND gemini's mcp-host subprocess dials into.
|
||||
func StableSocketPath(agentID string) string {
|
||||
return filepath.Join(os.TempDir(), "plexum-gemini-mcp-"+sanitize(agentID)+".sock")
|
||||
}
|
||||
|
||||
func geminiServerName(agentID string) string {
|
||||
return "plexum-host-" + sanitize(agentID)
|
||||
}
|
||||
|
||||
func sanitize(s string) string {
|
||||
out := make([]byte, 0, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z',
|
||||
c >= '0' && c <= '9', c == '-', c == '_':
|
||||
out = append(out, c)
|
||||
default:
|
||||
out = append(out, '_')
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
Reference in New Issue
Block a user