spacepal-mcp 0.3.0 → 0.4.1

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.
package/README.md CHANGED
@@ -12,7 +12,8 @@ It runs locally over stdio. It reads the Space list Spacepal writes to
12
12
 
13
13
  ## Requirements
14
14
 
15
- - macOS 14 or later, with Spacepal running (direct or Mac App Store build)
15
+ - macOS 14 or later, with Spacepal running (direct or Mac App Store build).
16
+ [Download Spacepal](https://getspacepal.com/download/?utm_source=mcp-readme&utm_medium=readme): 14-day free trial, no account; names, notes and switching stay free after it.
16
17
  - Node.js 18 or later
17
18
 
18
19
  ## Tools
@@ -34,6 +35,8 @@ It runs locally over stdio. It reads the Space list Spacepal writes to
34
35
  | `restore_window_layout` | Put windows back from the last saved layout |
35
36
  | `save_sessions` | Save terminal sessions and Chrome windows on one Space or all of them |
36
37
  | `restore_sessions` | Reopen saved sessions on their Spaces |
38
+ | `place_window` | Put the front window in a half, a quarter, maximized, centred, or on the next display |
39
+ | `set_presentation_mode` | Hide Space names, notes and previews for a screen share, or show them again |
37
40
  | `add_desktop` | Add a desktop on the current display |
38
41
  | `remove_desktop` | Remove a desktop; its windows move to another one |
39
42
 
package/index.js CHANGED
@@ -13,7 +13,7 @@ import { join } from "node:path";
13
13
  import { promisify } from "node:util";
14
14
 
15
15
  const run = promisify(execFile);
16
- const VERSION = "0.3.0";
16
+ const VERSION = "0.4.1";
17
17
 
18
18
  // Direct build writes to Application Support; the App Store build is sandboxed.
19
19
  const SUPPORT_DIRS = [
@@ -26,6 +26,26 @@ const PROFILE_FILES = SUPPORT_DIRS.map((d) => join(d, "profiles.json"));
26
26
  const TEMPLATES = ["Developer", "Designer", "Freelancer", "Student", "Streamer"];
27
27
 
28
28
  const NOT_RUNNING = "Spacepal is not running. Open Spacepal (getspacepal.com) and try again.";
29
+ const DOWNLOAD_URL = "https://getspacepal.com/download/?utm_source=mcp&utm_medium=tool&utm_campaign=not_installed";
30
+ const NOT_INSTALLED =
31
+ "Spacepal is not installed on this Mac. This MCP server needs the Spacepal app, which names, previews and switches macOS Spaces. " +
32
+ `Download it from ${DOWNLOAD_URL} (14-day free trial, no account; names and switching stay free after the trial), open it once, then try again.`;
33
+
34
+ // Not running and never installed reads differently: the user needs a download link, not "open it".
35
+ async function notRunning() {
36
+ const places = [
37
+ "/Applications/Spacepal.app",
38
+ join(homedir(), "Applications/Spacepal.app"),
39
+ ...SUPPORT_DIRS,
40
+ ];
41
+ for (const p of places) {
42
+ try {
43
+ await stat(p);
44
+ return NOT_RUNNING;
45
+ } catch {}
46
+ }
47
+ return NOT_INSTALLED;
48
+ }
29
49
 
30
50
  async function isRunning() {
31
51
  try {
@@ -63,7 +83,7 @@ async function readProfiles() {
63
83
  }
64
84
 
65
85
  async function send(path, query = {}) {
66
- if (!(await isRunning())) throw new Error(NOT_RUNNING);
86
+ if (!(await isRunning())) throw new Error(await notRunning());
67
87
  // Encode with %20, not "+": the app reads query values with URLComponents, which keeps "+".
68
88
  const qs = Object.entries(query)
69
89
  .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
@@ -119,7 +139,7 @@ Start with list_spaces to learn the Spaces, their numbers, names, notes and the
119
139
 
120
140
  Tools that only read: list_spaces, current_space, list_profiles. Tools that change the user's screen or setup: everything else. Ask the user before remove_desktop, apply_template and apply_profile, which replace or remove things; rename_space and set_space_note are easy to undo by calling them again.
121
141
 
122
- Every action goes through Spacepal, so it must be running (macOS 14 or later). Nothing is sent over the network. If a tool says Spacepal is not running, tell the user to open it. The overview and sessions need Spacepal's trial or a license; names, notes and switching work in the free version.`;
142
+ Every action goes through Spacepal, so it must be running (macOS 14 or later). Nothing is sent over the network. If a tool says Spacepal is not running, tell the user to open it. If it says Spacepal is not installed, give the user the download link from the message. The overview and sessions need Spacepal's trial or a license; names, notes and switching work in the free version.`;
123
143
 
124
144
  const server = new McpServer({ name: "spacepal", version: VERSION }, { instructions: INSTRUCTIONS });
125
145
 
@@ -162,7 +182,7 @@ server.registerTool(
162
182
  annotations: { title: "List Spaces", ...RO },
163
183
  },
164
184
  guarded(async () => {
165
- if (!(await isRunning())) return fail(NOT_RUNNING);
185
+ if (!(await isRunning())) return fail(await notRunning());
166
186
  const state = await readState();
167
187
  if (!state) return fail("Spacepal has not written its Space list yet. Open the Spacepal menu once and try again.");
168
188
  const lines = state.spaces.map((s) => {
@@ -193,7 +213,7 @@ server.registerTool(
193
213
  annotations: { title: "Current Space", ...RO },
194
214
  },
195
215
  guarded(async () => {
196
- if (!(await isRunning())) return fail(NOT_RUNNING);
216
+ if (!(await isRunning())) return fail(await notRunning());
197
217
  const state = await readState();
198
218
  if (!state) return fail("Spacepal has not written its Space list yet. Open the Spacepal menu once and try again.");
199
219
  const active = state.spaces.filter((s) => s.isActive);
@@ -483,6 +503,49 @@ server.registerTool(
483
503
  }),
484
504
  );
485
505
 
506
+ server.registerTool(
507
+ "place_window",
508
+ {
509
+ title: "Place window",
510
+ description:
511
+ "Resize and move the user's frontmost window on its current display: a half, a quarter, maximized or centred; " +
512
+ 'or, with "next-display", move it to the next display. It stays on the same Space. ' +
513
+ "The frontmost window is the one the user last focused (from Claude Desktop that is Claude's own window). " +
514
+ "Needs the Accessibility permission Spacepal already asks for. Not undoable as one step; place it again to change it. " +
515
+ "Use move_window_to_space to send a window to another Space instead. Needs Spacepal 1.2.8 or later.",
516
+ inputSchema: {
517
+ position: z
518
+ .enum(["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right", "maximize", "center", "next-display"])
519
+ .describe('Where the window goes: a half ("left", "right", "top", "bottom"), a quarter ("top-left" etc.), "maximize", "center", or "next-display".'),
520
+ },
521
+ annotations: { title: "Place window", ...CHANGE, idempotentHint: true },
522
+ },
523
+ guarded(async ({ position }) => {
524
+ if (position === "next-display") await send("next-display");
525
+ else await send(`snap/${position}`);
526
+ return text(`Asked Spacepal to place the front window: ${position}.`);
527
+ }),
528
+ );
529
+
530
+ server.registerTool(
531
+ "set_presentation_mode",
532
+ {
533
+ title: "Set presentation mode",
534
+ description:
535
+ "Turn Spacepal's presentation mode on or off. While it is on, Spacepal shows \"Desktop N\" instead of Space names and hides notes, " +
536
+ "badges and window previews in its overview, menu bar and Mission Control, so a screen share shows nothing private. " +
537
+ "Names and notes are kept and come back when it is turned off; list_spaces still returns the real names. Reversible by calling it again. Needs Spacepal 1.2.8 or later.",
538
+ inputSchema: {
539
+ on: z.boolean().describe("true hides names, notes and previews; false shows them again."),
540
+ },
541
+ annotations: { title: "Set presentation mode", ...CHANGE, idempotentHint: true },
542
+ },
543
+ guarded(async ({ on }) => {
544
+ await send(`presentation/${on ? "on" : "off"}`);
545
+ return text(on ? "Presentation mode on: names, notes and previews hidden." : "Presentation mode off.");
546
+ }),
547
+ );
548
+
486
549
  server.registerTool(
487
550
  "add_desktop",
488
551
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spacepal-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server for Spacepal: list, switch, rename macOS Spaces and move windows between them from Claude, Cursor or any MCP client.",
5
5
  "mcpName": "com.getspacepal/spacepal",
6
6
  "type": "module",
package/server.json CHANGED
@@ -3,13 +3,13 @@
3
3
  "name": "com.getspacepal/spacepal",
4
4
  "title": "Spacepal",
5
5
  "description": "List, switch and rename macOS Spaces and move windows between them. Needs the Spacepal app.",
6
- "version": "0.3.0",
6
+ "version": "0.4.1",
7
7
  "websiteUrl": "https://getspacepal.com/integrations/mcp/",
8
8
  "packages": [
9
9
  {
10
10
  "registryType": "npm",
11
11
  "identifier": "spacepal-mcp",
12
- "version": "0.3.0",
12
+ "version": "0.4.1",
13
13
  "transport": {
14
14
  "type": "stdio"
15
15
  }