@nestpilot/mcp-app 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.
Files changed (149) hide show
  1. package/README.md +350 -0
  2. package/dist/cli/doctor.d.ts +1 -0
  3. package/dist/cli/doctor.js +214 -0
  4. package/dist/cli/export-import.d.ts +6 -0
  5. package/dist/cli/export-import.js +132 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +168 -0
  8. package/dist/cli/init.d.ts +1 -0
  9. package/dist/cli/init.js +171 -0
  10. package/dist/host-configs/cowork.json +11 -0
  11. package/dist/host-configs/goose.yaml +22 -0
  12. package/dist/host-configs/openclaw-manifest.json +16 -0
  13. package/dist/main.d.ts +2 -0
  14. package/dist/main.js +128 -0
  15. package/dist/mcp-app.html +155 -0
  16. package/dist/nestpilot-client.d.ts +44 -0
  17. package/dist/nestpilot-client.js +160 -0
  18. package/dist/planner.html +222 -0
  19. package/dist/server.d.ts +19 -0
  20. package/dist/server.js +245 -0
  21. package/dist/skills/SKILL.md +162 -0
  22. package/dist/skills/manifest.json +51 -0
  23. package/dist/skills/tools/activate_plan.md +36 -0
  24. package/dist/skills/tools/coach.md +59 -0
  25. package/dist/skills/tools/comprehensive_plan.md +65 -0
  26. package/dist/skills/tools/create_plan.md +59 -0
  27. package/dist/skills/tools/create_saved_plan.md +49 -0
  28. package/dist/skills/tools/delete_plan.md +42 -0
  29. package/dist/skills/tools/delete_scenario.md +38 -0
  30. package/dist/skills/tools/generate_proposal.md +63 -0
  31. package/dist/skills/tools/generate_retirement_report.md +50 -0
  32. package/dist/skills/tools/get_active_plan.md +44 -0
  33. package/dist/skills/tools/get_baseline_forecast.md +47 -0
  34. package/dist/skills/tools/get_plan.md +44 -0
  35. package/dist/skills/tools/get_plan_components.md +50 -0
  36. package/dist/skills/tools/get_scenario.md +46 -0
  37. package/dist/skills/tools/list_plans.md +44 -0
  38. package/dist/skills/tools/list_scenarios.md +42 -0
  39. package/dist/skills/tools/medicare-guardian.md +59 -0
  40. package/dist/skills/tools/nestpilot_run_plan.md +61 -0
  41. package/dist/skills/tools/optimize_roth_conversion.md +107 -0
  42. package/dist/skills/tools/optimize_ss_claiming.md +30 -0
  43. package/dist/skills/tools/rename_plan.md +34 -0
  44. package/dist/skills/tools/retirement-planner.md +55 -0
  45. package/dist/skills/tools/run_forecast.md +65 -0
  46. package/dist/skills/tools/run_saved_forecast.md +52 -0
  47. package/dist/skills/tools/run_scenario.md +66 -0
  48. package/dist/skills/tools/save_plan.md +48 -0
  49. package/dist/skills/tools/save_scenario.md +50 -0
  50. package/dist/skills/tools/verify_forecast.md +43 -0
  51. package/dist/src/config.d.ts +20 -0
  52. package/dist/src/config.js +44 -0
  53. package/dist/src/contracts/provenance.d.ts +37 -0
  54. package/dist/src/contracts/provenance.js +71 -0
  55. package/dist/src/contracts/tool-contract-registry.d.ts +43 -0
  56. package/dist/src/contracts/tool-contract-registry.js +282 -0
  57. package/dist/src/local/cloud-compute-client.d.ts +55 -0
  58. package/dist/src/local/cloud-compute-client.js +135 -0
  59. package/dist/src/local/encryption.d.ts +24 -0
  60. package/dist/src/local/encryption.js +105 -0
  61. package/dist/src/local/keychain.d.ts +41 -0
  62. package/dist/src/local/keychain.js +236 -0
  63. package/dist/src/local/local-config.d.ts +34 -0
  64. package/dist/src/local/local-config.js +61 -0
  65. package/dist/src/local/local-data-layer.d.ts +20 -0
  66. package/dist/src/local/local-data-layer.js +15 -0
  67. package/dist/src/local/local-plan-store.d.ts +66 -0
  68. package/dist/src/local/local-plan-store.js +195 -0
  69. package/dist/src/local/pii-scrubber.d.ts +26 -0
  70. package/dist/src/local/pii-scrubber.js +219 -0
  71. package/dist/src/policy/policy-engine.d.ts +44 -0
  72. package/dist/src/policy/policy-engine.js +119 -0
  73. package/dist/src/rate-limit.d.ts +17 -0
  74. package/dist/src/rate-limit.js +41 -0
  75. package/dist/src/security.d.ts +19 -0
  76. package/dist/src/security.js +118 -0
  77. package/dist/src/skills/index.d.ts +12 -0
  78. package/dist/src/skills/index.js +16 -0
  79. package/dist/src/skills/retirement-pack-v1.d.ts +28 -0
  80. package/dist/src/skills/retirement-pack-v1.js +295 -0
  81. package/dist/src/skills/skill-executor.d.ts +65 -0
  82. package/dist/src/skills/skill-executor.js +174 -0
  83. package/dist/src/skills/skill-manifest-schema.d.ts +337 -0
  84. package/dist/src/skills/skill-manifest-schema.js +94 -0
  85. package/dist/src/skills/skill-registry.d.ts +71 -0
  86. package/dist/src/skills/skill-registry.js +116 -0
  87. package/dist/src/telemetry.d.ts +12 -0
  88. package/dist/src/telemetry.js +59 -0
  89. package/dist/src/types.d.ts +46 -0
  90. package/dist/src/types.js +4 -0
  91. package/dist/tools/agent-tools.d.ts +12 -0
  92. package/dist/tools/agent-tools.js +141 -0
  93. package/dist/tools/forecast-management-tools.d.ts +9 -0
  94. package/dist/tools/forecast-management-tools.js +133 -0
  95. package/dist/tools/local-plan-tools.d.ts +8 -0
  96. package/dist/tools/local-plan-tools.js +357 -0
  97. package/dist/tools/mcp-helpers.d.ts +52 -0
  98. package/dist/tools/mcp-helpers.js +177 -0
  99. package/dist/tools/medicare-tools.d.ts +3 -0
  100. package/dist/tools/medicare-tools.js +162 -0
  101. package/dist/tools/optimize-roth-tools-test.d.ts +2 -0
  102. package/dist/tools/optimize-roth-tools-test.js +36 -0
  103. package/dist/tools/optimize-roth-tools.d.ts +3 -0
  104. package/dist/tools/optimize-roth-tools.js +818 -0
  105. package/dist/tools/plan-management-tools.d.ts +3 -0
  106. package/dist/tools/plan-management-tools.js +196 -0
  107. package/dist/tools/planning-tools.d.ts +3 -0
  108. package/dist/tools/planning-tools.js +290 -0
  109. package/dist/tools/proposal-tools.d.ts +3 -0
  110. package/dist/tools/proposal-tools.js +428 -0
  111. package/dist/tools/report-tools.d.ts +3 -0
  112. package/dist/tools/report-tools.js +245 -0
  113. package/dist/tools/scenario-management-tools.d.ts +3 -0
  114. package/dist/tools/scenario-management-tools.js +136 -0
  115. package/dist/views/verification-packet.html +211 -0
  116. package/host-configs/cowork.json +11 -0
  117. package/host-configs/goose.yaml +22 -0
  118. package/host-configs/openclaw-manifest.json +16 -0
  119. package/package.json +66 -0
  120. package/skills/SKILL.md +162 -0
  121. package/skills/manifest.json +51 -0
  122. package/skills/tools/activate_plan.md +36 -0
  123. package/skills/tools/coach.md +59 -0
  124. package/skills/tools/comprehensive_plan.md +65 -0
  125. package/skills/tools/create_plan.md +59 -0
  126. package/skills/tools/create_saved_plan.md +49 -0
  127. package/skills/tools/delete_plan.md +42 -0
  128. package/skills/tools/delete_scenario.md +38 -0
  129. package/skills/tools/generate_proposal.md +63 -0
  130. package/skills/tools/generate_retirement_report.md +50 -0
  131. package/skills/tools/get_active_plan.md +44 -0
  132. package/skills/tools/get_baseline_forecast.md +47 -0
  133. package/skills/tools/get_plan.md +44 -0
  134. package/skills/tools/get_plan_components.md +50 -0
  135. package/skills/tools/get_scenario.md +46 -0
  136. package/skills/tools/list_plans.md +44 -0
  137. package/skills/tools/list_scenarios.md +42 -0
  138. package/skills/tools/medicare-guardian.md +59 -0
  139. package/skills/tools/nestpilot_run_plan.md +61 -0
  140. package/skills/tools/optimize_roth_conversion.md +107 -0
  141. package/skills/tools/optimize_ss_claiming.md +30 -0
  142. package/skills/tools/rename_plan.md +34 -0
  143. package/skills/tools/retirement-planner.md +55 -0
  144. package/skills/tools/run_forecast.md +65 -0
  145. package/skills/tools/run_saved_forecast.md +52 -0
  146. package/skills/tools/run_scenario.md +66 -0
  147. package/skills/tools/save_plan.md +48 -0
  148. package/skills/tools/save_scenario.md +50 -0
  149. package/skills/tools/verify_forecast.md +43 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `nestpilot export` / `nestpilot import` — plan portability.
