mcp-mode-design.md 17 KB

MCP Mode Implementation Design for Valaq

1. Overview

This document provides a comprehensive design for implementing MCP (Model Context Protocol) mode in valaq, enabling VAPI file exploration through the MCP protocol using STDIO communication. The design leverages the existing valaq architecture while adding MCP capabilities through the mcp-vala library.

2. Architecture Overview

2.1 High-Level Architecture

graph TD
    A[valaq --mcp] --> B[ArgumentParser]
    B --> C{MCP Mode?}
    C -->|Yes| D[McpModeCommandHandler]
    C -->|No| E[NormalCommandHandler]
    D --> F[McpServer]
    F --> G[Mc p.Core.Server]
    G --> H[Resource Providers]
    H --> I[VapiResourceProvider]
    I --> J[JSON Output]
    J --> K[STDIO Transport]
    K --> L[MCP Client]

2.2 Component Integration

The MCP mode integrates with existing valaq components:

  1. ArgumentParser: Extended to recognize --mcp flag
  2. McpModeCommandHandler: New component for MCP-specific command handling
  3. McpServer: Wrapper around Mcp.Core.Server with valaq-specific logic
  4. VapiResourceProvider: Custom resource provider for VAPI files
  5. Existing Components: Reuses VapiParser, SymbolNavigator, and JsonFormatter

3. URI Scheme and Resource Mapping

3.1 URI Scheme Design

The MCP mode will use the vapi:// URI scheme with the following patterns:

  1. Root VAPI Listing: vapi://{vapi}

    • Returns JSON formatted list of root symbols (namespaces)
    • Example: vapi://gtk+-3.0
  2. Symbol Navigation: vapi://{vapi}/{symbol-path}

    • Returns JSON formatted symbol children
    • Uses dot-separated symbol paths
    • Example: vapi://gtk+-3.0/Gtk.Window.show

3.2 Resource Mapping Strategy

URI Pattern Resource Type Implementation
vapi://{vapi} VAPI Root Symbols List all root symbols from parsed VAPI
vapi://{vapi}/{symbol} Symbol Children Navigate to symbol and return children
vapi://{vapi}/{symbol-path} Symbol Path Navigate to nested symbol using dot notation

4. VAPI Resource Provider Class Structure

4.1 VapiResourceProvider Class

public class VapiResourceProvider : Mcp.Resources.BaseProvider {
    private VapiParser parser;
    private SymbolNavigator navigator;
    private JsonFormatter formatter;
    private HashTable<string, Gee.ArrayList<Symbol>> vapi_cache;
    
    public VapiResourceProvider () {
        // Initialize base provider
        base ();
        
        // Initialize components
        parser = new VapiParser ();
        navigator = null;
        formatter = new JsonFormatter ();
        vapi_cache = new HashTable<string, Gee.ArrayList<Symbol>> ();
    }
    
    public override async Gee.ArrayList<Mcp.Resources.Types.Resource> list_resources (string? cursor) throws Error {
        var resources = new Gee.ArrayList<Mcp.Resources.Types.Resource> ();
        
        // Get list of available VAPI files
        var vapi_files = get_available_vapi_files ();
        
        foreach (string vapi_file in vapi_files) {
            string vapi_name = get_vapi_basename (vapi_file);
            string uri = @"vapi://$(vapi_name)";
            
            var resource = new Mcp.Resources.Types.Resource ();
            resource.uri = uri;
            resource.name = vapi_name;
            resource.mime_type = "application/json";
            resource.description = @"VAPI file: $(vapi_name)";
            
            resources.add (resource);
        }
        
        return resources;
    }
    
