Files
codebase-memory-mcp/internal/tools/projects.go
T
Martin Vogel bd6bbc2ab1 Add Go LSP hybrid type resolver, config store, and supporting infrastructure
- Go LSP cross-file type resolution engine (internal/cbm/lsp/) with C-based
  type registry, scope tracking, and Go stdlib data (30K+ definitions)
- lsp_bridge.go: CrossFileDef struct and RunGoLSPCrossFile CGo bridge
- config.go: persistent ConfigStore (SQLite) for auto_index, mem_limit settings
- CLI config subcommand for get/set/list/delete operations
- Store: edge batch upsert, project config columns, goleak test harness
- Pipeline: prefetch parallelism, resolver improvements, memleak tests
- CI: dry-run workflow improvements, vendor-grammar.sh enhancements
2026-03-12 13:26:13 +01:00

202 lines
5.5 KiB
Go

package tools
import (
"context"
"fmt"
"math"
"path/filepath"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func (s *Server) handleListProjects(_ context.Context, _ *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
projectInfos, err := s.router.ListProjects()
if err != nil {
return errResult(fmt.Sprintf("list projects: %v", err)), nil
}
type projectEntry struct {
Name string `json:"name"`
RootPath string `json:"root_path"`
IndexedAt string `json:"indexed_at"`
Nodes int `json:"nodes"`
Edges int `json:"edges"`
DBPath string `json:"db_path"`
ADRPresent bool `json:"adr_present"`
IsSessionProject bool `json:"is_session_project,omitempty"`
}
result := make([]projectEntry, 0, len(projectInfos))
for _, info := range projectInfos {
st, err := s.router.ForProject(info.Name)
if err != nil {
continue
}
nc, _ := st.CountNodes(info.Name)
ec, _ := st.CountEdges(info.Name)
indexedAt := ""
rootPath := info.RootPath
proj, _ := st.GetProject(info.Name)
if proj != nil {
indexedAt = proj.IndexedAt
rootPath = proj.RootPath
}
adr, _ := st.GetADR(info.Name)
entry := projectEntry{
Name: info.Name,
RootPath: rootPath,
IndexedAt: indexedAt,
Nodes: nc,
Edges: ec,
DBPath: info.DBPath,
ADRPresent: adr != nil,
}
if info.Name == s.sessionProject {
entry.IsSessionProject = true
}
result = append(result, entry)
}
return jsonResult(result), nil
}
func (s *Server) handleDeleteProject(_ context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args, err := parseArgs(req)
if err != nil {
return errResult(err.Error()), nil
}
name := getStringArg(args, "project_name")
if name == "" {
return errResult("project_name is required"), nil
}
// Verify project exists
if !s.router.HasProject(name) {
return errResult(fmt.Sprintf("project not found: %s", name)), nil
}
if err := s.router.DeleteProject(name); err != nil {
return errResult(fmt.Sprintf("delete failed: %v", err)), nil
}
return jsonResult(map[string]any{
"deleted": name,
"status": "ok",
}), nil
}
func (s *Server) handleIndexStatus(_ context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args, err := parseArgs(req)
if err != nil {
return errResult(err.Error()), nil
}
projectName := getStringArg(args, "project")
if projectName == "" {
projectName = s.sessionProject
}
if projectName == "" {
return jsonResult(map[string]any{
"status": "no_session",
"message": "No session project detected. Pass 'project' parameter or ensure the MCP client provides roots.",
}), nil
}
// Check if DB file exists
if !s.router.HasProject(projectName) {
return jsonResult(map[string]any{
"project": projectName,
"status": "not_indexed",
"message": fmt.Sprintf("No index found for project %q. Call index_repository to create one.", projectName),
"db_path": filepath.Join(s.router.Dir(), projectName+".db"),
}), nil
}
// Get store and project metadata
st, err := s.router.ForProject(projectName)
if err != nil {
return errResult(fmt.Sprintf("open store: %v", err)), nil
}
proj, _ := st.GetProject(projectName)
if proj == nil {
// DB file exists but no project row — partially indexed or corrupted
return jsonResult(map[string]any{
"project": projectName,
"status": "partial",
"message": "Database file exists but project metadata is missing. Re-run index_repository.",
"db_path": filepath.Join(s.router.Dir(), projectName+".db"),
}), nil
}
nodeCount, _ := st.CountNodes(projectName)
edgeCount, _ := st.CountEdges(projectName)
// Determine current indexing state
currentStatus, _ := s.indexStatus.Load().(string)
isSessionProject := projectName == s.sessionProject
result := map[string]any{
"project": projectName,
"status": "ready",
"indexed_at": proj.IndexedAt,
"root_path": proj.RootPath,
"nodes": nodeCount,
"edges": edgeCount,
"is_session_project": isSessionProject,
"db_path": filepath.Join(s.router.Dir(), projectName+".db"),
}
// Determine index type (initial vs incremental)
if nodeCount == 0 && edgeCount == 0 {
result["index_type"] = "none"
} else {
result["index_type"] = "incremental"
}
// Add edge type breakdown
if edgeTypes, err := st.EdgeCountsByType(projectName); err == nil && len(edgeTypes) > 0 {
result["edges_by_type"] = edgeTypes
}
// Add LSP coverage metrics for CALLS edges
if stats, err := st.CallsResolutionStats(projectName); err == nil && len(stats) > 0 {
totalCalls := 0
lspCalls := 0
for strategy, count := range stats {
totalCalls += count
if len(strategy) >= 4 && strategy[:4] == "lsp_" {
lspCalls += count
}
}
lspPct := 0.0
if totalCalls > 0 {
lspPct = float64(lspCalls) / float64(totalCalls) * 100
}
result["calls_total"] = totalCalls
result["calls_lsp"] = lspCalls
result["calls_lsp_pct"] = math.Round(lspPct*10) / 10
result["calls_by_strategy"] = stats
}
// If this is the session project and indexing is in progress
if isSessionProject && currentStatus == "indexing" {
result["status"] = "indexing"
if startedAt, ok := s.indexStartedAt.Load().(time.Time); ok {
result["index_started_at"] = startedAt.UTC().Format(time.RFC3339)
result["index_elapsed_seconds"] = int(time.Since(startedAt).Seconds())
}
if nodeCount == 0 {
result["index_type"] = "initial"
}
}
return jsonResult(result), nil
}