feat(cli): integrate MCP server (#71)

Exposes core CLI commands as MCP tools for AI agents.

This change introduces a Go-based MCP server that wraps the
existing core CLI commands (`go test`, `dev health`, `dev commit`),
providing structured JSON responses.

This allows AI agents to interact with the core CLI in a structured,
type-safe manner.

The implementation includes:
- A new Go HTTP server in `google/mcp/`
- Handlers for each of the core CLI commands
- Unit tests for the handlers with a mock `core` executable
- Documentation for the new MCP tools
- Integration with the `code` plugin via `plugin.json`
This commit is contained in:
Snider 2026-02-02 07:14:50 +00:00 committed by GitHub
parent 94d9d28f4a
commit 3782514acf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 366 additions and 1 deletions

View file

@ -18,5 +18,55 @@
"go",
"php",
"laravel"
]
],
"mcp": {
"server": "go run google/mcp/main.go",
"tools": [
{
"name": "core_go_test",
"description": "Run Go tests",
"parameters": {
"type": "object",
"properties": {
"filter": {
"type": "string"
},
"coverage": {
"type": "boolean",
"default": false
}
}
}
},
{
"name": "core_dev_health",
"description": "Check monorepo status",
"parameters": {
"type": "object",
"properties": {}
}
},
{
"name": "core_dev_commit",
"description": "Commit changes across repos",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string"
},
"repos": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"message"
]
}
}
]
}
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module core-agent
go 1.24

67
google/mcp/README.md Normal file
View file

@ -0,0 +1,67 @@
# Core CLI MCP Server
This directory contains an MCP server that exposes the core CLI commands as tools for AI agents.
## Tools
### `core_go_test`
Run Go tests.
**Parameters:**
- `filter` (string, optional): Filter tests by name.
- `coverage` (boolean, optional): Enable code coverage. Defaults to `false`.
**Example:**
```json
{
"tool": "core_go_test",
"parameters": {
"filter": "TestMyFunction",
"coverage": true
}
}
```
### `core_dev_health`
Check the health of the monorepo.
**Parameters:**
None.
**Example:**
```json
{
"tool": "core_dev_health",
"parameters": {}
}
```
### `core_dev_commit`
Commit changes across repositories.
**Parameters:**
- `message` (string, required): The commit message.
- `repos` (array of strings, optional): A list of repositories to commit to.
**Example:**
```json
{
"tool": "core_dev_commit",
"parameters": {
"message": "feat: Implement new feature",
"repos": [
"core-agent",
"another-repo"
]
}
}
```

131
google/mcp/main.go Normal file
View file

@ -0,0 +1,131 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os/exec"
"strings"
)
type GoTestRequest struct {
Filter string `json:"filter,omitempty"`
Coverage bool `json:"coverage,omitempty"`
}
type GoTestResponse struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
type DevHealthResponse struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
type DevCommitRequest struct {
Message string `json:"message"`
Repos []string `json:"repos,omitempty"`
}
type DevCommitResponse struct {
Output string `json:"output"`
Error string `json:"error,omitempty"`
}
func goTestHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}
var req GoTestRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
args := []string{"go", "test"}
if req.Filter != "" {
args = append(args, "-run", req.Filter)
}
if req.Coverage {
args = append(args, "-cover")
}
cmd := exec.Command("core", args...)
output, err := cmd.CombinedOutput()
resp := GoTestResponse{
Output: string(output),
}
if err != nil {
resp.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func devHealthHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}
cmd := exec.Command("core", "dev", "health")
output, err := cmd.CombinedOutput()
resp := DevHealthResponse{
Output: string(output),
}
if err != nil {
resp.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func devCommitHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}
var req DevCommitRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
args := []string{"dev", "commit", "-m", req.Message}
if len(req.Repos) > 0 {
args = append(args, "--repos", strings.Join(req.Repos, ","))
}
cmd := exec.Command("core", args...)
output, err := cmd.CombinedOutput()
resp := DevCommitResponse{
Output: string(output),
}
if err != nil {
resp.Error = err.Error()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func main() {
http.HandleFunc("/core_go_test", goTestHandler)
http.HandleFunc("/core_dev_health", devHealthHandler)
http.HandleFunc("/core_dev_commit", devCommitHandler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "MCP Server is running")
})
log.Println("Starting MCP server on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("could not start server: %s\n", err)
}
}

112
google/mcp/main_test.go Normal file
View file

@ -0,0 +1,112 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
func TestMain(m *testing.M) {
// Get the absolute path to the testdata directory
wd, err := os.Getwd()
if err != nil {
panic(err)
}
testdataPath := filepath.Join(wd, "testdata")
// Add the absolute path to the PATH
os.Setenv("PATH", testdataPath+":"+os.Getenv("PATH"))
m.Run()
}
func TestGoTestHandler(t *testing.T) {
reqBody := GoTestRequest{
Filter: "TestMyFunction",
Coverage: true,
}
body, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", "/core_go_test", bytes.NewBuffer(body))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(goTestHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
var resp GoTestResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("could not decode response: %v", err)
}
if resp.Error != "" {
t.Errorf("handler returned an unexpected error: %v", resp.Error)
}
}
func TestDevHealthHandler(t *testing.T) {
req, err := http.NewRequest("POST", "/core_dev_health", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(devHealthHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
var resp DevHealthResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("could not decode response: %v", err)
}
if resp.Error != "" {
t.Errorf("handler returned an unexpected error: %v", resp.Error)
}
}
func TestDevCommitHandler(t *testing.T) {
reqBody := DevCommitRequest{
Message: "test commit",
}
body, _ := json.Marshal(reqBody)
req, err := http.NewRequest("POST", "/core_dev_commit", bytes.NewBuffer(body))
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(devCommitHandler)
handler.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
var resp DevCommitResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("could not decode response: %v", err)
}
if resp.Error != "" {
t.Errorf("handler returned an unexpected error: %v", resp.Error)
}
}

2
google/mcp/testdata/core vendored Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
exit 0