apple-notes-mcp 2.5.7 → 2.5.9
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 +9 -5
- package/build/index.js +42755 -1080
- package/package.json +3 -3
- package/build/index.test.js +0 -446
- package/build/services/__fixtures__/notesNormalizedHtml.js +0 -32
- package/build/services/appleNotesManager.js +0 -2634
- package/build/services/appleNotesManager.test.js +0 -2416
- package/build/services/attachmentSave.test.js +0 -85
- package/build/services/fileConfig.js +0 -51
- package/build/services/fileConfig.test.js +0 -48
- package/build/services/notesHtmlMarkdown.test.js +0 -55
- package/build/tools/doctor.js +0 -50
- package/build/tools/doctor.test.js +0 -42
- package/build/tools/resourcesAndPrompts.js +0 -70
- package/build/tools/resourcesAndPrompts.test.js +0 -63
- package/build/types.js +0 -13
- package/build/utils/applescript.js +0 -421
- package/build/utils/applescript.test.js +0 -342
- package/build/utils/attachmentFs.js +0 -97
- package/build/utils/attachmentFs.test.js +0 -69
- package/build/utils/checklistParser.js +0 -259
- package/build/utils/checklistParser.test.js +0 -230
- package/build/utils/contentWarnings.js +0 -44
- package/build/utils/contentWarnings.test.js +0 -52
- package/build/utils/hashtags.js +0 -56
- package/build/utils/hashtags.test.js +0 -45
- package/build/utils/jxa.js +0 -139
- package/build/utils/jxa.test.js +0 -134
- package/build/utils/noteMetadata.js +0 -135
- package/build/utils/noteMetadata.test.js +0 -106
- package/build/utils/protobuf.js +0 -151
- package/build/utils/protobuf.test.js +0 -138
- package/build/utils/syncDetection.js +0 -242
- package/build/utils/syncDetection.test.js +0 -228
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for save-attachment / fetch-attachment manager methods (#27).
|
|
3
|
-
* AppleScript is mocked; the filesystem side runs for real in a temp dir, with
|
|
4
|
-
* the mock writing the file the way Notes.app's `save` would.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
7
|
-
import { writeFileSync, mkdtempSync } from "fs";
|
|
8
|
-
import { tmpdir } from "os";
|
|
9
|
-
import { join } from "path";
|
|
10
|
-
vi.mock("@/utils/applescript.js", () => ({ executeAppleScript: vi.fn() }));
|
|
11
|
-
vi.mock("@/utils/checklistParser.js", () => ({
|
|
12
|
-
getChecklistItems: vi.fn().mockReturnValue({ items: null }),
|
|
13
|
-
}));
|
|
14
|
-
import { AppleNotesManager } from "../services/appleNotesManager.js";
|
|
15
|
-
import { executeAppleScript } from "../utils/applescript.js";
|
|
16
|
-
const mockExec = vi.mocked(executeAppleScript);
|
|
17
|
-
const F = "\x1f";
|
|
18
|
-
let manager;
|
|
19
|
-
const tmpDirs = [];
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
vi.clearAllMocks();
|
|
22
|
-
manager = new AppleNotesManager();
|
|
23
|
-
});
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
vi.restoreAllMocks();
|
|
26
|
-
});
|
|
27
|
-
describe("saveAttachmentById (#27)", () => {
|
|
28
|
-
it("saves to an allowed path and returns metadata", () => {
|
|
29
|
-
const dir = mkdtempSync(join(tmpdir(), "anatt-"));
|
|
30
|
-
tmpDirs.push(dir);
|
|
31
|
-
const dest = join(dir, "photo.png");
|
|
32
|
-
mockExec.mockImplementation((script) => {
|
|
33
|
-
const m = script.match(/POSIX file "([^"]+)"/);
|
|
34
|
-
if (m)
|
|
35
|
-
writeFileSync(m[1], Buffer.from("PNGDATA"));
|
|
36
|
-
return { success: true, output: ["OK", "photo.png", "public.png"].join(F) };
|
|
37
|
-
});
|
|
38
|
-
const r = manager.saveAttachmentById("x-coredata://A/ICNote/p1", "att-1", dest);
|
|
39
|
-
expect(r.success).toBe(true);
|
|
40
|
-
expect(r.savedPath).toBe(dest);
|
|
41
|
-
expect(r.name).toBe("photo.png");
|
|
42
|
-
expect(r.contentType).toBe("public.png");
|
|
43
|
-
});
|
|
44
|
-
it("rejects an unsafe destination before running AppleScript", () => {
|
|
45
|
-
const r = manager.saveAttachmentById("x-coredata://A/ICNote/p1", "att-1", "/etc/evil.png");
|
|
46
|
-
expect(r.success).toBe(false);
|
|
47
|
-
expect(r.error).toMatch(/outside allowed/);
|
|
48
|
-
expect(mockExec).not.toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
it("surfaces 'attachment not found' from AppleScript", () => {
|
|
51
|
-
const dest = join(tmpdir(), "nope.png");
|
|
52
|
-
mockExec.mockReturnValue({ success: true, output: ["ERR", "attachment not found"].join(F) });
|
|
53
|
-
const r = manager.saveAttachmentById("x-coredata://A/ICNote/p1", "missing", dest);
|
|
54
|
-
expect(r.success).toBe(false);
|
|
55
|
-
expect(r.error).toMatch(/not found/);
|
|
56
|
-
});
|
|
57
|
-
it("fails when Notes reports OK but no file was written", () => {
|
|
58
|
-
const dest = join(tmpdir(), "ghost-" + Date.now() + ".png");
|
|
59
|
-
mockExec.mockReturnValue({ success: true, output: ["OK", "x.png", "public.png"].join(F) });
|
|
60
|
-
const r = manager.saveAttachmentById("x-coredata://A/ICNote/p1", "att-1", dest);
|
|
61
|
-
expect(r.success).toBe(false);
|
|
62
|
-
expect(r.error).toMatch(/no file was written/);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
describe("getAttachmentBase64ById (#27)", () => {
|
|
66
|
-
it("exports to a temp file and returns base64, cleaning up", () => {
|
|
67
|
-
mockExec.mockImplementation((script) => {
|
|
68
|
-
const m = script.match(/POSIX file "([^"]+)"/);
|
|
69
|
-
if (m)
|
|
70
|
-
writeFileSync(m[1], Buffer.from("hello-bytes"));
|
|
71
|
-
return { success: true, output: ["OK", "doc.pdf", "com.adobe.pdf"].join(F) };
|
|
72
|
-
});
|
|
73
|
-
const r = manager.getAttachmentBase64ById("x-coredata://A/ICNote/p1", "att-1");
|
|
74
|
-
expect(r.success).toBe(true);
|
|
75
|
-
expect(r.name).toBe("doc.pdf");
|
|
76
|
-
expect(r.bytes).toBe("hello-bytes".length);
|
|
77
|
-
expect(Buffer.from(r.base64 ?? "", "base64").toString()).toBe("hello-bytes");
|
|
78
|
-
});
|
|
79
|
-
it("returns the error when the save step fails", () => {
|
|
80
|
-
mockExec.mockReturnValue({ success: false, output: "", error: "Notes not running" });
|
|
81
|
-
const r = manager.getAttachmentBase64ById("x-coredata://A/ICNote/p1", "att-1");
|
|
82
|
-
expect(r.success).toBe(false);
|
|
83
|
-
expect(r.error).toMatch(/Notes not running/);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File-based configuration loader (#24).
|
|
3
|
-
*
|
|
4
|
-
* Some host apps (e.g. Claude Desktop) spawn the MCP server with a scrubbed
|
|
5
|
-
* environment and ignore the `env` block in their server config, so there's no
|
|
6
|
-
* way to pass `APPLE_NOTES_MCP_*` settings in. This loads them from a JSON file
|
|
7
|
-
* the host doesn't manage, merging into `process.env` WITHOUT overriding
|
|
8
|
-
* anything already set (so an explicit env still wins).
|
|
9
|
-
*
|
|
10
|
-
* Only non-secret config belongs here (e.g. APPLE_NOTES_MCP_MAX_BUFFER, DEBUG).
|
|
11
|
-
*
|
|
12
|
-
* Path: `APPLE_NOTES_MCP_CONFIG_FILE`, else
|
|
13
|
-
* `~/Library/Application Support/apple-notes-mcp/config.json`.
|
|
14
|
-
*
|
|
15
|
-
* @module services/fileConfig
|
|
16
|
-
*/
|
|
17
|
-
import { existsSync, readFileSync } from "fs";
|
|
18
|
-
import { join } from "path";
|
|
19
|
-
import { homedir } from "os";
|
|
20
|
-
export function fileConfigPath(env = process.env) {
|
|
21
|
-
const override = env.APPLE_NOTES_MCP_CONFIG_FILE;
|
|
22
|
-
if (override && override.trim())
|
|
23
|
-
return override.trim();
|
|
24
|
-
return join(homedir(), "Library", "Application Support", "apple-notes-mcp", "config.json");
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Merge a JSON config file's string values into `env` for keys not already set.
|
|
28
|
-
* Returns the keys applied. Tolerates a missing/corrupt file.
|
|
29
|
-
*/
|
|
30
|
-
export function loadFileConfig(env = process.env, path = fileConfigPath(env)) {
|
|
31
|
-
const applied = [];
|
|
32
|
-
try {
|
|
33
|
-
if (!existsSync(path))
|
|
34
|
-
return applied;
|
|
35
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
-
if (!parsed || typeof parsed !== "object")
|
|
37
|
-
return applied;
|
|
38
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
39
|
-
if (typeof v !== "string")
|
|
40
|
-
continue;
|
|
41
|
-
if (env[k] === undefined || env[k] === "") {
|
|
42
|
-
env[k] = v;
|
|
43
|
-
applied.push(k);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch (e) {
|
|
48
|
-
console.error(`Failed to load apple-notes-mcp config file ${path}: ${String(e)}`);
|
|
49
|
-
}
|
|
50
|
-
return applied;
|
|
51
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { loadFileConfig, fileConfigPath } from "../services/fileConfig.js";
|
|
6
|
-
let dir;
|
|
7
|
-
let file;
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
dir = mkdtempSync(join(tmpdir(), "anmcp-cfg-"));
|
|
10
|
-
file = join(dir, "config.json");
|
|
11
|
-
});
|
|
12
|
-
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
13
|
-
describe("loadFileConfig (#24)", () => {
|
|
14
|
-
it("applies file values for keys not already in env", () => {
|
|
15
|
-
writeFileSync(file, JSON.stringify({ APPLE_NOTES_MCP_MAX_BUFFER: "1048576", DEBUG: "1" }));
|
|
16
|
-
const env = {};
|
|
17
|
-
const applied = loadFileConfig(env, file);
|
|
18
|
-
expect(env.APPLE_NOTES_MCP_MAX_BUFFER).toBe("1048576");
|
|
19
|
-
expect(env.DEBUG).toBe("1");
|
|
20
|
-
expect(applied.sort()).toEqual(["APPLE_NOTES_MCP_MAX_BUFFER", "DEBUG"]);
|
|
21
|
-
});
|
|
22
|
-
it("never overrides a value already set in the environment", () => {
|
|
23
|
-
writeFileSync(file, JSON.stringify({ APPLE_NOTES_MCP_MAX_BUFFER: "1" }));
|
|
24
|
-
const env = { APPLE_NOTES_MCP_MAX_BUFFER: "999" };
|
|
25
|
-
loadFileConfig(env, file);
|
|
26
|
-
expect(env.APPLE_NOTES_MCP_MAX_BUFFER).toBe("999");
|
|
27
|
-
});
|
|
28
|
-
it("treats empty-string env as unset and fills it", () => {
|
|
29
|
-
writeFileSync(file, JSON.stringify({ DEBUG: "1" }));
|
|
30
|
-
const env = { DEBUG: "" };
|
|
31
|
-
loadFileConfig(env, file);
|
|
32
|
-
expect(env.DEBUG).toBe("1");
|
|
33
|
-
});
|
|
34
|
-
it("ignores non-string values", () => {
|
|
35
|
-
writeFileSync(file, JSON.stringify({ A: "ok", B: 5, C: true }));
|
|
36
|
-
const env = {};
|
|
37
|
-
expect(loadFileConfig(env, file)).toEqual(["A"]);
|
|
38
|
-
});
|
|
39
|
-
it("tolerates a missing file and a corrupt file", () => {
|
|
40
|
-
expect(loadFileConfig({}, join(dir, "nope.json"))).toEqual([]);
|
|
41
|
-
writeFileSync(file, "{ not json");
|
|
42
|
-
expect(loadFileConfig({}, file)).toEqual([]);
|
|
43
|
-
});
|
|
44
|
-
it("defaults the path to the app-support dir, honoring the override", () => {
|
|
45
|
-
expect(fileConfigPath({})).toMatch(/apple-notes-mcp\/config\.json$/);
|
|
46
|
-
expect(fileConfigPath({ APPLE_NOTES_MCP_CONFIG_FILE: "/tmp/x.json" })).toBe("/tmp/x.json");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { NOTES_NORMALIZED_HTML_FIXTURES } from "../services/__fixtures__/notesNormalizedHtml.js";
|
|
3
|
-
// Mock the same seams the main manager test does: AppleScript execution and the
|
|
4
|
-
// SQLite-backed checklist reader (no Full Disk Access in unit tests).
|
|
5
|
-
vi.mock("@/utils/applescript.js", () => ({
|
|
6
|
-
executeAppleScript: vi.fn(),
|
|
7
|
-
}));
|
|
8
|
-
vi.mock("@/utils/checklistParser.js", () => ({
|
|
9
|
-
getChecklistItems: vi.fn().mockReturnValue({ items: null }),
|
|
10
|
-
}));
|
|
11
|
-
import { executeAppleScript } from "../utils/applescript.js";
|
|
12
|
-
import { AppleNotesManager } from "../services/appleNotesManager.js";
|
|
13
|
-
const mockExecuteAppleScript = vi.mocked(executeAppleScript);
|
|
14
|
-
const NOTE_ID = "x-coredata://ABC/ICNote/p1";
|
|
15
|
-
/**
|
|
16
|
-
* Regression coverage for Notes-normalized HTML -> Markdown conversion.
|
|
17
|
-
*
|
|
18
|
-
* The fixtures encode the HTML shape Apple Notes returns and the Markdown the
|
|
19
|
-
* server currently emits. Routing through getNoteMarkdownById exercises the real
|
|
20
|
-
* Turndown pipeline (including the notesDivs rule) with AppleScript mocked to
|
|
21
|
-
* return the fixture body.
|
|
22
|
-
*/
|
|
23
|
-
describe("Notes-normalized HTML to Markdown", () => {
|
|
24
|
-
let manager;
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
vi.clearAllMocks();
|
|
27
|
-
manager = new AppleNotesManager();
|
|
28
|
-
});
|
|
29
|
-
for (const fixture of NOTES_NORMALIZED_HTML_FIXTURES) {
|
|
30
|
-
it(`converts ${fixture.name} (${fixture.description})`, () => {
|
|
31
|
-
mockExecuteAppleScript.mockReturnValue({ success: true, output: fixture.html });
|
|
32
|
-
const markdown = manager.getNoteMarkdownById(NOTE_ID);
|
|
33
|
-
expect(markdown).toBe(fixture.expectedMarkdown);
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
it("documents that a <div><br></div> spacer leaves a two-space line", () => {
|
|
37
|
-
mockExecuteAppleScript.mockReturnValue({
|
|
38
|
-
success: true,
|
|
39
|
-
output: "<div>A</div><div><br></div><div>B</div>",
|
|
40
|
-
});
|
|
41
|
-
const markdown = manager.getNoteMarkdownById(NOTE_ID);
|
|
42
|
-
// The spacer survives as a stray " " line — the Markdown-side fingerprint of
|
|
43
|
-
// the whitespace-accumulation behavior CLAUDE.md warns about.
|
|
44
|
-
expect(markdown).toBe("A\n \n\nB");
|
|
45
|
-
});
|
|
46
|
-
it("documents that <tt> is dropped, keeping only its text", () => {
|
|
47
|
-
mockExecuteAppleScript.mockReturnValue({
|
|
48
|
-
success: true,
|
|
49
|
-
output: "<div>Run <tt>npm install</tt> first.</div>",
|
|
50
|
-
});
|
|
51
|
-
const markdown = manager.getNoteMarkdownById(NOTE_ID);
|
|
52
|
-
expect(markdown).toBe("Run npm install first.");
|
|
53
|
-
expect(markdown).not.toContain("`");
|
|
54
|
-
});
|
|
55
|
-
});
|
package/build/tools/doctor.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { hasFullDiskAccess } from "../utils/checklistParser.js";
|
|
2
|
-
export function runDoctor(manager) {
|
|
3
|
-
const checks = [];
|
|
4
|
-
// 1. Notes.app reachability + Automation permission (existing health checks).
|
|
5
|
-
const hc = manager.healthCheck();
|
|
6
|
-
for (const c of hc.checks) {
|
|
7
|
-
checks.push({
|
|
8
|
-
name: `Notes.app: ${c.name}`,
|
|
9
|
-
status: c.passed ? "ok" : "fail",
|
|
10
|
-
detail: c.message,
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
// 2. Accounts.
|
|
14
|
-
try {
|
|
15
|
-
const accounts = manager.listAccounts();
|
|
16
|
-
checks.push({
|
|
17
|
-
name: "Accounts",
|
|
18
|
-
status: accounts.length > 0 ? "ok" : "warn",
|
|
19
|
-
detail: accounts.length > 0
|
|
20
|
-
? `${accounts.length} account(s): ${accounts.map((a) => a.name).join(", ")}`
|
|
21
|
-
: "no Notes accounts found",
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
catch (e) {
|
|
25
|
-
checks.push({
|
|
26
|
-
name: "Accounts",
|
|
27
|
-
status: "fail",
|
|
28
|
-
detail: `could not list accounts: ${String(e)}`,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
// 3. Full Disk Access — required for checklist state + checklist annotations.
|
|
32
|
-
const fda = hasFullDiskAccess();
|
|
33
|
-
checks.push({
|
|
34
|
-
name: "Full Disk Access",
|
|
35
|
-
status: fda ? "ok" : "warn",
|
|
36
|
-
detail: fda
|
|
37
|
-
? "granted — checklist features available"
|
|
38
|
-
: "not granted — get-checklist-state and checklist annotations in get-note-markdown won't work. Grant in System Settings > Privacy & Security > Full Disk Access.",
|
|
39
|
-
});
|
|
40
|
-
const healthy = !checks.some((c) => c.status === "fail");
|
|
41
|
-
return { healthy, checks };
|
|
42
|
-
}
|
|
43
|
-
/** Render a DoctorReport as readable text. */
|
|
44
|
-
export function formatDoctorReport(r) {
|
|
45
|
-
const icon = (s) => (s === "ok" ? "✅" : s === "warn" ? "⚠️ " : "❌");
|
|
46
|
-
const lines = [`🩺 apple-notes-mcp doctor — ${r.healthy ? "healthy" : "ISSUES FOUND"}`, ""];
|
|
47
|
-
for (const c of r.checks)
|
|
48
|
-
lines.push(`${icon(c.status)} ${c.name}: ${c.detail}`);
|
|
49
|
-
return lines.join("\n");
|
|
50
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
vi.mock("@/utils/checklistParser.js", () => ({ hasFullDiskAccess: vi.fn(() => true) }));
|
|
3
|
-
import { runDoctor, formatDoctorReport } from "../tools/doctor.js";
|
|
4
|
-
import { hasFullDiskAccess } from "../utils/checklistParser.js";
|
|
5
|
-
function fakeMgr(over = {}) {
|
|
6
|
-
return {
|
|
7
|
-
healthCheck: () => ({
|
|
8
|
-
healthy: true,
|
|
9
|
-
checks: [{ name: "reachable", passed: true, message: "Notes.app responded" }],
|
|
10
|
-
}),
|
|
11
|
-
listAccounts: () => [{ name: "iCloud" }, { name: "Gmail" }],
|
|
12
|
-
...over,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
describe("runDoctor (#22)", () => {
|
|
16
|
-
it("reports accounts + Full Disk Access and stays healthy on warnings", () => {
|
|
17
|
-
const r = runDoctor(fakeMgr());
|
|
18
|
-
expect(r.healthy).toBe(true);
|
|
19
|
-
expect(r.checks.find((c) => c.name === "Accounts")?.status).toBe("ok");
|
|
20
|
-
expect(r.checks.find((c) => c.name === "Accounts")?.detail).toMatch(/iCloud, Gmail/);
|
|
21
|
-
expect(r.checks.find((c) => c.name === "Full Disk Access")?.status).toBe("ok");
|
|
22
|
-
});
|
|
23
|
-
it("warns (not fails) when Full Disk Access is not granted", () => {
|
|
24
|
-
vi.mocked(hasFullDiskAccess).mockReturnValueOnce(false);
|
|
25
|
-
const r = runDoctor(fakeMgr());
|
|
26
|
-
const fda = r.checks.find((c) => c.name === "Full Disk Access");
|
|
27
|
-
expect(fda?.status).toBe("warn");
|
|
28
|
-
expect(fda?.detail).toMatch(/Full Disk Access/);
|
|
29
|
-
expect(r.healthy).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
it("is unhealthy when a Notes.app check fails", () => {
|
|
32
|
-
const r = runDoctor(fakeMgr({
|
|
33
|
-
healthCheck: () => ({
|
|
34
|
-
healthy: false,
|
|
35
|
-
checks: [{ name: "permission", passed: false, message: "not authorized" }],
|
|
36
|
-
}),
|
|
37
|
-
}));
|
|
38
|
-
expect(r.healthy).toBe(false);
|
|
39
|
-
expect(formatDoctorReport(r)).toMatch(/ISSUES FOUND/);
|
|
40
|
-
expect(formatDoctorReport(r)).toMatch(/❌ Notes\.app: permission/);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP resources & prompts for apple-notes (#23).
|
|
3
|
-
*
|
|
4
|
-
* Resources expose read-only views agents can attach as context without a tool
|
|
5
|
-
* round-trip (accounts, folders, stats, and a note-by-id template). Prompts are
|
|
6
|
-
* reusable starting points for common Notes workflows.
|
|
7
|
-
*
|
|
8
|
-
* @module tools/resourcesAndPrompts
|
|
9
|
-
*/
|
|
10
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
-
import { z } from "zod";
|
|
12
|
-
const json = (uri, data) => ({
|
|
13
|
-
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(data, null, 2) }],
|
|
14
|
-
});
|
|
15
|
-
export function registerResourcesAndPrompts(server, manager) {
|
|
16
|
-
// --- Resources ---
|
|
17
|
-
server.resource("accounts", "notes://accounts", (uri) => json(uri, { accounts: manager.listAccounts() }));
|
|
18
|
-
server.resource("folders", "notes://folders", (uri) => {
|
|
19
|
-
const data = manager
|
|
20
|
-
.listAccounts()
|
|
21
|
-
.map((a) => ({ account: a.name, folders: manager.listFolders(a.name) }));
|
|
22
|
-
return json(uri, { accounts: data });
|
|
23
|
-
});
|
|
24
|
-
server.resource("stats", "notes://stats", (uri) => json(uri, manager.getNotesStats()));
|
|
25
|
-
server.resource("note", new ResourceTemplate("notes://note/{id}", { list: undefined }), (uri, variables) => {
|
|
26
|
-
const id = decodeURIComponent(String(variables.id));
|
|
27
|
-
const markdown = manager.getNoteMarkdownById(id);
|
|
28
|
-
return {
|
|
29
|
-
contents: [{ uri: uri.href, mimeType: "text/markdown", text: markdown || "(not found)" }],
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
// --- Prompts ---
|
|
33
|
-
server.prompt("find-note", "Search Apple Notes for a topic and summarize the best match", { topic: z.string().describe("What to search for") }, ({ topic }) => ({
|
|
34
|
-
messages: [
|
|
35
|
-
{
|
|
36
|
-
role: "user",
|
|
37
|
-
content: {
|
|
38
|
-
type: "text",
|
|
39
|
-
text: `Search my Apple Notes for "${topic}" with the search-notes tool (set searchContent: true). Open the most relevant result with get-note-content and give me a concise summary plus its note id.`,
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
}));
|
|
44
|
-
server.prompt("weekly-review", "Review notes changed recently and surface follow-ups", () => ({
|
|
45
|
-
messages: [
|
|
46
|
-
{
|
|
47
|
-
role: "user",
|
|
48
|
-
content: {
|
|
49
|
-
type: "text",
|
|
50
|
-
text: "Use get-notes-stats to see how many notes changed in the last 7 days, then search-notes (searchContent: true, modifiedSince: the date 7 days ago) to list them. Summarize the themes and call out any open action items or checklists I should follow up on.",
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
}));
|
|
55
|
-
server.prompt("new-meeting-note", "Draft and create a structured meeting note", {
|
|
56
|
-
subject: z.string().describe("Meeting subject"),
|
|
57
|
-
attendees: z.string().optional().describe("Comma-separated attendees"),
|
|
58
|
-
folder: z.string().optional().describe("Target folder"),
|
|
59
|
-
}, ({ subject, attendees, folder }) => ({
|
|
60
|
-
messages: [
|
|
61
|
-
{
|
|
62
|
-
role: "user",
|
|
63
|
-
content: {
|
|
64
|
-
type: "text",
|
|
65
|
-
text: `Create an Apple Note titled "${subject}" ${folder ? `in folder "${folder}" ` : ""}using create-note (format: html). Include sections for Attendees${attendees ? ` (${attendees})` : ""}, Agenda, Discussion, and Action Items. Render Action Items as a plain bulleted list and remind me I can convert it to a checklist in Notes with ⇧⌘L.`,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
}));
|
|
70
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { registerResourcesAndPrompts } from "../tools/resourcesAndPrompts.js";
|
|
3
|
-
class FakeServer {
|
|
4
|
-
resources = new Map();
|
|
5
|
-
prompts = new Map();
|
|
6
|
-
resource(name, uriOrTemplate, cb) {
|
|
7
|
-
this.resources.set(name, { uriOrTemplate, cb });
|
|
8
|
-
}
|
|
9
|
-
prompt(name, ...rest) {
|
|
10
|
-
this.prompts.set(name, rest[rest.length - 1]);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function fakeMgr() {
|
|
14
|
-
return {
|
|
15
|
-
listAccounts: () => [{ name: "iCloud" }],
|
|
16
|
-
listFolders: vi.fn(() => [{ name: "Notes" }]),
|
|
17
|
-
getNotesStats: () => ({
|
|
18
|
-
totalNotes: 1,
|
|
19
|
-
accounts: [],
|
|
20
|
-
recentlyModified: { last24h: 0, last7d: 0, last30d: 0 },
|
|
21
|
-
}),
|
|
22
|
-
getNoteMarkdownById: vi.fn(() => "# Hello"),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
describe("registerResourcesAndPrompts (#23)", () => {
|
|
26
|
-
it("registers the expected resources and prompts", () => {
|
|
27
|
-
const s = new FakeServer();
|
|
28
|
-
registerResourcesAndPrompts(s, fakeMgr());
|
|
29
|
-
expect([...s.resources.keys()].sort()).toEqual(["accounts", "folders", "note", "stats"]);
|
|
30
|
-
expect([...s.prompts.keys()].sort()).toEqual([
|
|
31
|
-
"find-note",
|
|
32
|
-
"new-meeting-note",
|
|
33
|
-
"weekly-review",
|
|
34
|
-
]);
|
|
35
|
-
});
|
|
36
|
-
it("accounts/folders/stats resources return JSON", () => {
|
|
37
|
-
const s = new FakeServer();
|
|
38
|
-
registerResourcesAndPrompts(s, fakeMgr());
|
|
39
|
-
const acc = s.resources.get("accounts").cb(new URL("notes://accounts"), {});
|
|
40
|
-
expect(JSON.parse(acc.contents[0].text)).toEqual({ accounts: [{ name: "iCloud" }] });
|
|
41
|
-
const fol = s.resources.get("folders").cb(new URL("notes://folders"), {});
|
|
42
|
-
expect(JSON.parse(fol.contents[0].text).accounts[0].account).toBe("iCloud");
|
|
43
|
-
const st = s.resources.get("stats").cb(new URL("notes://stats"), {});
|
|
44
|
-
expect(JSON.parse(st.contents[0].text).totalNotes).toBe(1);
|
|
45
|
-
});
|
|
46
|
-
it("note template resolves the id and returns markdown", () => {
|
|
47
|
-
const s = new FakeServer();
|
|
48
|
-
const mgr = fakeMgr();
|
|
49
|
-
registerResourcesAndPrompts(s, mgr);
|
|
50
|
-
const out = s.resources.get("note").cb(new URL("notes://note/abc%20def"), { id: "abc%20def" });
|
|
51
|
-
expect(mgr.getNoteMarkdownById).toHaveBeenCalledWith("abc def");
|
|
52
|
-
expect(out.contents[0].text).toBe("# Hello");
|
|
53
|
-
});
|
|
54
|
-
it("prompts produce user messages including their args", () => {
|
|
55
|
-
const s = new FakeServer();
|
|
56
|
-
registerResourcesAndPrompts(s, fakeMgr());
|
|
57
|
-
expect(s.prompts.get("find-note")({ topic: "taxes" }).messages[0].content.text).toMatch(/taxes/);
|
|
58
|
-
expect(s.prompts.get("weekly-review")({}).messages[0].content.text).toMatch(/last 7 days/);
|
|
59
|
-
const mtg = s.prompts.get("new-meeting-note")({ subject: "Q3 Plan", attendees: "Rob, Sam" });
|
|
60
|
-
expect(mtg.messages[0].content.text).toMatch(/Q3 Plan/);
|
|
61
|
-
expect(mtg.messages[0].content.text).toMatch(/Rob, Sam/);
|
|
62
|
-
});
|
|
63
|
-
});
|
package/build/types.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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 {};
|