@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.
- package/README.md +350 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +214 -0
- package/dist/cli/export-import.d.ts +6 -0
- package/dist/cli/export-import.js +132 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +168 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +171 -0
- package/dist/host-configs/cowork.json +11 -0
- package/dist/host-configs/goose.yaml +22 -0
- package/dist/host-configs/openclaw-manifest.json +16 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +128 -0
- package/dist/mcp-app.html +155 -0
- package/dist/nestpilot-client.d.ts +44 -0
- package/dist/nestpilot-client.js +160 -0
- package/dist/planner.html +222 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +245 -0
- package/dist/skills/SKILL.md +162 -0
- package/dist/skills/manifest.json +51 -0
- package/dist/skills/tools/activate_plan.md +36 -0
- package/dist/skills/tools/coach.md +59 -0
- package/dist/skills/tools/comprehensive_plan.md +65 -0
- package/dist/skills/tools/create_plan.md +59 -0
- package/dist/skills/tools/create_saved_plan.md +49 -0
- package/dist/skills/tools/delete_plan.md +42 -0
- package/dist/skills/tools/delete_scenario.md +38 -0
- package/dist/skills/tools/generate_proposal.md +63 -0
- package/dist/skills/tools/generate_retirement_report.md +50 -0
- package/dist/skills/tools/get_active_plan.md +44 -0
- package/dist/skills/tools/get_baseline_forecast.md +47 -0
- package/dist/skills/tools/get_plan.md +44 -0
- package/dist/skills/tools/get_plan_components.md +50 -0
- package/dist/skills/tools/get_scenario.md +46 -0
- package/dist/skills/tools/list_plans.md +44 -0
- package/dist/skills/tools/list_scenarios.md +42 -0
- package/dist/skills/tools/medicare-guardian.md +59 -0
- package/dist/skills/tools/nestpilot_run_plan.md +61 -0
- package/dist/skills/tools/optimize_roth_conversion.md +107 -0
- package/dist/skills/tools/optimize_ss_claiming.md +30 -0
- package/dist/skills/tools/rename_plan.md +34 -0
- package/dist/skills/tools/retirement-planner.md +55 -0
- package/dist/skills/tools/run_forecast.md +65 -0
- package/dist/skills/tools/run_saved_forecast.md +52 -0
- package/dist/skills/tools/run_scenario.md +66 -0
- package/dist/skills/tools/save_plan.md +48 -0
- package/dist/skills/tools/save_scenario.md +50 -0
- package/dist/skills/tools/verify_forecast.md +43 -0
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +44 -0
- package/dist/src/contracts/provenance.d.ts +37 -0
- package/dist/src/contracts/provenance.js +71 -0
- package/dist/src/contracts/tool-contract-registry.d.ts +43 -0
- package/dist/src/contracts/tool-contract-registry.js +282 -0
- package/dist/src/local/cloud-compute-client.d.ts +55 -0
- package/dist/src/local/cloud-compute-client.js +135 -0
- package/dist/src/local/encryption.d.ts +24 -0
- package/dist/src/local/encryption.js +105 -0
- package/dist/src/local/keychain.d.ts +41 -0
- package/dist/src/local/keychain.js +236 -0
- package/dist/src/local/local-config.d.ts +34 -0
- package/dist/src/local/local-config.js +61 -0
- package/dist/src/local/local-data-layer.d.ts +20 -0
- package/dist/src/local/local-data-layer.js +15 -0
- package/dist/src/local/local-plan-store.d.ts +66 -0
- package/dist/src/local/local-plan-store.js +195 -0
- package/dist/src/local/pii-scrubber.d.ts +26 -0
- package/dist/src/local/pii-scrubber.js +219 -0
- package/dist/src/policy/policy-engine.d.ts +44 -0
- package/dist/src/policy/policy-engine.js +119 -0
- package/dist/src/rate-limit.d.ts +17 -0
- package/dist/src/rate-limit.js +41 -0
- package/dist/src/security.d.ts +19 -0
- package/dist/src/security.js +118 -0
- package/dist/src/skills/index.d.ts +12 -0
- package/dist/src/skills/index.js +16 -0
- package/dist/src/skills/retirement-pack-v1.d.ts +28 -0
- package/dist/src/skills/retirement-pack-v1.js +295 -0
- package/dist/src/skills/skill-executor.d.ts +65 -0
- package/dist/src/skills/skill-executor.js +174 -0
- package/dist/src/skills/skill-manifest-schema.d.ts +337 -0
- package/dist/src/skills/skill-manifest-schema.js +94 -0
- package/dist/src/skills/skill-registry.d.ts +71 -0
- package/dist/src/skills/skill-registry.js +116 -0
- package/dist/src/telemetry.d.ts +12 -0
- package/dist/src/telemetry.js +59 -0
- package/dist/src/types.d.ts +46 -0
- package/dist/src/types.js +4 -0
- package/dist/tools/agent-tools.d.ts +12 -0
- package/dist/tools/agent-tools.js +141 -0
- package/dist/tools/forecast-management-tools.d.ts +9 -0
- package/dist/tools/forecast-management-tools.js +133 -0
- package/dist/tools/local-plan-tools.d.ts +8 -0
- package/dist/tools/local-plan-tools.js +357 -0
- package/dist/tools/mcp-helpers.d.ts +52 -0
- package/dist/tools/mcp-helpers.js +177 -0
- package/dist/tools/medicare-tools.d.ts +3 -0
- package/dist/tools/medicare-tools.js +162 -0
- package/dist/tools/optimize-roth-tools-test.d.ts +2 -0
- package/dist/tools/optimize-roth-tools-test.js +36 -0
- package/dist/tools/optimize-roth-tools.d.ts +3 -0
- package/dist/tools/optimize-roth-tools.js +818 -0
- package/dist/tools/plan-management-tools.d.ts +3 -0
- package/dist/tools/plan-management-tools.js +196 -0
- package/dist/tools/planning-tools.d.ts +3 -0
- package/dist/tools/planning-tools.js +290 -0
- package/dist/tools/proposal-tools.d.ts +3 -0
- package/dist/tools/proposal-tools.js +428 -0
- package/dist/tools/report-tools.d.ts +3 -0
- package/dist/tools/report-tools.js +245 -0
- package/dist/tools/scenario-management-tools.d.ts +3 -0
- package/dist/tools/scenario-management-tools.js +136 -0
- package/dist/views/verification-packet.html +211 -0
- package/host-configs/cowork.json +11 -0
- package/host-configs/goose.yaml +22 -0
- package/host-configs/openclaw-manifest.json +16 -0
- package/package.json +66 -0
- package/skills/SKILL.md +162 -0
- package/skills/manifest.json +51 -0
- package/skills/tools/activate_plan.md +36 -0
- package/skills/tools/coach.md +59 -0
- package/skills/tools/comprehensive_plan.md +65 -0
- package/skills/tools/create_plan.md +59 -0
- package/skills/tools/create_saved_plan.md +49 -0
- package/skills/tools/delete_plan.md +42 -0
- package/skills/tools/delete_scenario.md +38 -0
- package/skills/tools/generate_proposal.md +63 -0
- package/skills/tools/generate_retirement_report.md +50 -0
- package/skills/tools/get_active_plan.md +44 -0
- package/skills/tools/get_baseline_forecast.md +47 -0
- package/skills/tools/get_plan.md +44 -0
- package/skills/tools/get_plan_components.md +50 -0
- package/skills/tools/get_scenario.md +46 -0
- package/skills/tools/list_plans.md +44 -0
- package/skills/tools/list_scenarios.md +42 -0
- package/skills/tools/medicare-guardian.md +59 -0
- package/skills/tools/nestpilot_run_plan.md +61 -0
- package/skills/tools/optimize_roth_conversion.md +107 -0
- package/skills/tools/optimize_ss_claiming.md +30 -0
- package/skills/tools/rename_plan.md +34 -0
- package/skills/tools/retirement-planner.md +55 -0
- package/skills/tools/run_forecast.md +65 -0
- package/skills/tools/run_saved_forecast.md +52 -0
- package/skills/tools/run_scenario.md +66 -0
- package/skills/tools/save_plan.md +48 -0
- package/skills/tools/save_scenario.md +50 -0
- 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,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>;
|
package/dist/cli/init.js
ADDED
|
@@ -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,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
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
|
+
});
|