diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..81cc531 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 diff --git a/main.go b/main.go index 035b649..e31c276 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,7 @@ func (c *OutlineClient) post(ctx context.Context, path string, payload any, resu if err != nil { 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) 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 { + 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)) } @@ -125,9 +132,14 @@ func handleSearch(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe var resp struct { Data []struct { - ID string `json:"id"` - Title string `json:"title"` - URL string `json:"url"` + 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 { @@ -139,8 +151,11 @@ func handleSearch(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRe } var lines []string - for _, d := range resp.Data { - lines = append(lines, fmt.Sprintf("- **%s** (id: %s) %s", d.Title, d.ID, d.URL)) + 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 } @@ -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 } + publish := req.GetBool("publish", true) + var resp struct { Data struct { ID string `json:"id"` @@ -199,6 +216,7 @@ func handleCreateDocument(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca "title": title, "text": text, "collectionId": collectionID, + "publish": publish, }, &resp); err != nil { return mcp.NewToolResultError(err.Error()), nil } @@ -218,6 +236,8 @@ type giteaRelease struct { 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) @@ -268,10 +288,10 @@ func doUpdate() error { if err != nil { return fmt.Errorf("error consultando Gitea: %w", err) } - defer resp.Body.Close() + defer resp.Body.Close() //nolint:errcheck 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 @@ -310,13 +330,13 @@ func doUpdate() error { if err != nil { return fmt.Errorf("error descargando asset: %w", err) } - defer resp.Body.Close() + 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); err != nil { + if err := selfupdate.Apply(resp.Body, selfupdate.Options{}); err != nil { return fmt.Errorf("error aplicando actualización: %w", err) } @@ -391,6 +411,9 @@ func main() { 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, ) diff --git a/openspec/changes/create-outline-mcp-server/tasks.md b/openspec/changes/create-outline-mcp-server/tasks.md index 85f35f8..a310978 100644 --- a/openspec/changes/create-outline-mcp-server/tasks.md +++ b/openspec/changes/create-outline-mcp-server/tasks.md @@ -16,38 +16,38 @@ ## 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) -- [ ] 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`) -- [ ] 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.1 Crear `main.go` con variables globales inyectables (`Version`, `GiteaURL`, `RepoOwner`, `RepoName`) y parsing de argumentos (`version`, `update`; sin argumentos → servidor MCP) +- [x] 3.2 Implementar el comando `version` que imprime la versión compilada +- [x] 3.3 Arrancar el servidor MCP con `server.NewMCPServer` y transporte stdio (`ServeStdio`) +- [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.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 ` 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.1 Implementar el struct `OutlineClient` configurado desde `OUTLINE_URL` y `OUTLINE_API_KEY` +- [x] 4.2 Implementar método genérico `post(ctx, path, payload, result)` que serialice JSON, incluya el header `Authorization: Bearer ` y propague errores HTTP con el mensaje de la API +- [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.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` -- [ ] 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` -- [ ] 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.1 Implementar `outline_list_collections` contra `/api/collections.list` devolviendo `id`, `name` y `description` +- [x] 5.2 Implementar `outline_search` contra `/api/documents.search` con parámetro `query` +- [x] 5.3 Implementar `outline_get_document` contra `/api/documents.info` con parámetro `id`, devolviendo título y texto en Markdown +- [x] 5.4 Implementar `outline_create_document` contra `/api/documents.create` con parámetros `title`, `text` y `collection_id` +- [x] 5.5 Registrar las cuatro herramientas en el servidor MCP con sus esquemas de entrada (`mcp.WithString`, `mcp.Required()`) +- [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.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) -- [ ] 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 -- [ ] 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.1 Implementar `doUpdate`: consulta a `{GiteaURL}/api/v1/repos/{RepoOwner}/{RepoName}/releases/latest` y manejo de errores de red +- [x] 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.3 Implementar selección y descarga del asset por convención `outline-mcp_{GOOS}_{GOARCH}[.exe]` usando `runtime.GOOS`/`runtime.GOARCH` +- [x] 6.4 Aplicar el binario descargado con `selfupdate.Apply` e informar el resultado +- [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.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`) -- [ ] 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`) -- [ ] 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.1 Crear `.gitea/workflows/release.yml` con trigger `on: push: tags: ['v*']` y runner `ubuntu-latest` +- [x] 7.2 Configurar Go 1.22 en el job (`actions/setup-go@v5`) +- [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) +- [x] 7.4 Publicar el release en Gitea adjuntando los tres binarios (acción oficial de Gitea o CLI `tea release create`) +- [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` diff --git a/openspec/changes/split-main-into-files/.openspec.yaml b/openspec/changes/split-main-into-files/.openspec.yaml new file mode 100644 index 0000000..032461f --- /dev/null +++ b/openspec/changes/split-main-into-files/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-02 diff --git a/openspec/changes/split-main-into-files/design.md b/openspec/changes/split-main-into-files/design.md new file mode 100644 index 0000000..5f93397 --- /dev/null +++ b/openspec/changes/split-main-into-files/design.md @@ -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) diff --git a/openspec/changes/split-main-into-files/proposal.md b/openspec/changes/split-main-into-files/proposal.md new file mode 100644 index 0000000..8fce5f8 --- /dev/null +++ b/openspec/changes/split-main-into-files/proposal.md @@ -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. diff --git a/openspec/changes/split-main-into-files/specs/code-structure/spec.md b/openspec/changes/split-main-into-files/specs/code-structure/spec.md new file mode 100644 index 0000000..7ec83e3 --- /dev/null +++ b/openspec/changes/split-main-into-files/specs/code-structure/spec.md @@ -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 diff --git a/openspec/changes/split-main-into-files/tasks.md b/openspec/changes/split-main-into-files/tasks.md new file mode 100644 index 0000000..a5b3e72 --- /dev/null +++ b/openspec/changes/split-main-into-files/tasks.md @@ -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 diff --git a/outline-mcp b/outline-mcp new file mode 100755 index 0000000..348932d Binary files /dev/null and b/outline-mcp differ