@freeflow-im/mcp-docs 0.2.24 → 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 +4 -4
  2. package/cli.js +80 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @freeflow-im/mcp-docs
2
2
 
3
- Installs **MCP Docs**, a Mac app for Word, PowerPoint and Excel files, Markdown, CSV, PDFs and pictures in a
4
- folder on your Mac, and the **mcp-docs** plugin for [Claude Code](https://claude.com/claude-code) and
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
5
  [Codex](https://developers.openai.com/codex), which lets your coding agent open and edit those files while you watch
6
6
  them change in the app. Any other MCP client can connect to the app too.
7
7
 
@@ -12,7 +12,7 @@ npx @freeflow-im/mcp-docs
12
12
  This:
13
13
 
14
14
  1. Downloads the latest MCP Docs release from GitHub and puts it in `/Applications` (or `~/Applications`),
15
- replacing an older version.
15
+ replacing an older version. On Windows it runs the release's setup program, which installs it for you.
16
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
17
  `mcp-docs` plugin marketplace and installs the plugin: the app's MCP server and a skill describing how to use it.
18
18
  3. Starts the app, and prints its MCP address, `http://127.0.0.1:39217/mcp`.
@@ -39,4 +39,4 @@ and its marketplace). It lists what it
39
39
  will remove and asks first (`--yes` skips the question). Your documents, in the folders you opened, are not
40
40
  touched.
41
41
 
42
- Requires a Mac with Apple silicon and Node.js 18 or later.
42
+ Requires a Mac with Apple silicon or Windows (x64), and Node.js 18 or later.
package/cli.js CHANGED
@@ -5,8 +5,8 @@
5
5
  // .claude-plugin/marketplace.json there) and a Codex plugin (.agents/plugins/marketplace.json). Any other MCP client
6
6
  // can connect to the app's address itself. With --uninstall, remove all of it again: the app, its data and settings,
7
7
  // and what it added to the agents.
8
- import { spawnSync } from 'node:child_process';
9
- import { accessSync, constants, createWriteStream, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
8
+ import { spawn, spawnSync } from 'node:child_process';
9
+ import { accessSync, constants, createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
10
10
  import { homedir, tmpdir } from 'node:os';
11
11
  import path from 'node:path';
12
12
  import { createInterface } from 'node:readline/promises';
@@ -15,7 +15,10 @@ import { pipeline } from 'node:stream/promises';
15
15
 
16
16
  const REPO = 'freeflow-im/mcp-docs';
17
17
  const APP_NAME = 'MCP Docs';
18
- const ASSET = /^MCP-Docs-(\d+\.\d+\.\d+)-mac-arm64\.zip$/;
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`;
19
22
  const MARKETPLACE = 'mcp-docs';
20
23
  const PLUGIN = `mcp-docs@${MARKETPLACE}`;
21
24
  /** The app's MCP server (desktop/main.js's fixed port), for any MCP client. */
@@ -28,7 +31,8 @@ if (flag('help') || flag('h') || args.some((a) => !a.startsWith('--'))) {
28
31
  console.log(`Usage: npx @freeflow-im/mcp-docs [--claude] [--codex] [--skip-app] [--skip-plugins] [--yes]
29
32
  npx @freeflow-im/mcp-docs --uninstall [--yes]
30
33
 
31
- Installs the latest MCP Docs desktop app into /Applications (or ~/Applications), and the mcp-docs plugin
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
32
36
  into the coding agents you choose, Claude Code and Codex, which connects them to the app. It asks for each
33
37
  agent it finds. Run it again to update them all. Any other MCP client can connect to the app at
34
38
  ${MCP_URL}.
@@ -51,6 +55,12 @@ const fail = (msg) => {
51
55
  };
52
56
  /** Run a program; its output is returned, not shown. */
53
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)];
54
64
 
55
65
  /** Where the app goes: /Applications when it can be written to, else the person's own Applications folder. */
56
66
  function appsDir() {
@@ -64,8 +74,22 @@ function appsDir() {
64
74
  }
65
75
  }
66
76
 
67
- /** The app already installed, in either place, and its version. */
77
+ /** The app already installed, in either place, and its version. On Windows `app` is its program. */
68
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
+ }
69
93
  for (const dir of ['/Applications', path.join(homedir(), 'Applications')]) {
70
94
  const app = path.join(dir, `${APP_NAME}.app`);
71
95
  if (!existsSync(app)) continue;
@@ -86,10 +110,15 @@ async function latestRelease() {
86
110
  if (m) return { version: m[1], url: asset.browser_download_url, name: asset.name, size: asset.size };
87
111
  }
88
112
  }
89
- fail(`No release of ${APP_NAME} for macOS was found on GitHub.`);
113
+ fail(`No release of ${APP_NAME} for ${WIN ? 'Windows' : 'macOS'} was found on GitHub.`);
90
114
  }
91
115
 
92
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
+ }
93
122
  const r = run('pgrep', ['-f', `${app}/Contents/MacOS/`]);
94
123
  return r.status === 0 ? r.stdout.trim().split('\n').filter(Boolean) : [];
95
124
  };
