diff --git a/.gitignore b/.gitignore index bf93c4d29..93521e73c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +stats/ .env.bak *cookies*txt cookies* diff --git a/mcp-servers/go/pandoc-server/Dockerfile b/mcp-servers/go/pandoc-server/Dockerfile new file mode 100644 index 000000000..bfd94158d --- /dev/null +++ b/mcp-servers/go/pandoc-server/Dockerfile @@ -0,0 +1,44 @@ +# Build stage +FROM --platform=$TARGETPLATFORM golang:1.23 AS builder +WORKDIR /src + +# Copy go mod files first for better caching +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build with optimizations +RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-s -w" -o /pandoc-server . + +# Pandoc stage - extract pandoc and its dependencies +FROM debian:stable-slim as pandoc_stage +RUN apt-get update && \ + apt-get install -y --no-install-recommends pandoc && \ + rm -rf /var/lib/apt/lists/* + +# Final stage - minimal runtime +FROM debian:stable-slim + +# Install runtime dependencies for pandoc +RUN apt-get update && \ + apt-get install -y --no-install-recommends libgmp10 && \ + rm -rf /var/lib/apt/lists/* && \ + # Create non-root user + useradd -m -u 1000 -s /bin/bash mcp + +# Copy binaries +COPY --from=builder --chown=mcp:mcp /pandoc-server /usr/local/bin/pandoc-server +COPY --from=pandoc_stage /usr/bin/pandoc /usr/bin/pandoc + +# Switch to non-root user +USER mcp +WORKDIR /home/mcp + +# Add metadata +LABEL org.opencontainers.image.title="Pandoc MCP Server" \ + org.opencontainers.image.description="MCP server for pandoc document conversion" \ + org.opencontainers.image.version="0.2.0" + +ENTRYPOINT ["/usr/local/bin/pandoc-server"] diff --git a/mcp-servers/go/pandoc-server/Makefile b/mcp-servers/go/pandoc-server/Makefile new file mode 100644 index 000000000..e7727cefb --- /dev/null +++ b/mcp-servers/go/pandoc-server/Makefile @@ -0,0 +1,274 @@ +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# 📄 PANDOC-SERVER - Makefile +# MCP server for pandoc document conversion +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# +# Author : Mihai Criveti +# Usage : make or just `make help` +# +# help: 📄 PANDOC-SERVER (Go MCP server for document conversion) +# ───────────────────────────────────────────────────────────────────────── + +# ============================================================================= +# 📖 DYNAMIC HELP +# ============================================================================= +.PHONY: help +help: + @grep '^# help\:' $(firstword $(MAKEFILE_LIST)) | sed 's/^# help\: //' + +# ============================================================================= +# 📦 PROJECT METADATA +# ============================================================================= +MODULE := pandoc-server +BIN_NAME := pandoc-server +VERSION ?= $(shell git describe --tags --dirty --always 2>/dev/null || echo "v0.2.0") + +DIST_DIR := dist +COVERPROFILE := $(DIST_DIR)/coverage.out +COVERHTML := $(DIST_DIR)/coverage.html + +GO ?= go +GOOS ?= $(shell $(GO) env GOOS) +GOARCH ?= $(shell $(GO) env GOARCH) + +LDFLAGS := -s -w -X 'main.appVersion=$(VERSION)' + +ifeq ($(shell test -t 1 && echo tty),tty) +C_GREEN := \033[38;5;82m +C_BLUE := \033[38;5;75m +C_RESET := \033[0m +else +C_GREEN := +C_BLUE := +C_RESET := +endif + +# ============================================================================= +# 🔧 TOOLING +# ============================================================================= +# help: 🔧 TOOLING +# help: tools - Install golangci-lint & staticcheck + +GOBIN := $(shell $(GO) env GOPATH)/bin + +.PHONY: tools +tools: $(GOBIN)/golangci-lint $(GOBIN)/staticcheck + +$(GOBIN)/golangci-lint: + @echo "$(C_BLUE)Installing golangci-lint...$(C_RESET)" + @$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +$(GOBIN)/staticcheck: + @echo "$(C_BLUE)Installing staticcheck...$(C_RESET)" + @$(GO) install honnef.co/go/tools/cmd/staticcheck@latest + +# ============================================================================= +# 📂 MODULE & FORMAT +# ============================================================================= +# help: 📂 MODULE & FORMAT +# help: tidy - go mod tidy + verify +# help: fmt - Run gofmt & goimports + +.PHONY: tidy fmt + +tidy: + @echo "$(C_BLUE)Tidying dependencies...$(C_RESET)" + @$(GO) mod tidy + @$(GO) mod verify + +fmt: + @echo "$(C_BLUE)Formatting code...$(C_RESET)" + @$(GO) fmt ./... + @go run golang.org/x/tools/cmd/goimports@latest -w . + +# ============================================================================= +# 🔍 LINTING & STATIC ANALYSIS +# ============================================================================= +# help: 🔍 LINTING & STATIC ANALYSIS +# help: vet - go vet +# help: staticcheck - Run staticcheck +# help: lint - Run golangci-lint +# help: pre-commit - Run all pre-commit hooks + +.PHONY: vet staticcheck lint pre-commit + +vet: + @echo "$(C_BLUE)Running go vet...$(C_RESET)" + @$(GO) vet ./... + +staticcheck: tools + @echo "$(C_BLUE)Running staticcheck...$(C_RESET)" + @staticcheck ./... + +lint: tools + @echo "$(C_BLUE)Running golangci-lint...$(C_RESET)" + @golangci-lint run + +pre-commit: + @command -v pre-commit >/dev/null 2>&1 || { \ + echo '✖ pre-commit not installed → pip install --user pre-commit'; exit 1; } + @pre-commit run --all-files --show-diff-on-failure + +# ============================================================================= +# 🧪 TESTS & COVERAGE +# ============================================================================= +# help: 🧪 TESTS & COVERAGE +# help: test - Run unit tests (race) +# help: test-verbose - Run tests with verbose output +# help: coverage - Generate HTML coverage report +# help: test-integration - Run integration tests + +.PHONY: test test-verbose coverage test-integration + +test: + @echo "$(C_BLUE)Running tests...$(C_RESET)" + @$(GO) test -race -timeout=90s ./... + +test-verbose: + @echo "$(C_BLUE)Running tests (verbose)...$(C_RESET)" + @$(GO) test -v -race -timeout=90s ./... + +coverage: + @mkdir -p $(DIST_DIR) + @echo "$(C_BLUE)Generating coverage report...$(C_RESET)" + @$(GO) test ./... -covermode=count -coverprofile=$(COVERPROFILE) + @$(GO) tool cover -html=$(COVERPROFILE) -o $(COVERHTML) + @echo "$(C_GREEN)✔ HTML coverage → $(COVERHTML)$(C_RESET)" + +test-integration: build + @echo "$(C_BLUE)Running integration tests...$(C_RESET)" + @./test_integration.sh + +# ============================================================================= +# 🛠 BUILD & RUN +# ============================================================================= +# help: 🛠 BUILD & RUN +# help: build - Build binary into ./dist +# help: install - go install into GOPATH/bin +# help: release - Cross-compile (honours GOOS/GOARCH) +# help: run - Build then run (stdio transport) +# help: run-translate - Run with MCP Gateway translate on :9000 + +.PHONY: build install release run run-translate + +build: tidy + @mkdir -p $(DIST_DIR) + @echo "$(C_BLUE)Building $(BIN_NAME)...$(C_RESET)" + @CGO_ENABLED=0 $(GO) build -trimpath -ldflags '$(LDFLAGS)' -o $(DIST_DIR)/$(BIN_NAME) . + @echo "$(C_GREEN)✔ Built → $(DIST_DIR)/$(BIN_NAME)$(C_RESET)" + +install: + @echo "$(C_BLUE)Installing $(BIN_NAME)...$(C_RESET)" + @$(GO) install -trimpath -ldflags '$(LDFLAGS)' . + @echo "$(C_GREEN)✔ Installed → $(GOBIN)/$(BIN_NAME)$(C_RESET)" + +release: + @mkdir -p $(DIST_DIR)/$(GOOS)-$(GOARCH) + @echo "$(C_BLUE)Building release for $(GOOS)/$(GOARCH)...$(C_RESET)" + @GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=0 \ + $(GO) build -trimpath -ldflags '$(LDFLAGS)' \ + -o $(DIST_DIR)/$(GOOS)-$(GOARCH)/$(BIN_NAME) . + @echo "$(C_GREEN)✔ Release → $(DIST_DIR)/$(GOOS)-$(GOARCH)/$(BIN_NAME)$(C_RESET)" + +run: build + @echo "$(C_BLUE)Starting $(BIN_NAME) (stdio)...$(C_RESET)" + @$(DIST_DIR)/$(BIN_NAME) + +run-translate: build + @echo "$(C_BLUE)Starting $(BIN_NAME) with MCP Gateway on :9000...$(C_RESET)" + @python3 -m mcpgateway.translate --stdio "$(DIST_DIR)/$(BIN_NAME)" --port 9000 + +# ============================================================================= +# 🐳 DOCKER +# ============================================================================= +# help: 🐳 DOCKER +# help: docker-build - Build container image +# help: docker-run - Run container (stdio) +# help: docker-test - Test container with pandoc conversion + +IMAGE ?= $(BIN_NAME):$(VERSION) + +.PHONY: docker-build docker-run docker-test + +docker-build: + @echo "$(C_BLUE)Building Docker image $(IMAGE)...$(C_RESET)" + @docker build --build-arg VERSION=$(VERSION) -t $(IMAGE) . + @docker images $(IMAGE) + @echo "$(C_GREEN)✔ Docker image → $(IMAGE)$(C_RESET)" + +docker-run: docker-build + @echo "$(C_BLUE)Running Docker container...$(C_RESET)" + @docker run --rm -i $(IMAGE) + +docker-test: docker-build + @echo "$(C_BLUE)Testing Docker container...$(C_RESET)" + @echo '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}' | \ + docker run --rm -i $(IMAGE) | python3 -m json.tool + +# ============================================================================= +# 📚 PANDOC SPECIFIC TESTS +# ============================================================================= +# help: 📚 PANDOC TESTS +# help: test-pandoc - Test pandoc conversion +# help: test-formats - Test listing formats +# help: test-health - Test health check + +.PHONY: test-pandoc test-formats test-health + +test-pandoc: build + @echo "$(C_BLUE)Testing pandoc conversion...$(C_RESET)" + @echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"pandoc","arguments":{"from":"markdown","to":"html","input":"# Hello\\n\\nThis is **bold** text."}},"id":1}' | \ + timeout 2 $(DIST_DIR)/$(BIN_NAME) 2>/dev/null | python3 -m json.tool | head -20 + +test-formats: build + @echo "$(C_BLUE)Testing format listing...$(C_RESET)" + @echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list-formats","arguments":{"type":"input"}},"id":1}' | \ + timeout 2 $(DIST_DIR)/$(BIN_NAME) 2>/dev/null | python3 -c "import json, sys; d=json.loads(sys.stdin.read()); print('Input formats:', len(d['result']['content'][0]['text'].split()))" + +test-health: build + @echo "$(C_BLUE)Testing health check...$(C_RESET)" + @echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"health","arguments":{}},"id":1}' | \ + timeout 2 $(DIST_DIR)/$(BIN_NAME) 2>/dev/null | python3 -c "import json, sys; d=json.loads(sys.stdin.read()); print('pandoc' in d['result']['content'][0]['text'] and '✔ Health check passed' or '✖ Health check failed')" + +# ============================================================================= +# 🧹 CLEANUP +# ============================================================================= +# help: 🧹 CLEANUP +# help: clean - Remove build & coverage artifacts +# help: clean-all - Clean + remove tool binaries + +.PHONY: clean clean-all + +clean: + @echo "$(C_BLUE)Cleaning build artifacts...$(C_RESET)" + @rm -rf $(DIST_DIR) $(COVERPROFILE) $(COVERHTML) + @echo "$(C_GREEN)✔ Workspace clean$(C_RESET)" + +clean-all: clean + @echo "$(C_BLUE)Removing tool binaries...$(C_RESET)" + @rm -f $(GOBIN)/golangci-lint $(GOBIN)/staticcheck + @echo "$(C_GREEN)✔ All clean$(C_RESET)" + +# ============================================================================= +# 🚀 QUICK DEVELOPMENT +# ============================================================================= +# help: 🚀 QUICK DEVELOPMENT +# help: dev - Format, test, and build +# help: check - Run all checks (vet, lint, test) +# help: all - Full pipeline (fmt, check, build, test-pandoc) + +.PHONY: dev check all + +dev: fmt test build + @echo "$(C_GREEN)✔ Development build complete$(C_RESET)" + +check: vet lint test + @echo "$(C_GREEN)✔ All checks passed$(C_RESET)" + +all: fmt check build test-pandoc test-formats test-health + @echo "$(C_GREEN)✔ Full pipeline complete$(C_RESET)" + +# --------------------------------------------------------------------------- +# Default goal +# --------------------------------------------------------------------------- +.DEFAULT_GOAL := help diff --git a/mcp-servers/go/pandoc-server/README.md b/mcp-servers/go/pandoc-server/README.md new file mode 100644 index 000000000..7adda3f7a --- /dev/null +++ b/mcp-servers/go/pandoc-server/README.md @@ -0,0 +1,144 @@ +# Pandoc Server + +An MCP server that provides pandoc document conversion capabilities. This server enables text conversion between various formats using the powerful pandoc tool. + +## Features + +- Convert between 30+ document formats +- Support for standalone documents +- Table of contents generation +- Custom metadata support +- Format discovery tools + +## Tools + +### `pandoc` +Convert text from one format to another. + +**Parameters:** +- `from` (required): Input format (e.g., markdown, html, latex, rst, docx, epub) +- `to` (required): Output format (e.g., html, markdown, latex, pdf, docx, plain) +- `input` (required): The text to convert +- `standalone` (optional): Produce a standalone document (default: false) +- `title` (optional): Document title for standalone documents +- `metadata` (optional): Additional metadata in key=value format +- `toc` (optional): Include table of contents (default: false) + +### `list-formats` +List available pandoc input and output formats. + +**Parameters:** +- `type` (optional): Format type to list: 'input', 'output', or 'all' (default: 'all') + +### `health` +Check if pandoc is installed and return version information. + +## Requirements + +- Go 1.23 or later +- Pandoc installed on the system + +## Installation + +### From Source + +```bash +# Clone the repository +git clone +cd pandoc-server + +# Install dependencies +go mod download + +# Build the server +make build +``` + +### Using Docker + +```bash +# Build the Docker image +docker build -t pandoc-server . + +# Run the container +docker run -i pandoc-server +``` + +## Usage + +### Direct Execution + +```bash +# Run the built server +./dist/pandoc-server +``` + +### With MCP Gateway + +```bash +# Use MCP Gateway's translate module to expose via HTTP/SSE +python3 -m mcpgateway.translate --stdio "./dist/pandoc-server" --port 9000 +``` + +### Testing + +```bash +# Run tests +make test + +# Format code +make fmt + +# Tidy dependencies +make tidy +``` + +## Example Usage + +### Convert Markdown to HTML + +```json +{ + "tool": "pandoc", + "arguments": { + "from": "markdown", + "to": "html", + "input": "# Hello World\n\nThis is **bold** text.", + "standalone": true, + "title": "My Document" + } +} +``` + +### List Available Formats + +```json +{ + "tool": "list-formats", + "arguments": { + "type": "input" + } +} +``` + +## Supported Formats + +Pandoc supports numerous input and output formats. Common ones include: + +**Input:** markdown, html, latex, rst, docx, epub, json, csv, mediawiki, org + +**Output:** html, markdown, latex, pdf, docx, epub, plain, json, asciidoc, rst + +Use the `list-formats` tool to see all available formats on your system. + +## Development + +Contributions are welcome! Please ensure: + +1. Code passes all tests: `make test` +2. Code is properly formatted: `make fmt` +3. Dependencies are tidied: `make tidy` + +## License + +MIT diff --git a/mcp-servers/go/pandoc-server/go.mod b/mcp-servers/go/pandoc-server/go.mod new file mode 100644 index 000000000..2b4243683 --- /dev/null +++ b/mcp-servers/go/pandoc-server/go.mod @@ -0,0 +1,13 @@ +module pandoc-server + +go 1.23 + +toolchain go1.23.10 + +require github.com/mark3labs/mcp-go v0.32.0 + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect +) diff --git a/mcp-servers/go/pandoc-server/go.sum b/mcp-servers/go/pandoc-server/go.sum new file mode 100644 index 000000000..a7353035b --- /dev/null +++ b/mcp-servers/go/pandoc-server/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8= +github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mcp-servers/go/pandoc-server/main.go b/mcp-servers/go/pandoc-server/main.go new file mode 100644 index 000000000..1b39dbff5 --- /dev/null +++ b/mcp-servers/go/pandoc-server/main.go @@ -0,0 +1,185 @@ +// main.go +package main + +import ( + "context" + "log" + "os" + "os/exec" + "strings" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + appName = "pandoc-server" + appVersion = "0.2.0" +) + +func handlePandoc(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + from, err := req.RequireString("from") + if err != nil { + return mcp.NewToolResultError("from parameter is required"), nil + } + + to, err := req.RequireString("to") + if err != nil { + return mcp.NewToolResultError("to parameter is required"), nil + } + + input, err := req.RequireString("input") + if err != nil { + return mcp.NewToolResultError("input parameter is required"), nil + } + + // Optional parameters + standalone := req.GetBool("standalone", false) + title := req.GetString("title", "") + metadata := req.GetString("metadata", "") + toc := req.GetBool("toc", false) + + // Build pandoc command + args := []string{"-f", from, "-t", to} + + if standalone { + args = append(args, "--standalone") + } + + if title != "" { + args = append(args, "--metadata", "title="+title) + } + + if metadata != "" { + args = append(args, "--metadata", metadata) + } + + if toc { + args = append(args, "--toc") + } + + cmd := exec.CommandContext(ctx, "pandoc", args...) + cmd.Stdin = strings.NewReader(input) + var out strings.Builder + cmd.Stdout = &out + var stderr strings.Builder + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + errMsg := stderr.String() + if errMsg == "" { + errMsg = err.Error() + } + return mcp.NewToolResultError("Pandoc conversion failed: " + errMsg), nil + } + + return mcp.NewToolResultText(out.String()), nil +} + +func handleHealth(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + cmd := exec.Command("pandoc", "--version") + out, err := cmd.CombinedOutput() + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + return mcp.NewToolResultText(string(out)), nil +} + +func handleListFormats(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + formatType := req.GetString("type", "all") + + var cmd *exec.Cmd + switch formatType { + case "input": + cmd = exec.Command("pandoc", "--list-input-formats") + case "output": + cmd = exec.Command("pandoc", "--list-output-formats") + case "all": + inputCmd := exec.Command("pandoc", "--list-input-formats") + inputOut, err := inputCmd.CombinedOutput() + if err != nil { + return mcp.NewToolResultError("Failed to get input formats: " + err.Error()), nil + } + + outputCmd := exec.Command("pandoc", "--list-output-formats") + outputOut, err := outputCmd.CombinedOutput() + if err != nil { + return mcp.NewToolResultError("Failed to get output formats: " + err.Error()), nil + } + + result := "Input Formats:\n" + string(inputOut) + "\nOutput Formats:\n" + string(outputOut) + return mcp.NewToolResultText(result), nil + default: + return mcp.NewToolResultError("Invalid type parameter. Use 'input', 'output', or 'all'"), nil + } + + out, err := cmd.CombinedOutput() + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + return mcp.NewToolResultText(string(out)), nil +} + +func main() { + logger := log.New(os.Stderr, "", log.LstdFlags) + logger.Printf("starting %s %s (stdio)", appName, appVersion) + + s := server.NewMCPServer( + appName, + appVersion, + server.WithToolCapabilities(false), + server.WithLogging(), + server.WithRecovery(), + ) + + pandocTool := mcp.NewTool("pandoc", + mcp.WithDescription("Convert text from one format to another using pandoc."), + mcp.WithTitleAnnotation("Pandoc"), + mcp.WithString("from", + mcp.Required(), + mcp.Description("The input format (e.g., markdown, html, latex, rst, docx, epub)"), + ), + mcp.WithString("to", + mcp.Required(), + mcp.Description("The output format (e.g., html, markdown, latex, pdf, docx, plain)"), + ), + mcp.WithString("input", + mcp.Required(), + mcp.Description("The text to convert"), + ), + mcp.WithBoolean("standalone", + mcp.Description("Produce a standalone document (default: false)"), + ), + mcp.WithString("title", + mcp.Description("Document title for standalone documents"), + ), + mcp.WithString("metadata", + mcp.Description("Additional metadata in key=value format"), + ), + mcp.WithBoolean("toc", + mcp.Description("Include table of contents (default: false)"), + ), + ) + s.AddTool(pandocTool, handlePandoc) + + healthTool := mcp.NewTool("health", + mcp.WithDescription("Check if pandoc is installed and return the version."), + mcp.WithTitleAnnotation("Health Check"), + mcp.WithReadOnlyHintAnnotation(true), + ) + s.AddTool(healthTool, handleHealth) + + listFormatsTool := mcp.NewTool("list-formats", + mcp.WithDescription("List available pandoc input and output formats."), + mcp.WithTitleAnnotation("List Formats"), + mcp.WithString("type", + mcp.Description("Format type to list: 'input', 'output', or 'all' (default: 'all')"), + ), + mcp.WithReadOnlyHintAnnotation(true), + ) + s.AddTool(listFormatsTool, handleListFormats) + + if err := server.ServeStdio(s); err != nil { + logger.Fatalf("stdio error: %v", err) + } +} diff --git a/mcp-servers/go/pandoc-server/main_test.go b/mcp-servers/go/pandoc-server/main_test.go new file mode 100644 index 000000000..a2a174e71 --- /dev/null +++ b/mcp-servers/go/pandoc-server/main_test.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "os/exec" + "strings" + "testing" + + "github.com/mark3labs/mcp-go/mcp" +) + +func TestPandocInstalled(t *testing.T) { + cmd := exec.Command("pandoc", "--version") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("pandoc not installed: %v", err) + } + t.Logf("Pandoc version: %s", string(out)) +} + +func TestPandocConversion(t *testing.T) { + tests := []struct { + name string + from string + to string + input string + want string + }{ + { + name: "markdown to html", + from: "markdown", + to: "html", + input: "# Hello World\n\nThis is **bold** text.", + want: "Hello

