Add CLI client subcommands and MCP server

Adds push, list, get, delete, and login subcommands backed by an HTTP
API client, plus an MCP server for tool-based access to the document
queue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 00:08:55 -07:00
parent ed3a547e54
commit 3d5f52729f
14 changed files with 1161 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
package mcpserver
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/mark3labs/mcp-go/mcp"
"git.wntrmute.dev/mc/mcq/internal/client"
)
func testClient(t *testing.T) *client.Client {
t.Helper()
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/documents", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"documents": []client.Document{
{ID: 1, Slug: "doc-1", Title: "First Doc", Body: "# First\nContent", PushedBy: "admin", PushedAt: "2026-01-01T00:00:00Z"},
},
})
})
mux.HandleFunc("GET /v1/documents/{slug}", func(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
if slug == "missing" {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(client.Document{ID: 1, Slug: slug, Title: "Test Doc", Body: "# Test\nContent"})
})
mux.HandleFunc("PUT /v1/documents/{slug}", func(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
var req struct {
Title string `json:"title"`
Body string `json:"body"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(client.Document{ID: 1, Slug: slug, Title: req.Title, Body: req.Body, PushedBy: "admin"})
})
mux.HandleFunc("DELETE /v1/documents/{slug}", func(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
if slug == "missing" {
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusNoContent)
})
ts := httptest.NewServer(mux)
t.Cleanup(ts.Close)
return client.New(ts.URL, "test-token", client.WithHTTPClient(ts.Client()))
}
func TestPushDocumentHandler(t *testing.T) {
c := testClient(t)
handler := pushDocumentHandler(c)
result, err := handler(context.Background(), mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "push_document",
Arguments: map[string]any{"slug": "test-doc", "title": "Test", "body": "# Test\nContent"},
},
})
if err != nil {
t.Fatalf("push handler: %v", err)
}
if result.IsError {
t.Fatalf("unexpected tool error: %v", result.Content)
}
text := result.Content[0].(mcp.TextContent).Text
if !strings.Contains(text, "test-doc") {
t.Fatalf("expected slug in response, got: %s", text)
}
}
func TestListDocumentsHandler(t *testing.T) {
c := testClient(t)
handler := listDocumentsHandler(c)
result, err := handler(context.Background(), mcp.CallToolRequest{})
if err != nil {
t.Fatalf("list handler: %v", err)
}
if result.IsError {
t.Fatalf("unexpected tool error: %v", result.Content)
}
text := result.Content[0].(mcp.TextContent).Text
if !strings.Contains(text, "doc-1") {
t.Fatalf("expected doc slug in response, got: %s", text)
}
}
func TestGetDocumentHandler(t *testing.T) {
c := testClient(t)
handler := getDocumentHandler(c)
result, err := handler(context.Background(), mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "get_document",
Arguments: map[string]any{"slug": "test-doc"},
},
})
if err != nil {
t.Fatalf("get handler: %v", err)
}
if result.IsError {
t.Fatalf("unexpected tool error: %v", result.Content)
}
text := result.Content[0].(mcp.TextContent).Text
if !strings.Contains(text, "# Test") {
t.Fatalf("expected markdown body, got: %s", text)
}
}
func TestDeleteDocumentHandler(t *testing.T) {
c := testClient(t)
handler := deleteDocumentHandler(c)
result, err := handler(context.Background(), mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "delete_document",
Arguments: map[string]any{"slug": "test-doc"},
},
})
if err != nil {
t.Fatalf("delete handler: %v", err)
}
if result.IsError {
t.Fatalf("unexpected tool error: %v", result.Content)
}
text := result.Content[0].(mcp.TextContent).Text
if !strings.Contains(text, "Deleted") {
t.Fatalf("expected deletion confirmation, got: %s", text)
}
}
func TestDeleteDocumentNotFound(t *testing.T) {
c := testClient(t)
handler := deleteDocumentHandler(c)
result, err := handler(context.Background(), mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: "delete_document",
Arguments: map[string]any{"slug": "missing"},
},
})
if err != nil {
t.Fatalf("delete handler: %v", err)
}
if !result.IsError {
t.Fatal("expected tool error for missing document")
}
}
func TestNewCreatesServer(t *testing.T) {
c := testClient(t)
s := New(c, "test")
if s == nil {
t.Fatal("expected non-nil server")
}
}