Slice 7: expose git commands as MCP tools
Add git_fetch/git_pull/git_push/git_commit/git_discard_changes MCP tools as thin adapters over the service (actor=claude), completing 1.7 symmetry so Claude can run the same commands as the right-click menu. git_discard_changes is flagged destructive (confirm first, 1.4). Extended the MCP test with a git_commit round-trip; synced AGENT.md 8.1 tool list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -365,8 +365,9 @@ obeys the safety rules (§1.4).
|
|||||||
the tool handlers (§1.7). Expected tools (grow as features land):
|
the tool handlers (§1.7). Expected tools (grow as features land):
|
||||||
- Read: `list_repos`, `get_repo`, `get_active_project`, `get_activity`,
|
- Read: `list_repos`, `get_repo`, `get_active_project`, `get_activity`,
|
||||||
`get_pending_switch`, `list_prs`.
|
`get_pending_switch`, `list_prs`.
|
||||||
- Act: `git_status/checkout/commit/push/pull/create_branch`,
|
- Act: `git_fetch`, `git_pull`, `git_push`, `git_commit`,
|
||||||
`create_pr`, `merge_and_cleanup_pr`, `set_active_project`, `ack_switch`.
|
`git_discard_changes`, `merge_and_cleanup_pr`, `set_active_project`,
|
||||||
|
`ack_switch`. (More — `git_checkout`, `create_branch`, `create_pr` — as they land.)
|
||||||
- **A tool's result type must be a struct, never a bare slice/map/scalar.** The
|
- **A tool's result type must be a struct, never a bare slice/map/scalar.** The
|
||||||
go-sdk infers each tool's `outputSchema` from its handler's result type, and MCP
|
go-sdk infers each tool's `outputSchema` from its handler's result type, and MCP
|
||||||
structured output must be a JSON **object** (`type: "object"`). A handler that
|
structured output must be a JSON **object** (`type: "object"`). A handler that
|
||||||
|
|||||||
@@ -188,3 +188,16 @@ Append-only running history of all changes (AGENT.md §9.1). Newest last.
|
|||||||
`cmd/server/main.go`, `components/repo-menu` (new), `components/repo-list`,
|
`cmd/server/main.go`, `components/repo-menu` (new), `components/repo-list`,
|
||||||
`web/templates/{index,help}.html`.
|
`web/templates/{index,help}.html`.
|
||||||
- **Next:** expose these git ops as MCP tools so Claude can run them too.
|
- **Next:** expose these git ops as MCP tools so Claude can run them too.
|
||||||
|
|
||||||
|
## 2026-09-20 — Slice 7: git commands as MCP tools (§1.7 symmetry)
|
||||||
|
- **What:** Added MCP tools `git_fetch`, `git_pull`, `git_push`, `git_commit`,
|
||||||
|
and `git_discard_changes` — thin adapters over the existing service methods
|
||||||
|
(actor=claude), so Claude can run the same commands as the right-click menu.
|
||||||
|
`git_discard_changes`'s description flags it destructive and tells Claude to
|
||||||
|
confirm first (§1.4). Extended the MCP test with a `git_commit` round-trip.
|
||||||
|
Synced AGENT.md §8.1's tool list to the actual names.
|
||||||
|
- **Why:** Complete the §1.7 symmetry — every capability reachable from both the
|
||||||
|
GUI and Claude.
|
||||||
|
- **Affects:** `internal/mcp` (+test), `AGENT.md` (§8.1).
|
||||||
|
- **Note:** the new tools appear in Claude Desktop only after its next restart
|
||||||
|
(tool list cached per connection); network ops still need container git creds.
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ type mergePRInput struct {
|
|||||||
Number int64 `json:"number" jsonschema:"the pull request number to merge and clean up"`
|
Number int64 `json:"number" jsonschema:"the pull request number to merge and clean up"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// gitCommitInput is the argument schema for git_commit.
|
||||||
|
type gitCommitInput struct {
|
||||||
|
Path string `json:"path" jsonschema:"absolute path of the repository, from list_repos"`
|
||||||
|
Message string `json:"message" jsonschema:"the commit message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gitActionOutput carries a git command's output (object, per the schema rule).
|
||||||
|
type gitActionOutput struct {
|
||||||
|
Output string `json:"output" jsonschema:"the git command output (may be empty)"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewServer builds the MCP server and registers the (currently read-only) tools.
|
// NewServer builds the MCP server and registers the (currently read-only) tools.
|
||||||
func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
||||||
s := mcpsdk.NewServer(&mcpsdk.Implementation{
|
s := mcpsdk.NewServer(&mcpsdk.Implementation{
|
||||||
@@ -185,6 +196,63 @@ func NewServer(svc *service.Service, version string) *mcpsdk.Server {
|
|||||||
return nil, res, nil
|
return nil, res, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Git commands (the same ops as the right-click menu, §6/§1.7) --------
|
||||||
|
|
||||||
|
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||||
|
Name: "git_fetch",
|
||||||
|
Description: "Fetch updates from the remote for a repository (does not change the working tree). Path is from list_repos.",
|
||||||
|
}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in repoPathInput) (*mcpsdk.CallToolResult, gitActionOutput, error) {
|
||||||
|
out, err := svc.GitFetch(ctx, activity.ActorClaude, in.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gitActionOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, gitActionOutput{Output: out}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||||
|
Name: "git_pull",
|
||||||
|
Description: "Pull the latest changes (fetch + merge) into a repository's current branch. Path is from list_repos.",
|
||||||
|
}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in repoPathInput) (*mcpsdk.CallToolResult, gitActionOutput, error) {
|
||||||
|
out, err := svc.GitPull(ctx, activity.ActorClaude, in.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gitActionOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, gitActionOutput{Output: out}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||||
|
Name: "git_push",
|
||||||
|
Description: "Push the current branch to its upstream (plain push, never forced). Path is from list_repos.",
|
||||||
|
}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in repoPathInput) (*mcpsdk.CallToolResult, gitActionOutput, error) {
|
||||||
|
out, err := svc.GitPush(ctx, activity.ActorClaude, in.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gitActionOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, gitActionOutput{Output: out}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||||
|
Name: "git_commit",
|
||||||
|
Description: "Stage all changes and commit them with a message. Path is from list_repos.",
|
||||||
|
}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in gitCommitInput) (*mcpsdk.CallToolResult, gitActionOutput, error) {
|
||||||
|
out, err := svc.GitCommit(ctx, activity.ActorClaude, in.Path, in.Message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gitActionOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, gitActionOutput{Output: out}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
mcpsdk.AddTool(s, &mcpsdk.Tool{
|
||||||
|
Name: "git_discard_changes",
|
||||||
|
Description: "DESTRUCTIVE: discard ALL uncommitted changes to tracked files (git reset --hard HEAD). This cannot be undone — confirm the exact repository with the user BEFORE calling. Path is from list_repos.",
|
||||||
|
}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in repoPathInput) (*mcpsdk.CallToolResult, gitActionOutput, error) {
|
||||||
|
out, err := svc.GitDiscard(ctx, activity.ActorClaude, in.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gitActionOutput{}, err
|
||||||
|
}
|
||||||
|
return nil, gitActionOutput{Output: out}, nil
|
||||||
|
})
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,21 @@ func TestMCPRoundTrip(t *testing.T) {
|
|||||||
if !res.IsError {
|
if !res.IsError {
|
||||||
t.Fatalf("expected IsError acking with no pending switch")
|
t.Fatalf("expected IsError acking with no pending switch")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- git_commit via MCP (adapter wiring) --------------------------------
|
||||||
|
if err := os.WriteFile(filepath.Join(repoPath, "extra.txt"), []byte("x\n"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res, err = cs.CallTool(ctx, &mcpsdk.CallToolParams{
|
||||||
|
Name: "git_commit",
|
||||||
|
Arguments: map[string]any{"path": repoPath, "message": "add extra via mcp"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("git_commit: %v", err)
|
||||||
|
}
|
||||||
|
if res.IsError {
|
||||||
|
t.Fatalf("git_commit tool error: %+v", res.Content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeResult unmarshals the JSON text content of a tool result into v.
|
// decodeResult unmarshals the JSON text content of a tool result into v.
|
||||||
|
|||||||
Reference in New Issue
Block a user