Replace internal task tracking (TODO.md, FINDINGS.md) with four structured documentation files covering architecture, tool reference, development guide, and project history. Trim CLAUDE.md to agent instructions only — all detailed content now lives in docs/. - docs/architecture.md: subsystem plugin model, transports, IDE bridge, AI facade, full package layout - docs/tools.md: all 49 MCP tools with parameters and descriptions - docs/development.md: prerequisites, test patterns, adding tools/subsystems - docs/history.md: split history, 5 phases with commit hashes, known issues Co-Authored-By: Virgil <virgil@lethean.io>
27 KiB
go-ai Architecture
Module: forge.lthn.ai/core/go-ai
Language: Go 1.25
Licence: EUPL-1.2
LOC: ~5.6 K total (~3.5 K non-test), 84 tests passing
1. Overview
go-ai is the MCP (Model Context Protocol) hub for the Lethean AI stack. It exposes 49 tools across file operations, RAG vector search, ML inference and scoring, process management, WebSocket streaming, browser automation via CDP, metrics recording, and IDE integration with the Laravel core-agentic backend.
Position in the Lethean Stack
AI Clients (Claude, Cursor, any MCP-capable IDE)
| MCP JSON-RPC (stdio / TCP / Unix)
v
[ go-ai MCP Server ] ← this module
| | |
| | └─ ide/ subsystem ─→ Laravel core-agentic (WebSocket)
| └─ go-rag ──────────────────→ Qdrant + Ollama
└─ go-ml ────────────────────────────→ inference backends (go-mlx, go-rocm, …)
Core CLI (forge.lthn.ai/core/go) bootstraps and wires everything
go-ai is a pure library module. It contains no main package. The Core CLI (core mcp serve) imports forge.lthn.ai/core/go-ai/mcp, constructs a mcp.Service, and calls Run().
2. MCP Server
2.1 The Service Struct
mcp.Service is the central container. It wraps the upstream MCP Go SDK server and owns all optional services:
type Service struct {
server *mcp.Server // upstream go-sdk server instance
workspaceRoot string // sandboxed root for file operations
medium io.Medium // filesystem abstraction (sandboxed or global)
subsystems []Subsystem // plugin subsystems registered via WithSubsystem
logger *log.Logger // audit logger for tool execution
processService *process.Service // optional: process lifecycle management
wsHub *ws.Hub // optional: WebSocket hub for streaming
wsServer *http.Server // optional: HTTP server hosting ws hub
wsAddr string // address the ws server is bound to
}
The io.Medium abstraction (from forge.lthn.ai/core/go/pkg/io) isolates file access. When a workspace root is configured, every read, write, list, and stat call is validated against that root. Paths that escape the workspace root are rejected by the sandboxed medium before they reach the operating system.
2.2 Construction: Functional Options
New() uses the functional options pattern. All options are applied before tools are registered, so subsystems and service dependencies are available when registerTools runs.
svc, err := mcp.New(
mcp.WithWorkspaceRoot("/path/to/project"),
mcp.WithProcessService(ps),
mcp.WithWSHub(hub),
mcp.WithSubsystem(ide.New(hub, ide.WithToken(token))),
mcp.WithSubsystem(mcp.NewMLSubsystem(mlSvc)),
)
Construction sequence inside New():
- Allocate
Servicewith an emptymcp.Server(implementation namecore-cli, version0.1.0). - Default the workspace root to
os.Getwd()and create a sandboxed medium for it. - Apply each
Optionin order — later options override earlier ones. - Call
s.registerTools(s.server)to install the 10 built-in file, directory, and language tools. - Call
registerRAGTools,registerMetricsTools, and conditionallyregisterWSToolsandregisterProcessTools. - Iterate
s.subsystemsand callsub.RegisterTools(s.server)for each plugin.
Available options:
| Option | Effect |
|---|---|
WithWorkspaceRoot(root string) |
Restrict file ops to root; empty string removes the restriction |
WithProcessService(ps) |
Enable process management tools |
WithWSHub(hub) |
Enable WebSocket streaming tools |
WithSubsystem(sub) |
Append a Subsystem plugin |
2.3 Workspace Sandboxing
func WithWorkspaceRoot(root string) Option {
return func(s *Service) error {
if root == "" {
s.medium = io.Local // unrestricted global filesystem
return nil
}
abs, _ := filepath.Abs(root)
m, err := io.NewSandboxed(abs)
// ...
s.medium = m
return nil
}
}
An empty root is accepted but not recommended; it switches the medium to io.Local, which has no path restrictions. All production deployments should provide an explicit workspace root.
3. Subsystem Plugin Model
3.1 Interfaces
// Subsystem registers additional MCP tools at startup.
// Implementations must be safe to call concurrently.
type Subsystem interface {
Name() string
RegisterTools(server *mcp.Server)
}
// SubsystemWithShutdown extends Subsystem with graceful cleanup.
type SubsystemWithShutdown interface {
Subsystem
Shutdown(ctx context.Context) error
}
RegisterTools is called once during New(), after built-in tools are registered. Subsystems receive the raw *mcp.Server and may register any number of tools.
Shutdown is optional. The Service.Shutdown(ctx) method iterates all subsystems, type-asserts each to SubsystemWithShutdown, and calls Shutdown if the assertion succeeds. This allows stateless subsystems (those without background goroutines or open connections) to omit the interface entirely.
3.2 Registration
func WithSubsystem(sub Subsystem) Option {
return func(s *Service) error {
s.subsystems = append(s.subsystems, sub)
return nil
}
}
Subsystems are appended in declaration order. Their tools appear in the MCP tool list after the built-in tools.
3.3 Built-in and Plugin Subsystems
| Subsystem | Type | Source |
|---|---|---|
| File, directory, language tools | Built-in (methods on Service) |
mcp/mcp.go |
| RAG tools | Built-in | mcp/tools_rag.go |
| Metrics tools | Built-in | mcp/tools_metrics.go |
| Process tools | Built-in (conditional on WithProcessService) |
mcp/tools_process.go |
| WebSocket tools | Built-in (conditional on WithWSHub) |
mcp/tools_ws.go |
| Webview tools | Built-in | mcp/tools_webview.go |
| ML subsystem | Plugin (MLSubsystem) |
mcp/tools_ml.go |
| IDE subsystem | Plugin (ide.Subsystem) |
mcp/ide/ |
4. Tool Registration Pattern
4.1 Typed Handlers
Every tool follows an identical pattern: a descriptor struct with Name and Description, and a handler function with a fixed signature.
mcp.AddTool(server, &mcp.Tool{
Name: "file_read",
Description: "Read the contents of a file",
}, s.readFile)
The handler signature is:
func(ctx context.Context, req *mcp.CallToolRequest, input InputStruct) (*mcp.CallToolResult, OutputStruct, error)
The MCP Go SDK deserialises the JSON-RPC params object into InputStruct before calling the handler, and serialises OutputStruct into the JSON-RPC response. Returning a non-nil error produces a JSON-RPC error response; returning a nil error with zero-value outputs is valid for no-op operations.
4.2 Input/Output Structs
Every tool has a dedicated pair of structs. Fields use json: tags for wire names and omitempty on optional fields.
type ReadFileInput struct {
Path string `json:"path"`
}
type ReadFileOutput struct {
Content string `json:"content"`
Language string `json:"language"`
Path string `json:"path"`
}
This produces self-documenting schemas that the MCP SDK can expose to clients via the tools/list capability.
4.3 Security Logging
Sensitive tools log at the Security level (a custom level in forge.lthn.ai/core/go/pkg/log) rather than Info, producing a distinct audit trail. The current username is captured from the OS via log.Username() and attached to every log entry.
// Elevated security log for write and ingest operations
s.logger.Security("MCP tool execution",
"tool", "rag_ingest",
"path", input.Path,
"collection", collection,
"user", log.Username(),
)
// Standard info log for read-only operations
s.logger.Info("MCP tool execution",
"tool", "rag_query",
"question", input.Question,
"user", log.Username(),
)
Mutating file operations (file_write, file_delete, file_rename, file_edit, rag_ingest, ws_start) use the Security level. Read-only operations use Info.
5. Transports
The server supports three transports. Run() auto-selects between stdio and TCP based on the MCP_ADDR environment variable. Unix domain socket mode must be started explicitly.
5.1 Stdio (default)
func (s *Service) ServeStdio(ctx context.Context) error {
s.logger.Info("MCP Stdio server starting", "user", log.Username())
return s.server.Run(ctx, &mcp.StdioTransport{})
}
Run() delegates to ServeStdio when MCP_ADDR is unset:
func (s *Service) Run(ctx context.Context) error {
addr := os.Getenv("MCP_ADDR")
if addr != "" {
return s.ServeTCP(ctx, addr)
}
return s.server.Run(ctx, &mcp.StdioTransport{})
}
Stdio is the standard integration mode for AI clients such as Claude and Cursor, which spawn the server as a subprocess and communicate over the process's stdin/stdout.
5.2 TCP
const DefaultTCPAddr = "127.0.0.1:9100"
ServeTCP starts a net.Listener and accepts connections in a loop. Each accepted connection receives its own fresh mcp.Server instance and its own connTransport:
func (s *Service) handleConnection(ctx context.Context, conn net.Conn) {
impl := &mcp.Implementation{Name: "core-cli", Version: "0.1.0"}
server := mcp.NewServer(impl, nil)
s.registerTools(server)
transport := &connTransport{conn: conn}
if err := server.Run(ctx, transport); err != nil {
diagPrintf("Connection error: %v\n", err)
}
}
Creating a new server per connection ensures that per-session state in the MCP SDK does not leak between clients. The shared Service fields (medium, processService, wsHub) are accessed concurrently and must themselves be concurrency-safe, which the underlying packages guarantee.
The connTransport uses a buffered line scanner with a 10 MB maximum message size:
const maxMCPMessageSize = 10 * 1024 * 1024
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, 64*1024), maxMCPMessageSize)
Messages are framed as newline-delimited JSON-RPC. A warning is emitted to stderr when the server binds to 0.0.0.0; local-only access (127.0.0.1) is strongly preferred.
Activate TCP mode:
MCP_ADDR=127.0.0.1:9100 core mcp serve
5.3 Unix Domain Socket
func (s *Service) ServeUnix(ctx context.Context, socketPath string) error {
_ = os.Remove(socketPath) // clean up stale socket file
listener, err := net.Listen("unix", socketPath)
// ...
defer func() {
_ = listener.Close()
_ = os.Remove(socketPath) // clean up on shutdown
}()
s.logger.Security("MCP Unix server listening", "path", socketPath, "user", log.Username())
// accept loop identical to TCP
}
The socket file is removed before binding (to recover from a previous unclean shutdown) and again on shutdown via defer. Like TCP, each connection spawns an independent mcp.Server instance via handleConnection. Logging uses the Security level because Unix socket path access implies filesystem permissions-based access control.
Transport comparison:
| Transport | Method | Activation | Use case |
|---|---|---|---|
| Stdio | ServeStdio() |
No MCP_ADDR set |
AI client subprocess integration |
| TCP | ServeTCP() |
MCP_ADDR=host:port |
Remote clients, multi-client daemons |
| Unix | ServeUnix() |
Explicit call | Local IPC with OS-level access control |
6. IDE Bridge
The mcp/ide package implements the IDE subsystem. Its primary role is to bridge the desktop MCP server to the Laravel core-agentic backend running on ws://localhost:9876/ws (or a configured URL).
6.1 Subsystem Structure
type Subsystem struct {
cfg Config
bridge *Bridge // nil in headless mode
hub *ws.Hub // local WebSocket hub for real-time forwarding
}
When a ws.Hub is provided, the subsystem creates a Bridge that actively connects to Laravel. Without a hub (hub == nil), the subsystem runs in headless mode: tools are still registered and return stub responses, but no real-time forwarding occurs.
6.2 Configuration
type Config struct {
LaravelWSURL string // WebSocket endpoint (default: ws://localhost:9876/ws)
WorkspaceRoot string // local path for workspace context
Token string // Bearer token for Authorization header
ReconnectInterval time.Duration // base backoff (default: 2s)
MaxReconnectInterval time.Duration // cap for exponential backoff (default: 30s)
}
All fields are overridable via functional options (WithLaravelURL, WithToken, WithWorkspaceRoot, WithReconnectInterval).
6.3 WebSocket Bridge
Bridge maintains a persistent WebSocket connection to Laravel and forwards inbound messages to the local ws.Hub.
Connection lifecycle:
StartBridge(ctx)
└─ go connectLoop(ctx)
├─ dial(ctx) ← WebSocket upgrade with Bearer token
│ sets b.connected = true
└─ readLoop(ctx) ← blocks reading frames
└─ dispatch(msg) ← routes to ws.Hub channel
[on read error]
sets b.connected = false, returns to connectLoop
Exponential backoff:
delay := b.cfg.ReconnectInterval // starts at 2s
for {
if err := b.dial(ctx); err != nil {
// wait delay, then double it up to MaxReconnectInterval
delay = min(delay*2, b.cfg.MaxReconnectInterval)
continue
}
delay = b.cfg.ReconnectInterval // reset on successful connection
b.readLoop(ctx)
}
The backoff sequence for default settings is: 2 s, 4 s, 8 s, 16 s, 30 s, 30 s, … The delay resets to 2 s on every successful connection, so a brief network interruption does not permanently slow reconnection.
Authentication:
var header http.Header
if b.cfg.Token != "" {
header = http.Header{}
header.Set("Authorization", "Bearer "+b.cfg.Token)
}
conn, _, err := dialer.DialContext(ctx, b.cfg.LaravelWSURL, header)
When Token is empty, no Authorization header is sent. This is appropriate for development environments where core-agentic is running without authentication.
6.4 Message Dispatch
Inbound frames from Laravel are deserialised into BridgeMessage:
type BridgeMessage struct {
Type string `json:"type"`
Channel string `json:"channel,omitempty"`
SessionID string `json:"sessionId,omitempty"`
Data any `json:"data,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
The dispatch method routes the message to the local ws.Hub:
func (b *Bridge) dispatch(msg BridgeMessage) {
channel := msg.Channel
if channel == "" {
channel = "ide:" + msg.Type // synthetic channel name
}
b.hub.SendToChannel(channel, ws.Message{Type: ws.TypeEvent, Data: msg.Data})
}
This allows browser-based UIs connected to the local WebSocket hub to receive real-time updates from Laravel without polling.
6.5 Outbound Messages
MCP tool handlers call bridge.Send() to push requests to Laravel:
func (b *Bridge) Send(msg BridgeMessage) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.conn == nil {
return fmt.Errorf("bridge: not connected")
}
msg.Timestamp = time.Now()
data, _ := json.Marshal(msg)
return b.conn.WriteMessage(websocket.TextMessage, data)
}
The mutex ensures that Send and the readLoop do not race on b.conn. If the bridge is not currently connected, Send returns an error, which the tool handler propagates to the MCP client as a JSON-RPC error.
6.6 IDE Tool Groups
The subsystem registers 11 tools across three groups:
Chat tools (tools_chat.go):
| Tool | Description |
|---|---|
ide_chat_send |
Send a message to an agent chat session |
ide_chat_history |
Retrieve message history for a session |
ide_session_list |
List active agent sessions |
ide_session_create |
Create a new agent session |
ide_plan_status |
Get current plan status for a session |
Build tools (tools_build.go):
| Tool | Description |
|---|---|
ide_build_status |
Get the status of a specific build |
ide_build_list |
List recent builds, optionally filtered by repository |
ide_build_logs |
Retrieve log output for a build |
Dashboard tools (tools_dashboard.go):
| Tool | Description |
|---|---|
ide_dashboard_overview |
High-level platform overview (repos, services, sessions, builds, bridge status) |
ide_dashboard_activity |
Recent activity feed |
ide_dashboard_metrics |
Aggregate build and agent metrics for a time period |
All IDE tools follow a fire-and-forward pattern: the tool sends a BridgeMessage to Laravel and returns an immediate acknowledgement or stub response. The real data arrives asynchronously via the WebSocket read loop and is forwarded to ws.Hub subscribers. The ide_dashboard_overview tool is the one exception — it reads bridge.Connected() synchronously to populate BridgeOnline.
7. AI Facade (ai/ Package)
The ai package is the canonical entry point for AI functionality within the Lethean Go ecosystem. It composes go-rag and exposes a metrics layer, avoiding circular imports by keeping the package dependency-lean.
7.1 RAG Integration
func QueryRAGForTask(task TaskInfo) string {
query := task.Title + " " + task.Description
// Truncate to 500 runes to keep the embedding focused
runes := []rune(query)
if len(runes) > 500 {
query = string(runes[:500])
}
qdrantClient, err := rag.NewQdrantClient(rag.DefaultQdrantConfig())
if err != nil {
return "" // graceful degradation
}
defer qdrantClient.Close()
ollamaClient, err := rag.NewOllamaClient(rag.DefaultOllamaConfig())
if err != nil {
return "" // graceful degradation
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
results, err := rag.Query(ctx, qdrantClient, ollamaClient, query, rag.QueryConfig{
Collection: "hostuk-docs",
Limit: 3,
Threshold: 0.5,
})
if err != nil {
return "" // graceful degradation
}
return rag.FormatResultsContext(results)
}
Every failure path returns an empty string rather than propagating an error. This is intentional: callers (typically agentic task planners) should continue operating without RAG context when Qdrant or Ollama are unavailable. The absence of context is preferable to a hard failure.
The query is capped at 500 runes before embedding to keep the vector focused on the task's core intent rather than noise from long descriptions.
7.2 JSONL Metrics Storage
Events are recorded to daily JSONL files at:
~/.core/ai/metrics/YYYY-MM-DD.jsonl
Each line is a JSON-encoded Event:
type Event struct {
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
AgentID string `json:"agent_id,omitempty"`
Repo string `json:"repo,omitempty"`
Duration time.Duration `json:"duration,omitempty"`
Data map[string]any `json:"data,omitempty"`
}
Writing:
func Record(event Event) error {
dir, _ := metricsDir()
os.MkdirAll(dir, 0o755)
path := metricsFilePath(dir, event.Timestamp)
f, _ := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
defer f.Close()
data, _ := json.Marshal(event)
f.Write(append(data, '\n'))
}
OpenFile with O_APPEND is atomically safe for single-process concurrent writers on POSIX systems. Multiple goroutines may call Record without external synchronisation.
Reading:
func ReadEvents(since time.Time) ([]Event, error)
ReadEvents iterates calendar days from since to today, opens each daily file, and filters events by timestamp. Missing files are silently skipped (the metric directory may be sparse for gaps in activity).
Malformed JSONL lines are skipped without returning an error, providing forward compatibility when the Event struct gains new fields.
Aggregation:
func Summary(events []Event) map[string]any {
// returns: total, by_type, by_repo, by_agent
// each dimension is a []map[string]any sorted by count descending
}
Summary is a pure function with no I/O. It is used by the metrics_query MCP tool to build its response.
7.3 MCP Metrics Tools
The metrics_record and metrics_query tools expose the JSONL layer directly to MCP clients:
metrics_record → ai.Record(Event{...})
metrics_query → ai.ReadEvents(since) → ai.Summary(events)
The since parameter accepts human-readable shorthand: 30m, 24h, 7d. The parser maps these to time.Duration values before calling ReadEvents.
8. Full Tool Inventory
49 tools across 12 groups:
| Group | Tools | Source file |
|---|---|---|
| File operations | file_read, file_write, file_delete, file_rename, file_exists, file_edit |
mcp/mcp.go |
| Directory operations | dir_list, dir_create |
mcp/mcp.go |
| Language detection | lang_detect, lang_list |
mcp/mcp.go |
| RAG | rag_query, rag_ingest, rag_collections |
mcp/tools_rag.go |
| ML inference | ml_generate, ml_score, ml_probe, ml_status, ml_backends |
mcp/tools_ml.go |
| Metrics | metrics_record, metrics_query |
mcp/tools_metrics.go |
| Process management | process_start, process_stop, process_kill, process_list, process_output, process_input |
mcp/tools_process.go |
| WebSocket | ws_start, ws_info |
mcp/tools_ws.go |
| Webview (CDP) | webview_connect, webview_disconnect, webview_navigate, webview_click, webview_type, webview_query, webview_console, webview_eval, webview_screenshot, webview_wait |
mcp/tools_webview.go |
| IDE chat | ide_chat_send, ide_chat_history, ide_session_list, ide_session_create, ide_plan_status |
mcp/ide/tools_chat.go |
| IDE build | ide_build_status, ide_build_list, ide_build_logs |
mcp/ide/tools_build.go |
| IDE dashboard | ide_dashboard_overview, ide_dashboard_activity, ide_dashboard_metrics |
mcp/ide/tools_dashboard.go |
9. Package Layout
go-ai/
├── ai/ # AI facade: RAG queries and JSONL metrics
│ ├── ai.go # Package documentation and composition overview
│ ├── rag.go # QueryRAGForTask() with graceful degradation
│ └── metrics.go # Event, Record(), ReadEvents(), Summary()
│
└── mcp/ # MCP server, built-in tools, and transports
├── mcp.go # Service struct, New(), functional options,
│ # registerTools() for file/dir/lang (10 tools)
├── subsystem.go # Subsystem and SubsystemWithShutdown interfaces,
│ # WithSubsystem() option
├── tools_rag.go # rag_query, rag_ingest, rag_collections
├── tools_ml.go # MLSubsystem: ml_generate, ml_score, ml_probe,
│ # ml_status, ml_backends
├── tools_metrics.go # metrics_record, metrics_query; parseDuration()
├── tools_process.go # process_start/stop/kill/list/output/input
├── tools_ws.go # ws_start, ws_info; ProcessEventCallback
├── tools_webview.go # webview_connect/disconnect/navigate/click/type/
│ # query/console/eval/screenshot/wait
├── transport_stdio.go # ServeStdio() — default subprocess transport
├── transport_tcp.go # ServeTCP(), connTransport, connConnection;
│ # per-connection server instances; security warnings
├── transport_unix.go # ServeUnix() — domain socket with stale-file cleanup
└── ide/ # IDE subsystem (plugin implementing Subsystem)
├── ide.go # Subsystem struct, New(), RegisterTools(),
│ # Shutdown(), StartBridge()
├── config.go # Config, DefaultConfig(), functional option helpers
├── bridge.go # Bridge: WebSocket connection to Laravel,
│ # connectLoop(), exponential backoff, Send(), dispatch()
├── tools_chat.go # ide_chat_send/history, ide_session_list/create,
│ # ide_plan_status; BridgeMessage wire types
├── tools_build.go # ide_build_status/list/logs; BuildInfo wire types
└── tools_dashboard.go # ide_dashboard_overview/activity/metrics;
# DashboardOverview, ActivityEvent wire types
10. Dependencies
Direct
| Module | Role |
|---|---|
forge.lthn.ai/core/go |
Core framework: pkg/io (sandboxed filesystem), pkg/log (security-level logging), pkg/process (process lifecycle), pkg/ws (WebSocket hub), pkg/webview (CDP client) |
forge.lthn.ai/core/go-ml |
ML scoring engine: heuristic scores, LLM judge backend, capability probes, InfluxDB pipeline status |
forge.lthn.ai/core/go-rag |
RAG layer: Qdrant vector DB client, Ollama embeddings, markdown chunking, FormatResultsContext |
forge.lthn.ai/core/go-inference |
Shared TextModel/Backend interfaces; inference.List(), inference.Get(), inference.Default() registry |
github.com/modelcontextprotocol/go-sdk |
MCP Go SDK: Server, StdioTransport, AddTool, JSON-RPC framing |
github.com/gorilla/websocket |
WebSocket client used by the IDE bridge |
github.com/stretchr/testify |
Test assertions |
Indirect (via go-ml and go-rag)
go-mlx, go-rocm, go-duckdb, parquet-go, ollama, qdrant/go-client, and the Arrow ecosystem are transitive dependencies. They are not imported directly by go-ai.
All forge.lthn.ai/core/* dependencies use replace directives in go.mod pointing to local sibling directories during development:
replace forge.lthn.ai/core/go => ../go
replace forge.lthn.ai/core/go-mlx => ../go-mlx
replace forge.lthn.ai/core/go-ml => ../go-ml
replace forge.lthn.ai/core/go-rag => ../go-rag
replace forge.lthn.ai/core/go-inference => ../go-inference
11. Integration Points
| System | Protocol | Direction | Notes |
|---|---|---|---|
Core CLI (forge.lthn.ai/core/go) |
Go import | Inbound — CLI bootstraps this module | No main package in go-ai; always embedded |
| AI clients (Claude, Cursor, etc.) | MCP JSON-RPC | Inbound | Stdio (subprocess), TCP, or Unix socket |
Laravel core-agentic |
WebSocket | Outbound — IDE bridge | ws://localhost:9876/ws by default; Bearer token auth |
| Qdrant | gRPC | Outbound — RAG tools | localhost:6334 default; via go-rag |
| Ollama | HTTP | Outbound — RAG embeddings | localhost:11434 default; via go-rag |
| Chrome / CDP | HTTP (DevTools Protocol) | Outbound — webview tools | Via pkg/webview in core/go |
| InfluxDB | HTTP | Outbound — ml_status tool |
localhost:8086 default; via go-ml |
| Filesystem | OS | Bidirectional — file/dir tools | Sandboxed to workspace root |
~/.core/ai/metrics/ |
JSONL files | Write (record) / Read (query) | Daily rotation, append-only |