@amenopohis1er/mynotes-mcp 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog — @amenopohis1er/mynotes-mcp
2
+
3
+ The companion versions independently of the My Notes extension; the bridge
4
+ protocol has its own version (`v: 1` in every frame). Compatibility notes
5
+ appear here whenever either side moves.
6
+
7
+ ## 0.1.0 — 2026-07-03
8
+
9
+ First release. Requires My Notes ≥ 0.5.0 (bridge protocol v1).
10
+
11
+ - MCP stdio server with five tools: `search_notes`, `list_notes`, `read_note`
12
+ (paged via `offset`), `create_note`, `append_to_note` — no delete, by design.
13
+ - Native-messaging host role (Chrome-spawned) relaying to a 0600 local socket;
14
+ multiple agents multiplexed over one bridge.
15
+ - `install`: registers the host with every detected Chromium browser
16
+ (Chrome/Beta/Chromium/Brave/Edge/Arc on macOS; XDG paths on Linux).
17
+ - `doctor`: end-to-end chain diagnosis with actionable hints.
18
+ - Cause-aware errors written for LLM consumption (Chrome closed vs Agent
19
+ access off vs bridge stalled), with explicit retry guidance.
20
+ - macOS + Linux. Windows: not yet (registry-based registration).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amen (amenophis1er)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @amenopohis1er/mynotes-mcp
2
+
3
+ MCP companion for the [My Notes](https://mynotes.amenophis.dev) Chrome
4
+ extension — lets local AI agents (Claude Code, or any MCP client) search,
5
+ read, and add to your notes. **Local-only**: no servers, no network; every
6
+ byte stays on your machine, flowing Chrome ↔ this process ↔ your agent.
7
+
8
+ ## Setup
9
+
10
+ ```bash
11
+ npx @amenopohis1er/mynotes-mcp install # register the native-messaging host, then restart Chrome
12
+ ```
13
+
14
+ 1. In My Notes → **Settings → Labs**, turn ON **Agent access (MCP)**.
15
+ 2. Add to your agent's MCP config:
16
+
17
+ ```json
18
+ { "mcpServers": { "mynotes": { "command": "npx", "args": ["@amenopohis1er/mynotes-mcp"] } } }
19
+ ```
20
+
21
+ 3. Anything wrong? `npx @amenopohis1er/mynotes-mcp doctor` diagnoses the whole chain.
22
+
23
+ ## Tools
24
+
25
+ | Tool | What it does |
26
+ |---|---|
27
+ | `search_notes` | Full-text search (optionally within a `#tag`), returns snippets |
28
+ | `list_notes` | Most-recently-updated notes (id, title, pinned) |
29
+ | `read_note` | One note as Markdown (pages via `offset` for large notes) |
30
+ | `create_note` | New note from Markdown |
31
+ | `append_to_note` | Append Markdown to a note (snapshotted first — restorable from History) |
32
+
33
+ Chrome must be running with the extension enabled — notes live in the
34
+ browser's storage and never leave it except through this local bridge.
35
+ Agent edits are snapshotted to the note's History before they land.
36
+
37
+ **Multiple Chrome profiles?** Notes are per-profile, and the bridge serves
38
+ ONE profile at a time: enable "Agent access" in exactly the profile whose
39
+ notes your agent should see. If it's on in several, the first to connect
40
+ wins and the others wait — flip the toggle off in the profiles you don't
41
+ want exposed.
42
+
43
+ Design & protocol: [`companion/DESIGN.md`](./DESIGN.md) in the repo.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Bridge protocol between the extension's service worker and the companion
3
+ * (native-messaging host ↔ MCP server). Imported by BOTH sides — this file is
4
+ * the single source of truth (see companion/DESIGN.md).
5
+ *
6
+ * Native messaging caps a message at ~1 MB each way; results that could grow
7
+ * (search snippets, note bodies) are truncated/paginated by the handler.
8
+ */
9
+ /** The native-messaging host name registered by `mynotes-mcp install`. */
10
+ export const NATIVE_HOST_NAME = 'com.mynotes.companion';
11
+ /** The published Chrome Web Store extension ID (pinned by the manifest `key`,
12
+ * so dev/unpacked builds share it) — the ONLY origin the host trusts. */
13
+ export const EXTENSION_ID = 'fcfkddblflheplmakgjaippneanoibge';
14
+ export const PROTOCOL_VERSION = 1;
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mynotes-mcp — one binary, two roles + utilities (companion/DESIGN.md):
4
+ * (spawned by Chrome w/ a chrome-extension:// arg) → native-messaging host
5
+ * mynotes-mcp → MCP stdio server
6
+ * mynotes-mcp install → register the host
7
+ * mynotes-mcp doctor → diagnose the chain
8
+ */
9
+ import { createRequire } from 'node:module';
10
+ import { isChromeSpawned, runHost } from './host.js';
11
+ import { runServer } from './server.js';
12
+ import { runInstall } from './install.js';
13
+ import { runDoctor } from './doctor.js';
14
+ const version = createRequire(import.meta.url)('../../package.json').version;
15
+ const arg = process.argv[2];
16
+ if (isChromeSpawned(process.argv)) {
17
+ void runHost();
18
+ }
19
+ else if (arg === 'install') {
20
+ runInstall();
21
+ }
22
+ else if (arg === 'doctor') {
23
+ void runDoctor();
24
+ }
25
+ else if (arg === 'host') {
26
+ void runHost(); // manual/debug entry
27
+ }
28
+ else if (arg === undefined || arg === 'serve') {
29
+ void runServer(version);
30
+ }
31
+ else {
32
+ console.error(`Unknown command: ${arg}\nUsage: mynotes-mcp [install|doctor] (no args = MCP stdio server)`);
33
+ process.exit(1);
34
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Socket client used by the server role and `doctor`: newline-delimited JSON
3
+ * to the host, with per-request timeouts and self-diagnosing errors.
4
+ */
5
+ import { execFile } from 'node:child_process';
6
+ import net from 'node:net';
7
+ import readline from 'node:readline';
8
+ import { PROTOCOL_VERSION } from '../protocol.js';
9
+ import { socketPath } from './paths.js';
10
+ /** Best-effort "is a Chromium browser running?" — splits the two failure modes
11
+ * so the error tells the user the RIGHT action. Never throws. */
12
+ function chromeRunning() {
13
+ const pattern = process.platform === 'darwin' ? 'Google Chrome|Chromium|Brave|Microsoft Edge|Arc' : 'chrome|chromium|brave|msedge';
14
+ return new Promise((resolve) => {
15
+ execFile('pgrep', ['-if', pattern], (error) => resolve(!error));
16
+ });
17
+ }
18
+ /** These strings are read by an LLM — each states the cause, the exact user
19
+ * action, and whether retrying helps. */
20
+ async function notReachable() {
21
+ if (await chromeRunning()) {
22
+ return ("My Notes isn't reachable. Chrome appears to be running, so the likely cause is that " +
23
+ '"Agent access (MCP)" is OFF: ask the user to open My Notes → Settings → Labs and turn it on ' +
24
+ '(or run `npx @amenopohis1er/mynotes-mcp doctor` if it already is). Do not retry until the user confirms — ' +
25
+ 'then the same call will work.');
26
+ }
27
+ return ("My Notes isn't reachable because Chrome is not running — the notes live inside the browser. " +
28
+ 'Ask the user to open Chrome (the bridge reconnects automatically within a few seconds), ' +
29
+ 'then retry the same call. Do not retry before that.');
30
+ }
31
+ export class BridgeClient {
32
+ path;
33
+ socket = null;
34
+ pending = new Map();
35
+ seq = 0;
36
+ constructor(path = socketPath()) {
37
+ this.path = path;
38
+ }
39
+ async ensureConnected() {
40
+ if (this.socket && !this.socket.destroyed)
41
+ return this.socket;
42
+ const socket = await new Promise((resolve, reject) => {
43
+ const s = net.connect(this.path, () => resolve(s));
44
+ s.on('error', () => {
45
+ void notReachable().then((message) => reject(new Error(message)));
46
+ });
47
+ });
48
+ const lines = readline.createInterface({ input: socket });
49
+ lines.on('line', (line) => {
50
+ let response;
51
+ try {
52
+ response = JSON.parse(line);
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ const resolve = this.pending.get(response.id);
58
+ if (resolve) {
59
+ this.pending.delete(response.id);
60
+ resolve(response);
61
+ }
62
+ });
63
+ const dropAll = () => {
64
+ this.socket = null;
65
+ for (const [id, resolve] of this.pending) {
66
+ this.pending.delete(id);
67
+ resolve({
68
+ v: PROTOCOL_VERSION,
69
+ id,
70
+ ok: false,
71
+ error: 'The connection to My Notes dropped mid-call (Chrome closed, or Agent access was ' +
72
+ 'turned off). Ask the user to check Chrome and the Labs toggle, then retry.',
73
+ });
74
+ }
75
+ };
76
+ socket.on('close', dropAll);
77
+ socket.on('error', dropAll);
78
+ this.socket = socket;
79
+ return socket;
80
+ }
81
+ async call(method, params, timeoutMs = 15_000) {
82
+ const socket = await this.ensureConnected();
83
+ const id = `r${++this.seq}`;
84
+ const response = await new Promise((resolve) => {
85
+ this.pending.set(id, resolve);
86
+ socket.write(JSON.stringify({ v: PROTOCOL_VERSION, id, method, params }) + '\n');
87
+ setTimeout(() => {
88
+ if (this.pending.delete(id)) {
89
+ resolve({
90
+ v: PROTOCOL_VERSION,
91
+ id,
92
+ ok: false,
93
+ error: `${method} timed out: the bridge socket exists but the extension didn't answer. ` +
94
+ 'Ask the user to toggle "Agent access" off and on in My Notes → Settings → Labs, ' +
95
+ 'or run `npx @amenopohis1er/mynotes-mcp doctor`. Retrying without that will likely time out again.',
96
+ });
97
+ }
98
+ }, timeoutMs).unref();
99
+ });
100
+ if (!response.ok)
101
+ throw new Error(response.error);
102
+ return response.result;
103
+ }
104
+ close() {
105
+ this.socket?.destroy();
106
+ this.socket = null;
107
+ }
108
+ }
@@ -0,0 +1,51 @@
1
+ /** `mynotes-mcp doctor` — diagnose the whole chain with actionable hints. */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { NATIVE_HOST_NAME } from '../protocol.js';
5
+ import { BridgeClient } from './client.js';
6
+ import { nativeMessagingDirs, socketPath } from './paths.js';
7
+ export async function runDoctor() {
8
+ let failed = false;
9
+ const bad = (msg, hint) => {
10
+ console.log(`✗ ${msg}\n ↳ ${hint}`);
11
+ failed = true;
12
+ };
13
+ const good = (msg) => console.log(`✓ ${msg}`);
14
+ const manifests = nativeMessagingDirs()
15
+ .map(({ browser, dir }) => ({ browser, file: path.join(dir, `${NATIVE_HOST_NAME}.json`) }))
16
+ .filter(({ file }) => fs.existsSync(file));
17
+ if (manifests.length) {
18
+ good(`Host manifest registered (${manifests.map((m) => m.browser).join(', ')})`);
19
+ for (const { file } of manifests) {
20
+ try {
21
+ const manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
22
+ if (!manifest.path || !fs.existsSync(manifest.path)) {
23
+ bad(`Launcher missing: ${manifest.path}`, 'Re-run `npx @amenopohis1er/mynotes-mcp install`.');
24
+ }
25
+ }
26
+ catch {
27
+ bad(`Unreadable manifest: ${file}`, 'Re-run `npx @amenopohis1er/mynotes-mcp install`.');
28
+ }
29
+ }
30
+ }
31
+ else {
32
+ bad('No native-messaging manifest found', 'Run `npx @amenopohis1er/mynotes-mcp install`, then restart Chrome.');
33
+ }
34
+ if (fs.existsSync(socketPath()) || process.platform === 'win32') {
35
+ const client = new BridgeClient();
36
+ try {
37
+ const pong = (await client.call('ping', undefined, 5_000));
38
+ good(`Bridge alive — My Notes ${pong.extension} (protocol v${pong.version})`);
39
+ }
40
+ catch (error) {
41
+ bad(`Socket exists but ping failed: ${error instanceof Error ? error.message : error}`, 'Toggle "Agent access" off/on in My Notes → Settings → Labs, or restart Chrome.');
42
+ }
43
+ finally {
44
+ client.close();
45
+ }
46
+ }
47
+ else {
48
+ bad(`Bridge socket not found (${socketPath()})`, 'Is Chrome running? Is "Agent access (MCP)" ON in My Notes → Settings → Labs? (And restart Chrome after install.)');
49
+ }
50
+ process.exit(failed ? 1 : 0);
51
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Chrome native-messaging framing: each message is a 4-byte little-endian
3
+ * uint32 length followed by that many bytes of UTF-8 JSON.
4
+ */
5
+ export function encodeFrame(message) {
6
+ const body = Buffer.from(JSON.stringify(message), 'utf8');
7
+ const frame = Buffer.allocUnsafe(4 + body.length);
8
+ frame.writeUInt32LE(body.length, 0);
9
+ body.copy(frame, 4);
10
+ return frame;
11
+ }
12
+ /**
13
+ * Incremental frame decoder — feed arbitrary chunks, get whole messages.
14
+ * Chrome never sends >1 MB; reject bigger lengths so a corrupt stream can't
15
+ * make us buffer unbounded garbage.
16
+ */
17
+ export class FrameDecoder {
18
+ buffer = Buffer.alloc(0);
19
+ push(chunk) {
20
+ this.buffer = Buffer.concat([this.buffer, chunk]);
21
+ const messages = [];
22
+ for (;;) {
23
+ if (this.buffer.length < 4)
24
+ break;
25
+ const length = this.buffer.readUInt32LE(0);
26
+ if (length > 8 * 1024 * 1024)
27
+ throw new Error(`Frame too large: ${length} bytes`);
28
+ if (this.buffer.length < 4 + length)
29
+ break;
30
+ const body = this.buffer.subarray(4, 4 + length).toString('utf8');
31
+ this.buffer = this.buffer.subarray(4 + length);
32
+ messages.push(JSON.parse(body));
33
+ }
34
+ return messages;
35
+ }
36
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Host role — spawned BY Chrome (native messaging) when the extension's Labs
3
+ * toggle connects. Relays frames between the extension (stdin/stdout) and any
4
+ * number of MCP server processes (unix socket clients), multiplexing by
5
+ * request id: client ids are prefixed `c<N>:` on the way in and stripped on
6
+ * the way out, so concurrent agents can't collide.
7
+ */
8
+ import fs from 'node:fs';
9
+ import net from 'node:net';
10
+ import path from 'node:path';
11
+ import readline from 'node:readline';
12
+ import { FrameDecoder, encodeFrame } from './framing.js';
13
+ import { dataDir, socketPath } from './paths.js';
14
+ export async function runHost(io = { input: process.stdin, output: process.stdout }) {
15
+ const { input, output } = io;
16
+ const exit = io.exit ?? ((code) => process.exit(code));
17
+ const sock = io.sock ?? socketPath();
18
+ fs.mkdirSync(dataDir(), { recursive: true });
19
+ // A stale socket file from a crashed host blocks bind — remove it. If another
20
+ // LIVE host holds it (second Chrome profile), fail: one host per user.
21
+ if (process.platform !== 'win32' && fs.existsSync(sock)) {
22
+ const alive = await new Promise((resolve) => {
23
+ const probe = net.connect(sock, () => {
24
+ probe.destroy();
25
+ resolve(true);
26
+ });
27
+ probe.on('error', () => resolve(false));
28
+ });
29
+ if (alive) {
30
+ process.stderr.write('mynotes-mcp: another host already owns the socket; exiting\n');
31
+ process.exit(1);
32
+ }
33
+ fs.unlinkSync(sock);
34
+ }
35
+ let clientSeq = 0;
36
+ const clients = new Map();
37
+ const server = net.createServer((client) => {
38
+ const clientId = ++clientSeq;
39
+ clients.set(clientId, client);
40
+ const lines = readline.createInterface({ input: client });
41
+ lines.on('line', (line) => {
42
+ let request;
43
+ try {
44
+ request = JSON.parse(line);
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ if (typeof request?.id !== 'string')
50
+ return;
51
+ // Tag the id with the client, forward to the extension.
52
+ output.write(encodeFrame({ ...request, id: `c${clientId}:${request.id}` }));
53
+ });
54
+ client.on('close', () => clients.delete(clientId));
55
+ client.on('error', () => clients.delete(clientId));
56
+ });
57
+ server.listen(sock, () => {
58
+ if (process.platform !== 'win32')
59
+ fs.chmodSync(sock, 0o600);
60
+ });
61
+ // Extension → host frames are responses; route back by the id prefix.
62
+ const decoder = new FrameDecoder();
63
+ input.on('data', (chunk) => {
64
+ let messages;
65
+ try {
66
+ messages = decoder.push(chunk);
67
+ }
68
+ catch {
69
+ exit(1); // corrupt stream — Chrome will respawn us on reconnect
70
+ return;
71
+ }
72
+ for (const message of messages) {
73
+ const response = message;
74
+ const match = /^c(\d+):(.*)$/.exec(response?.id ?? '');
75
+ if (!match)
76
+ continue;
77
+ const client = clients.get(Number(match[1]));
78
+ client?.write(JSON.stringify({ ...response, id: match[2] }) + '\n');
79
+ }
80
+ });
81
+ // Chrome closed the port (toggle off / browser quit): clean up and exit.
82
+ input.on('end', () => {
83
+ server.close();
84
+ for (const client of clients.values())
85
+ client.destroy();
86
+ try {
87
+ if (process.platform !== 'win32')
88
+ fs.unlinkSync(sock);
89
+ }
90
+ catch {
91
+ /* already gone */
92
+ }
93
+ exit(0);
94
+ });
95
+ }
96
+ /** Chrome passes the extension origin as an argument — that's the role signal. */
97
+ export function isChromeSpawned(argv) {
98
+ return argv.some((a) => a.startsWith('chrome-extension://'));
99
+ }
100
+ export const HOST_LOG_HINT = path.join(dataDir(), 'host.log');
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `mynotes-mcp install` — register the native-messaging host so Chrome can
3
+ * spawn us. Writes a launcher script (node + absolute path to this package)
4
+ * and a host manifest into each detected browser's NativeMessagingHosts dir.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { EXTENSION_ID, NATIVE_HOST_NAME } from '../protocol.js';
10
+ import { dataDir, nativeMessagingDirs } from './paths.js';
11
+ export function runInstall() {
12
+ if (process.platform === 'win32') {
13
+ console.error('Windows is not supported yet (native-messaging registration uses the registry).');
14
+ process.exit(1);
15
+ }
16
+ const cliPath = fileURLToPath(new URL('./cli.js', import.meta.url));
17
+ const launcher = path.join(dataDir(), 'mynotes-mcp-host.sh');
18
+ fs.mkdirSync(dataDir(), { recursive: true });
19
+ // Chrome spawns the manifest's `path` directly with no shell/PATH — pin the
20
+ // exact node binary running this install.
21
+ fs.writeFileSync(launcher, `#!/bin/sh\nexec "${process.execPath}" "${cliPath}" "$@"\n`, { mode: 0o755 });
22
+ const manifest = {
23
+ name: NATIVE_HOST_NAME,
24
+ description: 'My Notes MCP companion — local agent access to your notes',
25
+ path: launcher,
26
+ type: 'stdio',
27
+ allowed_origins: [`chrome-extension://${EXTENSION_ID}/`],
28
+ };
29
+ let wrote = 0;
30
+ for (const { browser, dir } of nativeMessagingDirs()) {
31
+ // Only register where the browser is actually present (its parent profile
32
+ // dir exists) — don't create skeleton dirs for browsers the user never ran.
33
+ if (!fs.existsSync(path.dirname(dir)))
34
+ continue;
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ const file = path.join(dir, `${NATIVE_HOST_NAME}.json`);
37
+ fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + '\n');
38
+ console.log(`✓ ${browser}: ${file}`);
39
+ wrote++;
40
+ }
41
+ if (!wrote) {
42
+ console.error('✗ No Chromium-based browser profile found — is Chrome installed?');
43
+ process.exit(1);
44
+ }
45
+ console.log(`\nDone. Next:
46
+ 1. Restart Chrome (native-messaging manifests are read at browser start).
47
+ 2. In My Notes → Settings → Labs, turn ON "Agent access (MCP)".
48
+ 3. Add to your agent's MCP config: {"command": "npx", "args": ["@amenopohis1er/mynotes-mcp"]}
49
+ 4. Check the chain anytime: npx @amenopohis1er/mynotes-mcp doctor`);
50
+ }
@@ -0,0 +1,44 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ /** Per-user data dir for the launcher script + socket (created by install/host). */
4
+ export function dataDir() {
5
+ if (process.platform === 'darwin') {
6
+ return path.join(os.homedir(), 'Library', 'Application Support', 'MyNotes');
7
+ }
8
+ if (process.platform === 'win32') {
9
+ return path.join(process.env.APPDATA ?? os.homedir(), 'MyNotes');
10
+ }
11
+ return path.join(process.env.XDG_DATA_HOME ?? path.join(os.homedir(), '.local', 'share'), 'mynotes');
12
+ }
13
+ /** The host↔server rendezvous: a 0600 unix socket (named pipe on Windows). */
14
+ export function socketPath() {
15
+ if (process.platform === 'win32')
16
+ return '\\\\.\\pipe\\mynotes-mcp';
17
+ if (process.platform === 'linux' && process.env.XDG_RUNTIME_DIR) {
18
+ return path.join(process.env.XDG_RUNTIME_DIR, 'mynotes-mcp.sock');
19
+ }
20
+ return path.join(dataDir(), 'mcp.sock');
21
+ }
22
+ /** Native-messaging manifest directories per browser (user-level). */
23
+ export function nativeMessagingDirs() {
24
+ const home = os.homedir();
25
+ if (process.platform === 'darwin') {
26
+ const base = path.join(home, 'Library', 'Application Support');
27
+ return [
28
+ { browser: 'Chrome', dir: path.join(base, 'Google', 'Chrome', 'NativeMessagingHosts') },
29
+ { browser: 'Chrome Beta', dir: path.join(base, 'Google', 'Chrome Beta', 'NativeMessagingHosts') },
30
+ { browser: 'Chromium', dir: path.join(base, 'Chromium', 'NativeMessagingHosts') },
31
+ { browser: 'Brave', dir: path.join(base, 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts') },
32
+ { browser: 'Edge', dir: path.join(base, 'Microsoft Edge', 'NativeMessagingHosts') },
33
+ { browser: 'Arc', dir: path.join(base, 'Arc', 'User Data', 'NativeMessagingHosts') },
34
+ ];
35
+ }
36
+ // Linux. (Windows uses the registry — not supported in v1; DESIGN.md non-goal.)
37
+ const cfg = process.env.XDG_CONFIG_HOME ?? path.join(home, '.config');
38
+ return [
39
+ { browser: 'Chrome', dir: path.join(cfg, 'google-chrome', 'NativeMessagingHosts') },
40
+ { browser: 'Chromium', dir: path.join(cfg, 'chromium', 'NativeMessagingHosts') },
41
+ { browser: 'Brave', dir: path.join(cfg, 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts') },
42
+ { browser: 'Edge', dir: path.join(cfg, 'microsoft-edge', 'NativeMessagingHosts') },
43
+ ];
44
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Server role — spawned by the agent (`npx @amenopohis1er/mynotes-mcp` in an MCP config).
3
+ * Exposes the note tools over MCP stdio and forwards each call to the
4
+ * Chrome-spawned host via the local socket.
5
+ */
6
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { z } from 'zod';
9
+ import { BridgeClient } from './client.js';
10
+ export async function runServer(version) {
11
+ const bridge = new BridgeClient();
12
+ const server = new McpServer({ name: 'mynotes', version });
13
+ const asText = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] });
14
+ const asError = (error) => ({
15
+ isError: true,
16
+ content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }],
17
+ });
18
+ server.registerTool('search_notes', {
19
+ description: 'Full-text search across the user\'s My Notes (local Chrome extension). Returns note ids, titles and matching snippets.',
20
+ inputSchema: {
21
+ query: z.string().describe('Text to search for'),
22
+ tag: z.string().optional().describe('Restrict to notes carrying this #tag (with or without the #)'),
23
+ limit: z.number().int().min(0).max(50).optional().describe('Max results (default 20)'),
24
+ },
25
+ }, async (args) => {
26
+ try {
27
+ return asText(await bridge.call('search_notes', args));
28
+ }
29
+ catch (error) {
30
+ return asError(error);
31
+ }
32
+ });
33
+ server.registerTool('list_notes', {
34
+ description: 'List the user\'s notes, most recently updated first (id, title, updatedAt, pinned).',
35
+ inputSchema: {
36
+ limit: z.number().int().min(0).max(200).optional().describe('Max results (default 50)'),
37
+ },
38
+ }, async (args) => {
39
+ try {
40
+ return asText(await bridge.call('list_notes', args));
41
+ }
42
+ catch (error) {
43
+ return asError(error);
44
+ }
45
+ });
46
+ server.registerTool('read_note', {
47
+ description: 'Read one note as Markdown. Large notes page: when `truncated` is true, call again with offset += markdown.length.',
48
+ inputSchema: {
49
+ id: z.string().describe('Note id (from search_notes / list_notes)'),
50
+ offset: z.number().int().min(0).optional().describe('Character offset to continue from (default 0)'),
51
+ },
52
+ }, async (args) => {
53
+ try {
54
+ return asText(await bridge.call('read_note', args));
55
+ }
56
+ catch (error) {
57
+ return asError(error);
58
+ }
59
+ });
60
+ server.registerTool('create_note', {
61
+ description: 'Create a new note from Markdown (headings #–###, - bullets, - [ ] checklists, numbered lists; inline markup stays literal).',
62
+ inputSchema: {
63
+ title: z.string().optional().describe('Note title (optional)'),
64
+ markdown: z.string().describe('The note body as Markdown'),
65
+ },
66
+ }, async (args) => {
67
+ try {
68
+ return asText(await bridge.call('create_note', args));
69
+ }
70
+ catch (error) {
71
+ return asError(error);
72
+ }
73
+ });
74
+ server.registerTool('append_to_note', {
75
+ description: 'Append Markdown to an existing note (separated by a divider). The note is snapshotted first, so the user can restore from History. On "note changed" errors, re-read and retry.',
76
+ inputSchema: {
77
+ id: z.string().describe('Note id to append to'),
78
+ markdown: z.string().describe('Markdown to append'),
79
+ },
80
+ }, async (args) => {
81
+ try {
82
+ return asText(await bridge.call('append_to_note', args));
83
+ }
84
+ catch (error) {
85
+ return asError(error);
86
+ }
87
+ });
88
+ await server.connect(new StdioServerTransport());
89
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@amenopohis1er/mynotes-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP companion for the My Notes Chrome extension \u2014 lets local AI agents search, read, and add to your notes. Local-only: no servers, no network.",
5
+ "keywords": [
6
+ "mcp",
7
+ "mcp-server",
8
+ "model-context-protocol",
9
+ "notes",
10
+ "chrome-extension",
11
+ "claude",
12
+ "ai-agent",
13
+ "local-first",
14
+ "native-messaging"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Amen (https://github.com/amenophis1er)",
18
+ "homepage": "https://mynotes.amenophis.dev/ai-agent-notes",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/amenophis1er/chrome-extension-my-notes.git",
22
+ "directory": "companion"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/amenophis1er/chrome-extension-my-notes/issues"
26
+ },
27
+ "type": "module",
28
+ "bin": {
29
+ "mynotes-mcp": "dist/src/cli.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE",
35
+ "CHANGELOG.md"
36
+ ],
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest run",
44
+ "prepublishOnly": "npm run build"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.12.0",
48
+ "zod": "^3.24.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.0.0",
52
+ "typescript": "^5.6.0",
53
+ "vitest": "^4.0.0"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }