Inicialización del módulo Go y cambio de version de go a la 1.26
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/minio/selfupdate"
|
||||
)
|
||||
|
||||
// Build-time variables inyectadas vía -ldflags -X
|
||||
var (
|
||||
Version = "dev"
|
||||
GiteaURL = ""
|
||||
RepoOwner = ""
|
||||
RepoName = ""
|
||||
)
|
||||
|
||||
// OutlineClient es el cliente HTTP para la API de Outline.
|
||||
type OutlineClient struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// newOutlineClient crea un cliente desde las variables de entorno.
|
||||
func newOutlineClient() (*OutlineClient, error) {
|
||||
baseURL := os.Getenv("OUTLINE_URL")
|
||||
apiKey := os.Getenv("OUTLINE_API_KEY")
|
||||
if baseURL == "" || apiKey == "" {
|
||||
return nil, fmt.Errorf("OUTLINE_URL y OUTLINE_API_KEY deben estar definidos")
|
||||
}
|
||||
return &OutlineClient{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
APIKey: apiKey,
|
||||
HTTPClient: &http.Client{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// post realiza una petición POST autenticada a la API de Outline.
|
||||
func (c *OutlineClient) post(ctx context.Context, path string, payload any, result any) error {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error serializando payload: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creando petición: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+c.APIKey)
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error ejecutando petición: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error leyendo respuesta: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
if err := json.Unmarshal(respBody, result); err != nil {
|
||||
return fmt.Errorf("error parseando respuesta: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Herramientas MCP ---
|
||||
|
||||
func handleListCollections(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
client, err := newOutlineClient()
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := client.post(ctx, "/api/collections.list", map[string]any{}, &resp); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for _, c := range resp.Data {
|
||||
lines = append(lines, fmt.Sprintf("- **%s** (id: %s): %s", c.Name, c.ID, c.Description))
|
||||
}
|
||||
if len(lines) == 0 {
|
||||
return mcp.NewToolResultText("No se encontraron colecciones."), nil
|
||||
}
|
||||
return mcp.NewToolResultText(strings.Join(lines, "\n")), nil
|
||||
}
|
||||
|
||||
func handleSearch(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
client, err := newOutlineClient()
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
query := req.GetString("query", "")
|
||||
if query == "" {
|
||||
return mcp.NewToolResultError("El parámetro query es requerido."), nil
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := client.post(ctx, "/api/documents.search", map[string]any{"query": query}, &resp); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
return mcp.NewToolResultText("No se encontraron documentos para la búsqueda."), nil
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for _, d := range resp.Data {
|
||||
lines = append(lines, fmt.Sprintf("- **%s** (id: %s) %s", d.Title, d.ID, d.URL))
|
||||
}
|
||||
return mcp.NewToolResultText(strings.Join(lines, "\n")), nil
|
||||
}
|
||||
|
||||
func handleGetDocument(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
client, err := newOutlineClient()
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
id := req.GetString("id", "")
|
||||
if id == "" {
|
||||
return mcp.NewToolResultError("El parámetro id es requerido."), nil
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := client.post(ctx, "/api/documents.info", map[string]any{"id": id}, &resp); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
if resp.Data.ID == "" {
|
||||
return mcp.NewToolResultError("Documento no encontrado."), nil
|
||||
}
|
||||
|
||||
result := fmt.Sprintf("# %s\n\n%s", resp.Data.Title, resp.Data.Text)
|
||||
return mcp.NewToolResultText(result), nil
|
||||
}
|
||||
|
||||
func handleCreateDocument(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
client, err := newOutlineClient()
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
title := req.GetString("title", "")
|
||||
text := req.GetString("text", "")
|
||||
collectionID := req.GetString("collection_id", "")
|
||||
|
||||
if title == "" || text == "" || collectionID == "" {
|
||||
return mcp.NewToolResultError("Los parámetros title, text y collection_id son requeridos."), nil
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := client.post(ctx, "/api/documents.create", map[string]any{
|
||||
"title": title,
|
||||
"text": text,
|
||||
"collectionId": collectionID,
|
||||
}, &resp); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
|
||||
return mcp.NewToolResultText(fmt.Sprintf("Documento creado: **%s** (id: %s)", resp.Data.Title, resp.Data.ID)), nil
|
||||
}
|
||||
|
||||
// --- Auto-update ---
|
||||
|
||||
type giteaRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Assets []struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
}
|
||||
|
||||
func parseVersion(v string) (int, int, int, error) {
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
parts := strings.SplitN(v, ".", 3)
|
||||
if len(parts) != 3 {
|
||||
return 0, 0, 0, fmt.Errorf("formato de versión inválido: %s", v)
|
||||
}
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
patch, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
return major, minor, patch, nil
|
||||
}
|
||||
|
||||
func versionNewer(latest, current string) (bool, error) {
|
||||
lMaj, lMin, lPat, err := parseVersion(latest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cMaj, cMin, cPat, err := parseVersion(current)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if lMaj > cMaj {
|
||||
return true, nil
|
||||
}
|
||||
if lMaj == cMaj && lMin > cMin {
|
||||
return true, nil
|
||||
}
|
||||
if lMaj == cMaj && lMin == cMin && lPat > cPat {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func doUpdate() error {
|
||||
if GiteaURL == "" || RepoOwner == "" || RepoName == "" {
|
||||
return fmt.Errorf("GiteaURL, RepoOwner y RepoName deben estar definidos vía -ldflags")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/releases/latest", GiteaURL, RepoOwner, RepoName)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error consultando Gitea: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("Gitea respondió con código %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var release giteaRelease
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return fmt.Errorf("error parseando respuesta de Gitea: %w", err)
|
||||
}
|
||||
|
||||
newer, err := versionNewer(release.TagName, Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error comparando versiones: %w", err)
|
||||
}
|
||||
if !newer {
|
||||
fmt.Println("Ya estás en la última versión:", Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
ext = ".exe"
|
||||
}
|
||||
assetName := fmt.Sprintf("outline-mcp_%s_%s%s", runtime.GOOS, runtime.GOARCH, ext)
|
||||
|
||||
var downloadURL string
|
||||
for _, a := range release.Assets {
|
||||
if a.Name == assetName {
|
||||
downloadURL = a.BrowserDownloadURL
|
||||
break
|
||||
}
|
||||
}
|
||||
if downloadURL == "" {
|
||||
return fmt.Errorf("no se encontró asset para la plataforma %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
fmt.Printf("Actualizando de %s a %s...\n", Version, release.TagName)
|
||||
resp, err = http.Get(downloadURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error descargando asset: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("error descargando asset: código %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if err := selfupdate.Apply(resp.Body); err != nil {
|
||||
return fmt.Errorf("error aplicando actualización: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Actualizado exitosamente a %s\n", release.TagName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "version":
|
||||
fmt.Println(Version)
|
||||
return
|
||||
case "update":
|
||||
if err := doUpdate(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s := server.NewMCPServer(
|
||||
"outline-mcp",
|
||||
Version,
|
||||
server.WithToolCapabilities(true),
|
||||
)
|
||||
|
||||
s.AddTool(
|
||||
mcp.NewTool("outline_list_collections",
|
||||
mcp.WithDescription("Lista todas las colecciones de Outline con su ID, nombre y descripción"),
|
||||
),
|
||||
handleListCollections,
|
||||
)
|
||||
|
||||
s.AddTool(
|
||||
mcp.NewTool("outline_search",
|
||||
mcp.WithDescription("Busca documentos en Outline por texto"),
|
||||
mcp.WithString("query",
|
||||
mcp.Required(),
|
||||
mcp.Description("Texto de búsqueda"),
|
||||
),
|
||||
),
|
||||
handleSearch,
|
||||
)
|
||||
|
||||
s.AddTool(
|
||||
mcp.NewTool("outline_get_document",
|
||||
mcp.WithDescription("Obtiene el contenido de un documento de Outline por su ID"),
|
||||
mcp.WithString("id",
|
||||
mcp.Required(),
|
||||
mcp.Description("ID del documento"),
|
||||
),
|
||||
),
|
||||
handleGetDocument,
|
||||
)
|
||||
|
||||
s.AddTool(
|
||||
mcp.NewTool("outline_create_document",
|
||||
mcp.WithDescription("Crea un nuevo documento en Outline"),
|
||||
mcp.WithString("title",
|
||||
mcp.Required(),
|
||||
mcp.Description("Título del documento"),
|
||||
),
|
||||
mcp.WithString("text",
|
||||
mcp.Required(),
|
||||
mcp.Description("Contenido del documento en Markdown"),
|
||||
),
|
||||
mcp.WithString("collection_id",
|
||||
mcp.Required(),
|
||||
mcp.Description("ID de la colección destino"),
|
||||
),
|
||||
),
|
||||
handleCreateDocument,
|
||||
)
|
||||
|
||||
if err := server.ServeStdio(s); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user