@scales-baby/nest-bridge 1.0.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/dist/mcp.js ADDED
@@ -0,0 +1,324 @@
1
+ "use strict";
2
+ // Bridge — the MCP server (stdio transport).
3
+ //
4
+ // Exposes list/get/search/create/update tools for people, companies, tasks,
5
+ // events (+ get_digest). Reads decrypt locally; writes encrypt locally. The
6
+ // Nest server only ever sees ciphertext + the API key.
7
+ //
8
+ // Scopes: the bridge respects the key's scopes by simply forwarding requests —
9
+ // the Nest REST API enforces scope per-route (a read-only key gets 403 on a
10
+ // write, surfaced back to Claude as an error). We also pre-gate write tools
11
+ // when the key is known to be read-only, for a cleaner message.
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.buildServer = buildServer;
47
+ exports.runStdio = runStdio;
48
+ const zod_1 = require("zod");
49
+ const nestClient_1 = require("./nestClient");
50
+ async function loadSdk() {
51
+ const mcp = await Promise.resolve().then(() => __importStar(require("@modelcontextprotocol/sdk/server/mcp.js")));
52
+ const stdio = await Promise.resolve().then(() => __importStar(require("@modelcontextprotocol/sdk/server/stdio.js")));
53
+ return {
54
+ McpServer: mcp.McpServer,
55
+ StdioServerTransport: stdio.StdioServerTransport,
56
+ };
57
+ }
58
+ function jsonResult(value) {
59
+ return {
60
+ content: [
61
+ { type: "text", text: JSON.stringify(value, null, 2) },
62
+ ],
63
+ };
64
+ }
65
+ function errResult(e) {
66
+ const msg = e instanceof nestClient_1.NestApiError
67
+ ? `Nest API error (${e.status}): ${e.message}`
68
+ : e instanceof Error
69
+ ? e.message
70
+ : String(e);
71
+ return {
72
+ isError: true,
73
+ content: [{ type: "text", text: msg }],
74
+ };
75
+ }
76
+ // Simple client-side fuzzy filter for search tools (the REST API has no text
77
+ // search on encrypted content — we decrypt locally then match).
78
+ function matches(row, q, keys) {
79
+ const needle = q.toLowerCase();
80
+ return keys.some((k) => {
81
+ const v = row[k];
82
+ return typeof v === "string" && v.toLowerCase().includes(needle);
83
+ });
84
+ }
85
+ async function buildServer(ctx) {
86
+ const { McpServer } = await loadSdk();
87
+ const server = new McpServer({
88
+ name: "nest-bridge",
89
+ version: "1.0.0",
90
+ });
91
+ const encNote = ctx.encrypted
92
+ ? ctx.unlocked
93
+ ? " This account is end-to-end encrypted; the bridge decrypts/encrypts locally so you see and write real content."
94
+ : " WARNING: this encrypted account is LOCKED (no key) — content fields will be blank."
95
+ : "";
96
+ // ---- generic registrars -------------------------------------------------
97
+ const listFilter = {
98
+ status: zod_1.z.string().optional().describe("Filter by status"),
99
+ dueWithin: zod_1.z
100
+ .enum(["today", "week", "overdue"])
101
+ .optional()
102
+ .describe("Filter by next-action / due date window"),
103
+ limit: zod_1.z.number().int().min(1).max(1000).optional(),
104
+ };
105
+ function registerList(name, model, desc, extra = {}) {
106
+ server.registerTool(name, {
107
+ description: desc + encNote,
108
+ inputSchema: { ...listFilter, ...extra },
109
+ }, async (args) => {
110
+ try {
111
+ const rows = await ctx.data.list(model, args);
112
+ return jsonResult({ count: rows.length, items: rows });
113
+ }
114
+ catch (e) {
115
+ return errResult(e);
116
+ }
117
+ });
118
+ }
119
+ function registerGet(name, model, desc) {
120
+ server.registerTool(name, {
121
+ description: desc + encNote,
122
+ inputSchema: { id: zod_1.z.string().describe("The record's _id") },
123
+ }, async (args) => {
124
+ try {
125
+ const row = await ctx.data.get(model, args.id);
126
+ if (!row)
127
+ return errResult(new Error("not_found"));
128
+ return jsonResult(row);
129
+ }
130
+ catch (e) {
131
+ return errResult(e);
132
+ }
133
+ });
134
+ }
135
+ function registerSearch(name, model, keys, desc) {
136
+ server.registerTool(name, {
137
+ description: desc + encNote,
138
+ inputSchema: {
139
+ query: zod_1.z.string().describe("Text to fuzzy-match against content"),
140
+ limit: zod_1.z.number().int().min(1).max(1000).optional(),
141
+ },
142
+ }, async (args) => {
143
+ try {
144
+ const rows = await ctx.data.list(model, { limit: 1000 });
145
+ const hits = rows
146
+ .filter((r) => matches(r, args.query, keys))
147
+ .slice(0, args.limit ?? 50);
148
+ return jsonResult({ count: hits.length, items: hits });
149
+ }
150
+ catch (e) {
151
+ return errResult(e);
152
+ }
153
+ });
154
+ }
155
+ function registerCreate(name, model, shape, desc) {
156
+ server.registerTool(name, { description: desc + encNote, inputSchema: shape }, async (args) => {
157
+ if (!ctx.canWrite) {
158
+ return errResult(new Error("This API key is read-only (no write scope)."));
159
+ }
160
+ try {
161
+ const clean = Object.fromEntries(Object.entries(args).filter(([, v]) => v !== undefined));
162
+ const created = await ctx.data.create(model, clean);
163
+ return jsonResult(created);
164
+ }
165
+ catch (e) {
166
+ return errResult(e);
167
+ }
168
+ });
169
+ }
170
+ function registerUpdate(name, model, shape, desc) {
171
+ server.registerTool(name, {
172
+ description: desc + encNote,
173
+ inputSchema: { id: zod_1.z.string().describe("The record's _id"), ...shape },
174
+ }, async (args) => {
175
+ if (!ctx.canWrite) {
176
+ return errResult(new Error("This API key is read-only (no write scope)."));
177
+ }
178
+ try {
179
+ const { id, ...rest } = args;
180
+ const changes = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined));
181
+ const updated = await ctx.data.update(model, id, changes);
182
+ return jsonResult(updated);
183
+ }
184
+ catch (e) {
185
+ return errResult(e);
186
+ }
187
+ });
188
+ }
189
+ // ---- PEOPLE -------------------------------------------------------------
190
+ registerList("list_people", "person", "List people in your CRM. Filter by status or dueWithin.");
191
+ registerGet("get_person", "person", "Get one person by id, with notes.");
192
+ registerSearch("search_people", "person", ["name", "companyName", "role", "notes", "channelMet"], "Search people by name/company/role/notes.");
193
+ registerCreate("create_person", "person", {
194
+ name: zod_1.z.string().describe("Full name"),
195
+ companyName: zod_1.z.string().optional(),
196
+ role: zod_1.z.string().optional(),
197
+ channelMet: zod_1.z.string().optional(),
198
+ status: zod_1.z.string().optional().describe("cold|contacted|replied|met|in_conversation|close|customer|dead|parked"),
199
+ nextAction: zod_1.z.string().optional(),
200
+ nextActionDate: zod_1.z.string().optional().describe("ISO date"),
201
+ linkedin: zod_1.z.string().optional(),
202
+ twitter: zod_1.z.string().optional(),
203
+ telegram: zod_1.z.string().optional(),
204
+ notes: zod_1.z.string().optional(),
205
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
206
+ }, "Create a person.");
207
+ registerUpdate("update_person", "person", {
208
+ name: zod_1.z.string().optional(),
209
+ companyName: zod_1.z.string().optional(),
210
+ role: zod_1.z.string().optional(),
211
+ channelMet: zod_1.z.string().optional(),
212
+ status: zod_1.z.string().optional(),
213
+ nextAction: zod_1.z.string().optional(),
214
+ nextActionDate: zod_1.z.string().optional(),
215
+ linkedin: zod_1.z.string().optional(),
216
+ twitter: zod_1.z.string().optional(),
217
+ telegram: zod_1.z.string().optional(),
218
+ notes: zod_1.z.string().optional(),
219
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
220
+ }, "Update a person (only the fields you pass change).");
221
+ // ---- COMPANIES ----------------------------------------------------------
222
+ registerList("list_companies", "company", "List companies.");
223
+ registerGet("get_company", "company", "Get one company by id, with notes.");
224
+ registerSearch("search_companies", "company", ["name", "notes"], "Search companies by name/notes.");
225
+ registerCreate("create_company", "company", {
226
+ name: zod_1.z.string().describe("Company name"),
227
+ city: zod_1.z.string().optional(),
228
+ category: zod_1.z.string().optional(),
229
+ website: zod_1.z.string().optional(),
230
+ notes: zod_1.z.string().optional(),
231
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
232
+ }, "Create a company.");
233
+ registerUpdate("update_company", "company", {
234
+ name: zod_1.z.string().optional(),
235
+ city: zod_1.z.string().optional(),
236
+ category: zod_1.z.string().optional(),
237
+ website: zod_1.z.string().optional(),
238
+ notes: zod_1.z.string().optional(),
239
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
240
+ }, "Update a company.");
241
+ // ---- TASKS --------------------------------------------------------------
242
+ registerList("list_tasks", "task", "List tasks. Filter by status or dueWithin.");
243
+ registerGet("get_task", "task", "Get one task by id, with notes.");
244
+ registerSearch("search_tasks", "task", ["title", "notes"], "Search tasks by title/notes.");
245
+ registerCreate("create_task", "task", {
246
+ title: zod_1.z.string().describe("Task title"),
247
+ dueDate: zod_1.z.string().optional().describe("ISO date"),
248
+ status: zod_1.z.string().optional().describe("open|done"),
249
+ priority: zod_1.z.string().optional().describe("high|medium|low"),
250
+ relatedPerson: zod_1.z.string().optional().describe("Person _id"),
251
+ relatedEvent: zod_1.z.string().optional().describe("Event _id"),
252
+ notes: zod_1.z.string().optional(),
253
+ }, "Create a task.");
254
+ registerUpdate("update_task", "task", {
255
+ title: zod_1.z.string().optional(),
256
+ dueDate: zod_1.z.string().optional(),
257
+ status: zod_1.z.string().optional(),
258
+ priority: zod_1.z.string().optional(),
259
+ relatedPerson: zod_1.z.string().optional(),
260
+ relatedEvent: zod_1.z.string().optional(),
261
+ notes: zod_1.z.string().optional(),
262
+ }, "Update a task.");
263
+ server.registerTool("complete_task", {
264
+ description: "Mark a task done (metadata-only).",
265
+ inputSchema: { id: zod_1.z.string().describe("The task _id") },
266
+ }, async (args) => {
267
+ if (!ctx.canWrite) {
268
+ return errResult(new Error("This API key is read-only (no write scope)."));
269
+ }
270
+ try {
271
+ return jsonResult(await ctx.data.completeTask(args.id));
272
+ }
273
+ catch (e) {
274
+ return errResult(e);
275
+ }
276
+ });
277
+ // ---- EVENTS -------------------------------------------------------------
278
+ registerList("list_events", "event", "List events. Filter by status.");
279
+ registerGet("get_event", "event", "Get one event by id, with notes.");
280
+ registerSearch("search_events", "event", ["title", "notes", "researchNotes"], "Search events by title/notes.");
281
+ registerCreate("create_event", "event", {
282
+ title: zod_1.z.string().describe("Event title (kept as cleartext metadata)"),
283
+ date: zod_1.z.string().describe("ISO date"),
284
+ city: zod_1.z.string().optional(),
285
+ venue: zod_1.z.string().optional(),
286
+ type: zod_1.z.string().optional(),
287
+ status: zod_1.z.string().optional(),
288
+ url: zod_1.z.string().optional(),
289
+ notes: zod_1.z.string().optional(),
290
+ researchNotes: zod_1.z.string().optional(),
291
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
292
+ }, "Create an event.");
293
+ registerUpdate("update_event", "event", {
294
+ title: zod_1.z.string().optional(),
295
+ date: zod_1.z.string().optional(),
296
+ city: zod_1.z.string().optional(),
297
+ venue: zod_1.z.string().optional(),
298
+ type: zod_1.z.string().optional(),
299
+ status: zod_1.z.string().optional(),
300
+ url: zod_1.z.string().optional(),
301
+ notes: zod_1.z.string().optional(),
302
+ researchNotes: zod_1.z.string().optional(),
303
+ tags: zod_1.z.array(zod_1.z.string()).optional(),
304
+ }, "Update an event.");
305
+ // ---- DIGEST -------------------------------------------------------------
306
+ server.registerTool("get_digest", {
307
+ description: "Get the actionable digest (overdue/today/this-week follow-ups + open tasks)." +
308
+ encNote,
309
+ inputSchema: {},
310
+ }, async () => {
311
+ try {
312
+ return jsonResult(await ctx.data.digest());
313
+ }
314
+ catch (e) {
315
+ return errResult(e);
316
+ }
317
+ });
318
+ return server;
319
+ }
320
+ async function runStdio(server) {
321
+ const { StdioServerTransport } = await loadSdk();
322
+ const transport = new StdioServerTransport();
323
+ await server.connect(transport);
324
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ // Bridge — typed HTTP client for the Nest REST API.
3
+ //
4
+ // Talks to nest.scales.baby (or a configured URL) using the user's SCOPED API
5
+ // KEY as a Bearer token. The server returns CIPHERTEXT for encrypted accounts
6
+ // (the `enc` blob + blanked plaintext columns) — decryption happens locally in
7
+ // the bridge with the DEK. On writes the bridge sends ciphertext we built
8
+ // locally. The server never sees the DEK, the password, or plaintext.
9
+ //
10
+ // trailingSlash:true on Nest → every API path MUST end in "/" (a no-slash path
11
+ // 308-redirects and can drop a POST body).
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.NestClient = exports.NestApiError = void 0;
14
+ class NestApiError extends Error {
15
+ status;
16
+ constructor(message, status) {
17
+ super(message);
18
+ this.name = "NestApiError";
19
+ this.status = status;
20
+ }
21
+ }
22
+ exports.NestApiError = NestApiError;
23
+ class NestClient {
24
+ base;
25
+ key;
26
+ constructor(cfg) {
27
+ // Normalise: strip a trailing slash from the base; we add per-path slashes.
28
+ this.base = cfg.apiUrl.replace(/\/+$/, "");
29
+ this.key = cfg.apiKey;
30
+ }
31
+ headers() {
32
+ return {
33
+ Authorization: `Bearer ${this.key}`,
34
+ "Content-Type": "application/json",
35
+ Accept: "application/json",
36
+ };
37
+ }
38
+ // Ensure exactly one trailing slash (before any query string).
39
+ url(path) {
40
+ const [p, q] = path.split("?");
41
+ const withSlash = p.endsWith("/") ? p : `${p}/`;
42
+ return `${this.base}${withSlash}${q ? `?${q}` : ""}`;
43
+ }
44
+ async request(method, path, body) {
45
+ const res = await fetch(this.url(path), {
46
+ method,
47
+ headers: this.headers(),
48
+ body: body === undefined ? undefined : JSON.stringify(body),
49
+ redirect: "follow",
50
+ });
51
+ let json = null;
52
+ const text = await res.text();
53
+ try {
54
+ json = text ? JSON.parse(text) : null;
55
+ }
56
+ catch {
57
+ json = null;
58
+ }
59
+ if (!res.ok) {
60
+ const msg = json?.error || text || res.statusText;
61
+ throw new NestApiError(msg || `request_failed_${res.status}`, res.status);
62
+ }
63
+ if (json && json.error) {
64
+ throw new NestApiError(json.error, res.status);
65
+ }
66
+ // Some endpoints return the data directly under `data`.
67
+ return (json ? json.data : null);
68
+ }
69
+ get(path) {
70
+ return this.request("GET", path);
71
+ }
72
+ post(path, body) {
73
+ return this.request("POST", path, body);
74
+ }
75
+ patch(path, body) {
76
+ return this.request("PATCH", path, body);
77
+ }
78
+ // --- bridge unlock: fetch the password-wrapped DEK blob ------------------
79
+ async getBridgeWrap() {
80
+ return this.get("/api/keys/bridge-wrap");
81
+ }
82
+ }
83
+ exports.NestClient = NestClient;
package/dist/prompt.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ // Bridge — secure hidden password prompt (stdin, no echo).
3
+ //
4
+ // Prompts on the CONTROLLING TTY so it works even when stdio is wired to an MCP
5
+ // client over the pipe. We write the prompt to stderr and read the password
6
+ // from the TTY with echo OFF, then restore terminal state. The password is held
7
+ // only in memory by the caller and never persisted.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.promptHidden = promptHidden;
10
+ const node_readline_1 = require("node:readline");
11
+ const node_fs_1 = require("node:fs");
12
+ // Read a line from the TTY with input hidden. Falls back to stdin if no TTY.
13
+ async function promptHidden(question) {
14
+ // Prefer the real terminal so we don't consume the MCP stdio pipe.
15
+ let input;
16
+ let isTty = false;
17
+ try {
18
+ const fd = (0, node_fs_1.openSync)("/dev/tty", "r");
19
+ input = (0, node_fs_1.createReadStream)("", { fd });
20
+ isTty = true;
21
+ }
22
+ catch {
23
+ input = process.stdin;
24
+ isTty = process.stdin.isTTY === true;
25
+ }
26
+ const output = process.stderr;
27
+ return new Promise((resolve, reject) => {
28
+ const rl = (0, node_readline_1.createInterface)({ input, output, terminal: true });
29
+ // Mute echo: override the readline output writer so typed chars don't show.
30
+ let muted = false;
31
+ const realWrite = output.write.bind(output);
32
+ // @ts-expect-error patching the stream write for masking
33
+ rl._writeToOutput = (s) => {
34
+ if (muted) {
35
+ // Allow the prompt line + newline through; mask everything else.
36
+ if (s.includes(question))
37
+ realWrite(s);
38
+ else if (s === "\n" || s === "\r\n")
39
+ realWrite(s);
40
+ return;
41
+ }
42
+ realWrite(s);
43
+ };
44
+ rl.question(question, (answer) => {
45
+ muted = false;
46
+ rl.close();
47
+ // Newline after the (hidden) input so the next output starts cleanly.
48
+ output.write("\n");
49
+ resolve(answer);
50
+ });
51
+ muted = true;
52
+ rl.on("SIGINT", () => {
53
+ rl.close();
54
+ reject(new Error("cancelled"));
55
+ });
56
+ void isTty;
57
+ });
58
+ }
Binary file
package/manifest.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "manifest_version": "0.3",
3
+ "name": "scales-nest",
4
+ "display_name": "Nest by SCALES",
5
+ "version": "1.0.0",
6
+ "description": "Read and write your end-to-end-encrypted Nest through your own AI. Your encryption key is derived locally and never leaves your machine; Nest's servers only ever see ciphertext.",
7
+ "long_description": "Nest is your encrypted second brain (people, companies, tasks, events). This connector runs a small MCP server on your own machine. It fetches your password-wrapped key from Nest, unwraps it locally, then decrypts on read and encrypts on write right here. The Nest server only ever stores ciphertext and never receives your password, your key, or your plaintext. Mint a scoped API key in Nest (Settings then Connect your AI), paste it plus your encryption password below, and your AI can read and (with a full-control key) write your Nest data.",
8
+ "author": {
9
+ "name": "SCALES",
10
+ "url": "https://nest.scales.baby"
11
+ },
12
+ "homepage": "https://nest.scales.baby",
13
+ "icon": "icon.png",
14
+ "server": {
15
+ "type": "node",
16
+ "entry_point": "dist/index.js",
17
+ "mcp_config": {
18
+ "command": "node",
19
+ "args": ["${__dirname}/dist/index.js"],
20
+ "env": {
21
+ "NEST_API_KEY": "${user_config.nest_api_key}",
22
+ "NEST_PASSWORD": "${user_config.nest_password}",
23
+ "NEST_API_URL": "${user_config.nest_api_url}",
24
+ "NEST_NON_INTERACTIVE": "1"
25
+ }
26
+ }
27
+ },
28
+ "tools": [
29
+ { "name": "list_people", "description": "List people in your Nest, decrypted locally." },
30
+ { "name": "get_person", "description": "Get one person by id, decrypted locally." },
31
+ { "name": "search_people", "description": "Search people by decrypted name or notes." },
32
+ { "name": "create_person", "description": "Create a person; encrypted locally before it is stored." },
33
+ { "name": "update_person", "description": "Update a person; re-encrypted locally before it is stored." },
34
+ { "name": "list_companies", "description": "List companies in your Nest." },
35
+ { "name": "get_company", "description": "Get one company by id." },
36
+ { "name": "search_companies", "description": "Search companies." },
37
+ { "name": "create_company", "description": "Create a company." },
38
+ { "name": "update_company", "description": "Update a company." },
39
+ { "name": "list_tasks", "description": "List tasks in your Nest." },
40
+ { "name": "get_task", "description": "Get one task by id." },
41
+ { "name": "search_tasks", "description": "Search tasks." },
42
+ { "name": "create_task", "description": "Create a task." },
43
+ { "name": "update_task", "description": "Update a task." },
44
+ { "name": "complete_task", "description": "Mark a task complete." },
45
+ { "name": "list_events", "description": "List events in your Nest." },
46
+ { "name": "get_event", "description": "Get one event by id." },
47
+ { "name": "search_events", "description": "Search events." },
48
+ { "name": "create_event", "description": "Create an event." },
49
+ { "name": "update_event", "description": "Update an event." },
50
+ { "name": "get_digest", "description": "Get your follow-up digest (overdue and upcoming)." }
51
+ ],
52
+ "keywords": ["crm", "encrypted", "mcp", "nest", "scales", "second-brain"],
53
+ "license": "MIT",
54
+ "compatibility": {
55
+ "claude_desktop": ">=0.10.0",
56
+ "platforms": ["darwin", "win32", "linux"],
57
+ "runtimes": {
58
+ "node": ">=20.0.0"
59
+ }
60
+ },
61
+ "user_config": {
62
+ "nest_api_key": {
63
+ "type": "string",
64
+ "title": "Nest API key",
65
+ "description": "Your scoped key from Nest (Settings then Connect your AI). Read-only is enough to read; choose Full control to let your AI create and update records. Looks like nest_xxxx_...",
66
+ "required": true,
67
+ "sensitive": true
68
+ },
69
+ "nest_password": {
70
+ "type": "string",
71
+ "title": "Nest encryption password",
72
+ "description": "The password you set when you turned on encryption in Nest. Used locally to unlock your key. It is stored in your operating system keychain and is never sent to Nest. Leave blank only if your account is not encrypted.",
73
+ "required": true,
74
+ "sensitive": true
75
+ },
76
+ "nest_api_url": {
77
+ "type": "string",
78
+ "title": "Nest URL (advanced)",
79
+ "description": "Leave as the default unless you are testing against another Nest instance.",
80
+ "required": false,
81
+ "default": "https://nest.scales.baby"
82
+ }
83
+ }
84
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@scales-baby/nest-bridge",
3
+ "version": "1.0.0",
4
+ "description": "Local MCP bridge for Nest. Read and write your end-to-end-encrypted Nest data through your own AI; the encryption key is derived locally from your password and never leaves your machine.",
5
+ "license": "MIT",
6
+ "author": "SCALES (https://nest.scales.baby)",
7
+ "homepage": "https://nest.scales.baby",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/scales-baby/nest-bridge.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/scales-baby/nest-bridge/issues"
14
+ },
15
+ "keywords": [
16
+ "crm",
17
+ "encrypted",
18
+ "e2e-encryption",
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "nest",
22
+ "scales",
23
+ "second-brain",
24
+ "claude",
25
+ "ai"
26
+ ],
27
+ "type": "commonjs",
28
+ "bin": {
29
+ "nest-bridge": "./dist/index.js"
30
+ },
31
+ "main": "./dist/index.js",
32
+ "files": [
33
+ "dist/",
34
+ "manifest.json",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=20.0.0"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "start": "node ./dist/index.js",
44
+ "test": "node test/crypto-roundtrip.mjs",
45
+ "pack:mcpb": "bash scripts/pack-mcpb.sh"
46
+ },
47
+ "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.26.0",
49
+ "hash-wasm": "^4.12.0",
50
+ "zod": "^4.4.3"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.0.0",
54
+ "typescript": "^5.5.3"
55
+ }
56
+ }