@freeflow-im/mcp-docs 0.0.0-stage → 0.2.25

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 (3) hide show
  1. package/README.md +41 -2
  2. package/cli.js +482 -0
  3. package/package.json +37 -4
package/README.md CHANGED
@@ -1,3 +1,42 @@
1
- # Temporary Holding Version
1
+ # @freeflow-im/mcp-docs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Installs **MCP Docs**, a Mac and Windows app for Word, PowerPoint and Excel files, Markdown, CSV, PDFs and pictures
4
+ in a folder on your computer, and the **mcp-docs** plugin for [Claude Code](https://claude.com/claude-code) and
5
+ [Codex](https://developers.openai.com/codex), which lets your coding agent open and edit those files while you watch
6
+ them change in the app. Any other MCP client can connect to the app too.
7
+
8
+ ```
9
+ npx @freeflow-im/mcp-docs
10
+ ```
11
+
12
+ This:
13
+
14
+ 1. Downloads the latest MCP Docs release from GitHub and puts it in `/Applications` (or `~/Applications`),
15
+ replacing an older version. On Windows it runs the release's setup program, which installs it for you.
16
+ 2. For each of Claude Code and Codex it finds, asks whether to add the `mcp-docs` plugin, and if so adds the
17
+ `mcp-docs` plugin marketplace and installs the plugin: the app's MCP server and a skill describing how to use it.
18
+ 3. Starts the app, and prints its MCP address, `http://127.0.0.1:39217/mcp`.
19
+
20
+ Then sign in once: in Claude Code, restart it, run `/mcp` and sign in to `plugin:mcp-docs:mcp-docs`; in Codex, run
21
+ `codex mcp login mcp-docs`. The app asks you to allow each agent. Run `npx @freeflow-im/mcp-docs` again to update
22
+ everything.
23
+
24
+ **Any other MCP client** can connect to `http://127.0.0.1:39217/mcp` (streamable HTTP) while the app is running. It
25
+ signs in with OAuth 2.1 (dynamic client registration and PKCE), and the app asks you to allow it.
26
+
27
+ Options: `--claude`, `--codex` (add the plugin to these agents without asking, and not to the others), `--yes` (add
28
+ it to every agent found without asking), `--skip-app` (plugins only), `--skip-plugins` (app only).
29
+
30
+ To remove it all again:
31
+
32
+ ```
33
+ npx @freeflow-im/mcp-docs --uninstall
34
+ ```
35
+
36
+ This removes the app, its data and settings (the assistant's chats, API keys, connected agents) and what it
37
+ added to Claude Code (the plugin, its marketplace, and an `mcp-docs` server added by hand) and to Codex (the plugin
38
+ and its marketplace). It lists what it
39
+ will remove and asks first (`--yes` skips the question). Your documents, in the folders you opened, are not
40
+ touched.
41
+
42
+ Requires a Mac with Apple silicon or Windows (x64), and Node.js 18 or later.
package/cli.js ADDED
@@ -0,0 +1,482 @@
1
+ #!/usr/bin/env node
2
+ // `npx @freeflow-im/mcp-docs`: install (or update) the MCP Docs desktop app from the latest GitHub release, then,
3
+ // for each coding agent found (Claude Code, Codex) that the person wants it for, the plugin that connects the agent
4
+ // to it: the claude-plugin folder of the same repository, which is both a Claude Code plugin (its marketplace is
5
+ // .claude-plugin/marketplace.json there) and a Codex plugin (.agents/plugins/marketplace.json). Any other MCP client
6
+ // can connect to the app's address itself. With --uninstall, remove all of it again: the app, its data and settings,
7
+ // and what it added to the agents.
8
+ import { spawn, spawnSync } from 'node:child_process';
9
+ import { accessSync, constants, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
10
+ import { homedir, tmpdir } from 'node:os';
11
+ import path from 'node:path';
12
+ import { createInterface } from 'node:readline/promises';
13
+ import { Readable } from 'node:stream';
14
+ import { pipeline } from 'node:stream/promises';
15
+
16
+ const REPO = 'freeflow-im/mcp-docs';
17
+ const APP_NAME = 'MCP Docs';
18
+ /** Windows (x64, which Windows on Arm runs too) or a Mac with Apple silicon. */
19
+ const WIN = process.platform === 'win32';
20
+ const ASSET = WIN ? /^MCP-Docs-(\d+\.\d+\.\d+)-win-x64-setup\.exe$/ : /^MCP-Docs-(\d+\.\d+\.\d+)-mac-arm64\.zip$/;
21
+ const EXE = `${APP_NAME}.exe`;
22
+ const MARKETPLACE = 'mcp-docs';
23
+ const PLUGIN = `mcp-docs@${MARKETPLACE}`;
24
+ /** The app's MCP server (desktop/main.js's fixed port), for any MCP client. */
25
+ const MCP_URL = 'http://127.0.0.1:39217/mcp';
26
+
27
+ const args = process.argv.slice(2);
28
+ const flag = (name) => args.includes(`--${name}`);
29
+
30
+ if (flag('help') || flag('h') || args.some((a) => !a.startsWith('--'))) {
31
+ console.log(`Usage: npx @freeflow-im/mcp-docs [--claude] [--codex] [--skip-app] [--skip-plugins] [--yes]
32
+ npx @freeflow-im/mcp-docs --uninstall [--yes]
33
+
34
+ Installs the latest MCP Docs desktop app (on a Mac into /Applications or ~/Applications, on Windows into
35
+ %LOCALAPPDATA%\\Programs\\MCP Docs), and the mcp-docs plugin
36
+ into the coding agents you choose, Claude Code and Codex, which connects them to the app. It asks for each
37
+ agent it finds. Run it again to update them all. Any other MCP client can connect to the app at
38
+ ${MCP_URL}.
39
+
40
+ --claude add the plugin to Claude Code (without asking; the other agents only if named too)
41
+ --codex add the plugin to Codex (likewise)
42
+ --skip-app only install or update the plugins
43
+ --skip-plugins only install or update the app
44
+ --uninstall remove the app, its data and settings (chats, API keys, connected agents), and the
45
+ mcp-docs plugin, marketplace and MCP server from Claude Code and Codex. Your documents,
46
+ in the folders you opened, are not touched. It lists what it will remove and asks first.
47
+ --yes do not ask: add the plugin to every agent found, or with --uninstall remove everything`);
48
+ process.exit(args.some((a) => !a.startsWith('--')) ? 1 : 0);
49
+ }
50
+
51
+ const say = (msg) => console.log(msg);
52
+ const fail = (msg) => {
53
+ console.error(`\n✗ ${msg}`);
54
+ process.exit(1);
55
+ };
56
+ /** Run a program; its output is returned, not shown. */
57
+ const run = (cmd, argv) => spawnSync(cmd, argv, { encoding: 'utf8' });
58
+ /** Run a command found on the PATH, as a terminal would: on Windows the agents' commands are often `.cmd` scripts,
59
+ * which only a shell runs (the arguments here have no spaces or quotes). */
60
+ const runCommand = (cmd, argv) => (WIN ? spawnSync([cmd, ...argv].join(' '), { encoding: 'utf8', shell: true }) : run(cmd, argv));
61
+
62
+ /** Where Windows installs the app: for the person (the installer's default), or for everyone. */
63
+ const winAppDirs = () => [path.join(process.env.LOCALAPPDATA ?? path.join(homedir(), 'AppData', 'Local'), 'Programs', APP_NAME), path.join(process.env.ProgramFiles ?? 'C:\\Program Files', APP_NAME)];
64
+
65
+ /** Where the app goes: /Applications when it can be written to, else the person's own Applications folder. */
66
+ function appsDir() {
67
+ try {
68
+ accessSync('/Applications', constants.W_OK);
69
+ return '/Applications';
70
+ } catch {
71
+ const own = path.join(homedir(), 'Applications');
72
+ mkdirSync(own, { recursive: true });
73
+ return own;
74
+ }
75
+ }
76
+
77
+ /** The app already installed, in either place, and its version. On Windows `app` is its program. */
78
+ function installedApp() {
79
+ if (WIN) {
80
+ for (const dir of winAppDirs()) {
81
+ const app = path.join(dir, EXE);
82
+ if (!existsSync(app)) continue;
83
+ let version = null;
84
+ try {
85
+ version = JSON.parse(readFileSync(path.join(dir, 'resources', 'app', 'package.json'), 'utf8')).version ?? null;
86
+ } catch {
87
+ // Unreadable: updated below as an older version.
88
+ }
89
+ return { app, version };
90
+ }
91
+ return null;
92
+ }
93
+ for (const dir of ['/Applications', path.join(homedir(), 'Applications')]) {
94
+ const app = path.join(dir, `${APP_NAME}.app`);
95
+ if (!existsSync(app)) continue;
96
+ const r = run('plutil', ['-extract', 'CFBundleShortVersionString', 'raw', '-o', '-', path.join(app, 'Contents', 'Info.plist')]);
97
+ return { app, version: r.status === 0 ? r.stdout.trim() : null };
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /** The newest release of the desktop app: its version and the download of its zip. */
103
+ async function latestRelease() {
104
+ const res = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=20`, { headers: { accept: 'application/vnd.github+json', 'user-agent': 'mcp-docs-installer' } });
105
+ if (!res.ok) fail(`Could not look up the latest release on GitHub (${res.status} ${res.statusText}).`);
106
+ for (const release of await res.json()) {
107
+ if (release.draft || release.prerelease) continue;
108
+ for (const asset of release.assets ?? []) {
109
+ const m = ASSET.exec(asset.name);
110
+ if (m) return { version: m[1], url: asset.browser_download_url, name: asset.name, size: asset.size };
111
+ }
112
+ }
113
+ fail(`No release of ${APP_NAME} for ${WIN ? 'Windows' : 'macOS'} was found on GitHub.`);
114
+ }
115
+
116
+ const appPids = (app) => {
117
+ if (WIN) {
118
+ // Every MCP Docs.exe, the app and the servers it starts for its folders (one installed copy is assumed).
119
+ const r = run('tasklist', ['/FI', `IMAGENAME eq ${EXE}`, '/FO', 'CSV', '/NH']);
120
+ return r.status === 0 ? [...r.stdout.matchAll(/^"[^"]+","(\d+)"/gm)].map((m) => m[1]) : [];
121
+ }
122
+ const r = run('pgrep', ['-f', `${app}/Contents/MacOS/`]);
123
+ return r.status === 0 ? r.stdout.trim().split('\n').filter(Boolean) : [];
124
+ };
125
+
126
+ /** Quit the running app (it saves as it goes) so it can be replaced or removed; waits up to 15 seconds. */
127
+ async function quitApp(app, why = 'to update it') {
128
+ if (!appPids(app).length) return false;
129
+ say(` Quitting ${APP_NAME} ${why}…`);
130
+ // On Windows its windows are asked to close, and it quits (and stops its servers) when the last one has.
131
+ if (WIN) run('taskkill', ['/IM', EXE]);
132
+ else run('osascript', ['-e', `tell application "${app}" to quit`]);
133
+ for (let i = 0; i < 30 && appPids(app).length; i++) await new Promise((r) => setTimeout(r, 500));
134
+ if (appPids(app).length) fail(`${APP_NAME} is still running. Quit it and run this again.`);
135
+ return true;
136
+ }
137
+
138
+ async function installApp() {
139
+ say(`\n${APP_NAME} app`);
140
+ const latest = await latestRelease();
141
+ const current = installedApp();
142
+ if (current?.version === latest.version) {
143
+ say(` ${APP_NAME} ${latest.version} is installed (${current.app}).`);
144
+ return current.app;
145
+ }
146
+ say(current ? ` Updating ${current.version ?? 'an older version'} to ${latest.version}…` : ` Installing ${APP_NAME} ${latest.version}…`);
147
+ const work = mkdtempSync(path.join(tmpdir(), 'mcp-docs-'));
148
+ try {
149
+ const zip = path.join(work, latest.name);
150
+ say(` Downloading ${latest.name} (${Math.round(latest.size / 1e6)} MB)…`);
151
+ const res = await fetch(latest.url);
152
+ if (!res.ok || !res.body) fail(`The download failed (${res.status} ${res.statusText}).`);
153
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(zip));
154
+ if (WIN) return await runWindowsSetup(zip, latest.version, current);
155
+ const out = path.join(work, 'app');
156
+ if (run('ditto', ['-x', '-k', zip, out]).status !== 0) fail('The download could not be unpacked.');
157
+ const unpacked = path.join(out, `${APP_NAME}.app`);
158
+ if (!existsSync(unpacked)) fail(`The download does not hold ${APP_NAME}.app.`);
159
+
160
+ const app = current?.app ?? path.join(appsDir(), `${APP_NAME}.app`);
161
+ const wasRunning = current ? await quitApp(app) : false;
162
+ rmSync(app, { recursive: true, force: true });
163
+ const copied = run('ditto', [unpacked, app]);
164
+ if (copied.status !== 0) fail(`Could not put the app in ${path.dirname(app)}: ${copied.stderr.trim()}`);
165
+ // Not notarized: without the quarantine mark macOS opens it without asking (a download made by this
166
+ // script has none, but one copied over from a browser's download might).
167
+ run('xattr', ['-dr', 'com.apple.quarantine', app]);
168
+ say(` Installed ${APP_NAME} ${latest.version} in ${path.dirname(app)}.`);
169
+ if (wasRunning) say(' It was running: it starts again below.');
170
+ return app;
171
+ } finally {
172
+ rmSync(work, { recursive: true, force: true });
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Install the downloaded Windows setup program without its questions: for the person, or over the copy installed for
178
+ * everyone (which asks Windows for permission). It closes a running app itself; returns the installed program.
179
+ */
180
+ async function runWindowsSetup(setup, version, current) {
181
+ const forAll = current && path.dirname(current.app) === winAppDirs()[1];
182
+ const wasRunning = current ? await quitApp(current.app) : false;
183
+ say(' Running the setup program…');
184
+ const r = run(setup, ['/S', ...(forAll ? ['/allusers'] : [])]);
185
+ const app = installedApp();
186
+ if (r.status !== 0 || app?.version !== version) fail(`The setup program did not install ${APP_NAME} ${version}${r.status ? ` (exit code ${r.status})` : ''}. Run ${path.basename(setup)} from the release yourself: github.com/${REPO}/releases/latest`);
187
+ say(` Installed ${APP_NAME} ${version} in ${path.dirname(app.app)}.`);
188
+ if (wasRunning) say(' It was running: it starts again below.');
189
+ return app.app;
190
+ }
191
+
192
+ /** Run `claude` with these arguments, showing what it says when it fails. */
193
+ function claude(argv) {
194
+ const r = runCommand('claude', argv);
195
+ return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() };
196
+ }
197
+
198
+ /** Run `codex` with these arguments, as `claude` above. */
199
+ function codex(argv) {
200
+ const r = runCommand('codex', argv);
201
+ return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() };
202
+ }
203
+
204
+ /**
205
+ * The coding agents the plugin is for: their command, how to install it there (which says whether it was 'installed'
206
+ * or 'updated'), and what to do next. An update keeps the agent's sign-in; only a new install needs one.
207
+ */
208
+ const AGENTS = [
209
+ {
210
+ key: 'claude',
211
+ name: 'Claude Code',
212
+ found: () => runCommand('claude', ['--version']).status === 0,
213
+ install: installClaudePlugin,
214
+ next: {
215
+ installed: 'In Claude Code: restart it (or start a new session) to load the plugin, then run /mcp, choose plugin:mcp-docs:mcp-docs and sign in.',
216
+ updated: 'In Claude Code: restart it (or start a new session) to load the new version.',
217
+ },
218
+ },
219
+ {
220
+ key: 'codex',
221
+ name: 'Codex',
222
+ found: () => runCommand('codex', ['--version']).status === 0,
223
+ install: installCodexPlugin,
224
+ next: {
225
+ installed: 'In Codex: sign in once with `codex mcp login mcp-docs`, then start a new session.',
226
+ updated: 'In Codex: start a new session to load the new version.',
227
+ },
228
+ },
229
+ ];
230
+
231
+ /** Ask a yes-or-no question; Enter is yes. */
232
+ async function confirm(question) {
233
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
234
+ const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
235
+ rl.close();
236
+ return answer === '' || answer === 'y' || answer === 'yes';
237
+ }
238
+
239
+ /**
240
+ * The agents to add the plugin to: the ones named (--claude, --codex), else each one found that the person
241
+ * says yes to (all of them with --yes, or when there is no one to ask).
242
+ */
243
+ async function chooseAgents() {
244
+ const named = AGENTS.filter((a) => flag(a.key));
245
+ const candidates = named.length ? named : AGENTS;
246
+ const chosen = [];
247
+ for (const agent of candidates) {
248
+ if (!agent.found()) {
249
+ if (named.length) say(`\n${agent.name} was not found (the \`${agent.key}\` command). Install it, then run \`npx @freeflow-im/mcp-docs --skip-app --${agent.key}\`.`);
250
+ continue;
251
+ }
252
+ if (named.length || flag('yes') || !process.stdin.isTTY || (await confirm(`\nAdd the MCP Docs plugin to ${agent.name}?`))) chosen.push(agent);
253
+ }
254
+ return chosen;
255
+ }
256
+
257
+ function installClaudePlugin() {
258
+ say('\nClaude Code plugin');
259
+ const listed = claude(['plugin', 'marketplace', 'list', '--json']);
260
+ let known = false;
261
+ try {
262
+ known = JSON.parse(listed.out).some((m) => m.name === MARKETPLACE);
263
+ } catch {
264
+ // Not JSON (an older Claude Code): add it, which says so if it is there already.
265
+ }
266
+ if (known) {
267
+ const up = claude(['plugin', 'marketplace', 'update', MARKETPLACE]);
268
+ if (!up.ok) say(` Could not refresh the ${MARKETPLACE} marketplace: ${up.out}`);
269
+ } else {
270
+ // Only the plugin's folders are checked out, not the whole app.
271
+ const add = claude(['plugin', 'marketplace', 'add', REPO, '--sparse', '.claude-plugin', 'claude-plugin']);
272
+ if (!add.ok && !/already/i.test(add.out)) fail(`Could not add the ${MARKETPLACE} marketplace to Claude Code:\n${add.out}`);
273
+ }
274
+
275
+ const plugins = claude(['plugin', 'list', '--json']);
276
+ let installed = false;
277
+ try {
278
+ installed = JSON.parse(plugins.out).some((p) => p.id === PLUGIN || `${p.name}@${p.marketplace}` === PLUGIN);
279
+ } catch {
280
+ installed = plugins.out.includes(PLUGIN);
281
+ }
282
+ const step = installed ? claude(['plugin', 'update', PLUGIN]) : claude(['plugin', 'install', PLUGIN]);
283
+ if (!step.ok) fail(`Could not ${installed ? 'update' : 'install'} the ${PLUGIN} plugin:\n${step.out}`);
284
+ say(` ${installed ? 'Updated' : 'Installed'} the ${PLUGIN} plugin.`);
285
+
286
+ // A server added by hand earlier (File › Connect an Agent's command) would be a second copy of the plugin's.
287
+ if (claude(['mcp', 'get', 'mcp-docs']).ok) {
288
+ say(' You also added the mcp-docs server by hand before. The plugin has its own now; remove that one with:');
289
+ say(' claude mcp remove mcp-docs --scope user');
290
+ }
291
+ return installed ? 'updated' : 'installed';
292
+ }
293
+
294
+ function installCodexPlugin() {
295
+ say('\nCodex plugin');
296
+ let known = false;
297
+ try {
298
+ known = JSON.parse(codex(['plugin', 'marketplace', 'list', '--json']).out).marketplaces.some((m) => m.name === MARKETPLACE);
299
+ } catch {
300
+ // Unreadable: add it, which says so if it is there already.
301
+ }
302
+ if (known) {
303
+ const up = codex(['plugin', 'marketplace', 'upgrade', MARKETPLACE]);
304
+ if (!up.ok) say(` Could not refresh the ${MARKETPLACE} marketplace: ${up.out}`);
305
+ } else {
306
+ // Only the plugin's folders are checked out, not the whole app.
307
+ const add = codex(['plugin', 'marketplace', 'add', REPO, '--sparse', '.agents', '--sparse', 'claude-plugin']);
308
+ if (!add.ok && !/already/i.test(add.out)) fail(`Could not add the ${MARKETPLACE} marketplace to Codex:\n${add.out}`);
309
+ }
310
+ let installed = false;
311
+ try {
312
+ installed = JSON.parse(codex(['plugin', 'list', '--json']).out).installed.some((p) => p.pluginId === PLUGIN);
313
+ } catch {
314
+ // Added below either way: adding it again installs the marketplace's version.
315
+ }
316
+ const step = codex(['plugin', 'add', PLUGIN]);
317
+ if (!step.ok) fail(`Could not ${installed ? 'update' : 'install'} the ${PLUGIN} plugin in Codex:\n${step.out}`);
318
+ say(` ${installed ? 'Updated' : 'Installed'} the ${PLUGIN} plugin.`);
319
+ return installed ? 'updated' : 'installed';
320
+ }
321
+
322
+ // --- Uninstall ------------------------------------------------------------------------------------------
323
+
324
+ /**
325
+ * Where the app keeps its own things: its data folder (chats, settings with the API keys, connected agents'
326
+ * tokens, each folder's index, deleted files), under each name it has had, and macOS's files for it.
327
+ */
328
+ function appData() {
329
+ if (WIN) {
330
+ const roaming = process.env.APPDATA ?? path.join(homedir(), 'AppData', 'Roaming');
331
+ return ['MCP Docs', 'Universal Docs', 'FreeFlow Docs'].map((name) => path.join(roaming, name)).filter((p) => existsSync(p));
332
+ }
333
+ const lib = path.join(homedir(), 'Library');
334
+ const BUNDLE_ID = 'im.freeflow.docs';
335
+ return [
336
+ ...['MCP Docs', 'Universal Docs', 'FreeFlow Docs'].map((name) => path.join(lib, 'Application Support', name)),
337
+ path.join(lib, 'Preferences', `${BUNDLE_ID}.plist`),
338
+ path.join(lib, 'Saved Application State', `${BUNDLE_ID}.savedState`),
339
+ path.join(lib, 'Caches', BUNDLE_ID),
340
+ path.join(lib, 'HTTPStorages', BUNDLE_ID),
341
+ path.join(lib, 'Logs', APP_NAME),
342
+ ].filter((p) => existsSync(p));
343
+ }
344
+
345
+ /** The apps installed, in either place (on Windows, their programs). */
346
+ const installedApps = () =>
347
+ (WIN ? winAppDirs().map((dir) => path.join(dir, EXE)) : ['/Applications', path.join(homedir(), 'Applications')].map((dir) => path.join(dir, `${APP_NAME}.app`))).filter((p) => existsSync(p));
348
+
349
+ /** What it added to Claude Code: the plugin, its marketplace, and an MCP server added by hand (by either name it has had). */
350
+ function claudeThings() {
351
+ if (runCommand('claude', ['--version']).status !== 0) return { plugin: false, marketplace: false, servers: [] };
352
+ let plugin = false;
353
+ let marketplace = false;
354
+ try {
355
+ plugin = JSON.parse(claude(['plugin', 'list', '--json']).out).some((p) => p.id === PLUGIN);
356
+ } catch {
357
+ // An older Claude Code: tried below anyway.
358
+ plugin = true;
359
+ }
360
+ try {
361
+ marketplace = JSON.parse(claude(['plugin', 'marketplace', 'list', '--json']).out).some((m) => m.name === MARKETPLACE);
362
+ } catch {
363
+ marketplace = true;
364
+ }
365
+ const servers = ['mcp-docs', 'universal-local'].filter((name) => claude(['mcp', 'get', name]).ok);
366
+ return { plugin, marketplace, servers };
367
+ }
368
+
369
+ /** What it added to Codex: the plugin and its marketplace. */
370
+ function codexThings() {
371
+ if (runCommand('codex', ['--version']).status !== 0) return { plugin: false, marketplace: false };
372
+ let plugin = false;
373
+ let marketplace = false;
374
+ try {
375
+ plugin = JSON.parse(codex(['plugin', 'list', '--json']).out).installed.some((p) => p.pluginId === PLUGIN);
376
+ } catch {
377
+ plugin = false;
378
+ }
379
+ try {
380
+ marketplace = JSON.parse(codex(['plugin', 'marketplace', 'list', '--json']).out).marketplaces.some((m) => m.name === MARKETPLACE);
381
+ } catch {
382
+ marketplace = false;
383
+ }
384
+ return { plugin, marketplace };
385
+ }
386
+
387
+ async function uninstall() {
388
+ const apps = installedApps();
389
+ const data = appData();
390
+ const fromClaude = claudeThings();
391
+ const fromCodex = codexThings();
392
+ const lines = [
393
+ ...apps.map((a) => ` the app: ${a}`),
394
+ ...data.map((d) => ` its data: ${d.replace(homedir(), '~')}`),
395
+ ...(fromClaude.plugin ? [` the ${PLUGIN} plugin in Claude Code`] : []),
396
+ ...(fromClaude.marketplace ? [` the ${MARKETPLACE} plugin marketplace in Claude Code`] : []),
397
+ ...fromClaude.servers.map((s) => ` the ${s} MCP server in Claude Code`),
398
+ ...(fromCodex.plugin ? [` the ${PLUGIN} plugin in Codex`] : []),
399
+ ...(fromCodex.marketplace ? [` the ${MARKETPLACE} plugin marketplace in Codex`] : []),
400
+ ];
401
+ if (!lines.length) {
402
+ say(`\nThere is nothing of ${APP_NAME} to remove.`);
403
+ return;
404
+ }
405
+ say(`\nThis removes:\n${lines.join('\n')}\n\nThe data holds your assistant chats, settings (API keys too) and connected agents. Your documents, in the folders you opened, are not touched.`);
406
+ if (!flag('yes')) {
407
+ if (!process.stdin.isTTY) fail('Run it with --yes to remove these without being asked.');
408
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
409
+ const answer = (await rl.question('\nRemove them? [y/N] ')).trim().toLowerCase();
410
+ rl.close();
411
+ if (answer !== 'y' && answer !== 'yes') {
412
+ say('Nothing was removed.');
413
+ return;
414
+ }
415
+ }
416
+
417
+ for (const app of apps) {
418
+ await quitApp(app, 'to remove it');
419
+ if (WIN) {
420
+ // Its own uninstaller, which also takes it off Settings › Apps and the Start menu.
421
+ const dir = path.dirname(app);
422
+ const r = run(path.join(dir, `Uninstall ${APP_NAME}.exe`), ['/S', ...(dir === winAppDirs()[1] ? ['/allusers'] : [])]);
423
+ say(r.status === 0 ? ` Removed ${dir}` : ` Could not remove ${dir}: uninstall ${APP_NAME} in Settings › Apps instead.`);
424
+ continue;
425
+ }
426
+ rmSync(app, { recursive: true, force: true });
427
+ say(` Removed ${app}`);
428
+ }
429
+ for (const d of data) {
430
+ rmSync(d, { recursive: true, force: true });
431
+ say(` Removed ${d.replace(homedir(), '~')}`);
432
+ }
433
+ if (fromClaude.plugin) {
434
+ const r = claude(['plugin', 'uninstall', PLUGIN]);
435
+ say(r.ok ? ` Uninstalled the ${PLUGIN} plugin from Claude Code` : ` Could not uninstall the ${PLUGIN} plugin from Claude Code: ${r.out}`);
436
+ }
437
+ if (fromClaude.marketplace) {
438
+ const r = claude(['plugin', 'marketplace', 'remove', MARKETPLACE]);
439
+ say(r.ok ? ` Removed the ${MARKETPLACE} marketplace from Claude Code` : ` Could not remove the ${MARKETPLACE} marketplace from Claude Code: ${r.out}`);
440
+ }
441
+ for (const s of fromClaude.servers) {
442
+ // Added for the user (File › Connect an Agent's command), or for one project.
443
+ const r = claude(['mcp', 'remove', s, '--scope', 'user']).ok ? { ok: true } : claude(['mcp', 'remove', s]);
444
+ say(r.ok ? ` Removed the ${s} MCP server` : ` Could not remove the ${s} MCP server: ${r.out}`);
445
+ }
446
+ if (fromCodex.plugin) {
447
+ const r = codex(['plugin', 'remove', PLUGIN]);
448
+ say(r.ok ? ` Uninstalled the ${PLUGIN} plugin from Codex` : ` Could not uninstall the ${PLUGIN} plugin from Codex: ${r.out}`);
449
+ }
450
+ if (fromCodex.marketplace) {
451
+ const r = codex(['plugin', 'marketplace', 'remove', MARKETPLACE]);
452
+ say(r.ok ? ` Removed the ${MARKETPLACE} marketplace from Codex` : ` Could not remove the ${MARKETPLACE} marketplace from Codex: ${r.out}`);
453
+ }
454
+ say(`\n${APP_NAME} is uninstalled. Restart Claude Code and Codex to let go of the plugin.`);
455
+ }
456
+
457
+ if (!WIN && (process.platform !== 'darwin' || process.arch !== 'arm64')) {
458
+ fail(`${APP_NAME} is only available for Windows and Macs with Apple silicon so far.`);
459
+ }
460
+
461
+ if (flag('uninstall')) {
462
+ say(`MCP Docs uninstaller (${REPO})`);
463
+ await uninstall();
464
+ process.exit(0);
465
+ }
466
+
467
+ say(`MCP Docs installer (${REPO})`);
468
+ const app = flag('skip-app') ? installedApp()?.app : await installApp();
469
+ // --skip-plugin is the flag's name before Codex.
470
+ const agents = flag('skip-plugins') || flag('skip-plugin') ? [] : await chooseAgents();
471
+ const done = agents.map((agent) => ({ agent, step: agent.install() }));
472
+
473
+ if (app && !appPids(app).length) {
474
+ // The MCP server is the running app.
475
+ if (WIN) spawn(app, [], { detached: true, stdio: 'ignore' }).unref();
476
+ else run('open', ['-a', app]);
477
+ say(`\nStarted ${APP_NAME}.`);
478
+ }
479
+ say(`
480
+ While ${APP_NAME} is running, its MCP server is at ${MCP_URL}. Any MCP client on this computer that
481
+ supports streamable HTTP and OAuth can connect to it there; ${APP_NAME} asks you to allow each one.`);
482
+ if (done.length) say(`\nNext:\n${done.map(({ agent, step }) => ` - ${agent.next[step]}`).join('\n')}`);
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@freeflow-im/mcp-docs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.25",
4
+ "description": "Install the MCP Docs desktop app and its plugin for Claude Code and Codex: office files on your Mac, edited by your coding agent or any MCP client.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp-docs": "cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "os": [
17
+ "darwin"
18
+ ],
19
+ "keywords": [
20
+ "mcp",
21
+ "claude-code",
22
+ "claude-code-plugin",
23
+ "codex",
24
+ "codex-plugin",
25
+ "documents",
26
+ "office"
27
+ ],
28
+ "author": "Scott Persinger",
29
+ "license": "UNLICENSED",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/freeflow-im/mcp-docs.git",
33
+ "directory": "installer"
34
+ },
35
+ "homepage": "https://github.com/freeflow-im/mcp-docs#readme",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }