Skip to content

Commit 213d329

Browse files
authored
Merge pull request #469 from yoheimuta/mcp/468
Implement Model Context Protocol (MCP) server for protolint
2 parents f371336 + 91b4199 commit 213d329

File tree

18 files changed

+2009
-5
lines changed

18 files changed

+2009
-5
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,24 @@ protolint is the pluggable linting/fixing utility for Protocol Buffer files (pro
2323

2424
## Demo
2525

26-
For example, vim-protolint works like the following.
26+
Once MCP server configured, you can ask any MCP clients like Claude Desktop to lint and fix your Protocol Buffer files like this:
27+
28+
<img src="_doc/claude.gif" alt="demo" width="720"/>
29+
30+
Also, vim-protolint works like the following.
2731

2832
<img src="_doc/demo-v2.gif" alt="demo" width="600"/>
2933

34+
## MCP Server
35+
protolint now includes support for the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), which allows AI models to interact with protolint directly.
36+
37+
### Usage
38+
```sh
39+
protolint --mcp
40+
```
41+
42+
For detailed documentation on how to use and integrate protolint's MCP server functionality, see the [MCP documentation](./mcp/README.md).
43+
3044
## Installation
3145

3246
### Via Homebrew

_doc/claude.gif

5.02 MB
Loading

cmd/pl/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88

99
// DEPRECATED: Use cmd/protolint. See https://github.com/yoheimuta/protolint/issues/20.
1010
func main() {
11+
// Initialize the lint runner
12+
cmd.Initialize()
13+
1114
os.Exit(int(
1215
cmd.Do(
1316
os.Args[1:],

cmd/protolint/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
)
88

99
func main() {
10+
// Initialize the lint runner
11+
cmd.Initialize()
12+
1013
os.Exit(int(
1114
cmd.Do(
1215
os.Args[1:],

internal/cmd/cmd.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/yoheimuta/protolint/internal/cmd/subcmds/lint"
99
"github.com/yoheimuta/protolint/internal/cmd/subcmds/list"
1010
"github.com/yoheimuta/protolint/internal/osutil"
11+
"github.com/yoheimuta/protolint/mcp"
1112
)
1213

1314
const (
@@ -17,6 +18,7 @@ Protocol Buffer Linter Command.
1718
Usage:
1819
protolint <command> [arguments]
1920
protolint --version
21+
protolint --mcp
2022
2123
The commands are:
2224
lint lint protocol buffer files
@@ -26,13 +28,15 @@ The commands are:
2628
The flags are:
2729
--version print protolint version
2830
-v print protolint version (when used as the only argument)
31+
--mcp start as an MCP server
2932
`
3033
)
3134

3235
const (
3336
subCmdLint = "lint"
3437
subCmdList = "list"
3538
subCmdVersion = "version"
39+
mcpFlag = "--mcp"
3640
)
3741

3842
var (
@@ -46,11 +50,14 @@ func Do(
4650
stdout io.Writer,
4751
stderr io.Writer,
4852
) osutil.ExitCode {
49-
// Check for --version flag
53+
// Check for --version and --mcp flags
5054
for _, arg := range args {
5155
if arg == "--version" || (arg == "-v" && len(args) == 1) {
5256
return doVersion(stdout)
5357
}
58+
if arg == mcpFlag {
59+
return doMCP(stdout, stderr)
60+
}
5461
}
5562

5663
switch {
@@ -146,3 +153,11 @@ func doVersion(
146153
_, _ = fmt.Fprintln(stdout, "protolint version "+version+"("+revision+")")
147154
return osutil.ExitSuccess
148155
}
156+
157+
func doMCP(
158+
stdout io.Writer,
159+
stderr io.Writer,
160+
) osutil.ExitCode {
161+
server := mcp.NewServer(stdout, stderr)
162+
return server.Run()
163+
}

internal/cmd/lint_runner.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cmd
2+
3+
import (
4+
"io"
5+
6+
"github.com/yoheimuta/protolint/internal/osutil"
7+
"github.com/yoheimuta/protolint/lib"
8+
)
9+
10+
// CmdLintRunner implements the lib.LintRunner interface for cmd package
11+
type CmdLintRunner struct{}
12+
13+
// NewCmdLintRunner creates a new CmdLintRunner
14+
func NewCmdLintRunner() *CmdLintRunner {
15+
return &CmdLintRunner{}
16+
}
17+
18+
// Run executes the lint command
19+
func (r *CmdLintRunner) Run(args []string, stdout, stderr io.Writer) osutil.ExitCode {
20+
return Do(args, stdout, stderr)
21+
}
22+
23+
// Initialize registers the cmd lint runner with the lib package
24+
func Initialize() {
25+
lib.SetLintRunner(NewCmdLintRunner())
26+
}

internal/cmd/subcmds/lint/reporterFlag.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func GetReporter(value string) (report.Reporter, error) {
9797
"sarif": reporters.SarifReporter{},
9898
"sonar": reporters.SonarReporter{},
9999
"tsc": reporters.TscReporter{},
100+
"mcp": reporters.MCPReporter{},
100101
"ci": reporters.NewCiReporterWithGenericFormat(),
101102
"ci-az": reporters.NewCiReporterForAzureDevOps(),
102103
"ci-gh": reporters.NewCiReporterForGithubActions(),
@@ -106,5 +107,5 @@ func GetReporter(value string) (report.Reporter, error) {
106107
if r, ok := rs[value]; ok {
107108
return r, nil
108109
}
109-
return nil, fmt.Errorf(`available reporters are "plain", "junit", "json", "sarif", and "unix, available reporters for CI/CD are ci, ci-az, ci-gh, ci-glab"`)
110+
return nil, fmt.Errorf(`available reporters are "plain", "junit", "json", "sarif", "unix", "mcp", available reporters for CI/CD are ci, ci-az, ci-gh, ci-glab"`)
110111
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package reporters
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
8+
"github.com/yoheimuta/protolint/linter/report"
9+
)
10+
11+
// MCPReporter prints failures in MCP friendly JSON format.
12+
type MCPReporter struct{}
13+
14+
// Report writes failures to w in MCP friendly format.
15+
func (r MCPReporter) Report(w io.Writer, fs []report.Failure) error {
16+
// Group failures by file
17+
fileFailures := make(map[string][]map[string]interface{})
18+
19+
for _, failure := range fs {
20+
filePath := failure.Pos().Filename
21+
22+
failureInfo := map[string]interface{}{
23+
"rule_id": failure.RuleID(),
24+
"message": failure.Message(),
25+
"line": failure.Pos().Line,
26+
"column": failure.Pos().Column,
27+
"severity": failure.Severity(),
28+
}
29+
30+
fileFailures[filePath] = append(fileFailures[filePath], failureInfo)
31+
}
32+
33+
// Convert to array of results
34+
var fileResults []map[string]interface{} = []map[string]interface{}{}
35+
for filePath, failures := range fileFailures {
36+
fileResults = append(fileResults, map[string]interface{}{
37+
"file_path": filePath,
38+
"failures": failures,
39+
})
40+
}
41+
42+
result := map[string]interface{}{
43+
"results": fileResults,
44+
}
45+
46+
bs, err := json.MarshalIndent(result, "", " ")
47+
if err != nil {
48+
return err
49+
}
50+
51+
_, err = fmt.Fprintln(w, string(bs))
52+
return err
53+
}

0 commit comments

Comments
 (0)