3
+ *
4
+ * Export decrypts plans and packages them as portable JSON.
5
+ * Import re-encrypts with the local encryption key.
6
+ *
7
+ * Export bundle format:
8
+ * ```json
9
+ * {
10
+ * "version": "1.0",
11
+ * "exported": "2026-03-02T...",
12
+ * "source": "machine-name",
13
+ * "plans": [{ id, plan, forecasts, scenarios }]
14
+ * }
15
+ * ```
16
+ *
17
+ * @feature FEAT-0088
18
+ */
19
+ import fs from "node:fs/promises";
20
+ import os from "node:os";
21
+ import path from "node:path";
22
+ import { loadLocalConfig, plansDir, } from "../src/local/local-config.js";
23
+ import { createKeychainProvider } from "../src/local/keychain.js";
24
+ import { EncryptionService } from "../src/local/encryption.js";
25
+ import { LocalPlanStore } from "../src/local/local-plan-store.js";
26
+ // ── Export command ────────────────────────────────────────────────────────
27
+ export async function exportCommand(opts) {
28
+ const config = loadLocalConfig();
29
+ const keychain = createKeychainProvider(config.dataDir);
30
+ const encryption = new EncryptionService(keychain);
31
+ const store = new LocalPlanStore(config.dataDir, encryption);
32
+ console.log("\n📦 NestPilot Export\n");
33
+ let plans = [];
34
+ if (opts.plan) {
35
+ // Export specific plan
36
+ try {
37
+ const plan = await store.get(opts.plan);
38
+ plans = [plan];
39
+ }
40
+ catch (e) {
41
+ console.error(`❌ Plan not found: ${opts.plan}. ${e instanceof Error ? e.message : ""}`);
42
+ process.exit(1);
43
+ }
44
+ }
45
+ else if (opts.all) {
46
+ // Export all plans
47
+ const summaries = await store.list();
48
+ if (summaries.length === 0) {
49
+ console.log("No plans to export.");
50
+ return;
51
+ }
52
+ for (const summary of summaries) {
53
+ plans.push(await store.get(summary.id));
54
+ }
55
+ }
56
+ else {
57
+ console.error("Specify --plan <id> or --all");
58
+ process.exit(1);
59
+ }
60
+ const bundle = {
61
+ version: "1.0",
62
+ exported: new Date().toISOString(),
63
+ source: os.hostname(),
64
+ plans: plans.map((p) => ({
65
+ id: p.id,
66
+ plan: p.plan,
67
+ forecasts: p.forecasts,
68
+ scenarios: p.scenarios,
69
+ })),
70
+ };
71
+ const outputPath = opts.output ??
72
+ path.join(process.cwd(), `nestpilot-export-${new Date().toISOString().slice(0, 10)}.json`);
73
+ await fs.writeFile(outputPath, JSON.stringify(bundle, null, 2));
74
+ console.log(`✅ Exported ${plans.length} plan(s) to ${outputPath}`);
75
+ console.log(`\nImport on another machine: nestpilot import ${path.basename(outputPath)}\n`);
76
+ }
77
+ // ── Import command ───────────────────────────────────────────────────────
78
+ export async function importCommand(file) {
79
+ const config = loadLocalConfig();
80
+ const keychain = createKeychainProvider(config.dataDir);
81
+ const encryption = new EncryptionService(keychain);
82
+ const store = new LocalPlanStore(config.dataDir, encryption);
83
+ console.log("\n📥 NestPilot Import\n");
84
+ // Resolve file path
85
+ const filePath = path.resolve(file);
86
+ let raw;
87
+ try {
88
+ raw = await fs.readFile(filePath, "utf-8");
89
+ }
90
+ catch (e) {
91
+ console.error(`❌ Cannot read file: ${filePath}. ${e instanceof Error ? e.message : ""}`);
92
+ process.exit(1);
93
+ }
94
+ let bundle;
95
+ try {
96
+ bundle = JSON.parse(raw);
97
+ }
98
+ catch {
99
+ console.error("❌ Invalid JSON file.");
100
+ process.exit(1);
101
+ }
102
+ if (bundle.version !== "1.0") {
103
+ console.error(`❌ Unsupported export version: ${bundle.version}`);
104
+ process.exit(1);
105
+ }
106
+ console.log(`Source: ${bundle.source}`);
107
+ console.log(`Exported: ${bundle.exported}`);
108
+ console.log(`Plans: ${bundle.plans.length}\n`);
109
+ let imported = 0;
110
+ let skipped = 0;
111
+ for (const planData of bundle.plans) {
112
+ try {
113
+ // Check if plan already exists
114
+ try {
115
+ await store.get(planData.id);
116
+ console.log(` ⏭ ${planData.id} — already exists (skipped)`);
117
+ skipped++;
118
+ continue;
119
+ }
120
+ catch {
121
+ // Plan doesn't exist — import it
122
+ }
123
+ await store.create(planData.plan);
124
+ console.log(` ✓ ${planData.id} — imported`);
125
+ imported++;
126
+ }
127
+ catch (e) {
128
+ console.error(` ❌ ${planData.id} — failed: ${e instanceof Error ? e.message : String(e)}`);
129
+ }
130
+ }
131
+ console.log(`\n✅ Imported ${imported} plan(s), skipped ${skipped} existing.\n`);
132
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * NestPilot MCP Server CLI — unified entry point for the npm package.
4
+ *
5
+ * Commands:
6
+ * serve (default) — Start MCP server (stdio or HTTP)
7
+ * init — Initialize ~/.nestpilot/ and provision API key
8
+ * config — Interactive configuration
9
+ * export — Export plans to portable JSON
10
+ * import — Import plans from export file
11
+ * status — Show local data summary
12
+ * doctor — Diagnose common issues
13
+ *
14
+ * @feature FEAT-0088
15
+ */
16
+ import { Command } from "commander";
17
+ import { initCommand } from "./init.js";
18
+ import { doctorCommand } from "./doctor.js";
19
+ import { exportCommand, importCommand } from "./export-import.js";
20
+ const VERSION = "1.0.0";
21
+ const program = new Command("nestpilot-mcp-server");
22
+ program
23
+ .description("NestPilot MCP Server — Local-first retirement planning appliance")
24
+ .version(VERSION);
25
+ // ── serve (default command) ──────────────────────────────────────────────
26
+ program
27
+ .command("serve", { isDefault: true })
28
+ .description("Start MCP server (stdio transport by default)")
29
+ .option("--http", "Use HTTP transport instead of stdio")
30
+ .option("--port <port>", "HTTP port (default: 3001)", "3001")
31
+ .action(async (opts) => {
32
+ // Set environment before importing main
33
+ process.env.NESTPILOT_MODE = process.env.NESTPILOT_MODE ?? "local";
34
+ if (opts.http) {
35
+ process.env.PORT = opts.port;
36
+ }
37
+ else {
38
+ process.argv.push("--stdio");
39
+ }
40
+ // Dynamic import to avoid loading server code during CLI-only commands
41
+ await import("../main.js");
42
+ });
43
+ // ── init ─────────────────────────────────────────────────────────────────
44
+ program
45
+ .command("init")
46
+ .description("Initialize ~/.nestpilot/ directory and provision API key")
47
+ .action(initCommand);
48
+ // ── config ───────────────────────────────────────────────────────────────
49
+ program
50
+ .command("config")
51
+ .description("Show or update configuration")
52
+ .option("--set <key=value>", "Set a configuration value")
53
+ .action(async (opts) => {
54
+ const { loadLocalConfig, configFilePath } = await import("../src/local/local-config.js");
55
+ const fs = await import("node:fs/promises");
56
+ const config = loadLocalConfig();
57
+ const configPath = configFilePath(config.dataDir);
58
+ if (opts.set) {
59
+ const [key, value] = opts.set.split("=");
60
+ if (!key || value === undefined) {
61
+ console.error("Usage: nestpilot config --set key=value");
62
+ process.exit(1);
63
+ }
64
+ let existing = {};
65
+ try {
66
+ existing = JSON.parse(await fs.readFile(configPath, "utf-8"));
67
+ }
68
+ catch {
69
+ // No existing config
70
+ }
71
+ existing[key] = value;
72
+ await fs.writeFile(configPath, JSON.stringify(existing, null, 2));
73
+ console.log(`✓ Set ${key} = ${value}`);
74
+ }
75
+ else {
76
+ console.log("\nNestPilot Configuration:");
77
+ console.log(` Mode: ${config.mode}`);
78
+ console.log(` Data dir: ${config.dataDir}`);
79
+ console.log(` Cloud API: ${config.cloudApiUrl}`);
80
+ console.log(` Encryption: ${config.encryptionEnabled ? "enabled" : "disabled"}`);
81
+ console.log(` API key: ${config.apiKey ? "configured" : "not set"}`);
82
+ console.log(`\nConfig file: ${configPath}`);
83
+ }
84
+ });
85
+ // ── export ───────────────────────────────────────────────────────────────
86
+ program
87
+ .command("export")
88
+ .description("Export plans to portable JSON")
89
+ .option("--plan <planId>", "Export specific plan by ID")
90
+ .option("--all", "Export all plans")
91
+ .option("-o, --output <file>", "Output file path")
92
+ .action(exportCommand);
93
+ // ── import ───────────────────────────────────────────────────────────────
94
+ program
95
+ .command("import")
96
+ .description("Import plans from export file")
97
+ .argument("<file>", "Path to exported JSON file")
98
+ .action(importCommand);
99
+ // ── status ───────────────────────────────────────────────────────────────
100
+ program
101
+ .command("status")
102
+ .description("Show local data summary")
103
+ .action(async () => {
104
+ const { loadLocalConfig, plansDir, forecastsDir } = await import("../src/local/local-config.js");
105
+ const fs = await import("node:fs/promises");
106
+ const path = await import("node:path");
107
+ const config = loadLocalConfig();
108
+ let planCount = 0;
109
+ let forecastCount = 0;
110
+ let totalSize = 0;
111
+ try {
112
+ const plans = await fs.readdir(plansDir(config.dataDir));
113
+ planCount = plans.filter((f) => f.endsWith(".json")).length;
114
+ for (const file of plans) {
115
+ const stat = await fs.stat(path.join(plansDir(config.dataDir), file));
116
+ totalSize += stat.size;
117
+ }
118
+ }
119
+ catch {
120
+ // Directory may not exist yet
121
+ }
122
+ try {
123
+ const forecasts = await fs.readdir(forecastsDir(config.dataDir));
124
+ forecastCount = forecasts.filter((f) => f.endsWith(".json")).length;
125
+ for (const file of forecasts) {
126
+ const stat = await fs.stat(path.join(forecastsDir(config.dataDir), file));
127
+ totalSize += stat.size;
128
+ }
129
+ }
130
+ catch {
131
+ // Directory may not exist yet
132
+ }
133
+ const sizeKb = (totalSize / 1024).toFixed(1);
134
+ console.log("\n📊 NestPilot Status");
135
+ console.log("══════════════════");
136
+ console.log(` Mode: ${config.mode}`);
137
+ console.log(` Data directory: ${config.dataDir}`);
138
+ console.log(` Plans: ${planCount}`);
139
+ console.log(` Cached forecasts: ${forecastCount}`);
140
+ console.log(` Disk usage: ${sizeKb} KB`);
141
+ console.log(` Cloud API: ${config.cloudApiUrl}`);
142
+ console.log(` API key: ${config.apiKey ? "✓ configured" : "✗ not set"}`);
143
+ });
144
+ // ── doctor ───────────────────────────────────────────────────────────────
145
+ program
146
+ .command("doctor")
147
+ .description("Diagnose common issues")
148
+ .action(doctorCommand);
149
+ // ── Parse ────────────────────────────────────────────────────────────────
150
+ // Non-blocking update check (fires and forgets)
151
+ void checkForUpdates();
152
+ program.parse();
153
+ // ── Auto-update check ────────────────────────────────────────────────────
154
+ async function checkForUpdates() {
155
+ try {
156
+ const response = await fetch("https://registry.npmjs.org/nestpilot-mcp-server/latest", { signal: AbortSignal.timeout(3_000) });
157
+ if (!response.ok)
158
+ return;
159
+ const { version: latest } = (await response.json());
160
+ if (latest && latest !== VERSION) {
161
+ console.error(`[nestpilot] Update available: ${VERSION} → ${latest}`);
162
+ console.error("[nestpilot] Run: npm update -g nestpilot-mcp-server");
163
+ }
164
+ }
165
+ catch {
166
+ // Silent fail — don't block startup
167
+ }
168
+ }
@@ -0,0 +1 @@
1
+ export declare function initCommand(): Promise<void>;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * `nestpilot init` — bootstraps the local NestPilot environment.
3
+ *
4
+ * Steps:
5
+ * 1. Create ~/.nestpilot/ directory structure
6
+ * 2. Generate encryption key and store in OS keychain
7
+ * 3. Optionally provision API key via cloud endpoint
8
+ * 4. Write config.json with preferences
9
+ * 5. Copy host config templates
10
+ * 6. Print success message with next steps
11
+ *
12
+ * @feature FEAT-0088
13
+ */
14
+ import fs from "node:fs/promises";
15
+ import path from "node:path";
16
+ import * as readline from "node:readline/promises";
17
+ import { stdin, stdout } from "node:process";
18
+ import { loadLocalConfig, DATA_SUBDIRS, configFilePath, } from "../src/local/local-config.js";
19
+ import { createKeychainProvider } from "../src/local/keychain.js";
20
+ import { EncryptionService } from "../src/local/encryption.js";
21
+ // ── Init command ─────────────────────────────────────────────────────────
22
+ export async function initCommand() {
23
+ console.log("\n🏠 NestPilot Local Setup");
24
+ console.log("========================\n");
25
+ const config = loadLocalConfig();
26
+ const { dataDir } = config;
27
+ // Step 1: Create directory structure
28
+ console.log("1. Creating data directory structure...");
29
+ for (const sub of DATA_SUBDIRS) {
30
+ const dir = path.join(dataDir, sub);
31
+ await fs.mkdir(dir, { recursive: true });
32
+ console.log(` ✓ ${dir}`);
33
+ }
34
+ // Also create host-configs output directory
35
+ const hostConfigDir = path.join(dataDir, "host-configs");
36
+ await fs.mkdir(hostConfigDir, { recursive: true });
37
+ console.log(` ✓ ${hostConfigDir}`);
38
+ console.log();
39
+ // Step 2: Encryption setup
40
+ console.log("2. Encryption Setup");
41
+ const keychain = createKeychainProvider(dataDir);
42
+ const encryption = new EncryptionService(keychain);
43
+ if (await encryption.hasKey()) {
44
+ console.log(" ✓ Encryption key already exists in keychain");
45
+ }
46
+ else {
47
+ await encryption.generateAndStoreKey();
48
+ console.log(" ✓ Encryption key generated and stored in OS keychain");
49
+ }
50
+ console.log();
51
+ // Step 3: API Key provisioning (optional)
52
+ console.log("3. API Key Provisioning");
53
+ const existingKey = await keychain.get("nestpilot", "api-key");
54
+ if (existingKey) {
55
+ console.log(" ✓ API key already configured");
56
+ }
57
+ else {
58
+ const rl = readline.createInterface({ input: stdin, output: stdout });
59
+ try {
60
+ const email = await rl.question(" Enter your email for free tier (10 forecasts/mo), or press Enter to skip: ");
61
+ if (email.trim()) {
62
+ try {
63
+ const response = await fetch(`${config.cloudApiUrl}/api/auth/provision-key`, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify({ email: email.trim() }),
67
+ signal: AbortSignal.timeout(10_000),
68
+ });
69
+ if (response.ok) {
70
+ const data = (await response.json());
71
+ await keychain.set("nestpilot", "api-key", data.apiKey);
72
+ console.log(" ✓ API key provisioned (10 forecasts/mo free)");
73
+ console.log(" ✓ API key stored in OS keychain");
74
+ }
75
+ else {
76
+ console.log(" ⚠ Could not provision API key (cloud API unavailable).");
77
+ console.log(" Set NESTPILOT_API_KEY env var manually, or run init again later.");
78
+ }
79
+ }
80
+ catch {
81
+ console.log(" ⚠ Could not reach cloud API for key provisioning.");
82
+ console.log(" Set NESTPILOT_API_KEY env var manually, or run init again later.");
83
+ }
84
+ }
85
+ else {
86
+ console.log(" ⏭ Skipped. Set NESTPILOT_API_KEY env var later.");
87
+ }
88
+ }
89
+ finally {
90
+ rl.close();
91
+ }
92
+ }
93
+ console.log();
94
+ // Step 4: Write config.json
95
+ console.log("4. Configuration");
96
+ const configPath = configFilePath(dataDir);
97
+ const configData = {
98
+ mode: "local",
99
+ cloudApiUrl: config.cloudApiUrl,
100
+ encryptionEnabled: true,
101
+ created: new Date().toISOString(),
102
+ version: "1.0.0",
103
+ };
104
+ await fs.writeFile(configPath, JSON.stringify(configData, null, 2));
105
+ console.log(` ✓ ${configPath} created`);
106
+ console.log();
107
+ // Step 5: Copy host config templates
108
+ console.log("5. Host Configuration Templates");
109
+ await writeHostConfigs(hostConfigDir);
110
+ console.log(` ✓ Goose config: ${path.join(hostConfigDir, "goose.yaml")}`);
111
+ console.log(` ✓ Cowork config: ${path.join(hostConfigDir, "cowork.json")}`);
112
+ console.log(` ✓ OpenClaw config: ${path.join(hostConfigDir, "openclaw-manifest.json")}`);
113
+ console.log();
114
+ // Step 6: Success
115
+ console.log("✅ NestPilot is ready!\n");
116
+ console.log("Next steps:");
117
+ console.log(` • Goose: Add config from ${path.join(hostConfigDir, "goose.yaml")}`);
118
+ console.log(` • Claude: Add config from ${path.join(hostConfigDir, "cowork.json")}`);
119
+ console.log(` • OpenClaw: Import from ${path.join(hostConfigDir, "openclaw-manifest.json")}`);
120
+ console.log();
121
+ console.log("Quick test: npx nestpilot-mcp-server status");
122
+ console.log();
123
+ }
124
+ // ── Host config writer ───────────────────────────────────────────────────
125
+ async function writeHostConfigs(dir) {
126
+ // Goose
127
+ const gooseConfig = `# NestPilot MCP Server — Goose Configuration
128
+ # Add this to ~/.config/goose/profiles.yaml under extensions:
129
+
130
+ extensions:
131
+ nestpilot:
132
+ type: mcp
133
+ transport: stdio
134
+ command: npx
135
+ args: ["nestpilot-mcp-server"]
136
+ env:
137
+ NESTPILOT_MODE: local
138
+ `;
139
+ await fs.writeFile(path.join(dir, "goose.yaml"), gooseConfig);
140
+ // Cowork
141
+ const coworkConfig = {
142
+ mcpServers: {
143
+ nestpilot: {
144
+ command: "npx",
145
+ args: ["nestpilot-mcp-server"],
146
+ env: {
147
+ NESTPILOT_MODE: "local",
148
+ },
149
+ },
150
+ },
151
+ };
152
+ await fs.writeFile(path.join(dir, "cowork.json"), JSON.stringify(coworkConfig, null, 2));
153
+ // OpenClaw
154
+ const openclawManifest = {
155
+ name: "nestpilot",
156
+ version: "1.0.0",
157
+ description: "Local-first retirement planning — your data stays on your machine",
158
+ transport: "stdio",
159
+ command: "npx",
160
+ args: ["nestpilot-mcp-server"],
161
+ env: {
162
+ NESTPILOT_MODE: "local",
163
+ },
164
+ capabilities: {
165
+ tools: true,
166
+ resources: true,
167
+ apps: true,
168
+ },
169
+ };
170
+ await fs.writeFile(path.join(dir, "openclaw-manifest.json"), JSON.stringify(openclawManifest, null, 2));
171
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "nestpilot": {
4
+ "command": "npx",
5
+ "args": ["nestpilot-mcp-server"],
6
+ "env": {
7
+ "NESTPILOT_MODE": "local"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,22 @@
1
+ # NestPilot MCP Server — Goose Host Configuration
2
+ #
3
+ # Add this to your Goose profile configuration:
4
+ # ~/.config/goose/profiles.yaml
5
+ #
6
+ # Under the `extensions:` section of your active profile,
7
+ # paste the following block.
8
+ #
9
+ # @feature FEAT-0088
10
+
11
+ extensions:
12
+ nestpilot:
13
+ type: mcp
14
+ transport: stdio
15
+ command: npx
16
+ args: ["nestpilot-mcp-server"]
17
+ env:
18
+ NESTPILOT_MODE: local
19
+ # Optional overrides:
20
+ # NESTPILOT_DATA_DIR: "~/.nestpilot"
21
+ # NESTPILOT_CLOUD_API_URL: "https://api.nestpilot.com"
22
+ # NESTPILOT_API_KEY: "your-api-key"
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "nestpilot",
3
+ "version": "1.0.0",
4
+ "description": "Local-first retirement planning — your data stays on your machine",
5
+ "transport": "stdio",
6
+ "command": "npx",
7
+ "args": ["nestpilot-mcp-server"],
8
+ "env": {
9
+ "NESTPILOT_MODE": "local"
10
+ },
11
+ "capabilities": {
12
+ "tools": true,
13
+ "resources": true,
14
+ "apps": true
15
+ }
16
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
package/dist/main.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * NestPilot MCP Server — unified entry point.
4
+ *
5
+ * Supports two transports:
6
+ * --stdio → Claude Desktop / any MCP client with stdio transport
7
+ * (default) → Streamable HTTP on PORT (default 3001)
8
+ *
9
+ * Production features:
10
+ * - Bearer token authentication (configurable via MCP_AUTH_* env)
11
+ * - Per-user rate limiting
12
+ * - OpenTelemetry tracing
13
+ * - CORS support
14
+ *
15
+ * Run with:
16
+ * npx tsx main.ts # HTTP transport
17
+ * npx tsx main.ts --stdio # stdio transport
18
+ */
19
+ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
20
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
22
+ import cors from "cors";
23
+ import "dotenv/config";
24
+ import { loadConfig } from "./src/config.js";
25
+ import { InMemoryRateLimiter } from "./src/rate-limit.js";
26
+ import { authenticateRequest } from "./src/security.js";
27
+ import { initializeTelemetry, shutdownTelemetry } from "./src/telemetry.js";
28
+ import { createServer } from "./server.js";
29
+ const config = loadConfig();
30
+ initializeTelemetry(config);
31
+ const rateLimiter = new InMemoryRateLimiter(config.rateLimitWindowMs, config.rateLimitMaxRequests);
32
+ function writeMcpJsonRpcError(res, statusCode, message) {
33
+ if (res.headersSent)
34
+ return;
35
+ res.status(statusCode).json({
36
+ jsonrpc: "2.0",
37
+ error: { code: -32000, message },
38
+ id: null,
39
+ });
40
+ }
41
+ /**
42
+ * Starts an MCP server with Streamable HTTP transport.
43
+ * Includes auth, rate limiting, and CORS from mcp-edge.
44
+ */
45
+ async function startStreamableHTTPServer(factory) {
46
+ const app = createMcpExpressApp({ host: "0.0.0.0" });
47
+ app.use(cors());
48
+ app.options("/mcp", cors());
49
+ app.all("/mcp", async (req, res) => {
50
+ // ── Authentication ──────────────────────────────────────────────
51
+ const auth = authenticateRequest(req.headers, config);
52
+ if (!auth.ok) {
53
+ writeMcpJsonRpcError(res, auth.statusCode, auth.message);
54
+ return;
55
+ }
56
+ // ── Rate limiting ───────────────────────────────────────────────
57
+ const rate = rateLimiter.check(auth.userId);
58
+ res.setHeader("X-RateLimit-Limit", String(config.rateLimitMaxRequests));
59
+ res.setHeader("X-RateLimit-Remaining", String(rate.remaining));
60
+ res.setHeader("X-RateLimit-Reset", String(rate.resetAtMs));
61
+ if (!rate.allowed) {
62
+ res.setHeader("Retry-After", String(rate.retryAfterSeconds));
63
+ writeMcpJsonRpcError(res, 429, `Rate limit exceeded. Retry in ${rate.retryAfterSeconds} seconds.`);
64
+ return;
65
+ }
66
+ // ── MCP request handling ────────────────────────────────────────
67
+ const authCtx = {
68
+ userId: auth.userId,
69
+ bearerToken: auth.bearerToken,
70
+ };
71
+ const server = factory(authCtx);
72
+ const transport = new StreamableHTTPServerTransport({
73
+ sessionIdGenerator: undefined,
74
+ });
75
+ res.on("close", () => {
76
+ transport.close().catch(() => { });
77
+ server.close().catch(() => { });
78
+ });
79
+ try {
80
+ await server.connect(transport);
81
+ await transport.handleRequest(req, res, req.body);
82
+ }
83
+ catch (error) {
84
+ console.error("[mcp-app] MCP error:", error);
85
+ writeMcpJsonRpcError(res, 500, "Internal server error");
86
+ }
87
+ });
88
+ const httpServer = app.listen(config.port, () => {
89
+ console.log(`NestPilot MCP server listening on http://localhost:${config.port}/mcp`);
90
+ const authLabel = config.authMode === "jwt-passthrough"
91
+ ? "jwt-passthrough"
92
+ : config.requireAuth || config.authTokens.size > 0
93
+ ? "enabled (token)"
94
+ : "disabled (open)";
95
+ console.log(` Auth: ${authLabel}`);
96
+ console.log(` Rate limit: ${config.rateLimitMaxRequests} req / ${config.rateLimitWindowMs / 1000}s`);
97
+ });
98
+ const shutdown = () => {
99
+ console.log("\nShutting down...");
100
+ void shutdownTelemetry();
101
+ httpServer.close(() => process.exit(0));
102
+ };
103
+ process.on("SIGINT", shutdown);
104
+ process.on("SIGTERM", shutdown);
105
+ }
106
+ /**
107
+ * Starts an MCP server with stdio transport (for Claude Desktop).
108
+ */
109
+ async function startStdioServer(factory) {
110
+ await factory().connect(new StdioServerTransport());
111
+ }
112
+ async function main() {
113
+ if (process.argv.includes("--stdio")) {
114
+ await startStdioServer(createServer);
115
+ }
116
+ else {
117
+ if (config.authMode === "token" &&
118
+ config.requireAuth &&
119
+ config.authTokens.size === 0) {
120
+ throw new Error("Authentication is enabled but MCP_AUTH_TOKENS is empty. Set MCP_AUTH_TOKENS to one or more comma-separated bearer tokens.");
121
+ }
122
+ await startStreamableHTTPServer(createServer);
123
+ }
124
+ }
125
+ main().catch((e) => {
126
+ console.error(e);
127
+ process.exit(1);
128
+ });