Inicialización del módulo Go y cambio de version de go a la 1.26
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "outline-mcp",
|
"name": "outline-mcp",
|
||||||
"image": "mcr.microsoft.com/devcontainers/go:1-1.22-bookworm",
|
"image": "mcr.microsoft.com/devcontainers/go:1-trixie",
|
||||||
"features": {},
|
"features": {},
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Ejecución de comandos Go — OBLIGATORIO vía Docker
|
||||||
|
|
||||||
|
**No hay Go ni agentes instalados en el host (macOS).** Todos los comandos Go/lint/build se ejecutan dentro del contenedor DevPod:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Resolver el nombre del contenedor (es auto-generado, puede cambiar):
|
||||||
|
docker ps --filter ancestor=mcr.microsoft.com/devcontainers/go --format '{{.Names}}'
|
||||||
|
|
||||||
|
# Ejecutar comandos dentro (workspace montado en /workspaces/outline-mcp):
|
||||||
|
docker exec <container> sh -c "cd /workspaces/outline-mcp && go build ./..."
|
||||||
|
```
|
||||||
|
|
||||||
|
- El nombre actual es `great_jang`, pero NO asumirlo: resolverlo con `docker ps` cada sesión.
|
||||||
|
- Las ediciones de archivos se hacen en el host (ruta normal del repo); solo la ejecución va por `docker exec`.
|
||||||
|
- `openspec` CLI se ejecuta en el **host** (no está en el contenedor).
|
||||||
|
|
||||||
|
## Toolchain
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
- Módulo: `outline-mcp`. Todo el código en `main.go` (decisión de diseño D2: monolito deliberado).
|
||||||
|
|
||||||
|
## Build y verificación
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dentro del contenedor:
|
||||||
|
go build -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 .
|
||||||
|
golangci-lint run
|
||||||
|
# Ver tools/list del servidor MCP (handshake + listing por stdio):
|
||||||
|
printf '{"jsonrpc":"2.0","id":1,"method":"initialize",...}' | ./outline-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Sin tests aún; verificación = `go build` + `golangci-lint run` + prueba manual por stdio.
|
||||||
|
|
||||||
|
## Contrato crítico: nombres de assets
|
||||||
|
|
||||||
|
El pipeline de release (`.gitea/workflows/release.yml`) y el auto-update (`doUpdate` en main.go) comparten un contrato implícito: los assets DEBEN llamarse `outline-mcp_{GOOS}_{GOARCH}` (con `.exe` para windows). Cambiar una parte rompe la otra.
|
||||||
|
|
||||||
|
## Convenciones del repo
|
||||||
|
|
||||||
|
- Artefactos OpenSpec (`openspec/changes/*/`) en **español**, tono formal; tasks con formato Given/When/Than (regla de `openspec/config.yaml`).
|
||||||
|
- Flujo: `openspec new change` → proposal → specs → design → tasks → implementar marcando checkboxes en `tasks.md`.
|
||||||
|
- Errores de configuración (`OUTLINE_URL`, `OUTLINE_API_KEY`) se devuelven como resultado MCP de error, nunca panic.
|
||||||
|
- La API key de Outline nunca se loguea.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module outline-mcp
|
||||||
|
|
||||||
|
go 1.26
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mark3labs/mcp-go v1.0.0
|
||||||
|
github.com/minio/selfupdate v0.6.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
aead.dev/minisign v0.2.0 // indirect
|
||||||
|
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||||
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b // indirect
|
||||||
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
|
||||||
|
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
||||||
|
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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
||||||
|
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||||
|
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 v1.0.0 h1:CZqCzXwUiTOstkIdW1MyOZGuM+LaKMEJSu+ZYluN4DU=
|
||||||
|
github.com/mark3labs/mcp-go v1.0.0/go.mod h1:r2fW4o3wsoJ7IMsx1Wuq5xeP8PRGXPDfNveoGAYbb/s=
|
||||||
|
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
|
||||||
|
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
||||||
|
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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||||
|
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||||
|
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
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=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||||
|
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
|
||||||
|
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ El repositorio está vacío: el proyecto `outline-mcp` se construye desde cero.
|
|||||||
|
|
||||||
### D1: SDK de MCP — `github.com/mark3labs/mcp-go`
|
### D1: SDK de MCP — `github.com/mark3labs/mcp-go`
|
||||||
|
|
||||||
- **Elección**: usar `mcp-go` (paquetes `mcp` y `server`) con su `server.NewMCPServer` y transporte `ServeStdio`.
|
- **Elección**: usar `mcp-go` v1.0.0 (paquetes `mcp` y `server`) con su `server.NewMCPServer` y transporte `ServeStdio`.
|
||||||
- **Alternativa**: implementar el protocolo JSON-RPC manualmente. Se descarta por coste y riesgo; el SDK ya resuelve handshake, esquemas de herramientas y serialización.
|
- **Alternativa**: implementar el protocolo JSON-RPC manualmente. Se descarta por coste y riesgo; el SDK ya resuelve handshake, esquemas de herramientas y serialización.
|
||||||
- **Nota**: `mcp-go` permite declarar esquemas de entrada tipados (`mcp.WithString(...)`, `mcp.Required()`) que se traducen a JSON Schema para el cliente.
|
- **Nota**: `mcp-go` permite declarar esquemas de entrada tipados (`mcp.WithString(...)`, `mcp.Required()`) que se traducen a JSON Schema para el cliente.
|
||||||
|
|
||||||
@@ -58,12 +58,12 @@ El repositorio está vacío: el proyecto `outline-mcp` se construye desde cero.
|
|||||||
|
|
||||||
### D6: Pipeline de release
|
### D6: Pipeline de release
|
||||||
|
|
||||||
- **Elección**: workflow en `.gitea/workflows/release.yml` con `on: push: tags: ['v*']`, runner `ubuntu-latest`, setup de Go 1.22 (action `actions/setup-go@v5`, compatible con Gitea Actions), bucle de build sobre las 3 plataformas con `CGO_ENABLED=0` y `ldflags` inyectando `main.Version` (desde `github.ref_name`), `main.GiteaURL`, `main.RepoOwner`, `main.RepoName` (desde `github.server_url` y el contexto del repo). Publicación con la acción `akkuman/gitea-release-action` o el CLI `tea release create` como fallback.
|
- **Elección**: workflow en `.gitea/workflows/release.yml` con `on: push: tags: ['v*']`, runner `ubuntu-latest`, setup de Go 1.26 (action `actions/setup-go@v5`, compatible con Gitea Actions), bucle de build sobre las 3 plataformas con `CGO_ENABLED=0` y `ldflags` inyectando `main.Version` (desde `github.ref_name`), `main.GiteaURL`, `main.RepoOwner`, `main.RepoName` (desde `github.server_url` y el contexto del repo). Publicación con la acción `akkuman/gitea-release-action` o el CLI `tea release create` como fallback.
|
||||||
- **Alternativa**: generar los binarios en Docker dentro del pipeline. Se descarta: el runner de Gitea ya provee el toolchain vía setup-go y es más rápido.
|
- **Alternativa**: generar los binarios en Docker dentro del pipeline. Se descarta: el runner de Gitea ya provee el toolchain vía setup-go y es más rápido.
|
||||||
|
|
||||||
### D7: Devcontainer
|
### D7: Devcontainer
|
||||||
|
|
||||||
- **Elección**: imagen `mcr.microsoft.com/devcontainers/go:1-1.22-bookworm` con:
|
- **Elección**: imagen `mcr.microsoft.com/devcontainers/go:1-trixie` (Go 1.26) con:
|
||||||
- Features: `ghcr.io/devcontainers/features/golang:1` no es necesaria (la imagen ya incluye Go); se añade la feature `docker-in-docker` opcional no requerida — se omite para minimizar superficie.
|
- Features: `ghcr.io/devcontainers/features/golang:1` no es necesaria (la imagen ya incluye Go); se añade la feature `docker-in-docker` opcional no requerida — se omite para minimizar superficie.
|
||||||
- `postCreateCommand`: instalación de `golangci-lint` vía script oficial y `go mod download` si existe `go.mod`.
|
- `postCreateCommand`: instalación de `golangci-lint` vía script oficial y `go mod download` si existe `go.mod`.
|
||||||
- Extensiones: `golang.go` (Go oficial, incluye gopls), `eamodio.gitlens`.
|
- Extensiones: `golang.go` (Go oficial, incluye gopls), `eamodio.gitlens`.
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
## ADDED Requirements
|
## ADDED Requirements
|
||||||
|
|
||||||
### Requirement: Contenedor de desarrollo con Go 1.22
|
### Requirement: Contenedor de desarrollo con Go 1.26
|
||||||
El sistema SHALL incluir un devcontainer basado en la imagen oficial `mcr.microsoft.com/devcontainers/go:1-1.22-bookworm` que permita desarrollar el proyecto sin Go instalado localmente.
|
El sistema SHALL incluir un devcontainer basado en la imagen oficial `mcr.microsoft.com/devcontainers/go:1-trixie` que permita desarrollar el proyecto sin Go instalado localmente.
|
||||||
|
|
||||||
#### Scenario: Apertura del proyecto en el contenedor
|
#### Scenario: Apertura del proyecto en el contenedor
|
||||||
- **WHEN** el usuario abre el proyecto en DevPod o Dev Containers
|
- **WHEN** el usuario abre el proyecto en DevPod o Dev Containers
|
||||||
- **THEN** el contenedor se construye a partir de la imagen oficial de Go 1.22 y dispone del toolchain necesario para compilar y ejecutar el proyecto
|
- **THEN** el contenedor se construye a partir de la imagen oficial de Go 1.26 y dispone del toolchain necesario para compilar y ejecutar el proyecto
|
||||||
|
|
||||||
### Requirement: Tooling del editor preconfigurado
|
### Requirement: Tooling del editor preconfigurado
|
||||||
El devcontainer SHALL preconfigurar el soporte de `gopls` (language server de Go) e instalar `golangci-lint`, además de las extensiones básicas de Go para el editor.
|
El devcontainer SHALL preconfigurar el soporte de `gopls` (language server de Go) e instalar `golangci-lint`, además de las extensiones básicas de Go para el editor.
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ El pipeline de release SHALL ejecutarse cuando se realice push de tags cuyo nomb
|
|||||||
- **THEN** el workflow de release no se ejecuta
|
- **THEN** el workflow de release no se ejecuta
|
||||||
|
|
||||||
### Requirement: Entorno de compilación
|
### Requirement: Entorno de compilación
|
||||||
El pipeline SHALL utilizar una imagen de Ubuntu y SHALL configurar Go 1.22 como versión del toolchain antes de compilar.
|
El pipeline SHALL utilizar una imagen de Ubuntu y SHALL configurar Go 1.26 como versión del toolchain antes de compilar.
|
||||||
|
|
||||||
#### Scenario: Job de build
|
#### Scenario: Job de build
|
||||||
- **WHEN** el pipeline se ejecuta
|
- **WHEN** el pipeline se ejecuta
|
||||||
- **THEN** el entorno de build dispone de Go 1.22 instalado sobre una imagen Ubuntu
|
- **THEN** el entorno de build dispone de Go 1.26 instalado sobre una imagen Ubuntu
|
||||||
|
|
||||||
### Requirement: Inyección de variables en la compilación
|
### Requirement: Inyección de variables en la compilación
|
||||||
El pipeline SHALL inyectar en tiempo de compilación las variables `Version`, `GiteaURL`, `RepoOwner` y `RepoName` mediante `-ldflags`, derivadas del tag y del repositorio.
|
El pipeline SHALL inyectar en tiempo de compilación las variables `Version`, `GiteaURL`, `RepoOwner` y `RepoName` mediante `-ldflags`, derivadas del tag y del repositorio.
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
## 1. Entorno de desarrollo (devcontainer)
|
## 1. Entorno de desarrollo (devcontainer)
|
||||||
|
|
||||||
- [x] 1.1 Crear `.devcontainer/devcontainer.json` con la imagen `mcr.microsoft.com/devcontainers/go:1-1.22-bookworm`
|
- [x] 1.1 Crear `.devcontainer/devcontainer.json` con la imagen `mcr.microsoft.com/devcontainers/go:1-trixie`
|
||||||
- [x] 1.2 Configurar extensiones del editor (`golang.go` con gopls) y settings (`go.useLanguageServer: true`)
|
- [x] 1.2 Configurar extensiones del editor (`golang.go` con gopls) y settings (`go.useLanguageServer: true`)
|
||||||
- [x] 1.3 Añadir `postCreateCommand` que instale `golangci-lint` y ejecute `go mod download` cuando exista `go.mod`
|
- [x] 1.3 Añadir `postCreateCommand` que instale `golangci-lint` y ejecute `go mod download` cuando exista `go.mod`
|
||||||
- [x] 1.4 Verificar: Given el proyecto abierto en DevPod/Dev Containers, When el contenedor se construye, Then dispone de Go 1.22, gopls y golangci-lint operativos
|
- [x] 1.4 Verificar: Given el proyecto abierto en DevPod/Dev Containers, When el contenedor se construye, Then dispone de Go 1.22, gopls y golangci-lint operativos
|
||||||
|
|
||||||
## 2. Inicialización del módulo Go
|
## 2. Inicialización del módulo Go
|
||||||
|
|
||||||
- [ ] 2.1 Ejecutar `go mod init outline-mcp` dentro del contenedor
|
- [x] 2.1 Ejecutar `go mod init outline-mcp` dentro del contenedor
|
||||||
- [ ] 2.2 Instalar dependencias: `go get github.com/mark3labs/mcp-go/mcp github.com/mark3labs/mcp-go/server`
|
- [x] 2.2 Instalar dependencias: `go get github.com/mark3labs/mcp-go/mcp github.com/mark3labs/mcp-go/server`
|
||||||
- [ ] 2.3 Instalar la librería de auto-update: `go get github.com/minio/selfupdate`
|
- [x] 2.3 Instalar la librería de auto-update: `go get github.com/minio/selfupdate`
|
||||||
- [ ] 2.4 Verificar: Given `go.mod` creado, When se ejecuta `go mod tidy`, Then no hay errores y `go.sum` queda generado
|
- [x] 2.4 Verificar: Given `go.mod` creado, When se ejecuta `go mod tidy`, Then no hay errores y `go.sum` queda generado
|
||||||
|
|
||||||
## 3. CLI y esqueleto del servidor MCP
|
## 3. CLI y esqueleto del servidor MCP
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user