@numeratica/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Numeratica / Francis Townsend-Merino
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # @numeratica/mcp
2
+
3
+ A thin [MCP](https://modelcontextprotocol.io) bridge that connects any MCP client (Claude Desktop, Cursor, …) to the **[Numeratica](https://docs.numeratica.com)** financial-planning API — retirement Monte Carlo, taxes, RMDs, Social Security, Roth conversions, and more.
4
+
5
+ **Get a free key at [https://docs.numeratica.com](https://docs.numeratica.com).**
6
+
7
+ ## How it works
8
+
9
+ This package is a **transport bridge, nothing more**. It reads JSON-RPC from stdin and forwards each message to the hosted `POST /mcp` endpoint with your API key as a bearer token, then writes the response back to stdout. The tool list and all results come straight from the API — so the bridge always stays in sync and ships no calculation logic of its own. Your key is sent only in the `Authorization` header and **is never logged**.
10
+
11
+ - **Zero runtime dependencies** (Node ≥ 18 built-in `fetch` + `node:readline`).
12
+ - Open source (MIT). The interesting part is the API; this is just the wire.
13
+
14
+ ## Install
15
+
16
+ No install needed — your MCP client runs it on demand with `npx`. (You can also `npm i -g @numeratica/mcp` to get the `numeratica-mcp` command.)
17
+
18
+ ### Claude Desktop
19
+
20
+ Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
21
+
22
+ ```json
23
+ {
24
+ "mcpServers": {
25
+ "numeratica": {
26
+ "command": "npx",
27
+ "args": ["-y", "@numeratica/mcp"],
28
+ "env": { "NUMERATICA_API_KEY": "nmr_sk_your_key_here" }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### Cursor
35
+
36
+ Add to `~/.cursor/mcp.json` (or a project `.cursor/mcp.json`):
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "numeratica": {
42
+ "command": "npx",
43
+ "args": ["-y", "@numeratica/mcp"],
44
+ "env": { "NUMERATICA_API_KEY": "nmr_sk_your_key_here" }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ Restart the client and the Numeratica tools appear. Try: *"Run a retirement Monte Carlo for a 40-year-old retiring at 65."*
51
+
52
+ ## Configuration
53
+
54
+ | Variable | Required | Default | Notes |
55
+ | --- | --- | --- | --- |
56
+ | `NUMERATICA_API_KEY` | **yes** | — | Your key. Get one free at [docs.numeratica.com](https://docs.numeratica.com). Never logged. |
57
+ | `NUMERATICA_BASE_URL` | no | `https://api.numeratica.com` | Override the API base (e.g. for testing). |
58
+
59
+ You can also pass the key with `--key <value>` instead of the env var.
60
+
61
+ ## Links
62
+
63
+ - **API docs & reference:** [https://docs.numeratica.com](https://docs.numeratica.com)
64
+ - **Get a free key:** [https://docs.numeratica.com/get-key](https://docs.numeratica.com/get-key)
65
+
66
+ ## License
67
+
68
+ MIT © Numeratica / Francis Townsend-Merino
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/bridge.js';
3
+
4
+ main();
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@numeratica/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Thin stdio MCP bridge to the Numeratica financial-planning API — add retirement, tax, and planning tools to any MCP client with npx.",
5
+ "type": "module",
6
+ "bin": {
7
+ "numeratica-mcp": "bin/numeratica-mcp.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "src/",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "keywords": [
19
+ "mcp",
20
+ "numeratica",
21
+ "retirement",
22
+ "tax",
23
+ "finance",
24
+ "agent"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/numeratica/mcp.git"
29
+ },
30
+ "homepage": "https://docs.numeratica.com",
31
+ "bugs": {
32
+ "url": "https://github.com/numeratica/mcp/issues"
33
+ },
34
+ "license": "MIT",
35
+ "author": "Numeratica / Francis Townsend-Merino",
36
+ "scripts": {
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "node --test"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.14.0",
42
+ "typescript": "^5.6.0"
43
+ }
44
+ }
package/src/bridge.js ADDED
@@ -0,0 +1,144 @@
1
+ // Numeratica MCP bridge — a stdio <-> hosted /mcp transport shim.
2
+ //
3
+ // It is deliberately dumb: it knows nothing about the tool catalog or any
4
+ // calculation. Every JSON-RPC message read from stdin is forwarded verbatim to
5
+ // the hosted /mcp endpoint and the response is written back to stdout. So
6
+ // `initialize`, `tools/list`, and `tools/call` are all answered by the server —
7
+ // the bridge auto-syncs with the API and can leak nothing about it.
8
+
9
+ import { createInterface } from 'node:readline';
10
+
11
+ const DEFAULT_BASE_URL = 'https://api.numeratica.com';
12
+
13
+ /**
14
+ * @typedef {Object} Config
15
+ * @property {string|undefined} apiKey
16
+ * @property {string} baseUrl
17
+ */
18
+
19
+ /**
20
+ * Resolve configuration from argv (`--key <value>`) and the environment.
21
+ * @param {string[]} argv process args (without node/script)
22
+ * @param {Record<string,string|undefined>} env
23
+ * @returns {Config}
24
+ */
25
+ export function loadConfig(argv, env) {
26
+ let apiKey = env.NUMERATICA_API_KEY;
27
+ const i = argv.indexOf('--key');
28
+ if (i !== -1 && argv[i + 1]) apiKey = argv[i + 1];
29
+ const baseUrl = (env.NUMERATICA_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, '');
30
+ return { apiKey, baseUrl };
31
+ }
32
+
33
+ /**
34
+ * Validate config. Returns an error message string if invalid, else null.
35
+ * The message never contains the key (there is nothing to leak when it's absent).
36
+ * @param {Config} config
37
+ * @returns {string|null}
38
+ */
39
+ export function validateConfig(config) {
40
+ if (!config.apiKey) {
41
+ return 'NUMERATICA_API_KEY is required. Get a free key at https://docs.numeratica.com';
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Build a JSON-RPC 2.0 error response string.
48
+ * @param {number|string|null} id
49
+ * @param {number} code
50
+ * @param {string} message
51
+ * @returns {string}
52
+ */
53
+ function jsonRpcError(id, code, message) {
54
+ return JSON.stringify({ jsonrpc: '2.0', id: id ?? null, error: { code, message } });
55
+ }
56
+
57
+ /**
58
+ * Forward one raw JSON-RPC line to the hosted endpoint. Returns the text to
59
+ * write to stdout, or null when there is nothing to write (a notification ack).
60
+ * @param {string} line
61
+ * @param {{ baseUrl: string, apiKey: string, fetch: typeof globalThis.fetch }} opts
62
+ * @returns {Promise<string|null>}
63
+ */
64
+ export async function forward(line, opts) {
65
+ const body = line.trim();
66
+ if (!body) return null;
67
+
68
+ // Parse only to learn the id and whether this is a notification (no `id`), so
69
+ // we can echo a matching id on error. The body is still forwarded verbatim.
70
+ let id = null;
71
+ let isNotification = false;
72
+ try {
73
+ const msg = JSON.parse(body);
74
+ if (msg && typeof msg === 'object' && !Array.isArray(msg)) {
75
+ id = msg.id ?? null;
76
+ isNotification = msg.id === undefined;
77
+ }
78
+ } catch {
79
+ // Not valid JSON; forward as-is. We just can't echo a matching id on error.
80
+ }
81
+
82
+ let res;
83
+ try {
84
+ res = await opts.fetch(`${opts.baseUrl}/mcp`, {
85
+ method: 'POST',
86
+ headers: {
87
+ Authorization: `Bearer ${opts.apiKey}`,
88
+ 'Content-Type': 'application/json',
89
+ Accept: 'application/json',
90
+ },
91
+ body,
92
+ });
93
+ } catch (err) {
94
+ if (isNotification) return null;
95
+ const detail = err instanceof Error ? err.message : 'unknown error';
96
+ return jsonRpcError(id, -32000, `transport error contacting Numeratica: ${detail}`);
97
+ }
98
+
99
+ // Notification acks (202/204) carry no body — write nothing.
100
+ if (res.status === 202 || res.status === 204) return null;
101
+
102
+ const text = (await res.text()).trim();
103
+
104
+ if (!res.ok) {
105
+ if (isNotification) return null;
106
+ const detail = text ? `: ${text.slice(0, 500)}` : '';
107
+ return jsonRpcError(id, -32000, `Numeratica API error (HTTP ${res.status})${detail}`);
108
+ }
109
+
110
+ return text === '' ? null : text;
111
+ }
112
+
113
+ /**
114
+ * Run the bridge: read newline-delimited JSON-RPC from `stdin`, forward each
115
+ * message in order, and write each response with `write`. Resolves on stdin EOF.
116
+ * @param {{ stdin: NodeJS.ReadableStream, write: (s: string) => void, fetch: typeof globalThis.fetch, baseUrl: string, apiKey: string }} opts
117
+ * @returns {Promise<void>}
118
+ */
119
+ export async function run(opts) {
120
+ const rl = createInterface({ input: opts.stdin, crlfDelay: Infinity });
121
+ for await (const line of rl) {
122
+ const out = await forward(line, opts);
123
+ if (out !== null) opts.write(out);
124
+ }
125
+ }
126
+
127
+ /** CLI entrypoint: wire process stdio + global fetch, or exit cleanly on misconfig. */
128
+ export async function main() {
129
+ const config = loadConfig(process.argv.slice(2), process.env);
130
+ const err = validateConfig(config);
131
+ if (err) {
132
+ process.stderr.write(`numeratica-mcp: ${err}\n`);
133
+ process.exit(1);
134
+ return;
135
+ }
136
+ await run({
137
+ stdin: process.stdin,
138
+ write: (s) => process.stdout.write(s.endsWith('\n') ? s : s + '\n'),
139
+ fetch: globalThis.fetch,
140
+ baseUrl: config.baseUrl,
141
+ apiKey: /** @type {string} */ (config.apiKey),
142
+ });
143
+ process.exit(0);
144
+ }