Hello from MCP server
package main
import (
"bufio"
"html/template"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
var projectRoot string
var devopsDir string
var baseTmpl = template.Must(template.New("base").Parse(`<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MCP Server</title>
<style>
form { margin: 2px 0; }
button, input { margin: 0 2px 0 0; }
</style>
</head>
<body>
<p>Hello from MCP server</p>
<div>
<a href="/dev/files/">List Files</a> |
<a href="/dev/commands/">Just Commands</a> |
<a href="/dev/repo/">Repo</a> |
<a href="/dev/logs/">Logs</a>
</div>
<hr>
{{if .SubNav}}<div>{{.SubNav}}</div>{{end}}
<div>{{.Content}}</div>
</body>
</html>`))
type PageData struct {
SubNav template.HTML
Content template.HTML
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8082"
}
cwd, _ := os.Getwd()
devopsDir = filepath.Dir(cwd)
projectRoot = filepath.Dir(devopsDir)
log.Printf("Project root: %s", projectRoot)
http.HandleFunc("/", handleIndex)
http.HandleFunc("/dev/files/", handleFiles)
http.HandleFunc("/dev/commands/", handleCommands)
http.HandleFunc("/dev/repo/", handleRepo)
http.HandleFunc("/dev/logs/", handleLogs)
http.HandleFunc("/health", handleHealth)
log.Printf("MCP server starting on port %s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
render(w, PageData{})
}
func handleFiles(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/dev/files/")
// Check for file view
if r.URL.Query().Get("file") != "" {
file := r.URL.Query().Get("file")
render(w, PageData{Content: template.HTML(renderFile(file))})
return
}
render(w, PageData{Content: template.HTML(renderFiles(path))})
}
func handleCommands(w http.ResponseWriter, r *http.Request) {
action := r.URL.Query().Get("action")
if action == "run" {
cmd := r.URL.Query().Get("cmd")
args := r.URL.Query().Get("args")
content := renderRunCommand(cmd, args)
render(w, PageData{Content: template.HTML(content)})
return
}
render(w, PageData{Content: template.HTML(renderJustCommands())})
}
func handleRepo(w http.ResponseWriter, r *http.Request) {
action := r.URL.Query().Get("action")
subNav := `<a href="/dev/repo/?action=fetch">Fetch</a> | <a href="/dev/repo/?action=changes">Changes</a>`
var content string
var message string
switch action {
case "fetch":
output, err := runGit("fetch", "--all", "-v")
if err != nil {
message = "Fetch failed: " + output
} else {
if output == "" {
message = "Fetch complete (no updates)"
} else {
message = "Fetch complete:\n" + output
}
}
subNav += `<br><br>
<form action="/dev/repo/" method="get" style="display:inline">
<input type="hidden" name="action" value="newbranch">
<input type="text" name="name" placeholder="new branch name">
<button type="submit">Create Branch</button>
</form>
|
<form action="/dev/repo/" method="get" style="display:inline">
<input type="hidden" name="action" value="switch">
<select name="name">` + renderBranchOptions() + `</select>
<button type="submit">Switch Branch</button>
</form>`
content = renderRepoStatus()
if message != "" {
content = message + "\n\n" + content
}
case "newbranch":
name := r.URL.Query().Get("name")
if name == "" {
content = "Branch name required"
} else {
output, err := runGit("checkout", "-b", name)
if err != nil {
content = "Failed to create branch: " + output
} else {
content = "Created and switched to branch: " + name + "\n" + output
}
}
case "switch":
name := r.URL.Query().Get("name")
if name == "" {
content = "Branch name required"
} else {
var output string
var err error
if strings.Contains(name, "/") {
// Remote branch - create local tracking branch
output, err = runGit("checkout", "--track", name)
} else {
// Local branch - just switch
output, err = runGit("checkout", name)
}
if err != nil {
content = "Failed to switch branch: " + output
} else {
content = "Switched to branch: " + name + "\n" + output
}
}
case "changes":
subNav += `<br><br><form action="/dev/repo/" method="get" style="display:inline">
<input type="hidden" name="action" value="commit">
<input type="text" name="msg" placeholder="commit message (optional)">
<button type="submit">Commit & Push</button>
</form>`
content = renderChanges()
case "commit":
msg := r.URL.Query().Get("msg")
content = renderRepoCommitPush(msg)
default:
content = renderRepoStatus()
}
render(w, PageData{SubNav: template.HTML(subNav), Content: template.HTML(content)})
}
func handleLogs(w http.ResponseWriter, r *http.Request) {
render(w, PageData{Content: template.HTML(renderLogs())})
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status": "ok"}`))
}
func render(w http.ResponseWriter, data PageData) {
w.Header().Set("Content-Type", "text/html")
baseTmpl.Execute(w, data)
}
func renderFiles(path string) string {
if strings.Contains(filepath.Clean(path), "..") {
return "invalid path"
}
dirPath := filepath.Join(projectRoot, path)
entries, err := os.ReadDir(dirPath)
if err != nil {
return "error: " + err.Error()
}
var lines []string
if path != "" {
parent := filepath.Dir(path)
if parent == "." {
parent = ""
}
lines = append(lines, `<a href="/dev/files/`+parent+`">..</a>`)
}
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() {
fullPath := filepath.Join(path, name)
lines = append(lines, `<a href="/dev/files/`+fullPath+`/">`+name+`/</a>`)
} else {
fullPath := filepath.Join(path, name)
lines = append(lines, `<a href="/dev/files/?file=`+fullPath+`">`+name+`</a>`)
}
}
return strings.Join(lines, "\n")
}
func renderFile(name string) string {
if strings.Contains(filepath.Clean(name), "..") {
return "invalid path"
}
filePath := filepath.Join(projectRoot, name)
content, err := os.ReadFile(filePath)
if err != nil {
return "error: " + err.Error()
}
escaped := escapeHTML(string(content))
parent := filepath.Dir(name)
if parent == "." {
parent = ""
}
return `<a href="/dev/files/` + parent + `">← back</a> | <button onclick="navigator.clipboard.writeText(document.getElementById('filecontent').textContent)">Copy</button>
<pre id="filecontent">` + escaped + `</pre>`
}
func renderJustCommands() string {
justfilePath := filepath.Join(devopsDir, "justfile")
file, err := os.Open(justfilePath)
if err != nil {
return "error: " + err.Error()
}
defer file.Close()
var lines []string
var lastComment string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") {
lastComment = strings.TrimPrefix(line, "# ")
} else if len(line) > 0 && !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, "@") && strings.Contains(line, ":") {
parts := strings.SplitN(line, ":", 2)
nameParts := strings.TrimSpace(parts[0])
tokens := strings.Fields(nameParts)
if len(tokens) == 0 {
continue
}
name := tokens[0]
params := tokens[1:]
if name != "" && !strings.HasPrefix(name, "set") && name != "default" {
form := `<form action="/dev/commands/" method="get">
<input type="hidden" name="action" value="run">
<input type="hidden" name="cmd" value="` + name + `">
<button type="submit">` + name + `</button>`
if len(params) > 0 {
var paramNames []string
for _, p := range params {
// Strip default values like param="default"
pName := strings.Split(p, "=")[0]
paramNames = append(paramNames, pName)
}
form += ` <input type="text" name="args" placeholder="` + strings.Join(paramNames, " ") + `" size="40">`
}
if lastComment != "" {
form += ` <span>` + lastComment + `</span>`
}
form += `</form>`
lines = append(lines, form)
}
lastComment = ""
} else if line == "" {
lastComment = ""
}
}
return strings.Join(lines, "\n")
}
func renderRunCommand(cmd, args string) string {
if cmd == "" {
return "No command specified"
}
cmdArgs := []string{cmd}
if args != "" {
cmdArgs = append(cmdArgs, strings.Fields(args)...)
}
execCmd := exec.Command("just", cmdArgs...)
execCmd.Dir = devopsDir
output, err := execCmd.CombinedOutput()
result := "Running: just " + strings.Join(cmdArgs, " ") + "\n\n"
if err != nil {
result += "Error: " + err.Error() + "\n\n"
}
result += escapeHTML(string(output))
return `<a href="/dev/commands/">← back</a>
<pre>` + result + `</pre>`
}
func renderRepoStatus() string {
var lines []string
branch, err := runGit("rev-parse", "--abbrev-ref", "HEAD")
if err != nil {
return "error: " + err.Error()
}
lines = append(lines, "Branch: "+strings.TrimSpace(branch))
lines = append(lines, "")
status, _ := runGit("status", "--short")
if status == "" {
lines = append(lines, "Status: clean")
} else {
lines = append(lines, "Status:")
lines = append(lines, status)
}
lines = append(lines, "")
commits, _ := runGit("log", "--oneline", "-10")
lines = append(lines, "Recent commits:")
lines = append(lines, commits)
lines = append(lines, "")
branches, _ := runGit("branch", "-a")
lines = append(lines, "Branches:")
lines = append(lines, branches)
return "<pre>" + strings.Join(lines, "\n") + "</pre>"
}
func renderChanges() string {
status, err := runGit("status")
if err != nil {
return "error: " + err.Error()
}
return "<pre>" + escapeHTML(status) + "</pre>"
}
func renderRepoCommitPush(msg string) string {
var lines []string
branch, _ := runGit("rev-parse", "--abbrev-ref", "HEAD")
branch = strings.TrimSpace(branch)
if branch == "master" || branch == "main" {
return "Cannot push to " + branch + " directly. Create a feature branch first."
}
status, _ := runGit("status", "--short")
if status == "" {
return "Nothing to commit, working tree clean"
}
output, err := runGit("add", ".")
if err != nil {
return "Failed to stage changes: " + output
}
lines = append(lines, "Staged all changes")
commitMsg := "wip"
if msg != "" {
commitMsg = msg
}
output, err = runGit("commit", "-m", commitMsg)
if err != nil {
return "Failed to commit: " + output
}
lines = append(lines, "Committed: "+commitMsg)
output, err = runGit("push", "-u", "origin", branch)
if err != nil {
lines = append(lines, "Failed to push: "+output)
} else {
lines = append(lines, "Pushed to "+branch)
if output != "" {
lines = append(lines, output)
}
}
return "<pre>" + strings.Join(lines, "\n") + "</pre>"
}
func renderLogs() string {
return "<pre>Logs are output to terminal.\n\nCheck the terminal where 'just dev-mcp' is running.</pre>"
}
func renderBranchOptions() string {
var options []string
// Get current branch
current, _ := runGit("rev-parse", "--abbrev-ref", "HEAD")
current = strings.TrimSpace(current)
// Local branches
localOutput, _ := runGit("branch", "--format=%(refname:short)")
localBranches := strings.Split(strings.TrimSpace(localOutput), "\n")
if len(localBranches) > 0 && localBranches[0] != "" {
options = append(options, `<optgroup label="Local">`)
for _, b := range localBranches {
b = strings.TrimSpace(b)
if b == "" {
continue
}
selected := ""
if b == current {
selected = " selected"
}
options = append(options, `<option value="`+b+`"`+selected+`>`+b+`</option>`)
}
options = append(options, `</optgroup>`)
}
// Remote branches
remoteOutput, _ := runGit("branch", "-r", "--format=%(refname:short)")
remoteBranches := strings.Split(strings.TrimSpace(remoteOutput), "\n")
if len(remoteBranches) > 0 && remoteBranches[0] != "" {
options = append(options, `<optgroup label="Remote">`)
for _, b := range remoteBranches {
b = strings.TrimSpace(b)
if b == "" || strings.Contains(b, "HEAD") {
continue
}
options = append(options, `<option value="`+b+`">`+b+`</option>`)
}
options = append(options, `</optgroup>`)
}
return strings.Join(options, "\n")
}
func runGit(args ...string) (string, error) {
cmd := exec.Command("git", args...)
cmd.Dir = projectRoot
out, err := cmd.CombinedOutput()
return string(out), err
}
func escapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&")
s = strings.ReplaceAll(s, "<", "<")
s = strings.ReplaceAll(s, ">", ">")
return s
}