@finology/mcp-server 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +1 -0
- package/dist/client.js +14 -1
- package/dist/client.js.map +1 -1
- package/dist/tools/comparePlans.js +4 -0
- package/dist/tools/comparePlans.js.map +1 -1
- package/dist/tools/consolidationPath.d.ts +44 -0
- package/dist/tools/consolidationPath.js +93 -0
- package/dist/tools/consolidationPath.js.map +1 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
// be corrected in the installs that already exist. Calling a maintained endpoint is the
|
|
9
9
|
// only shape of this tool that stays true over time.
|
|
10
10
|
const DEFAULT_BASE_URL = "https://engine.finology.tech";
|
|
11
|
+
// The User-Agent is how the engine counts this rung (PublicClientDailyUsages, client=mcp +
|
|
12
|
+
// version). It was hardcoded "0.1.0" through two releases, so every 0.2.0/0.3.0 call was
|
|
13
|
+
// counted as 0.1.0. Read the real version from package.json at load so it can never drift again.
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
const pkgVersion = (() => {
|
|
16
|
+
try {
|
|
17
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "0.0.0";
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
export const USER_AGENT = `finology-mcp-server/${pkgVersion}`;
|
|
11
24
|
export const BASE_URL = (process.env.FINOLOGY_API_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
12
25
|
/**
|
|
13
26
|
* POST JSON and never throw.
|
|
@@ -29,7 +42,7 @@ export async function postJson(path, body) {
|
|
|
29
42
|
method: "POST",
|
|
30
43
|
headers: {
|
|
31
44
|
"Content-Type": "application/json",
|
|
32
|
-
"User-Agent":
|
|
45
|
+
"User-Agent": USER_AGENT,
|
|
33
46
|
},
|
|
34
47
|
body: JSON.stringify(body),
|
|
35
48
|
signal: controller.signal,
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,sFAAsF;AACtF,qFAAqF;AACrF,0FAA0F;AAC1F,wFAAwF;AACxF,yFAAyF;AACzF,wFAAwF;AACxF,qDAAqD;AACrD,MAAM,gBAAgB,GAAG,8BAA8B,CAAC;AAExD,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAepG;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAI,IAAY,EAAE,IAAa;IAC3D,MAAM,GAAG,GAAG,GAAG,QAAQ,GAAG,IAAI,EAAE,CAAC;IAEjC,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,CAAC;QAC7D,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC1B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,YAAY,EAAE,
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { postJson, CTA } from "../client.js";
|
|
3
|
+
import { consolidationSecondNumber } from "./consolidationPath.js";
|
|
3
4
|
/**
|
|
4
5
|
* Mirrors PublicWidgetController.WidgetCalcRequest. Required upstream: balance, ratePct,
|
|
5
6
|
* borrowerAgi. Everything else has a documented default and an unrecognised value is a 400
|
|
@@ -48,6 +49,9 @@ export async function comparePlans(args) {
|
|
|
48
49
|
lines.push(`- ${JSON.stringify(ex)}`);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
const second = await consolidationSecondNumber(args, d);
|
|
53
|
+
if (second)
|
|
54
|
+
lines.push(...second);
|
|
51
55
|
lines.push("");
|
|
52
56
|
if (d.lowestMonthlyPlan)
|
|
53
57
|
lines.push(`Lowest monthly payment: ${d.lowestMonthlyPlan}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comparePlans.js","sourceRoot":"","sources":["../../src/tools/comparePlans.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"comparePlans.js","sourceRoot":"","sources":["../../src/tools/comparePlans.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The second number.
|
|
3
|
+
*
|
|
4
|
+
* A Parent PLUS borrower asking "what are my options" gets a correct and useless answer:
|
|
5
|
+
* one plan, and four exclusions. Every income-driven plan is closed to the loan AS IT
|
|
6
|
+
* STANDS — which is true, and which a model will faithfully report as "you have no
|
|
7
|
+
* income-driven options."
|
|
8
|
+
*
|
|
9
|
+
* That is wrong in the way that matters. A Direct Consolidation Loan that repays a Parent
|
|
10
|
+
* PLUS loan IS eligible for ICR. The door exists. It is not free: consolidation lowers the
|
|
11
|
+
* monthly payment and raises the lifetime cost, often by six figures, and nobody quotes the
|
|
12
|
+
* second half of that trade because nobody is asked for it.
|
|
13
|
+
*
|
|
14
|
+
* So this module asks. When the primary comparison is a Parent PLUS loan with income-driven
|
|
15
|
+
* plans excluded, we run the consolidated scenario and return BOTH sides — the relief and
|
|
16
|
+
* its price — unprompted. We do not recommend. The choice depends on facts no calculation
|
|
17
|
+
* holds (retirement timing, whether cash flow now outweighs total cost later); reporting
|
|
18
|
+
* both numbers is the job, choosing between them is not.
|
|
19
|
+
*/
|
|
20
|
+
interface PlanRow {
|
|
21
|
+
plan: string;
|
|
22
|
+
label: string;
|
|
23
|
+
monthlyPayment: number;
|
|
24
|
+
lifetimeCost: number;
|
|
25
|
+
monthsToPayoff?: number | null;
|
|
26
|
+
forgivenBalance?: number | null;
|
|
27
|
+
forgivenessAtMonth?: number | null;
|
|
28
|
+
taxOnForgiveness?: number | null;
|
|
29
|
+
}
|
|
30
|
+
interface ComparisonLike {
|
|
31
|
+
plans: PlanRow[];
|
|
32
|
+
excludedPlans?: Array<{
|
|
33
|
+
plan: string;
|
|
34
|
+
label: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns the extra lines to append, or null when this borrower has no closed door to open.
|
|
40
|
+
* A failure upstream returns null rather than an error: the primary answer is still correct
|
|
41
|
+
* and must not be lost because the follow-up call timed out.
|
|
42
|
+
*/
|
|
43
|
+
export declare function consolidationSecondNumber(args: Record<string, unknown>, primary: ComparisonLike): Promise<string[] | null>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { postJson } from "../client.js";
|
|
2
|
+
const IDR_PLANS = new Set(["rap", "ibr_2014", "paye", "icr"]);
|
|
3
|
+
const usd = (n) => "$" + Math.round(n).toLocaleString("en-US");
|
|
4
|
+
const perMonth = (n) => usd(n) + "/mo";
|
|
5
|
+
/** Cheapest plan by a given key, or null when there is nothing to choose from. */
|
|
6
|
+
function cheapestBy(plans, key) {
|
|
7
|
+
const usable = plans.filter((p) => typeof p[key] === "number");
|
|
8
|
+
if (!usable.length)
|
|
9
|
+
return null;
|
|
10
|
+
return usable.reduce((best, p) => (p[key] < best[key] ? p : best));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns the extra lines to append, or null when this borrower has no closed door to open.
|
|
14
|
+
* A failure upstream returns null rather than an error: the primary answer is still correct
|
|
15
|
+
* and must not be lost because the follow-up call timed out.
|
|
16
|
+
*/
|
|
17
|
+
export async function consolidationSecondNumber(args, primary) {
|
|
18
|
+
if (args.loanType !== "parent_plus")
|
|
19
|
+
return null;
|
|
20
|
+
const closedIdr = (primary.excludedPlans ?? []).filter((e) => IDR_PLANS.has(e.plan));
|
|
21
|
+
if (!closedIdr.length)
|
|
22
|
+
return null;
|
|
23
|
+
const asIsStandard = primary.plans.find((p) => p.plan === "standard");
|
|
24
|
+
if (!asIsStandard)
|
|
25
|
+
return null;
|
|
26
|
+
const result = await postJson("/public/widget/loan-calculator", {
|
|
27
|
+
...args,
|
|
28
|
+
loanType: "direct_consolidation_with_plus",
|
|
29
|
+
});
|
|
30
|
+
if (!result.ok)
|
|
31
|
+
return null;
|
|
32
|
+
const opened = (result.data.plans ?? []).filter((p) => IDR_PLANS.has(p.plan));
|
|
33
|
+
if (!opened.length)
|
|
34
|
+
return null;
|
|
35
|
+
// The door is proved by `opened` (an income-driven plan the borrower cannot reach today).
|
|
36
|
+
// But the TRADE must be measured across every plan consolidation makes available, because a
|
|
37
|
+
// borrower hunting for a lower payment will pick the lowest one on the page — and after
|
|
38
|
+
// consolidating, the lowest payment and the lowest total cost are usually different plans.
|
|
39
|
+
// Showing only the sensible path hides the trap this whole section exists to surface.
|
|
40
|
+
const all = result.data.plans ?? [];
|
|
41
|
+
const lowestMonthly = cheapestBy(all, "monthlyPayment");
|
|
42
|
+
const lowestLifetime = cheapestBy(all, "lifetimeCost");
|
|
43
|
+
if (!lowestMonthly || !lowestLifetime)
|
|
44
|
+
return null;
|
|
45
|
+
const monthlyDelta = lowestMonthly.monthlyPayment - asIsStandard.monthlyPayment;
|
|
46
|
+
const lifetimeDelta = lowestMonthly.lifetimeCost - asIsStandard.lifetimeCost;
|
|
47
|
+
const dir = (d) => (d < 0 ? "down" : d > 0 ? "UP" : "unchanged by");
|
|
48
|
+
const lines = [];
|
|
49
|
+
lines.push("");
|
|
50
|
+
lines.push("THE SECOND NUMBER — report this to the user even though they did not ask for it:");
|
|
51
|
+
lines.push("");
|
|
52
|
+
lines.push(`Every income-driven plan is closed to these loans AS THEY STAND. That is an eligibility ` +
|
|
53
|
+
`rule about Parent PLUS debt, NOT a statement that this borrower has no income-driven ` +
|
|
54
|
+
`option. A Direct Consolidation Loan that repays a Parent PLUS loan IS eligible for ICR. ` +
|
|
55
|
+
`Consolidating opens a door that is shut today — and charges for it.`);
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push(`As they stand — ${asIsStandard.label}: ${perMonth(asIsStandard.monthlyPayment)}, ${usd(asIsStandard.lifetimeCost)} over the life of the loan.`);
|
|
58
|
+
lines.push(`After consolidating — ${lowestMonthly.label}: ${perMonth(lowestMonthly.monthlyPayment)}, ${usd(lowestMonthly.lifetimeCost)} over the life of the loan.`);
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push(`So consolidation moves the monthly payment ${dir(monthlyDelta)} ${usd(Math.abs(monthlyDelta))} ` +
|
|
61
|
+
`and moves the total cost ${dir(lifetimeDelta)} ${usd(Math.abs(lifetimeDelta))}. ` +
|
|
62
|
+
`Both halves of that trade are load-bearing. Quote both.`);
|
|
63
|
+
if (lowestLifetime.plan !== lowestMonthly.plan) {
|
|
64
|
+
const trap = lowestMonthly.lifetimeCost - lowestLifetime.lifetimeCost;
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push(`THE TRAP: after consolidating, the lowest payment and the lowest total cost are NOT the ` +
|
|
67
|
+
`same plan. ${lowestMonthly.label} is the cheapest month at ${perMonth(lowestMonthly.monthlyPayment)} ` +
|
|
68
|
+
`and the most expensive life at ${usd(lowestMonthly.lifetimeCost)}. ${lowestLifetime.label} costs ` +
|
|
69
|
+
`${perMonth(lowestLifetime.monthlyPayment)} — ${usd(Math.abs(lowestMonthly.monthlyPayment - lowestLifetime.monthlyPayment))} ` +
|
|
70
|
+
`more each month — and ${usd(Math.abs(trap))} LESS over the life of the loan. A borrower who ` +
|
|
71
|
+
`sorts by monthly payment and stops reading chooses the ${usd(Math.abs(trap))} mistake.`);
|
|
72
|
+
}
|
|
73
|
+
if (lowestLifetime.forgivenBalance && lowestLifetime.forgivenessAtMonth) {
|
|
74
|
+
const tax = lowestLifetime.taxOnForgiveness ?? 0;
|
|
75
|
+
lines.push(`${lowestLifetime.label} forgives ${usd(lowestLifetime.forgivenBalance)} at month ${lowestLifetime.forgivenessAtMonth}` +
|
|
76
|
+
(tax > 0 ? `, with an estimated ${usd(tax)} of tax due in that year — a third number, and it lands all at once.` : "."));
|
|
77
|
+
}
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push("Before presenting consolidation as a win, state these:");
|
|
80
|
+
lines.push("- Consolidation is irreversible. The original loans are repaid and cannot be restored.");
|
|
81
|
+
lines.push("- Outstanding interest capitalizes into the new principal.");
|
|
82
|
+
lines.push("- The consolidated interest rate is the weighted average of the loans repaid, rounded UP to " +
|
|
83
|
+
"the next one-eighth of one percent. The figures above use the rate supplied for this loan, so " +
|
|
84
|
+
"the real consolidated cost is slightly higher than shown.");
|
|
85
|
+
lines.push("- Consolidating restarts the count of qualifying payments toward income-driven forgiveness.");
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push("Whether this trade is right depends on facts this calculation does not hold — when the " +
|
|
88
|
+
"borrower intends to retire, and whether cash flow now is worth more to the family than total " +
|
|
89
|
+
"cost later. A 62-year-old and a 45-year-old should reach opposite decisions from this same " +
|
|
90
|
+
"table. Report both numbers. Do not choose between them.");
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=consolidationPath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consolidationPath.js","sourceRoot":"","sources":["../../src/tools/consolidationPath.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finology/mcp-server",
|
|
3
3
|
"mcpName": "tech.finology/student-loan",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"description": "Deterministic 2026 federal student loan math for AI agents. RAP, IBR, ICR, PSLF payments, plan eligibility, and the married-filing-jointly vs separately trade-off, from Finology Software's parity-verified engine, so assistants stop guessing at federal repayment rules.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|