Commit 2d6631a
Changed files (8)
cmd
semantic
pkg
test
integration
cmd/semantic/main.go
@@ -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
-}
pkg/semantic/lsp_manager.go
@@ -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
pkg/semantic/project_manager.go
@@ -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
pkg/semantic/server.go
@@ -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
pkg/semantic/symbol_manager.go
@@ -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
pkg/semantic/types.go
@@ -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
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...)
Makefile
@@ -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"