feat: agregar auto update
spec: separar responsabilidades feat: agregra pipeline de construccion en gitea
This commit is contained in:
@@ -0,0 +1,95 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: build-binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: setup-go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
|
||||||
|
- name: build-linux-amd64
|
||||||
|
env:
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
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: |
|
||||||
|
mkdir -p dist
|
||||||
|
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH} .
|
||||||
|
|
||||||
|
- name: build-darwin-arm64
|
||||||
|
env:
|
||||||
|
GOOS: darwin
|
||||||
|
GOARCH: arm64
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
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: |
|
||||||
|
mkdir -p dist
|
||||||
|
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH} .
|
||||||
|
|
||||||
|
- name: build-windows-amd64
|
||||||
|
env:
|
||||||
|
GOOS: windows
|
||||||
|
GOARCH: amd64
|
||||||
|
CGO_ENABLED: '0'
|
||||||
|
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: |
|
||||||
|
mkdir -p dist
|
||||||
|
go build -buildvcs=false -ldflags "$LDFLAGS" -o dist/outline-mcp_${GOOS}_${GOARCH}.exe .
|
||||||
|
|
||||||
|
- name: list-artifacts
|
||||||
|
run: ls -la dist/
|
||||||
|
|
||||||
|
- name: upload-assets
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binaries
|
||||||
|
path: dist/*
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: publish-release
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: download-binaries
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: binaries
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: list-downloads
|
||||||
|
run: ls -la dist/
|
||||||
|
|
||||||
|
- name: install-tea
|
||||||
|
run: |
|
||||||
|
curl -sL https://dl.gitea.com/tea/0.10.0/tea-0.10.0-linux-amd64 -o /usr/local/bin/tea
|
||||||
|
chmod +x /usr/local/bin/tea
|
||||||
|
tea --version
|
||||||
|
|
||||||
|
- name: publish-release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
REPO_FULL: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
tea release create \
|
||||||
|
--repo "${REPO_FULL}" \
|
||||||
|
--tag "${{ github.ref_name }}" \
|
||||||
|
--title "${{ github.ref_name }}" \
|
||||||
|
--note "Release ${{ github.ref_name }}" \
|
||||||
|
--asset dist/outline-mcp_linux_amd64 \
|
||||||
|
--asset dist/outline-mcp_darwin_arm64 \
|
||||||
|
--asset dist/outline-mcp_windows_amd64.exe
|
||||||
@@ -64,7 +64,7 @@ func (c *OutlineClient) post(ctx context.Context, path string, payload any, resu
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error ejecutando petición: %w", err)
|
return fmt.Errorf("error ejecutando petición: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close() //nolint:errcheck //nolint:errcheck
|
||||||
|
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,6 +72,13 @@ func (c *OutlineClient) post(ctx context.Context, path string, payload any, resu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
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))
|
return fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,9 +132,14 @@ func handleSearch(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
|
|||||||
|
|
||||||
var resp struct {
|
var resp struct {
|
||||||
Data []struct {
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Ranking int `json:"ranking"`
|
||||||
|
Context string `json:"context"`
|
||||||
|
Document struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
|
} `json:"document"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
if err := client.post(ctx, "/api/documents.search", map[string]any{"query": query}, &resp); err != nil {
|
if err := client.post(ctx, "/api/documents.search", map[string]any{"query": query}, &resp); err != nil {
|
||||||
@@ -139,8 +151,11 @@ func handleSearch(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
for _, d := range resp.Data {
|
for _, r := range resp.Data {
|
||||||
lines = append(lines, fmt.Sprintf("- **%s** (id: %s) %s", d.Title, d.ID, d.URL))
|
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
|
return mcp.NewToolResultText(strings.Join(lines, "\n")), nil
|
||||||
}
|
}
|
||||||
@@ -189,6 +204,8 @@ func handleCreateDocument(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
|
|||||||
return mcp.NewToolResultError("Los parámetros title, text y collection_id son requeridos."), nil
|
return mcp.NewToolResultError("Los parámetros title, text y collection_id son requeridos."), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publish := req.GetBool("publish", true)
|
||||||
|
|
||||||
var resp struct {
|
var resp struct {
|
||||||
Data struct {
|
Data struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@@ -199,6 +216,7 @@ func handleCreateDocument(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
|
|||||||
"title": title,
|
"title": title,
|
||||||
"text": text,
|
"text": text,
|
||||||
"collectionId": collectionID,
|
"collectionId": collectionID,
|
||||||
|
"publish": publish,
|
||||||
}, &resp); err != nil {
|
}, &resp); err != nil {
|
||||||
return mcp.NewToolResultError(err.Error()), nil
|
return mcp.NewToolResultError(err.Error()), nil
|
||||||
}
|
}
|
||||||
@@ -218,6 +236,8 @@ type giteaRelease struct {
|
|||||||
|
|
||||||
func parseVersion(v string) (int, int, int, error) {
|
func parseVersion(v string) (int, int, int, error) {
|
||||||
v = strings.TrimPrefix(v, "v")
|
v = strings.TrimPrefix(v, "v")
|
||||||
|
v = strings.SplitN(v, "-", 2)[0]
|
||||||
|
v = strings.SplitN(v, "+", 2)[0]
|
||||||
parts := strings.SplitN(v, ".", 3)
|
parts := strings.SplitN(v, ".", 3)
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
return 0, 0, 0, fmt.Errorf("formato de versión inválido: %s", v)
|
return 0, 0, 0, fmt.Errorf("formato de versión inválido: %s", v)
|
||||||
@@ -268,10 +288,10 @@ func doUpdate() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error consultando Gitea: %w", err)
|
return fmt.Errorf("error consultando Gitea: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close() //nolint:errcheck
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return fmt.Errorf("Gitea respondió con código %d", resp.StatusCode)
|
return fmt.Errorf("gitea respondió con código %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
var release giteaRelease
|
var release giteaRelease
|
||||||
@@ -310,13 +330,13 @@ func doUpdate() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error descargando asset: %w", err)
|
return fmt.Errorf("error descargando asset: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close() //nolint:errcheck
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return fmt.Errorf("error descargando asset: código %d", resp.StatusCode)
|
return fmt.Errorf("error descargando asset: código %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := selfupdate.Apply(resp.Body); err != nil {
|
if err := selfupdate.Apply(resp.Body, selfupdate.Options{}); err != nil {
|
||||||
return fmt.Errorf("error aplicando actualización: %w", err)
|
return fmt.Errorf("error aplicando actualización: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +411,9 @@ func main() {
|
|||||||
mcp.Required(),
|
mcp.Required(),
|
||||||
mcp.Description("ID de la colección destino"),
|
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,
|
handleCreateDocument,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,38 +16,38 @@
|
|||||||
|
|
||||||
## 3. CLI y esqueleto del servidor MCP
|
## 3. CLI y esqueleto del servidor MCP
|
||||||
|
|
||||||
- [ ] 3.1 Crear `main.go` con variables globales inyectables (`Version`, `GiteaURL`, `RepoOwner`, `RepoName`) y parsing de argumentos (`version`, `update`; sin argumentos → servidor MCP)
|
- [x] 3.1 Crear `main.go` con variables globales inyectables (`Version`, `GiteaURL`, `RepoOwner`, `RepoName`) y parsing de argumentos (`version`, `update`; sin argumentos → servidor MCP)
|
||||||
- [ ] 3.2 Implementar el comando `version` que imprime la versión compilada
|
- [x] 3.2 Implementar el comando `version` que imprime la versión compilada
|
||||||
- [ ] 3.3 Arrancar el servidor MCP con `server.NewMCPServer` y transporte stdio (`ServeStdio`)
|
- [x] 3.3 Arrancar el servidor MCP con `server.NewMCPServer` y transporte stdio (`ServeStdio`)
|
||||||
- [ ] 3.4 Verificar: Given el binario compilado con `-ldflags -X main.Version=v0.0.1-dev`, When se ejecuta `version`, Then imprime `v0.0.1-dev`; When se ejecuta sin argumentos, Then el proceso queda a la espera en stdio
|
- [x] 3.4 Verificar: Given el binario compilado con `-ldflags -X main.Version=v0.0.1-dev`, When se ejecuta `version`, Then imprime `v0.0.1-dev`; When se ejecuta sin argumentos, Then el proceso queda a la espera en stdio
|
||||||
|
|
||||||
## 4. Cliente HTTP de Outline
|
## 4. Cliente HTTP de Outline
|
||||||
|
|
||||||
- [ ] 4.1 Implementar el struct `OutlineClient` configurado desde `OUTLINE_URL` y `OUTLINE_API_KEY`
|
- [x] 4.1 Implementar el struct `OutlineClient` configurado desde `OUTLINE_URL` y `OUTLINE_API_KEY`
|
||||||
- [ ] 4.2 Implementar método genérico `post(ctx, path, payload, result)` que serialice JSON, incluya el header `Authorization: Bearer <TOKEN>` y propague errores HTTP con el mensaje de la API
|
- [x] 4.2 Implementar método genérico `post(ctx, path, payload, result)` que serialice JSON, incluya el header `Authorization: Bearer <TOKEN>` y propague errores HTTP con el mensaje de la API
|
||||||
- [ ] 4.3 Verificar: Given `OUTLINE_URL`/`OUTLINE_API_KEY` ausentes, When se invoca una herramienta, Then se responde con error descriptivo sin panic
|
- [x] 4.3 Verificar: Given `OUTLINE_URL`/`OUTLINE_API_KEY` ausentes, When se invoca una herramienta, Then se responde con error descriptivo sin panic
|
||||||
|
|
||||||
## 5. Herramientas MCP
|
## 5. Herramientas MCP
|
||||||
|
|
||||||
- [ ] 5.1 Implementar `outline_list_collections` contra `/api/collections.list` devolviendo `id`, `name` y `description`
|
- [x] 5.1 Implementar `outline_list_collections` contra `/api/collections.list` devolviendo `id`, `name` y `description`
|
||||||
- [ ] 5.2 Implementar `outline_search` contra `/api/documents.search` con parámetro `query`
|
- [x] 5.2 Implementar `outline_search` contra `/api/documents.search` con parámetro `query`
|
||||||
- [ ] 5.3 Implementar `outline_get_document` contra `/api/documents.info` con parámetro `id`, devolviendo título y texto en Markdown
|
- [x] 5.3 Implementar `outline_get_document` contra `/api/documents.info` con parámetro `id`, devolviendo título y texto en Markdown
|
||||||
- [ ] 5.4 Implementar `outline_create_document` contra `/api/documents.create` con parámetros `title`, `text` y `collection_id`
|
- [x] 5.4 Implementar `outline_create_document` contra `/api/documents.create` con parámetros `title`, `text` y `collection_id`
|
||||||
- [ ] 5.5 Registrar las cuatro herramientas en el servidor MCP con sus esquemas de entrada (`mcp.WithString`, `mcp.Required()`)
|
- [x] 5.5 Registrar las cuatro herramientas en el servidor MCP con sus esquemas de entrada (`mcp.WithString`, `mcp.Required()`)
|
||||||
- [ ] 5.6 Verificar: Given un cliente MCP conectado por stdio, When se lista `tools/list`, Then aparecen las cuatro herramientas con sus esquemas; When se invoca `outline_search` con query sin coincidencias, Then devuelve lista vacía sin error
|
- [x] 5.6 Verificar: Given un cliente MCP conectado por stdio, When se lista `tools/list`, Then aparecen las cuatro herramientas con sus esquemas; When se invoca `outline_search` con query sin coincidencias, Then devuelve lista vacía sin error
|
||||||
|
|
||||||
## 6. Auto-update
|
## 6. Auto-update
|
||||||
|
|
||||||
- [ ] 6.1 Implementar `doUpdate`: consulta a `{GiteaURL}/api/v1/repos/{RepoOwner}/{RepoName}/releases/latest` y manejo de errores de red
|
- [x] 6.1 Implementar `doUpdate`: consulta a `{GiteaURL}/api/v1/repos/{RepoOwner}/{RepoName}/releases/latest` y manejo de errores de red
|
||||||
- [ ] 6.2 Implementar comparación de versiones (parseo de `vX.Y.Z` frente a `Version`; fail-safe si el tag no es parseable)
|
- [x] 6.2 Implementar comparación de versiones (parseo de `vX.Y.Z` frente a `Version`; fail-safe si el tag no es parseable)
|
||||||
- [ ] 6.3 Implementar selección y descarga del asset por convención `outline-mcp_{GOOS}_{GOARCH}[.exe]` usando `runtime.GOOS`/`runtime.GOARCH`
|
- [x] 6.3 Implementar selección y descarga del asset por convención `outline-mcp_{GOOS}_{GOARCH}[.exe]` usando `runtime.GOOS`/`runtime.GOARCH`
|
||||||
- [ ] 6.4 Aplicar el binario descargado con `selfupdate.Apply` e informar el resultado
|
- [x] 6.4 Aplicar el binario descargado con `selfupdate.Apply` e informar el resultado
|
||||||
- [ ] 6.5 Verificar: Given `Version` igual o superior al tag de Gitea, When se ejecuta `update`, Then informa que no hay actualizaciones y no modifica el binario; Given la API de Gitea inaccesible, Then finaliza con error sin tocar el binario
|
- [x] 6.5 Verificar: Given `Version` igual o superior al tag de Gitea, When se ejecuta `update`, Then informa que no hay actualizaciones y no modifica el binario; Given la API de Gitea inaccesible, Then finaliza con error sin tocar el binario
|
||||||
|
|
||||||
## 7. Pipeline de release (Gitea Actions)
|
## 7. Pipeline de release (Gitea Actions)
|
||||||
|
|
||||||
- [ ] 7.1 Crear `.gitea/workflows/release.yml` con trigger `on: push: tags: ['v*']` y runner `ubuntu-latest`
|
- [x] 7.1 Crear `.gitea/workflows/release.yml` con trigger `on: push: tags: ['v*']` y runner `ubuntu-latest`
|
||||||
- [ ] 7.2 Configurar Go 1.22 en el job (`actions/setup-go@v5`)
|
- [x] 7.2 Configurar Go 1.22 en el job (`actions/setup-go@v5`)
|
||||||
- [ ] 7.3 Implementar el build con `CGO_ENABLED=0` para linux/amd64, darwin/arm64 y windows/amd64, nombrando los artefactos `outline-mcp_{GOOS}_{GOARCH}[.exe]` e inyectando `-ldflags` con `Version` (desde `github.ref_name`), `GiteaURL`, `RepoOwner` y `RepoName` (desde el contexto del repo)
|
- [x] 7.3 Implementar el build con `CGO_ENABLED=0` para linux/amd64, darwin/arm64 y windows/amd64, nombrando los artefactos `outline-mcp_{GOOS}_{GOARCH}[.exe]` e inyectando `-ldflags` con `Version` (desde `github.ref_name`), `GiteaURL`, `RepoOwner` y `RepoName` (desde el contexto del repo)
|
||||||
- [ ] 7.4 Publicar el release en Gitea adjuntando los tres binarios (acción oficial de Gitea o CLI `tea release create`)
|
- [x] 7.4 Publicar el release en Gitea adjuntando los tres binarios (acción oficial de Gitea o CLI `tea release create`)
|
||||||
- [ ] 7.5 Verificar: Given un push de tag `v0.1.0`, When el workflow se ejecuta, Then el release queda publicado en Gitea con los tres assets y el comando `version` del binario publicado reporta `v0.1.0`
|
- [x] 7.5 Verificar: Given un push de tag `v0.1.0`, When el workflow se ejecuta, Then el release queda publicado en Gitea con los tres assets y el comando `version` del binario publicado reporta `v0.1.0`
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-09-02
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Design: split-main-into-files
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
El proyecto `outline-mcp` vive íntegramente en `main.go` (425 líneas, paquete `main`), según la decisión D2 del change `create-outline-mcp-server`, que explícitamente anticipaba la extracción a múltiples archivos como refactor futuro. El archivo mezcla tres dominios independientes (cliente HTTP de Outline, handlers de tools MCP, auto-update vía Gitea). No hay tests aún; la verificación actual es `go build`, `golangci-lint run` y prueba manual por stdio.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Repartir `main.go` en archivos por responsabilidad dentro del mismo paquete `main`, sin ningún cambio de comportamiento.
|
||||||
|
- Dejar el código preparado para añadir tests y nuevas tools sin fricción.
|
||||||
|
- Cumplir la promesa de D2: extracción sin impacto en specs de comportamiento ni en el pipeline de release.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Introducir paquetes `internal/` (descartado: no hay necesidad de ocultar API entre dominios con el alcance actual).
|
||||||
|
- Añadir tests (cambio posterior; solo se deja el terreno listo).
|
||||||
|
- Renombrar o refactorizar funciones, tipos o lógica.
|
||||||
|
- Modificar `go.mod`, dependencias, pipeline de release o contrato de nombres de assets.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1: Múltiples archivos en paquete `main` (opción B) frente a `internal/` (opción C)
|
||||||
|
|
||||||
|
- **Elección**: dividir en archivos del mismo paquete `main`. Estructura resultante:
|
||||||
|
- `main.go` — función `main()`, variables de build (`Version`, `GiteaURL`, `RepoOwner`, `RepoName`) y registro de tools.
|
||||||
|
- `client.go` — tipo `OutlineClient`, `newOutlineClient`, método `post`.
|
||||||
|
- `tools.go` — los cuatro handlers de tools MCP.
|
||||||
|
- `updater.go` — `giteaRelease`, `parseVersion`, `versionNewer`, `doUpdate`.
|
||||||
|
- **Alternativa** (`internal/outline`, `internal/tools`, `internal/selfupdate`): descartada porque obliga a decidir superficie exportada acoplando los handlers al SDK (`mcp.CallToolResult` en firmas) y no aporta ocultación real con el alcance actual. Queda como refactor futuro si el número de tools crece de forma significativa.
|
||||||
|
- **Alternativa** (mantener un solo archivo): descartada; el argumento de bootstrap de D2 ya se cumplió y la mezcla de dominios penaliza la legibilidad y los futuros diffs.
|
||||||
|
|
||||||
|
### D2 (revisión): se actualiza la decisión del change `create-outline-mcp-server`
|
||||||
|
|
||||||
|
La D2 original ("todo el código en `main.go`") queda sustituida por esta organización multi-archivo en paquete único. El carácter de monolito deliberado (un único binario, un único paquete) se mantiene; lo que cambia es la distribución física en archivos.
|
||||||
|
|
||||||
|
### D3: Movimiento mecánico de código, sin reescritura
|
||||||
|
|
||||||
|
Las funciones y tipos se trasladan íntegras (mismos nombres, firmas, comentarios y directivas `//nolint`). Solo se ajustan los bloques `import` de cada archivo nuevo. Esto minimiza el riesgo del refactor y hace el diff auditable como puro movimiento.
|
||||||
|
|
||||||
|
### D4: Las variables de build permanecen en `main.go`
|
||||||
|
|
||||||
|
`Version`, `GiteaURL`, `RepoOwner` y `RepoName` se inyectan con `-ldflags -X main.Version=...` y el paquete sigue siendo `main`, por lo que no hay razón para moverlas. Permanecen junto al entrypoint, que es su punto natural de referencia.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [División incorrecta de imports causa fallo de compilación] → Verificación inmediata con `go build ./...` en el contenedor tras cada movimiento; es un error de compilación, no un error silencioso.
|
||||||
|
- [`golangci-lint` señala imports sin usar o duplicados] → Ejecutar `golangci-lint run` en el contenedor como parte de la verificación.
|
||||||
|
- [Confusión temporal por familiaridad con el archivo único] → El diff es puro movimiento de código; los nombres de archivo (`client.go`, `tools.go`, `updater.go`) se corresponden directamente con los dominios.
|
||||||
|
- [Regresión funcional no detectada por compilación] → Prueba manual por stdio del servidor MCP (handshake + tools/list) tras el refactor, igual que en el change original.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Crear los tres archivos nuevos moviendo bloques completos desde `main.go`.
|
||||||
|
2. Reducir `main.go` a entrypoint + variables de build + registro de tools.
|
||||||
|
3. Compilar, lintear y probar por stdio en el contenedor DevPod.
|
||||||
|
4. Rollback trivial: `git revert` del commit único del refactor.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
(ninguna)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Proposal: split-main-into-files
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Todo el código del servidor reside en un único archivo `main.go` (425 líneas) que mezcla tres responsabilidades sin relación entre sí: el cliente HTTP de Outline, los handlers de tools MCP y el mecanismo de auto-update. La decisión de diseño D2 (arquitectura de un solo archivo) se tomó para facilitar el bootstrap del proyecto; ese objetivo ya se cumplió y la pendiente de crecimiento (cada tool nueva agrega ~35-40 líneas) hará el archivo difícil de mantener y de testear a corto plazo.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Dividir `main.go` en varios archivos dentro del **mismo paquete `main`** (sin introducir paquetes `internal/`):
|
||||||
|
- `main.go`: únicamente la función `main()` (wiring del servidor MCP y registro de tools) y las variables inyectadas por `-ldflags`.
|
||||||
|
- `client.go`: tipo `OutlineClient`, constructor `newOutlineClient` y método `post`.
|
||||||
|
- `tools.go`: los cuatro handlers de tools MCP (`handleListCollections`, `handleSearch`, `handleGetDocument`, `handleCreateDocument`).
|
||||||
|
- `updater.go`: `giteaRelease`, `parseVersion`, `versionNewer` y `doUpdate`.
|
||||||
|
- Se trata de un refactor mecánico: se mueve código, no se modifica. No hay cambios de comportamiento, de API MCP ni de contrato de nombres de assets (`outline-mcp_{GOOS}_{GOARCH}`).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `code-structure`: Convención de organización del código fuente del proyecto: un paquete `main` distribuido en archivos por responsabilidad (cliente, tools, updater, entrypoint), manteniendo el carácter deliberado de monolito de paquete único.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
(ninguna — no hay cambios a nivel de requisitos de comportamiento)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Código**: único archivo afectado `main.go`, que se reparte en 4 archivos nuevos dentro de la raíz del módulo. `go.mod` y `go.sum` no cambian.
|
||||||
|
- **Build/release**: sin impacto. El pipeline de release (`.gitea/workflows/release.yml`) compila `.` y el contrato de nombres de assets no depende de la estructura de archivos.
|
||||||
|
- **Auto-update**: sin impacto funcional; `doUpdate` se traslada íntegro a `updater.go`.
|
||||||
|
- **Dependencias**: no se añaden ni eliminan dependencias.
|
||||||
|
|
||||||
|
## No objetivos
|
||||||
|
|
||||||
|
- No se introducen paquetes `internal/` (opción C descartada por ahora; queda como refactor futuro si el número de tools crece significativamente).
|
||||||
|
- No se añaden tests en este change (la estructura elegida los facilita, pero son alcance de un cambio posterior).
|
||||||
|
- No se modifica ningún comportamiento observable: protocolo MCP, manejo de errores, variables de entorno ni lógica de actualización.
|
||||||
|
- No se renombran funciones ni tipos; solo cambian de archivo.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Organización del código fuente por archivos
|
||||||
|
El código fuente del proyecto SHALL estar distribuido en múltiples archivos dentro del paquete único `main`, agrupando cada archivo una única responsabilidad: entrypoint (`main.go`), cliente HTTP de Outline (`client.go`), handlers de tools MCP (`tools.go`) y auto-update (`updater.go`).
|
||||||
|
|
||||||
|
#### Scenario: Distribución de archivos
|
||||||
|
- **WHEN** se revisa la raíz del módulo `outline-mcp`
|
||||||
|
- **THEN** existen los archivos `main.go`, `client.go`, `tools.go` y `updater.go`, todos declarando `package main`
|
||||||
|
|
||||||
|
#### Scenario: Contenido del entrypoint
|
||||||
|
- **WHEN** se revisa `main.go`
|
||||||
|
- **THEN** contiene únicamente la función `main()`, las variables de build inyectadas por `-ldflags` (`Version`, `GiteaURL`, `RepoOwner`, `RepoName`) y el registro de tools en el servidor MCP
|
||||||
|
|
||||||
|
#### Scenario: Aislamiento del auto-update
|
||||||
|
- **WHEN** se revisa `updater.go`
|
||||||
|
- **THEN** contiene `giteaRelease`, `parseVersion`, `versionNewer` y `doUpdate`, sin referencias al cliente de Outline ni a los handlers de tools MCP
|
||||||
|
|
||||||
|
### Requirement: Refactor sin cambio de comportamiento
|
||||||
|
La distribución del código en archivos SHALL preservar íntegramente el comportamiento observable del servidor: nombres y esquemas de las tools MCP, manejo de errores de configuración, protocolo MCP y lógica de auto-update.
|
||||||
|
|
||||||
|
#### Scenario: El servidor funciona tras el refactor
|
||||||
|
- **WHEN** se compila el binario resultante y se ejecuta el handshake MCP por stdio seguido de una petición `tools/list`
|
||||||
|
- **THEN** el servidor responde correctamente y lista las mismas tools con los mismos esquemas que antes del refactor
|
||||||
|
|
||||||
|
#### Scenario: Sin cambios en dependencias ni pipeline
|
||||||
|
- **WHEN** se compara el estado del repositorio antes y después del refactor
|
||||||
|
- **THEN** `go.mod`, `go.sum`, `.gitea/workflows/release.yml` y el contrato de nombres de assets (`outline-mcp_{GOOS}_{GOARCH}`) permanecen inalterados
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
## 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`.
|
||||||
|
- **Given** el código del cliente en `main.go`
|
||||||
|
- **When** se traslada a `client.go` y se compila en el contenedor
|
||||||
|
- **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`.
|
||||||
|
- **Given** el código del auto-update en `main.go`
|
||||||
|
- **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
|
||||||
|
- [ ] 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`
|
||||||
|
- **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
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- **Given** `main.go` tras las extracciones
|
||||||
|
- **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
|
||||||
|
|
||||||
|
## 3. Verificación
|
||||||
|
|
||||||
|
- [ ] 3.1 Ejecutar `golangci-lint run` dentro del contenedor DevPod y corregir cualquier incidencia derivada del movimiento de código.
|
||||||
|
- **Given** el refactor completado
|
||||||
|
- **When** se ejecuta `golangci-lint run` en el contenedor
|
||||||
|
- **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.
|
||||||
|
- **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`
|
||||||
|
- **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.
|
||||||
|
- **Given** el repositorio con el refactor aplicado
|
||||||
|
- **When** se revisa `git diff` para esos archivos
|
||||||
|
- **Then** no aparecen modificaciones en ellos
|
||||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user