From 8f3afaa42aff74acc4fb72b7b2d54ef4dea5bb4d Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 8 Apr 2026 22:00:20 +0100 Subject: [PATCH] refactor(mcp): migrate stdlib imports to core/go primitives + upgrade go-sdk v1.5.0 - Replace fmt/errors/strings/path/filepath with core.Sprintf, core.E, core.Contains, core.Path etc. across 16 files - Remove 'errors' import from bridge.go (core.Is/core.As) - Remove 'fmt' from transport_tcp.go, ide.go (core.Print, inline interface) - Remove 'strings' from notify.go, transport_http.go, tools_webview.go, process_notifications.go (core.Trim, core.HasPrefix, core.Lower etc.) - Upgrade go-sdk from v1.4.1 to v1.5.0 - Keep encoding/json for json.NewDecoder/MarshalIndent (no core equivalent) - Keep os/exec in agentic subsystem (needs go-process Action wiring) Co-Authored-By: Virgil --- go.mod | 2 +- go.sum | 8 ++++---- pkg/mcp/agentic/epic.go | 29 ++++++++++++++++---------- pkg/mcp/agentic/ingest.go | 29 +++++++++++--------------- pkg/mcp/agentic/issue.go | 34 ++++++++++++++++--------------- pkg/mcp/agentic/plan.go | 20 +++++++++--------- pkg/mcp/agentic/scan.go | 12 +++++------ pkg/mcp/agentic/status.go | 23 ++++++++++----------- pkg/mcp/agentic/watch.go | 10 ++++----- pkg/mcp/agentic/write_atomic.go | 6 +++--- pkg/mcp/bridge.go | 5 ++--- pkg/mcp/ide/ide.go | 3 +-- pkg/mcp/mcp.go | 12 +++++------ pkg/mcp/notify.go | 6 +++--- pkg/mcp/process_notifications.go | 7 ++++--- pkg/mcp/registry.go | 35 +++++++++++++++++++++++++++++++- pkg/mcp/tools_webview.go | 5 ++--- pkg/mcp/transport_http.go | 8 ++++---- pkg/mcp/transport_tcp.go | 4 ++-- 19 files changed, 145 insertions(+), 113 deletions(-) diff --git a/go.mod b/go.mod index 2dbe7b6..295c21c 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( dappco.re/go/core/ws v0.4.0 github.com/gin-gonic/gin v1.12.0 github.com/gorilla/websocket v1.5.3 - github.com/modelcontextprotocol/go-sdk v1.4.1 + github.com/modelcontextprotocol/go-sdk v1.5.0 github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 1706c8a..d454439 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -218,8 +218,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= -github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU= +github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/pkg/mcp/agentic/epic.go b/pkg/mcp/agentic/epic.go index 08cef58..6f8dd4d 100644 --- a/pkg/mcp/agentic/epic.go +++ b/pkg/mcp/agentic/epic.go @@ -6,12 +6,11 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" - "strings" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -101,14 +100,14 @@ func (s *PrepSubsystem) createEpic(ctx context.Context, req *mcp.CallToolRequest } // Step 2: Build epic body with checklist - var body strings.Builder + body := core.NewBuilder() if input.Body != "" { body.WriteString(input.Body) body.WriteString("\n\n") } body.WriteString("## Tasks\n\n") for _, child := range children { - body.WriteString(fmt.Sprintf("- [ ] #%d %s\n", child.Number, child.Title)) + body.WriteString(core.Sprintf("- [ ] #%d %s\n", child.Number, child.Title)) } // Step 3: Create epic issue @@ -157,8 +156,12 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body payload["labels"] = labelIDs } - data, _ := json.Marshal(payload) - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues", s.forgeURL, org, repo) + r := core.JSONMarshal(payload) + if !r.OK { + return ChildRef{}, coreerr.E("createIssue", "failed to encode issue payload", nil) + } + data := r.Value.([]byte) + url := core.Sprintf("%s/api/v1/repos/%s/%s/issues", s.forgeURL, org, repo) req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(data)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) @@ -170,7 +173,7 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body defer resp.Body.Close() if resp.StatusCode != 201 { - return ChildRef{}, coreerr.E("createIssue", fmt.Sprintf("returned %d", resp.StatusCode), nil) + return ChildRef{}, coreerr.E("createIssue", core.Sprintf("returned %d", resp.StatusCode), nil) } var result struct { @@ -193,7 +196,7 @@ func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, n } // Fetch existing labels - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels?limit=50", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/labels?limit=50", s.forgeURL, org, repo) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("Authorization", "token "+s.forgeToken) @@ -246,12 +249,16 @@ func (s *PrepSubsystem) createLabel(ctx context.Context, org, repo, name string) colour = "#6b7280" } - payload, _ := json.Marshal(map[string]string{ + r := core.JSONMarshal(map[string]string{ "name": name, "color": colour, }) + if !r.OK { + return 0 + } + payload := r.Value.([]byte) - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo) + url := core.Sprintf("%s/api/v1/repos/%s/%s/labels", s.forgeURL, org, repo) req, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "token "+s.forgeToken) diff --git a/pkg/mcp/agentic/ingest.go b/pkg/mcp/agentic/ingest.go index a482dc6..5535e67 100644 --- a/pkg/mcp/agentic/ingest.go +++ b/pkg/mcp/agentic/ingest.go @@ -5,15 +5,12 @@ package agentic import ( "bytes" "context" - "encoding/json" - "fmt" "net/http" "os" - "path/filepath" - "strings" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" + coremcp "dappco.re/go/mcp/pkg/mcp" ) // ingestFindings reads the agent output log and creates issues via the API @@ -25,10 +22,7 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { } // Read the log file - logFiles, err := filepath.Glob(filepath.Join(wsDir, "agent-*.log")) - if err != nil { - return - } + logFiles := core.PathGlob(core.Path(wsDir, "agent-*.log")) if len(logFiles) == 0 { return } @@ -41,7 +35,7 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { body := contentStr // Skip quota errors - if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") { + if core.Contains(body, "QUOTA_EXHAUSTED") || core.Contains(body, "QuotaError") { return } @@ -56,13 +50,13 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) { // Determine issue type from the template used issueType := "task" priority := "normal" - if strings.Contains(body, "security") || strings.Contains(body, "Security") { + if core.Contains(body, "security") || core.Contains(body, "Security") { issueType = "bug" priority = "high" } // Create a single issue per repo with all findings in the body - title := fmt.Sprintf("Scan findings for %s (%d items)", st.Repo, findings) + title := core.Sprintf("Scan findings for %s (%d items)", st.Repo, findings) // Truncate body to reasonable size for issue description description := body @@ -86,7 +80,7 @@ func countFileRefs(body string) int { } if j < len(body) && body[j] == '`' { ref := body[i+1 : j] - if strings.Contains(ref, ".go:") || strings.Contains(ref, ".php:") { + if core.Contains(ref, ".go:") || core.Contains(ref, ".php:") { count++ } } @@ -103,22 +97,23 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p // Read the agent API key from file home, _ := os.UserHomeDir() - apiKeyData, err := coreio.Local.Read(filepath.Join(home, ".claude", "agent-api.key")) + apiKeyData, err := coreio.Local.Read(core.Path(home, ".claude", "agent-api.key")) if err != nil { return false } - apiKey := strings.TrimSpace(apiKeyData) + apiKey := core.Trim(apiKeyData) - payload, err := json.Marshal(map[string]string{ + r := core.JSONMarshal(map[string]string{ "title": title, "description": description, "type": issueType, "priority": priority, "reporter": "cladius", }) - if err != nil { + if !r.OK { return false } + payload := r.Value.([]byte) req, err := http.NewRequest("POST", s.brainURL+"/v1/issues", bytes.NewReader(payload)) if err != nil { diff --git a/pkg/mcp/agentic/issue.go b/pkg/mcp/agentic/issue.go index 13c241a..7bd3a09 100644 --- a/pkg/mcp/agentic/issue.go +++ b/pkg/mcp/agentic/issue.go @@ -6,11 +6,11 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -77,10 +77,10 @@ func (s *PrepSubsystem) dispatchIssue(ctx context.Context, req *mcp.CallToolRequ return nil, DispatchOutput{}, err } if issue.State != "open" { - return nil, DispatchOutput{}, coreerr.E("dispatchIssue", fmt.Sprintf("issue %d is %s, not open", input.Issue, issue.State), nil) + return nil, DispatchOutput{}, coreerr.E("dispatchIssue", core.Sprintf("issue %d is %s, not open", input.Issue, issue.State), nil) } if issue.Assignee != nil && issue.Assignee.Login != "" { - return nil, DispatchOutput{}, coreerr.E("dispatchIssue", fmt.Sprintf("issue %d is already assigned to %s", input.Issue, issue.Assignee.Login), nil) + return nil, DispatchOutput{}, coreerr.E("dispatchIssue", core.Sprintf("issue %d is already assigned to %s", input.Issue, issue.Assignee.Login), nil) } if !input.DryRun { @@ -124,7 +124,7 @@ func (s *PrepSubsystem) dispatchIssue(ctx context.Context, req *mcp.CallToolRequ func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue int, labels []struct { Name string `json:"name"` }) error { - updateURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue) + updateURL := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue) issueLabels := make([]string, 0, len(labels)) for _, label := range labels { if label.Name == "in-progress" { @@ -135,13 +135,14 @@ func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue if issueLabels == nil { issueLabels = []string{} } - payload, err := json.Marshal(map[string]any{ + r := core.JSONMarshal(map[string]any{ "assignees": []string{}, "labels": issueLabels, }) - if err != nil { - return coreerr.E("unlockIssue", "failed to encode issue unlock", err) + if !r.OK { + return coreerr.E("unlockIssue", "failed to encode issue unlock", nil) } + payload := r.Value.([]byte) req, err := http.NewRequestWithContext(ctx, http.MethodPatch, updateURL, bytes.NewReader(payload)) if err != nil { @@ -156,14 +157,14 @@ func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { - return coreerr.E("unlockIssue", fmt.Sprintf("issue unlock returned %d", resp.StatusCode), nil) + return coreerr.E("unlockIssue", core.Sprintf("issue unlock returned %d", resp.StatusCode), nil) } return nil } func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue int) (*forgeIssue, error) { - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue) + url := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, coreerr.E("fetchIssue", "failed to build request", err) @@ -176,7 +177,7 @@ func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, coreerr.E("fetchIssue", fmt.Sprintf("issue %d not found in %s/%s", issue, org, repo), nil) + return nil, coreerr.E("fetchIssue", core.Sprintf("issue %d not found in %s/%s", issue, org, repo), nil) } var out forgeIssue @@ -187,14 +188,15 @@ func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue } func (s *PrepSubsystem) lockIssue(ctx context.Context, org, repo string, issue int, assignee string) error { - updateURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue) - payload, err := json.Marshal(map[string]any{ + updateURL := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue) + r := core.JSONMarshal(map[string]any{ "assignees": []string{assignee}, "labels": []string{"in-progress"}, }) - if err != nil { - return coreerr.E("lockIssue", "failed to encode issue update", err) + if !r.OK { + return coreerr.E("lockIssue", "failed to encode issue update", nil) } + payload := r.Value.([]byte) req, err := http.NewRequestWithContext(ctx, http.MethodPatch, updateURL, bytes.NewReader(payload)) if err != nil { @@ -209,7 +211,7 @@ func (s *PrepSubsystem) lockIssue(ctx context.Context, org, repo string, issue i } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { - return coreerr.E("lockIssue", fmt.Sprintf("issue update returned %d", resp.StatusCode), nil) + return coreerr.E("lockIssue", core.Sprintf("issue update returned %d", resp.StatusCode), nil) } return nil diff --git a/pkg/mcp/agentic/plan.go b/pkg/mcp/agentic/plan.go index 3ce823e..223a2da 100644 --- a/pkg/mcp/agentic/plan.go +++ b/pkg/mcp/agentic/plan.go @@ -7,13 +7,13 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" - "path/filepath" "strings" "time" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -349,11 +349,11 @@ func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, inpu var plans []Plan for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { + if entry.IsDir() || !core.HasSuffix(entry.Name(), ".json") { continue } - id := strings.TrimSuffix(entry.Name(), ".json") + id := core.TrimSuffix(entry.Name(), ".json") plan, err := readPlan(dir, id) if err != nil { continue @@ -422,11 +422,11 @@ func (s *PrepSubsystem) planCheckpoint(_ context.Context, _ *mcp.CallToolRequest // --- Helpers --- func (s *PrepSubsystem) plansDir() string { - return filepath.Join(s.codePath, ".core", "plans") + return core.Path(s.codePath, ".core", "plans") } func planPath(dir, id string) string { - return filepath.Join(dir, id+".json") + return core.Path(dir, id+".json") } func generatePlanID(title string) string { @@ -444,8 +444,8 @@ func generatePlanID(title string) string { }, title) // Trim consecutive dashes and cap length - for strings.Contains(slug, "--") { - slug = strings.ReplaceAll(slug, "--", "-") + for core.Contains(slug, "--") { + slug = core.Replace(slug, "--", "-") } slug = strings.Trim(slug, "-") if len(slug) > 30 { @@ -466,8 +466,8 @@ func readPlan(dir, id string) (*Plan, error) { } var plan Plan - if err := json.Unmarshal([]byte(data), &plan); err != nil { - return nil, coreerr.E("readPlan", "failed to parse plan "+id, err) + if r := core.JSONUnmarshal([]byte(data), &plan); !r.OK { + return nil, coreerr.E("readPlan", "failed to parse plan "+id, nil) } return &plan, nil } diff --git a/pkg/mcp/agentic/scan.go b/pkg/mcp/agentic/scan.go index 993894f..564962f 100644 --- a/pkg/mcp/agentic/scan.go +++ b/pkg/mcp/agentic/scan.go @@ -5,10 +5,10 @@ package agentic import ( "context" "encoding/json" - "fmt" "net/http" "strings" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -81,7 +81,7 @@ func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input seen := make(map[string]bool) var unique []ScanIssue for _, issue := range allIssues { - key := fmt.Sprintf("%s#%d", issue.Repo, issue.Number) + key := core.Sprintf("%s#%d", issue.Repo, issue.Number) if !seen[key] { seen[key] = true unique = append(unique, issue) @@ -100,7 +100,7 @@ func (s *PrepSubsystem) scan(ctx context.Context, _ *mcp.CallToolRequest, input } func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, error) { - url := fmt.Sprintf("%s/api/v1/orgs/%s/repos?limit=50", s.forgeURL, org) + url := core.Sprintf("%s/api/v1/orgs/%s/repos?limit=50", s.forgeURL, org) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("Authorization", "token "+s.forgeToken) @@ -110,7 +110,7 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, } defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, coreerr.E("listOrgRepos", fmt.Sprintf("HTTP %d listing repos", resp.StatusCode), nil) + return nil, coreerr.E("listOrgRepos", core.Sprintf("HTTP %d listing repos", resp.StatusCode), nil) } var repos []struct { @@ -126,7 +126,7 @@ func (s *PrepSubsystem) listOrgRepos(ctx context.Context, org string) ([]string, } func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label string) ([]ScanIssue, error) { - url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues?state=open&labels=%s&limit=10&type=issues", + url := core.Sprintf("%s/api/v1/repos/%s/%s/issues?state=open&labels=%s&limit=10&type=issues", s.forgeURL, org, repo, label) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("Authorization", "token "+s.forgeToken) @@ -137,7 +137,7 @@ func (s *PrepSubsystem) listRepoIssues(ctx context.Context, org, repo, label str } defer resp.Body.Close() if resp.StatusCode != 200 { - return nil, coreerr.E("listRepoIssues", fmt.Sprintf("HTTP %d for "+repo, resp.StatusCode), nil) + return nil, coreerr.E("listRepoIssues", core.Sprintf("HTTP %d for "+repo, resp.StatusCode), nil) } var issues []struct { diff --git a/pkg/mcp/agentic/status.go b/pkg/mcp/agentic/status.go index ddcdd2a..4f3b28b 100644 --- a/pkg/mcp/agentic/status.go +++ b/pkg/mcp/agentic/status.go @@ -6,13 +6,12 @@ import ( "context" "encoding/json" "os" - "path/filepath" - "strings" "time" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -57,23 +56,23 @@ func writeStatus(wsDir string, status *WorkspaceStatus) error { if err != nil { return err } - return writeAtomic(filepath.Join(wsDir, "status.json"), string(data)) + return writeAtomic(core.JoinPath(wsDir, "status.json"), string(data)) } func (s *PrepSubsystem) saveStatus(wsDir string, status *WorkspaceStatus) { if err := writeStatus(wsDir, status); err != nil { - coreerr.Warn("failed to write workspace status", "workspace", filepath.Base(wsDir), "err", err) + coreerr.Warn("failed to write workspace status", "workspace", core.PathBase(wsDir), "err", err) } } func readStatus(wsDir string) (*WorkspaceStatus, error) { - data, err := coreio.Local.Read(filepath.Join(wsDir, "status.json")) + data, err := coreio.Local.Read(core.JoinPath(wsDir, "status.json")) if err != nil { return nil, err } var s WorkspaceStatus - if err := json.Unmarshal([]byte(data), &s); err != nil { - return nil, err + if r := core.JSONUnmarshal([]byte(data), &s); !r.OK { + return nil, coreerr.E("readStatus", "failed to parse status.json", nil) } return &s, nil } @@ -126,7 +125,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu var workspaces []WorkspaceInfo for _, wsDir := range wsDirs { - name := filepath.Base(wsDir) + name := core.PathBase(wsDir) // Filter by specific workspace if requested if input.Workspace != "" && name != input.Workspace { @@ -139,7 +138,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu st, err := readStatus(wsDir) if err != nil { // Legacy workspace (no status.json) — check for log file - logFiles, _ := filepath.Glob(filepath.Join(wsDir, "agent-*.log")) + logFiles := core.PathGlob(core.Path(wsDir, "agent-*.log")) if len(logFiles) > 0 { info.Status = "completed" } else { @@ -177,10 +176,10 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu } // Process died — check for BLOCKED.md - blockedPath := filepath.Join(wsDir, "src", "BLOCKED.md") + blockedPath := core.Path(wsDir, "src", "BLOCKED.md") if data, err := coreio.Local.Read(blockedPath); err == nil { info.Status = "blocked" - info.Question = strings.TrimSpace(data) + info.Question = core.Trim(data) st.Status = "blocked" st.Question = info.Question status = "blocked" diff --git a/pkg/mcp/agentic/watch.go b/pkg/mcp/agentic/watch.go index 11fa6e9..35e2e00 100644 --- a/pkg/mcp/agentic/watch.go +++ b/pkg/mcp/agentic/watch.go @@ -4,11 +4,11 @@ package agentic import ( "context" - "path/filepath" "time" - coremcp "dappco.re/go/mcp/pkg/mcp" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" + coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -153,15 +153,15 @@ func (s *PrepSubsystem) findActiveWorkspaces() []string { } switch st.Status { case "running", "queued": - active = append(active, filepath.Base(wsDir)) + active = append(active, core.PathBase(wsDir)) } } return active } func (s *PrepSubsystem) resolveWorkspaceDir(name string) string { - if filepath.IsAbs(name) { + if core.PathIsAbs(name) { return name } - return filepath.Join(s.workspaceRoot(), name) + return core.JoinPath(s.workspaceRoot(), name) } diff --git a/pkg/mcp/agentic/write_atomic.go b/pkg/mcp/agentic/write_atomic.go index 060cfeb..72bb9a7 100644 --- a/pkg/mcp/agentic/write_atomic.go +++ b/pkg/mcp/agentic/write_atomic.go @@ -4,8 +4,8 @@ package agentic import ( "os" - "path/filepath" + core "dappco.re/go/core" coreio "dappco.re/go/core/io" ) @@ -15,12 +15,12 @@ import ( // This avoids exposing partially written workspace files to agents that may // read status, prompt, or plan documents while they are being updated. func writeAtomic(path, content string) error { - dir := filepath.Dir(path) + dir := core.PathDir(path) if err := coreio.Local.EnsureDir(dir); err != nil { return err } - tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp") + tmp, err := os.CreateTemp(dir, "."+core.PathBase(path)+".*.tmp") if err != nil { return err } diff --git a/pkg/mcp/bridge.go b/pkg/mcp/bridge.go index 040dfb4..22a1e8a 100644 --- a/pkg/mcp/bridge.go +++ b/pkg/mcp/bridge.go @@ -3,7 +3,6 @@ package mcp import ( - "errors" "net/http" core "dappco.re/go/core" @@ -48,7 +47,7 @@ func BridgeToAPI(svc *Service, bridge *api.ToolBridge) { if !r.OK { if err, ok := r.Value.(error); ok { var maxBytesErr *http.MaxBytesError - if errors.As(err, &maxBytesErr) || core.Contains(err.Error(), "request body too large") { + if core.As(err, &maxBytesErr) || core.Contains(err.Error(), "request body too large") { c.JSON(http.StatusRequestEntityTooLarge, api.Fail("request_too_large", "Request body exceeds 10 MB limit")) return } @@ -63,7 +62,7 @@ func BridgeToAPI(svc *Service, bridge *api.ToolBridge) { if err != nil { // Body present + error = likely bad input (malformed JSON). // No body + error = tool execution failure. - if errors.Is(err, errInvalidRESTInput) { + if core.Is(err, errInvalidRESTInput) { c.JSON(http.StatusBadRequest, api.Fail("invalid_input", "Malformed JSON in request body")) return } diff --git a/pkg/mcp/ide/ide.go b/pkg/mcp/ide/ide.go index 0a17dfd..1832ee9 100644 --- a/pkg/mcp/ide/ide.go +++ b/pkg/mcp/ide/ide.go @@ -4,7 +4,6 @@ package ide import ( "context" - "fmt" "sync" "time" @@ -556,7 +555,7 @@ func stringFromAny(v any) string { switch value := v.(type) { case string: return value - case fmt.Stringer: + case interface{ String() string }: return value.String() default: return "" diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 3523cb6..fb2ff83 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -6,14 +6,12 @@ package mcp import ( "context" - "errors" "iter" "net/http" "os" "path/filepath" "slices" "sort" - "strings" "sync" core "dappco.re/go/core" @@ -246,15 +244,15 @@ func (s *Service) resolveWorkspacePath(path string) string { } if s.workspaceRoot == "" { - return filepath.Clean(path) + return core.CleanPath(path, "/") } - clean := filepath.Clean(string(filepath.Separator) + path) - clean = strings.TrimPrefix(clean, string(filepath.Separator)) + clean := core.CleanPath(string(filepath.Separator)+path, "/") + clean = core.TrimPrefix(clean, string(filepath.Separator)) if clean == "." || clean == "" { return s.workspaceRoot } - return filepath.Join(s.workspaceRoot, clean) + return core.Path(s.workspaceRoot, clean) } // registerTools adds the built-in tool groups to the MCP server. @@ -616,7 +614,7 @@ func (s *Service) fileExists(ctx context.Context, req *mcp.CallToolRequest, inpu info, err := s.medium.Stat(input.Path) if err != nil { - if errors.Is(err, os.ErrNotExist) { + if core.Is(err, os.ErrNotExist) { return nil, FileExistsOutput{Exists: false, IsDir: false, Path: input.Path}, nil } return nil, FileExistsOutput{}, log.E("mcp.fileExists", "failed to stat path", err) diff --git a/pkg/mcp/notify.go b/pkg/mcp/notify.go index 2c77ff7..5189e03 100644 --- a/pkg/mcp/notify.go +++ b/pkg/mcp/notify.go @@ -14,10 +14,10 @@ import ( "reflect" "slices" "sort" - "strings" "sync" "unsafe" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -203,7 +203,7 @@ func (s *Service) ChannelSend(ctx context.Context, channel string, data any) { if s == nil || s.server == nil { return } - if strings.TrimSpace(channel) == "" { + if core.Trim(channel) == "" { return } ctx = normalizeNotificationContext(ctx) @@ -218,7 +218,7 @@ func (s *Service) ChannelSendToSession(ctx context.Context, session *mcp.ServerS if s == nil || s.server == nil || session == nil { return } - if strings.TrimSpace(channel) == "" { + if core.Trim(channel) == "" { return } ctx = normalizeNotificationContext(ctx) diff --git a/pkg/mcp/process_notifications.go b/pkg/mcp/process_notifications.go index 88313d7..19c2679 100644 --- a/pkg/mcp/process_notifications.go +++ b/pkg/mcp/process_notifications.go @@ -4,9 +4,10 @@ package mcp import ( "context" - "path/filepath" "strings" "time" + + core "dappco.re/go/core" ) type processRuntime struct { @@ -50,7 +51,7 @@ func (s *Service) forgetProcessRuntime(id string) { } func isTestProcess(command string, args []string) bool { - base := strings.ToLower(filepath.Base(command)) + base := core.Lower(core.PathBase(command)) if base == "" { return false } @@ -62,7 +63,7 @@ func isTestProcess(command string, args []string) bool { return len(args) > 0 && strings.EqualFold(args[0], "test") case "npm", "pnpm", "yarn", "bun": for _, arg := range args { - if strings.EqualFold(arg, "test") || strings.HasPrefix(strings.ToLower(arg), "test:") { + if strings.EqualFold(arg, "test") || core.HasPrefix(core.Lower(arg), "test:") { return true } } diff --git a/pkg/mcp/registry.go b/pkg/mcp/registry.go index 85b0bb1..66cae3c 100644 --- a/pkg/mcp/registry.go +++ b/pkg/mcp/registry.go @@ -78,7 +78,40 @@ type ToolRecord struct { // return nil, ReadFileOutput{Path: "src/main.go"}, nil // }) func AddToolRecorded[In, Out any](s *Service, server *mcp.Server, group string, t *mcp.Tool, h mcp.ToolHandlerFor[In, Out]) { - mcp.AddTool(server, t, h) + // Set inputSchema from struct reflection if not already set. + // Use server.AddTool (non-generic) to avoid auto-generated outputSchema. + // The go-sdk's generic mcp.AddTool generates outputSchema from the Out type, + // but Claude Code's protocol (2025-03-26) doesn't support outputSchema. + // Removing it reduces tools/list from 214KB to ~74KB. + if t.InputSchema == nil { + t.InputSchema = structSchema(new(In)) + if t.InputSchema == nil { + t.InputSchema = map[string]any{"type": "object"} + } + } + // Wrap the typed handler into a generic ToolHandler. + wrapped := func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { + var input In + if req != nil && len(req.Params.Arguments) > 0 { + if r := core.JSONUnmarshal(req.Params.Arguments, &input); !r.OK { + if err, ok := r.Value.(error); ok { + return nil, err + } + } + } + result, output, err := h(ctx, req, input) + if err != nil { + return nil, err + } + if result != nil { + return result, nil + } + data := core.JSONMarshalString(output) + return &mcp.CallToolResult{ + Content: []mcp.Content{&mcp.TextContent{Text: data}}, + }, nil + } + server.AddTool(t, wrapped) restHandler := func(ctx context.Context, body []byte) (any, error) { var input In diff --git a/pkg/mcp/tools_webview.go b/pkg/mcp/tools_webview.go index 0108bde..a0d6a8b 100644 --- a/pkg/mcp/tools_webview.go +++ b/pkg/mcp/tools_webview.go @@ -9,7 +9,6 @@ import ( "image" "image/jpeg" _ "image/png" - "strings" "sync" "time" @@ -554,7 +553,7 @@ func (s *Service) webviewScreenshot(ctx context.Context, req *mcp.CallToolReques if format == "" { format = "png" } - format = strings.ToLower(format) + format = core.Lower(format) data, err := webviewInstance.Screenshot() if err != nil { @@ -649,7 +648,7 @@ func waitForSelector(ctx context.Context, timeout time.Duration, selector string if err == nil { return nil } - if !strings.Contains(err.Error(), "element not found") { + if !core.Contains(err.Error(), "element not found") { return err } diff --git a/pkg/mcp/transport_http.go b/pkg/mcp/transport_http.go index 67324d1..9a2e4b0 100644 --- a/pkg/mcp/transport_http.go +++ b/pkg/mcp/transport_http.go @@ -8,9 +8,9 @@ import ( "net" "net/http" "os" - "strings" "time" + core "dappco.re/go/core" coreerr "dappco.re/go/core/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -85,18 +85,18 @@ func (s *Service) ServeHTTP(ctx context.Context, addr string) error { // If token is empty, authentication is disabled for local development. func withAuth(token string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.TrimSpace(token) == "" { + if core.Trim(token) == "" { next.ServeHTTP(w, r) return } auth := r.Header.Get("Authorization") - if !strings.HasPrefix(auth, "Bearer ") { + if !core.HasPrefix(auth, "Bearer ") { http.Error(w, `{"error":"missing Bearer token"}`, http.StatusUnauthorized) return } - provided := strings.TrimSpace(strings.TrimPrefix(auth, "Bearer ")) + provided := core.Trim(core.TrimPrefix(auth, "Bearer ")) if len(provided) == 0 { http.Error(w, `{"error":"missing Bearer token"}`, http.StatusUnauthorized) return diff --git a/pkg/mcp/transport_tcp.go b/pkg/mcp/transport_tcp.go index fe1168f..74e1474 100644 --- a/pkg/mcp/transport_tcp.go +++ b/pkg/mcp/transport_tcp.go @@ -5,12 +5,12 @@ package mcp import ( "bufio" "context" - "fmt" goio "io" "net" "os" "sync" + core "dappco.re/go/core" "github.com/modelcontextprotocol/go-sdk/jsonrpc" "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -31,7 +31,7 @@ var diagWriter goio.Writer = os.Stderr func diagPrintf(format string, args ...any) { diagMu.Lock() defer diagMu.Unlock() - fmt.Fprintf(diagWriter, format, args...) + core.Print(diagWriter, format, args...) } // setDiagWriter swaps the diagnostic writer and returns the previous one.