fix: address re-review findings — nil pointer, races, curl, clone error
Important: - Fix nil pointer dereference on resp.StatusCode when err!=nil (8 sites) - Replace curl shell-out with net/http in monitor inbox check - Handle clone failure in prep.go (was silently swallowed) - Use GitHubOrg() instead of hardcoded "dAppCore" Medium: - Fix JSONL append race (read+write → os.OpenFile O_APPEND) - Remove dead google/mcp/ directory Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
6d04c893b7
commit
e66ea0512b
12 changed files with 92 additions and 334 deletions
|
|
@ -1,67 +0,0 @@
|
|||
# Core CLI MCP Server
|
||||
|
||||
This directory contains an MCP server that exposes the core CLI commands as tools for AI agents.
|
||||
|
||||
## Tools
|
||||
|
||||
### `core_go_test`
|
||||
|
||||
Run Go tests.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `filter` (string, optional): Filter tests by name.
|
||||
- `coverage` (boolean, optional): Enable code coverage. Defaults to `false`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tool": "core_go_test",
|
||||
"parameters": {
|
||||
"filter": "TestMyFunction",
|
||||
"coverage": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `core_dev_health`
|
||||
|
||||
Check the health of the monorepo.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
None.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tool": "core_dev_health",
|
||||
"parameters": {}
|
||||
}
|
||||
```
|
||||
|
||||
### `core_dev_commit`
|
||||
|
||||
Commit changes across repositories.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `message` (string, required): The commit message.
|
||||
- `repos` (array of strings, optional): A list of repositories to commit to.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"tool": "core_dev_commit",
|
||||
"parameters": {
|
||||
"message": "feat: Implement new feature",
|
||||
"repos": [
|
||||
"core-agent",
|
||||
"another-repo"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GoTestRequest struct {
|
||||
Filter string `json:"filter,omitempty"`
|
||||
Coverage bool `json:"coverage,omitempty"`
|
||||
}
|
||||
|
||||
type GoTestResponse struct {
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type DevHealthResponse struct {
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type DevCommitRequest struct {
|
||||
Message string `json:"message"`
|
||||
Repos []string `json:"repos,omitempty"`
|
||||
}
|
||||
|
||||
type DevCommitResponse struct {
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func goTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req GoTestRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{"go", "test"}
|
||||
if req.Filter != "" {
|
||||
args = append(args, "-run", req.Filter)
|
||||
}
|
||||
if req.Coverage {
|
||||
args = append(args, "-cover")
|
||||
}
|
||||
|
||||
cmd := exec.Command("core", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
resp := GoTestResponse{
|
||||
Output: string(output),
|
||||
}
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func devHealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
cmd := exec.Command("core", "dev", "health")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
resp := DevHealthResponse{
|
||||
Output: string(output),
|
||||
}
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func devCommitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req DevCommitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
args := []string{"dev", "commit", "-m", req.Message}
|
||||
if len(req.Repos) > 0 {
|
||||
args = append(args, "--repos", strings.Join(req.Repos, ","))
|
||||
}
|
||||
|
||||
cmd := exec.Command("core", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
resp := DevCommitResponse{
|
||||
Output: string(output),
|
||||
}
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/core_go_test", goTestHandler)
|
||||
http.HandleFunc("/core_dev_health", devHealthHandler)
|
||||
http.HandleFunc("/core_dev_commit", devCommitHandler)
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "MCP Server is running")
|
||||
})
|
||||
|
||||
log.Println("Starting MCP server on :8080")
|
||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||
log.Fatalf("could not start server: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Get the absolute path to the testdata directory
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
testdataPath := filepath.Join(wd, "testdata")
|
||||
|
||||
// Add the absolute path to the PATH
|
||||
os.Setenv("PATH", testdataPath+":"+os.Getenv("PATH"))
|
||||
m.Run()
|
||||
}
|
||||
func TestGoTestHandler(t *testing.T) {
|
||||
reqBody := GoTestRequest{
|
||||
Filter: "TestMyFunction",
|
||||
Coverage: true,
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req, err := http.NewRequest("POST", "/core_go_test", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(goTestHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
var resp GoTestResponse
|
||||
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("could not decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
t.Errorf("handler returned an unexpected error: %v", resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevHealthHandler(t *testing.T) {
|
||||
req, err := http.NewRequest("POST", "/core_dev_health", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(devHealthHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
var resp DevHealthResponse
|
||||
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("could not decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
t.Errorf("handler returned an unexpected error: %v", resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevCommitHandler(t *testing.T) {
|
||||
reqBody := DevCommitRequest{
|
||||
Message: "test commit",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req, err := http.NewRequest("POST", "/core_dev_commit", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
handler := http.HandlerFunc(devCommitHandler)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, http.StatusOK)
|
||||
}
|
||||
|
||||
var resp DevCommitResponse
|
||||
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("could not decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
t.Errorf("handler returned an unexpected error: %v", resp.Error)
|
||||
}
|
||||
}
|
||||
2
google/mcp/testdata/core
vendored
2
google/mcp/testdata/core
vendored
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/bash
|
||||
exit 0
|
||||
|
|
@ -196,7 +196,12 @@ func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, n
|
|||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
@ -252,7 +257,12 @@ func (s *PrepSubsystem) createLabel(ctx context.Context, org, repo, name string)
|
|||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 201 {
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 201 {
|
||||
return 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ package agentic
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
coreio "forge.lthn.ai/core/go-io"
|
||||
|
||||
)
|
||||
|
||||
// CompletionEvent is emitted when a dispatched agent finishes.
|
||||
|
|
@ -39,6 +40,10 @@ func emitCompletionEvent(agent, workspace string) {
|
|||
}
|
||||
|
||||
// Append to events log
|
||||
existing, _ := coreio.Local.Read(eventsFile)
|
||||
coreio.Local.Write(eventsFile, existing+string(data)+"\n")
|
||||
f, err := os.OpenFile(eventsFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
f.Write(append(data, '\n'))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,3 +42,11 @@ func AgentName() string {
|
|||
}
|
||||
return "charon"
|
||||
}
|
||||
|
||||
// GitHubOrg returns the GitHub org for mirror operations.
|
||||
func GitHubOrg() string {
|
||||
if org := os.Getenv("GITHUB_ORG"); org != "" {
|
||||
return org
|
||||
}
|
||||
return "dAppCore"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,7 +308,12 @@ func (s *PrepSubsystem) listRepoPRs(ctx context.Context, org, repo, state string
|
|||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if err != nil {
|
||||
return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, coreerr.E("listRepoPRs", "failed to list PRs for "+repo, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
|
|||
|
|
@ -167,7 +167,9 @@ func (s *PrepSubsystem) prepWorkspace(ctx context.Context, _ *mcp.CallToolReques
|
|||
// 1. Clone repo into src/ and create feature branch
|
||||
srcDir := filepath.Join(wsDir, "src")
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", repoPath, srcDir)
|
||||
cloneCmd.Run()
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return nil, PrepOutput{}, coreerr.E("prep", "git clone failed for "+input.Repo, err)
|
||||
}
|
||||
|
||||
// Create feature branch
|
||||
taskSlug := strings.Map(func(r rune) rune {
|
||||
|
|
@ -459,7 +461,12 @@ func (s *PrepSubsystem) pullWiki(ctx context.Context, org, repo, wsDir string) i
|
|||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
@ -544,7 +551,12 @@ func (s *PrepSubsystem) generateContext(ctx context.Context, repo, wsDir string)
|
|||
req.Header.Set("Authorization", "Bearer "+s.brainKey)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
@ -637,7 +649,12 @@ func (s *PrepSubsystem) generateTodo(ctx context.Context, org, repo string, issu
|
|||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ func (s *PrepSubsystem) pushAndMerge(ctx context.Context, repoDir, repo string)
|
|||
}
|
||||
|
||||
// Mark PR ready if draft
|
||||
readyCmd := exec.CommandContext(ctx, "gh", "pr", "ready", "--repo", "dAppCore/"+repo)
|
||||
readyCmd := exec.CommandContext(ctx, "gh", "pr", "ready", "--repo", GitHubOrg()+"/"+repo)
|
||||
readyCmd.Dir = repoDir
|
||||
readyCmd.Run() // Ignore error — might already be ready
|
||||
|
||||
|
|
@ -339,8 +339,11 @@ func (s *PrepSubsystem) storeReviewOutput(repoDir, repo, reviewer, output string
|
|||
jsonLine, _ := json.Marshal(entry)
|
||||
|
||||
jsonlPath := filepath.Join(dataDir, "reviews.jsonl")
|
||||
existing, _ := coreio.Local.Read(jsonlPath)
|
||||
coreio.Local.Write(jsonlPath, existing+string(jsonLine)+"\n")
|
||||
f, err := os.OpenFile(jsonlPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
f.Write(append(jsonLine, '\n'))
|
||||
}
|
||||
}
|
||||
|
||||
// saveRateLimitState persists rate limit info for cross-run awareness.
|
||||
|
|
|
|||
|
|
@ -105,7 +105,12 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string,
|
|||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if err != nil {
|
||||
return nil, coreerr.E("scan.listOrgRepos", "failed to list repos", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, coreerr.E("scan.listOrgRepos", "failed to list repos", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
@ -129,7 +134,12 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str
|
|||
req.Header.Set("Authorization", "token "+s.forgeToken)
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
if err != nil {
|
||||
return nil, coreerr.E("scan.listRepoIssues", "failed to list issues for "+repo, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, coreerr.E("scan.listRepoIssues", "failed to list issues for "+repo, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -238,14 +238,26 @@ func (m *Subsystem) checkInbox() string {
|
|||
}
|
||||
|
||||
// Call the API to check inbox
|
||||
cmd := exec.Command("curl", "-sf",
|
||||
"-H", "Authorization: Bearer "+strings.TrimSpace(apiKeyStr),
|
||||
"https://api.lthn.sh/v1/messages/inbox?agent="+agentic.AgentName(),
|
||||
)
|
||||
out, err := cmd.Output()
|
||||
apiURL := os.Getenv("CORE_API_URL")
|
||||
if apiURL == "" {
|
||||
apiURL = "https://api.lthn.sh"
|
||||
}
|
||||
req, err := http.NewRequest("GET", apiURL+"/v1/messages/inbox?agent="+agentic.AgentName(), nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(apiKeyStr))
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
httpResp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
|
||||
if httpResp.StatusCode != 200 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Data []struct {
|
||||
|
|
@ -254,7 +266,7 @@ func (m *Subsystem) checkInbox() string {
|
|||
Subject string `json:"subject"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if json.Unmarshal(out, &resp) != nil {
|
||||
if json.NewDecoder(httpResp.Body).Decode(&resp) != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue