projectionlab-mcp 0.0.0-stage → 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 (137) hide show
  1. package/HOW-IT-WORKS.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +295 -2
  4. package/dist/bootstrap.d.ts +9 -0
  5. package/dist/bootstrap.js +90 -0
  6. package/dist/bootstrap.js.map +1 -0
  7. package/dist/bridge.d.ts +29 -0
  8. package/dist/bridge.js +2 -0
  9. package/dist/bridge.js.map +1 -0
  10. package/dist/clonePlan.d.ts +19 -0
  11. package/dist/clonePlan.js +108 -0
  12. package/dist/clonePlan.js.map +1 -0
  13. package/dist/config.d.ts +15 -0
  14. package/dist/config.js +60 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/currentFinances.d.ts +12 -0
  17. package/dist/currentFinances.js +43 -0
  18. package/dist/currentFinances.js.map +1 -0
  19. package/dist/errors.d.ts +20 -0
  20. package/dist/errors.js +36 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/extensionBridge.d.ts +54 -0
  23. package/dist/extensionBridge.js +440 -0
  24. package/dist/extensionBridge.js.map +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +94 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/instructions.d.ts +10 -0
  29. package/dist/instructions.js +24 -0
  30. package/dist/instructions.js.map +1 -0
  31. package/dist/json.d.ts +6 -0
  32. package/dist/json.js +12 -0
  33. package/dist/json.js.map +1 -0
  34. package/dist/largeOutput.d.ts +7 -0
  35. package/dist/largeOutput.js +28 -0
  36. package/dist/largeOutput.js.map +1 -0
  37. package/dist/logger.d.ts +6 -0
  38. package/dist/logger.js +25 -0
  39. package/dist/logger.js.map +1 -0
  40. package/dist/pathPatch.d.ts +33 -0
  41. package/dist/pathPatch.js +183 -0
  42. package/dist/pathPatch.js.map +1 -0
  43. package/dist/planSettings.d.ts +11 -0
  44. package/dist/planSettings.js +30 -0
  45. package/dist/planSettings.js.map +1 -0
  46. package/dist/planVersion.d.ts +25 -0
  47. package/dist/planVersion.js +72 -0
  48. package/dist/planVersion.js.map +1 -0
  49. package/dist/plans.d.ts +53 -0
  50. package/dist/plans.js +208 -0
  51. package/dist/plans.js.map +1 -0
  52. package/dist/prompts/index.d.ts +3 -0
  53. package/dist/prompts/index.js +232 -0
  54. package/dist/prompts/index.js.map +1 -0
  55. package/dist/redact.d.ts +18 -0
  56. package/dist/redact.js +42 -0
  57. package/dist/redact.js.map +1 -0
  58. package/dist/resources/guide.d.ts +2 -0
  59. package/dist/resources/guide.js +13 -0
  60. package/dist/resources/guide.js.map +1 -0
  61. package/dist/resources/knowledge.d.ts +2 -0
  62. package/dist/resources/knowledge.js +100 -0
  63. package/dist/resources/knowledge.js.map +1 -0
  64. package/dist/resources/planData.d.ts +3 -0
  65. package/dist/resources/planData.js +39 -0
  66. package/dist/resources/planData.js.map +1 -0
  67. package/dist/resources/summary.d.ts +3 -0
  68. package/dist/resources/summary.js +40 -0
  69. package/dist/resources/summary.js.map +1 -0
  70. package/dist/secureFs.d.ts +11 -0
  71. package/dist/secureFs.js +34 -0
  72. package/dist/secureFs.js.map +1 -0
  73. package/dist/snapshot.d.ts +10 -0
  74. package/dist/snapshot.js +102 -0
  75. package/dist/snapshot.js.map +1 -0
  76. package/dist/summary.d.ts +7 -0
  77. package/dist/summary.js +77 -0
  78. package/dist/summary.js.map +1 -0
  79. package/dist/tools/bootstrap.d.ts +4 -0
  80. package/dist/tools/bootstrap.js +24 -0
  81. package/dist/tools/bootstrap.js.map +1 -0
  82. package/dist/tools/clonePlan.d.ts +4 -0
  83. package/dist/tools/clonePlan.js +113 -0
  84. package/dist/tools/clonePlan.js.map +1 -0
  85. package/dist/tools/connection.d.ts +3 -0
  86. package/dist/tools/connection.js +57 -0
  87. package/dist/tools/connection.js.map +1 -0
  88. package/dist/tools/export.d.ts +3 -0
  89. package/dist/tools/export.js +42 -0
  90. package/dist/tools/export.js.map +1 -0
  91. package/dist/tools/getCurrentFinances.d.ts +3 -0
  92. package/dist/tools/getCurrentFinances.js +68 -0
  93. package/dist/tools/getCurrentFinances.js.map +1 -0
  94. package/dist/tools/getPlan.d.ts +3 -0
  95. package/dist/tools/getPlan.js +58 -0
  96. package/dist/tools/getPlan.js.map +1 -0
  97. package/dist/tools/getPlanSection.d.ts +3 -0
  98. package/dist/tools/getPlanSection.js +125 -0
  99. package/dist/tools/getPlanSection.js.map +1 -0
  100. package/dist/tools/getPlanSettings.d.ts +3 -0
  101. package/dist/tools/getPlanSettings.js +73 -0
  102. package/dist/tools/getPlanSettings.js.map +1 -0
  103. package/dist/tools/restore.d.ts +11 -0
  104. package/dist/tools/restore.js +408 -0
  105. package/dist/tools/restore.js.map +1 -0
  106. package/dist/tools/schema.d.ts +3 -0
  107. package/dist/tools/schema.js +90 -0
  108. package/dist/tools/schema.js.map +1 -0
  109. package/dist/tools/snapshots.d.ts +4 -0
  110. package/dist/tools/snapshots.js +146 -0
  111. package/dist/tools/snapshots.js.map +1 -0
  112. package/dist/tools/summary.d.ts +3 -0
  113. package/dist/tools/summary.js +43 -0
  114. package/dist/tools/summary.js.map +1 -0
  115. package/dist/tools/updateAccount.d.ts +4 -0
  116. package/dist/tools/updateAccount.js +214 -0
  117. package/dist/tools/updateAccount.js.map +1 -0
  118. package/dist/tools/updatePlanEvent.d.ts +4 -0
  119. package/dist/tools/updatePlanEvent.js +223 -0
  120. package/dist/tools/updatePlanEvent.js.map +1 -0
  121. package/dist/tools/updatePlanSection.d.ts +4 -0
  122. package/dist/tools/updatePlanSection.js +206 -0
  123. package/dist/tools/updatePlanSection.js.map +1 -0
  124. package/dist/tools/updatePlanSettings.d.ts +4 -0
  125. package/dist/tools/updatePlanSettings.js +125 -0
  126. package/dist/tools/updatePlanSettings.js.map +1 -0
  127. package/dist/types/projectionlab.d.ts +42 -0
  128. package/dist/types/projectionlab.js +2 -0
  129. package/dist/types/projectionlab.js.map +1 -0
  130. package/dist/types/wire.d.ts +88 -0
  131. package/dist/types/wire.js +27 -0
  132. package/dist/types/wire.js.map +1 -0
  133. package/dist/version.d.ts +6 -0
  134. package/dist/version.js +10 -0
  135. package/dist/version.js.map +1 -0
  136. package/package.json +71 -4
  137. package/skills/projectionlab/SKILL.md +208 -0
