- Go 98.4%
- Dockerfile 1.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| internal | ||
| .gitignore | ||
| Containerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| main.go | ||
| main_test.go | ||
| mediawiki-mcp.container | ||
| mediawiki-mcp.network | ||
| README.md | ||
mediawiki-mcp
A Go MCP server for accessing any MediaWiki wiki through the MediaWiki
Action API. The target wiki is chosen at runtime with --mw-base-url, so the
same binary works against any MediaWiki instance, not a specific site.
The server runs in one of two modes, chosen by whether --mcp-auth-header
is set:
| Mode | When | Who the wiki sees | Tools |
|---|---|---|---|
| Anonymous (default) | --mcp-auth-header unset |
nobody for reads; the static MW_USERNAME bot for edits, if configured |
6 read tools, +2 write tools with a static bot and --read-only off |
| Personal credentials | --mcp-auth-header=<Header>, http transport |
the account whose login:password arrived in that header, per session |
per session: none without a usable credential, otherwise 6 read tools, +2 write tools with --read-only off |
On a public wiki reading is anonymous and always available, and the server
is read-only by default. Edits (edit/create) are enabled only when
credentials are set (MW_USERNAME + MW_PASSWORD, a Special:BotPasswords bot
password) and --read-only is not set. The write tools edit the REAL
configured wiki.
A wiki closed to anonymous readers — the usual internal-wiki setup, where
even action=query&meta=siteinfo answers readapidenied — can only be served
in the second mode: the server has no account of its own and works under each
client's own credential (see
Personal credentials).
Switching modes changes nothing about the first one: without
--mcp-auth-header the server behaves exactly as it did before personal
credentials existed — anonymous reads, static bot for writes, client headers
ignored.
Configuration
The wiki is not hard-coded. Everything is driven by flags / environment variables:
--mw-base-url(envMW_BASE_URL) — base URL of the target MediaWiki wiki. From it<base>/api.phpis built. The built-in default (https://www.altlinux.org) is only an example; point it at your own wiki, e.g.--mw-base-url=https://wiki.example.org.--wiki-label(envMW_LABEL) — one line naming this wiki, e.g.--wiki-label='внутренняя вики компании — wiki.example.org'. It is stamped at the front of every tool description. The tool names are the same on every instance of this server, so when an assistant has two of them connected — a public wiki and an internal one — the description is the only thing that distinguishessearch_wikifromsearch_wiki. Without the flag the host of--mw-base-urlis used, which is worse but still unambiguous.--mw-username(envMW_USERNAME) — bot name (Special:BotPasswords), required for write tools.--mw-password(envMW_PASSWORD) — bot password (Special:BotPasswords), required for write tools.--mcp-auth-header(envMCP_AUTH_HEADER, http only) — name of the inbound header in which a client sends its own wiki credential, e.g.Authorization. Unset → inbound header auth is disabled and client headers are ignored.--session-ttl(default30m, http only) — how long a logged-in session of one credential is cached before logging in again.--read-only(envMW_READ_ONLY, defaultfalse) — disable the write tools even when credentials are set.
Reads of a public wiki never need credentials. Writes require both a
credential and the --read-only gate to be off:
writeEnabled = (credential present) && !--read-only.
Personal credentials (http)
--mcp-auth-header=Authorization turns the server into a multi-user one: it has
no account of its own, and each session works under the account whose credential
arrived in that header. This is the only way to serve a wiki that refuses
anonymous reads, and it keeps every user's permissions their own instead of
levelling everybody to one service account.
The value is a pair login:password, split on the first colon. On MediaWiki
the login usually comes from Special:BotPasswords and looks like
Ivanov@mcp — the @ belongs to the login and is preserved:
Ivanov@mcp:kq8f3l2n5x7v9b1m4c6z8t0y2w4r6e8u
Basic <base64(login:password)> # also accepted
What one session gets depends on its credential:
| Header | Tools |
|---|---|
| absent / empty / unparseable | none — on such a wiki they could not work anyway, and an empty tool list is what tells the client to fill the credential in |
| a well-formed pair | the 6 read tools, plus the 2 write tools when --read-only is off |
Sessions
Login is action=query&meta=tokens&type=login → action=login, after which the
session cookie authenticates every request. Sessions are cached per
credential (--session-ttl, default 30 minutes), so a conversation does not
log in again on every tool call; different credentials never share a session, the
cache is bounded and thread-safe, and the password is neither logged nor included
in any error message. An expired session is renewed once, transparently. A
credential the wiki refuses produces a readable explanation with the way out
(Special:BotPasswords), not a protocol error.
Without --mcp-auth-header nothing of this applies: the server reads the wiki
anonymously, exactly as before.
Bot password rights
A bot password carries only the rights its owner ticked when creating it on
Special:BotPasswords, so the boxes matter. The labels below are quoted
verbatim from the MediaWiki interface (English, and Russian as grant-*
messages render them):
| Checkbox | Needed for |
|---|---|
| Basic rights — «Основные права» | everything, including reading: without it even get_site_info fails |
| Edit existing pages — «Редактирование существующих страниц» | edit_page |
| Create, edit, and move pages — «Создание, редактирование и переименование страниц» | create_page |
| Edit protected pages — «Редактирование защищённых страниц» | editing a protected page (an admin right on top of that) |
A refusal from the API (permissiondenied, writeapidenied, cantcreate,
protectedpage, blocked, readonly, ratelimited, …) is answered with the
name of the missing checkbox and where to tick it, rather than with the raw
code. The wiki shows a new bot password once; the set of rights of an
existing one can be changed on the same page.
Edits go live immediately
There is no draft, no preview and no confirmation step: a successful
edit_page / create_page publishes the change at once, under the account
whose credential is in the header, and it shows up in the page history and in
Recent Changes as an edit by that person. Edits made with a personal credential
are deliberately not flagged bot=1 — they are that person's edits, not a
robot's, and must be visible as such. Undoing one is a manual step through the
page history. Before a mode=replace edit, read the page with get_page
first: replace overwrites the whole article, including anyone else's edits
made in the meantime.
Open WebUI (per-user secrets)
The header is what Open WebUI's per-user secrets fill in: the connection
declares one user_config slot, and each user pastes their own bot password
into it.
"headers": { "Authorization": "{{USER_SECRET:wiki_credentials}}" },
"config": { "user_config": {
"type": "object",
"properties": {
"wiki_credentials": {
"type": "string",
"title": "Логин и пароль бота вики",
"description": "Одной строкой `Имя@ИмяБота:пароль` — заводится на Special:BotPasswords",
"input": { "type": "password" }
}
},
"required": ["wiki_credentials"]
}}
A user who has not filled the slot in simply sees no tools, which is the signal to fill it in.
Data source
The MediaWiki Action API <base>/api.php, format=json. Read requests are made
without authentication. The base URL is configured with --mw-base-url (env
MW_BASE_URL); from it <base>/api.php is built. User-Agent is
mediawiki-mcp/0.2.1, timeout 30 s.
How article text is extracted
The TextExtracts extension (prop=extracts&explaintext=1, plain text) may
not be installed on a given wiki — the API then replies Unrecognized value for parameter "prop": extracts. The readable text is therefore taken via
action=parse&prop=text (the engine renders the article to HTML, container
div.mw-parser-output) and flattened into a markdown-like form using
golang.org/x/net/html: headings → ## …, paragraphs, lists (- …), code
blocks (```). Service markup (edit [edit] links, table of contents,
footnotes <sup>, navboxes, catlinks) is dropped. Search snippets and section
headings are stripped of tags by a separate simplified parser.
Tools (read-only)
| Tool | API | Purpose |
|---|---|---|
search_wiki(query, limit?) |
list=search |
Full-text search of articles by keywords; titles + snippets. |
get_page(title) |
parse&prop=text|sections |
Article content as plain text (extracted from HTML). Follows redirects. Missing article → friendly text with search hints. |
get_page_sections(title) |
parse&prop=sections |
Table of contents (list of sections) of an article. |
get_site_info() |
meta=siteinfo |
Engine version, language, statistics (articles/pages/edits/files/users), namespaces. |
list_category_members(category, limit?) |
list=categorymembers |
Articles and subcategories of a category. The Category:/Категория: prefix is optional. |
page_url(title) |
— | Canonical article URL (spaces → underscores, percent-encoding). Does not fetch the page. |
Defaults: search_wiki limit=10, list_category_members limit=20 (maximum 50
for both).
Tool descriptions are bilingual (English + Russian keywords) so they match both English and Russian queries.
Tools (write) — only with credentials
Registered only when writeEnabled = (credential present) && !--read-only. Otherwise they are absent from the tool list (read-only — 6 tools,
read-write — 8).
| Tool | API | Purpose |
|---|---|---|
edit_page(title, text, summary?, mode?) |
action=edit |
Edit an article. mode: replace (default, overwrite all), append (to the end), prepend (to the beginning). |
create_page(title, text, summary?) |
action=edit + createonly=1 |
Create a new page; error if the article already exists. |
Both tools publish immediately, with no confirmation, as the account whose
credential is in play — the static bot in anonymous mode, the session's own user
in personal-credential mode — and the change is visible in the page history
right away. mode=replace overwrites the entire article, so read it with
get_page first.
Authentication is done as a bot (Special:BotPasswords): login is lazy (on the
first write call), with a cookie session (net/http/cookiejar) and a csrf token
(cached; on badtoken — one automatic re-login). The password and tokens are
not written to logs and do not leak into response text. Wiki refusals (no
permission / page protection / spam filter / CAPTCHA) are returned as friendly
text naming the missing right and suggesting a manual edit.
Build and run
go build ./...
go test ./... -count=1 -race
# stdio (for a local agent / debugging)
./mediawiki-mcp --transport stdio --mw-base-url https://wiki.example.org
# streamable-http (endpoint /mcp, health /healthz)
./mediawiki-mcp --transport http --addr :8094 --mw-base-url https://wiki.example.org
Flags:
--transport stdio|http(defaultstdio)--addr(default:8094)--mw-base-url(envMW_BASE_URL, defaulthttps://www.altlinux.org— an example)--wiki-label(envMW_LABEL) — one-line identity of this wiki inside every tool description; default: the host of the base URL--mw-username(envMW_USERNAME) — bot name (Special:BotPasswords)--mw-password(envMW_PASSWORD) — bot password--mcp-auth-header(envMCP_AUTH_HEADER, http only) — inbound header with the client's ownlogin:password--session-ttl(default30m, http only) — lifetime of a cached logged-in session--read-only(envMW_READ_ONLY, defaultfalse) — disable write even when credentials are set
# read-write: edits performed as the bot
MW_USERNAME='YourBot@mcp' MW_PASSWORD='<botpassword>' \
./mediawiki-mcp --transport http --addr :8094 --mw-base-url https://wiki.example.org
# closed wiki: no account of its own, every client sends its own credential
./mediawiki-mcp --transport http --addr :8094 \
--mw-base-url https://intranet.example.org --mcp-auth-header Authorization --read-only
On startup the server logs its mode: personal credentials via header X,
read-write mode (user X), read-only mode (no MW_USERNAME/MW_PASSWORD) or
read-only mode (--read-only). Passwords are not logged — neither the static one
nor the ones arriving in headers.
Usage with MCP clients
The server speaks MCP over stdio; the binary is mediawiki-mcp. Point it
at your wiki with --mw-base-url. Set MW_USERNAME/MW_PASSWORD only if you
want the write tools (they stay hidden otherwise).
Claude (.mcp.json)
{
"mcpServers": {
"mediawiki": {
"command": "mediawiki-mcp",
"args": ["--transport=stdio", "--mw-base-url=https://wiki.example.org"],
"env": {
"MW_USERNAME": "<bot>",
"MW_PASSWORD": "<bot password, optional for write>"
}
}
}
}
Codex (~/.codex/config.toml)
[mcp_servers.mediawiki]
command = "mediawiki-mcp"
args = ["--transport=stdio", "--mw-base-url=https://wiki.example.org"]
env = { MW_USERNAME = "...", MW_PASSWORD = "..." }
opencode (opencode.json)
{
"mcp": {
"mediawiki": {
"type": "local",
"command": ["mediawiki-mcp", "--transport=stdio", "--mw-base-url=https://wiki.example.org"],
"enabled": true
}
}
}
Container / quadlet
podman build -t mediawiki-mcp .
The final image is FROM scratch, CGO_ENABLED=0, USER 65534, with a CA bundle
for TLS. Quadlet units: mediawiki-mcp.container + mediawiki-mcp.network
(Options=mtu=1500 is required for rootless podman). The default port is :8094.