diff --git a/cmd/brain-seed/main.go b/cmd/brain-seed/main.go index a8edb26..172ff31 100644 --- a/cmd/brain-seed/main.go +++ b/cmd/brain-seed/main.go @@ -21,8 +21,8 @@ import ( "regexp" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + coreio "dappco.re/go/io" + coreerr "dappco.re/go/log" brainclient "dappco.re/go/mcp/pkg/mcp/brain/client" ) diff --git a/cmd/core-mcp/main.go b/cmd/core-mcp/main.go index d64557e..7a7ca87 100644 --- a/cmd/core-mcp/main.go +++ b/cmd/core-mcp/main.go @@ -1,7 +1,7 @@ package main import ( - "dappco.re/go/core/cli/pkg/cli" + "dappco.re/go/cli/pkg/cli" mcpcmd "dappco.re/go/mcp/cmd/mcpcmd" ) diff --git a/cmd/openbrain-mcp/README.md b/cmd/openbrain-mcp/README.md new file mode 100644 index 0000000..a20df72 --- /dev/null +++ b/cmd/openbrain-mcp/README.md @@ -0,0 +1,29 @@ +# openbrain-mcp + +`openbrain-mcp` is a thin stdio MCP wrapper for the OpenBrain tools registered in `pkg/mcp/brain`. + +Install: + +```sh +go install dappco.re/go/mcp/cmd/openbrain-mcp@latest +``` + +Add it to Claude Code: + +```sh +claude mcp add openbrain -- openbrain-mcp --brain-url=http://127.0.0.1:8000/v1/brain --api-key=$OPENBRAIN_API_KEY +``` + +The wrapper exposes: + +- `brain_remember` +- `brain_recall` +- `brain_forget` +- `brain_list` + +Flags: + +- `--brain-url`: OpenBrain BrainService URL. Defaults to `http://127.0.0.1:8000/v1/brain`. +- `--api-key`: OpenBrain API key. Defaults to `OPENBRAIN_API_KEY`. + +The process logs to stderr only. Stdout is reserved for MCP framing. diff --git a/cmd/openbrain-mcp/main.go b/cmd/openbrain-mcp/main.go new file mode 100644 index 0000000..cc73da5 --- /dev/null +++ b/cmd/openbrain-mcp/main.go @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: EUPL-1.2 + +// openbrain-mcp exposes the OpenBrain MCP tools over stdio for Claude Code. +package main + +import ( + "context" + "flag" + "os" + "os/signal" + "syscall" + "time" + + core "dappco.re/go/core" + coreerr "dappco.re/go/log" + "dappco.re/go/mcp/pkg/mcp" + "dappco.re/go/mcp/pkg/mcp/brain" +) + +const defaultBrainURL = "http://127.0.0.1:8000/v1/brain" + +var ( + brainURLFlag = flag.String("brain-url", defaultBrainURL, "OpenBrain BrainService URL") + apiKeyFlag = flag.String("api-key", "", "OpenBrain API key (defaults to OPENBRAIN_API_KEY)") +) + +func main() { + if err := run(); err != nil { + coreerr.Error("openbrain-mcp failed", "err", err) + os.Exit(1) + } +} + +func run() error { + flag.Parse() + + if err := configureBrainEnv(*brainURLFlag, *apiKeyFlag); err != nil { + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + svc, err := mcp.New(mcp.Options{ + Subsystems: []mcp.Subsystem{ + brain.NewDirect(), + }, + }) + if err != nil { + return core.E("openbrain-mcp.run", "create MCP service", err) + } + defer shutdownService(svc) + + if err := svc.ServeStdio(ctx); err != nil && !core.Is(err, context.Canceled) { + return core.E("openbrain-mcp.run", "serve stdio", err) + } + return nil +} + +func configureBrainEnv(brainURL, apiKey string) error { + baseURL := directBrainBaseURL(brainURL) + if baseURL == "" { + baseURL = directBrainBaseURL(defaultBrainURL) + } + if err := os.Setenv("CORE_BRAIN_URL", baseURL); err != nil { + return core.E("openbrain-mcp.configure", "set CORE_BRAIN_URL", err) + } + + key := core.Trim(apiKey) + if key == "" { + key = core.Trim(core.Env("OPENBRAIN_API_KEY")) + } + if key == "" { + return nil + } + if err := os.Setenv("CORE_BRAIN_KEY", key); err != nil { + return core.E("openbrain-mcp.configure", "set CORE_BRAIN_KEY", err) + } + return nil +} + +func directBrainBaseURL(brainURL string) string { + baseURL := core.Trim(brainURL) + baseURL = core.TrimSuffix(baseURL, "/") + baseURL = core.TrimSuffix(baseURL, "/v1/brain") + return core.TrimSuffix(baseURL, "/") +} + +func shutdownService(svc *mcp.Service) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := svc.Shutdown(ctx); err != nil { + coreerr.Error("openbrain-mcp shutdown failed", "err", err) + } +} diff --git a/docs/security-vulnerabilities.md b/docs/security-vulnerabilities.md new file mode 100644 index 0000000..3462203 --- /dev/null +++ b/docs/security-vulnerabilities.md @@ -0,0 +1,141 @@ +# Security Vulnerabilities — Accepted Findings + Operator Mitigations + +This document records security findings (govulncheck, etc.) that have been +manually reviewed and **accepted with documented rationale** rather than +patched. Each entry names the CVE, what makes it not-applicable to our use +case, and any operator-side mitigations required to keep that not-applicable +status valid. + +Audit history: +- Mantis #323 — 9 ollama CVEs reviewed and documented (2026-04-25) + +--- + +## github.com/ollama/ollama (indirect via go-rag) + +**Status as of 2026-04-25:** all 9 CVEs filed in Mantis #323 are **UNFIXED +upstream** per [pkg.go.dev/vuln](https://pkg.go.dev/vuln/). Pin-bumping does +not resolve any of them. We are on `v0.18.1` indirect; ollama upstream is at +`v0.21.2` (2026-04-23). + +**Our usage scope:** the entire workspace imports `github.com/ollama/ollama/api` +from exactly ONE file (`go-rag/ollama.go`). The surface in use is **3 symbols +only**: +- `api.NewClient(baseURL, *http.Client)` — constructor +- `api.Client` — struct value (held as a field by `OllamaClient`) +- `api.EmbedRequest` — embedding-request DTO + +**We are a CLIENT** of someone else's Ollama server. We do NOT host an Ollama +server. Most CVEs in the list are server-side code paths that govulncheck's +reachability graph flags because the package is imported, but our actual call +sites do not traverse those paths. + +### CVE-by-CVE reachability assessment + +| CVE | Description | Reachable from our call graph? | Action | +|---|---|---|---| +| GO-2025-3548 (CVE-2024-12886) | DoS via crafted GZIP | NO — server-side parser | Accept | +| GO-2025-3557 (CVE-2025-0315) | Resource alloc without limits | NO — server-side dispatcher | Accept | +| GO-2025-3558 | Out-of-bounds read | NO — server-side inference | Accept | +| GO-2025-3559 | Divide by zero | NO — server-side inference | Accept | +| GO-2025-3582 | Null pointer deref DoS | NO — server-side handler | Accept | +| GO-2025-3689 | Divide by zero | NO — server-side inference | Accept | +| GO-2025-3695 | Server DoS | NO — server-side handler | Accept | +| GO-2025-3824 (CVE-2025-51471) | Cross-domain token exposure | **CONDITIONAL** — see below | Watch | +| GO-2025-4251 (CVE-2025-63389) | Missing auth on model-mgmt | **OPERATOR-SIDE** — see below | Runbook | + +### GO-2025-3824 — token-exposure watch flag + +This CVE concerns auth tokens leaking across domain boundaries when Ollama +clients pass authentication. Currently `NewOllamaClient(cfg)` constructs over +plain HTTP/HTTPS without auth headers — the embedding client connects to a +trusted local Ollama instance per the deployment runbook below. + +**If we ever add auth-token plumbing to the Ollama client** (e.g. for hosted +Ollama services), re-evaluate this CVE. The reachability flips from NO to YES +the moment we set an Authorization header on `api.NewClient`. + +### GO-2025-4251 — operator-side mitigation required + +This CVE is a missing authentication / authorization gap on Ollama's +model-management endpoints. The vulnerability is in the **Ollama server**, +not our client code. Our client doesn't expose model-management calls; +operators do via running an Ollama server. + +**Operator mitigation (REQUIRED):** see "Ollama deployment" section below. +Operators MUST front their Ollama instance with network-level access controls +or an authentication proxy. This is also Ollama upstream's own recommendation +in the advisory. + +### Watch flag + +If any of the 9 CVEs gets a fixed version released, re-evaluate: +- Bump `go-rag/go.mod` require for `github.com/ollama/ollama` to the fixed version +- Re-run govulncheck and prune entries from this document accordingly + +--- + +## Ollama deployment — operator runbook + +The Ollama instance the agent connects to runs OUTSIDE of our application +boundary. Operators are responsible for these mitigations: + +### 1. Network-level isolation (mandatory) + +Bind the Ollama server to a private interface or front it with a reverse proxy: + +```bash +# OPTION A — localhost-only binding (single-host deployments) +OLLAMA_HOST=127.0.0.1:11434 ollama serve + +# OPTION B — private network only (multi-host fleet) +# Bind to the wireguard / tailscale / private-VLAN interface, not 0.0.0.0 +OLLAMA_HOST=10.42.0.5:11434 ollama serve +``` + +**Never** expose Ollama directly to the public internet. GO-2025-4251 makes +model-management operations possible without auth. + +### 2. Reverse proxy with auth (recommended for shared deployments) + +If multiple agents share an Ollama server, front it with nginx/caddy/traefik +adding HTTP Basic Auth or an authentication proxy (oauth2-proxy, authentik): + +```nginx +location /ollama/ { + auth_basic "Ollama API"; + auth_basic_user_file /etc/nginx/ollama.htpasswd; + proxy_pass http://10.42.0.5:11434/; +} +``` + +Configure the agent's `OllamaConfig.Endpoint` to point at the reverse proxy +URL, and add an `Authorization` header to the http.Client passed to +`api.NewClient`. (When that change lands, re-evaluate GO-2025-3824 per +the watch-flag note above.) + +### 3. CI-side govulncheck filter + +Until upstream Ollama ships fixes for any of the 9 CVEs, CI should suppress +just these specific findings (not blanket-suppress all govulncheck output): + +```bash +govulncheck ./... 2>&1 | grep -vE 'GO-2025-(3548|3557|3558|3559|3582|3689|3695|3824|4251)\b' +``` + +When a CVE gets a fix and we bump past it, drop that CVE ID from the grep +filter so future regressions surface cleanly. + +--- + +## How to add to this document + +When a new accepted finding lands: + +1. Open a new H2 section named for the dependency +2. Document the reachability + rationale per CVE in a table +3. Add operator-side mitigations if any +4. Update the audit-history bullet at the top with a Mantis ticket reference + +**Do NOT add findings here without a Mantis ticket.** Every accepted finding +must have a tracker entry so the rationale is auditable + reviewable. diff --git a/go.mod b/go.mod index 295c21c..9da690b 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,15 @@ go 1.26.0 require ( dappco.re/go/core v0.8.0-alpha.1 - dappco.re/go/core/ai v0.2.2 - dappco.re/go/core/api v0.3.0 - dappco.re/go/core/cli v0.5.2 - dappco.re/go/core/io v0.4.1 - dappco.re/go/core/log v0.1.2 - dappco.re/go/core/process v0.5.0 - dappco.re/go/core/rag v0.1.13 - dappco.re/go/core/webview v0.2.1 - dappco.re/go/core/ws v0.4.0 + dappco.re/go/ai v0.8.0-alpha.1 + dappco.re/go/api v0.8.0-alpha.1 + dappco.re/go/cli v0.8.0-alpha.1 + dappco.re/go/io v0.8.0-alpha.1 + dappco.re/go/log v0.8.0-alpha.1 + dappco.re/go/process v0.8.0-alpha.1 + dappco.re/go/rag v0.8.0-alpha.1 + dappco.re/go/webview v0.8.0-alpha.1 + dappco.re/go/ws v0.8.0-alpha.1 github.com/gin-gonic/gin v1.12.0 github.com/gorilla/websocket v1.5.3 github.com/modelcontextprotocol/go-sdk v1.5.0 @@ -21,8 +21,8 @@ require ( ) require ( - dappco.re/go/core/i18n v0.2.3 // indirect - dappco.re/go/core/inference v0.3.0 // indirect + dappco.re/go/i18n v0.8.0-alpha.1 // indirect + dappco.re/go/inference v0.8.0-alpha.1 // indirect github.com/99designs/gqlgen v0.17.88 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect diff --git a/pkg/mcp/agentic/dispatch.go b/pkg/mcp/agentic/dispatch.go index e0a47b8..2596dfd 100644 --- a/pkg/mcp/agentic/dispatch.go +++ b/pkg/mcp/agentic/dispatch.go @@ -10,8 +10,8 @@ import ( "time" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + coreio "dappco.re/go/io" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/epic.go b/pkg/mcp/agentic/epic.go index 6f8dd4d..256f25b 100644 --- a/pkg/mcp/agentic/epic.go +++ b/pkg/mcp/agentic/epic.go @@ -9,7 +9,7 @@ import ( "net/http" core "dappco.re/go/core" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/ingest.go b/pkg/mcp/agentic/ingest.go index ca87761..3a3c23a 100644 --- a/pkg/mcp/agentic/ingest.go +++ b/pkg/mcp/agentic/ingest.go @@ -7,7 +7,7 @@ import ( "net/http" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" + coreio "dappco.re/go/io" coremcp "dappco.re/go/mcp/pkg/mcp" ) diff --git a/pkg/mcp/agentic/issue.go b/pkg/mcp/agentic/issue.go index 1b66c0e..bfae689 100644 --- a/pkg/mcp/agentic/issue.go +++ b/pkg/mcp/agentic/issue.go @@ -9,7 +9,7 @@ import ( "net/http" core "dappco.re/go/core" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/mirror.go b/pkg/mcp/agentic/mirror.go index 708c99c..1849693 100644 --- a/pkg/mcp/agentic/mirror.go +++ b/pkg/mcp/agentic/mirror.go @@ -7,7 +7,7 @@ import ( "os/exec" core "dappco.re/go/core" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/plan.go b/pkg/mcp/agentic/plan.go index e872069..cc7d23e 100644 --- a/pkg/mcp/agentic/plan.go +++ b/pkg/mcp/agentic/plan.go @@ -10,8 +10,8 @@ import ( "time" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + coreio "dappco.re/go/io" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/pr.go b/pkg/mcp/agentic/pr.go index 9f624b0..f766c80 100644 --- a/pkg/mcp/agentic/pr.go +++ b/pkg/mcp/agentic/pr.go @@ -10,8 +10,8 @@ import ( "os/exec" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + coreio "dappco.re/go/io" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/prep.go b/pkg/mcp/agentic/prep.go index 71ca64e..4c4f859 100644 --- a/pkg/mcp/agentic/prep.go +++ b/pkg/mcp/agentic/prep.go @@ -13,8 +13,8 @@ import ( "time" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + coreio "dappco.re/go/io" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" "gopkg.in/yaml.v3" diff --git a/pkg/mcp/agentic/queue.go b/pkg/mcp/agentic/queue.go index a02c7b9..5eaf772 100644 --- a/pkg/mcp/agentic/queue.go +++ b/pkg/mcp/agentic/queue.go @@ -9,7 +9,7 @@ import ( "time" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" + coreio "dappco.re/go/io" "gopkg.in/yaml.v3" ) diff --git a/pkg/mcp/agentic/repo_helpers.go b/pkg/mcp/agentic/repo_helpers.go index cb03de0..1930cac 100644 --- a/pkg/mcp/agentic/repo_helpers.go +++ b/pkg/mcp/agentic/repo_helpers.go @@ -11,8 +11,8 @@ import ( "time" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + coreio "dappco.re/go/io" + coreerr "dappco.re/go/log" ) func listLocalRepos(basePath string) []string { diff --git a/pkg/mcp/agentic/resume.go b/pkg/mcp/agentic/resume.go index ac5160f..a70976b 100644 --- a/pkg/mcp/agentic/resume.go +++ b/pkg/mcp/agentic/resume.go @@ -9,8 +9,8 @@ import ( "syscall" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + coreio "dappco.re/go/io" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/review_queue.go b/pkg/mcp/agentic/review_queue.go index 050df04..832f022 100644 --- a/pkg/mcp/agentic/review_queue.go +++ b/pkg/mcp/agentic/review_queue.go @@ -11,7 +11,7 @@ import ( "time" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" + coreio "dappco.re/go/io" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/scan.go b/pkg/mcp/agentic/scan.go index 7e40f47..0817df5 100644 --- a/pkg/mcp/agentic/scan.go +++ b/pkg/mcp/agentic/scan.go @@ -8,7 +8,7 @@ import ( "net/http" core "dappco.re/go/core" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/status.go b/pkg/mcp/agentic/status.go index d789ece..3a300c5 100644 --- a/pkg/mcp/agentic/status.go +++ b/pkg/mcp/agentic/status.go @@ -9,8 +9,8 @@ import ( "time" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" - coreerr "dappco.re/go/core/log" + coreio "dappco.re/go/io" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/watch.go b/pkg/mcp/agentic/watch.go index 3449daf..802f55a 100644 --- a/pkg/mcp/agentic/watch.go +++ b/pkg/mcp/agentic/watch.go @@ -7,7 +7,7 @@ import ( "time" core "dappco.re/go/core" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/agentic/write_atomic.go b/pkg/mcp/agentic/write_atomic.go index 8f46118..c323788 100644 --- a/pkg/mcp/agentic/write_atomic.go +++ b/pkg/mcp/agentic/write_atomic.go @@ -6,7 +6,7 @@ import ( "os" core "dappco.re/go/core" - coreio "dappco.re/go/core/io" + coreio "dappco.re/go/io" ) // os.CreateTemp, os.Remove, os.Rename are framework-boundary calls for diff --git a/pkg/mcp/authz.go b/pkg/mcp/authz.go index d2de3b3..a86e330 100644 --- a/pkg/mcp/authz.go +++ b/pkg/mcp/authz.go @@ -14,7 +14,7 @@ import ( "time" core "dappco.re/go/core" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/brain/brain.go b/pkg/mcp/brain/brain.go index 14d9b71..a8b7185 100644 --- a/pkg/mcp/brain/brain.go +++ b/pkg/mcp/brain/brain.go @@ -7,7 +7,7 @@ package brain import ( "context" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "dappco.re/go/mcp/pkg/mcp/ide" ) diff --git a/pkg/mcp/brain/provider.go b/pkg/mcp/brain/provider.go index 9c3ac8e..d67f45d 100644 --- a/pkg/mcp/brain/provider.go +++ b/pkg/mcp/brain/provider.go @@ -5,9 +5,9 @@ package brain import ( "net/http" - "dappco.re/go/core/api" + "dappco.re/go/api" "dappco.re/go/core/api/pkg/provider" - "dappco.re/go/core/ws" + "dappco.re/go/ws" coremcp "dappco.re/go/mcp/pkg/mcp" "dappco.re/go/mcp/pkg/mcp/ide" "github.com/gin-gonic/gin" diff --git a/pkg/mcp/brain/tools.go b/pkg/mcp/brain/tools.go index 0280762..ced8601 100644 --- a/pkg/mcp/brain/tools.go +++ b/pkg/mcp/brain/tools.go @@ -6,7 +6,7 @@ import ( "context" "time" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" coremcp "dappco.re/go/mcp/pkg/mcp" "dappco.re/go/mcp/pkg/mcp/ide" "github.com/modelcontextprotocol/go-sdk/mcp" diff --git a/pkg/mcp/bridge.go b/pkg/mcp/bridge.go index 22a1e8a..fda799b 100644 --- a/pkg/mcp/bridge.go +++ b/pkg/mcp/bridge.go @@ -8,7 +8,7 @@ import ( core "dappco.re/go/core" "github.com/gin-gonic/gin" - api "dappco.re/go/core/api" + api "dappco.re/go/api" ) // maxBodySize is the maximum request body size accepted by bridged tool endpoints. diff --git a/pkg/mcp/bridge_test.go b/pkg/mcp/bridge_test.go index 4bfd169..1dfee19 100644 --- a/pkg/mcp/bridge_test.go +++ b/pkg/mcp/bridge_test.go @@ -17,7 +17,7 @@ import ( "dappco.re/go/mcp/pkg/mcp/agentic" "dappco.re/go/mcp/pkg/mcp/brain" "dappco.re/go/mcp/pkg/mcp/ide" - api "dappco.re/go/core/api" + api "dappco.re/go/api" ) func init() { diff --git a/pkg/mcp/ide/bridge.go b/pkg/mcp/ide/bridge.go index 928eb8a..67aceaa 100644 --- a/pkg/mcp/ide/bridge.go +++ b/pkg/mcp/ide/bridge.go @@ -9,8 +9,8 @@ import ( "sync" "time" - coreerr "dappco.re/go/core/log" - "dappco.re/go/core/ws" + coreerr "dappco.re/go/log" + "dappco.re/go/ws" "github.com/gorilla/websocket" ) diff --git a/pkg/mcp/ide/bridge_test.go b/pkg/mcp/ide/bridge_test.go index 41f9ae1..823a1e2 100644 --- a/pkg/mcp/ide/bridge_test.go +++ b/pkg/mcp/ide/bridge_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "dappco.re/go/core/ws" + "dappco.re/go/ws" "github.com/gorilla/websocket" ) diff --git a/pkg/mcp/ide/ide.go b/pkg/mcp/ide/ide.go index 1832ee9..fcbf21f 100644 --- a/pkg/mcp/ide/ide.go +++ b/pkg/mcp/ide/ide.go @@ -9,8 +9,8 @@ import ( core "dappco.re/go/core" coremcp "dappco.re/go/mcp/pkg/mcp" - coreerr "dappco.re/go/core/log" - "dappco.re/go/core/ws" + coreerr "dappco.re/go/log" + "dappco.re/go/ws" ) // errBridgeNotAvailable is returned when a tool requires the Laravel bridge diff --git a/pkg/mcp/ide/tools_chat.go b/pkg/mcp/ide/tools_chat.go index 068ac36..3133eed 100644 --- a/pkg/mcp/ide/tools_chat.go +++ b/pkg/mcp/ide/tools_chat.go @@ -7,7 +7,7 @@ import ( "time" coremcp "dappco.re/go/mcp/pkg/mcp" - coreerr "dappco.re/go/core/log" + coreerr "dappco.re/go/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/ide/tools_test.go b/pkg/mcp/ide/tools_test.go index c462e9f..59c5855 100644 --- a/pkg/mcp/ide/tools_test.go +++ b/pkg/mcp/ide/tools_test.go @@ -9,7 +9,7 @@ import ( "time" coremcp "dappco.re/go/mcp/pkg/mcp" - "dappco.re/go/core/ws" + "dappco.re/go/ws" ) // --- Helpers --- diff --git a/pkg/mcp/ipc.go b/pkg/mcp/ipc.go new file mode 100644 index 0000000..34ea691 --- /dev/null +++ b/pkg/mcp/ipc.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package mcp + +import ( + "context" + + core "dappco.re/go/core" +) + +func (s *Service) handleChannelPushIPC(ctx context.Context, ev ChannelPush) core.Result { + if core.Trim(ev.Channel) == "" { + return core.Result{Value: core.E("mcp.HandleIPCEvents", "channel is required", nil), OK: false} + } + + s.ChannelSend(ctx, ev.Channel, ev.Data) + return core.Result{OK: true} +} diff --git a/pkg/mcp/ipc_test.go b/pkg/mcp/ipc_test.go new file mode 100644 index 0000000..b19dca2 --- /dev/null +++ b/pkg/mcp/ipc_test.go @@ -0,0 +1,111 @@ +package mcp + +import ( + "testing" + "time" +) + +func TestIPC_HandleIPCEvents_Good(t *testing.T) { + svc, err := New(Options{}) + if err != nil { + t.Fatalf("New() failed: %v", err) + } + + cancel, session, clientConn := connectNotificationSession(t, svc) + defer cancel() + defer session.Close() + defer clientConn.Close() + + clientConn.SetDeadline(time.Now().Add(5 * time.Second)) + read := readNotificationMessageUntil(t, clientConn, func(msg map[string]any) bool { + return msg["method"] == ChannelNotificationMethod + }) + + result := svc.HandleIPCEvents(nil, ChannelPush{ + Channel: "agent.completed", + Data: map[string]any{ + "repo": "core/mcp", + "ok": true, + }, + }) + if !result.OK { + t.Fatalf("HandleIPCEvents() returned non-OK result: %#v", result.Value) + } + + res := <-read + if res.err != nil { + t.Fatalf("failed to read channel notification: %v", res.err) + } + + params, ok := res.msg["params"].(map[string]any) + if !ok { + t.Fatalf("expected params object, got %T", res.msg["params"]) + } + if params["channel"] != "agent.completed" { + t.Fatalf("expected channel agent.completed, got %#v", params["channel"]) + } + + payload, ok := params["data"].(map[string]any) + if !ok { + t.Fatalf("expected data object, got %T", params["data"]) + } + if payload["repo"] != "core/mcp" || payload["ok"] != true { + t.Fatalf("unexpected payload: %#v", payload) + } +} + +func TestIPC_HandleIPCEvents_Bad(t *testing.T) { + svc, err := New(Options{}) + if err != nil { + t.Fatalf("New() failed: %v", err) + } + + result := svc.HandleIPCEvents(nil, ChannelPush{ + Channel: " \t ", + Data: map[string]any{"ok": false}, + }) + if result.OK { + t.Fatal("expected empty ChannelPush channel to fail") + } + if _, ok := result.Value.(error); !ok { + t.Fatalf("expected error result value, got %T", result.Value) + } +} + +func TestIPC_HandleIPCEvents_Ugly(t *testing.T) { + svc, err := New(Options{}) + if err != nil { + t.Fatalf("New() failed: %v", err) + } + + cancel, session, clientConn := connectNotificationSession(t, svc) + defer cancel() + defer session.Close() + defer clientConn.Close() + + clientConn.SetDeadline(time.Now().Add(5 * time.Second)) + read := readNotificationMessageUntil(t, clientConn, func(msg map[string]any) bool { + params, ok := msg["params"].(map[string]any) + return msg["method"] == ChannelNotificationMethod && ok && params["channel"] == "agent.edge" + }) + + result := svc.HandleIPCEvents(nil, ChannelPush{Channel: "agent.edge"}) + if !result.OK { + t.Fatalf("HandleIPCEvents() returned non-OK result: %#v", result.Value) + } + + res := <-read + if res.err != nil { + t.Fatalf("failed to read edge notification: %v", res.err) + } + params, ok := res.msg["params"].(map[string]any) + if !ok { + t.Fatalf("expected params object, got %T", res.msg["params"]) + } + if _, ok := params["data"]; !ok { + t.Fatalf("expected data key for nil ChannelPush data: %#v", params) + } + if params["data"] != nil { + t.Fatalf("expected nil data, got %#v", params["data"]) + } +} diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 7864111..45f523b 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -15,10 +15,10 @@ import ( "sync" core "dappco.re/go/core" - "dappco.re/go/core/io" - "dappco.re/go/core/log" - "dappco.re/go/core/process" - "dappco.re/go/core/ws" + "dappco.re/go/io" + "dappco.re/go/log" + "dappco.re/go/process" + "dappco.re/go/ws" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/notify.go b/pkg/mcp/notify.go index 17c9eeb..a916512 100644 --- a/pkg/mcp/notify.go +++ b/pkg/mcp/notify.go @@ -11,7 +11,7 @@ import ( "context" "io" "iter" - "os" + "os" // Note: required for process stdout; core Fs/Env do not expose a stdio writer. "reflect" "slices" "sync" diff --git a/pkg/mcp/register.go b/pkg/mcp/register.go index e3c6dc1..51ab889 100644 --- a/pkg/mcp/register.go +++ b/pkg/mcp/register.go @@ -7,8 +7,8 @@ import ( "time" core "dappco.re/go/core" - "dappco.re/go/core/process" - "dappco.re/go/core/ws" + "dappco.re/go/process" + "dappco.re/go/ws" ) // Register is the service factory for core.WithService. @@ -98,6 +98,7 @@ func (s *Service) OnStartup(ctx context.Context) core.Result { // HandleIPCEvents implements Core's IPC handler interface. // // c.ACTION(mcp.ChannelPush{Channel: "agent.status", Data: statusMap}) +// // Catches ChannelPush messages from other services and pushes them to Claude Code sessions. func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result { ctx := context.Background() @@ -109,7 +110,7 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result { switch ev := msg.(type) { case ChannelPush: - s.ChannelSend(ctx, ev.Channel, ev.Data) + return s.handleChannelPushIPC(ctx, ev) case process.ActionProcessStarted: startedAt := time.Now() s.recordProcessRuntime(ev.ID, processRuntime{ diff --git a/pkg/mcp/register_test.go b/pkg/mcp/register_test.go index b907e66..f1bf904 100644 --- a/pkg/mcp/register_test.go +++ b/pkg/mcp/register_test.go @@ -9,8 +9,8 @@ import ( "time" "dappco.re/go/core" - "dappco.re/go/core/process" - "dappco.re/go/core/ws" + "dappco.re/go/process" + "dappco.re/go/ws" ) func TestRegister_Good_WiresOptionalServices(t *testing.T) { diff --git a/pkg/mcp/registry_test.go b/pkg/mcp/registry_test.go index c2019d8..4a1a807 100644 --- a/pkg/mcp/registry_test.go +++ b/pkg/mcp/registry_test.go @@ -7,7 +7,7 @@ import ( "errors" "testing" - "dappco.re/go/core/process" + "dappco.re/go/process" ) func TestToolRegistry_Good_RecordsTools(t *testing.T) { diff --git a/pkg/mcp/tools_metrics.go b/pkg/mcp/tools_metrics.go index 6214f5b..3979e62 100644 --- a/pkg/mcp/tools_metrics.go +++ b/pkg/mcp/tools_metrics.go @@ -8,8 +8,8 @@ import ( "time" core "dappco.re/go/core" - "dappco.re/go/core/ai/ai" - "dappco.re/go/core/log" + "dappco.re/go/ai/ai" + "dappco.re/go/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/tools_process.go b/pkg/mcp/tools_process.go index 7999b40..090776f 100644 --- a/pkg/mcp/tools_process.go +++ b/pkg/mcp/tools_process.go @@ -6,8 +6,8 @@ import ( "context" "time" - "dappco.re/go/core/log" - "dappco.re/go/core/process" + "dappco.re/go/log" + "dappco.re/go/process" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/tools_process_ci_test.go b/pkg/mcp/tools_process_ci_test.go index 2e7dfe9..5c81e84 100644 --- a/pkg/mcp/tools_process_ci_test.go +++ b/pkg/mcp/tools_process_ci_test.go @@ -9,7 +9,7 @@ import ( "time" "dappco.re/go/core" - "dappco.re/go/core/process" + "dappco.re/go/process" ) // newTestProcessService creates a real process.Service backed by a core.Core for CI tests. diff --git a/pkg/mcp/tools_rag.go b/pkg/mcp/tools_rag.go index ab9b981..3397175 100644 --- a/pkg/mcp/tools_rag.go +++ b/pkg/mcp/tools_rag.go @@ -6,8 +6,8 @@ import ( "context" core "dappco.re/go/core" - "dappco.re/go/core/log" - "dappco.re/go/core/rag" + "dappco.re/go/log" + "dappco.re/go/rag" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/tools_webview.go b/pkg/mcp/tools_webview.go index d582a38..9893274 100644 --- a/pkg/mcp/tools_webview.go +++ b/pkg/mcp/tools_webview.go @@ -13,8 +13,8 @@ import ( "time" core "dappco.re/go/core" - "dappco.re/go/core/log" - "dappco.re/go/core/webview" + "dappco.re/go/log" + "dappco.re/go/webview" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/tools_webview_embed.go b/pkg/mcp/tools_webview_embed.go index ff6d336..8d91abb 100644 --- a/pkg/mcp/tools_webview_embed.go +++ b/pkg/mcp/tools_webview_embed.go @@ -8,7 +8,7 @@ import ( "time" core "dappco.re/go/core" - "dappco.re/go/core/log" + "dappco.re/go/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/tools_webview_test.go b/pkg/mcp/tools_webview_test.go index 43b3661..53b2b3a 100644 --- a/pkg/mcp/tools_webview_test.go +++ b/pkg/mcp/tools_webview_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "dappco.re/go/core/webview" + "dappco.re/go/webview" ) // skipIfShort skips webview tests in short mode (go test -short). diff --git a/pkg/mcp/tools_ws.go b/pkg/mcp/tools_ws.go index 4d46c17..e8d810b 100644 --- a/pkg/mcp/tools_ws.go +++ b/pkg/mcp/tools_ws.go @@ -8,8 +8,8 @@ import ( "net/http" core "dappco.re/go/core" - "dappco.re/go/core/log" - "dappco.re/go/core/ws" + "dappco.re/go/log" + "dappco.re/go/ws" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/tools_ws_client.go b/pkg/mcp/tools_ws_client.go index 1895d1a..c333d63 100644 --- a/pkg/mcp/tools_ws_client.go +++ b/pkg/mcp/tools_ws_client.go @@ -11,7 +11,7 @@ import ( "time" core "dappco.re/go/core" - "dappco.re/go/core/log" + "dappco.re/go/log" "github.com/gorilla/websocket" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/tools_ws_test.go b/pkg/mcp/tools_ws_test.go index 831022b..6d775fd 100644 --- a/pkg/mcp/tools_ws_test.go +++ b/pkg/mcp/tools_ws_test.go @@ -3,7 +3,7 @@ package mcp import ( "testing" - "dappco.re/go/core/ws" + "dappco.re/go/ws" ) // TestWSToolsRegistered_Good verifies that WebSocket tools are registered when hub is available. diff --git a/pkg/mcp/transport_http.go b/pkg/mcp/transport_http.go index 94a8436..1796231 100644 --- a/pkg/mcp/transport_http.go +++ b/pkg/mcp/transport_http.go @@ -13,8 +13,8 @@ import ( "time" core "dappco.re/go/core" - api "dappco.re/go/core/api" - coreerr "dappco.re/go/core/log" + api "dappco.re/go/api" + coreerr "dappco.re/go/log" "github.com/gin-gonic/gin" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/transport_stdio.go b/pkg/mcp/transport_stdio.go index db5bed8..d72d3fb 100644 --- a/pkg/mcp/transport_stdio.go +++ b/pkg/mcp/transport_stdio.go @@ -6,7 +6,7 @@ import ( "context" "os" - "dappco.re/go/core/log" + "dappco.re/go/log" "github.com/modelcontextprotocol/go-sdk/mcp" ) diff --git a/pkg/mcp/transport_unix.go b/pkg/mcp/transport_unix.go index 1889183..f1d2493 100644 --- a/pkg/mcp/transport_unix.go +++ b/pkg/mcp/transport_unix.go @@ -6,8 +6,8 @@ import ( "context" "net" - "dappco.re/go/core/io" - "dappco.re/go/core/log" + "dappco.re/go/io" + "dappco.re/go/log" ) // ServeUnix starts a Unix domain socket server for the MCP service. diff --git a/tests/cli/mcp/Taskfile.yaml b/tests/cli/mcp/Taskfile.yaml new file mode 100644 index 0000000..3d61778 --- /dev/null +++ b/tests/cli/mcp/Taskfile.yaml @@ -0,0 +1,26 @@ +version: "3" + +tasks: + default: + deps: + - build + - vet + - test + + build: + desc: Compile every package + binary in mcp. + dir: ../../.. + cmds: + - GOWORK=off go build ./... + + vet: + desc: Run go vet across the module. + dir: ../../.. + cmds: + - GOWORK=off go vet ./... + + test: + desc: Run unit tests. + dir: ../../.. + cmds: + - GOWORK=off go test -count=1 ./...