// Package mcpserver implements an MCP stdio server for MCQ. package mcpserver import ( "context" "fmt" "strings" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "git.wntrmute.dev/mc/mcq/internal/client" ) // New creates an MCP server backed by the given MCQ client. func New(c *client.Client, version string) *server.MCPServer { s := server.NewMCPServer("mcq", version, server.WithToolCapabilities(false), ) s.AddTool(pushDocumentTool(), pushDocumentHandler(c)) s.AddTool(listDocumentsTool(), listDocumentsHandler(c)) s.AddTool(getDocumentTool(), getDocumentHandler(c)) s.AddTool(deleteDocumentTool(), deleteDocumentHandler(c)) return s } func pushDocumentTool() mcp.Tool { return mcp.NewTool("push_document", mcp.WithDescription("Push a markdown document to the MCQ reading queue. If a document with the same slug exists, it will be replaced and marked as unread."), mcp.WithString("slug", mcp.Description("Unique identifier for the document (used in URLs)"), mcp.Required()), mcp.WithString("title", mcp.Description("Document title"), mcp.Required()), mcp.WithString("body", mcp.Description("Document body in markdown format"), mcp.Required()), ) } func pushDocumentHandler(c *client.Client) server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { slug, err := request.RequireString("slug") if err != nil { return toolError("slug is required"), nil } title, err := request.RequireString("title") if err != nil { return toolError("title is required"), nil } body, err := request.RequireString("body") if err != nil { return toolError("body is required"), nil } doc, err := c.PutDocument(ctx, slug, title, body) if err != nil { return toolError(fmt.Sprintf("failed to push document: %v", err)), nil } return toolText(fmt.Sprintf("Pushed document %q (%s) to queue.", doc.Title, doc.Slug)), nil } } func listDocumentsTool() mcp.Tool { return mcp.NewTool("list_documents", mcp.WithDescription("List all documents in the MCQ reading queue."), ) } func listDocumentsHandler(c *client.Client) server.ToolHandlerFunc { return func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { docs, err := c.ListDocuments(ctx) if err != nil { return toolError(fmt.Sprintf("failed to list documents: %v", err)), nil } if len(docs) == 0 { return toolText("No documents in the queue."), nil } var b strings.Builder fmt.Fprintf(&b, "%d document(s) in queue:\n\n", len(docs)) for _, d := range docs { read := "unread" if d.Read { read = "read" } fmt.Fprintf(&b, "- **%s** (`%s`) — pushed by %s at %s [%s]\n", d.Title, d.Slug, d.PushedBy, d.PushedAt, read) } return toolText(b.String()), nil } } func getDocumentTool() mcp.Tool { return mcp.NewTool("get_document", mcp.WithDescription("Get a document's markdown content from the MCQ reading queue."), mcp.WithString("slug", mcp.Description("Document slug"), mcp.Required()), ) } func getDocumentHandler(c *client.Client) server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { slug, err := request.RequireString("slug") if err != nil { return toolError("slug is required"), nil } doc, err := c.GetDocument(ctx, slug) if err != nil { return toolError(fmt.Sprintf("failed to get document: %v", err)), nil } return toolText(doc.Body), nil } } func deleteDocumentTool() mcp.Tool { return mcp.NewTool("delete_document", mcp.WithDescription("Delete a document from the MCQ reading queue."), mcp.WithString("slug", mcp.Description("Document slug"), mcp.Required()), ) } func deleteDocumentHandler(c *client.Client) server.ToolHandlerFunc { return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { slug, err := request.RequireString("slug") if err != nil { return toolError("slug is required"), nil } if err := c.DeleteDocument(ctx, slug); err != nil { return toolError(fmt.Sprintf("failed to delete document: %v", err)), nil } return toolText(fmt.Sprintf("Deleted document %q from queue.", slug)), nil } } func toolText(text string) *mcp.CallToolResult { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{Type: "text", Text: text}, }, } } func toolError(msg string) *mcp.CallToolResult { return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.TextContent{Type: "text", Text: msg}, }, IsError: true, } }