apple-notes-mcp 1.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.
package/build/types.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Type Definitions for Apple Notes MCP Server
3
+ *
4
+ * This module contains all TypeScript interfaces and types used throughout
5
+ * the Apple Notes MCP server. These types model:
6
+ *
7
+ * - Apple Notes data structures (notes, folders, accounts)
8
+ * - AppleScript execution results
9
+ * - MCP tool parameters
10
+ *
11
+ * @module types
12
+ */
13
+ export {};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * AppleScript Execution Utilities
3
+ *
4
+ * This module provides a safe interface for executing AppleScript commands
5
+ * on macOS. It handles script execution, error capture, and result parsing.
6
+ *
7
+ * @module utils/applescript
8
+ */
9
+ import { execSync } from "child_process";
10
+ /**
11
+ * Maximum execution time for AppleScript commands in milliseconds.
12
+ * Apple Notes operations are typically fast, but complex searches
13
+ * or operations on large note collections may take longer.
14
+ */
15
+ const EXECUTION_TIMEOUT_MS = 10000;
16
+ /**
17
+ * Escapes a string for safe inclusion in a shell command.
18
+ *
19
+ * When passing AppleScript to osascript via shell, we need to handle
20
+ * the interaction between shell quoting and AppleScript string literals.
21
+ * This function escapes single quotes since we wrap the script in single quotes.
22
+ *
23
+ * @param script - The raw AppleScript code
24
+ * @returns Shell-safe version of the script
25
+ *
26
+ * @example
27
+ * // Input: tell app "Notes" to get note "Rob's Note"
28
+ * // Output: tell app "Notes" to get note "Rob'\''s Note"
29
+ */
30
+ function escapeForShell(script) {
31
+ // Replace single quotes with: end quote, escaped quote, start quote
32
+ // This is the standard shell escaping pattern for single-quoted strings
33
+ return script.replace(/'/g, "'\\''");
34
+ }
35
+ /**
36
+ * Parses error output from osascript to extract meaningful error messages.
37
+ *
38
+ * osascript errors typically include execution error numbers and descriptions.
39
+ * This function attempts to extract the human-readable portion.
40
+ *
41
+ * @param errorOutput - Raw error string from execSync
42
+ * @returns Cleaned error message
43
+ */
44
+ function parseErrorMessage(errorOutput) {
45
+ // Check for common AppleScript error patterns
46
+ const executionError = errorOutput.match(/execution error: (.+?)(?:\s*\(-?\d+\))?$/m);
47
+ if (executionError) {
48
+ return executionError[1].trim();
49
+ }
50
+ // Check for "not found" type errors
51
+ const notFoundError = errorOutput.match(/Can't get (.+?)\./);
52
+ if (notFoundError) {
53
+ return `Not found: ${notFoundError[1]}`;
54
+ }
55
+ // Return cleaned version of original error
56
+ return errorOutput.trim() || "Unknown AppleScript error";
57
+ }
58
+ /**
59
+ * Executes an AppleScript command and returns a structured result.
60
+ *
61
+ * This function serves as the bridge between TypeScript and macOS AppleScript.
62
+ * It handles the complexity of shell escaping, execution, and error handling
63
+ * so that calling code can work with clean TypeScript interfaces.
64
+ *
65
+ * The script is executed synchronously via the `osascript` command-line tool.
66
+ * Multi-line scripts are supported and preserved (important for AppleScript
67
+ * tell blocks and repeat loops).
68
+ *
69
+ * @param script - The AppleScript code to execute
70
+ * @returns A result object with success status and output or error message
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const result = executeAppleScript(`
75
+ * tell application "Notes"
76
+ * get name of every note
77
+ * end tell
78
+ * `);
79
+ *
80
+ * if (result.success) {
81
+ * console.log("Notes:", result.output);
82
+ * } else {
83
+ * console.error("Failed:", result.error);
84
+ * }
85
+ * ```
86
+ */
87
+ export function executeAppleScript(script) {
88
+ // Validate input - empty scripts are likely programmer errors
89
+ if (!script || !script.trim()) {
90
+ return {
91
+ success: false,
92
+ output: "",
93
+ error: "Cannot execute empty AppleScript",
94
+ };
95
+ }
96
+ // Prepare the script:
97
+ // 1. Trim leading/trailing whitespace (cosmetic)
98
+ // 2. Preserve internal newlines (required for AppleScript syntax)
99
+ // 3. Escape for shell execution
100
+ const preparedScript = escapeForShell(script.trim());
101
+ // Build the osascript command
102
+ // We use single quotes to wrap the script, which is why we escape
103
+ // single quotes within the script itself
104
+ const command = `osascript -e '${preparedScript}'`;
105
+ try {
106
+ // Execute synchronously - MCP tools are inherently synchronous
107
+ // and Apple Notes operations are fast enough that async isn't needed
108
+ const output = execSync(command, {
109
+ encoding: "utf8",
110
+ timeout: EXECUTION_TIMEOUT_MS,
111
+ // Capture stderr separately to get error details
112
+ stdio: ["pipe", "pipe", "pipe"],
113
+ });
114
+ return {
115
+ success: true,
116
+ output: output.trim(),
117
+ };
118
+ }
119
+ catch (error) {
120
+ // execSync throws on non-zero exit codes
121
+ // The error object contains stderr output with AppleScript error details
122
+ let errorMessage;
123
+ if (error instanceof Error) {
124
+ // Node's ExecException includes stderr in the message
125
+ errorMessage = parseErrorMessage(error.message);
126
+ }
127
+ else if (typeof error === "string") {
128
+ errorMessage = parseErrorMessage(error);
129
+ }
130
+ else {
131
+ errorMessage = "AppleScript execution failed with unknown error";
132
+ }
133
+ // Log for debugging (MCP servers typically run in terminal)
134
+ console.error(`AppleScript error: ${errorMessage}`);
135
+ return {
136
+ success: false,
137
+ output: "",
138
+ error: errorMessage,
139
+ };
140
+ }
141
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Tests for AppleScript execution utilities
3
+ *
4
+ * These tests mock the child_process.execSync function to avoid
5
+ * requiring actual AppleScript execution during testing.
6
+ */
7
+ import { describe, it, expect, vi, beforeEach } from "vitest";
8
+ import { execSync } from "child_process";
9
+ import { executeAppleScript } from "./applescript.js";
10
+ // Mock the child_process module
11
+ vi.mock("child_process", () => ({
12
+ execSync: vi.fn(),
13
+ }));
14
+ const mockExecSync = vi.mocked(execSync);
15
+ describe("executeAppleScript", () => {
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+ describe("successful execution", () => {
20
+ it("returns success result with trimmed output", () => {
21
+ // Arrange: Mock a successful AppleScript execution
22
+ mockExecSync.mockReturnValue(" Note Title \n");
23
+ // Act: Execute a simple script
24
+ const result = executeAppleScript('tell app "Notes" to get name of note 1');
25
+ // Assert: Output should be trimmed
26
+ expect(result.success).toBe(true);
27
+ expect(result.output).toBe("Note Title");
28
+ expect(result.error).toBeUndefined();
29
+ });
30
+ it("preserves newlines within the script for AppleScript syntax", () => {
31
+ mockExecSync.mockReturnValue("success");
32
+ // Multi-line AppleScript with tell blocks
33
+ const script = `
34
+ tell application "Notes"
35
+ tell account "iCloud"
36
+ get notes
37
+ end tell
38
+ end tell
39
+ `;
40
+ executeAppleScript(script);
41
+ // Verify the script was passed with newlines preserved
42
+ const calledCommand = mockExecSync.mock.calls[0][0];
43
+ expect(calledCommand).toContain("tell application");
44
+ expect(calledCommand).toContain("end tell");
45
+ });
46
+ it("escapes single quotes in the script for shell safety", () => {
47
+ mockExecSync.mockReturnValue("content");
48
+ // Script containing a single quote (e.g., in a note title)
49
+ executeAppleScript('get note "Rob\'s Notes"');
50
+ // Verify the quote was escaped for shell
51
+ const calledCommand = mockExecSync.mock.calls[0][0];
52
+ expect(calledCommand).toContain("Rob'\\''s");
53
+ });
54
+ });
55
+ describe("error handling", () => {
56
+ it("returns error result when execution fails", () => {
57
+ // Arrange: Mock an AppleScript execution failure
58
+ mockExecSync.mockImplementation(() => {
59
+ throw new Error("execution error: Can't get note. (-1728)");
60
+ });
61
+ // Act: Try to execute a script that will fail
62
+ const result = executeAppleScript('get note "Nonexistent"');
63
+ // Assert: Should return structured error
64
+ expect(result.success).toBe(false);
65
+ expect(result.output).toBe("");
66
+ expect(result.error).toBeDefined();
67
+ });
68
+ it("parses execution error messages cleanly", () => {
69
+ mockExecSync.mockImplementation(() => {
70
+ throw new Error("execution error: Note not found (-1728)");
71
+ });
72
+ const result = executeAppleScript("get note 1");
73
+ // Should extract the meaningful part of the error
74
+ expect(result.error).toBe("Note not found");
75
+ });
76
+ it("handles 'not found' error patterns", () => {
77
+ mockExecSync.mockImplementation(() => {
78
+ throw new Error('Can\'t get note "Missing".');
79
+ });
80
+ const result = executeAppleScript('get note "Missing"');
81
+ expect(result.error).toContain("Not found");
82
+ });
83
+ it("handles non-Error exceptions gracefully", () => {
84
+ mockExecSync.mockImplementation(() => {
85
+ throw "string error"; // Some code throws strings
86
+ });
87
+ const result = executeAppleScript("some script");
88
+ expect(result.success).toBe(false);
89
+ expect(result.error).toBe("string error");
90
+ });
91
+ it("handles unknown error types", () => {
92
+ mockExecSync.mockImplementation(() => {
93
+ throw { weird: "object" }; // Unusual but possible
94
+ });
95
+ const result = executeAppleScript("some script");
96
+ expect(result.success).toBe(false);
97
+ expect(result.error).toBe("AppleScript execution failed with unknown error");
98
+ });
99
+ });
100
+ describe("input validation", () => {
101
+ it("returns error for empty script", () => {
102
+ const result = executeAppleScript("");
103
+ expect(result.success).toBe(false);
104
+ expect(result.error).toBe("Cannot execute empty AppleScript");
105
+ expect(mockExecSync).not.toHaveBeenCalled();
106
+ });
107
+ it("returns error for whitespace-only script", () => {
108
+ const result = executeAppleScript(" \n\t ");
109
+ expect(result.success).toBe(false);
110
+ expect(result.error).toBe("Cannot execute empty AppleScript");
111
+ expect(mockExecSync).not.toHaveBeenCalled();
112
+ });
113
+ });
114
+ describe("execution options", () => {
115
+ it("uses correct timeout setting", () => {
116
+ mockExecSync.mockReturnValue("ok");
117
+ executeAppleScript("test");
118
+ const options = mockExecSync.mock.calls[0][1];
119
+ expect(options.timeout).toBe(10000); // 10 second timeout
120
+ });
121
+ it("uses UTF-8 encoding for output", () => {
122
+ mockExecSync.mockReturnValue("日本語テスト");
123
+ const result = executeAppleScript("test");
124
+ expect(result.output).toBe("日本語テスト");
125
+ const options = mockExecSync.mock.calls[0][1];
126
+ expect(options.encoding).toBe("utf8");
127
+ });
128
+ });
129
+ });
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "apple-notes-mcp",
3
+ "version": "1.1.0",
4
+ "description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "types": "build/index.d.ts",
8
+ "files": [
9
+ "build",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc && tsc-alias",
15
+ "start": "node build/index.js",
16
+ "dev": "tsc --watch",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext .ts",
20
+ "lint:fix": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext .ts --fix",
21
+ "format": "prettier --write src",
22
+ "format:check": "prettier --check src",
23
+ "typecheck": "tsc --noEmit",
24
+ "prepublishOnly": "npm run lint && npm run test && npm run build"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "apple-notes",
29
+ "claude",
30
+ "ai",
31
+ "applescript",
32
+ "macos",
33
+ "notes",
34
+ "model-context-protocol"
35
+ ],
36
+ "author": "Rob Sweet <rob@superiortech.io>",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/sweetrb/mcp-apple-notes.git"
41
+ },
42
+ "homepage": "https://github.com/sweetrb/mcp-apple-notes#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/sweetrb/mcp-apple-notes/issues"
45
+ },
46
+ "engines": {
47
+ "node": ">=20.0.0"
48
+ },
49
+ "os": [
50
+ "darwin"
51
+ ],
52
+ "dependencies": {
53
+ "@modelcontextprotocol/sdk": "1.4.1",
54
+ "zod": "^3.22.4"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "^20.0.0",
58
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
59
+ "@typescript-eslint/parser": "^8.0.0",
60
+ "eslint": "^9.0.0",
61
+ "prettier": "^3.0.0",
62
+ "tsc-alias": "^1.8.10",
63
+ "tsconfig-paths": "^4.2.0",
64
+ "typescript": "^5.0.0",
65
+ "vitest": "^2.0.0"
66
+ },
67
+ "volta": {
68
+ "node": "22.13.1"
69
+ }
70
+ }