@@ -98,7 +127,9 @@ const appPids = (app) => {
98
127
  async function quitApp(app, why = 'to update it') {
99
128
  if (!appPids(app).length) return false;
100
129
  say(` Quitting ${APP_NAME} ${why}…`);
101
- run('osascript', ['-e', `tell application "${app}" to quit`]);
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`]);
102
133
  for (let i = 0; i < 30 && appPids(app).length; i++) await new Promise((r) => setTimeout(r, 500));
103
134
  if (appPids(app).length) fail(`${APP_NAME} is still running. Quit it and run this again.`);
104
135
  return true;
@@ -120,6 +151,7 @@ async function installApp() {
120
151
  const res = await fetch(latest.url);
121
152
  if (!res.ok || !res.body) fail(`The download failed (${res.status} ${res.statusText}).`);
122
153
  await pipeline(Readable.fromWeb(res.body), createWriteStream(zip));
154
+ if (WIN) return await runWindowsSetup(zip, latest.version, current);
123
155
  const out = path.join(work, 'app');
124
156
  if (run('ditto', ['-x', '-k', zip, out]).status !== 0) fail('The download could not be unpacked.');
125
157
  const unpacked = path.join(out, `${APP_NAME}.app`);
@@ -141,15 +173,31 @@ async function installApp() {
141
173
  }
142
174
  }
143
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
+
144
192
  /** Run `claude` with these arguments, showing what it says when it fails. */
145
193
  function claude(argv) {
146
- const r = run('claude', argv);
194
+ const r = runCommand('claude', argv);
147
195
  return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() };
148
196
  }
149
197
 
150
198
  /** Run `codex` with these arguments, as `claude` above. */
151
199
  function codex(argv) {
152
- const r = run('codex', argv);
200
+ const r = runCommand('codex', argv);
153
201
  return { ok: r.status === 0, out: `${r.stdout ?? ''}${r.stderr ?? ''}`.trim() };
154
202
  }
155
203
 
@@ -161,7 +209,7 @@ const AGENTS = [
161
209
  {
162
210
  key: 'claude',
163
211
  name: 'Claude Code',
164
- found: () => run('claude', ['--version']).status === 0,
212
+ found: () => runCommand('claude', ['--version']).status === 0,
165
213
  install: installClaudePlugin,
166
214
  next: {
167
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.',
@@ -171,7 +219,7 @@ const AGENTS = [
171
219
  {
172
220
  key: 'codex',
173
221
  name: 'Codex',
174
- found: () => run('codex', ['--version']).status === 0,
222
+ found: () => runCommand('codex', ['--version']).status === 0,
175
223
  install: installCodexPlugin,
176
224
  next: {
177
225
  installed: 'In Codex: sign in once with `codex mcp login mcp-docs`, then start a new session.',
@@ -278,6 +326,10 @@ function installCodexPlugin() {
278
326
  * tokens, each folder's index, deleted files), under each name it has had, and macOS's files for it.
279
327
  */
280
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
+ }
281
333
  const lib = path.join(homedir(), 'Library');
282
334
  const BUNDLE_ID = 'im.freeflow.docs';
283
335
  return [
@@ -290,12 +342,13 @@ function appData() {
290
342
  ].filter((p) => existsSync(p));
291
343
  }
292
344
 
293
- /** The apps installed, in either place. */
294
- const installedApps = () => ['/Applications', path.join(homedir(), 'Applications')].map((dir) => path.join(dir, `${APP_NAME}.app`)).filter((p) => existsSync(p));
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));
295
348
 
296
349
  /** What it added to Claude Code: the plugin, its marketplace, and an MCP server added by hand (by either name it has had). */
297
350
  function claudeThings() {
298
- if (run('claude', ['--version']).status !== 0) return { plugin: false, marketplace: false, servers: [] };
351
+ if (runCommand('claude', ['--version']).status !== 0) return { plugin: false, marketplace: false, servers: [] };
299
352
  let plugin = false;
300
353
  let marketplace = false;
301
354
  try {
@@ -315,7 +368,7 @@ function claudeThings() {
315
368
 
316
369
  /** What it added to Codex: the plugin and its marketplace. */
317
370
  function codexThings() {
318
- if (run('codex', ['--version']).status !== 0) return { plugin: false, marketplace: false };
371
+ if (runCommand('codex', ['--version']).status !== 0) return { plugin: false, marketplace: false };
319
372
  let plugin = false;
320
373
  let marketplace = false;
321
374
  try {
@@ -363,6 +416,13 @@ async function uninstall() {
363
416
 
364
417
  for (const app of apps) {
365
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
+ }
366
426
  rmSync(app, { recursive: true, force: true });
367
427
  say(` Removed ${app}`);
368
428
  }
@@ -394,8 +454,8 @@ async function uninstall() {
394
454
  say(`\n${APP_NAME} is uninstalled. Restart Claude Code and Codex to let go of the plugin.`);
395
455
  }
396
456
 
397
- if (process.platform !== 'darwin' || process.arch !== 'arm64') {
398
- fail(`${APP_NAME} is only available for Macs with Apple silicon so far.`);
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.`);
399
459
  }
400
460
 
401
461
  if (flag('uninstall')) {
@@ -412,10 +472,11 @@ const done = agents.map((agent) => ({ agent, step: agent.install() }));
412
472
 
413
473
  if (app && !appPids(app).length) {
414
474
  // The MCP server is the running app.
415
- run('open', ['-a', app]);
475
+ if (WIN) spawn(app, [], { detached: true, stdio: 'ignore' }).unref();
476
+ else run('open', ['-a', app]);
416
477
  say(`\nStarted ${APP_NAME}.`);
417
478
  }
418
479
  say(`
419
- While ${APP_NAME} is running, its MCP server is at ${MCP_URL}. Any MCP client on this Mac that
480
+ While ${APP_NAME} is running, its MCP server is at ${MCP_URL}. Any MCP client on this computer that
420
481
  supports streamable HTTP and OAuth can connect to it there; ${APP_NAME} asks you to allow each one.`);
421
482
  if (done.length) say(`\nNext:\n${done.map(({ agent, step }) => ` - ${agent.next[step]}`).join('\n')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freeflow-im/mcp-docs",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
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
5
  "type": "module",
6
6
  "bin": {