@@ -0,0 +1,67 @@
1
+ # How the ProjectionLab MCP Works
2
+
3
+ Here is a breakdown of exactly how the ProjectionLab Model Context Protocol (MCP) server works, from the underlying problem it solves to the step-by-step data flow.
4
+
5
+ ## 1. The Core Problem It Solves
6
+
7
+ ProjectionLab exposes its supported automation surface through a JavaScript object (`window.projectionlabPluginAPI`) inside the logged-in browser tab rather than through a public REST API or webhook endpoint.
8
+
9
+ Because an MCP host runs outside that page, it cannot directly call a JavaScript object inside the browser. This MCP server bridges that gap.
10
+
11
+ ## 2. The Architecture
12
+
13
+ The server speaks the Model Context Protocol (MCP) over `stdio` to its host and exposes tools (`projectionlab_update_account`, `projectionlab_export_data`, …), resources, and prompts. Everything above the browser transport depends only on the `Bridge` interface (`src/bridge.ts`), which `ExtensionBridge` implements.
14
+
15
+ The server opens a WebSocket listener on `127.0.0.1:<PL_WS_PORT>`. A browser-side **executor** (either the packaged browser extension or a pasted bootstrap snippet) connects *out* to it, receives command frames, calls `window.projectionlabPluginAPI` in the real, already-logged-in ProjectionLab tab, and returns results. No second browser, no second login.
16
+
17
+ ### The wire protocol
18
+
19
+ `src/types/wire.ts` is the contract shared by the server and every executor:
20
+
21
+ - The executor connects and sends a `hello` frame with a shared token and protocol version. The server validates the `Origin` header (extension or app origin only), checks the token, and replies `helloAck`, or closes with code `4001` (auth), `4002` (version), or `4003` (a newer executor replaced this one).
22
+ - Requests are `{ id, method, params }`; responses are `{ id, ok, result | error }`. Methods map directly to Plugin API calls plus `describeApi`/`waitForApi`.
23
+ - A ~20 s `ping`/`pong` heartbeat keeps the connection healthy (and keeps a Manifest V3 service worker from being suspended).
24
+
25
+ ### The browser extension
26
+
27
+ Manifest V3, in `extension/`. A background service worker owns the WebSocket, reconnects with exponential backoff, and uses an `alarms` wake after suspension. For each request it locates a ProjectionLab tab (opening one in the background when configured to do so) and uses `scripting.executeScript` to run the operation directly in the page's **MAIN world**, where `window.projectionlabPluginAPI` is visible. The structured result returns through the scripting API without a content-script relay. Chrome/Edge/Brave use the service-worker manifest; Firefox 128 or newer uses an event-page manifest with MAIN-world scripting support.
28
+
29
+ ### The bootstrap snippet
30
+
31
+ `src/bootstrap.ts` generates a self-contained snippet (returned by the `projectionlab_get_bootstrap` tool) that plays the same executor role from the page's console, with the same protocol and methods. It's the zero-install path and a handy debugging harness, but it lives only until the tab reloads.
32
+
33
+ ## 3. Step-by-Step Execution Lifecycle
34
+
35
+ ### A. Initialization
36
+
37
+ The server starts listening immediately. Tool calls made before an executor connects return an actionable `EXTENSION_NOT_CONNECTED` error. Once the extension (or snippet) connects and authenticates, requests flow.
38
+
39
+ ### B. Read Operations (`export_data`, `get_plan`, `get_summary`, …)
40
+
41
+ 1. The MCP tool validates input with Zod.
42
+ 2. The bridge sends an `exportData({ key })` request over the WebSocket to the executor, which calls it in the browser context.
43
+ 3. The browser serializes your financial state to JSON and returns it.
44
+ 4. The server selects or summarizes the requested data, redacts secrets where appropriate, and returns it to the MCP host. Results above the inline size limit are written to a protected local export file and returned as a file pointer. A short-lived export cache avoids re-serializing the complete state on every read; every write invalidates it.
45
+
46
+ ### C. Write Operations & Safety Flow (`update_*`, `restore_*`, `clone_plan`)
47
+
48
+ The Plugin API provides a targeted `updateAccount` operation for Current Finances and whole-area restore operations (`restorePlans` and the others) for everything else. Plan edits, including `clone_plan`, are built as a complete new plans array and written with `restorePlans`. The server builds safe patches on top of those primitives and enforces a multi-layered safety net:
49
+
50
+ 1. **Dry-Run by Default:** without `confirm: true`, the tool returns a structured diff preview instead of mutating anything.
51
+ 2. **Fresh State:** the tool requests an uncached export and validates the requested change against the current data.
52
+ 3. **Auto-Snapshotting:** on `confirm: true`, the tool passes the affected current section to `SnapshotManager`, which writes a restore-safe local JSON snapshot with an ID and timestamp.
53
+ 4. **Conflict Check:** because `restorePlans` replaces every plan, a plan write re-reads the plans just before writing and refuses with `PLAN_CONFLICT` if any plan was added, removed, or saved in ProjectionLab since the tool read them (`assertPlansUnchanged` in `src/planVersion.ts`). `projectionlab_update_plan_section` also accepts the dry run's `planVersion` as `expectedPlanVersion` and refuses if the plan was saved after the preview.
54
+ 5. **Execution:** only then does the bridge call `updateAccount()` or the appropriate `restore*()` method.
55
+ 6. **Verification:** the tool performs a fresh re-read and checks that the requested change landed. A verification failure is reported with the snapshot retained for recovery.
56
+
57
+ ### D. Rollbacks & Recovery
58
+
59
+ `projectionlab_rollback` reads a saved snapshot and pipes it back through the matching `restore_*` method, reverting the tab's state. A pre-rollback snapshot is also saved.
60
+
61
+ ## 4. Security
62
+
63
+ - The Plugin API key is read only from the MCP host's `env` block, `~/.projectionlab-mcp/.env`, or a development `.env` (gitignored). It is never logged. It stays in the server process and is passed to the page only as an argument to Plugin API calls; the extension does not persist it.
64
+ - The WebSocket binds to `127.0.0.1` only, accepts only the ProjectionLab app and browser-extension origins, and requires a shared token (`PL_WS_TOKEN`, or auto-generated at `~/.projectionlab-mcp/ws-token` with owner-only permissions), so neither other local processes nor arbitrary web pages can drive your data. Native clients without an `Origin` header still need the token.
65
+ - Snapshots scrub the literal Plugin API key while preserving restore data. Exports, plan reads, and the `plan-data` resource additionally redact secret-named fields before they are returned or written to disk.
66
+ - Read-only mode (the default; any `PL_READ_ONLY` other than `false`) rejects mutation tools before they read, snapshot, or write plan data.
67
+ - The published npm package ships only `dist/`, the agent skill, and docs (the `files` allowlist in `package.json`), so source and `.env` files are never distributed.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ted Lanham
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,296 @@
1
- # Temporary Holding Version
1
+ # ProjectionLab MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Talk to an AI assistant about your retirement plan, and let it read and update your numbers in [ProjectionLab](https://projectionlab.com) for you.**
4
+
5
+ ProjectionLab is a retirement and financial-planning app. This project connects it to an AI assistant (like Claude, or any assistant that supports the "MCP" standard; more on that below) so you can ask questions in plain English and get real answers based on *your* actual plan:
6
+
7
+ - *"Am I on track to retire at 60?"*
8
+ - *"What happens to my plan if the market returns 2% less than I assumed?"*
9
+ - *"Bump my planned travel spending to $12,000 a year and show me the effect."*
10
+
11
+ The assistant can look at your plan, walk through the tradeoffs with you, and, only when you say so, make changes for you. Every change is previewed before it happens and can be undone.
12
+
13
+ ---
14
+
15
+ ## Is this for me?
16
+
17
+ You'll get value from this if:
18
+
19
+ - You already use (or want to use) **ProjectionLab** for retirement planning, and
20
+ - You use an **AI assistant that supports MCP** (see below).
21
+
22
+ You do **not** need to be a programmer. The recommended path is to let your assistant do the technical setup for you. You'll mostly be answering a few questions and clicking a couple of buttons in your browser.
23
+
24
+ > **A note on trust and safety:** This tool can *change* your financial plan, so it's built to be careful. It starts in **read-only mode** (it can look but not touch). Nothing is changed without showing you a preview first, and every change is backed up so it can be reversed. Your ProjectionLab API key stays on your own computer and is never sent anywhere.
25
+
26
+ ### What's "MCP"? Which assistants work?
27
+
28
+ **MCP** (Model Context Protocol) is an open standard that lets AI assistants safely connect to outside tools. This project is an MCP "server," so it works with **any** assistant or app that supports MCP, so you are not locked into one company. Popular options include **Claude Desktop** and **Claude Code**, and a growing list of other MCP-compatible assistants and editors. If your assistant can "add an MCP server," it can use this.
29
+
30
+ *(The author uses Claude Code, but that's just a preference. Pick whatever you like.)*
31
+
32
+ ---
33
+
34
+ ## What you'll need
35
+
36
+ 1. A **ProjectionLab account** with **Plugins enabled**. In ProjectionLab, go to **Account Settings → Plugins**, turn plugins on, and copy your **Plugin API Key** (a long code; keep it handy, since you'll give it to your assistant).
37
+ 2. An **AI assistant that supports MCP** (see above).
38
+ 3. A **web browser** you already use: Chrome, Edge, Brave, or Firefox (version 128 or newer).
39
+ 4. **Node.js** installed on your computer (version 20 or newer). If you don't have it, your assistant can help you install it, or download it from [nodejs.org](https://nodejs.org).
40
+
41
+ ---
42
+
43
+ ## The easy way: let your assistant set it up
44
+
45
+ Open your AI assistant and give it this instruction (copy-paste it as-is):
46
+
47
+ > Please set up the ProjectionLab MCP server (the `projectionlab-mcp` npm
48
+ > package) for me. Follow the "For your AI assistant" section of its README at
49
+ > `https://github.com/Backroads4Me/projectionlab-mcp`. I'm not technical, so
50
+ > walk me through anything you need from me (like my ProjectionLab Plugin API key
51
+ > and loading the browser extension) one step at a time. Keep it in read-only
52
+ > mode until I say otherwise.
53
+
54
+ Your assistant will download the project, install it, and hand you a couple of short, click-by-click steps in your browser (loading a small helper extension and pasting in a code). When it's done, just start asking questions about your plan.
55
+
56
+ If anything gets stuck, tell your assistant what you see on screen; it can troubleshoot from there.
57
+
58
+ ---
59
+
60
+ ## For your AI assistant
61
+
62
+ *(This section is written for the AI agent doing the install. If you're a person reading this, you can skip it. Your assistant follows these steps.)*
63
+
64
+ You are setting this up for a **non-technical user**. Explain each thing you need from them in plain language, one step at a time, and never ask them to do something you can do yourself.
65
+
66
+ **1. Get the user's Plugin API key.** Ask them to open ProjectionLab → Account Settings → Plugins, enable plugins, and copy their **Plugin API Key**. Do **not** invent one, and never print, log, or commit it.
67
+
68
+ **2. Store the key.** Write it to `~/.projectionlab-mcp/.env` as the line `PL_API_KEY=...` and make the file readable only by the user (`chmod 600`). Every MCP host on the machine reads this file, so the key lives in one place. Do not repeat the key back to the user. Writes stay disabled by default; do not set `PL_READ_ONLY=false` until the user has seen the tool working and explicitly asks.
69
+
70
+ **3. Register the server with the user's MCP host.** The server runs with `npx`, which needs Node.js 20 or newer:
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "projectionlab": {
75
+ "command": "npx",
76
+ "args": ["-y", "projectionlab-mcp"]
77
+ }
78
+ }
79
+ }
80
+ ```
81
+ The config location varies by host; see [Adding the server to your MCP host](#adding-the-server-to-your-mcp-host) for common ones. Have the user restart the host so it starts the server. On first start the server creates the browser connection token at `~/.projectionlab-mcp/ws-token`.
82
+
83
+ **4. Connect the browser.** The server reaches ProjectionLab through the user's own browser. Pick the path that fits:
84
+
85
+ - **Recommended: the browser extension** (persists across restarts). Download the zip for the user's browser from the [latest release](https://github.com/Backroads4Me/projectionlab-mcp/releases/latest) and unzip it. Then walk the user through loading it (this is a click-by-click browser task; describe exactly what they'll see):
86
+ - **Chrome / Edge / Brave:** open `chrome://extensions`, turn on "Developer mode" (top-right), click "Load unpacked", and choose the unzipped Chromium folder.
87
+ - **Firefox (128+):** open `about:debugging` → "This Firefox" → "Load Temporary Add-on", and choose `manifest.json` in the unzipped Firefox folder. Firefox removes temporary add-ons when it restarts.
88
+ Then have them open the extension's **options page** and enter the WebSocket **port** (default `8788`) and the **shared token** from `~/.projectionlab-mcp/ws-token`.
89
+
90
+ - **Quick alternative: no install.** Call the `projectionlab_get_bootstrap` tool, then have the user paste the returned snippet into their ProjectionLab tab's developer console (F12 → Console). It works the same way but must be re-pasted if they reload the page. Good for a first test drive.
91
+
92
+ **5. Verify.** Have the user open [ProjectionLab](https://app.projectionlab.com), log in, and confirm plugins are enabled. Then call `projectionlab_check_connection`; it should report all checks passing. Do this before any other tool call.
93
+
94
+ **6. Enabling changes (writes).** Leave everything in dry-run/read-only until the user explicitly asks to make changes. When they do: set `PL_READ_ONLY=false` in `~/.projectionlab-mcp/.env` or the host's `env` block, restart the host, and remember that every write still previews first (`confirm: true` required) and auto-snapshots so it can be rolled back. Reassure the user of this before the first change.
95
+
96
+ ---
97
+
98
+ ## Technical reference
99
+
100
+ The rest of this document is for developers or advanced users. Everyday use needs none of it.
101
+
102
+ ### How it works
103
+
104
+ ProjectionLab has no REST API, so the server reaches its browser-side `window.projectionlabPluginAPI` over a localhost WebSocket that a browser-side executor connects out to: the **extension** for normal use, or a pasted **bootstrap snippet** for a zero-install test. See **[HOW-IT-WORKS.md](HOW-IT-WORKS.md)** for the full architecture: the bridge design, wire protocol, extension internals, and safety model.
105
+
106
+ ### Adding the server to your MCP host
107
+
108
+ The server is a standard stdio MCP server started with `npx -y projectionlab-mcp`. Put the key in `~/.projectionlab-mcp/.env` (see [Configuration](#configuration)) so no host config holds a secret.
109
+
110
+ **Claude Code:**
111
+ ```bash
112
+ claude mcp add --scope user projectionlab -- npx -y projectionlab-mcp
113
+ ```
114
+
115
+ **Claude Desktop** (`claude_desktop_config.json`) and other hosts that use the `mcpServers` JSON format:
116
+ ```json
117
+ {
118
+ "mcpServers": {
119
+ "projectionlab": {
120
+ "command": "npx",
121
+ "args": ["-y", "projectionlab-mcp"]
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ **Hermes Agent** (`config.yaml` of the profile):
128
+ ```yaml
129
+ mcp_servers:
130
+ projectionlab:
131
+ command: npx
132
+ args: ["-y", "projectionlab-mcp"]
133
+ enabled: true
134
+ ```
135
+
136
+ Only one server instance can hold the WebSocket port. When a second host starts the server while another instance holds it, the second retries every few seconds and takes over when the first exits; `projectionlab_check_connection` explains the state in the meantime. To run both at once, give one of them its own `PL_WS_PORT` in its `env` block.
137
+
138
+ ### Agent instructions
139
+
140
+ The server carries its own instructions for the agents that use it, so no per-host prompt setup is needed:
141
+
142
+ - **Server instructions**, sent in the MCP initialize result. Hosts that surface them give the model the core rules: check the connection first, the read-only/dry-run/confirm workflow, privacy.
143
+ - **The `projectionlab://guide` resource**, the full agent guide: setup walkthrough for each failed connection check, which tool answers which question, the change workflow, and privacy rules. `projectionlab_check_connection` points agents to it.
144
+ - **The `projectionlab` skill** at [`skills/projectionlab/SKILL.md`](skills/projectionlab/SKILL.md), the same guide in the Agent Skills format, for hosts that load skills (copy the folder into the host's skills directory).
145
+
146
+ ### Manual setup from source
147
+
148
+ ```bash
149
+ npm install
150
+ npm run build
151
+ cd extension && npm install && npm run build # build the extension
152
+ ```
153
+
154
+ Register `node /absolute/path/to/projectionlab-mcp/dist/index.js` with your MCP host, load the built extension (`extension/dist/chromium` or `extension/dist/firefox`), configure its port + token on the options page, then run `projectionlab_check_connection`.
155
+
156
+ ### Configuration
157
+
158
+ Each setting is read from the first of these that sets it:
159
+
160
+ 1. The `env` block of the server entry in the MCP host's config.
161
+ 2. `~/.projectionlab-mcp/.env`, shared by every MCP host on the machine.
162
+ 3. `.env` at the package root, for a development checkout (copy `.env.example`).
163
+
164
+ The server starts without `PL_API_KEY` so it can tell the agent what is missing; tools that need the key return `API_KEY_MISSING` until it is set.
165
+
166
+ ### Environment variables
167
+
168
+ | Variable | Required | Default | Description |
169
+ |---|---|---|---|
170
+ | `PL_API_KEY` | Yes | - | ProjectionLab Plugin API key; tools that reach the plan fail until it is set |
171
+ | `PL_WS_PORT` | No | `8788` | Localhost WebSocket port |
172
+ | `PL_WS_TOKEN` | No | auto-generated | Shared auth token; if unset, generated and saved to `~/.projectionlab-mcp/ws-token` |
173
+ | `PL_APP_URL` | No | `https://app.projectionlab.com` | ProjectionLab app URL |
174
+ | `PL_API_READY_TIMEOUT_MS` | No | `30000` | Timeout waiting for Plugin API to attach |
175
+ | `PL_EXPORT_CACHE_MS` | No | `90000` | How long a full export may be served from cache (writes always invalidate) |
176
+ | `PL_PRETTY_JSON` | No | `false` | Pretty-print JSON output (uses ~25–40% more tokens) |
177
+ | `PL_READ_ONLY` | No | `true` | Write/mutation tools are disabled unless this is exactly `false` |
178
+ | `PL_SNAPSHOT_DIR` | No | `~/.projectionlab-mcp/snapshots` | Directory for pre-mutation snapshots |
179
+ | `LOG_LEVEL` | No | `info` | Log verbosity (`debug`, `info`, `warn`, `error`) |
180
+
181
+ ### Available tools
182
+
183
+ | Tool | Description | Mutates |
184
+ |---|---|---|
185
+ | `projectionlab_check_connection` | Verify browser + API connectivity | No |
186
+ | `projectionlab_get_bootstrap` | Return a console snippet to connect a ProjectionLab tab | No |
187
+ | `projectionlab_describe_plugin_api` | Describe the available Plugin API surface | No |
188
+ | `projectionlab_describe_schema` | Describe the data schema (plan names, account IDs) | No |
189
+ | `projectionlab_export_data` | Export full plan state as JSON | No |
190
+ | `projectionlab_get_summary` | Inventory Current Finances balances and current net worth (not projected outcomes) | No |
191
+ | `projectionlab_get_plan` | Retrieve a single plan | No |
192
+ | `projectionlab_get_plan_section` | Retrieve a plan section; supports event/path filters and typed field-path listing | No |
193
+ | `projectionlab_get_plan_settings` | List discrete plan settings as path-addressable values | No |
194
+ | `projectionlab_get_current_finances` | Get Current Finances or list typed paths for one account | No |
195
+ | `projectionlab_update_account` | Patch one Current Finances account by top-level fields or nested dot paths | Yes |
196
+ | `projectionlab_update_plan_event` | Patch one event by top-level fields or nested dot paths | Yes |
197
+ | `projectionlab_update_plan_settings` | Patch existing assumptions, Monte Carlo, or withdrawal settings by path | Yes |
198
+ | `projectionlab_update_plan_section` | Replace a whole section of a plan (dry-run by default) | Yes |
199
+ | `projectionlab_clone_plan` | Copy a plan to a new, named plan for what-if scenarios | Yes |
200
+ | `projectionlab_list_snapshots` | List saved pre-mutation snapshots | No |
201
+ | `projectionlab_restore_current_finances` | Restore Current Finances from snapshot | Yes |
202
+ | `projectionlab_restore_plans` | Restore Plans from snapshot | Yes |
203
+ | `projectionlab_restore_progress` | Restore Progress from snapshot | Yes |
204
+ | `projectionlab_restore_settings` | Restore Settings from snapshot | Yes |
205
+ | `projectionlab_rollback` | Roll back to a previous snapshot | Yes |
206
+
207
+ ### Resources
208
+
209
+ | URI | Description |
210
+ |---|---|
211
+ | `projectionlab://plan-data` | Full export of all plans/finances/progress/settings (refreshed per access) |
212
+ | `projectionlab://summary` | High-level snapshot: net worth, account breakdown, plan count |
213
+ | `projectionlab://knowledge` | Curated ProjectionLab data-model + FIRE/tax reference (static) |
214
+ | `projectionlab://guide` | Agent guide: setup walkthrough, tool selection, change workflow, privacy rules (static) |
215
+
216
+ ### Prompts
217
+
218
+ | Prompt | Purpose |
219
+ |---|---|
220
+ | `analyze-plan` | Structured analysis: cash flow, tax, risk, withdrawal, estate |
221
+ | `compare-plans` | Side-by-side comparison of two plans |
222
+ | `optimize-tax` | Roth conversion / bracket / IRMAA / gain-harvesting review |
223
+ | `review-assumptions` | Stress-test returns, inflation, healthcare, longevity |
224
+
225
+ ### Safety model
226
+
227
+ Write operations (`update_*`, `restore_*`, `clone_plan`) default to **dry-run mode**. Pass `confirm: true` to execute. Before any confirmed mutation, the server automatically snapshots the affected data section to disk so you can roll back with `projectionlab_rollback`. Writes are disabled entirely unless `PL_READ_ONLY=false`.
228
+
229
+ Every write that replaces the plans array re-reads the plans just before writing and refuses with `PLAN_CONFLICT` if any plan was added, removed, or saved in ProjectionLab since the tool read them, so an edit made in the ProjectionLab UI is never silently overwritten. `projectionlab_update_plan_section` also returns a `planVersion` in its dry run; pass it back as `expectedPlanVersion` when confirming to refuse the write if the plan was saved after the preview.
230
+
231
+ For a small assumptions or strategy change, first call
232
+ `projectionlab_get_plan_settings` with the plan name. It returns the current
233
+ `variables`, `montecarlo`, and `withdrawalStrategy` values as dot paths with
234
+ their JSON types. Preview a path-level patch with
235
+ `projectionlab_update_plan_settings`, for example:
236
+
237
+ ```json
238
+ {
239
+ "planIdentifier": "Retire at 55",
240
+ "section": "variables",
241
+ "changes": {
242
+ "showFutureDollars": true
243
+ }
244
+ }
245
+ ```
246
+
247
+ The update tool preserves sibling settings, rejects unknown paths and JSON type
248
+ changes, and still requires `"confirm": true` before it sends a mutation.
249
+ Advanced inflation, stock-growth, dividend-yield, bond-growth, and bond-yield
250
+ schedules expose both the complete ordered array and indexed control-point
251
+ fields, so one existing year or rate can be adjusted without replacing the
252
+ rest of the schedule.
253
+
254
+ Event forms use the same path workflow. Fetch one event with
255
+ `projectionlab_get_plan_section` using its `eventIdentifier` and
256
+ `"listPaths": true`, then pass selected paths to
257
+ `projectionlab_update_plan_event` as `pathChanges`. The older `patch` input
258
+ remains available for shallow top-level changes.
259
+
260
+ Current Finances accounts also support targeted discovery and nested patches.
261
+ Call `projectionlab_get_current_finances` with an exact `accountId` and
262
+ `"listPaths": true`, then pass selected paths to
263
+ `projectionlab_update_account` as `pathChanges`. The legacy `data` input
264
+ remains available for top-level fields. Dry runs validate the account and
265
+ return before/after values as a safety diff; confirmed no-ops do not create a
266
+ snapshot or send a mutation.
267
+
268
+ ### Evaluating plan impact
269
+
270
+ Projected ending net worth and Chance of Success are the primary outcome
271
+ metrics for comparing a plan before and after a change. Current Finances
272
+ balances are inputs. Mutation before/after values verify that the requested
273
+ input changed, but they do not measure the plan's financial impact.
274
+
275
+ ProjectionLab's Plugin API currently exposes plan definitions and simulation
276
+ settings, but not a supported method to recalculate and retrieve projected
277
+ ending net worth or a completed Chance of Success result. After a confirmed
278
+ mutation, re-read the targeted input through MCP for verification, then
279
+ recalculate the plan and run Chance of Success in the ProjectionLab UI before
280
+ drawing a conclusion.
281
+
282
+ ### Secret handling
283
+
284
+ The Plugin API key stays on your computer: it is never logged, and the browser side never stores it. The WebSocket accepts only local connections that present a shared token. See [Security in HOW-IT-WORKS.md](HOW-IT-WORKS.md#4-security) for the details.
285
+
286
+ ### After code changes
287
+
288
+ This server runs over stdio; the host process keeps compiled `dist/` in memory.
289
+
290
+ ```bash
291
+ npm run build
292
+ # Then restart your MCP host
293
+ projectionlab_check_connection # verify before other operations
294
+ ```
295
+
296
+ With the extension the browser side reconnects on its own, so you only restart the MCP host. If you change extension source, rebuild it (`cd extension && npm run build`) and reload it in the browser.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tier 0 transport: a self-contained snippet the user pastes into the
3
+ * ProjectionLab tab's devtools console. It plays the same executor role as
4
+ * the browser extension - connects to the local WebSocket server, answers
5
+ * ping and request frames against window.projectionlabPluginAPI - but lives
6
+ * only until the tab reloads. Keep the executor logic in sync with
7
+ * extension/src/page-main.js.
8
+ */
9
+ export declare function buildBootstrapSnippet(port: number, token: string): string;
@@ -0,0 +1,90 @@
1
+ import { PROTOCOL_VERSION } from "./types/wire.js";
2
+ import { VERSION } from "./version.js";
3
+ /**
4
+ * Tier 0 transport: a self-contained snippet the user pastes into the
5
+ * ProjectionLab tab's devtools console. It plays the same executor role as
6
+ * the browser extension - connects to the local WebSocket server, answers
7
+ * ping and request frames against window.projectionlabPluginAPI - but lives
8
+ * only until the tab reloads. Keep the executor logic in sync with
9
+ * extension/src/page-main.js.
10
+ */
11
+ export function buildBootstrapSnippet(port, token) {
12
+ const cfg = JSON.stringify({
13
+ port,
14
+ token,
15
+ protocolVersion: PROTOCOL_VERSION,
16
+ executor: `bootstrap/${VERSION}`,
17
+ });
18
+ return `(() => {
19
+ const cfg = ${cfg};
20
+ if (window.__plmcpBootstrap) { console.log("[plmcp] bootstrap already running"); return; }
21
+ window.__plmcpBootstrap = true;
22
+ let stopped = false;
23
+ const log = (m) => console.log("[plmcp]", m);
24
+ function waitForApi(timeoutMs) {
25
+ return new Promise((resolve, reject) => {
26
+ const deadline = Date.now() + timeoutMs;
27
+ (function poll() {
28
+ if (window.projectionlabPluginAPI) return resolve(window.projectionlabPluginAPI);
29
+ if (Date.now() >= deadline) { const e = new Error("Plugin API did not appear within " + Math.round(timeoutMs / 1000) + "s - log in and enable plugins"); e.code = "API_NOT_AVAILABLE"; return reject(e); }
30
+ setTimeout(poll, 500);
31
+ })();
32
+ });
33
+ }
34
+ function describeApi(api) {
35
+ const out = {}; const seen = new Set(); let o = api;
36
+ while (o && o !== Object.prototype) {
37
+ for (const k of Object.getOwnPropertyNames(o)) {
38
+ if (k === "constructor" || seen.has(k)) continue;
39
+ seen.add(k);
40
+ const v = api[k];
41
+ out[k] = typeof v === "function" ? "function(" + v.length + " args)" : typeof v;
42
+ }
43
+ o = Object.getPrototypeOf(o);
44
+ }
45
+ return out;
46
+ }
47
+ async function handle(frame, ws) {
48
+ try {
49
+ const requested = frame.method === "waitForApi" ? Number((frame.params || [])[0]) : NaN;
50
+ const api = await waitForApi(Number.isFinite(requested) && requested > 0 ? requested : 15000);
51
+ let result;
52
+ if (frame.method === "waitForApi") result = true;
53
+ else if (frame.method === "describeApi") result = describeApi(api);
54
+ else {
55
+ const fn = api[frame.method];
56
+ if (typeof fn !== "function") throw new Error("Plugin API method unavailable: " + frame.method);
57
+ result = await fn.apply(api, frame.params);
58
+ }
59
+ ws.send(JSON.stringify({ type: "response", id: frame.id, ok: true, result }));
60
+ } catch (err) {
61
+ const msg = err && err.message ? err.message : String(err);
62
+ const code = err && err.code === "API_NOT_AVAILABLE" ? "API_NOT_AVAILABLE"
63
+ : /api.?key|invalid key|unauthori[sz]ed/i.test(msg) ? "API_KEY_INVALID" : "UNKNOWN";
64
+ ws.send(JSON.stringify({ type: "response", id: frame.id, ok: false, error: { code, message: msg } }));
65
+ }
66
+ }
67
+ function connect() {
68
+ if (stopped) return;
69
+ const ws = new WebSocket("ws://127.0.0.1:" + cfg.port);
70
+ ws.onopen = () => ws.send(JSON.stringify({ type: "hello", token: cfg.token, protocolVersion: cfg.protocolVersion, executor: cfg.executor }));
71
+ ws.onmessage = (ev) => {
72
+ let frame;
73
+ try { frame = JSON.parse(ev.data); } catch { return; }
74
+ if (frame.type === "helloAck") log("connected to projectionlab-mcp");
75
+ else if (frame.type === "ping") ws.send(JSON.stringify({ type: "pong" }));
76
+ else if (frame.type === "request") handle(frame, ws);
77
+ };
78
+ ws.onclose = (ev) => {
79
+ if (stopped) return;
80
+ if (ev.code === 4001 || ev.code === 4002) { stopped = true; window.__plmcpBootstrap = false; log("stopped: " + (ev.reason || ev.code)); return; }
81
+ if (ev.code === 4003) { stopped = true; window.__plmcpBootstrap = false; log("replaced by another executor (probably the extension) - bootstrap no longer needed"); return; }
82
+ log("disconnected; retrying in 3s");
83
+ setTimeout(connect, 3000);
84
+ };
85
+ }
86
+ connect();
87
+ log("bootstrap installed - keep this tab open; re-paste after a page reload");
88
+ })();`;
89
+ }
90
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,KAAa;IAC/D,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC;QACzB,IAAI;QACJ,KAAK;QACL,eAAe,EAAE,gBAAgB;QACjC,QAAQ,EAAE,aAAa,OAAO,EAAE;KACjC,CAAC,CAAC;IACH,OAAO;gBACO,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAqEb,CAAC;AACP,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { ExportedData } from "./types/projectionlab.js";
2
+ export interface ConnectionStatus {
3
+ browser: boolean;
4
+ pageLoaded: boolean;
5
+ apiAvailable: boolean;
6
+ apiKeyValid: boolean;
7
+ errors: string[];
8
+ }
9
+ /**
10
+ * Surface for reaching window.projectionlabPluginAPI in a live ProjectionLab
11
+ * page. Implemented by ExtensionBridge, which relays over a localhost
12
+ * WebSocket to a browser extension or bootstrap snippet. Tools, resources,
13
+ * prompts, and the snapshot manager depend only on this interface.
14
+ */
15
+ export interface Bridge {
16
+ checkConnection(): Promise<ConnectionStatus>;
17
+ describePluginApi(): Promise<Record<string, string>>;
18
+ /** Full export, served from a short-lived cache unless `fresh` is true. */
19
+ exportData(opts?: {
20
+ fresh?: boolean;
21
+ }): Promise<ExportedData>;
22
+ updateAccount(accountId: string, data: Record<string, unknown>, force?: boolean): Promise<void>;
23
+ restoreCurrentFinances(state: unknown): Promise<void>;
24
+ restorePlans(plans: unknown[]): Promise<void>;
25
+ restoreProgress(progress: unknown): Promise<void>;
26
+ restoreSettings(settings: unknown): Promise<void>;
27
+ invalidateCache(): void;
28
+ close(): Promise<void>;
29
+ }
package/dist/bridge.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import { type PlanObj } from "./plans.js";
2
+ /** True when another plan already uses `name` (case-insensitive, trimmed). */
3
+ export declare function planNameTaken(plans: unknown[], name: string): boolean;
4
+ /**
5
+ * Build a copy of `source` to append to `plans` as a new plan.
6
+ *
7
+ * Event ids are kept as they are. Event ids are scoped to their plan:
8
+ * ProjectionLab's own duplicated plans share every event id, built-in
9
+ * milestones use fixed ids such as "retirement", and events, Monte Carlo
10
+ * settings, the withdrawal strategy and bond account order all reference
11
+ * milestone and account ids within the same plan. Copying them unchanged keeps
12
+ * those references intact.
13
+ *
14
+ * The plan-level identity is replaced: a new id and name, a fresh lastUpdated,
15
+ * inactive, and simKey / meta.index / meta.runKey one past the highest among
16
+ * `plans` so the copy does not collide with an existing plan.
17
+ */
18
+ export declare function buildPlanClone(source: PlanObj, plans: unknown[], newName: string, now?: number): PlanObj;
19
+ export declare function verifyPlanClone(before: unknown[], after: unknown[], clone: PlanObj): string[];