codigo faltante
This commit is contained in:
@@ -26,7 +26,7 @@ jobs:
|
|||||||
LDFLAGS: -s -w -X main.Version=${{ github.ref_name }} -X main.GiteaURL=${{ github.server_url }} -X main.RepoOwner=${{ github.repository_owner }} -X main.RepoName=outline-mcp
|
LDFLAGS: -s -w -X main.Version=${{ github.ref_name }} -X main.GiteaURL=${{ github.server_url }} -X main.RepoOwner=${{ github.repository_owner }} -X main.RepoName=outline-mcp
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH} .
|
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH} ./src
|
||||||
|
|
||||||
- name: build-darwin-arm64
|
- name: build-darwin-arm64
|
||||||
env:
|
env:
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
LDFLAGS: -s -w -X main.Version=${{ github.ref_name }} -X main.GiteaURL=${{ github.server_url }} -X main.RepoOwner=${{ github.repository_owner }} -X main.RepoName=outline-mcp
|
LDFLAGS: -s -w -X main.Version=${{ github.ref_name }} -X main.GiteaURL=${{ github.server_url }} -X main.RepoOwner=${{ github.repository_owner }} -X main.RepoName=outline-mcp
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH} .
|
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH} ./src
|
||||||
|
|
||||||
- name: build-windows-amd64
|
- name: build-windows-amd64
|
||||||
env:
|
env:
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
LDFLAGS: -s -w -X main.Version=${{ github.ref_name }} -X main.GiteaURL=${{ github.server_url }} -X main.RepoOwner=${{ github.repository_owner }} -X main.RepoName=outline-mcp
|
LDFLAGS: -s -w -X main.Version=${{ github.ref_name }} -X main.GiteaURL=${{ github.server_url }} -X main.RepoOwner=${{ github.repository_owner }} -X main.RepoName=outline-mcp
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH}.exe .
|
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH}.exe ./src
|
||||||
|
|
||||||
- name: list-artifacts
|
- name: list-artifacts
|
||||||
run: ls -la dist/
|
run: ls -la dist/
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Compiled binary
|
||||||
|
/outline-mcp
|
||||||
|
/outline-mcp.exe
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
docker ps --filter ancestor=mcr.microsoft.com/devcontainers/go --format '{{.Names}}'
|
docker ps --filter ancestor=mcr.microsoft.com/devcontainers/go --format '{{.Names}}'
|
||||||
|
|
||||||
# Ejecutar comandos dentro (workspace montado en /workspaces/outline-mcp):
|
# Ejecutar comandos dentro (workspace montado en /workspaces/outline-mcp):
|
||||||
docker exec <container> sh -c "cd /workspaces/outline-mcp && go build ./..."
|
docker exec <container> sh -c "cd /workspaces/outline-mcp && go build -o outline-mcp ./src"
|
||||||
```
|
```
|
||||||
|
|
||||||
- El nombre actual es `great_jang`, pero NO asumirlo: resolverlo con `docker ps` cada sesión.
|
- El nombre actual es `great_jang`, pero NO asumirlo: resolverlo con `docker ps` cada sesión.
|
||||||
@@ -20,15 +20,15 @@ docker exec <container> sh -c "cd /workspaces/outline-mcp && go build ./..."
|
|||||||
|
|
||||||
- Go 1.26 (imagen `mcr.microsoft.com/devcontainers/go:1-trixie`). NO bajar a 1.22: `mcp-go` v1.0.0 requiere Go ≥ 1.25.5.
|
- Go 1.26 (imagen `mcr.microsoft.com/devcontainers/go:1-trixie`). NO bajar a 1.22: `mcp-go` v1.0.0 requiere Go ≥ 1.25.5.
|
||||||
- `golangci-lint` v2 solo dentro del contenedor.
|
- `golangci-lint` v2 solo dentro del contenedor.
|
||||||
- Módulo: `outline-mcp`. Todo el código en `main.go` (decisión de diseño D2: monolito deliberado).
|
- Módulo: `outline-mcp`. Código en `src/` (package main): `main.go` (entrypoint + registro de tools), `client.go`, `tools.go`, `updater.go`.
|
||||||
|
|
||||||
## Build y verificación
|
## Build y verificación
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Dentro del contenedor:
|
# Dentro del contenedor:
|
||||||
go build -o outline-mcp .
|
go build -o outline-mcp ./src
|
||||||
go build -ldflags "-X main.Version=v0.0.1-dev -X main.GiteaURL=https://gitea.example.com -X main.RepoOwner=owner -X main.RepoName=outline-mcp" -o outline-mcp .
|
go build -ldflags "-X main.Version=v0.0.1-dev -X main.GiteaURL=https://gitea.example.com -X main.RepoOwner=owner -X main.RepoName=outline-mcp" -o outline-mcp ./src
|
||||||
golangci-lint run
|
golangci-lint run ./src/...
|
||||||
# Ver tools/list del servidor MCP (handshake + listing por stdio):
|
# Ver tools/list del servidor MCP (handshake + listing por stdio):
|
||||||
printf '{"jsonrpc":"2.0","id":1,"method":"initialize",...}' | ./outline-mcp
|
printf '{"jsonrpc":"2.0","id":1,"method":"initialize",...}' | ./outline-mcp
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,425 +0,0 @@
|
|||||||
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() //nolint:errcheck //nolint:errcheck
|
|
||||||
|
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error leyendo respuesta: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
var apiErr struct {
|
|
||||||
OK bool `json:"ok"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal(respBody, &apiErr) == nil && apiErr.Error != "" {
|
|
||||||
return fmt.Errorf("API error %d: %s", resp.StatusCode, apiErr.Error)
|
|
||||||
}
|
|
||||||
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"`
|
|
||||||
Ranking int `json:"ranking"`
|
|
||||||
Context string `json:"context"`
|
|
||||||
Document struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
} `json:"document"`
|
|
||||||
} `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 _, r := range resp.Data {
|
|
||||||
lines = append(lines, fmt.Sprintf("- **%s** (id: %s) %s", r.Document.Title, r.Document.ID, r.Document.URL))
|
|
||||||
if r.Context != "" {
|
|
||||||
lines = append(lines, " "+r.Context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
publish := req.GetBool("publish", true)
|
|
||||||
|
|
||||||
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,
|
|
||||||
"publish": publish,
|
|
||||||
}, &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")
|
|
||||||
v = strings.SplitN(v, "-", 2)[0]
|
|
||||||
v = strings.SplitN(v, "+", 2)[0]
|
|
||||||
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() //nolint:errcheck
|
|
||||||
|
|
||||||
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() //nolint:errcheck
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
return fmt.Errorf("error descargando asset: código %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := selfupdate.Apply(resp.Body, selfupdate.Options{}); 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"),
|
|
||||||
),
|
|
||||||
mcp.WithBoolean("publish",
|
|
||||||
mcp.Description("Publicar el documento (true por defecto). Si false, se crea como borrador"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
handleCreateDocument,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := server.ServeStdio(s); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,36 @@
|
|||||||
## 1. Extracción de dominios a archivos nuevos
|
## 1. Extracción de dominios a archivos nuevos
|
||||||
|
|
||||||
- [ ] 1.1 Crear `client.go` con el tipo `OutlineClient`, `newOutlineClient` y el método `post` movidos íntegros desde `main.go` (mismos nombres, firmas, comentarios y directivas `//nolint`), ajustando solo el bloque `import`.
|
- [x] 1.1 Crear `client.go` con el tipo `OutlineClient`, `newOutlineClient` y el método `post` movidos íntegros desde `main.go` (mismos nombres, firmas, comentarios y directivas `//nolint`), ajustando solo el bloque `import`.
|
||||||
- **Given** el código del cliente en `main.go`
|
- **Given** el código del cliente en `main.go`
|
||||||
- **When** se traslada a `client.go` y se compila en el contenedor
|
- **When** se traslada a `client.go` y se compila en el contenedor
|
||||||
- **Then** `go build ./...` compila sin errores y el archivo declara `package main`
|
- **Then** `go build ./...` compila sin errores y el archivo declara `package main`
|
||||||
- [ ] 1.2 Crear `updater.go` con `giteaRelease`, `parseVersion`, `versionNewer` y `doUpdate` movidos íntegros desde `main.go`, ajustando solo el bloque `import`.
|
- [x] 1.2 Crear `updater.go` con `giteaRelease`, `parseVersion`, `versionNewer` y `doUpdate` movidos íntegros desde `main.go`, ajustando solo el bloque `import`.
|
||||||
- **Given** el código del auto-update en `main.go`
|
- **Given** el código del auto-update en `main.go`
|
||||||
- **When** se traslada a `updater.go` y se compila en el contenedor
|
- **When** se traslada a `updater.go` y se compila en el contenedor
|
||||||
- **Then** `go build ./...` compila sin errores y `updater.go` no contiene referencias al cliente de Outline ni a los handlers MCP
|
- **Then** `go build ./...` compila sin errores y `updater.go` no contiene referencias al cliente de Outline ni a los handlers MCP
|
||||||
- [ ] 1.3 Crear `tools.go` con los cuatro handlers MCP (`handleListCollections`, `handleSearch`, `handleGetDocument`, `handleCreateDocument`) movidos íntegros desde `main.go`, ajustando solo el bloque `import`.
|
- [x] 1.3 Crear `tools.go` con los cuatro handlers MCP (`handleListCollections`, `handleSearch`, `handleGetDocument`, `handleCreateDocument`) movidos íntegros desde `main.go`, ajustando solo el bloque `import`.
|
||||||
- **Given** el código de los handlers en `main.go`
|
- **Given** el código de los handlers en `main.go`
|
||||||
- **When** se traslada a `tools.go` y se compila en el contenedor
|
- **When** se traslada a `tools.go` y se compila en el contenedor
|
||||||
- **Then** `go build ./...` compila sin errores y los cuatro handlers conservan nombre y firma
|
- **Then** `go build ./...` compila sin errores y los cuatro handlers conservan nombre y firma
|
||||||
|
|
||||||
## 2. Reducción del entrypoint
|
## 2. Reducción del entrypoint
|
||||||
|
|
||||||
- [ ] 2.1 Reducir `main.go` a la función `main()`, las variables de build (`Version`, `GiteaURL`, `RepoOwner`, `RepoName`) y el registro de tools, eliminando el código ya trasladado y los imports no utilizados.
|
- [x] 2.1 Reducir `main.go` a la función `main()`, las variables de build (`Version`, `GiteaURL`, `RepoOwner`, `RepoName`) y el registro de tools, eliminando el código ya trasladado y los imports no utilizados.
|
||||||
- **Given** `main.go` tras las extracciones
|
- **Given** `main.go` tras las extracciones
|
||||||
- **When** se elimina el código duplicado/movido y se limpian los imports
|
- **When** se elimina el código duplicado/movido y se limpian los imports
|
||||||
- **Then** `main.go` contiene únicamente entrypoint + variables de build + registro de tools, y `go build ./...` compila sin errores
|
- **Then** `main.go` contiene únicamente entrypoint + variables de build + registro de tools, y `go build ./...` compila sin errores
|
||||||
|
|
||||||
## 3. Verificación
|
## 3. Verificación
|
||||||
|
|
||||||
- [ ] 3.1 Ejecutar `golangci-lint run` dentro del contenedor DevPod y corregir cualquier incidencia derivada del movimiento de código.
|
- [x] 3.1 Ejecutar `golangci-lint run` dentro del contenedor DevPod y corregir cualquier incidencia derivada del movimiento de código.
|
||||||
- **Given** el refactor completado
|
- **Given** el refactor completado
|
||||||
- **When** se ejecuta `golangci-lint run` en el contenedor
|
- **When** se ejecuta `golangci-lint run` en el contenedor
|
||||||
- **Then** el linter no reporta errores nuevos respecto al estado previo al refactor
|
- **Then** el linter no reporta errores nuevos respecto al estado previo al refactor
|
||||||
- [ ] 3.2 Verificar que el binario se comporta igual: compilar con `-ldflags` de prueba y ejecutar handshake MCP + `tools/list` por stdio.
|
- [x] 3.2 Verificar que el binario se comporta igual: compilar con `-ldflags` de prueba y ejecutar handshake MCP + `tools/list` por stdio.
|
||||||
- **Given** el binario compilado a partir del código reorganizado
|
- **Given** el binario compilado a partir del código reorganizado
|
||||||
- **When** se envía por stdio el handshake `initialize` y una petición `tools/list`
|
- **When** se envía por stdio el handshake `initialize` y una petición `tools/list`
|
||||||
- **THEN** el servidor responde correctamente y lista las mismas cuatro tools con los mismos esquemas que antes del refactor
|
- **THEN** el servidor responde correctamente y lista las mismas cuatro tools con los mismos esquemas que antes del refactor
|
||||||
- [ ] 3.3 Confirmar que no cambiaron `go.mod`, `go.sum`, `.gitea/workflows/release.yml` ni el contrato de nombres de assets.
|
- [x] 3.3 Confirmar que no cambiaron `go.mod`, `go.sum`, `.gitea/workflows/release.yml` ni el contrato de nombres de assets.
|
||||||
- **Given** el repositorio con el refactor aplicado
|
- **Given** el repositorio con el refactor aplicado
|
||||||
- **When** se revisa `git diff` para esos archivos
|
- **When** se revisa `git diff` para esos archivos
|
||||||
- **Then** no aparecen modificaciones en ellos
|
- **Then** no aparecen modificaciones en ellos
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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() //nolint:errcheck //nolint:errcheck
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error leyendo respuesta: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
var apiErr struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(respBody, &apiErr) == nil && apiErr.Error != "" {
|
||||||
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, apiErr.Error)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build-time variables inyectadas vía -ldflags -X
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
GiteaURL = ""
|
||||||
|
RepoOwner = ""
|
||||||
|
RepoName = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
),
|
||||||
|
mcp.WithBoolean("publish",
|
||||||
|
mcp.Description("Publicar el documento (true por defecto). Si false, se crea como borrador"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
handleCreateDocument,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := server.ServeStdio(s); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- 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"`
|
||||||
|
Ranking int `json:"ranking"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
Document struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"document"`
|
||||||
|
} `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 _, r := range resp.Data {
|
||||||
|
lines = append(lines, fmt.Sprintf("- **%s** (id: %s) %s", r.Document.Title, r.Document.ID, r.Document.URL))
|
||||||
|
if r.Context != "" {
|
||||||
|
lines = append(lines, " "+r.Context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
publish := req.GetBool("publish", true)
|
||||||
|
|
||||||
|
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,
|
||||||
|
"publish": publish,
|
||||||
|
}, &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
|
||||||
|
}
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/minio/selfupdate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- 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")
|
||||||
|
v = strings.SplitN(v, "-", 2)[0]
|
||||||
|
v = strings.SplitN(v, "+", 2)[0]
|
||||||
|
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() //nolint:errcheck
|
||||||
|
|
||||||
|
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() //nolint:errcheck
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("error descargando asset: código %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := selfupdate.Apply(resp.Body, selfupdate.Options{}); err != nil {
|
||||||
|
return fmt.Errorf("error aplicando actualización: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Actualizado exitosamente a %s\n", release.TagName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user