Implementation Plan For MCP Server

by ADMIN 35 views

Overview

This document outlines the implementation plan for adding Model Context Protocol (MCP) server functionality to protolint. The implementation will focus on local file system usage, utilizing stdio transport for communication between MCP clients and the protolint server.

1. Basic Design

Add MCP Reporter

  • Create a new reporter in linter/report/reporters/mcpReporter.go
  • Extend the existing reporter mechanism to include MCP-compatible formatting

Add --mcp Flag

  • Add a --mcp option to the protolint command
  • Enable MCP server mode when this flag is present

Implement MCP Server

  • Create an internal/mcp directory for server-related code
  • Implement request/response protocol handling
  • Implement communication via stdin/stdout

2. Detailed Implementation

2.1 MCP Reporter Implementation

Add a new reporter in mcpReporter.go that formats failures into MCP-compatible JSON:

package reporters

import (
    "encoding/json"
    "fmt"
    "io"

    "github.com/yoheimuta/protolint/linter/report"
)

// MCPReporter prints failures in MCP friendly JSON format.
type MCPReporter struct{}

// Report writes failures to w in MCP friendly format.
func (r MCPReporter) Report(w io.Writer, fs []report.Failure) error {
    // Group failures by file
    fileFailures := make(map[string][]map[string]interface{})
    
    for _, failure := range fs {
        filePath := failure.Pos().Filename
        
        failureInfo := map[string]interface{}{
            "rule_id":  failure.RuleID(),
            "message":  failure.Message(),
            "line":     failure.Pos().Line,
            "column":   failure.Pos().Column,
            "severity": failure.Severity(),
        }
        
        fileFailures[filePath] = append(fileFailures[filePath], failureInfo)
    }
    
    // Convert to array of results
    var fileResults []map[string]interface{}
    for filePath, failures := range fileFailures {
        fileResults = append(fileResults, map[string]interface{}{
            "file_path": filePath,
            "failures":  failures,
        })
    }
    
    result := map[string]interface{}{
        "results": fileResults,
    }
    
    bs, err := json.MarshalIndent(result, "", "  ")
    if err != nil {
        return err
    }
    
    _, err = fmt.Fprintln(w, string(bs))
    return err
}

Next, add the mcp reporter to the GetReporter function in internal/cmd/subcmds/lint/reporterFlag.go:

// GetReporter returns a reporter from the specified key.
func GetReporter(value string) (report.Reporter, error) {
    rs := map[string]report.Reporter{
        // Existing reporters
        ...
        "mcp": reporters.MCPReporter{},  // Add MCP reporter
        ...
    }
    // ...
}

2.2 MCP Server Implementation Files

protocol.go

package mcp

import (
    "encoding/json"
)

// Request represents an MCP request
type Request struct {
    Type    string          `json:"type"`
    ID      string          `json:"id"`
    Payload json.RawMessage `json:"payload"`
}

// Response represents an MCP response
type Response struct {
    Type    string      `json:"type"`
    ID      string      `json:"id"`
    Payload interface{} `json:"payload"`
}

// ListToolsResponse represents the response for list_tools request
type ListToolsResponse struct {
    Tools []ToolInfo `json:"tools"`
}

// ToolInfo represents information about a tool
type ToolInfo struct {
    Name        string      `json:"name"`
    Description string      `json:"description"`
    Schema      interface{} `json:"schema,omitempty"`
}

// CallToolPayload represents the payload for call_tool request
type CallToolPayload struct {
    Name      string          `json:"name"`
    Arguments json.RawMessage `json:"arguments"`
}

// CallToolResponse represents the response for call_tool request
type CallToolResponse struct {
    Result interface{} `json:"result"`
}

server.go

package mcp

import (
    "encoding/json"
    "fmt"
    "io"
    "os"
    
    "github.com/yoheimuta/protolint/internal/osutil"
)

// Server represents an MCP server
type Server struct {
    tools []Tool
}

// NewServer creates a new MCP server
func NewServer() *Server {
    return &Server{
        tools: []Tool{
            NewLintFilesTool(),
            // Other tools can be added in the future
        },
    }
}

// Run starts the MCP server
func (s *Server) Run(stdout, stderr io.Writer) osutil.ExitCode {
    fmt.Fprintf(stderr, "protolint MCP server is running. cwd: %s\n", getCurrentDir())
    
    decoder := json.NewDecoder(os.Stdin)
    encoder := json.NewEncoder(stdout)
    
    for {
        var request Request
        if err := decoder.Decode(&request); err != nil {
            if err == io.EOF {
                // Normal termination
                return osutil.ExitSuccess
            }
            fmt.Fprintf(stderr, "Error decoding request: %v\n", err)
            return osutil.ExitInternalFailure
        }
        
        response := s.handleRequest(&request)
        if err := encoder.Encode(response); err != nil {
            fmt.Fprintf(stderr, "Error encoding response: %v\n", err)
            return osutil.ExitInternalFailure
        }
    }
}

// handleRequest handles a single MCP request
func (s *Server) handleRequest(req *Request) *Response {
    switch req.Type {
    case "list_tools":
        return s.handleListTools(req)
    case "call_tool":
        return s.handleCallTool(req)
    default:
        return &Response{
            Type: "error",
            ID:   req.ID,
            Payload: map[string]string{
                "message": fmt.Sprintf("Unknown request type: %s", req.Type),
            },
        }
    }
}