    public override async Gee.ArrayList<Mcp.Types.Common.ResourceContents> read_resource (string uri) throws Error {
        // Parse URI to extract VAPI name and symbol path
        string[] uri_parts = uri.split ("://");
        if (uri_parts.length != 2 || uri_parts[0] != "vapi") {
            throw new Mcp.Core.McpError.INVALID_PARAMS ("Invalid VAPI URI format");
        }
        
        string[] path_parts = uri_parts[1].split ("/");
        string vapi_name = path_parts[0];
        string? symbol_path = path_parts.length > 1 ? string.joinv (".", path_parts[1:]) : null;
        
        // Resolve VAPI file path
        string vapi_path = resolve_vapi_file (vapi_name);
        if (vapi_path == null) {
            throw new Mcp.Core.McpError.RESOURCE_NOT_FOUND ("VAPI file not found: %s".printf (vapi_name));
        }
        
        // Parse VAPI file (use cache if available)
        if (!vapi_cache.contains (vapi_name)) {
            if (!parser.parse_file (vapi_path)) {
                throw new Mcp.Core.McpError.PARSE_ERROR ("Failed to parse VAPI file: %s".printf (vapi_name));
            }
            
            var root_symbols = parser.get_root_symbols ();
            vapi_cache.insert (vapi_name, root_symbols);
            navigator = new SymbolNavigator (root_symbols);
        }
        
        var root_symbols = vapi_cache.lookup (vapi_name);
        
        Gee.ArrayList<Mcp.Types.Common.ResourceContents> contents = new Gee.ArrayList<Mcp.Types.Common.ResourceContents> ();
        
        if (symbol_path == null) {
            // Return root symbols as JSON
            string json_data = formatter.format_symbol_list (root_symbols);
            var content = new Mcp.Types.Common.TextContent (json_data);
            contents.add (content);
        } else {
            // Navigate to symbol and return children
            string[] symbol_parts = symbol_path.split (".");
            Symbol? target_symbol = navigator.navigate_to_path (symbol_parts);
            
            if (target_symbol == null) {
                throw new Mcp.Core.McpError.RESOURCE_NOT_FOUND ("Symbol not found: %s".printf (symbol_path));
            }
            
            var child_symbols = navigator.get_child_symbols (target_symbol);
            string json_data = formatter.format_symbol_list (child_symbols);
            var content = new Mcp.Types.Common.TextContent (json_data);
            contents.add (content);
        }
        
        return contents;
    }
    
    private string[] get_available_vapi_files () {
        // Reuse existing file discovery logic
        var file_utils = new FileUtils ();
        return file_utils.find_all_vapi_files ();
    }
    
    private string get_vapi_basename (string vapi_file) {
        // Extract basename without extension
        if (vapi_file.has_suffix (".vapi")) {
            return vapi_file.substring (0, vapi_file.length - 5);
        }
        return vapi_file;
    }
    
    private string? resolve_vapi_file (string vapi_name) {
        // Reuse existing VAPI resolution logic
        var file_utils = new FileUtils ();
        return file_utils.resolve_vapi_file (vapi_name);
    }
}

4.2 Resource Content Structure

The resource content follows the existing valaq JSON output format:

{
  "result_type": "symbol_list",
  "symbols": [
    {
      "name": "Gtk",
      "type": "namespace",
      "access": "public",
      "full_path": "Gtk",
      "child_count": 15
    }
  ]
}

5. MCP Server Integration

5.1 McpModeCommandHandler Class

public class McpModeCommandHandler : Object {
    private McpServer mcp_server;
    
    public McpModeCommandHandler (McpServer mcp_server) {
        this.mcp_server = mcp_server;
    }
    
    public int handle_mcp_command () {
        try {
            // Start MCP server in STDIO mode
            bool started = yield mcp_server.start ();
            
            if (!started) {
                stderr.printf ("Failed to start MCP server\n");
                return 1;
            }
            
            // Keep main loop running
            var loop = new MainLoop ();
            loop.run ();
            
            return 0;
        } catch (Error e) {
            stderr.printf ("MCP server error: %s\n", e.message);
            return 1;
        }
    }
}

5.2 McpServer Class

public class McpServer : Object {
    private Mcp.Core.Server mcp_core_server;
    private VapiResourceProvider vapi_provider;
    
    public McpServer () {
        // Create server info
        var server_info = new Mcp.Types.Protocol.ServerInfo (
            "valaq",
            "1.0.0",
            "Valaq VAPI File Query Tool - MCP Server"
        );
        
        // Create capabilities
        var capabilities = new Mcp.Types.Protocol.ServerCapabilities ();
        capabilities.resources = new Mcp.Types.Protocol.ResourcesCapabilities ();
        capabilities.resources.subscribe = true;
        capabilities.resources.list_changed = true;
        
        // Create MCP server
        mcp_core_server = new Mcp.Core.Server (server_info, capabilities);
        
        // Create and register VAPI resource provider
        vapi_provider = new VapiResourceProvider ();
        mcp_core_server.resource_manager.register_provider ("vapi", vapi_provider);
        
        // Connect server signals
        mcp_core_server.initialized.connect (on_server_initialized);
        mcp_core_server.shutdown.connect (on_server_shutdown);
    }
    
