# AGENTS.md Guidance for coding agents working on this repository. ## What this project is A Go implementation of a Model Context Protocol (MCP) server for WikiJS. It is a rewrite of the TypeScript package [`@cahaseler/wikijs-mcp`](https://github.com/cahaseler/wikijs-mcp). The public tool surface is compatible with that server; no Node.js, npm, or GraphQL codegen is involved. The server exposes two transports: - **stdio** (default): the client launches the binary and speaks JSON-RPC over stdin/stdout. - **streamable HTTP**: `--transport http`, serving `/mcp` and `/healthz`. ## Commands ```bash make build # bin/wikijs-mcp, version injected from git describe make test # go test ./... make test-race # go test -race ./... make vet # go vet ./... make fmt # gofmt -w . make check # vet + race tests make run # stdio transport (needs WIKIJS_URL/WIKIJS_API_KEY) make run-http # HTTP transport on 127.0.0.1:8080 make docker-up # local WikiJS on http://localhost:3000 ``` ### Formatting and imports (gofmt + gopls) Always format Go files with `gofmt`. Use `gopls` on top of it for import tidying, canonical formatting, and diagnostics. Run both on the files you changed: ```bash gofmt -w path/to/file.go # standard formatting gopls imports -w path/to/file.go # add/remove/sort imports gopls format -w path/to/file.go # canonical formatting gopls check path/to/file.go # type/lint diagnostics ``` `make fmt` runs `gofmt -w .`; CI enforces `gofmt -l .`. Before finishing a change, run at minimum: ```bash gofmt -w gopls imports -w gopls format -w gopls check gofmt -l . # must print nothing go vet ./... go test -race ./... go build ./... ``` ## Layout ``` cmd/wikijs-mcp/main.go flags, env wiring, logger, signals, transport selection internal/config/ environment and .env parsing, validation config.go Config, defaults, validation dotenv.go optional .env loading internal/wikijs/ typed WikiJS GraphQL client (stdlib HTTP only) types.go API and output types queries.go GraphQL documents as raw string constants client.go request/response handling, URL enrichment, errors internal/server/ MCP server construction and tool handlers server.go mcp.Server, tool metadata, edit gate tools.go input structs, validation, handlers internal/transport/ streamable HTTP handler, bearer auth, graceful shutdown ``` Data flow: ``` MCP client ── stdio / HTTP ──▶ mcp.Server ──▶ tool handler ──▶ wikijs.Client ──▶ WikiJS /graphql ``` ## Invariants - **stdout is protocol-only.** With stdio, stdout carries MCP JSON-RPC. All logging must go to stderr (`slog` is configured that way). Never use `fmt.Print` or `log.Print` for diagnostics. - **Tool names and argument names are a public API.** Keep them stable: `search_pages`, `get_page_by_id`, `get_page_by_path`, `get_all_pages`, `create_page`, `edit_page`; camelCase arguments such as `isPublished`. - **Write tools are opt-in.** `create_page` and `edit_page` are registered only when `WIKIJS_ENABLE_EDIT=true`. - **URLs are always absolute.** `wikijs.Client` appends `url` to pages, list items, and search hits. The locale segment is included only for non-English pages (`/de/path`, but `/path` for `en`). - **Secrets stay out of flags and logs.** `WIKIJS_API_KEY` and `MCP_AUTH_TOKEN` come from the environment or the env file. Request bodies and headers are never logged. - **Environment first, `.env` optional.** `--env-file` (scanned by `envFileFromArgs` before flag parsing) wins over `ENV_FILE`, which wins over `./.env`. `config.LoadDotEnv()`/`LoadEnvFile()` never override existing process variables. A missing default file is ignored, an explicit one must exist. Document new variables in `README.md` and `env.example`. - **HTTP is fail-closed without auth.** `config.Validate()` rejects an HTTP listen address that is not loopback unless `MCP_AUTH_TOKEN` is set. Keep the loopback default (`127.0.0.1:8080`) and require a token for `:8080` or `0.0.0.0:8080`. - **Errors are tool errors.** Handlers return Go errors; the MCP SDK marks the result with `isError` and puts the message in the content so the model can react. ## Tool handler conventions Each handler uses the generic `mcp.AddTool` and a typed input struct: ```go type exampleInput struct { Required string `json:"required" jsonschema:"description for the model"` Optional string `json:"optional,omitempty" jsonschema:"optional value"` } func (h *handlers) example(ctx context.Context, _ *mcp.CallToolRequest, in exampleInput) (*mcp.CallToolResult, someOutput, error) { if err := validate...; err != nil { return nil, someOutput{}, err } return nil, result, nil // SDK fills structuredContent and JSON text content } ``` Rules: - Fields without `omitempty` are required and appear in `required` in the input schema. - Use pointers (`*int`, `*bool`) for optional values with defaults so "unset" and "zero" remain distinguishable. Apply defaults in the handler. - Validate semantic constraints in the handler (positive IDs, locale syntax, non-empty paths). The SDK validates JSON types and required fields. - Return `nil` for `*mcp.CallToolResult` when the SDK should serialize the output. Use `nullResult()` only for nullable results: a typed nil pointer would otherwise be coerced to an empty object by the SDK's output schema handling. - Register the tool in `internal/server/server.go` with a description and annotations (`ReadOnlyHint` for reads). ### Adding a tool 1. Add an input struct and handler in `internal/server/tools.go`. 2. If it needs a new WikiJS operation, add the document to `internal/wikijs/queries.go`, a typed method to `client.go`, and tests. 3. Register the tool in `server.go` (inside the edit gate if it mutates). 4. Add a handler test in `internal/server/tools_test.go` and, for HTTP, a transport test if relevant. 5. Document the tool in `README.md`. ## WikiJS GraphQL notes - Endpoint: `/graphql` with `Authorization: Bearer `. - `WIKIJS_URL` may already end in `/graphql` or `/`; `New()` normalizes both. - Requests are plain JSON (`query` + `variables`). Keep optional variables absent rather than sending `null`/empty strings. - GraphQL errors arrive with HTTP 200 in `errors[]`; `APIError` aggregates their messages. HTTP failures include the status and a truncated body. - Mutations return `responseResult { succeeded errorCode slug message }`. Successful transport does not imply a successful mutation: check `Succeeded`. - `list` returns `tags` as `[]string`; `single`/`singleByPath` return `tags { tag title }`. The types reflect this difference. - Timestamps (`createdAt`, `updatedAt`) are kept as strings to preserve WikiJS' format and avoid time zone/layout assumptions. - Pages may not exist: `single` and `singleByPath` are nullable. Return a nil page and let the handler emit `null`. ## Testing strategy - `internal/wikijs`: `httptest` fake GraphQL server. Assert auth header, variables, normalization, URL building, GraphQL errors, and HTTP errors. - `internal/server`: real MCP client over `mcp.NewInMemoryTransports()` and a fake WikiJS. Assert tool visibility, structured output, defaults, validation, and `isError` behavior. - `internal/transport`: `httptest` for `/healthz`, bearer auth (401 and 200), and a full streamable-HTTP MCP roundtrip. - Prefer table-driven tests where inputs vary; keep fake responses minimal. ## Configuration reference | Variable | Default | Notes | |----------|---------|-------| | `WIKIJS_URL` | – | Required; absolute http(s) URL | | `WIKIJS_API_KEY` | – | Required | | `WIKIJS_ENABLE_EDIT` | `false` | Registers write tools | | `WIKIJS_TIMEOUT` | `30s` | Per-request HTTP timeout | | `MCP_TRANSPORT` | `stdio` | `stdio` or `http` | | `MCP_ADDR` | `127.0.0.1:8080` | HTTP listen address | | `MCP_PATH` | `/mcp` | Must start with `/`, must not be `/healthz` | | `MCP_STATELESS` | `false` | No sessions; for load-balanced setups | | `MCP_AUTH_TOKEN` | – | Bearer token for HTTP; required for non-loopback addresses; `/healthz` stays open | | `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` | | `ENV_FILE` | `.env` | Alternative env file; `--env-file` overrides it | Flags override the transport-related environment values. Secrets are available only through the environment or the env file. ## Dependencies and versioning - Direct dependencies: - `github.com/modelcontextprotocol/go-sdk/mcp` for the protocol. - `github.com/joho/godotenv` for correct `.env` parsing (quotes, comments, `export`). - GraphQL is implemented with the standard library; do not add a GraphQL client or code generator without a strong reason. - `version` is a `main` variable set with `-ldflags "-X main.version="`; `make build` uses `git describe` and falls back to `dev`.