a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
87 lines
2.7 KiB
Go
87 lines
2.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
|
|
"github.com/zzet/gortex/internal/config"
|
|
"github.com/zzet/gortex/internal/query"
|
|
)
|
|
|
|
// handleQueryProject runs a read-only symbol search against another
|
|
// project (or a bare tracked-repo prefix) without switching the active
|
|
// project. It deliberately bypasses the session-workspace clamp — this
|
|
// is the sanctioned cross-project read — and never mutates the active
|
|
// project or persists config.
|
|
func (s *Server) handleQueryProject(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
project, err := req.RequireString("project")
|
|
if err != nil {
|
|
return mcp.NewToolResultError("project is required"), nil
|
|
}
|
|
q, err := req.RequireString("query")
|
|
if err != nil {
|
|
return mcp.NewToolResultError("query is required"), nil
|
|
}
|
|
if s.configManager == nil {
|
|
return mcp.NewToolResultError("multi-project configuration is not available"), nil
|
|
}
|
|
limit := req.GetInt("limit", 20)
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
gc := s.configManager.Global()
|
|
|
|
// Resolve the target as a project name / per-repo project tag, or
|
|
// fall back to treating it as a bare tracked-repo prefix.
|
|
var prefixes []string
|
|
if repos, rErr := gc.ResolveRepos(project); rErr == nil {
|
|
for _, r := range repos {
|
|
prefixes = append(prefixes, config.ResolvePrefix(r))
|
|
}
|
|
} else if prefix := s.resolveRepoPrefix(project); prefix != "" {
|
|
prefixes = []string{prefix}
|
|
}
|
|
if len(prefixes) == 0 {
|
|
available := make([]string, 0, len(gc.Projects))
|
|
for name := range gc.Projects {
|
|
available = append(available, name)
|
|
}
|
|
sort.Strings(available)
|
|
return NewStructuredErrorResult(StructuredError{
|
|
ErrorCode: ErrCodeProjectUnknown,
|
|
Message: fmt.Sprintf("no project or tracked repo named %q", project),
|
|
Data: map[string]any{"project": project, "available_projects": available},
|
|
}), nil
|
|
}
|
|
allowed := make(map[string]bool, len(prefixes))
|
|
for _, p := range prefixes {
|
|
allowed[p] = true
|
|
}
|
|
|
|
// Search the base graph unscoped, then confine to the target's
|
|
// repos. Using s.engine (not engineFor) keeps this off the session
|
|
// overlay and clamp — query_project is an explicit cross-project read.
|
|
nodes := s.engine.SearchSymbolsScoped(q, limit*5, query.QueryOptions{})
|
|
nodes = filterNodes(nodes, allowed)
|
|
if len(nodes) > limit {
|
|
nodes = nodes[:limit]
|
|
}
|
|
nodes = s.withAbsPaths(nodes)
|
|
|
|
results := make([]map[string]any, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
results = append(results, n.Brief())
|
|
}
|
|
sort.Strings(prefixes)
|
|
return s.respondJSONOrTOON(ctx, req, map[string]any{
|
|
"project": project,
|
|
"query": q,
|
|
"repos": prefixes,
|
|
"total": len(results),
|
|
"results": results,
|
|
})
|
|
}
|