@gesslar/mediawiki-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions" # See documentation for possible values
4
+ directory: "/" # Location of package manifests
5
+ schedule:
6
+ interval: "daily"
7
+ - package-ecosystem: "npm"
8
+ directory: "/"
9
+ schedule:
10
+ interval: "daily"
@@ -0,0 +1,21 @@
1
+ name: Quality
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches: [main]
7
+ pull_request:
8
+ branches: [main]
9
+ schedule:
10
+ - cron: "20 14 * * 1"
11
+
12
+ jobs:
13
+ Quality:
14
+ permissions:
15
+ contents: read
16
+ uses: gesslar/Maint/.github/workflows/Quality.yaml@main
17
+ secrets: inherit
18
+ with:
19
+ package_manager: "auto"
20
+ perform_linting: "${{ vars.PERFORM_LINTING || 'yes' }}"
21
+ perform_testing: "${{ vars.PERFORM_TESTING || 'no' }}"
@@ -0,0 +1,17 @@
1
+ name: Release
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+ branches: [main]
7
+
8
+ jobs:
9
+ Release:
10
+ if: github.event.pull_request.merged == true
11
+ uses: gesslar/Maint/.github/workflows/Release.yaml@main
12
+ secrets: inherit
13
+ with:
14
+ package_manager: "auto"
15
+ quality_check: "Quality"
16
+ permissions:
17
+ contents: write
package/LICENSE.txt ADDED
@@ -0,0 +1,12 @@
1
+ Copyright (C) 2026 by Brian M. Workman bmw@gesslar.dev
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
7
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
8
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
10
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
11
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
12
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @gesslar/mediawiki-mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io/) server that lets
4
+ MCP-compatible clients create, edit, delete, read, and search articles on a
5
+ MediaWiki instance.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js `>=24.11.0`
10
+ - A MediaWiki instance with API access
11
+ - A [bot account](https://www.mediawiki.org/wiki/Manual:Bot_passwords) with
12
+ sufficient permissions for the operations you intend to perform
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install -g @gesslar/mediawiki-mcp
18
+ ```
19
+
20
+ Or run it directly via `npx`:
21
+
22
+ ```bash
23
+ npx @gesslar/mediawiki-mcp
24
+ ```
25
+
26
+ ## Configuration
27
+
28
+ The server is configured through environment variables:
29
+
30
+ | Variable | Description |
31
+ | --- | --- |
32
+ | `MEDIAWIKI_URL` | Base URL of the MediaWiki instance (e.g. `https://wiki.example.com`) |
33
+ | `MEDIAWIKI_BOT_USERNAME` | Bot account username |
34
+ | `MEDIAWIKI_BOT_PASSWORD` | Bot account password |
35
+
36
+ All three variables are required. The server will exit on startup if any are
37
+ missing.
38
+
39
+ > **Note:** The server currently authenticates all reads and writes
40
+ > (`private: true` on the underlying `Wikid` client). This is intentional for
41
+ > wikis that require authentication for read access. If you're pointing this at
42
+ > a fully public wiki, reads will still work — they'll just be authenticated
43
+ > when they wouldn't strictly need to be. This package is released under
44
+ > [0BSD](LICENSE.txt), so if the hardcoded behaviour doesn't suit your setup,
45
+ > you're welcome to fork it and tweak the `Wikid` constructor options to taste.
46
+
47
+ ## Usage with an MCP client
48
+
49
+ Add the server to your MCP client configuration. For example, in a Claude
50
+ Desktop `claude_desktop_config.json`:
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "mediawiki": {
56
+ "command": "npx",
57
+ "args": ["-y", "@gesslar/mediawiki-mcp"],
58
+ "env": {
59
+ "MEDIAWIKI_URL": "https://wiki.example.com",
60
+ "MEDIAWIKI_BOT_USERNAME": "YourBot@YourBotPassword",
61
+ "MEDIAWIKI_BOT_PASSWORD": "your-bot-password"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ The server communicates over stdio.
69
+
70
+ ## Tools
71
+
72
+ The server exposes the following tools:
73
+
74
+ ### `mediawiki_create_article`
75
+
76
+ Create a new article. Fails if the article already exists.
77
+
78
+ | Parameter | Type | Required | Description |
79
+ | --- | --- | --- | --- |
80
+ | `title` | string | yes | Title of the article to create |
81
+ | `content` | string | yes | Wikitext content for the article |
82
+ | `summary` | string | no | Edit summary |
83
+
84
+ ### `mediawiki_edit_article`
85
+
86
+ Edit an existing article. Supports full replacement, append, or prepend.
87
+
88
+ | Parameter | Type | Required | Description |
89
+ | --- | --- | --- | --- |
90
+ | `title` | string | yes | Title of the article to edit |
91
+ | `content` | string | yes | New wikitext content |
92
+ | `summary` | string | no | Edit summary |
93
+ | `append` | boolean | no | Append `content` instead of replacing |
94
+ | `prepend` | boolean | no | Prepend `content` instead of replacing |
95
+
96
+ ### `mediawiki_delete_article`
97
+
98
+ Delete an article. Requires delete permissions on the bot account.
99
+
100
+ | Parameter | Type | Required | Description |
101
+ | --- | --- | --- | --- |
102
+ | `title` | string | yes | Title of the article to delete |
103
+ | `reason` | string | no | Reason for deletion |
104
+
105
+ ### `mediawiki_get_article`
106
+
107
+ Return the current wikitext content of an article.
108
+
109
+ | Parameter | Type | Required | Description |
110
+ | --- | --- | --- | --- |
111
+ | `title` | string | yes | Title of the article to retrieve |
112
+
113
+ ### `mediawiki_search`
114
+
115
+ Search articles on the wiki.
116
+
117
+ | Parameter | Type | Required | Description |
118
+ | --- | --- | --- | --- |
119
+ | `query` | string | yes | Search query |
120
+ | `limit` | number | no | Maximum number of results (default: 10) |
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ # Start the server locally
126
+ npm start
127
+
128
+ # Lint
129
+ npm run lint
130
+ npm run lint:fix
131
+ ```
132
+
133
+ ## License
134
+
135
+ `@gesslar/mediawiki-mcp` is released under the [0BSD](LICENSE.txt).
136
+
137
+ This package includes or depends on third-party components under their own
138
+ licenses:
139
+
140
+ | Dependency | License |
141
+ | --- | --- |
142
+ | [@gesslar/wikid](https://github.com/gesslar/wikid) | 0BSD |
143
+ | [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/typescript-sdk) | MIT |
144
+ | [zod](https://github.com/colinhacks/zod) | MIT |
@@ -0,0 +1,11 @@
1
+ import uglify from "@gesslar/uglier"
2
+
3
+ export default [
4
+ ...uglify({
5
+ with: [
6
+ "lints-js", // default files: ["**/*.{js,mjs,cjs}"]
7
+ "lints-jsdoc", // default files: ["**/*.{js,mjs,cjs}"]
8
+ "node", // default files: ["**/*.{js,mjs,cjs}"]
9
+ ]
10
+ })
11
+ ]
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@gesslar/mediawiki-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Integrating with MediaWiki",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "mediawiki-mcp": "./src/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=24.11.0"
12
+ },
13
+ "scripts": {
14
+ "start": "node src/index.js",
15
+ "lint": "eslint src/",
16
+ "lint:fix": "eslint src/ --fix",
17
+ "submit": "npm publish --access public --//registry.npmjs.org/:_authToken=\"${NPM_ACCESS_TOKEN}\"",
18
+ "update": "npx npm-check-updates -u && npm install",
19
+ "pr": "gt submit --ai -p",
20
+ "patch": "npm version patch",
21
+ "minor": "npm version minor",
22
+ "major": "npm version major"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/gesslar/mediawiki-mcp.git"
27
+ },
28
+ "keywords": [
29
+ "wiki",
30
+ "mediawiki",
31
+ "mcp",
32
+ "model-context-protocol",
33
+ "janet",
34
+ "jackson",
35
+ "was",
36
+ "played"
37
+ ],
38
+ "author": "gesslar",
39
+ "license": "0BSD",
40
+ "homepage": "https://github.com/gesslar/mediawiki-mcp#readme",
41
+ "dependencies": {
42
+ "@gesslar/wikid": "^2.3.1",
43
+ "@modelcontextprotocol/sdk": "^1.29.0",
44
+ "zod": "^4.3.6"
45
+ },
46
+ "devDependencies": {
47
+ "@gesslar/uglier": "^2.4.1",
48
+ "eslint": "^10.2.0"
49
+ }
50
+ }
package/src/index.js ADDED
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+
3
+ import Wikid from "@gesslar/wikid"
4
+ import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js"
5
+ import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js"
6
+ import * as z from "zod/v4"
7
+ import pkg from "../package.json" with {type: "json"}
8
+
9
+ class MediaWikiMCPServer {
10
+ #client
11
+
12
+ constructor() {
13
+ this.server = new McpServer(
14
+ {
15
+ name: "mediawiki-mcp-server",
16
+ version: pkg.version,
17
+ },
18
+ {
19
+ capabilities: {
20
+ tools: {},
21
+ },
22
+ }
23
+ )
24
+
25
+ // Required environment variables
26
+ this.wikiUrl = process.env.MEDIAWIKI_URL
27
+ this.botUsername = process.env.MEDIAWIKI_BOT_USERNAME
28
+ this.botPassword = process.env.MEDIAWIKI_BOT_PASSWORD
29
+
30
+ if(!this.wikiUrl || !this.botUsername || !this.botPassword) {
31
+ console.error("Error: Required environment variables not set:")
32
+ console.error(" MEDIAWIKI_URL - The base URL of your MediaWiki instance")
33
+ console.error(" MEDIAWIKI_BOT_USERNAME - Bot account username")
34
+ console.error(" MEDIAWIKI_BOT_PASSWORD - Bot account password")
35
+ process.exit(1)
36
+ }
37
+
38
+ // Normalize URL (remove trailing slash)
39
+ this.wikiUrl = this.wikiUrl.replace(/\/$/, "")
40
+
41
+ this.setupTools()
42
+ }
43
+
44
+ /**
45
+ * Create and authenticate a MediaWiki client
46
+ *
47
+ * @returns {Promise<Wikid>} Authenticated client
48
+ */
49
+ async #assureClient() {
50
+ if(!this.#client) {
51
+ const client = new Wikid({
52
+ baseUrl: this.wikiUrl,
53
+ botUsername: this.botUsername,
54
+ botPassword: this.botPassword,
55
+ private: true // Your wiki requires auth for reads
56
+ })
57
+
58
+ const loginResult = await client.login()
59
+
60
+ if(!loginResult.ok)
61
+ throw new Error(`Login failed: ${loginResult.error?.message ?? "unknown error"}`)
62
+
63
+ this.#client = client
64
+ }
65
+
66
+ return this.#client
67
+ }
68
+
69
+ setupTools() {
70
+ this.server.registerTool("mediawiki_create_article", {
71
+ description:
72
+ "Create a new article on the MediaWiki instance. Will fail if the article already exists.",
73
+ inputSchema: z.object({
74
+ title: z.string().describe("Title of the article to create"),
75
+ content: z.string().describe("Wikitext content for the article"),
76
+ summary: z.string().optional().describe("Edit summary (reason for creating the article)"),
77
+ }),
78
+ }, async({title, content, summary}) => {
79
+ try {
80
+ await this.#assureClient()
81
+
82
+ const result = await this.#client.post("api.php", {
83
+ action: "edit",
84
+ title,
85
+ text: content,
86
+ summary: summary || "Created via MediaWiki MCP",
87
+ createonly: "true",
88
+ format: "json"
89
+ })
90
+
91
+ if(result.edit?.result !== "Success")
92
+ throw new Error(`Edit failed: ${JSON.stringify(result)}`)
93
+
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: `✓ Successfully created article "${title}"\nRevision ID: ${result.edit.newrevid}\nTimestamp: ${result.edit.newtimestamp}`,
99
+ },
100
+ ],
101
+ }
102
+ } catch(error) {
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: `Error: ${error.message}`,
108
+ },
109
+ ],
110
+ isError: true,
111
+ }
112
+ }
113
+ })
114
+
115
+ this.server.registerTool("mediawiki_edit_article", {
116
+ description:
117
+ "Edit an existing article on the MediaWiki instance. Creates the article if it does not already exist.",
118
+ inputSchema: z.object({
119
+ title: z.string().describe("Title of the article to edit"),
120
+ content: z.string().describe("New wikitext content for the article"),
121
+ summary: z.string().optional().describe("Edit summary (reason for the edit)"),
122
+ append: z.boolean().optional().describe("If true, append content instead of replacing. Default: false"),
123
+ prepend: z.boolean().optional().describe("If true, prepend content instead of replacing. Default: false"),
124
+ }),
125
+ }, async({title, content, summary, append, prepend}) => {
126
+ try {
127
+ await this.#assureClient()
128
+
129
+ const editParams = {
130
+ action: "edit",
131
+ title,
132
+ summary: summary || "Edited via MediaWiki MCP",
133
+ format: "json"
134
+ }
135
+
136
+ if(append)
137
+ editParams.appendtext = content
138
+ else if(prepend)
139
+ editParams.prependtext = content
140
+ else
141
+ editParams.text = content
142
+
143
+ const result = await this.#client.post("api.php", editParams)
144
+
145
+ if(result.edit?.result !== "Success")
146
+ throw new Error(`Edit failed: ${JSON.stringify(result)}`)
147
+
148
+ const action = result.edit.new ? "created" : "edited"
149
+
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: `✓ Successfully ${action} article "${title}"\nRevision ID: ${result.edit.newrevid}\nTimestamp: ${result.edit.newtimestamp}`,
155
+ },
156
+ ],
157
+ }
158
+ } catch(error) {
159
+ return {
160
+ content: [
161
+ {
162
+ type: "text",
163
+ text: `Error: ${error.message}`,
164
+ },
165
+ ],
166
+ isError: true,
167
+ }
168
+ }
169
+ })
170
+
171
+ this.server.registerTool("mediawiki_delete_article", {
172
+ description:
173
+ "Delete an article from the MediaWiki instance. Requires appropriate permissions.",
174
+ inputSchema: z.object({
175
+ title: z.string().describe("Title of the article to delete"),
176
+ reason: z.string().optional().describe("Reason for deletion"),
177
+ }),
178
+ }, async({title, reason}) => {
179
+ try {
180
+ await this.#assureClient()
181
+
182
+ const deleteReason = reason || "Deleted via MediaWiki MCP"
183
+ const result = await this.#client.post("api.php", {
184
+ action: "delete",
185
+ title,
186
+ reason: deleteReason,
187
+ format: "json"
188
+ })
189
+
190
+ if(!result.delete)
191
+ throw new Error(`Delete failed: ${JSON.stringify(result)}`)
192
+
193
+ return {
194
+ content: [
195
+ {
196
+ type: "text",
197
+ text: `✓ Successfully deleted article "${title}"\nReason: ${deleteReason}`,
198
+ },
199
+ ],
200
+ }
201
+ } catch(error) {
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: `Error: ${error.message}`,
207
+ },
208
+ ],
209
+ isError: true,
210
+ }
211
+ }
212
+ })
213
+
214
+ this.server.registerTool("mediawiki_get_article", {
215
+ description:
216
+ "Get the current content of an article from the MediaWiki instance.",
217
+ inputSchema: z.object({
218
+ title: z.string().describe("Title of the article to retrieve"),
219
+ }),
220
+ }, async({title}) => {
221
+ try {
222
+ await this.#assureClient()
223
+
224
+ const data = await this.#client.get("api.php", {
225
+ action: "query",
226
+ titles: title,
227
+ prop: "revisions",
228
+ rvprop: "content",
229
+ rvslots: "main",
230
+ format: "json"
231
+ })
232
+
233
+ const pages = data.query.pages
234
+ const pageId = Object.keys(pages)[0]
235
+ const page = pages[pageId]
236
+
237
+ if(page.missing) {
238
+ return {
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: `Article "${title}" does not exist.`,
243
+ },
244
+ ],
245
+ }
246
+ }
247
+
248
+ const content = page.revisions?.[0]?.slots?.main?.["*"] ?? ""
249
+
250
+ if(!content) {
251
+ return {
252
+ content: [
253
+ {
254
+ type: "text",
255
+ text: `Article "${title}" exists but has no readable content.`,
256
+ },
257
+ ],
258
+ }
259
+ }
260
+
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: `Content of "${title}":\n\n${content}`,
266
+ },
267
+ ],
268
+ }
269
+ } catch(error) {
270
+ return {
271
+ content: [
272
+ {
273
+ type: "text",
274
+ text: `Error: ${error.message}`,
275
+ },
276
+ ],
277
+ isError: true,
278
+ }
279
+ }
280
+ })
281
+
282
+ this.server.registerTool("mediawiki_search", {
283
+ description: "Search for articles on the MediaWiki instance.",
284
+ inputSchema: z.object({
285
+ query: z.string().describe("Search query"),
286
+ limit: z.number().optional().describe("Maximum number of results to return (default: 10)"),
287
+ }),
288
+ }, async({query, limit}) => {
289
+ try {
290
+ await this.#assureClient()
291
+
292
+ const data = await this.#client.get("api.php", {
293
+ action: "query",
294
+ list: "search",
295
+ srsearch: query,
296
+ srlimit: limit || 10,
297
+ format: "json"
298
+ })
299
+
300
+ const results = data.query.search
301
+
302
+ if(results.length === 0) {
303
+ return {
304
+ content: [
305
+ {
306
+ type: "text",
307
+ text: `No results found for "${query}".`,
308
+ },
309
+ ],
310
+ }
311
+ }
312
+
313
+ const stripTags = s => {
314
+ let prev
315
+ let curr = s
316
+
317
+ do {
318
+ prev = curr
319
+ curr = curr.replace(/<[^<>]*>/g, "")
320
+ } while(curr !== prev)
321
+
322
+ return curr
323
+ }
324
+
325
+ const formatted = results.map((r, i) =>
326
+ `${i + 1}. ${r.title}\n Snippet: ${stripTags(r.snippet)}`
327
+ ).join("\n\n")
328
+
329
+ return {
330
+ content: [
331
+ {
332
+ type: "text",
333
+ text: `Found ${results.length} result(s) for "${query}":\n\n${formatted}`,
334
+ },
335
+ ],
336
+ }
337
+ } catch(error) {
338
+ return {
339
+ content: [
340
+ {
341
+ type: "text",
342
+ text: `Error: ${error.message}`,
343
+ },
344
+ ],
345
+ isError: true,
346
+ }
347
+ }
348
+ })
349
+ }
350
+
351
+ async run() {
352
+ const transport = new StdioServerTransport()
353
+ await this.server.connect(transport)
354
+
355
+ console.error("MediaWiki MCP Server running on stdio")
356
+ }
357
+ }
358
+
359
+ const server = new MediaWikiMCPServer()
360
+ server.run().catch(console.error)