This is bold text.

", + want: "Hello", + }, + { + name: "markdown to plain", + from: "markdown", + to: "plain", + input: "# Hello\n\nThis is **bold** text.", + want: "Hello", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command("pandoc", "-f", tt.from, "-t", tt.to) + cmd.Stdin = strings.NewReader(tt.input) + var out strings.Builder + cmd.Stdout = &out + var stderr strings.Builder + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + t.Fatalf("pandoc failed: %v, stderr: %s", err, stderr.String()) + } + + result := out.String() + if !strings.Contains(result, tt.want) { + t.Errorf("got %q, want substring %q", result, tt.want) + } + }) + } +} + +func TestHandlers(t *testing.T) { + ctx := context.Background() + + t.Run("health handler", func(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "health", + Arguments: map[string]interface{}{}, + }, + } + result, err := handleHealth(ctx, req) + if err != nil { + t.Fatalf("handleHealth failed: %v", err) + } + if result == nil { + t.Fatal("handleHealth returned nil") + } + }) + + t.Run("pandoc handler with valid params", func(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "pandoc", + Arguments: map[string]interface{}{ + "from": "markdown", + "to": "html", + "input": "# Hello World", + }, + }, + } + result, err := handlePandoc(ctx, req) + if err != nil { + t.Fatalf("handlePandoc failed: %v", err) + } + if result == nil { + t.Fatal("handlePandoc returned nil") + } + }) + + t.Run("pandoc handler missing from param", func(t *testing.T) { + req := mcp.CallToolRequest{ + Params: mcp.CallToolParams{ + Name: "pandoc", + Arguments: map[string]interface{}{ + "to": "html", + "input": "# Hello World", + }, + }, + } + result, err := handlePandoc(ctx, req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected error result, got nil") + } + }) +} diff --git a/mcp-servers/go/pandoc-server/test_integration.sh b/mcp-servers/go/pandoc-server/test_integration.sh new file mode 100755 index 000000000..50482dd08 --- /dev/null +++ b/mcp-servers/go/pandoc-server/test_integration.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -e + +echo "=== Pandoc Server Integration Test ===" + +# Colors for output +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Build the server +echo "Building server..." +make build + +# Start the server in background +echo "Starting server..." +./dist/pandoc-server & +SERVER_PID=$! +sleep 2 + +# Function to send JSON-RPC request +send_request() { + local method=$1 + local params=$2 + echo "{\"jsonrpc\":\"2.0\",\"method\":\"$method\",\"params\":$params,\"id\":1}" | ./dist/pandoc-server +} + +# Test 1: List tools +echo -e "\n${GREEN}Test 1: List tools${NC}" +echo '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}' | timeout 2 ./dist/pandoc-server | head -1 + +# Test 2: Health check +echo -e "\n${GREEN}Test 2: Health check${NC}" +echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"health"},"id":1}' | timeout 2 ./dist/pandoc-server | head -1 + +# Test 3: List formats +echo -e "\n${GREEN}Test 3: List formats (input only)${NC}" +echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list-formats","arguments":{"type":"input"}},"id":1}' | timeout 2 ./dist/pandoc-server | head -1 + +# Test 4: Convert markdown to HTML +echo -e "\n${GREEN}Test 4: Convert markdown to HTML${NC}" +echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"pandoc","arguments":{"from":"markdown","to":"html","input":"# Hello\n\nThis is **bold** text."}},"id":1}' | timeout 2 ./dist/pandoc-server | head -1 + +# Clean up +kill $SERVER_PID 2>/dev/null || true + +echo -e "\n${GREEN}All tests completed successfully!${NC}" +exit 0 \ No newline at end of file