Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
Snider
f9c5362151 feat(mcp): export NotifySession for raw JSON-RPC notifications
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-09 11:07:28 +01:00
Snider
8f3afaa42a 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 <virgil@lethean.io>
2026-04-08 22:03:52 +01:00
Snider
f8f137b465 fix(mcp): disable ListChanged to prevent premature stdio notifications
The go-sdk fires notifications/tools/list_changed and
notifications/resources/list_changed with 10ms delay after AddTool/AddResource.
Since all registration happens before server.Run(), these hit stdout
before the client sends initialize, breaking the MCP handshake.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-08 20:50:46 +01:00
Snider
429f1c2b6c Revert "perf(mcp): gate extended built-in tools behind CORE_MCP_FULL"
This reverts commit 9f7dd84d4a.
2026-04-08 20:47:34 +01:00
Snider
9f7dd84d4a perf(mcp): gate extended built-in tools behind CORE_MCP_FULL
Metrics, RAG, and webview tools only register when CORE_MCP_FULL=1.
Process and WS tools always register (used by factory).
Reduces default tool count by 15.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-08 19:17:32 +01:00
Snider
9bd3084da4 fix(mcp): bridge test body + process dep resolution
- Fix TestBridgeToAPI_Good_EndToEnd: POST with empty JSON body instead of nil
- Add local replace for go-process to resolve API drift with core v0.8.0

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-08 16:39:28 +01:00
Snider
20e4a381cf fix: migrate module paths from forge.lthn.ai to dappco.re
Update all import paths and version pins:
- forge.lthn.ai/core/go-* → dappco.re/go/core/*
- forge.lthn.ai/core/api → dappco.re/go/core/api
- forge.lthn.ai/core/cli → dappco.re/go/core/cli
- Updated: api v0.3.0, cli v0.5.2, ai v0.2.2, io v0.4.1, log v0.1.2
- Updated: process v0.5.0, rag v0.1.13, ws v0.4.0, webview v0.2.1
- Updated: i18n v0.2.3, inference v0.3.0, scm v0.6.1

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-07 12:59:22 +01:00
Snider
cd305904e5 fix: migrate module paths from forge.lthn.ai to dappco.re
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 16:21:14 +01:00
51 changed files with 250 additions and 211 deletions

View file

@ -27,8 +27,8 @@ import (
"strings" "strings"
"time" "time"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
) )
var ( var (

View file

@ -1,7 +1,7 @@
package main package main
import ( import (
"forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/cli/pkg/cli"
mcpcmd "dappco.re/go/mcp/cmd/mcpcmd" mcpcmd "dappco.re/go/mcp/cmd/mcpcmd"
) )

View file

@ -13,7 +13,7 @@ import (
"dappco.re/go/mcp/pkg/mcp" "dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/agentic" "dappco.re/go/mcp/pkg/mcp/agentic"
"dappco.re/go/mcp/pkg/mcp/brain" "dappco.re/go/mcp/pkg/mcp/brain"
"forge.lthn.ai/core/cli/pkg/cli" "dappco.re/go/core/cli/pkg/cli"
) )
var workspaceFlag string var workspaceFlag string

27
go.mod
View file

@ -4,26 +4,25 @@ go 1.26.0
require ( require (
dappco.re/go/core v0.8.0-alpha.1 dappco.re/go/core v0.8.0-alpha.1
forge.lthn.ai/core/api v0.1.5 dappco.re/go/core/ai v0.2.2
forge.lthn.ai/core/cli v0.3.7 dappco.re/go/core/api v0.3.0
forge.lthn.ai/core/go-ai v0.1.12 dappco.re/go/core/cli v0.5.2
forge.lthn.ai/core/go-io v0.1.7 dappco.re/go/core/io v0.4.1
forge.lthn.ai/core/go-log v0.0.4 dappco.re/go/core/log v0.1.2
forge.lthn.ai/core/go-process v0.2.9 dappco.re/go/core/process v0.5.0
forge.lthn.ai/core/go-rag v0.1.11 dappco.re/go/core/rag v0.1.13
forge.lthn.ai/core/go-webview v0.1.6 dappco.re/go/core/webview v0.2.1
forge.lthn.ai/core/go-ws v0.2.5 dappco.re/go/core/ws v0.4.0
github.com/gin-gonic/gin v1.12.0 github.com/gin-gonic/gin v1.12.0
github.com/gorilla/websocket v1.5.3 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 github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
forge.lthn.ai/core/go v0.3.3 // indirect dappco.re/go/core/i18n v0.2.3 // indirect
forge.lthn.ai/core/go-i18n v0.1.7 // indirect dappco.re/go/core/inference v0.3.0 // indirect
forge.lthn.ai/core/go-inference v0.1.6 // indirect
github.com/99designs/gqlgen v0.17.88 // indirect github.com/99designs/gqlgen v0.17.88 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect
@ -149,3 +148,5 @@ require (
google.golang.org/grpc v1.79.2 // indirect google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )
replace dappco.re/go/core/process => ../go-process

48
go.sum
View file

@ -1,29 +1,25 @@
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
forge.lthn.ai/core/api v0.1.5 h1:NwZrcOyBjaiz5/cn0n0tnlMUodi8Or6FHMx59C7Kv2o= dappco.re/go/core/ai v0.2.2 h1:fkSKm3ezAljYbghlax5qHDm11uq7LUyIedIQO1PtdcY=
forge.lthn.ai/core/api v0.1.5/go.mod h1:PBnaWyOVXSOGy+0x2XAPUFMYJxQ2CNhppia/D06ZPII= dappco.re/go/core/ai v0.2.2/go.mod h1:+MZN/EArn/W2ag91McL034WxdMSO4IPqFcQER5/POGU=
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg= dappco.re/go/core/api v0.3.0 h1:uWYgDQ+B4e5pXPX3S5lMsqSJamfpui3LWD5hcdwvWew=
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs= dappco.re/go/core/api v0.3.0/go.mod h1:1ZDNwPHV6YjkUsjtC3nfLk6U4eqWlQ6qj6yT/MB8r6k=
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0= dappco.re/go/core/cli v0.5.2 h1:mo+PERo3lUytE+r3ArHr8o2nTftXjgPPsU/rn3ETXDM=
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ= dappco.re/go/core/cli v0.5.2/go.mod h1:D4zfn3ec/hb72AWX/JWDvkW+h2WDKQcxGUrzoss7q2s=
forge.lthn.ai/core/go-ai v0.1.12 h1:OHt0bUABlyhvgxZxyMwueRoh8rS3YKWGFY6++zCAwC8= dappco.re/go/core/i18n v0.2.3 h1:GqFaTR1I0SfSEc4WtsAkgao+jp8X5qcMPqrX0eMAOrY=
forge.lthn.ai/core/go-ai v0.1.12/go.mod h1:5Pc9lszxgkO7Aj2Z3dtq4L9Xk9l/VNN+Baj1t///OCM= dappco.re/go/core/i18n v0.2.3/go.mod h1:LoyX/4fIEJO/wiHY3Q682+4P0Ob7zPemcATfwp0JBUg=
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA= dappco.re/go/core/inference v0.3.0 h1:ANFnlVO1LEYDipeDeBgqmb8CHvOTUFhMPyfyHGqO0IY=
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8= dappco.re/go/core/inference v0.3.0/go.mod h1:wbRY0v6iwOoJCpTvcBFarAM08bMgpPcrF6yv3vccYoA=
forge.lthn.ai/core/go-inference v0.1.6 h1:ce42zC0zO8PuISUyAukAN1NACEdWp5wF1mRgnh5+58E= dappco.re/go/core/io v0.4.1 h1:15dm7ldhFIAuZOrBiQG6XVZDpSvCxtZsUXApwTAB3wQ=
forge.lthn.ai/core/go-inference v0.1.6/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= dappco.re/go/core/io v0.4.1/go.mod h1:w71dukyunczLb8frT9JOd5B78PjwWQD3YAXiCt3AcPA=
forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk= dappco.re/go/core/log v0.1.2 h1:pQSZxKD8VycdvjNJmatXbPSq2OxcP2xHbF20zgFIiZI=
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4= dappco.re/go/core/log v0.1.2/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= dappco.re/go/core/rag v0.1.13 h1:R2Q+Xw5YenT4uFemXLBu+xQYtyUIYGSmMln5/Z+nol4=
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= dappco.re/go/core/rag v0.1.13/go.mod h1:wthXtCqYEChjlGIHcJXetlgk49lPDmzG6jFWd1PEIZc=
forge.lthn.ai/core/go-process v0.2.9 h1:Wql+5TUF+lfU2oJ9I+S764MkTqJhBsuyMM0v1zsfZC4= dappco.re/go/core/webview v0.2.1 h1:rdy2sV+MS6RZsav8BiARJxtWhfx7eOAJp3b1Ynp1sYs=
forge.lthn.ai/core/go-process v0.2.9/go.mod h1:NIzZOF5IVYYCjHkcNIGcg1mZH+bzGoie4SlZUDYOKIM= dappco.re/go/core/webview v0.2.1/go.mod h1:Qdo1V/sJJwOnL0hYd3+vzVUJxWYC8eGyILZROya6KoM=
forge.lthn.ai/core/go-rag v0.1.11 h1:KXTOtnOdrx8YKmvnj0EOi2EI/+cKjE8w2PpJCQIrSd8= dappco.re/go/core/ws v0.4.0 h1:yEDV9whXyo+GWzBSjuB3NiLiH2bmBPBWD6rydwHyBn8=
forge.lthn.ai/core/go-rag v0.1.11/go.mod h1:vIlOKVD1SdqqjkJ2XQyXPuKPtiajz/STPLCaDpqOzk8= dappco.re/go/core/ws v0.4.0/go.mod h1:L1rrgW6zU+DztcVBJW2yO5Lm3rGXpyUMOA8OL9zsAok=
forge.lthn.ai/core/go-webview v0.1.6 h1:szXQxRJf2bOZJKh3v1P01B1Vf9mgXaBCXzh0EZu9aoc=
forge.lthn.ai/core/go-webview v0.1.6/go.mod h1:5n1tECD1wBV/uFZRY9ZjfPFO5TYZrlaR3mQFwvO2nek=
forge.lthn.ai/core/go-ws v0.2.5 h1:ZIV7Yrv01R/xpJUogA5vrfP9yB9li1w7EV3eZFMt8h0=
forge.lthn.ai/core/go-ws v0.2.5/go.mod h1:C3riJyLLcV6QhLvYlq3P/XkGTsN598qQeGBoLdoHBU4=
github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc= github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc=
github.com/99designs/gqlgen v0.17.88/go.mod h1:qeqYFEgOeSKqWedOjogPizimp2iu4E23bdPvl4jTYic= github.com/99designs/gqlgen v0.17.88/go.mod h1:qeqYFEgOeSKqWedOjogPizimp2iu4E23bdPvl4jTYic=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
@ -222,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-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 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 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.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU=
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= 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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View file

@ -13,8 +13,8 @@ import (
"time" "time"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -6,12 +6,11 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"strings"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreerr "forge.lthn.ai/core/go-log"
"github.com/modelcontextprotocol/go-sdk/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 // Step 2: Build epic body with checklist
var body strings.Builder body := core.NewBuilder()
if input.Body != "" { if input.Body != "" {
body.WriteString(input.Body) body.WriteString(input.Body)
body.WriteString("\n\n") body.WriteString("\n\n")
} }
body.WriteString("## Tasks\n\n") body.WriteString("## Tasks\n\n")
for _, child := range children { 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 // Step 3: Create epic issue
@ -157,8 +156,12 @@ func (s *PrepSubsystem) createIssue(ctx context.Context, org, repo, title, body
payload["labels"] = labelIDs payload["labels"] = labelIDs
} }
data, _ := json.Marshal(payload) r := core.JSONMarshal(payload)
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues", s.forgeURL, org, repo) 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, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(data))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "token "+s.forgeToken) 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() defer resp.Body.Close()
if resp.StatusCode != 201 { 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 { var result struct {
@ -193,7 +196,7 @@ func (s *PrepSubsystem) resolveLabelIDs(ctx context.Context, org, repo string, n
} }
// Fetch existing labels // 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, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)
@ -246,12 +249,16 @@ func (s *PrepSubsystem) createLabel(ctx context.Context, org, repo, name string)
colour = "#6b7280" colour = "#6b7280"
} }
payload, _ := json.Marshal(map[string]string{ r := core.JSONMarshal(map[string]string{
"name": name, "name": name,
"color": colour, "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, _ := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "token "+s.forgeToken) req.Header.Set("Authorization", "token "+s.forgeToken)

View file

@ -5,15 +5,12 @@ package agentic
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io"
) )
// ingestFindings reads the agent output log and creates issues via the API // 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 // Read the log file
logFiles, err := filepath.Glob(filepath.Join(wsDir, "agent-*.log")) logFiles := core.PathGlob(core.Path(wsDir, "agent-*.log"))
if err != nil {
return
}
if len(logFiles) == 0 { if len(logFiles) == 0 {
return return
} }
@ -41,7 +35,7 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) {
body := contentStr body := contentStr
// Skip quota errors // Skip quota errors
if strings.Contains(body, "QUOTA_EXHAUSTED") || strings.Contains(body, "QuotaError") { if core.Contains(body, "QUOTA_EXHAUSTED") || core.Contains(body, "QuotaError") {
return return
} }
@ -56,13 +50,13 @@ func (s *PrepSubsystem) ingestFindings(wsDir string) {
// Determine issue type from the template used // Determine issue type from the template used
issueType := "task" issueType := "task"
priority := "normal" priority := "normal"
if strings.Contains(body, "security") || strings.Contains(body, "Security") { if core.Contains(body, "security") || core.Contains(body, "Security") {
issueType = "bug" issueType = "bug"
priority = "high" priority = "high"
} }
// Create a single issue per repo with all findings in the body // 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 // Truncate body to reasonable size for issue description
description := body description := body
@ -86,7 +80,7 @@ func countFileRefs(body string) int {
} }
if j < len(body) && body[j] == '`' { if j < len(body) && body[j] == '`' {
ref := body[i+1 : 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++ count++
} }
} }
@ -103,22 +97,23 @@ func (s *PrepSubsystem) createIssueViaAPI(repo, title, description, issueType, p
// Read the agent API key from file // Read the agent API key from file
home, _ := os.UserHomeDir() 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 { if err != nil {
return false 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, "title": title,
"description": description, "description": description,
"type": issueType, "type": issueType,
"priority": priority, "priority": priority,
"reporter": "cladius", "reporter": "cladius",
}) })
if err != nil { if !r.OK {
return false return false
} }
payload := r.Value.([]byte)
req, err := http.NewRequest("POST", s.brainURL+"/v1/issues", bytes.NewReader(payload)) req, err := http.NewRequest("POST", s.brainURL+"/v1/issues", bytes.NewReader(payload))
if err != nil { if err != nil {

View file

@ -6,11 +6,11 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreerr "forge.lthn.ai/core/go-log"
"github.com/modelcontextprotocol/go-sdk/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 return nil, DispatchOutput{}, err
} }
if issue.State != "open" { 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 != "" { 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 { 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 { func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue int, labels []struct {
Name string `json:"name"` Name string `json:"name"`
}) error { }) 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)) issueLabels := make([]string, 0, len(labels))
for _, label := range labels { for _, label := range labels {
if label.Name == "in-progress" { if label.Name == "in-progress" {
@ -135,13 +135,14 @@ func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue
if issueLabels == nil { if issueLabels == nil {
issueLabels = []string{} issueLabels = []string{}
} }
payload, err := json.Marshal(map[string]any{ r := core.JSONMarshal(map[string]any{
"assignees": []string{}, "assignees": []string{},
"labels": issueLabels, "labels": issueLabels,
}) })
if err != nil { if !r.OK {
return coreerr.E("unlockIssue", "failed to encode issue unlock", err) 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)) req, err := http.NewRequestWithContext(ctx, http.MethodPatch, updateURL, bytes.NewReader(payload))
if err != nil { if err != nil {
@ -156,14 +157,14 @@ func (s *PrepSubsystem) unlockIssue(ctx context.Context, org, repo string, issue
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest { 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 return nil
} }
func (s *PrepSubsystem) fetchIssue(ctx context.Context, org, repo string, issue int) (*forgeIssue, error) { 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) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, coreerr.E("fetchIssue", "failed to build request", err) 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() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { 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 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 { 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) updateURL := core.Sprintf("%s/api/v1/repos/%s/%s/issues/%d", s.forgeURL, org, repo, issue)
payload, err := json.Marshal(map[string]any{ r := core.JSONMarshal(map[string]any{
"assignees": []string{assignee}, "assignees": []string{assignee},
"labels": []string{"in-progress"}, "labels": []string{"in-progress"},
}) })
if err != nil { if !r.OK {
return coreerr.E("lockIssue", "failed to encode issue update", err) 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)) req, err := http.NewRequestWithContext(ctx, http.MethodPatch, updateURL, bytes.NewReader(payload))
if err != nil { if err != nil {
@ -209,7 +211,7 @@ func (s *PrepSubsystem) lockIssue(ctx context.Context, org, repo string, issue i
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest { 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 return nil

View file

@ -9,7 +9,7 @@ import (
"path/filepath" "path/filepath"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -7,13 +7,13 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"path/filepath"
"strings" "strings"
"time" "time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
@ -349,11 +349,11 @@ func (s *PrepSubsystem) planList(_ context.Context, _ *mcp.CallToolRequest, inpu
var plans []Plan var plans []Plan
for _, entry := range entries { for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") { if entry.IsDir() || !core.HasSuffix(entry.Name(), ".json") {
continue continue
} }
id := strings.TrimSuffix(entry.Name(), ".json") id := core.TrimSuffix(entry.Name(), ".json")
plan, err := readPlan(dir, id) plan, err := readPlan(dir, id)
if err != nil { if err != nil {
continue continue
@ -422,11 +422,11 @@ func (s *PrepSubsystem) planCheckpoint(_ context.Context, _ *mcp.CallToolRequest
// --- Helpers --- // --- Helpers ---
func (s *PrepSubsystem) plansDir() string { 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 { func planPath(dir, id string) string {
return filepath.Join(dir, id+".json") return core.Path(dir, id+".json")
} }
func generatePlanID(title string) string { func generatePlanID(title string) string {
@ -444,8 +444,8 @@ func generatePlanID(title string) string {
}, title) }, title)
// Trim consecutive dashes and cap length // Trim consecutive dashes and cap length
for strings.Contains(slug, "--") { for core.Contains(slug, "--") {
slug = strings.ReplaceAll(slug, "--", "-") slug = core.Replace(slug, "--", "-")
} }
slug = strings.Trim(slug, "-") slug = strings.Trim(slug, "-")
if len(slug) > 30 { if len(slug) > 30 {
@ -466,8 +466,8 @@ func readPlan(dir, id string) (*Plan, error) {
} }
var plan Plan var plan Plan
if err := json.Unmarshal([]byte(data), &plan); err != nil { if r := core.JSONUnmarshal([]byte(data), &plan); !r.OK {
return nil, coreerr.E("readPlan", "failed to parse plan "+id, err) return nil, coreerr.E("readPlan", "failed to parse plan "+id, nil)
} }
return &plan, nil return &plan, nil
} }

View file

@ -13,8 +13,8 @@ import (
"strings" "strings"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -18,8 +18,8 @@ import (
"time" "time"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )

View file

@ -11,7 +11,7 @@ import (
"syscall" "syscall"
"time" "time"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )

View file

@ -13,7 +13,7 @@ import (
"strings" "strings"
"time" "time"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
) )
func listLocalRepos(basePath string) []string { func listLocalRepos(basePath string) []string {

View file

@ -12,8 +12,8 @@ import (
"syscall" "syscall"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -14,7 +14,7 @@ import (
"time" "time"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -5,11 +5,11 @@ package agentic
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"strings" "strings"
coreerr "forge.lthn.ai/core/go-log" core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "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) seen := make(map[string]bool)
var unique []ScanIssue var unique []ScanIssue
for _, issue := range allIssues { 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] { if !seen[key] {
seen[key] = true seen[key] = true
unique = append(unique, issue) 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) { 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, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "token "+s.forgeToken) 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() defer resp.Body.Close()
if resp.StatusCode != 200 { 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 { 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) { 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) s.forgeURL, org, repo, label)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "token "+s.forgeToken) 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() defer resp.Body.Close()
if resp.StatusCode != 200 { 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 { var issues []struct {

View file

@ -6,13 +6,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"os" "os"
"path/filepath"
"strings"
"time" "time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io"
coreerr "forge.lthn.ai/core/go-log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
@ -57,23 +56,23 @@ func writeStatus(wsDir string, status *WorkspaceStatus) error {
if err != nil { if err != nil {
return err 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) { func (s *PrepSubsystem) saveStatus(wsDir string, status *WorkspaceStatus) {
if err := writeStatus(wsDir, status); err != nil { 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) { 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 { if err != nil {
return nil, err return nil, err
} }
var s WorkspaceStatus var s WorkspaceStatus
if err := json.Unmarshal([]byte(data), &s); err != nil { if r := core.JSONUnmarshal([]byte(data), &s); !r.OK {
return nil, err return nil, coreerr.E("readStatus", "failed to parse status.json", nil)
} }
return &s, nil return &s, nil
} }
@ -126,7 +125,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
var workspaces []WorkspaceInfo var workspaces []WorkspaceInfo
for _, wsDir := range wsDirs { for _, wsDir := range wsDirs {
name := filepath.Base(wsDir) name := core.PathBase(wsDir)
// Filter by specific workspace if requested // Filter by specific workspace if requested
if input.Workspace != "" && name != input.Workspace { if input.Workspace != "" && name != input.Workspace {
@ -139,7 +138,7 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
st, err := readStatus(wsDir) st, err := readStatus(wsDir)
if err != nil { if err != nil {
// Legacy workspace (no status.json) — check for log file // 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 { if len(logFiles) > 0 {
info.Status = "completed" info.Status = "completed"
} else { } else {
@ -177,10 +176,10 @@ func (s *PrepSubsystem) status(ctx context.Context, _ *mcp.CallToolRequest, inpu
} }
// Process died — check for BLOCKED.md // 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 { if data, err := coreio.Local.Read(blockedPath); err == nil {
info.Status = "blocked" info.Status = "blocked"
info.Question = strings.TrimSpace(data) info.Question = core.Trim(data)
st.Status = "blocked" st.Status = "blocked"
st.Question = info.Question st.Question = info.Question
status = "blocked" status = "blocked"

View file

@ -4,11 +4,11 @@ package agentic
import ( import (
"context" "context"
"path/filepath"
"time" "time"
core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreerr "forge.lthn.ai/core/go-log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
@ -153,15 +153,15 @@ func (s *PrepSubsystem) findActiveWorkspaces() []string {
} }
switch st.Status { switch st.Status {
case "running", "queued": case "running", "queued":
active = append(active, filepath.Base(wsDir)) active = append(active, core.PathBase(wsDir))
} }
} }
return active return active
} }
func (s *PrepSubsystem) resolveWorkspaceDir(name string) string { func (s *PrepSubsystem) resolveWorkspaceDir(name string) string {
if filepath.IsAbs(name) { if core.PathIsAbs(name) {
return name return name
} }
return filepath.Join(s.workspaceRoot(), name) return core.JoinPath(s.workspaceRoot(), name)
} }

View file

@ -4,9 +4,9 @@ package agentic
import ( import (
"os" "os"
"path/filepath"
coreio "forge.lthn.ai/core/go-io" core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
) )
// writeAtomic writes content to path by staging it in a temporary file and // writeAtomic writes content to path by staging it in a temporary file and
@ -15,12 +15,12 @@ import (
// This avoids exposing partially written workspace files to agents that may // This avoids exposing partially written workspace files to agents that may
// read status, prompt, or plan documents while they are being updated. // read status, prompt, or plan documents while they are being updated.
func writeAtomic(path, content string) error { func writeAtomic(path, content string) error {
dir := filepath.Dir(path) dir := core.PathDir(path)
if err := coreio.Local.EnsureDir(dir); err != nil { if err := coreio.Local.EnsureDir(dir); err != nil {
return err return err
} }
tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*.tmp") tmp, err := os.CreateTemp(dir, "."+core.PathBase(path)+".*.tmp")
if err != nil { if err != nil {
return err return err
} }

View file

@ -9,7 +9,7 @@ import (
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/ide" "dappco.re/go/mcp/pkg/mcp/ide"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
) )
// errBridgeNotAvailable is returned when a tool requires the Laravel bridge // errBridgeNotAvailable is returned when a tool requires the Laravel bridge

View file

@ -15,8 +15,8 @@ import (
"time" "time"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreio "forge.lthn.ai/core/go-io" coreio "dappco.re/go/core/io"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -7,9 +7,9 @@ import (
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/ide" "dappco.re/go/mcp/pkg/mcp/ide"
"forge.lthn.ai/core/api" "dappco.re/go/core/api"
"forge.lthn.ai/core/api/pkg/provider" "dappco.re/go/core/api/pkg/provider"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )

View file

@ -8,7 +8,7 @@ import (
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/ide" "dappco.re/go/mcp/pkg/mcp/ide"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -3,13 +3,12 @@
package mcp package mcp
import ( import (
"errors"
"net/http" "net/http"
core "dappco.re/go/core" core "dappco.re/go/core"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
api "forge.lthn.ai/core/api" api "dappco.re/go/core/api"
) )
// maxBodySize is the maximum request body size accepted by bridged tool endpoints. // maxBodySize is the maximum request body size accepted by bridged tool endpoints.
@ -48,7 +47,7 @@ func BridgeToAPI(svc *Service, bridge *api.ToolBridge) {
if !r.OK { if !r.OK {
if err, ok := r.Value.(error); ok { if err, ok := r.Value.(error); ok {
var maxBytesErr *http.MaxBytesError 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")) c.JSON(http.StatusRequestEntityTooLarge, api.Fail("request_too_large", "Request body exceeds 10 MB limit"))
return return
} }
@ -63,7 +62,7 @@ func BridgeToAPI(svc *Service, bridge *api.ToolBridge) {
if err != nil { if err != nil {
// Body present + error = likely bad input (malformed JSON). // Body present + error = likely bad input (malformed JSON).
// No body + error = tool execution failure. // 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")) c.JSON(http.StatusBadRequest, api.Fail("invalid_input", "Malformed JSON in request body"))
return return
} }

View file

@ -17,7 +17,7 @@ import (
"dappco.re/go/mcp/pkg/mcp/agentic" "dappco.re/go/mcp/pkg/mcp/agentic"
"dappco.re/go/mcp/pkg/mcp/brain" "dappco.re/go/mcp/pkg/mcp/brain"
"dappco.re/go/mcp/pkg/mcp/ide" "dappco.re/go/mcp/pkg/mcp/ide"
api "forge.lthn.ai/core/api" api "dappco.re/go/core/api"
) )
func init() { func init() {
@ -250,7 +250,7 @@ func TestBridgeToAPI_Good_EndToEnd(t *testing.T) {
} }
// Verify a tool endpoint is reachable through the engine. // Verify a tool endpoint is reachable through the engine.
resp2, err := http.Post(srv.URL+"/tools/lang_list", "application/json", nil) resp2, err := http.Post(srv.URL+"/tools/lang_list", "application/json", strings.NewReader("{}"))
if err != nil { if err != nil {
t.Fatalf("lang_list request failed: %v", err) t.Fatalf("lang_list request failed: %v", err)
} }

View file

@ -9,8 +9,8 @@ import (
"sync" "sync"
"time" "time"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )

View file

@ -11,7 +11,7 @@ import (
"testing" "testing"
"time" "time"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )

View file

@ -4,14 +4,13 @@ package ide
import ( import (
"context" "context"
"fmt"
"sync" "sync"
"time" "time"
core "dappco.re/go/core" core "dappco.re/go/core"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
) )
// errBridgeNotAvailable is returned when a tool requires the Laravel bridge // errBridgeNotAvailable is returned when a tool requires the Laravel bridge
@ -556,7 +555,7 @@ func stringFromAny(v any) string {
switch value := v.(type) { switch value := v.(type) {
case string: case string:
return value return value
case fmt.Stringer: case interface{ String() string }:
return value.String() return value.String()
default: default:
return "" return ""

View file

@ -7,7 +7,7 @@ import (
"time" "time"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
coreerr "forge.lthn.ai/core/go-log" coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -9,7 +9,7 @@ import (
"time" "time"
coremcp "dappco.re/go/mcp/pkg/mcp" coremcp "dappco.re/go/mcp/pkg/mcp"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
) )
// --- Helpers --- // --- Helpers ---

View file

@ -6,21 +6,19 @@ package mcp
import ( import (
"context" "context"
"errors"
"iter" "iter"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"slices" "slices"
"sort" "sort"
"strings"
"sync" "sync"
core "dappco.re/go/core" core "dappco.re/go/core"
"forge.lthn.ai/core/go-io" "dappco.re/go/core/io"
"forge.lthn.ai/core/go-log" "dappco.re/go/core/log"
"forge.lthn.ai/core/go-process" "dappco.re/go/core/process"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
@ -74,7 +72,8 @@ func New(opts Options) (*Service, error) {
server := mcp.NewServer(impl, &mcp.ServerOptions{ server := mcp.NewServer(impl, &mcp.ServerOptions{
Capabilities: &mcp.ServerCapabilities{ Capabilities: &mcp.ServerCapabilities{
Tools: &mcp.ToolCapabilities{ListChanged: true}, Resources: &mcp.ResourceCapabilities{ListChanged: false},
Tools: &mcp.ToolCapabilities{ListChanged: false},
Logging: &mcp.LoggingCapabilities{}, Logging: &mcp.LoggingCapabilities{},
Experimental: channelCapability(), Experimental: channelCapability(),
}, },
@ -245,15 +244,15 @@ func (s *Service) resolveWorkspacePath(path string) string {
} }
if s.workspaceRoot == "" { if s.workspaceRoot == "" {
return filepath.Clean(path) return core.CleanPath(path, "/")
} }
clean := filepath.Clean(string(filepath.Separator) + path) clean := core.CleanPath(string(filepath.Separator)+path, "/")
clean = strings.TrimPrefix(clean, string(filepath.Separator)) clean = core.TrimPrefix(clean, string(filepath.Separator))
if clean == "." || clean == "" { if clean == "." || clean == "" {
return s.workspaceRoot 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. // registerTools adds the built-in tool groups to the MCP server.
@ -615,7 +614,7 @@ func (s *Service) fileExists(ctx context.Context, req *mcp.CallToolRequest, inpu
info, err := s.medium.Stat(input.Path) info, err := s.medium.Stat(input.Path)
if err != nil { 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{Exists: false, IsDir: false, Path: input.Path}, nil
} }
return nil, FileExistsOutput{}, log.E("mcp.fileExists", "failed to stat path", err) return nil, FileExistsOutput{}, log.E("mcp.fileExists", "failed to stat path", err)

View file

@ -14,10 +14,10 @@ import (
"reflect" "reflect"
"slices" "slices"
"sort" "sort"
"strings"
"sync" "sync"
"unsafe" "unsafe"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/mcp" "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 { if s == nil || s.server == nil {
return return
} }
if strings.TrimSpace(channel) == "" { if core.Trim(channel) == "" {
return return
} }
ctx = normalizeNotificationContext(ctx) 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 { if s == nil || s.server == nil || session == nil {
return return
} }
if strings.TrimSpace(channel) == "" { if core.Trim(channel) == "" {
return return
} }
ctx = normalizeNotificationContext(ctx) ctx = normalizeNotificationContext(ctx)
@ -275,6 +275,15 @@ func (s *Service) debugNotify(msg string, args ...any) {
s.logger.Debug(msg, args...) s.logger.Debug(msg, args...)
} }
// NotifySession sends a raw JSON-RPC notification to a specific MCP session.
//
// coremcp.NotifySession(ctx, session, "notifications/claude/channel", map[string]any{
// "content": "build failed", "meta": map[string]string{"severity": "high"},
// })
func NotifySession(ctx context.Context, session *mcp.ServerSession, method string, payload any) error {
return sendSessionNotification(ctx, session, method, payload)
}
func sendSessionNotification(ctx context.Context, session *mcp.ServerSession, method string, payload any) error { func sendSessionNotification(ctx context.Context, session *mcp.ServerSession, method string, payload any) error {
if session == nil { if session == nil {
return nil return nil

View file

@ -4,9 +4,10 @@ package mcp
import ( import (
"context" "context"
"path/filepath"
"strings" "strings"
"time" "time"
core "dappco.re/go/core"
) )
type processRuntime struct { type processRuntime struct {
@ -50,7 +51,7 @@ func (s *Service) forgetProcessRuntime(id string) {
} }
func isTestProcess(command string, args []string) bool { func isTestProcess(command string, args []string) bool {
base := strings.ToLower(filepath.Base(command)) base := core.Lower(core.PathBase(command))
if base == "" { if base == "" {
return false return false
} }
@ -62,7 +63,7 @@ func isTestProcess(command string, args []string) bool {
return len(args) > 0 && strings.EqualFold(args[0], "test") return len(args) > 0 && strings.EqualFold(args[0], "test")
case "npm", "pnpm", "yarn", "bun": case "npm", "pnpm", "yarn", "bun":
for _, arg := range args { 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 return true
} }
} }

View file

@ -7,8 +7,8 @@ import (
"time" "time"
core "dappco.re/go/core" core "dappco.re/go/core"
"forge.lthn.ai/core/go-process" "dappco.re/go/core/process"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
) )
// Register is the service factory for core.WithService. // Register is the service factory for core.WithService.

View file

@ -9,8 +9,8 @@ import (
"time" "time"
"dappco.re/go/core" "dappco.re/go/core"
"forge.lthn.ai/core/go-process" "dappco.re/go/core/process"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
) )
func TestRegister_Good_WiresOptionalServices(t *testing.T) { func TestRegister_Good_WiresOptionalServices(t *testing.T) {

View file

@ -78,7 +78,40 @@ type ToolRecord struct {
// return nil, ReadFileOutput{Path: "src/main.go"}, nil // 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]) { 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) { restHandler := func(ctx context.Context, body []byte) (any, error) {
var input In var input In

View file

@ -7,7 +7,7 @@ import (
"errors" "errors"
"testing" "testing"
"forge.lthn.ai/core/go-process" "dappco.re/go/core/process"
) )
func TestToolRegistry_Good_RecordsTools(t *testing.T) { func TestToolRegistry_Good_RecordsTools(t *testing.T) {

View file

@ -8,8 +8,8 @@ import (
"time" "time"
core "dappco.re/go/core" core "dappco.re/go/core"
"forge.lthn.ai/core/go-ai/ai" "dappco.re/go/core/ai/ai"
"forge.lthn.ai/core/go-log" "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -6,8 +6,8 @@ import (
"context" "context"
"time" "time"
"forge.lthn.ai/core/go-log" "dappco.re/go/core/log"
"forge.lthn.ai/core/go-process" "dappco.re/go/core/process"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -9,7 +9,7 @@ import (
"time" "time"
"dappco.re/go/core" "dappco.re/go/core"
"forge.lthn.ai/core/go-process" "dappco.re/go/core/process"
) )
// newTestProcessService creates a real process.Service backed by a core.Core for CI tests. // newTestProcessService creates a real process.Service backed by a core.Core for CI tests.

View file

@ -6,8 +6,8 @@ import (
"context" "context"
core "dappco.re/go/core" core "dappco.re/go/core"
"forge.lthn.ai/core/go-log" "dappco.re/go/core/log"
"forge.lthn.ai/core/go-rag" "dappco.re/go/core/rag"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -9,13 +9,12 @@ import (
"image" "image"
"image/jpeg" "image/jpeg"
_ "image/png" _ "image/png"
"strings"
"sync" "sync"
"time" "time"
core "dappco.re/go/core" core "dappco.re/go/core"
"forge.lthn.ai/core/go-log" "dappco.re/go/core/log"
"forge.lthn.ai/core/go-webview" "dappco.re/go/core/webview"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
@ -554,7 +553,7 @@ func (s *Service) webviewScreenshot(ctx context.Context, req *mcp.CallToolReques
if format == "" { if format == "" {
format = "png" format = "png"
} }
format = strings.ToLower(format) format = core.Lower(format)
data, err := webviewInstance.Screenshot() data, err := webviewInstance.Screenshot()
if err != nil { if err != nil {
@ -649,7 +648,7 @@ func waitForSelector(ctx context.Context, timeout time.Duration, selector string
if err == nil { if err == nil {
return nil return nil
} }
if !strings.Contains(err.Error(), "element not found") { if !core.Contains(err.Error(), "element not found") {
return err return err
} }

View file

@ -11,7 +11,7 @@ import (
"testing" "testing"
"time" "time"
"forge.lthn.ai/core/go-webview" "dappco.re/go/core/webview"
) )
// skipIfShort skips webview tests in short mode (go test -short). // skipIfShort skips webview tests in short mode (go test -short).

View file

@ -8,8 +8,8 @@ import (
"net/http" "net/http"
core "dappco.re/go/core" core "dappco.re/go/core"
"forge.lthn.ai/core/go-log" "dappco.re/go/core/log"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -3,7 +3,7 @@ package mcp
import ( import (
"testing" "testing"
"forge.lthn.ai/core/go-ws" "dappco.re/go/core/ws"
) )
// TestWSToolsRegistered_Good verifies that WebSocket tools are registered when hub is available. // TestWSToolsRegistered_Good verifies that WebSocket tools are registered when hub is available.

View file

@ -8,10 +8,10 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
coreerr "forge.lthn.ai/core/go-log" core "dappco.re/go/core"
coreerr "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "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. // If token is empty, authentication is disabled for local development.
func withAuth(token string, next http.Handler) http.Handler { func withAuth(token string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.TrimSpace(token) == "" { if core.Trim(token) == "" {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
auth := r.Header.Get("Authorization") auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") { if !core.HasPrefix(auth, "Bearer ") {
http.Error(w, `{"error":"missing Bearer token"}`, http.StatusUnauthorized) http.Error(w, `{"error":"missing Bearer token"}`, http.StatusUnauthorized)
return return
} }
provided := strings.TrimSpace(strings.TrimPrefix(auth, "Bearer ")) provided := core.Trim(core.TrimPrefix(auth, "Bearer "))
if len(provided) == 0 { if len(provided) == 0 {
http.Error(w, `{"error":"missing Bearer token"}`, http.StatusUnauthorized) http.Error(w, `{"error":"missing Bearer token"}`, http.StatusUnauthorized)
return return

View file

@ -6,7 +6,7 @@ import (
"context" "context"
"os" "os"
"forge.lthn.ai/core/go-log" "dappco.re/go/core/log"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )

View file

@ -5,12 +5,12 @@ package mcp
import ( import (
"bufio" "bufio"
"context" "context"
"fmt"
goio "io" goio "io"
"net" "net"
"os" "os"
"sync" "sync"
core "dappco.re/go/core"
"github.com/modelcontextprotocol/go-sdk/jsonrpc" "github.com/modelcontextprotocol/go-sdk/jsonrpc"
"github.com/modelcontextprotocol/go-sdk/mcp" "github.com/modelcontextprotocol/go-sdk/mcp"
) )
@ -31,7 +31,7 @@ var diagWriter goio.Writer = os.Stderr
func diagPrintf(format string, args ...any) { func diagPrintf(format string, args ...any) {
diagMu.Lock() diagMu.Lock()
defer diagMu.Unlock() defer diagMu.Unlock()
fmt.Fprintf(diagWriter, format, args...) core.Print(diagWriter, format, args...)
} }
// setDiagWriter swaps the diagnostic writer and returns the previous one. // setDiagWriter swaps the diagnostic writer and returns the previous one.

View file

@ -6,8 +6,8 @@ import (
"context" "context"
"net" "net"
"forge.lthn.ai/core/go-io" "dappco.re/go/core/io"
"forge.lthn.ai/core/go-log" "dappco.re/go/core/log"
) )
// ServeUnix starts a Unix domain socket server for the MCP service. // ServeUnix starts a Unix domain socket server for the MCP service.