    public async bool start () throws Error {
        return yield mcp_core_server.start ();
    }
    
    public async void stop () {
        yield mcp_core_server.stop ();
    }
    
    private void on_server_initialized () {
        // Server is ready for MCP communication
        print ("MCP server initialized and ready\n");
    }
    
    private void on_server_shutdown () {
        // Cleanup resources
        vapi_provider.cleanup ();
    }
}

6. Command-Line Argument Extension

6.1 ArgumentParser Modifications

// In ArgumentParser class, add new field
private bool mcp_mode;

// In parse_long_option method, add:
} else if (arg == "--mcp") {
    mcp_mode = true;
} else if (arg == "--help") {
    show_help = true;
}

// Add new methods:
public bool is_mcp_mode () {
    return mcp_mode;
}

public void show_mcp_help_text () {
    print ("Valaq MCP Server Mode\n");
    print ("====================\n\n");
    print ("USAGE:\n");
    print ("    valaq --mcp\n\n");
    print ("DESCRIPTION:\n");
    print ("    Starts valaq as an MCP server using STDIO communication.\n");
    print ("    Provides access to VAPI files through MCP protocol with vapi:// URI scheme.\n\n");
    print ("RESOURCE ACCESS:\n");
    print ("    vapi://{vapi_name}           - List root symbols in VAPI file\n");
    print ("    vapi://{vapi_name}/{symbol}  - Navigate to symbol and list children\n");
    print ("    vapi://{vapi_name}/{path.to.symbol} - Navigate to nested symbol\n\n");
    print ("EXAMPLES:\n");
    print ("    valaq --mcp\n");
    print ("    # Server starts and listens for MCP requests\n\n");
    print ("EXIT CODES:\n");
    print ("    0    Success\n");
    print ("    1    MCP server error\n");
}

6.2 CommandHandler Integration

// In CommandHandler class, modify handle_command method:
public int handle_command (ArgumentParser args) {
    if (args.is_mcp_mode ()) {
        // Create and run MCP server
        var mcp_handler = new McpModeCommandHandler (mcp_server);
        return mcp_handler.handle_mcp_command ();
    }
    
    // ... existing command handling logic
}

7. Build System Changes

7.1 Meson.build Updates

# Add mcp-vala dependency
mcp_dep = dependency('mcp-vala')

# Add MCP mode source files
sources += files(
    'src/mcp/mcp-server.vala',
    'src/mcp/mcp-command-handler.vala',
    'src/mcp/vapi-resource-provider.vala'
)

# Update vala_args for MCP
vala_args += [
    '--pkg', 'mcp-vala'
]

# Update executable dependencies
executable('valaq',
    sources,
    dependencies: [glib_dep, gobject_dep, json_glib_dep, gee_dep, libvala_dep, mcp_dep, jsonrpc_dep],
    vala_args: vala_args,
    install: true,
)

7.2 New Directory Structure

src/
├── mcp/
│   ├── mcp-server.vala           # Main MCP server implementation
│   ├── mcp-command-handler.vala  # MCP mode command handling
│   └── vapi-resource-provider.vala # VAPI resource provider
├── cli/
│   ├── argument-parser.vala       # Extended for --mcp flag
│   └── command-handler.vala        # Modified for MCP mode
└── [existing files...]

8. Error Handling and Logging

8.1 Error Handling Strategy

  1. MCP Protocol Errors: Use Mcp.Core.McpError domain with proper error codes
  2. VAPI Parsing Errors: Wrap existing ValaqError with MCP error responses
  3. Resource Not Found: Return proper MCP error codes for invalid URIs
  4. STDIO Transport Errors: Handle JSON-RPC communication failures

8.2 Logging Strategy

  1. MCP Server Logs: Use print() for server status messages
  2. Error Logging: Use stderr.printf() for error messages
  3. Debug Logging: Optional verbose mode for troubleshooting
  4. MCP Protocol Logging: Leverage mcp-vala's built-in logging capabilities

9. Implementation Roadmap

