wiki-skills
# ok-wiki-skills
A local **MCP connector and Codex plugin** that lets Claude and Codex search, read, create, and edit
pages in the [ok-wiki](https://github.com/obe711/ok-wiki) knowledge base.
It reaches the wiki over HTTP with an API key that never enters the model's context, and speaks MCP
two ways: over **stdio** as a child process of Claude Code or the self-contained Codex plugin, or
over **Streamable HTTP** for remote hosts such as claude.ai and ChatGPT, which need a public HTTPS
URL (or a supported secure tunnel) and OAuth
([`docs/adr/0006`](./docs/adr/0006-oauth-for-remote-connectors.md)).
> Start at [`docs/architecture.md`](./docs/architecture.md).
## What it can do
- **Search and read** — full-text search, fetch a page by path or id, list pages by tag or recency
- **Create and edit** — create markdown pages at a path the connector derives (see below); patch an
existing page's content, title, description, tags, or published state
- **History** — list a page's revisions, fetch a specific version, restore a page to an earlier one
- **Tags** — list and autocomplete the wiki's existing tag vocabulary
- **Assets** — upload a file and get back a ready-to-paste markdown reference; list assets and
create folders
## What it deliberately cannot do
**No moving, renaming, or deleting pages.** Those break inbound links or lose content, and a
mistyped path is exactly the error a model makes. Do them in the ok-wiki UI. The boundary is
enforced by the tool surface—move and delete tools do not exist—and the connector group withholds
`delete:pages`. The group still needs `manage:pages` because ok-wiki requires it for single-page
reads. Reasoning: [`docs/adr/0005`](./docs/adr/0005-no-destructive-operations.md).
Also out of scope: comments, navigation, users and groups, theming, and administrative operations.
## Where new pages land
**You don't choose the path—each creation tool derives it.** There is no `path` input on any
creation tool. Claude's `wiki_create_page` takes three human names and slugifies each into one
segment:
```
project: "Moontower" chatTitle: "Release planning" artifactName: "Deployment checklist"
↓
claude/moontower/release-planning/deployment-checklist
```
Codex uses the separate `wiki_create_codex_page` contract:
```
workspace: "wiki-skills" threadTitle: "Add Codex support" artifactName: "Plugin guide.md"
↓
codex/wiki-skills/add-codex-support/plugin-guide
```
Stapler documents use `wiki_create_stapler_page`, which files into folders a human already made:
```
folder: "Moontower AI" subFolder: "Wiki Skills" artifactName: "Architecture.md"
↓
stapler/moontower-ai/wiki-skills/architecture
```
The `claude/`, `codex/`, and `stapler/` roots preserve host provenance. Slugification lowercases,
turns spaces, underscores and dots into hyphens, drops a trailing file extension, and discards
anything outside `a-z`, `0-9`, and `-`.
`wiki_create_stapler_page` carries one extra rule the other two don't: **`folder` and `subFolder`
must already exist, and it never creates them.** Both values come from the user asking for the
document—the model is told not to infer them. Before writing, the tool asks the wiki's page tree
whether the parent folder is there and refuses if it isn't, naming the missing level. ok-wiki has no
folder entity for pages (`pageTree` folder rows are synthesized from the paths of existing pages), so
the *first* page under a new Stapler folder has to be created in the ok-wiki UI; every later document
can go through the tool.
Because there is no move tool, a wrong path is permanent—which is why placement is a schema rule
rather than advice. Pages that predate these conventions stay where they are and remain editable by
path through `wiki_update_page`. Claude conventions live in [`SKILL.md`](./SKILL.md); Codex policy
lives in [`plugins/ok-wiki/skills/wiki-authoring/SKILL.md`](./plugins/ok-wiki/skills/wiki-authoring/SKILL.md).
## Quickstart
### 1. Build
Install Node.js 22 or newer, then:
```bash
cd /path/to/wiki-skills
npm install
npm run build # MCP registration points at dist/index.js, not src/
```
### 2. Enable the ok-wiki API
In the wiki: **Administration → API Access → enable**. Without this, every request is rejected with
*"API is disabled. You must enable it from the Administration Area first."*
### 3. Mint a scoped API key
Create a group for the connector granting exactly:
`read:pages`, `read:source`, `write:pages`, `read:history`, `read:assets`, `write:assets`,
`manage:pages`
Grant them in **both** the group's global permissions and its page rules — the wiki checks both
layers. `manage:pages` looks like more than a reader needs, but the wiki's single-page resolvers
require it: without it, page lookups fail with *"You are not authorized to view this page"* even
though listing works ([`docs/ok-wiki-api.md` §4](./docs/ok-wiki-api.md#4-operation-reference)).
Then **Administration → API Access → New API Key**, bound to that group.
`read:source` is the one that fails quietly if you forget it — pages come back with a `null` body
instead of an error. Use a dedicated key so it can be revoked independently and so wiki history
distinguishes agent edits from human ones. Details:
[`docs/ok-wiki-api.md` §2](./docs/ok-wiki-api.md#2-authentication).
### 4. Register with Claude Code
```bash
claude mcp add wiki \
--env WIKI_BASE_URL=http://localhost:3000 \
--env WIKI_API_KEY=<your-api-key> \
-- node /path/to/wiki-skills/dist/index.js
```
Or, to share it with a project via `.mcp.json`:
```json
{
"mcpServers": {
"wiki": {
"command": "node",
"args": ["/path/to/wiki-skills/dist/index.js"],
"env": {
"WIKI_BASE_URL": "http://localhost:3000",
"WIKI_API_KEY": "${WIKI_API_KEY}"
}
}
}
}
```
Prefer the `${VAR}` form in any file you might commit — don't put the key in version control.
### 5. Verify
Run `/mcp` in Claude Code and confirm `wiki` is connected with its tools listed. Then ask for
something read-only, like *"search the wiki for onboarding"*, to exercise `wiki_search_pages`.
## Codex plugin installation
The repository marketplace packages `ok-wiki` version `1.0.0` from `plugins/ok-wiki` for Codex CLI
and other local Codex hosts. It uses the bundled `./mcp/server.mjs` and local `ok-wiki` MCP server
definition. This is not the package used by hosted ChatGPT. Writable mode publishes 15 total tools;
readonly mode publishes 8 read-only tools.
The plugin requires Node.js 22 or newer, matching `package.json`
(`engines.node: ">=22"`).
From a clean clone, install dependencies and deterministically rebuild the checked-in bundle:
```bash
npm install
npm run build:plugin
```
The plugin reads required `WIKI_BASE_URL` and `WIKI_API_KEY` values from the local process
environment. It also forwards the seven optional variables `WIKI_LOCALE`, `WIKI_TIMEOUT_MS`,
`WIKI_READONLY`, `WIKI_MAX_CONTENT_BYTES`, `WIKI_LOG_LEVEL`, `WIKI_MAX_UPLOAD`, and
`WIKI_UPLOAD_ALLOWLIST`. Keep secret values only in local environment configuration; never add
them to the plugin, `.mcp.json`, `.codex-plugin/plugin.json`, or
`.agents/plugins/marketplace.json`.
### Codex CLI
Register the repository marketplace, verify discovery, and install the plugin in this exact order:
```bash
codex plugin marketplace add <repo-root>
codex plugin list --marketplace wiki-skills --available --json
codex plugin add ok-wiki@wiki-skills
```
Start a fresh Codex CLI thread and confirm `/mcp` lists the `ok-wiki` server. After local source
changes, run `npm run build:plugin`, rerun `codex plugin add ok-wiki@wiki-skills`, and start a new
thread so the rebuilt artifact is loaded.
### ChatGPT desktop and chatgpt.com
Do not use the repository marketplace or the local stdio package for the ChatGPT interface. The
desktop application uses the same hosted ChatGPT connection and plugin records as chatgpt.com; it
does not inherit `WIKI_BASE_URL` or `WIKI_API_KEY` from the shell that launched it. Use the public
HTTP/OAuth server, register it as a ChatGPT MCP app, and install the separate
`plugins/ok-wiki-chatgpt` package described below.
### Authoring modes
The bundled skill starts in **ask mode**: creating Markdown does not write to the wiki or trigger
an unsolicited save offer. An explicit request to save, post, or publish a new Markdown artifact
uses `wiki_create_codex_page`.
A clear instruction such as *"use auto mode"* enables conversation-local automatic saving until
the user disables it. Auto mode saves each completed `.md` file newly created by Codex during the
active task exactly once after finalization. It excludes edited pre-existing Markdown, scratch
files, files created by another process, and non-Markdown outputs. Auto mode is never persisted and
never converts a collision into an update; existing pages require an explicit
`wiki_get_page` → `wiki_update_page` workflow.
The product and safety contracts for this workflow are
[`docs/codex-plugin-architecture.md`](./docs/codex-plugin-architecture.md),
[`docs/codex-plugin-prd.md`](./docs/codex-plugin-prd.md), and
[`docs/codex-plugin-tasks.md`](./docs/codex-plugin-tasks.md).
## Connecting from ChatGPT or claude.ai
Remote clients do not spawn this repository's local process. They reach a public HTTPS URL (or a
supported secure tunnel), so the connector runs its own OAuth 2.1 authorization server
([`docs/adr/0006`](./docs/adr/0006-oauth-for-remote-connectors.md)). Client registration is
automatic through CIMD or DCR; do not configure a static OAuth client ID or secret.
### 1. Run the HTTP entrypoint
```bash
WIKI_BASE_URL=http://localhost:3000 \
WIKI_API_KEY=<your-api-key> \
WIKI_MCP_BEARER=$(openssl rand -base64 32) \
WIKI_MCP_PUBLIC_URL=https://wiki.example.com \
WIKI_MCP_HOST=127.0.0.1 \
npm run http
```
`npm run http` runs the *compiled* `dist/http.js`, so step 1 above has to have happened.
`WIKI_MCP_PUBLIC_URL` is the OAuth issuer and must match the URL you give the remote client, minus
the `/mcp` path. Setting it is what turns OAuth on; without it the entrypoint stays static-bearer
only.
To run it as a service rather than a foreground process, see
[`deploy/README.md`](./deploy/README.md) — a hardened systemd **user** unit, plus the two settings
whose obvious values are the wrong ones.
### Redeploying after a source change
The unit runs `dist/http.js`, which systemd loaded into a long-lived process at start. Building
new output does not touch that process — **restart it, or the old code keeps serving**:
```bash
npm run build
systemctl --user restart wiki-skills-http
systemctl --user status wiki-skills-http --no-pager
```
This is a **user** unit, so `--user` is required and `sudo` is wrong — `sudo systemctl restart
wiki-skills-http` looks for a system unit that does not exist. (`sudo systemctl restart
wiki.service`, without `--user`, is the *wiki itself* — a different service.)
A restart drops nothing a client will notice: the entrypoint is stateless, with a fresh
`McpServer` per request, and issued OAuth tokens survive in `WIKI_MCP_STATE_FILE`. Clients
reconnect on their next call without re-consenting. Only edits to the unit file itself need
`systemctl --user daemon-reload` first.
The failure this prevents is a quiet one: the connector stays up and answers normally, just with
the previous build's tool list — a new tool never appears, and a fixed bug is still there.
### 2. Put an HTTPS ingress in front
TLS and the public hostname belong to the ingress, not this process. With a Cloudflare Tunnel, point
the public hostname at the **origin**, with no path:
```yaml
ingress:
- hostname: wiki.example.com
service: http://127.0.0.1:8787
```
If `cloudflared` runs in a Docker container, `127.0.0.1` there is the *container*, not your host —
use the bridge gateway (typically `http://172.17.0.1:8787`) and set `WIKI_MCP_HOST` to match. Point
the tunnel's health check at `/healthz`, which is unauthenticated; `/mcp` answers `405` to `GET`.
### 3. Register the MCP app in ChatGPT
Use the [ChatGPT Plugins interface](https://chatgpt.com/plugins) in a browser and create an MCP
app/connection using:
```
https://wiki.example.com/mcp
```
The exact navigation and labels vary by ChatGPT account and workspace. Do not rely on a
**Settings → Security and login → Developer mode** toggle: it was not present in the account used
for this integration. Likewise, do not manually enter an OAuth client ID, client secret, or API
key. ChatGPT registers itself as a public client and negotiates `token_endpoint_auth_method=none`.
Complete browser authorization using `WIKI_MCP_BEARER` as the consent passphrase. Enable the write
scope only if ChatGPT should create and edit pages. After ChatGPT scans the tools, record the
generated technical app ID; it has the form `plugin_asdk_app_...`.
An MCP app exposes tools, but it does not automatically install this repository's authoring skill.
Build the upload archive with:
```bash
npm run package:chatgpt-plugin
```
This validates the package and writes `dist/ok-wiki-chatgpt-plugin.tar.gz`. In ChatGPT's plugin
creator, attach that archive and ask it to create/install a personal plugin from the package. The
checked-in `.app.json` maps the plugin to the registered technical app ID. Start a new conversation
after installing or updating it so the skill and tools are reloaded.
See [`docs/chatgpt-plugin-installation.md`](./docs/chatgpt-plugin-installation.md) for the complete
procedure, update workflow, and the OAuth failures encountered during the first installation.
ChatGPT's production OAuth callback is under `https://chatgpt.com/connector/oauth/`; the server
learns the exact redirect URI through CIMD or dynamic registration and validates it on every OAuth
exchange.
### 4. Add the connector to claude.ai
In claude.ai, **Settings → Connectors → Add custom connector**, URL:
```
https://wiki.example.com/mcp
```
The `/mcp` suffix is required. Approve in the browser using `WIKI_MCP_BEARER` as the passphrase, and
tick **Create and edit pages** if you want write access — it is unchecked by default, and a
read-only grant publishes 8 tools instead of 15.
### Rotating credentials
`WIKI_MCP_BEARER` guards two doors: it is the header credential *and* the consent passphrase.
Rotating it closes both but does **not** invalidate tokens already issued — for that, delete
`WIKI_MCP_STATE_FILE`, which is the revoke-everything gesture. That file holds the token signing
key, so it lives at mode `0600`; the server refuses to start if that has slipped.
## Configuration
| Variable | Required | Default | Purpose |
|---|---|---|---|
| `WIKI_BASE_URL` | yes | — | e.g. `http://localhost:3000` — `http://ok-wiki.local` also serves the wiki |
| `WIKI_API_KEY` | yes | — | ok-wiki API key (JWT) |
| `WIKI_LOCALE` | no | `en` | Default locale |
| `WIKI_TIMEOUT_MS` | no | `15000` | Per-request timeout |
| `WIKI_READONLY` | no | `0` | `1` registers only read tools |
| `WIKI_MAX_CONTENT_BYTES` | no | `100000` | Cap on page body returned into context |
| `WIKI_LOG_LEVEL` | no | `info` | Diagnostics, always to stderr |
| `WIKI_MAX_UPLOAD` | no | `10485760` | Max upload size in bytes for `wiki_upload_asset` (10 MB) |
| `WIKI_UPLOAD_ALLOWLIST` | no | unset | Colon-separated absolute path prefixes uploads may come from; unset = unrestricted |
| `WIKI_MCP_HOST` | no | `127.0.0.1` | Bind address for the remote HTTP entrypoint |
| `WIKI_MCP_PORT` | no | `8787` | Listen port for the remote HTTP entrypoint |
| `WIKI_MCP_BEARER` | no | unset | Static bearer token for header-auth clients; also the consent passphrase |
| `WIKI_MCP_PUBLIC_URL` | no | unset | Public https:// origin — the OAuth issuer. Unset disables OAuth |
| `WIKI_MCP_STATE_FILE` | no | `$XDG_STATE_HOME/wiki-skills/oauth.json`, else `~/.local/state/wiki-skills/oauth.json` | OAuth signing key and issued-token state |
| `WIKI_MCP_CLIENT_HOSTS` | no | `claude.ai,chatgpt.com` | Hosts whose OAuth client metadata may be fetched; an explicit override must retain every remote client you use |
## Development
| Command | What it does |
|---|---|
| `npm run build` | Compile the shared stdio and HTTP entrypoints into `dist/` |
| `npm run build:plugin` | Deterministically rebuild the checked-in self-contained Codex bundle |
| `npm run package:chatgpt-plugin` | Validate and rebuild `dist/ok-wiki-chatgpt-plugin.tar.gz` for upload |
| `npm run dev` | Watch-mode stdio server via `tsx` — no build step |
| `npm test` | Full vitest suite, including the doc-consistency tests that pin this README to `src/config.ts` |
| `npm run lint` | ESLint, which carries the rule that forbids writing to stdout |
| `npm run smoke` | End-to-end against a live wiki; needs `WIKI_BASE_URL` and `WIKI_API_KEY`. Creates one unpublished throwaway page and one small asset, and deletes nothing — clean up by hand |
| `systemctl --user restart wiki-skills-http` | Load a new build into the deployed HTTP connector. `npm run build` alone leaves the running process on the old code — see [Redeploying after a source change](#redeploying-after-a-source-change) |
## Documentation
| Document | What's in it |
|---|---|
| [`docs/architecture.md`](./docs/architecture.md) | System context, module layout, invariants, request flows, error model, security, testing |
| [`docs/codex-plugin-architecture.md`](./docs/codex-plugin-architecture.md) | Codex desktop/CLI plugin packaging, page namespace, authoring modes, and compatibility design |
| [`docs/codex-plugin-prd.md`](./docs/codex-plugin-prd.md) | Numbered Codex plugin requirements, milestones, acceptance criteria, risks, and rollout plan |
| [`docs/codex-plugin-tasks.md`](./docs/codex-plugin-tasks.md) | Developer/QA task pairs, dependency graph, milestone gates, and completion criteria for the Codex plugin |
| [`docs/codex-plugin-development-status.md`](./docs/codex-plugin-development-status.md) | Implementation ledger, completed QA evidence, and remaining manual release work |
| [`docs/chatgpt-plugin-installation.md`](./docs/chatgpt-plugin-installation.md) | Working hosted ChatGPT setup, update procedure, and first-install failure record |
| [`docs/tool-surface.md`](./docs/tool-surface.md) | Every tool's inputs, outputs, and behavior |
| [`docs/ok-wiki-api.md`](./docs/ok-wiki-api.md) | Upstream endpoints, auth, permissions, and the gotchas that shape the design |
| [`SKILL.md`](./SKILL.md) | The `wiki-authoring` skill — placement, naming, and formatting conventions the model follows |
| [`docs/conventions.md`](./docs/conventions.md) | Survey of how the live wiki is actually written, which is where those conventions came from |
| [`deploy/README.md`](./deploy/README.md) | Running the HTTP entrypoint as a systemd user unit |
| [`docs/prd.md`](./docs/prd.md) | Numbered requirements, milestones, and acceptance criteria — the build plan, decomposed into tasks in [`docs/tasks.md`](./docs/tasks.md) |
| [`docs/adr/`](./docs/adr/) | Why stdio, why GraphQL, why TypeScript, why read-modify-write, why no deletes, why our own OAuth server |
**If you read only one thing before writing code**, make it
[`docs/adr/0004`](./docs/adr/0004-read-modify-write-updates.md): ok-wiki's `pages.update` is a full
replace, not a patch, and a naive implementation silently unpublishes pages and drops their tags.
## Troubleshooting
| Symptom | Cause |
|---|---|
| Connector won't connect; no useful error | Something wrote to **stdout**. Under stdio, stdout is the protocol channel — all logging must go to stderr. |
| Tools missing after a source edit | You edited `src/` but the server runs `dist/`. Re-run `npm run build`. |
| A new tool or fix is missing from the *remote* connector, which is otherwise healthy | The deployed process is still on the previous build. `npm run build` does not restart it: `systemctl --user restart wiki-skills-http`. Confirm the reload with `systemctl --user status wiki-skills-http` — the start time should be the restart, not the original boot. |
| Codex still uses old plugin behavior | Re-run `npm run build:plugin`, reinstall or restart the plugin for that surface, and start a fresh thread. |
| *"API is disabled"* | Step 2 not done. |
| *"API Key is invalid or was revoked"* | Key revoked or expired; mint a new one. |
| Listing works but fetching a single page is *"not authorized"* | The key's group is missing `manage:pages` (required by the single-page resolvers), or lacks it in the group's page rules. |
| Page reads succeed but `content` is null | The key's group is missing `read:source`. |
| Edits rejected as conflicts | Someone edited the page after your read. Re-read and retry. |
| claude.ai fails at the authorize step | `WIKI_MCP_PUBLIC_URL` must be the connector URL minus `/mcp`, exactly. It is the OAuth issuer, and a mismatch fails discovery. |
| *"cannot reach the wiki"* | ok-wiki isn't up. It runs as a systemd unit — check `systemctl status wiki.service`, and `journalctl -u wiki.service` for why it stopped. |
## License
MIT
TDQS
Scored across 13 tools
Most tools target a distinct resource and action (search vs list, current vs version, page vs asset). The two tag tools (list_tags and search_tags) overlap in purpose but their descriptions clearly distinguish batch listing from duplicate-checking, so an agent can separate them. Overall low ambiguity.
All tools share the wiki_ prefix and mostly follow verb_noun (list_tags, get_page, create_page). The one exception is wiki_page_history, which is noun_noun rather than a verb form, making it slightly inconsistent. Otherwise the pattern is predictable.
At 13 tools, the set is well-scoped for a wiki server covering pages, tags, history, and assets. Each tool serves a clear function without redundancy. This is within the ideal range and not overwhelming.
The page lifecycle is incomplete: there is no delete or move/rename tool, and update_page explicitly refuses to do so. Asset support also lacks folder listing, deletion, and update, leaving obvious dead ends. These gaps would cause agent failures when cleanup or reorganization is needed.