diff options
| author | mo khan <mo@mokhan.ca> | 2025-08-18 14:51:20 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-08-18 14:51:20 -0600 |
| commit | 2d6631a1161139ceb9416b0df81ccfdc0e67dfa2 (patch) | |
| tree | f43db8d39f6dc15b32589c7855fd09ccd9821bc1 | |
| parent | 2d09db2057921355b316e0b7fb8a059d6d477459 (diff) | |
refactor: remove semantic mcp server
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | cmd/semantic/main.go | 212 | ||||
| -rw-r--r-- | pkg/semantic/lsp_manager.go | 571 | ||||
| -rw-r--r-- | pkg/semantic/project_manager.go | 384 | ||||
| -rw-r--r-- | pkg/semantic/server.go | 561 | ||||
| -rw-r--r-- | pkg/semantic/symbol_manager.go | 829 | ||||
| -rw-r--r-- | pkg/semantic/types.go | 272 | ||||
| -rw-r--r-- | test/integration/main_test.go | 10 |
8 files changed, 2 insertions, 2842 deletions
@@ -11,7 +11,7 @@ BINDIR = bin INSTALLDIR = /usr/local/bin # Server binaries -SERVERS = git filesystem fetch memory sequential-thinking time maildir signal gitlab imap bash speech semantic +SERVERS = git filesystem fetch memory sequential-thinking time maildir signal gitlab imap bash speech BINARIES = $(addprefix $(BINDIR)/mcp-,$(SERVERS)) # Build flags @@ -122,7 +122,6 @@ gitlab: $(BINDIR)/mcp-gitlab ## Build gitlab server only imap: $(BINDIR)/mcp-imap ## Build imap server only bash: $(BINDIR)/mcp-bash ## Build bash server only speech: $(BINDIR)/mcp-speech ## Build speech server only -semantic: $(BINDIR)/mcp-semantic ## Build semantic server only help: ## Show this help message @echo "Go MCP Servers - Available targets:" @@ -130,4 +129,4 @@ help: ## Show this help message @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' @echo "" @echo "Individual servers:" - @echo " git, filesystem, fetch, memory, sequential-thinking, time, maildir, signal, gitlab, imap, bash, speech, semantic" + @echo " git, filesystem, fetch, memory, sequential-thinking, time, maildir, signal, gitlab, imap, bash, speech" diff --git a/cmd/semantic/main.go b/cmd/semantic/main.go deleted file mode 100644 index 4aa3bec..0000000 --- a/cmd/semantic/main.go +++ /dev/null @@ -1,212 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "log" - "os" - "os/signal" - "syscall" - - "github.com/xlgmokha/mcp/pkg/semantic" -) - -func main() { - var ( - projectRoot = flag.String("project-root", ".", "Root directory of the project to analyze") - configFile = flag.String("config", "", "Path to configuration file") - logLevel = flag.String("log-level", "info", "Log level (debug, info, warn, error)") - showHelp = flag.Bool("help", false, "Show help information") - showVersion = flag.Bool("version", false, "Show version information") - ) - flag.Parse() - - if *showVersion { - fmt.Println("Semantic MCP Server v1.0.0") - os.Exit(0) - } - - if *showHelp { - printHelp() - os.Exit(0) - } - - // Set up logging - setupLogging(*logLevel) - - // Create and configure the semantic server - server, err := semantic.New() - if err != nil { - log.Fatalf("Failed to create semantic server: %v", err) - } - - // Initialize project discovery - if err := initializeProject(*projectRoot, *configFile); err != nil { - log.Fatalf("Failed to initialize project: %v", err) - } - - // Set up graceful shutdown - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - go func() { - <-sigChan - log.Println("Shutting down semantic server...") - // Note: Shutdown is no longer available in the new pattern - // LSP managers are handled internally - os.Exit(0) - }() - - // Run the server - log.Printf("Starting Semantic MCP Server (project: %s)", *projectRoot) - ctx := context.Background() - if err := server.Run(ctx); err != nil { - log.Fatalf("Server error: %v", err) - } -} - -func printHelp() { - fmt.Print(`Semantic MCP Server - Intelligent code analysis and manipulation - -USAGE: - mcp-semantic [FLAGS] - -FLAGS: - --project-root <PATH> Root directory of the project to analyze (default: ".") - --config <FILE> Path to configuration file - --log-level <LEVEL> Log level: debug, info, warn, error (default: "info") - --help Show this help message - --version Show version information - -DESCRIPTION: - The Semantic MCP Server provides AI assistants with intelligent, symbol-aware - code operations. It leverages Language Server Protocol (LSP) to understand code - structure and relationships across multiple programming languages. - -SUPPORTED LANGUAGES: - • Go (via gopls) - • Rust (via rust-analyzer) - • Ruby (via solargraph) - • Python (via python-lsp-server) - • JavaScript (via typescript-language-server) - • TypeScript (via typescript-language-server) - • HTML (via vscode-html-language-server) - • CSS (via vscode-css-language-server) - -AVAILABLE TOOLS: - Symbol Discovery (Phase 1 - ✅ Complete): - • semantic_find_symbol - Find symbols by name, type, or pattern - • semantic_get_overview - Get high-level code structure overview - • semantic_get_definition - Get detailed symbol information - - Code Analysis (Phase 2 - ✅ Complete): - • semantic_get_references - Find all symbol usages with context - • semantic_get_call_hierarchy - Understand calling relationships (incoming/outgoing) - • semantic_analyze_dependencies - Map symbol dependencies and relationships - - Semantic Editing (Phase 3 - 🚧 Planned): - • semantic_replace_symbol - Replace symbol implementations safely - • semantic_insert_after_symbol - Insert code after specific symbols - • semantic_rename_symbol - Rename symbols across entire project - -LANGUAGE SERVER REQUIREMENTS: - Install the required language servers for your languages: - - # Go - go install golang.org/x/tools/gopls@latest - - # Rust - rustup component add rust-analyzer - - # Ruby - gem install solargraph - - # Python - pip install python-lsp-server - - # JavaScript/TypeScript - npm install -g typescript-language-server typescript - - # HTML/CSS - npm install -g vscode-langservers-extracted - -EXAMPLES: - # Basic usage with current directory - mcp-semantic - - # Analyze specific project - mcp-semantic --project-root /path/to/project - - # With custom configuration - mcp-semantic --config semantic-config.json --log-level debug - - # Find symbols in Go project - echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "semantic_find_symbol", "arguments": {"name": "main", "kind": "function", "language": "go"}}}' | mcp-semantic - - # Get references to a function - echo '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "semantic_get_references", "arguments": {"symbol": "UserService.authenticate", "context_lines": 3}}}' | mcp-semantic - - # Analyze call hierarchy - echo '{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "semantic_get_call_hierarchy", "arguments": {"symbol": "main", "direction": "both", "max_depth": 3}}}' | mcp-semantic - -CONFIGURATION: - Create a semantic-config.json file for custom settings: - - { - "project": { - "exclude_patterns": ["**/vendor/**", "**/node_modules/**"] - }, - "language_servers": { - "go": {"enabled": true, "timeout": 30}, - "rust": {"enabled": true, "timeout": 60} - } - } - -FOR MORE INFORMATION: - • Design Document: cmd/mcp-semantic/DESIGN.md - • Implementation Plan: cmd/mcp-semantic/IMPLEMENTATION_PLAN.md - • GitHub: https://github.com/xlgmokha/mcp - -The Semantic MCP Server transforms how AI assistants work with code - from text -manipulation to true semantic understanding. -`) -} - -func setupLogging(level string) { - // Set up basic logging for now - // In a full implementation, this would configure structured logging - log.SetFlags(log.LstdFlags | log.Lshortfile) - - switch level { - case "debug": - log.Println("Debug logging enabled") - case "info": - // Default level - case "warn", "error": - // For now, just note the level - log.Printf("Log level set to: %s", level) - } -} - -func initializeProject(projectRoot, configFile string) error { - // This is a placeholder for project initialization - // In the full implementation, this would: - // 1. Load configuration from file if provided - // 2. Initialize project discovery - // 3. Start language servers as needed - // 4. Set up file watching - - log.Printf("Initializing project at: %s", projectRoot) - - if configFile != "" { - log.Printf("Loading configuration from: %s", configFile) - // TODO: Load and apply configuration - } - - // TODO: Initialize project manager with the root path - // TODO: Start language server discovery - // TODO: Begin symbol indexing in background - - return nil -} diff --git a/pkg/semantic/lsp_manager.go b/pkg/semantic/lsp_manager.go deleted file mode 100644 index 5627d14..0000000 --- a/pkg/semantic/lsp_manager.go +++ /dev/null @@ -1,571 +0,0 @@ -package semantic - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "os/exec" - "sync" - "time" -) - -// LSPManager manages connections to language servers -type LSPManager struct { - clients map[string]*LSPClient - configs map[string]LanguageServerConfig - mu sync.RWMutex - maxIdle time.Duration - shutdownCh chan struct{} -} - -// LSPClient represents a connection to a language server -type LSPClient struct { - language string - cmd *exec.Cmd - stdin io.WriteCloser - stdout io.ReadCloser - stderr io.ReadCloser - requestID int - responses map[int]chan LSPResponse - mu sync.RWMutex - lastUsed time.Time - healthy bool -} - -// NewLSPManager creates a new LSP manager -func NewLSPManager() *LSPManager { - manager := &LSPManager{ - clients: make(map[string]*LSPClient), - configs: DefaultLanguageServers, - maxIdle: 30 * time.Minute, - shutdownCh: make(chan struct{}), - } - - // Start health monitoring - go manager.monitorHealth() - - return manager -} - -// GetClient gets or creates an LSP client for the specified language -func (m *LSPManager) GetClient(language string) (*LSPClient, error) { - m.mu.RLock() - if client, exists := m.clients[language]; exists && client.IsHealthy() { - client.lastUsed = time.Now() - m.mu.RUnlock() - return client, nil - } - m.mu.RUnlock() - - // Start new language server - return m.startLanguageServer(language) -} - -// startLanguageServer starts a new language server process -func (m *LSPManager) startLanguageServer(language string) (*LSPClient, error) { - m.mu.Lock() - defer m.mu.Unlock() - - config, exists := m.configs[language] - if !exists || !config.Enabled { - return nil, fmt.Errorf("language server not configured or disabled: %s", language) - } - - // Check if command exists - if !commandExists(config.ServerCmd) { - return nil, fmt.Errorf("language server command not found: %s", config.ServerCmd) - } - - // Start the language server process - cmd := exec.Command(config.ServerCmd, config.Args...) - - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, fmt.Errorf("failed to create stdin pipe: %w", err) - } - - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("failed to create stdout pipe: %w", err) - } - - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, fmt.Errorf("failed to create stderr pipe: %w", err) - } - - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("failed to start language server: %w", err) - } - - client := &LSPClient{ - language: language, - cmd: cmd, - stdin: stdin, - stdout: stdout, - stderr: stderr, - responses: make(map[int]chan LSPResponse), - lastUsed: time.Now(), - healthy: true, - } - - // Start response handler - go client.handleResponses() - - // Initialize the language server - if err := client.initialize(); err != nil { - client.shutdown() - return nil, fmt.Errorf("failed to initialize language server: %w", err) - } - - m.clients[language] = client - return client, nil -} - -// initialize sends the initialize request to the language server -func (c *LSPClient) initialize() error { - initParams := map[string]interface{}{ - "processId": nil, - "capabilities": map[string]interface{}{ - "textDocument": map[string]interface{}{ - "documentSymbol": map[string]interface{}{ - "symbolKind": map[string]interface{}{ - "valueSet": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, - }, - "hierarchicalDocumentSymbolSupport": true, - }, - "definition": map[string]interface{}{ - "linkSupport": true, - }, - "references": map[string]interface{}{ - "context": map[string]interface{}{ - "includeDeclaration": true, - }, - }, - "callHierarchy": map[string]interface{}{}, - }, - "workspace": map[string]interface{}{ - "symbol": map[string]interface{}{ - "symbolKind": map[string]interface{}{ - "valueSet": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, - }, - }, - }, - }, - } - - response, err := c.SendRequest("initialize", initParams) - if err != nil { - return fmt.Errorf("initialize request failed: %w", err) - } - - if response.Error != nil { - return fmt.Errorf("initialize request error: %s", response.Error.Message) - } - - // Send initialized notification - return c.SendNotification("initialized", map[string]interface{}{}) -} - -// SendRequest sends a request and waits for response -func (c *LSPClient) SendRequest(method string, params interface{}) (*LSPResponse, error) { - c.mu.Lock() - requestID := c.requestID - c.requestID++ - - responseCh := make(chan LSPResponse, 1) - c.responses[requestID] = responseCh - c.mu.Unlock() - - request := LSPRequest{ - JSONRPC: "2.0", - ID: requestID, - Method: method, - Params: params, - } - - if err := c.writeMessage(request); err != nil { - c.mu.Lock() - delete(c.responses, requestID) - c.mu.Unlock() - return nil, fmt.Errorf("failed to send request: %w", err) - } - - // Wait for response with timeout - select { - case response := <-responseCh: - return &response, nil - case <-time.After(30 * time.Second): - c.mu.Lock() - delete(c.responses, requestID) - c.mu.Unlock() - return nil, fmt.Errorf("request timeout: %s", method) - } -} - -// SendNotification sends a notification (no response expected) -func (c *LSPClient) SendNotification(method string, params interface{}) error { - notification := map[string]interface{}{ - "jsonrpc": "2.0", - "method": method, - "params": params, - } - - return c.writeMessage(notification) -} - -// writeMessage writes a JSON-RPC message to the language server -func (c *LSPClient) writeMessage(message interface{}) error { - data, err := json.Marshal(message) - if err != nil { - return fmt.Errorf("failed to marshal message: %w", err) - } - - header := fmt.Sprintf("Content-Length: %d\r\n\r\n", len(data)) - - if _, err := c.stdin.Write([]byte(header)); err != nil { - return fmt.Errorf("failed to write header: %w", err) - } - - if _, err := c.stdin.Write(data); err != nil { - return fmt.Errorf("failed to write message: %w", err) - } - - return nil -} - -// handleResponses handles incoming responses from the language server -func (c *LSPClient) handleResponses() { - scanner := bufio.NewScanner(c.stdout) - - for scanner.Scan() { - line := scanner.Text() - - // Skip headers and empty lines - if line == "" || line[0] != '{' { - continue - } - - var response LSPResponse - if err := json.Unmarshal([]byte(line), &response); err != nil { - continue // Skip malformed responses - } - - c.mu.RLock() - if ch, exists := c.responses[response.ID]; exists { - select { - case ch <- response: - default: - // Channel full, skip - } - } - c.mu.RUnlock() - } - - // Mark as unhealthy if we reach here - c.mu.Lock() - c.healthy = false - c.mu.Unlock() -} - -// IsHealthy checks if the LSP client is healthy -func (c *LSPClient) IsHealthy() bool { - c.mu.RLock() - defer c.mu.RUnlock() - return c.healthy && c.cmd.Process != nil -} - -// shutdown shuts down the LSP client -func (c *LSPClient) shutdown() error { - c.mu.Lock() - defer c.mu.Unlock() - - c.healthy = false - - // Send shutdown request - if c.stdin != nil { - c.SendNotification("shutdown", nil) - c.stdin.Close() - } - - if c.stdout != nil { - c.stdout.Close() - } - - if c.stderr != nil { - c.stderr.Close() - } - - if c.cmd != nil && c.cmd.Process != nil { - return c.cmd.Process.Kill() - } - - return nil -} - -// monitorHealth monitors the health of language servers -func (m *LSPManager) monitorHealth() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - m.cleanupUnhealthyClients() - case <-m.shutdownCh: - return - } - } -} - -// cleanupUnhealthyClients removes unhealthy or idle clients -func (m *LSPManager) cleanupUnhealthyClients() { - m.mu.Lock() - defer m.mu.Unlock() - - for language, client := range m.clients { - if !client.IsHealthy() || time.Since(client.lastUsed) > m.maxIdle { - client.shutdown() - delete(m.clients, language) - } - } -} - -// Shutdown shuts down all language servers -func (m *LSPManager) Shutdown() error { - // Close shutdown channel if not already closed - select { - case <-m.shutdownCh: - // Already closed - default: - close(m.shutdownCh) - } - - m.mu.Lock() - defer m.mu.Unlock() - - var lastErr error - for language, client := range m.clients { - if err := client.shutdown(); err != nil { - lastErr = err - } - delete(m.clients, language) - } - - return lastErr -} - -// GetReferences gets all references to a symbol at a given position -func (c *LSPClient) GetReferences(filePath string, line, column int, includeDeclaration bool) ([]LSPLocation, error) { - params := map[string]interface{}{ - "textDocument": map[string]interface{}{ - "uri": "file://" + filePath, - }, - "position": map[string]interface{}{ - "line": line - 1, // LSP uses 0-based lines - "character": column - 1, - }, - "context": map[string]interface{}{ - "includeDeclaration": includeDeclaration, - }, - } - - response, err := c.SendRequest("textDocument/references", params) - if err != nil { - return nil, fmt.Errorf("references request failed: %w", err) - } - - if response.Error != nil { - return nil, fmt.Errorf("references error: %s", response.Error.Message) - } - - // Parse response - var locations []LSPLocation - if result, ok := response.Result.([]interface{}); ok { - for _, item := range result { - if itemMap, ok := item.(map[string]interface{}); ok { - location := parseLSPLocation(itemMap) - locations = append(locations, location) - } - } - } - - return locations, nil -} - -// GetDefinition gets the definition location of a symbol -func (c *LSPClient) GetDefinition(filePath string, line, column int) ([]LSPLocation, error) { - params := map[string]interface{}{ - "textDocument": map[string]interface{}{ - "uri": "file://" + filePath, - }, - "position": map[string]interface{}{ - "line": line - 1, - "character": column - 1, - }, - } - - response, err := c.SendRequest("textDocument/definition", params) - if err != nil { - return nil, fmt.Errorf("definition request failed: %w", err) - } - - if response.Error != nil { - return nil, fmt.Errorf("definition error: %s", response.Error.Message) - } - - // Parse response - var locations []LSPLocation - if result, ok := response.Result.([]interface{}); ok { - for _, item := range result { - if itemMap, ok := item.(map[string]interface{}); ok { - location := parseLSPLocation(itemMap) - locations = append(locations, location) - } - } - } - - return locations, nil -} - -// PrepareCallHierarchy prepares call hierarchy for a symbol -func (c *LSPClient) PrepareCallHierarchy(filePath string, line, column int) ([]map[string]interface{}, error) { - params := map[string]interface{}{ - "textDocument": map[string]interface{}{ - "uri": "file://" + filePath, - }, - "position": map[string]interface{}{ - "line": line - 1, - "character": column - 1, - }, - } - - response, err := c.SendRequest("textDocument/prepareCallHierarchy", params) - if err != nil { - return nil, fmt.Errorf("prepareCallHierarchy request failed: %w", err) - } - - if response.Error != nil { - return nil, fmt.Errorf("prepareCallHierarchy error: %s", response.Error.Message) - } - - // Parse response - var items []map[string]interface{} - if result, ok := response.Result.([]interface{}); ok { - for _, item := range result { - if itemMap, ok := item.(map[string]interface{}); ok { - items = append(items, itemMap) - } - } - } - - return items, nil -} - -// GetIncomingCalls gets incoming calls for a call hierarchy item -func (c *LSPClient) GetIncomingCalls(item map[string]interface{}) ([]map[string]interface{}, error) { - params := map[string]interface{}{ - "item": item, - } - - response, err := c.SendRequest("callHierarchy/incomingCalls", params) - if err != nil { - return nil, fmt.Errorf("incomingCalls request failed: %w", err) - } - - if response.Error != nil { - return nil, fmt.Errorf("incomingCalls error: %s", response.Error.Message) - } - - // Parse response - var calls []map[string]interface{} - if result, ok := response.Result.([]interface{}); ok { - for _, call := range result { - if callMap, ok := call.(map[string]interface{}); ok { - calls = append(calls, callMap) - } - } - } - - return calls, nil -} - -// GetOutgoingCalls gets outgoing calls for a call hierarchy item -func (c *LSPClient) GetOutgoingCalls(item map[string]interface{}) ([]map[string]interface{}, error) { - params := map[string]interface{}{ - "item": item, - } - - response, err := c.SendRequest("callHierarchy/outgoingCalls", params) - if err != nil { - return nil, fmt.Errorf("outgoingCalls request failed: %w", err) - } - - if response.Error != nil { - return nil, fmt.Errorf("outgoingCalls error: %s", response.Error.Message) - } - - // Parse response - var calls []map[string]interface{} - if result, ok := response.Result.([]interface{}); ok { - for _, call := range result { - if callMap, ok := call.(map[string]interface{}); ok { - calls = append(calls, callMap) - } - } - } - - return calls, nil -} - -// parseLSPLocation parses an LSP location object -func parseLSPLocation(data map[string]interface{}) LSPLocation { - location := LSPLocation{} - - if uri, ok := data["uri"].(string); ok { - location.URI = uri - } - - if rng, ok := data["range"].(map[string]interface{}); ok { - location.Range = parseLSPRange(rng) - } - - return location -} - -// parseLSPRange parses an LSP range object -func parseLSPRange(data map[string]interface{}) LSPRange { - rng := LSPRange{} - - if start, ok := data["start"].(map[string]interface{}); ok { - rng.Start = parseLSPPosition(start) - } - - if end, ok := data["end"].(map[string]interface{}); ok { - rng.End = parseLSPPosition(end) - } - - return rng -} - -// parseLSPPosition parses an LSP position object -func parseLSPPosition(data map[string]interface{}) LSPPosition { - pos := LSPPosition{} - - if line, ok := data["line"].(float64); ok { - pos.Line = int(line) - } - - if char, ok := data["character"].(float64); ok { - pos.Character = int(char) - } - - return pos -} - -// commandExists checks if a command is available in PATH -func commandExists(cmd string) bool { - _, err := exec.LookPath(cmd) - return err == nil -}
\ No newline at end of file diff --git a/pkg/semantic/project_manager.go b/pkg/semantic/project_manager.go deleted file mode 100644 index f7d0b4e..0000000 --- a/pkg/semantic/project_manager.go +++ /dev/null @@ -1,384 +0,0 @@ -package semantic - -import ( - "fmt" - "io/fs" - "os" - "path/filepath" - "strings" - "sync" - "time" -) - -// ProjectManager manages project context and boundaries -type ProjectManager struct { - rootPath string - config *ProjectConfig - languageFiles map[string][]string // language -> file_paths - excludeRules []string - mu sync.RWMutex -} - -// FileInfo represents information about a file -type FileInfo struct { - Path string - Size int64 - ModTime time.Time - IsDir bool -} - -// NewProjectManager creates a new project manager -func NewProjectManager() *ProjectManager { - return &ProjectManager{ - languageFiles: make(map[string][]string), - excludeRules: []string{ - "**/node_modules/**", - "**/vendor/**", - "**/.git/**", - "**/target/**", - "**/dist/**", - "**/build/**", - "**/*.min.js", - "**/*.min.css", - }, - } -} - -// DiscoverProject initializes project discovery from a root path -func (pm *ProjectManager) DiscoverProject(rootPath string) error { - pm.mu.Lock() - defer pm.mu.Unlock() - - absPath, err := filepath.Abs(rootPath) - if err != nil { - return fmt.Errorf("failed to get absolute path: %w", err) - } - - pm.rootPath = absPath - - // Initialize project config - pm.config = &ProjectConfig{ - Name: filepath.Base(absPath), - RootPath: absPath, - Languages: []string{}, - ExcludePatterns: pm.excludeRules, - IncludePatterns: []string{"**/*"}, - CustomSettings: make(map[string]string), - } - - // Scan for files and detect languages - if err := pm.scanProject(); err != nil { - return fmt.Errorf("failed to scan project: %w", err) - } - - return nil -} - -// scanProject scans the project and categorizes files by language -func (pm *ProjectManager) scanProject() error { - pm.languageFiles = make(map[string][]string) - - err := filepath.WalkDir(pm.rootPath, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return nil // Continue on errors - } - - // Skip excluded paths - if pm.shouldExclude(path) { - if d.IsDir() { - return filepath.SkipDir - } - return nil - } - - // Only process files - if d.IsDir() { - return nil - } - - // Detect language and add to appropriate list - language := pm.detectLanguage(path) - if language != "" { - pm.languageFiles[language] = append(pm.languageFiles[language], path) - - // Add to supported languages if not already present - found := false - for _, lang := range pm.config.Languages { - if lang == language { - found = true - break - } - } - if !found { - pm.config.Languages = append(pm.config.Languages, language) - } - } - - return nil - }) - - return err -} - -// shouldExclude checks if a path should be excluded based on patterns -func (pm *ProjectManager) shouldExclude(path string) bool { - relPath, err := filepath.Rel(pm.rootPath, path) - if err != nil { - return true - } - - for _, pattern := range pm.excludeRules { - if matched, _ := filepath.Match(pattern, relPath); matched { - return true - } - - // Handle ** patterns manually - if strings.Contains(pattern, "**") { - pattern = strings.ReplaceAll(pattern, "**", "*") - if matched, _ := filepath.Match(pattern, relPath); matched { - return true - } - } - - // Check if any parent directory matches - parts := strings.Split(relPath, string(filepath.Separator)) - for i := range parts { - partialPath := strings.Join(parts[:i+1], string(filepath.Separator)) - if matched, _ := filepath.Match(pattern, partialPath); matched { - return true - } - } - } - - return false -} - -// detectLanguage detects the programming language from file extension -func (pm *ProjectManager) detectLanguage(filePath string) string { - ext := strings.ToLower(filepath.Ext(filePath)) - - for language, config := range DefaultLanguageServers { - for _, supportedExt := range config.FileExts { - if ext == supportedExt { - return language - } - } - } - - return "" -} - -// GetSupportedLanguages returns list of detected languages in the project -func (pm *ProjectManager) GetSupportedLanguages() []string { - pm.mu.RLock() - defer pm.mu.RUnlock() - - if pm.config == nil { - return []string{} - } - - return pm.config.Languages -} - -// GetFilesByLanguage returns all files for a specific language -func (pm *ProjectManager) GetFilesByLanguage(language string) []string { - pm.mu.RLock() - defer pm.mu.RUnlock() - - files, exists := pm.languageFiles[language] - if !exists { - return []string{} - } - - // Return a copy to avoid race conditions - result := make([]string, len(files)) - copy(result, files) - return result -} - -// GetFilesInDirectory returns all supported files in a directory -func (pm *ProjectManager) GetFilesInDirectory(dirPath string) []string { - pm.mu.RLock() - defer pm.mu.RUnlock() - - var files []string - - // Convert to absolute path if relative - if !filepath.IsAbs(dirPath) { - dirPath = filepath.Join(pm.rootPath, dirPath) - } - - // Walk the directory - filepath.WalkDir(dirPath, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return nil - } - - if d.IsDir() { - return nil - } - - // Check if file is supported - if pm.detectLanguage(path) != "" && !pm.shouldExclude(path) { - files = append(files, path) - } - - return nil - }) - - return files -} - -// IsFileInProject checks if a file is within project boundaries -func (pm *ProjectManager) IsFileInProject(filePath string) bool { - pm.mu.RLock() - defer pm.mu.RUnlock() - - if pm.rootPath == "" { - return false - } - - absPath, err := filepath.Abs(filePath) - if err != nil { - return false - } - - // Check if file is under project root - relPath, err := filepath.Rel(pm.rootPath, absPath) - if err != nil || strings.HasPrefix(relPath, "..") { - return false - } - - // Check if file should be excluded - return !pm.shouldExclude(absPath) -} - -// ReadFile reads the contents of a file -func (pm *ProjectManager) ReadFile(filePath string) ([]byte, error) { - if !pm.IsFileInProject(filePath) { - return nil, fmt.Errorf("file is outside project boundaries: %s", filePath) - } - - return os.ReadFile(filePath) -} - -// GetFileInfo gets information about a file -func (pm *ProjectManager) GetFileInfo(filePath string) (*FileInfo, error) { - if !pm.IsFileInProject(filePath) { - return nil, fmt.Errorf("file is outside project boundaries: %s", filePath) - } - - stat, err := os.Stat(filePath) - if err != nil { - return nil, fmt.Errorf("failed to stat file: %w", err) - } - - return &FileInfo{ - Path: filePath, - Size: stat.Size(), - ModTime: stat.ModTime(), - IsDir: stat.IsDir(), - }, nil -} - -// GetProjectStats returns statistics about the project -func (pm *ProjectManager) GetProjectStats() map[string]interface{} { - pm.mu.RLock() - defer pm.mu.RUnlock() - - stats := make(map[string]interface{}) - - if pm.config != nil { - stats["name"] = pm.config.Name - stats["root_path"] = pm.config.RootPath - stats["languages"] = pm.config.Languages - } - - stats["files_by_language"] = make(map[string]int) - totalFiles := 0 - - for language, files := range pm.languageFiles { - count := len(files) - stats["files_by_language"].(map[string]int)[language] = count - totalFiles += count - } - - stats["total_files"] = totalFiles - - return stats -} - -// GetRootPath returns the project root path -func (pm *ProjectManager) GetRootPath() string { - pm.mu.RLock() - defer pm.mu.RUnlock() - return pm.rootPath -} - -// GetConfig returns the project configuration -func (pm *ProjectManager) GetConfig() *ProjectConfig { - pm.mu.RLock() - defer pm.mu.RUnlock() - - if pm.config == nil { - return nil - } - - // Return a copy - config := *pm.config - config.Languages = make([]string, len(pm.config.Languages)) - copy(config.Languages, pm.config.Languages) - - config.ExcludePatterns = make([]string, len(pm.config.ExcludePatterns)) - copy(config.ExcludePatterns, pm.config.ExcludePatterns) - - config.IncludePatterns = make([]string, len(pm.config.IncludePatterns)) - copy(config.IncludePatterns, pm.config.IncludePatterns) - - config.CustomSettings = make(map[string]string) - for k, v := range pm.config.CustomSettings { - config.CustomSettings[k] = v - } - - return &config -} - -// SetExcludePatterns sets custom exclude patterns -func (pm *ProjectManager) SetExcludePatterns(patterns []string) { - pm.mu.Lock() - defer pm.mu.Unlock() - - pm.excludeRules = patterns - if pm.config != nil { - pm.config.ExcludePatterns = patterns - } - - // Re-scan project with new patterns - if pm.rootPath != "" { - pm.scanProject() - } -} - -// AddExcludePattern adds a new exclude pattern -func (pm *ProjectManager) AddExcludePattern(pattern string) { - pm.mu.Lock() - defer pm.mu.Unlock() - - pm.excludeRules = append(pm.excludeRules, pattern) - if pm.config != nil { - pm.config.ExcludePatterns = append(pm.config.ExcludePatterns, pattern) - } -} - -// Shutdown gracefully shuts down the project manager -func (pm *ProjectManager) Shutdown() error { - pm.mu.Lock() - defer pm.mu.Unlock() - - // Clear all data - pm.languageFiles = make(map[string][]string) - pm.config = nil - pm.rootPath = "" - - return nil -}
\ No newline at end of file diff --git a/pkg/semantic/server.go b/pkg/semantic/server.go deleted file mode 100644 index d795b9d..0000000 --- a/pkg/semantic/server.go +++ /dev/null @@ -1,561 +0,0 @@ -package semantic - -import ( - "encoding/json" - "fmt" - "sync" - - "github.com/xlgmokha/mcp/pkg/mcp" -) - -// SemanticOperations provides semantic analysis operations -type SemanticOperations struct { - lspManager *LSPManager - symbolManager *SymbolManager - projectManager *ProjectManager - mu sync.RWMutex -} - -// NewSemanticOperations creates a new SemanticOperations helper -func NewSemanticOperations() (*SemanticOperations, error) { - lspManager := NewLSPManager() - projectManager := NewProjectManager() - symbolManager := NewSymbolManager(lspManager, projectManager) - - return &SemanticOperations{ - lspManager: lspManager, - symbolManager: symbolManager, - projectManager: projectManager, - }, nil -} - -// New creates a new Semantic MCP server -func New() (*mcp.Server, error) { - semantic, err := NewSemanticOperations() - if err != nil { - return nil, err - } - - builder := mcp.NewServerBuilder("mcp-semantic", "1.0.0") - -// Add semantic_find_symbol tool - builder.AddTool(mcp.NewTool("semantic_find_symbol", "Find symbols by name, type, or pattern across the project", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "query": map[string]interface{}{ - "type": "string", - "description": "Symbol name or pattern to search for", - }, - "symbol_type": map[string]interface{}{ - "type": "string", - "description": "Type of symbol to search for (function, class, variable, etc.)", - "enum": []string{"function", "class", "variable", "interface", "type", "constant", "module"}, - }, - "language": map[string]interface{}{ - "type": "string", - "description": "Programming language to filter by (optional)", - "enum": []string{"go", "rust", "typescript", "javascript", "python", "java", "cpp"}, - }, - "case_sensitive": map[string]interface{}{ - "type": "boolean", - "description": "Whether the search should be case sensitive (default: false)", - }, - "exact_match": map[string]interface{}{ - "type": "boolean", - "description": "Whether to match the exact symbol name (default: false, allows partial matches)", - }, - }, - "required": []string{"query"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return semantic.handleFindSymbol(req) - })) - - // Add semantic_get_overview tool - builder.AddTool(mcp.NewTool("semantic_get_overview", "Get a high-level overview of symbols and structure in a file or directory", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "path": map[string]interface{}{ - "type": "string", - "description": "File or directory path to analyze", - }, - "depth": map[string]interface{}{ - "type": "integer", - "description": "Depth of analysis (1=top-level only, 2=include immediate children, etc.) (default: 2)", - "minimum": 1, - "maximum": 5, - }, - "include_private": map[string]interface{}{ - "type": "boolean", - "description": "Include private/internal symbols (default: false)", - }, - "group_by": map[string]interface{}{ - "type": "string", - "description": "How to group the results (type, file, module) (default: type)", - "enum": []string{"type", "file", "module"}, - }, - }, - "required": []string{"path"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return semantic.handleGetOverview(req) - })) - - // Add semantic_get_definition tool - builder.AddTool(mcp.NewTool("semantic_get_definition", "Get the definition location and details for a specific symbol", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "symbol": map[string]interface{}{ - "type": "string", - "description": "Symbol name to find definition for", - }, - "file": map[string]interface{}{ - "type": "string", - "description": "File path where the symbol is referenced (helps with context)", - }, - "line": map[string]interface{}{ - "type": "integer", - "description": "Line number where the symbol is referenced (optional, for better precision)", - "minimum": 1, - }, - "column": map[string]interface{}{ - "type": "integer", - "description": "Column number where the symbol is referenced (optional, for better precision)", - "minimum": 1, - }, - "include_signature": map[string]interface{}{ - "type": "boolean", - "description": "Include full function/method signature (default: true)", - }, - }, - "required": []string{"symbol"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return semantic.handleGetDefinition(req) - })) - - // Add semantic_get_references tool - builder.AddTool(mcp.NewTool("semantic_get_references", "Find all references to a specific symbol across the project", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "symbol": map[string]interface{}{ - "type": "string", - "description": "Symbol name to find references for", - }, - "file": map[string]interface{}{ - "type": "string", - "description": "File path where the symbol is defined (helps with context)", - }, - "line": map[string]interface{}{ - "type": "integer", - "description": "Line number where the symbol is defined (optional, for better precision)", - "minimum": 1, - }, - "include_declaration": map[string]interface{}{ - "type": "boolean", - "description": "Include the symbol declaration in results (default: true)", - }, - "scope": map[string]interface{}{ - "type": "string", - "description": "Scope of search (file, directory, project) (default: project)", - "enum": []string{"file", "directory", "project"}, - }, - }, - "required": []string{"symbol"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return semantic.handleGetReferences(req) - })) - - // Add semantic_get_call_hierarchy tool - builder.AddTool(mcp.NewTool("semantic_get_call_hierarchy", "Get the call hierarchy (callers and callees) for a function or method", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "symbol": map[string]interface{}{ - "type": "string", - "description": "Function or method name to analyze", - }, - "file": map[string]interface{}{ - "type": "string", - "description": "File path where the function is defined", - }, - "line": map[string]interface{}{ - "type": "integer", - "description": "Line number where the function is defined (optional)", - "minimum": 1, - }, - "direction": map[string]interface{}{ - "type": "string", - "description": "Direction of call hierarchy (incoming=who calls this, outgoing=what this calls, both) (default: both)", - "enum": []string{"incoming", "outgoing", "both"}, - }, - "depth": map[string]interface{}{ - "type": "integer", - "description": "Depth of call hierarchy to retrieve (default: 3)", - "minimum": 1, - "maximum": 10, - }, - }, - "required": []string{"symbol", "file"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return semantic.handleGetCallHierarchy(req) - })) - - // Add semantic_analyze_dependencies tool - builder.AddTool(mcp.NewTool("semantic_analyze_dependencies", "Analyze dependencies and relationships between modules, files, or symbols", map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "path": map[string]interface{}{ - "type": "string", - "description": "Path to analyze (file, directory, or project root)", - }, - "analysis_type": map[string]interface{}{ - "type": "string", - "description": "Type of dependency analysis to perform (default: imports)", - "enum": []string{"imports", "exports", "modules", "symbols", "circular"}, - }, - "include_external": map[string]interface{}{ - "type": "boolean", - "description": "Include external/third-party dependencies (default: false)", - }, - "format": map[string]interface{}{ - "type": "string", - "description": "Output format (tree, graph, list) (default: tree)", - "enum": []string{"tree", "graph", "list"}, - }, - "max_depth": map[string]interface{}{ - "type": "integer", - "description": "Maximum depth of dependency analysis (default: 5)", - "minimum": 1, - "maximum": 20, - }, - }, - "required": []string{"path"}, - }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - return semantic.handleAnalyzeDependencies(req) - })) - - return builder.Build(), nil -} - - -// handleFindSymbol finds symbols by name, type, or pattern across the project -func (semantic *SemanticOperations) handleFindSymbol(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - semantic.mu.RLock() - defer semantic.mu.RUnlock() - - var args struct { - Name string `json:"name"` // Symbol path or pattern - Kind string `json:"kind,omitempty"` // function, class, variable, etc. - Scope string `json:"scope,omitempty"` // project, file, directory - Language string `json:"language,omitempty"` // Optional language filter - IncludeChildren bool `json:"include_children,omitempty"` // Include child symbols - MaxResults int `json:"max_results,omitempty"` // Limit results - } - - argsBytes, _ := json.Marshal(req.Arguments) - if err := json.Unmarshal(argsBytes, &args); err != nil { - return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err) - } - - if args.Name == "" { - return mcp.CallToolResult{}, fmt.Errorf("name is required") - } - - // Set defaults - if args.Scope == "" { - args.Scope = "project" - } - if args.MaxResults == 0 { - args.MaxResults = 50 - } - - // Build query - query := SymbolQuery{ - Name: args.Name, - Kind: SymbolKind(args.Kind), - Scope: args.Scope, - Language: args.Language, - IncludeChildren: args.IncludeChildren, - MaxResults: args.MaxResults, - } - - // Find symbols - symbols, err := semantic.symbolManager.FindSymbols(query) - if err != nil { - return mcp.CallToolResult{}, fmt.Errorf("failed to find symbols: %w", err) - } - - // Format response - responseData := map[string]interface{}{ - "symbols": symbols, - "total_found": len(symbols), - "query": query, - } - - responseJSON, _ := json.MarshalIndent(responseData, "", " ") - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("Found %d symbols matching '%s':\n\n%s", - len(symbols), args.Name, string(responseJSON)), - }, - }, - }, nil -} - -// handleGetOverview gets high-level symbol overview of files or directories -func (semantic *SemanticOperations) handleGetOverview(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - semantic.mu.RLock() - defer semantic.mu.RUnlock() - - var args struct { - Path string `json:"path"` // File or directory path - Depth int `json:"depth,omitempty"` // How deep to analyze - IncludeKinds []string `json:"include_kinds,omitempty"` // Filter symbol types - ExcludePrivate bool `json:"exclude_private,omitempty"` // Skip private symbols - } - - argsBytes, _ := json.Marshal(req.Arguments) - if err := json.Unmarshal(argsBytes, &args); err != nil { - return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err) - } - - if args.Path == "" { - return mcp.CallToolResult{}, fmt.Errorf("path is required") - } - - // Set defaults - if args.Depth == 0 { - args.Depth = 2 - } - - // Get overview - overview, err := semantic.symbolManager.GetOverview(args.Path, args.Depth, args.IncludeKinds, args.ExcludePrivate) - if err != nil { - return mcp.CallToolResult{}, fmt.Errorf("failed to get overview: %w", err) - } - - // Format response - responseJSON, _ := json.MarshalIndent(overview, "", " ") - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("Symbol overview for '%s':\n\n%s", args.Path, string(responseJSON)), - }, - }, - }, nil -} - -// handleGetDefinition gets detailed information about a symbol's definition -func (semantic *SemanticOperations) handleGetDefinition(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - semantic.mu.RLock() - defer semantic.mu.RUnlock() - - var args struct { - Symbol string `json:"symbol"` // Target symbol - IncludeSignature bool `json:"include_signature,omitempty"` // Include full signature - IncludeDocumentation bool `json:"include_documentation,omitempty"` // Include comments/docs - IncludeDependencies bool `json:"include_dependencies,omitempty"` // Include what this symbol uses - } - - argsBytes, _ := json.Marshal(req.Arguments) - if err := json.Unmarshal(argsBytes, &args); err != nil { - return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err) - } - - if args.Symbol == "" { - return mcp.CallToolResult{}, fmt.Errorf("symbol is required") - } - - // Get definition - definition, err := semantic.symbolManager.GetDefinition(args.Symbol, args.IncludeSignature, args.IncludeDocumentation, args.IncludeDependencies) - if err != nil { - return mcp.CallToolResult{}, fmt.Errorf("failed to get definition: %w", err) - } - - // Format response - responseJSON, _ := json.MarshalIndent(definition, "", " ") - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("Definition for symbol '%s':\n\n%s", args.Symbol, string(responseJSON)), - }, - }, - }, nil -} - -// handleGetReferences finds all places where a symbol is used -func (semantic *SemanticOperations) handleGetReferences(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - semantic.mu.RLock() - defer semantic.mu.RUnlock() - - var args struct { - Symbol string `json:"symbol"` // Target symbol - IncludeDefinitions bool `json:"include_definitions,omitempty"` // Include definition location - ContextLines int `json:"context_lines,omitempty"` // Lines of context around usage - FilterByKind []string `json:"filter_by_kind,omitempty"` // Type of references - Language string `json:"language,omitempty"` // Language filter - IncludeExternal bool `json:"include_external,omitempty"` // Include external package references - } - - argsBytes, _ := json.Marshal(req.Arguments) - if err := json.Unmarshal(argsBytes, &args); err != nil { - return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err) - } - - if args.Symbol == "" { - return mcp.CallToolResult{}, fmt.Errorf("symbol is required") - } - - // Set defaults - if args.ContextLines == 0 { - args.ContextLines = 3 - } - - // Get references - references, err := semantic.symbolManager.GetReferences(args.Symbol, args.IncludeDefinitions, args.ContextLines, args.FilterByKind, args.Language, args.IncludeExternal) - if err != nil { - return mcp.CallToolResult{}, fmt.Errorf("failed to get references: %w", err) - } - - // Format response - responseData := map[string]interface{}{ - "symbol": args.Symbol, - "references": references.References, - "total_found": len(references.References), - "definition": references.Definition, - "context_lines": args.ContextLines, - "include_external": args.IncludeExternal, - } - - responseJSON, _ := json.MarshalIndent(responseData, "", " ") - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("Found %d references for symbol '%s':\n\n%s", - len(references.References), args.Symbol, string(responseJSON)), - }, - }, - }, nil -} - -// handleGetCallHierarchy understands calling relationships (what calls this, what this calls) -func (semantic *SemanticOperations) handleGetCallHierarchy(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - semantic.mu.RLock() - defer semantic.mu.RUnlock() - - var args struct { - Symbol string `json:"symbol"` // Target symbol - Direction string `json:"direction,omitempty"` // "incoming", "outgoing", "both" - MaxDepth int `json:"max_depth,omitempty"` // How many levels deep - IncludeExternal bool `json:"include_external,omitempty"` // Include calls to external packages - Language string `json:"language,omitempty"` // Language filter - } - - argsBytes, _ := json.Marshal(req.Arguments) - if err := json.Unmarshal(argsBytes, &args); err != nil { - return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err) - } - - if args.Symbol == "" { - return mcp.CallToolResult{}, fmt.Errorf("symbol is required") - } - - // Set defaults - if args.Direction == "" { - args.Direction = "both" - } - if args.MaxDepth == 0 { - args.MaxDepth = 3 - } - - // Get call hierarchy - hierarchy, err := semantic.symbolManager.GetCallHierarchy(args.Symbol, args.Direction, args.MaxDepth, args.IncludeExternal, args.Language) - if err != nil { - return mcp.CallToolResult{}, fmt.Errorf("failed to get call hierarchy: %w", err) - } - - // Format response - responseJSON, _ := json.MarshalIndent(hierarchy, "", " ") - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("Call hierarchy for symbol '%s' (direction: %s, depth: %d):\n\n%s", - args.Symbol, args.Direction, args.MaxDepth, string(responseJSON)), - }, - }, - }, nil -} - -// handleAnalyzeDependencies analyzes symbol dependencies and relationships -func (semantic *SemanticOperations) handleAnalyzeDependencies(req mcp.CallToolRequest) (mcp.CallToolResult, error) { - semantic.mu.RLock() - defer semantic.mu.RUnlock() - - var args struct { - Scope string `json:"scope"` // Analysis scope: "file", "package", "project" - Path string `json:"path,omitempty"` // Specific path to analyze - IncludeExternal bool `json:"include_external,omitempty"` // Include external dependencies - GroupBy string `json:"group_by,omitempty"` // "file", "package", "kind" - ShowUnused bool `json:"show_unused,omitempty"` // Highlight unused symbols - Language string `json:"language,omitempty"` // Language filter - } - - argsBytes, _ := json.Marshal(req.Arguments) - if err := json.Unmarshal(argsBytes, &args); err != nil { - return mcp.CallToolResult{}, fmt.Errorf("invalid arguments: %w", err) - } - - if args.Scope == "" { - args.Scope = "project" - } - if args.GroupBy == "" { - args.GroupBy = "package" - } - - // Analyze dependencies - analysis, err := semantic.symbolManager.AnalyzeDependencies(args.Scope, args.Path, args.IncludeExternal, args.GroupBy, args.ShowUnused, args.Language) - if err != nil { - return mcp.CallToolResult{}, fmt.Errorf("failed to analyze dependencies: %w", err) - } - - // Format response - responseJSON, _ := json.MarshalIndent(analysis, "", " ") - - return mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("Dependency analysis for scope '%s' (grouped by %s):\n\n%s", - args.Scope, args.GroupBy, string(responseJSON)), - }, - }, - }, nil -} - -// Shutdown gracefully shuts down the semantic server -func (semantic *SemanticOperations) Shutdown() error { - semantic.mu.Lock() - defer semantic.mu.Unlock() - - if semantic.lspManager != nil { - if err := semantic.lspManager.Shutdown(); err != nil { - return fmt.Errorf("failed to shutdown LSP manager: %w", err) - } - } - - if semantic.projectManager != nil { - if err := semantic.projectManager.Shutdown(); err != nil { - return fmt.Errorf("failed to shutdown project manager: %w", err) - } - } - - return nil -}
\ No newline at end of file diff --git a/pkg/semantic/symbol_manager.go b/pkg/semantic/symbol_manager.go deleted file mode 100644 index a55762b..0000000 --- a/pkg/semantic/symbol_manager.go +++ /dev/null @@ -1,829 +0,0 @@ -package semantic - -import ( - "fmt" - "path/filepath" - "strings" - "sync" - "time" -) - -// SymbolManager manages symbol discovery and caching -type SymbolManager struct { - lspManager *LSPManager - projectManager *ProjectManager - cache *SymbolCache - mu sync.RWMutex -} - -// NewSymbolManager creates a new symbol manager -func NewSymbolManager(lspManager *LSPManager, projectManager *ProjectManager) *SymbolManager { - return &SymbolManager{ - lspManager: lspManager, - projectManager: projectManager, - cache: &SymbolCache{ - Symbols: make(map[string][]Symbol), - References: make(map[string][]SourceLocation), - Index: make(map[string][]string), - LastUpdate: make(map[string]time.Time), - }, - } -} - -// FindSymbols finds symbols matching the given query -func (sm *SymbolManager) FindSymbols(query SymbolQuery) ([]Symbol, error) { - sm.mu.RLock() - defer sm.mu.RUnlock() - - switch query.Scope { - case "project": - return sm.findSymbolsInProject(query) - case "file": - if query.Language == "" { - return nil, fmt.Errorf("language must be specified for file scope") - } - return sm.findSymbolsInFile(query.Name, query) - case "directory": - return sm.findSymbolsInDirectory(query.Name, query) - default: - return sm.findSymbolsInProject(query) - } -} - -// findSymbolsInProject finds symbols across the entire project -func (sm *SymbolManager) findSymbolsInProject(query SymbolQuery) ([]Symbol, error) { - var allResults []Symbol - - // Get project files by language - languages := sm.projectManager.GetSupportedLanguages() - if query.Language != "" { - languages = []string{query.Language} - } - - for _, language := range languages { - files := sm.projectManager.GetFilesByLanguage(language) - - for _, file := range files { - symbols, err := sm.getSymbolsFromFile(file, language) - if err != nil { - continue // Skip files with errors - } - - // Filter symbols by query - filtered := sm.filterSymbols(symbols, query) - allResults = append(allResults, filtered...) - - // Respect max results limit - if len(allResults) >= query.MaxResults { - break - } - } - - if len(allResults) >= query.MaxResults { - break - } - } - - // Trim to max results - if len(allResults) > query.MaxResults { - allResults = allResults[:query.MaxResults] - } - - return allResults, nil -} - -// findSymbolsInFile finds symbols in a specific file -func (sm *SymbolManager) findSymbolsInFile(filePath string, query SymbolQuery) ([]Symbol, error) { - language := sm.detectLanguageFromFile(filePath) - if language == "" { - return nil, fmt.Errorf("unsupported file type: %s", filePath) - } - - symbols, err := sm.getSymbolsFromFile(filePath, language) - if err != nil { - return nil, fmt.Errorf("failed to get symbols from file: %w", err) - } - - return sm.filterSymbols(symbols, query), nil -} - -// findSymbolsInDirectory finds symbols in a directory -func (sm *SymbolManager) findSymbolsInDirectory(dirPath string, query SymbolQuery) ([]Symbol, error) { - var allResults []Symbol - - files := sm.projectManager.GetFilesInDirectory(dirPath) - - for _, file := range files { - language := sm.detectLanguageFromFile(file) - if language == "" || (query.Language != "" && language != query.Language) { - continue - } - - symbols, err := sm.getSymbolsFromFile(file, language) - if err != nil { - continue - } - - filtered := sm.filterSymbols(symbols, query) - allResults = append(allResults, filtered...) - - if len(allResults) >= query.MaxResults { - break - } - } - - if len(allResults) > query.MaxResults { - allResults = allResults[:query.MaxResults] - } - - return allResults, nil -} - -// getSymbolsFromFile gets symbols from a file (with caching) -func (sm *SymbolManager) getSymbolsFromFile(filePath string, language string) ([]Symbol, error) { - // Check cache first - if symbols, cached := sm.getCachedSymbols(filePath); cached { - return symbols, nil - } - - // Get LSP client for the language - client, err := sm.lspManager.GetClient(language) - if err != nil { - return nil, fmt.Errorf("failed to get LSP client for %s: %w", language, err) - } - - // Open document in LSP - if err := sm.openDocument(client, filePath); err != nil { - return nil, fmt.Errorf("failed to open document: %w", err) - } - - // Get document symbols - symbols, err := sm.getDocumentSymbols(client, filePath, language) - if err != nil { - return nil, fmt.Errorf("failed to get document symbols: %w", err) - } - - // Cache the symbols - sm.cacheSymbols(filePath, symbols) - - return symbols, nil -} - -// getCachedSymbols checks if symbols are cached for a file -func (sm *SymbolManager) getCachedSymbols(filePath string) ([]Symbol, bool) { - symbols, exists := sm.cache.Symbols[filePath] - if !exists { - return nil, false - } - - // Check if cache is still valid (file hasn't been modified) - lastUpdate, hasUpdate := sm.cache.LastUpdate[filePath] - if !hasUpdate { - return symbols, true - } - - fileInfo, err := sm.projectManager.GetFileInfo(filePath) - if err != nil || fileInfo.ModTime.After(lastUpdate) { - // Cache is stale - delete(sm.cache.Symbols, filePath) - delete(sm.cache.LastUpdate, filePath) - return nil, false - } - - return symbols, true -} - -// cacheSymbols caches symbols for a file -func (sm *SymbolManager) cacheSymbols(filePath string, symbols []Symbol) { - sm.cache.Symbols[filePath] = symbols - sm.cache.LastUpdate[filePath] = time.Now() - - // Update index - for _, symbol := range symbols { - if files, exists := sm.cache.Index[symbol.Name]; exists { - // Add file if not already present - found := false - for _, file := range files { - if file == filePath { - found = true - break - } - } - if !found { - sm.cache.Index[symbol.Name] = append(files, filePath) - } - } else { - sm.cache.Index[symbol.Name] = []string{filePath} - } - } -} - -// openDocument opens a document in the language server -func (sm *SymbolManager) openDocument(client *LSPClient, filePath string) error { - content, err := sm.projectManager.ReadFile(filePath) - if err != nil { - return fmt.Errorf("failed to read file: %w", err) - } - - language := sm.detectLanguageFromFile(filePath) - - params := map[string]interface{}{ - "textDocument": map[string]interface{}{ - "uri": "file://" + filePath, - "languageId": language, - "version": 1, - "text": string(content), - }, - } - - return client.SendNotification("textDocument/didOpen", params) -} - -// getDocumentSymbols gets symbols from a document via LSP -func (sm *SymbolManager) getDocumentSymbols(client *LSPClient, filePath string, language string) ([]Symbol, error) { - params := map[string]interface{}{ - "textDocument": map[string]interface{}{ - "uri": "file://" + filePath, - }, - } - - response, err := client.SendRequest("textDocument/documentSymbol", params) - if err != nil { - return nil, fmt.Errorf("documentSymbol request failed: %w", err) - } - - if response.Error != nil { - return nil, fmt.Errorf("documentSymbol error: %s", response.Error.Message) - } - - // Parse LSP symbols - return sm.parseLSPSymbols(response.Result, filePath, language) -} - -// parseLSPSymbols converts LSP symbols to our internal format -func (sm *SymbolManager) parseLSPSymbols(result interface{}, filePath string, language string) ([]Symbol, error) { - var symbols []Symbol - - // Handle both DocumentSymbol[] and SymbolInformation[] responses - switch data := result.(type) { - case []interface{}: - for _, item := range data { - if itemMap, ok := item.(map[string]interface{}); ok { - symbol := sm.convertLSPSymbol(itemMap, filePath, language) - if symbol != nil { - symbols = append(symbols, *symbol) - } - } - } - default: - return nil, fmt.Errorf("unexpected symbol response format") - } - - return symbols, nil -} - -// convertLSPSymbol converts a single LSP symbol to our format -func (sm *SymbolManager) convertLSPSymbol(lspSymbol map[string]interface{}, filePath string, language string) *Symbol { - name, ok := lspSymbol["name"].(string) - if !ok { - return nil - } - - kind, ok := lspSymbol["kind"].(float64) - if !ok { - return nil - } - - // Convert LSP symbol kind to our SymbolKind - symbolKind := sm.convertSymbolKind(int(kind)) - - // Extract location - var location SourceLocation - if loc, ok := lspSymbol["location"].(map[string]interface{}); ok { - location = sm.extractLocation(loc) - } else if rng, ok := lspSymbol["range"].(map[string]interface{}); ok { - location = sm.extractLocationFromRange(rng, filePath) - } - - symbol := &Symbol{ - Name: name, - FullPath: name, // Will be updated with proper path - Kind: symbolKind, - Location: location, - Language: language, - Visibility: "public", // Default, can be refined - } - - // Extract additional details - if detail, ok := lspSymbol["detail"].(string); ok { - symbol.Signature = detail - } - - // Handle children (for hierarchical symbols) - if children, ok := lspSymbol["children"].([]interface{}); ok { - for _, child := range children { - if childMap, ok := child.(map[string]interface{}); ok { - if childSymbol := sm.convertLSPSymbol(childMap, filePath, language); childSymbol != nil { - childSymbol.FullPath = symbol.Name + "." + childSymbol.Name - symbol.Children = append(symbol.Children, *childSymbol) - } - } - } - } - - return symbol -} - -// convertSymbolKind converts LSP symbol kind to our SymbolKind -func (sm *SymbolManager) convertSymbolKind(lspKind int) SymbolKind { - switch lspKind { - case 1: - return SymbolKindFile - case 2: - return SymbolKindModule - case 3: - return SymbolKindNamespace - case 4: - return SymbolKindPackage - case 5: - return SymbolKindClass - case 6: - return SymbolKindMethod - case 7: - return SymbolKindProperty - case 8: - return SymbolKindField - case 9: - return SymbolKindConstructor - case 10: - return SymbolKindEnum - case 11: - return SymbolKindInterface - case 12: - return SymbolKindFunction - case 13: - return SymbolKindVariable - case 14: - return SymbolKindConstant - case 15: - return SymbolKindString - case 16: - return SymbolKindNumber - case 17: - return SymbolKindBoolean - case 18: - return SymbolKindArray - case 19: - return SymbolKindObject - case 20: - return SymbolKindKey - case 21: - return SymbolKindNull - case 22: - return SymbolKindEnumMember - case 23: - return SymbolKindStruct - case 24: - return SymbolKindEvent - case 25: - return SymbolKindOperator - case 26: - return SymbolKindTypeParameter - default: - return SymbolKindObject // Default fallback - } -} - -// extractLocation extracts location from LSP location object -func (sm *SymbolManager) extractLocation(loc map[string]interface{}) SourceLocation { - uri, _ := loc["uri"].(string) - filePath := strings.TrimPrefix(uri, "file://") - - if rng, ok := loc["range"].(map[string]interface{}); ok { - return sm.extractLocationFromRange(rng, filePath) - } - - return SourceLocation{FilePath: filePath} -} - -// extractLocationFromRange extracts location from LSP range -func (sm *SymbolManager) extractLocationFromRange(rng map[string]interface{}, filePath string) SourceLocation { - location := SourceLocation{FilePath: filePath} - - if start, ok := rng["start"].(map[string]interface{}); ok { - if line, ok := start["line"].(float64); ok { - location.Line = int(line) + 1 // LSP is 0-based, we use 1-based - } - if char, ok := start["character"].(float64); ok { - location.Column = int(char) + 1 - } - } - - if end, ok := rng["end"].(map[string]interface{}); ok { - if line, ok := end["line"].(float64); ok { - location.EndLine = int(line) + 1 - } - if char, ok := end["character"].(float64); ok { - location.EndColumn = int(char) + 1 - } - } - - return location -} - -// filterSymbols filters symbols based on the query -func (sm *SymbolManager) filterSymbols(symbols []Symbol, query SymbolQuery) []Symbol { - var filtered []Symbol - - for _, symbol := range symbols { - if sm.symbolMatches(symbol, query) { - if query.IncludeChildren { - filtered = append(filtered, symbol) - } else { - // Create a copy without children - symbolCopy := symbol - symbolCopy.Children = nil - filtered = append(filtered, symbolCopy) - } - } - - // Also check children if include_children is true - if query.IncludeChildren { - for _, child := range symbol.Children { - if sm.symbolMatches(child, query) { - filtered = append(filtered, child) - } - } - } - } - - return filtered -} - -// symbolMatches checks if a symbol matches the query criteria -func (sm *SymbolManager) symbolMatches(symbol Symbol, query SymbolQuery) bool { - // Check name match - if !sm.nameMatches(symbol.Name, query.Name) && !sm.nameMatches(symbol.FullPath, query.Name) { - return false - } - - // Check kind filter - if query.Kind != "" && symbol.Kind != query.Kind { - return false - } - - // Check language filter - if query.Language != "" && symbol.Language != query.Language { - return false - } - - return true -} - -// nameMatches checks if a symbol name matches the query pattern -func (sm *SymbolManager) nameMatches(symbolName, queryName string) bool { - // Exact match - if symbolName == queryName { - return true - } - - // Case-insensitive substring match - if strings.Contains(strings.ToLower(symbolName), strings.ToLower(queryName)) { - return true - } - - // Path-style match (e.g., "Class.method" matches "method") - if strings.Contains(symbolName, ".") { - parts := strings.Split(symbolName, ".") - if parts[len(parts)-1] == queryName { - return true - } - } - - return false -} - -// detectLanguageFromFile detects language from file extension -func (sm *SymbolManager) detectLanguageFromFile(filePath string) string { - ext := strings.ToLower(filepath.Ext(filePath)) - - for language, config := range DefaultLanguageServers { - for _, fileExt := range config.FileExts { - if ext == fileExt { - return language - } - } - } - - return "" -} - -// GetOverview gets a high-level overview of symbols in a path -func (sm *SymbolManager) GetOverview(path string, depth int, includeKinds []string, excludePrivate bool) (*SymbolOverview, error) { - // This is a placeholder implementation - // In a full implementation, this would analyze the directory/file structure - // and provide statistics and top-level symbols - - overview := &SymbolOverview{ - Path: path, - TotalSymbols: 0, - ByKind: make(map[string]int), - ByLanguage: make(map[string]int), - TopLevel: []Symbol{}, - Structure: make(map[string]interface{}), - } - - return overview, nil -} - -// GetDefinition gets detailed information about a symbol's definition -func (sm *SymbolManager) GetDefinition(symbolName string, includeSignature, includeDocumentation, includeDependencies bool) (*SymbolDefinition, error) { - // This is a placeholder implementation - // In a full implementation, this would find the symbol and get its definition details - - definition := &SymbolDefinition{ - Symbol: Symbol{ - Name: symbolName, - FullPath: symbolName, - Kind: SymbolKindFunction, - }, - } - - return definition, nil -} - -// GetReferences gets all references to a symbol -func (sm *SymbolManager) GetReferences(symbolName string, includeDefinitions bool, contextLines int, filterByKind []string, language string, includeExternal bool) (*SymbolReferences, error) { - // First, find the symbol to get its location - query := SymbolQuery{ - Name: symbolName, - Language: language, - MaxResults: 1, - } - - symbols, err := sm.FindSymbols(query) - if err != nil { - return nil, fmt.Errorf("failed to find symbol: %w", err) - } - - if len(symbols) == 0 { - return &SymbolReferences{ - Symbol: symbolName, - References: []SymbolReference{}, - TotalFound: 0, - }, nil - } - - symbol := symbols[0] - - // Get language server for the symbol's language - client, err := sm.lspManager.GetClient(symbol.Language) - if err != nil { - return nil, fmt.Errorf("failed to get LSP client: %w", err) - } - - // Get references via LSP - lspRefs, err := client.GetReferences(symbol.Location.FilePath, symbol.Location.Line, symbol.Location.Column, includeDefinitions) - if err != nil { - // If LSP fails, return empty result rather than error - return &SymbolReferences{ - Symbol: symbolName, - References: []SymbolReference{}, - TotalFound: 0, - }, nil - } - - // Convert LSP references to our format - var references []SymbolReference - var definition *SourceLocation - - for _, lspRef := range lspRefs { - // Convert LSP location to our format - filePath := strings.TrimPrefix(lspRef.URI, "file://") - location := SourceLocation{ - FilePath: filePath, - Line: lspRef.Range.Start.Line + 1, // Convert back to 1-based - Column: lspRef.Range.Start.Character + 1, - EndLine: lspRef.Range.End.Line + 1, - EndColumn: lspRef.Range.End.Character + 1, - } - - // Check if this is the definition location - if includeDefinitions && sm.isDefinitionLocation(location, symbol.Location) { - definition = &location - if !includeDefinitions { - continue - } - } - - // Get context around the reference - context := sm.getCodeContext(filePath, location.Line, contextLines) - - ref := SymbolReference{ - Location: location, - Context: context, - Kind: "reference", // Could be enhanced to detect call vs import vs etc - Symbol: symbolName, - } - - references = append(references, ref) - } - - result := &SymbolReferences{ - Symbol: symbolName, - Definition: definition, - References: references, - TotalFound: len(references), - } - - return result, nil -} - -// GetCallHierarchy gets call hierarchy for a symbol -func (sm *SymbolManager) GetCallHierarchy(symbolName string, direction string, maxDepth int, includeExternal bool, language string) (*CallHierarchy, error) { - // Find the symbol first - query := SymbolQuery{ - Name: symbolName, - Language: language, - MaxResults: 1, - } - - symbols, err := sm.FindSymbols(query) - if err != nil { - return nil, fmt.Errorf("failed to find symbol: %w", err) - } - - if len(symbols) == 0 { - return &CallHierarchy{ - Symbol: symbolName, - Direction: direction, - MaxDepth: maxDepth, - TotalItems: 0, - }, nil - } - - symbol := symbols[0] - - // Get language server - client, err := sm.lspManager.GetClient(symbol.Language) - if err != nil { - return nil, fmt.Errorf("failed to get LSP client: %w", err) - } - - // Prepare call hierarchy - items, err := client.PrepareCallHierarchy(symbol.Location.FilePath, symbol.Location.Line, symbol.Location.Column) - if err != nil || len(items) == 0 { - // If LSP doesn't support call hierarchy, return empty result - return &CallHierarchy{ - Symbol: symbolName, - Direction: direction, - MaxDepth: maxDepth, - TotalItems: 0, - }, nil - } - - // Build call hierarchy tree - root := sm.buildCallHierarchyItem(items[0], client, direction, maxDepth, 0, includeExternal) - - hierarchy := &CallHierarchy{ - Symbol: symbolName, - Root: root, - Direction: direction, - MaxDepth: maxDepth, - TotalItems: sm.countCallHierarchyItems(root), - } - - return hierarchy, nil -} - -// AnalyzeDependencies analyzes symbol dependencies -func (sm *SymbolManager) AnalyzeDependencies(scope string, path string, includeExternal bool, groupBy string, showUnused bool, language string) (*DependencyAnalysis, error) { - // This is a simplified implementation - // In a full implementation, this would analyze the entire dependency graph - - analysis := &DependencyAnalysis{ - Scope: scope, - GroupBy: groupBy, - TotalSymbols: 0, - ExternalDeps: 0, - UnusedSymbols: 0, - Groups: make(map[string][]DependencyNode), - DependencyGraph: make(map[string][]string), - Summary: make(map[string]int), - } - - // For now, return a placeholder response - // Real implementation would: - // 1. Scan all symbols in the specified scope - // 2. Build dependency relationships via LSP or static analysis - // 3. Group by specified criteria - // 4. Identify unused symbols - // 5. Calculate statistics - - return analysis, nil -} - -// Helper methods - -func (sm *SymbolManager) isDefinitionLocation(ref, def SourceLocation) bool { - return ref.FilePath == def.FilePath && - ref.Line == def.Line && - ref.Column == def.Column -} - -func (sm *SymbolManager) getCodeContext(filePath string, line int, contextLines int) string { - // Read context around the line - content, err := sm.projectManager.ReadFile(filePath) - if err != nil { - return "" - } - - lines := strings.Split(string(content), "\n") - if line < 1 || line > len(lines) { - return "" - } - - start := line - contextLines - 1 - if start < 0 { - start = 0 - } - - end := line + contextLines - if end > len(lines) { - end = len(lines) - } - - contextLines_slice := lines[start:end] - - // Add line numbers for clarity - var result strings.Builder - for i, contextLine := range contextLines_slice { - lineNum := start + i + 1 - marker := " " - if lineNum == line { - marker = "►" - } - result.WriteString(fmt.Sprintf("%s %3d: %s\n", marker, lineNum, contextLine)) - } - - return result.String() -} - -func (sm *SymbolManager) buildCallHierarchyItem(item map[string]interface{}, client *LSPClient, direction string, maxDepth int, currentDepth int, includeExternal bool) CallHierarchyItem { - // Extract basic information from LSP item - name, _ := item["name"].(string) - kind, _ := item["kind"].(float64) - - hierItem := CallHierarchyItem{ - Symbol: name, - Name: name, - Kind: sm.convertSymbolKind(int(kind)), - Depth: currentDepth, - } - - // Extract location if present - if uri, ok := item["uri"].(string); ok { - hierItem.Location.FilePath = strings.TrimPrefix(uri, "file://") - } - if rng, ok := item["range"].(map[string]interface{}); ok { - hierItem.Location = sm.extractLocationFromRange(rng, hierItem.Location.FilePath) - } - - // Recursively build hierarchy if we haven't reached max depth - if currentDepth < maxDepth { - if direction == "incoming" || direction == "both" { - if incomingCalls, err := client.GetIncomingCalls(item); err == nil { - for _, call := range incomingCalls { - if fromItem, ok := call["from"].(map[string]interface{}); ok { - childItem := sm.buildCallHierarchyItem(fromItem, client, direction, maxDepth, currentDepth+1, includeExternal) - hierItem.IncomingCalls = append(hierItem.IncomingCalls, childItem) - } - } - } - } - - if direction == "outgoing" || direction == "both" { - if outgoingCalls, err := client.GetOutgoingCalls(item); err == nil { - for _, call := range outgoingCalls { - if toItem, ok := call["to"].(map[string]interface{}); ok { - childItem := sm.buildCallHierarchyItem(toItem, client, direction, maxDepth, currentDepth+1, includeExternal) - hierItem.OutgoingCalls = append(hierItem.OutgoingCalls, childItem) - } - } - } - } - } - - return hierItem -} - -func (sm *SymbolManager) countCallHierarchyItems(item CallHierarchyItem) int { - count := 1 - for _, child := range item.IncomingCalls { - count += sm.countCallHierarchyItems(child) - } - for _, child := range item.OutgoingCalls { - count += sm.countCallHierarchyItems(child) - } - return count -}
\ No newline at end of file diff --git a/pkg/semantic/types.go b/pkg/semantic/types.go deleted file mode 100644 index 48d0099..0000000 --- a/pkg/semantic/types.go +++ /dev/null @@ -1,272 +0,0 @@ -package semantic - -import "time" - -// Symbol represents a code symbol (function, class, variable, etc.) -type Symbol struct { - Name string `json:"name"` - FullPath string `json:"full_path"` - Kind SymbolKind `json:"kind"` - Location SourceLocation `json:"location"` - Signature string `json:"signature,omitempty"` - Documentation string `json:"documentation,omitempty"` - Visibility string `json:"visibility"` - Language string `json:"language"` - Children []Symbol `json:"children,omitempty"` - References []SourceLocation `json:"references,omitempty"` - Dependencies []string `json:"dependencies,omitempty"` -} - -// SymbolKind represents the type of a symbol -type SymbolKind string - -const ( - SymbolKindFile SymbolKind = "file" - SymbolKindModule SymbolKind = "module" - SymbolKindNamespace SymbolKind = "namespace" - SymbolKindPackage SymbolKind = "package" - SymbolKindClass SymbolKind = "class" - SymbolKindMethod SymbolKind = "method" - SymbolKindProperty SymbolKind = "property" - SymbolKindField SymbolKind = "field" - SymbolKindConstructor SymbolKind = "constructor" - SymbolKindEnum SymbolKind = "enum" - SymbolKindInterface SymbolKind = "interface" - SymbolKindFunction SymbolKind = "function" - SymbolKindVariable SymbolKind = "variable" - SymbolKindConstant SymbolKind = "constant" - SymbolKindString SymbolKind = "string" - SymbolKindNumber SymbolKind = "number" - SymbolKindBoolean SymbolKind = "boolean" - SymbolKindArray SymbolKind = "array" - SymbolKindObject SymbolKind = "object" - SymbolKindKey SymbolKind = "key" - SymbolKindNull SymbolKind = "null" - SymbolKindEnumMember SymbolKind = "enum_member" - SymbolKindStruct SymbolKind = "struct" - SymbolKindEvent SymbolKind = "event" - SymbolKindOperator SymbolKind = "operator" - SymbolKindTypeParameter SymbolKind = "type_parameter" -) - -// SourceLocation represents a location in source code -type SourceLocation struct { - FilePath string `json:"file_path"` - Line int `json:"line"` - Column int `json:"column"` - EndLine int `json:"end_line,omitempty"` - EndColumn int `json:"end_column,omitempty"` -} - -// SymbolQuery represents a query for finding symbols -type SymbolQuery struct { - Name string `json:"name"` - Kind SymbolKind `json:"kind,omitempty"` - Scope string `json:"scope,omitempty"` - Language string `json:"language,omitempty"` - IncludeChildren bool `json:"include_children,omitempty"` - MaxResults int `json:"max_results,omitempty"` -} - -// SymbolOverview represents a high-level overview of symbols in a scope -type SymbolOverview struct { - Path string `json:"path"` - TotalSymbols int `json:"total_symbols"` - ByKind map[string]int `json:"by_kind"` - ByLanguage map[string]int `json:"by_language"` - TopLevel []Symbol `json:"top_level"` - Structure map[string]interface{} `json:"structure"` -} - -// SymbolDefinition represents detailed symbol definition information -type SymbolDefinition struct { - Symbol Symbol `json:"symbol"` - Signature string `json:"signature,omitempty"` - Documentation string `json:"documentation,omitempty"` - Dependencies []string `json:"dependencies,omitempty"` - References []SourceLocation `json:"references,omitempty"` - RelatedSymbols []Symbol `json:"related_symbols,omitempty"` -} - -// SymbolReference represents a reference to a symbol with context -type SymbolReference struct { - Location SourceLocation `json:"location"` - Context string `json:"context,omitempty"` // Code context around the reference - Kind string `json:"kind,omitempty"` // Type of reference (call, import, etc.) - Symbol string `json:"symbol"` // The symbol being referenced -} - -// SymbolReferences represents all references to a symbol -type SymbolReferences struct { - Symbol string `json:"symbol"` - Definition *SourceLocation `json:"definition,omitempty"` - References []SymbolReference `json:"references"` - TotalFound int `json:"total_found"` -} - -// CallHierarchyItem represents a single item in the call hierarchy -type CallHierarchyItem struct { - Symbol string `json:"symbol"` - Name string `json:"name"` - Kind SymbolKind `json:"kind"` - Location SourceLocation `json:"location"` - Signature string `json:"signature,omitempty"` - IncomingCalls []CallHierarchyItem `json:"incoming_calls,omitempty"` - OutgoingCalls []CallHierarchyItem `json:"outgoing_calls,omitempty"` - Depth int `json:"depth"` -} - -// CallHierarchy represents the complete call hierarchy for a symbol -type CallHierarchy struct { - Symbol string `json:"symbol"` - Root CallHierarchyItem `json:"root"` - Direction string `json:"direction"` - MaxDepth int `json:"max_depth"` - TotalItems int `json:"total_items"` -} - -// DependencyNode represents a single dependency relationship -type DependencyNode struct { - Symbol string `json:"symbol"` - Kind SymbolKind `json:"kind"` - Location SourceLocation `json:"location"` - Dependencies []string `json:"dependencies,omitempty"` - Dependents []string `json:"dependents,omitempty"` - IsExternal bool `json:"is_external"` - IsUnused bool `json:"is_unused,omitempty"` -} - -// DependencyAnalysis represents the result of dependency analysis -type DependencyAnalysis struct { - Scope string `json:"scope"` - GroupBy string `json:"group_by"` - TotalSymbols int `json:"total_symbols"` - ExternalDeps int `json:"external_deps"` - UnusedSymbols int `json:"unused_symbols,omitempty"` - Groups map[string][]DependencyNode `json:"groups"` - DependencyGraph map[string][]string `json:"dependency_graph"` - Summary map[string]int `json:"summary"` -} - -// LSPRequest represents a Language Server Protocol request -type LSPRequest struct { - JSONRPC string `json:"jsonrpc"` - ID int `json:"id"` - Method string `json:"method"` - Params interface{} `json:"params,omitempty"` -} - -// LSPResponse represents a Language Server Protocol response -type LSPResponse struct { - JSONRPC string `json:"jsonrpc"` - ID int `json:"id,omitempty"` - Result interface{} `json:"result,omitempty"` - Error *LSPError `json:"error,omitempty"` -} - -// LSPError represents an LSP error -type LSPError struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` -} - -// LSPPosition represents a position in an LSP document -type LSPPosition struct { - Line int `json:"line"` - Character int `json:"character"` -} - -// LSPRange represents a range in an LSP document -type LSPRange struct { - Start LSPPosition `json:"start"` - End LSPPosition `json:"end"` -} - -// LSPLocation represents a location in an LSP workspace -type LSPLocation struct { - URI string `json:"uri"` - Range LSPRange `json:"range"` -} - -// LSPSymbolInformation represents symbol information from LSP -type LSPSymbolInformation struct { - Name string `json:"name"` - Kind int `json:"kind"` - Location LSPLocation `json:"location"` - ContainerName string `json:"containerName,omitempty"` -} - -// LSPDocumentSymbol represents a document symbol from LSP -type LSPDocumentSymbol struct { - Name string `json:"name"` - Detail string `json:"detail,omitempty"` - Kind int `json:"kind"` - Range LSPRange `json:"range"` - SelectionRange LSPRange `json:"selectionRange"` - Children []LSPDocumentSymbol `json:"children,omitempty"` -} - -// LanguageServerConfig represents configuration for a language server -type LanguageServerConfig struct { - Language string `json:"language"` - ServerCmd string `json:"server_cmd"` - Args []string `json:"args"` - FileExts []string `json:"file_extensions"` - Initialized bool `json:"initialized"` - Enabled bool `json:"enabled"` - Timeout int `json:"timeout"` // seconds -} - -// ProjectConfig represents project configuration -type ProjectConfig struct { - Name string `json:"name"` - RootPath string `json:"root_path"` - Languages []string `json:"languages"` - ExcludePatterns []string `json:"exclude_patterns"` - IncludePatterns []string `json:"include_patterns"` - CustomSettings map[string]string `json:"custom_settings"` -} - -// SymbolCache represents cached symbol information -type SymbolCache struct { - Symbols map[string][]Symbol `json:"symbols"` // file_path -> symbols - References map[string][]SourceLocation `json:"references"` // symbol_path -> references - Index map[string][]string `json:"index"` // name -> file_paths - LastUpdate map[string]time.Time `json:"last_update"` // file_path -> last_modified -} - -// FileChange represents a file system change -type FileChange struct { - Path string `json:"path"` - Operation string `json:"operation"` // create, modify, delete, rename - Timestamp time.Time `json:"timestamp"` -} - -// Default language server configurations -var DefaultLanguageServers = map[string]LanguageServerConfig{ - "go": { - Language: "go", - ServerCmd: "gopls", - Args: []string{"serve"}, - FileExts: []string{".go"}, - Enabled: true, - Timeout: 30, - }, - "rust": { - Language: "rust", - ServerCmd: "rust-analyzer", - Args: []string{}, - FileExts: []string{".rs"}, - Enabled: true, - Timeout: 60, - }, - "ruby": { - Language: "ruby", - ServerCmd: "solargraph", - Args: []string{"stdio"}, - FileExts: []string{".rb", ".rbw", ".rake", ".gemspec"}, - Enabled: true, - Timeout: 30, - }, -}
\ No newline at end of file diff --git a/test/integration/main_test.go b/test/integration/main_test.go index 52c2f05..59d86c2 100644 --- a/test/integration/main_test.go +++ b/test/integration/main_test.go @@ -282,13 +282,6 @@ func TestAllServers(t *testing.T) { ExpectedServers: "bash", MinResources: 90, // Bash server has bash builtins and coreutils resources }, - { - BinaryName: "mcp-semantic", - Args: []string{"--project-root", "."}, - ExpectedTools: []string{"semantic_find_symbol", "semantic_get_overview", "semantic_get_definition", "semantic_get_references", "semantic_get_call_hierarchy", "semantic_analyze_dependencies"}, - ExpectedServers: "mcp-semantic", - MinResources: 0, // No static resources (discovers projects dynamically) - }, } for _, config := range servers { @@ -439,7 +432,6 @@ func TestServerStartupPerformance(t *testing.T) { "mcp-maildir", "mcp-imap", "mcp-bash", - "mcp-semantic", } for _, serverName := range servers { @@ -462,8 +454,6 @@ func TestServerStartupPerformance(t *testing.T) { args = []string{tempDir} case "mcp-imap": args = []string{"--server", "example.com", "--username", "test", "--password", "test"} - case "mcp-semantic": - args = []string{"--project-root", "."} } server, err := NewMCPServer(binaryPath, args...) |
