@fswap/mcp-outline 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +125 -0
  3. package/dist/index.js +556 -0
  4. package/package.json +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gleb Okhrimenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @fswap/mcp-outline
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server for [Outline](https://www.getoutline.com) that lets Claude Desktop, Claude Code, Cursor and other MCP clients search, read, create and update your wiki.
4
+
5
+ Runs locally over stdio. No install step — clients launch it with `npx`.
6
+
7
+ ## Quick start
8
+
9
+ 1. Create an API token in Outline under **Settings → API**.
10
+ 2. Run the interactive setup once:
11
+
12
+ ```bash
13
+ npx -y @fswap/mcp-outline setup
14
+ ```
15
+
16
+ It asks for your Outline URL and token, verifies them against `auth.info`, and stores them in your OS config directory (mode `0600`).
17
+
18
+ 3. Add the server to your client. Every value asked in `setup` can be skipped with Enter; anything you skip goes into the `env` block shown below instead. `setup --print` shows these snippets again at any time.
19
+
20
+ **Claude Desktop** (`claude_desktop_config.json`) and **Cursor** (`~/.cursor/mcp.json` or `<project>/.cursor/mcp.json`):
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "outline": {
26
+ "command": "npx",
27
+ "args": ["-y", "@fswap/mcp-outline"]
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ **Codex** (`~/.codex/config.toml`):
34
+
35
+ ```toml
36
+ [mcp_servers.outline]
37
+ command = "npx"
38
+ args = ["-y", "@fswap/mcp-outline"]
39
+ ```
40
+
41
+ **Claude Code**:
42
+
43
+ ```bash
44
+ claude mcp add outline -- npx -y @fswap/mcp-outline
45
+ ```
46
+
47
+ ### Without `setup` (environment variables)
48
+
49
+ Environment variables take precedence over the config file, so you can skip `setup` entirely (or skip individual values in it and set them here):
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "outline": {
55
+ "command": "npx",
56
+ "args": ["-y", "@fswap/mcp-outline"],
57
+ "env": {
58
+ "OUTLINE_URL": "https://app.getoutline.com",
59
+ "OUTLINE_API_TOKEN": "ol_api_..."
60
+ }
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ Codex equivalent:
67
+
68
+ ```toml
69
+ [mcp_servers.outline]
70
+ command = "npx"
71
+ args = ["-y", "@fswap/mcp-outline"]
72
+ [mcp_servers.outline.env]
73
+ OUTLINE_URL = "https://app.getoutline.com"
74
+ OUTLINE_API_TOKEN = "ol_api_..."
75
+ ```
76
+
77
+ | Variable | Purpose |
78
+ |---|---|
79
+ | `OUTLINE_URL` | Outline base URL (cloud or self-hosted) |
80
+ | `OUTLINE_API_TOKEN` | API token |
81
+ | `OUTLINE_ALLOW_DELETE` | `true` to expose `delete_document` |
82
+ | `OUTLINE_DEFAULT_COLLECTION` | Optional default collection name |
83
+
84
+ ## Tools
85
+
86
+ | Tool | Outline endpoint | Notes |
87
+ |---|---|---|
88
+ | `list_collections` | `collections.list` | id, name, description, url |
89
+ | `get_collection` | `collections.info` | includes document tree |
90
+ | `search_documents` | `documents.search` | query, optional `collectionId`; returns snippets, not bodies |
91
+ | `get_document` | `documents.info` | full markdown body; accepts id or URL slug |
92
+ | `list_documents` | `documents.list` | filter by `collectionId` / `parentDocumentId` |
93
+ | `create_document` | `documents.create` | title, markdown, collection, optional parent; published by default |
94
+ | `update_document` | `documents.update` | title/text; `append=true` appends instead of replacing |
95
+ | `move_document` | `documents.move` | change collection and/or parent |
96
+ | `archive_document` | `documents.archive` | reversible |
97
+ | `delete_document` | `documents.delete` | only when delete is allowed (setup answer or `OUTLINE_ALLOW_DELETE=true`) |
98
+
99
+ API errors are returned to the model as `isError` results rather than crashing the server.
100
+
101
+ ## Development
102
+
103
+ TypeScript source in `src/`, bundled to `dist/` with [tsdown](https://tsdown.dev). Only `dist/` is published.
104
+
105
+ ```bash
106
+ npm install
107
+ npm run build # tsdown → dist/index.js
108
+ npm run lint # eslint (typescript-eslint)
109
+ npm run typecheck # tsc --noEmit
110
+ npm test # builds, then spawns the server and checks the tool list
111
+ npm run check # all of the above (also runs on prepublishOnly)
112
+ OUTLINE_URL=... OUTLINE_API_TOKEN=... npm run inspect # MCP Inspector UI against dist/
113
+ ```
114
+
115
+ Never write to stdout from server code — it is the protocol channel. Use `console.error`.
116
+
117
+ ## Reset
118
+
119
+ ```bash
120
+ npx -y @fswap/mcp-outline setup --reset
121
+ ```
122
+
123
+ ## License
124
+
125
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import envPaths from "env-paths";
8
+ import { z } from "zod";
9
+ import * as p from "@clack/prompts";
10
+ //#region \0rolldown/runtime.js
11
+ var __defProp = Object.defineProperty;
12
+ var __esmMin = (fn, res, err) => () => {
13
+ if (err) throw err[0];
14
+ try {
15
+ return fn && (res = fn(fn = 0)), res;
16
+ } catch (e) {
17
+ throw err = [e], e;
18
+ }
19
+ };
20
+ var __exportAll = (all, no_symbols) => {
21
+ let target = {};
22
+ for (var name in all) __defProp(target, name, {
23
+ get: all[name],
24
+ enumerable: true
25
+ });
26
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
27
+ return target;
28
+ };
29
+ //#endregion
30
+ //#region src/config.ts
31
+ function configPath() {
32
+ return path.join(paths.config, "config.json");
33
+ }
34
+ function truthy(v) {
35
+ return v != null && /^(1|true|yes|on)$/i.test(v.trim());
36
+ }
37
+ function readConfigFile() {
38
+ try {
39
+ return JSON.parse(fs.readFileSync(configPath(), "utf8"));
40
+ } catch (err) {
41
+ if (err.code === "ENOENT") return null;
42
+ console.error(`Warning: could not read ${configPath()}: ${err.message}`);
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * Resolve runtime configuration.
48
+ * Precedence: environment variables > config file written by `setup`.
49
+ * Returns null when neither provides url + token.
50
+ */
51
+ function loadConfig() {
52
+ const env = process.env;
53
+ const file = readConfigFile() ?? {};
54
+ const url = env.OUTLINE_URL || file.url;
55
+ const token = env.OUTLINE_API_TOKEN || file.token;
56
+ if (!url || !token) return null;
57
+ const deleteEnv = env.OUTLINE_ALLOW_DELETE ?? env.ALLOW_DELETE;
58
+ const allowDelete = deleteEnv != null ? truthy(deleteEnv) : Boolean(file.allowDelete);
59
+ return {
60
+ url: url.replace(/\/+$/, ""),
61
+ token,
62
+ allowDelete,
63
+ defaultCollection: env.OUTLINE_DEFAULT_COLLECTION || file.defaultCollection || ""
64
+ };
65
+ }
66
+ function saveConfig(config) {
67
+ const file = configPath();
68
+ fs.mkdirSync(path.dirname(file), {
69
+ recursive: true,
70
+ mode: 448
71
+ });
72
+ fs.writeFileSync(file, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
73
+ fs.chmodSync(file, 384);
74
+ return file;
75
+ }
76
+ function deleteConfig() {
77
+ try {
78
+ fs.unlinkSync(configPath());
79
+ return true;
80
+ } catch (err) {
81
+ if (err.code === "ENOENT") return false;
82
+ throw err;
83
+ }
84
+ }
85
+ var pkg, PACKAGE_NAME, VERSION, paths;
86
+ var init_config = __esmMin((() => {
87
+ pkg = createRequire(import.meta.url)("../package.json");
88
+ PACKAGE_NAME = pkg.name;
89
+ VERSION = pkg.version;
90
+ paths = envPaths("mcp-outline", { suffix: "" });
91
+ }));
92
+ //#endregion
93
+ //#region src/client.ts
94
+ function makeClient(baseUrl, token) {
95
+ const base = baseUrl.replace(/\/+$/, "");
96
+ async function post(endpoint, body = {}) {
97
+ const url = `${base}/api/${endpoint}`;
98
+ let res;
99
+ try {
100
+ res = await fetch(url, {
101
+ method: "POST",
102
+ headers: {
103
+ Authorization: `Bearer ${token}`,
104
+ "Content-Type": "application/json",
105
+ Accept: "application/json"
106
+ },
107
+ body: JSON.stringify(body)
108
+ });
109
+ } catch (err) {
110
+ throw new Error(`POST ${endpoint} failed: ${err.message}`);
111
+ }
112
+ if (!res.ok) {
113
+ const text = await res.text();
114
+ let detail = text;
115
+ try {
116
+ const json = JSON.parse(text);
117
+ detail = json.message || json.error || text;
118
+ } catch {}
119
+ throw new Error(`POST ${endpoint} → ${res.status}: ${detail}`);
120
+ }
121
+ return await res.json();
122
+ }
123
+ return {
124
+ post,
125
+ baseUrl: base
126
+ };
127
+ }
128
+ /** Cheap authenticated call used by `setup` to validate URL + token. */
129
+ async function authInfo(baseUrl, token) {
130
+ const { post } = makeClient(baseUrl, token);
131
+ const { data } = await post("auth.info");
132
+ return data;
133
+ }
134
+ var init_client = __esmMin((() => {}));
135
+ //#endregion
136
+ //#region src/tools/_shared.ts
137
+ init_config();
138
+ init_client();
139
+ function ok(value) {
140
+ return { content: [{
141
+ type: "text",
142
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
143
+ }] };
144
+ }
145
+ function fail(err) {
146
+ return {
147
+ isError: true,
148
+ content: [{
149
+ type: "text",
150
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
151
+ }]
152
+ };
153
+ }
154
+ /** Wrap a tool handler so API errors become isError results instead of crashes. */
155
+ function guard(fn) {
156
+ return async (args) => {
157
+ try {
158
+ return await fn(args);
159
+ } catch (err) {
160
+ return fail(err);
161
+ }
162
+ };
163
+ }
164
+ function stripUndefined(obj) {
165
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
166
+ }
167
+ //#endregion
168
+ //#region src/tools/collections.ts
169
+ function registerCollectionTools(server, outline) {
170
+ server.registerTool("list_collections", {
171
+ title: "List collections",
172
+ description: "List Outline collections (top-level groupings of documents). Returns id, name, description and url. Use the id with search_documents, list_documents or create_document.",
173
+ inputSchema: {
174
+ query: z.string().optional().describe("Filter collections by name (substring match)"),
175
+ limit: z.number().int().min(1).max(100).default(25).describe("Max collections to return")
176
+ }
177
+ }, guard(async ({ query, limit }) => {
178
+ const body = {
179
+ limit,
180
+ sort: "name",
181
+ direction: "ASC"
182
+ };
183
+ if (query) body.query = query;
184
+ const { data } = await outline.post("collections.list", body);
185
+ return ok(data.map((c) => ({
186
+ id: c.id,
187
+ name: c.name,
188
+ description: c.description || null,
189
+ url: c.url
190
+ })));
191
+ }));
192
+ server.registerTool("get_collection", {
193
+ title: "Get collection",
194
+ description: "Get one Outline collection including its document tree (ids, titles and nesting). Useful to understand how a collection is organised before creating or moving documents.",
195
+ inputSchema: { id: z.string().describe("Collection id") }
196
+ }, guard(async ({ id }) => {
197
+ const { data } = await outline.post("collections.info", { id });
198
+ return ok({
199
+ id: data.id,
200
+ name: data.name,
201
+ description: data.description || null,
202
+ url: data.url,
203
+ documents: data.documents ?? []
204
+ });
205
+ }));
206
+ }
207
+ //#endregion
208
+ //#region src/tools/documents.ts
209
+ function summarize(d) {
210
+ return {
211
+ id: d.id,
212
+ title: d.title,
213
+ url: d.url,
214
+ urlId: d.urlId,
215
+ collectionId: d.collectionId,
216
+ parentDocumentId: d.parentDocumentId ?? null,
217
+ updatedAt: d.updatedAt,
218
+ publishedAt: d.publishedAt ?? null,
219
+ archivedAt: d.archivedAt ?? null
220
+ };
221
+ }
222
+ function registerDocumentTools(server, outline, { allowDelete = false } = {}) {
223
+ server.registerTool("search_documents", {
224
+ title: "Search documents",
225
+ description: "Full-text search across Outline documents. Returns id, title, url and a short context snippet per hit. Does NOT return document bodies — call get_document with an id to read one.",
226
+ inputSchema: {
227
+ query: z.string().min(1).describe("Search terms"),
228
+ collectionId: z.string().optional().describe("Restrict results to one collection"),
229
+ limit: z.number().int().min(1).max(50).default(10).describe("Max results"),
230
+ offset: z.number().int().min(0).default(0).describe("Pagination offset")
231
+ }
232
+ }, guard(async ({ query, collectionId, limit, offset }) => {
233
+ const { data } = await outline.post("documents.search", stripUndefined({
234
+ query,
235
+ collectionId,
236
+ limit,
237
+ offset
238
+ }));
239
+ return ok(data.map((r) => ({
240
+ id: r.document.id,
241
+ title: r.document.title,
242
+ url: r.document.url,
243
+ collectionId: r.document.collectionId,
244
+ updatedAt: r.document.updatedAt,
245
+ snippet: r.context,
246
+ ranking: r.ranking
247
+ })));
248
+ }));
249
+ server.registerTool("get_document", {
250
+ title: "Get document",
251
+ description: "Fetch a single Outline document including its full markdown body. Accepts a document id, a urlId (the slug part of the URL) or a share id.",
252
+ inputSchema: { id: z.string().describe("Document id, urlId or share id") }
253
+ }, guard(async ({ id }) => {
254
+ const { data } = await outline.post("documents.info", { id });
255
+ return ok({
256
+ ...summarize(data),
257
+ text: data.text ?? ""
258
+ });
259
+ }));
260
+ server.registerTool("list_documents", {
261
+ title: "List documents",
262
+ description: "List documents, optionally filtered by collection and/or parent document. Returns id, title, url and timestamps (no bodies). Sorted by most recently updated by default.",
263
+ inputSchema: {
264
+ collectionId: z.string().optional().describe("Only documents in this collection"),
265
+ parentDocumentId: z.string().optional().describe("Only direct children of this document"),
266
+ sort: z.enum([
267
+ "updatedAt",
268
+ "createdAt",
269
+ "title",
270
+ "index"
271
+ ]).default("updatedAt"),
272
+ direction: z.enum(["ASC", "DESC"]).default("DESC"),
273
+ limit: z.number().int().min(1).max(100).default(25),
274
+ offset: z.number().int().min(0).default(0)
275
+ }
276
+ }, guard(async ({ collectionId, parentDocumentId, sort, direction, limit, offset }) => {
277
+ const { data } = await outline.post("documents.list", stripUndefined({
278
+ collectionId,
279
+ parentDocumentId,
280
+ sort,
281
+ direction,
282
+ limit,
283
+ offset
284
+ }));
285
+ return ok(data.map(summarize));
286
+ }));
287
+ server.registerTool("create_document", {
288
+ title: "Create document",
289
+ description: "Create a new Outline document from markdown. Published by default; pass publish=false to save a draft. Requires a collectionId (use list_collections). Optionally nest under a parent document.",
290
+ inputSchema: {
291
+ title: z.string().min(1).describe("Document title"),
292
+ text: z.string().default("").describe("Document body in markdown"),
293
+ collectionId: z.string().describe("Collection to create the document in"),
294
+ parentDocumentId: z.string().optional().describe("Nest under this document"),
295
+ publish: z.boolean().default(true).describe("Publish immediately (false = draft)")
296
+ }
297
+ }, guard(async ({ title, text, collectionId, parentDocumentId, publish }) => {
298
+ const { data } = await outline.post("documents.create", stripUndefined({
299
+ title,
300
+ text,
301
+ collectionId,
302
+ parentDocumentId,
303
+ publish
304
+ }));
305
+ return ok(summarize(data));
306
+ }));
307
+ server.registerTool("update_document", {
308
+ title: "Update document",
309
+ description: "Update an existing document's title and/or markdown body. By default `text` REPLACES the body; set append=true to add `text` to the end instead. Only the fields you pass are changed.",
310
+ inputSchema: {
311
+ id: z.string().describe("Document id"),
312
+ title: z.string().optional().describe("New title"),
313
+ text: z.string().optional().describe("New markdown body (or text to append when append=true)"),
314
+ append: z.boolean().default(false).describe("Append `text` to the existing body instead of replacing it"),
315
+ publish: z.boolean().optional().describe("Publish a draft as part of this update"),
316
+ done: z.boolean().optional().describe("Mark the editing session as finished (triggers notifications)")
317
+ }
318
+ }, guard(async ({ id, title, text, append, publish, done }) => {
319
+ if (title === void 0 && text === void 0 && publish === void 0) throw new Error("Nothing to update: pass title, text or publish.");
320
+ const { data } = await outline.post("documents.update", stripUndefined({
321
+ id,
322
+ title,
323
+ text,
324
+ append: text !== void 0 ? append : void 0,
325
+ publish,
326
+ done
327
+ }));
328
+ return ok(summarize(data));
329
+ }));
330
+ server.registerTool("move_document", {
331
+ title: "Move document",
332
+ description: "Move a document to another collection and/or under a different parent document. Omit parentDocumentId to move it to the top level of the collection.",
333
+ inputSchema: {
334
+ id: z.string().describe("Document id"),
335
+ collectionId: z.string().optional().describe("Target collection id"),
336
+ parentDocumentId: z.string().optional().describe("Target parent document id"),
337
+ index: z.number().int().min(0).optional().describe("Position among siblings")
338
+ }
339
+ }, guard(async ({ id, collectionId, parentDocumentId, index }) => {
340
+ if (!collectionId && !parentDocumentId) throw new Error("Pass collectionId and/or parentDocumentId.");
341
+ const { data } = await outline.post("documents.move", stripUndefined({
342
+ id,
343
+ collectionId,
344
+ parentDocumentId,
345
+ index
346
+ }));
347
+ return ok({ documents: (data.documents ?? []).map(summarize) });
348
+ }));
349
+ server.registerTool("archive_document", {
350
+ title: "Archive document",
351
+ description: "Archive a document (reversible; it disappears from the collection but can be restored from the archive). Prefer this over delete_document.",
352
+ inputSchema: { id: z.string().describe("Document id") }
353
+ }, guard(async ({ id }) => {
354
+ const { data } = await outline.post("documents.archive", { id });
355
+ return ok(summarize(data));
356
+ }));
357
+ if (allowDelete) server.registerTool("delete_document", {
358
+ title: "Delete document",
359
+ description: "Move a document to the trash. Documents in the trash are permanently deleted after 30 days. Only use when the user explicitly asks to delete; archive_document is the safer alternative.",
360
+ inputSchema: {
361
+ id: z.string().describe("Document id"),
362
+ permanent: z.boolean().default(false).describe("Permanently delete instead of trashing (irreversible)")
363
+ }
364
+ }, guard(async ({ id, permanent }) => {
365
+ await outline.post("documents.delete", {
366
+ id,
367
+ permanent
368
+ });
369
+ return ok({
370
+ deleted: id,
371
+ permanent
372
+ });
373
+ }));
374
+ }
375
+ //#endregion
376
+ //#region src/snippets.ts
377
+ function jsonSnippet(missing) {
378
+ const server = {
379
+ command: "npx",
380
+ args: ["-y", PACKAGE_NAME]
381
+ };
382
+ if (Object.keys(missing).length > 0) server.env = missing;
383
+ return JSON.stringify({ mcpServers: { [SERVER_KEY]: server } }, null, 2);
384
+ }
385
+ function tomlSnippet(missing) {
386
+ const lines = [
387
+ `[mcp_servers.${SERVER_KEY}]`,
388
+ "command = \"npx\"",
389
+ `args = ["-y", "${PACKAGE_NAME}"]`
390
+ ];
391
+ if (Object.keys(missing).length > 0) {
392
+ lines.push(`[mcp_servers.${SERVER_KEY}.env]`);
393
+ for (const [k, v] of Object.entries(missing)) lines.push(`${k} = "${v}"`);
394
+ }
395
+ return lines.join("\n");
396
+ }
397
+ function claudeCodeSnippet(missing) {
398
+ const env = Object.entries(missing).map(([k, v]) => `-e ${k}=${v} `).join("");
399
+ return `claude mcp add ${SERVER_KEY} ${env}-- npx -y ${PACKAGE_NAME}`;
400
+ }
401
+ function clientSnippets(missing = {}) {
402
+ return [
403
+ {
404
+ title: "Claude Desktop — claude_desktop_config.json",
405
+ body: jsonSnippet(missing)
406
+ },
407
+ {
408
+ title: "Cursor — ~/.cursor/mcp.json (or <project>/.cursor/mcp.json)",
409
+ body: jsonSnippet(missing)
410
+ },
411
+ {
412
+ title: "Codex — ~/.codex/config.toml",
413
+ body: tomlSnippet(missing)
414
+ },
415
+ {
416
+ title: "Claude Code",
417
+ body: claudeCodeSnippet(missing)
418
+ }
419
+ ];
420
+ }
421
+ var SERVER_KEY, ENV_URL, ENV_TOKEN;
422
+ var init_snippets = __esmMin((() => {
423
+ SERVER_KEY = "outline";
424
+ ENV_URL = "OUTLINE_URL";
425
+ ENV_TOKEN = "OUTLINE_API_TOKEN";
426
+ }));
427
+ //#endregion
428
+ //#region src/setup.ts
429
+ var setup_exports = /* @__PURE__ */ __exportAll({ runSetup: () => runSetup });
430
+ function abort() {
431
+ p.cancel("Setup aborted.");
432
+ process.exit(1);
433
+ }
434
+ function printSnippets(missing) {
435
+ for (const s of clientSnippets(missing)) p.note(s.body, s.title);
436
+ }
437
+ async function runSetup(args = []) {
438
+ if (args.includes("--reset")) {
439
+ const removed = deleteConfig();
440
+ console.log(removed ? `Removed ${configPath()}` : `Nothing to remove (${configPath()} does not exist)`);
441
+ return;
442
+ }
443
+ const existing = readConfigFile() ?? {};
444
+ if (args.includes("--print")) {
445
+ const missing = {};
446
+ if (!existing.url) missing[ENV_URL] = "https://app.getoutline.com";
447
+ if (!existing.token) missing[ENV_TOKEN] = "ol_api_...";
448
+ printSnippets(missing);
449
+ return;
450
+ }
451
+ p.intro(`${PACKAGE_NAME} setup`);
452
+ p.log.info("Press Enter to skip any value. Skipped values can be supplied later via the env block of your MCP client config.");
453
+ const answers = await p.group({
454
+ url: () => p.text({
455
+ message: `Outline URL (${ENV_URL})`,
456
+ placeholder: "https://app.getoutline.com — Enter to skip",
457
+ initialValue: existing.url ?? "",
458
+ validate: (v) => !v || /^https?:\/\/\S+$/.test(v) ? void 0 : "Must start with http:// or https://"
459
+ }),
460
+ token: () => p.password({
461
+ message: `API token (${ENV_TOKEN}; Settings → API in Outline) — Enter to skip`,
462
+ mask: "▪"
463
+ }),
464
+ defaultCollection: () => p.text({
465
+ message: "Default collection name (optional)",
466
+ initialValue: existing.defaultCollection ?? "",
467
+ defaultValue: ""
468
+ }),
469
+ allowDelete: () => p.confirm({
470
+ message: "Allow delete tools?",
471
+ initialValue: Boolean(existing.allowDelete)
472
+ })
473
+ }, { onCancel: abort });
474
+ const url = (answers.url ?? "").trim().replace(/\/+$/, "") || void 0;
475
+ const token = (answers.token ?? "").trim() || existing.token || void 0;
476
+ if (url && token) {
477
+ const s = p.spinner();
478
+ s.start("Checking connection");
479
+ try {
480
+ const me = await authInfo(url, token);
481
+ const who = me.user?.email ?? me.user?.name ?? "unknown user";
482
+ s.stop(`ok (logged in as ${who} on ${me.team?.name ?? url})`);
483
+ } catch (err) {
484
+ s.stop("failed");
485
+ p.cancel(`Could not authenticate: ${err.message}`);
486
+ process.exit(1);
487
+ }
488
+ } else p.log.warn("Skipping connection check because URL and/or token were not provided.");
489
+ const config = {
490
+ defaultCollection: (answers.defaultCollection ?? "").trim(),
491
+ allowDelete: Boolean(answers.allowDelete)
492
+ };
493
+ if (url) config.url = url;
494
+ if (token) config.token = token;
495
+ const file = saveConfig(config);
496
+ const missing = {};
497
+ if (!url) missing[ENV_URL] = "https://app.getoutline.com";
498
+ if (!token) missing[ENV_TOKEN] = "ol_api_...";
499
+ if (Object.keys(missing).length > 0) p.log.warn(`Still needed: ${Object.keys(missing).join(", ")}. Add them to the env block below, or run setup again.`);
500
+ printSnippets(missing);
501
+ p.outro(`Saved to ${file}`);
502
+ }
503
+ var init_setup = __esmMin((() => {
504
+ init_config();
505
+ init_snippets();
506
+ }));
507
+ //#endregion
508
+ //#region src/index.ts
509
+ init_config();
510
+ init_client();
511
+ const [cmd, ...rest] = process.argv.slice(2);
512
+ if (cmd === "setup") {
513
+ const { runSetup } = await Promise.resolve().then(() => (init_setup(), setup_exports));
514
+ await runSetup(rest);
515
+ } else if (cmd === "--help" || cmd === "-h" || cmd === "help") printHelp();
516
+ else if (cmd === "--version" || cmd === "-v") console.log(VERSION);
517
+ else await runServer();
518
+ function printHelp() {
519
+ console.log(`${PACKAGE_NAME} ${VERSION}
520
+
521
+ Usage:
522
+ npx ${PACKAGE_NAME} start the MCP server (stdio)
523
+ npx ${PACKAGE_NAME} setup interactive configuration
524
+ npx ${PACKAGE_NAME} setup --reset delete stored configuration
525
+ npx ${PACKAGE_NAME} setup --print print MCP client config snippets
526
+
527
+ Configuration precedence:
528
+ 1. OUTLINE_URL, OUTLINE_API_TOKEN, OUTLINE_ALLOW_DELETE env vars
529
+ 2. ${configPath()}
530
+ `);
531
+ }
532
+ async function runServer() {
533
+ const config = loadConfig();
534
+ if (!config) {
535
+ console.error(`No configuration found. Run: npx ${PACKAGE_NAME} setup`);
536
+ console.error("(or set OUTLINE_URL and OUTLINE_API_TOKEN in the environment)");
537
+ process.exit(1);
538
+ }
539
+ if (process.stdin.isTTY) {
540
+ console.error(`${PACKAGE_NAME} is an MCP stdio server and expects to be launched by an MCP client.`);
541
+ console.error(`Run "npx ${PACKAGE_NAME} setup" to configure it, or "npx ${PACKAGE_NAME} --help".`);
542
+ process.exit(1);
543
+ }
544
+ const outline = makeClient(config.url, config.token);
545
+ const server = new McpServer({
546
+ name: "mcp-outline",
547
+ version: VERSION
548
+ });
549
+ registerCollectionTools(server, outline);
550
+ registerDocumentTools(server, outline, { allowDelete: config.allowDelete });
551
+ const transport = new StdioServerTransport();
552
+ await server.connect(transport);
553
+ console.error(`mcp-outline ${VERSION} connected (${config.url}, delete tools ${config.allowDelete ? "enabled" : "disabled"})`);
554
+ }
555
+ //#endregion
556
+ export {};
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@fswap/mcp-outline",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Outline (getoutline.com) — search, read, create and update wiki documents from Claude",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp-outline": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "scripts": {
16
+ "build": "tsdown",
17
+ "dev": "tsdown --watch",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "eslint src test",
20
+ "test": "npm run build && node test/smoke.test.mjs",
21
+ "check": "npm run lint && npm run typecheck && npm test",
22
+ "start": "node dist/index.js",
23
+ "setup": "node dist/index.js setup",
24
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
25
+ "prepublishOnly": "npm run check"
26
+ },
27
+ "dependencies": {
28
+ "@clack/prompts": "^1.0.0",
29
+ "@modelcontextprotocol/sdk": "^1.30.0",
30
+ "env-paths": "^4.0.0",
31
+ "zod": "^4.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "eslint": "^10.0.0",
36
+ "tsdown": "^0.23.0",
37
+ "typescript": "^5.9.0",
38
+ "typescript-eslint": "^8.70.0"
39
+ },
40
+ "keywords": [
41
+ "mcp",
42
+ "outline",
43
+ "getoutline",
44
+ "wiki",
45
+ "claude",
46
+ "modelcontextprotocol"
47
+ ],
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/sunblob/mcp-outline.git"
52
+ },
53
+ "homepage": "https://github.com/sunblob/mcp-outline#readme",
54
+ "bugs": {
55
+ "url": "https://github.com/sunblob/mcp-outline/issues"
56
+ }
57
+ }