// handleListTools handles list_tools request
func (s *Server) handleListTools(req *Request) *Response {
    toolInfos := make([]ToolInfo, 0, len(s.tools))
    for _, tool := range s.tools {
        toolInfos = append(toolInfos, tool.GetInfo())
    }
    
    return &Response{
        Type: "list_tools_response",
        ID:   req.ID,
        Payload: &ListToolsResponse{
            Tools: toolInfos,
        },
    }
}

// handleCallTool handles call_tool request
func (s *Server) handleCallTool(req *Request) *Response {
    var payload CallToolPayload
    if err := json.Unmarshal(req.Payload, &payload); err != nil {
        return &Response{
            Type: "error",
            ID:   req.ID,
            Payload: map[string]string{
                "message": fmt.Sprintf("Invalid payload: %v", err),
            },
        }
    }
    
    // Find the tool
    var tool Tool
    for _, t := range s.tools {
        if t.GetInfo().Name == payload.Name {
            tool = t
            break
        }
    }
    
    if tool == nil {
        return &Response{
            Type: "error",
            ID:   req.ID,
            Payload: map[string]string{
                "message": fmt.Sprintf("Tool not found: %s", payload.Name),
            },
        }
    }
    
    // Execute the tool
    result, err := tool.Execute(payload.Arguments)
    if err != nil {
        return &Response{
            Type: "error",
            ID:   req.ID,
            Payload: map[string]string{
                "message": fmt.Sprintf("Tool execution failed: %v", err),
            },
        }
    }
    
    return &Response{
        Type: "call_tool_response",
        ID:   req.ID,
        Payload: &CallToolResponse{
            Result: result,
        },
    }
}

// getCurrentDir returns the current working directory
func getCurrentDir() string {
    dir, err := os.Getwd()
    if err != nil {
        return "<unknown>"
    }
    return dir
}

tools.go

package mcp

import (
    "bytes"
    "encoding/json"
    "fmt"
    
    "github.com/yoheimuta/protolint/lib"
)

// Tool defines the interface for MCP tools
type Tool interface {
    GetInfo() ToolInfo
    Execute(args json.RawMessage) (interface{}, error)
}

// LintFilesTool is a tool for linting Proto files
type LintFilesTool struct{}

// NewLintFilesTool creates a new LintFilesTool
func NewLintFilesTool() *LintFilesTool {
    return &LintFilesTool{}
}

// GetInfo returns the tool information
func (t *LintFilesTool) GetInfo() ToolInfo {
    return ToolInfo{
        Name:        "lint-files",
        Description: "Lint Protocol Buffer files using protolint",
        Schema: map[string]interface{}{
            "type": "object",
            "properties": map[string]interface{}{
                "files": map[string]interface{}{
                    "type": "array",
                    "items": map[string]interface{}{
                        "type": "string",
                    },
                    "description": "List of file paths to lint",
                },
                "config_path": map[string]interface{}{
                    "type": "string",
                    "description": "Path to protolint config file",
                },
                "fix": map[string]interface{}{
                    "type": "<br/>
**Q&A: Implementation Plan for MCP Server**
=============================================

**Q: What is the purpose of the MCP server?**
------------------------------------------

A: The MCP server is designed to provide a communication interface between MCP clients and the protolint server. It will handle requests and responses in MCP format, allowing for seamless integration with MCP clients.

**Q: What are the key components of the MCP server implementation?**
----------------------------------------------------------------

A: The key components of the MCP server implementation include:

1.  **MCP Reporter**: A new reporter that formats failures into MCP-compatible JSON.
2.  **--mcp Flag**: A new flag that enables MCP server mode when present.
3.  **MCP Server**: A server that handles request/response processing and communication via stdin/stdout.

**Q: How does the MCP reporter work?**
--------------------------------------

A: The MCP reporter is responsible for formatting failures into MCP-compatible JSON. It groups failures by file and converts them into an array of results, which is then encoded into JSON.

**Q: What is the purpose of the --mcp flag?**
------------------------------------------

A: The --mcp flag enables MCP server mode when present. When this flag is detected, the protolint command will start the MCP server, allowing for communication with MCP clients.

**Q: How does the MCP server handle requests and responses?**
---------------------------------------------------------

A: The MCP server uses a request/response protocol to handle incoming requests and send responses back to the client. It uses JSON encoding to serialize and deserialize requests and responses.

**Q: What are the benefits of using the MCP server?**
------------------------------------------------

A: The MCP server provides several benefits, including:

1.  **Reuse of Existing Code**: The MCP server leverages existing reporter and lint functionality, reducing the amount of code that needs to be written.
2.  **Clear Responsibility Separation**: The MCP server has a clear and well-defined responsibility, making it easier to maintain and extend.
3.  **Future Extensibility**: The MCP server is designed to be extensible, allowing for the addition of new tools and features in the future.

**Q: How does the MCP server integrate with MCP clients?**
------------------------------------------------------

A: The MCP server integrates with MCP clients through the use of the MCP protocol. MCP clients can send requests to the MCP server, which will then process the requests and send responses back to the client.

**Q: What are the advantages of using the MCP server with Claude Desktop?**
-------------------------------------------------------------------

A: The MCP server provides several advantages when used with Claude Desktop, including:

1.  **Seamless Integration**: The MCP server provides a seamless integration with Claude Desktop, allowing for easy communication and data exchange.
2.  **Improved Productivity**: The MCP server enables improved productivity by providing a streamlined and efficient way to work with MCP clients.
3.  **Enhanced Collaboration**: The MCP server enables enhanced collaboration by providing a common interface for communication and data exchange between MCP clients and the protolint server.