9.1 Phase 1: Core MCP Infrastructure

  1. Create MCP Server Classes

    • Implement McpServer wrapper class
    • Implement McpModeCommandHandler for MCP mode
    • Set up basic server initialization and lifecycle
  2. Implement VAPI Resource Provider

    • Create VapiResourceProvider class
    • Implement resource listing and reading
    • Integrate with existing VAPI parsing logic
  3. Extend Command-Line Interface

    • Add --mcp flag to ArgumentParser
    • Update CommandHandler for MCP mode detection
    • Add help text for MCP mode

9.2 Phase 2: Integration and Testing

  1. Integrate Components

    • Connect MCP server to resource provider
    • Implement proper error handling
    • Add resource caching for performance
  2. Testing

    • Unit tests for VAPI resource provider
    • Integration tests for MCP protocol
    • Manual testing with MCP clients
  3. Documentation

    • Update README with MCP mode instructions
    • Add examples of MCP client usage
    • Document URI scheme and resource access

9.3 Phase 3: Optimization and Polish

  1. Performance Optimization

    • Implement resource caching
    • Optimize VAPI parsing for large files
    • Add lazy loading for symbol details
  2. Advanced Features

    • Resource change notifications
    • Symbol search capabilities
    • Custom resource templates

10. Testing Strategy

10.1 Unit Testing

// Test VAPI resource provider
class VapiResourceProviderTest : TestCase {
    private VapiResourceProvider provider;
    
    protected override void set_up () {
        provider = new VapiResourceProvider ();
    }
    
    public void test_list_resources () {
        var resources = provider.list_resources (null);
        assert (resources.size > 0);
        
        // Test specific VAPI URI
        var contents = provider.read_resource ("vapi://gtk+-3.0");
        assert (contents != null);
        assert (contents.size > 0);
    }
    
    public void test_read_resource_invalid_uri () {
        try {
            provider.read_resource ("invalid://format");
            assert_not_reached ();
        } catch (Mcp.Core.McpError e) {
            assert (e.code == -32603); // INVALID_PARAMS
        }
    }
}

10.2 Integration Testing

# Test MCP server with sample client
./valaq --mcp &
echo '{"jsonrpc": "2.0", "method": "resources/list", "id": "test1"}' | ./valaq --mcp

# Expected response
{
  "result": {
    "resources": [
      {
        "uri": "vapi://gtk+-3.0",
        "name": "gtk+-3.0",
        "mime_type": "application/json",
        "description": "VAPI file: gtk+-3.0"
      }
    ]
  }
}

10.3 Performance Testing

  1. Large VAPI Files: Test with complex VAPIs like GTK
  2. Concurrent Access: Test multiple simultaneous requests
  3. Memory Usage: Monitor memory consumption
  4. Response Times: Ensure timely responses

11. Security Considerations

11.1 Input Validation

  1. URI Validation: Validate vapi:// URIs strictly
  2. Path Traversal: Prevent directory traversal attacks
  3. VAPI Names: Only allow known VAPI file names
  4. Symbol Paths: Validate symbol path components

11.2 Resource Access Control

  1. VAPI Directories: Restrict to standard VAPI locations
  2. File Permissions: Ensure proper read access
  3. Resource Limits: Implement rate limiting if needed

12. Migration and Compatibility

12.1 Backward Compatibility

  1. Existing CLI: Maintain full compatibility with current CLI
  2. JSON Format: Use existing JSON output structure
  3. VAPI Parsing: Leverage existing libvala integration

12.2 MCP Protocol Compliance

  1. Standard MCP: Follow MCP v2025-11-25 specification
  2. JSON-RPC 2.0: Use mcp-vala's implementation
  3. Error Codes: Use standard MCP error codes
  4. Resource Types: Implement standard resource content types

13. Summary

This design provides a comprehensive approach to implementing MCP mode in valaq that:

  1. Leverages Existing Architecture: Reuses proven VAPI parsing and navigation logic
  2. Clean Integration: Minimal changes to existing components
  3. Standard MCP Compliance: Uses mcp-vala library for protocol implementation
  4. Flexible Resource Access: Provides intuitive VAPI exploration through MCP protocol
  5. Robust Error Handling: Comprehensive error handling and logging
  6. Performance Optimized: Caching and efficient resource access
  7. Thoroughly Tested: Complete testing strategy for reliability

The implementation follows valaq's modular design principles while adding MCP capabilities through well-defined interfaces and standard protocols.