@finology/mcp-server 0.1.0 → 0.3.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 +11 -0
- package/dist/index.js +12 -0
- package/dist/index.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/dist/tools/filingStatusSwing.d.ts +36 -0
- package/dist/tools/filingStatusSwing.js +119 -0
- package/dist/tools/filingStatusSwing.js.map +1 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -30,6 +30,17 @@ SAVE, Graduated, Extended, tiered Standard — returning monthly payment, lifeti
|
|
|
30
30
|
forgiveness and tax on forgiveness for each, **plus the plans that are excluded and the
|
|
31
31
|
eligibility rule that excludes them.**
|
|
32
32
|
|
|
33
|
+
### `compare_married_filing_jointly_vs_separately_student_loans`
|
|
34
|
+
|
|
35
|
+
For a married borrower, prices the filing-status decision. Filing separately removes the spouse's
|
|
36
|
+
income from the income-driven payment calculation, which routinely moves the lifetime loan cost by
|
|
37
|
+
tens of thousands of dollars.
|
|
38
|
+
|
|
39
|
+
**It answers half the question on purpose, and says so.** The engine models the loan side exactly and
|
|
40
|
+
does not model the tax cost of filing separately — lost credits, worse brackets, community-property
|
|
41
|
+
splits — which is often large enough to reverse the answer. Returning only the favourable half as if
|
|
42
|
+
it were the whole decision would be the same confident half-truth this server exists to replace.
|
|
43
|
+
|
|
33
44
|
### `estimate_rap_monthly_payment`
|
|
34
45
|
|
|
35
46
|
The RAP monthly payment alone, for the common case. Requires the borrower's AGI and refuses to
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { comparePlansShape, comparePlans } from "./tools/comparePlans.js";
|
|
5
5
|
import { rapPaymentShape, rapPayment } from "./tools/rapPayment.js";
|
|
6
|
+
import { filingStatusSwingShape, filingStatusSwing } from "./tools/filingStatusSwing.js";
|
|
6
7
|
import { BASE_URL } from "./client.js";
|
|
7
8
|
const server = new McpServer({
|
|
8
9
|
name: "finology-student-loan",
|
|
@@ -33,6 +34,17 @@ server.registerTool("estimate_rap_monthly_payment", {
|
|
|
33
34
|
"assumed zero returns the $10 floor and reads like a real answer.",
|
|
34
35
|
inputSchema: rapPaymentShape,
|
|
35
36
|
}, rapPayment);
|
|
37
|
+
server.registerTool("compare_married_filing_jointly_vs_separately_student_loans", {
|
|
38
|
+
title: "Compare filing jointly vs separately for student loans",
|
|
39
|
+
description: "For a MARRIED borrower on an income-driven federal student loan plan, prices the filing-status " +
|
|
40
|
+
"decision: filing separately removes the spouse's income from the payment calculation, which " +
|
|
41
|
+
"lowers the monthly payment and often the lifetime cost by tens of thousands of dollars. Returns " +
|
|
42
|
+
"both sides with the monthly and lifetime difference. IMPORTANT: this models the LOAN side only. " +
|
|
43
|
+
"It does not model the tax cost of filing separately (lost credits, worse brackets, " +
|
|
44
|
+
"community-property splits), which is often large enough to reverse the answer, and the tool says " +
|
|
45
|
+
"so in its output. Use it whenever a married borrower asks about IDR, RAP, PSLF or how to file.",
|
|
46
|
+
inputSchema: filingStatusSwingShape,
|
|
47
|
+
}, filingStatusSwing);
|
|
36
48
|
async function main() {
|
|
37
49
|
const transport = new StdioServerTransport();
|
|
38
50
|
await server.connect(transport);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,uBAAuB;IAC7B,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,4FAA4F;AAC5F,uFAAuF;AACvF,wEAAwE;AAExE,MAAM,CAAC,YAAY,CACjB,8CAA8C,EAC9C;IACE,KAAK,EAAE,8CAA8C;IACrD,WAAW,EACT,0FAA0F;QAC1F,4FAA4F;QAC5F,2FAA2F;QAC3F,yFAAyF;QACzF,2FAA2F;QAC3F,0FAA0F;QAC1F,2FAA2F;QAC3F,kFAAkF;IACpF,WAAW,EAAE,iBAAiB;CAC/B,EACD,YAAY,CACb,CAAC;AAEF,MAAM,CAAC,YAAY,CACjB,8BAA8B,EAC9B;IACE,KAAK,EAAE,gCAAgC;IACvC,WAAW,EACT,4FAA4F;QAC5F,oFAAoF;QACpF,4FAA4F;QAC5F,yFAAyF;QACzF,0FAA0F;QAC1F,kEAAkE;IACpE,WAAW,EAAE,eAAe;CAC7B,EACD,UAAU,CACX,CAAC;AAEF,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,8FAA8F;IAC9F,OAAO,CAAC,KAAK,CAAC,mDAAmD,QAAQ,GAAG,CAAC,CAAC;AAChF,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,KAAK,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACzF,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,uBAAuB;IAC7B,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,4FAA4F;AAC5F,uFAAuF;AACvF,wEAAwE;AAExE,MAAM,CAAC,YAAY,CACjB,8CAA8C,EAC9C;IACE,KAAK,EAAE,8CAA8C;IACrD,WAAW,EACT,0FAA0F;QAC1F,4FAA4F;QAC5F,2FAA2F;QAC3F,yFAAyF;QACzF,2FAA2F;QAC3F,0FAA0F;QAC1F,2FAA2F;QAC3F,kFAAkF;IACpF,WAAW,EAAE,iBAAiB;CAC/B,EACD,YAAY,CACb,CAAC;AAEF,MAAM,CAAC,YAAY,CACjB,8BAA8B,EAC9B;IACE,KAAK,EAAE,gCAAgC;IACvC,WAAW,EACT,4FAA4F;QAC5F,oFAAoF;QACpF,4FAA4F;QAC5F,yFAAyF;QACzF,0FAA0F;QAC1F,kEAAkE;IACpE,WAAW,EAAE,eAAe;CAC7B,EACD,UAAU,CACX,CAAC;AAEF,MAAM,CAAC,YAAY,CACjB,4DAA4D,EAC5D;IACE,KAAK,EAAE,wDAAwD;IAC/D,WAAW,EACT,iGAAiG;QACjG,8FAA8F;QAC9F,kGAAkG;QAClG,kGAAkG;QAClG,qFAAqF;QACrF,mGAAmG;QACnG,gGAAgG;IAClG,WAAW,EAAE,sBAAsB;CACpC,EACD,iBAAiB,CAClB,CAAC;AAEF,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,8FAA8F;IAC9F,OAAO,CAAC,KAAK,CAAC,mDAAmD,QAAQ,GAAG,CAAC,CAAC;AAChF,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,KAAK,CAAC,CAAC;IACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -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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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* The filing-status decision, priced on the loan side.
|
|
4
|
+
*
|
|
5
|
+
* A married borrower on an income-driven plan faces a genuine trade-off that no single
|
|
6
|
+
* payment estimate can express: filing SEPARATELY removes the spouse's income from the
|
|
7
|
+
* payment calculation, which lowers the monthly payment and usually the lifetime cost —
|
|
8
|
+
* and raises the tax bill, because separate filers lose credits and face worse brackets.
|
|
9
|
+
*
|
|
10
|
+
* HARD, and the reason this tool is written the way it is: Finology's engine models the
|
|
11
|
+
* LOAN side of that trade-off exactly. It does NOT model the tax cost of filing separately.
|
|
12
|
+
* Returning only the loan half as though it were the answer would be the same confident
|
|
13
|
+
* half-truth this whole server exists to replace. So the tax side is named explicitly as
|
|
14
|
+
* unmodelled every time, and the tool says plainly that the two have to be weighed together.
|
|
15
|
+
*/
|
|
16
|
+
export declare const filingStatusSwingShape: {
|
|
17
|
+
balance: z.ZodNumber;
|
|
18
|
+
ratePct: z.ZodNumber;
|
|
19
|
+
borrowerAgi: z.ZodNumber;
|
|
20
|
+
spouseAgi: z.ZodNumber;
|
|
21
|
+
dependents: z.ZodDefault<z.ZodNumber>;
|
|
22
|
+
loanType: z.ZodDefault<z.ZodEnum<["direct_unsubsidized", "direct_subsidized", "grad_plus_legacy", "parent_plus", "direct_consolidation", "direct_consolidation_with_plus"]>>;
|
|
23
|
+
};
|
|
24
|
+
export declare function filingStatusSwing(args: Record<string, unknown>): Promise<{
|
|
25
|
+
content: {
|
|
26
|
+
type: "text";
|
|
27
|
+
text: string;
|
|
28
|
+
}[];
|
|
29
|
+
isError: boolean;
|
|
30
|
+
} | {
|
|
31
|
+
content: {
|
|
32
|
+
type: "text";
|
|
33
|
+
text: string;
|
|
34
|
+
}[];
|
|
35
|
+
isError?: undefined;
|
|
36
|
+
}>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { postJson, CTA } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* The filing-status decision, priced on the loan side.
|
|
5
|
+
*
|
|
6
|
+
* A married borrower on an income-driven plan faces a genuine trade-off that no single
|
|
7
|
+
* payment estimate can express: filing SEPARATELY removes the spouse's income from the
|
|
8
|
+
* payment calculation, which lowers the monthly payment and usually the lifetime cost —
|
|
9
|
+
* and raises the tax bill, because separate filers lose credits and face worse brackets.
|
|
10
|
+
*
|
|
11
|
+
* HARD, and the reason this tool is written the way it is: Finology's engine models the
|
|
12
|
+
* LOAN side of that trade-off exactly. It does NOT model the tax cost of filing separately.
|
|
13
|
+
* Returning only the loan half as though it were the answer would be the same confident
|
|
14
|
+
* half-truth this whole server exists to replace. So the tax side is named explicitly as
|
|
15
|
+
* unmodelled every time, and the tool says plainly that the two have to be weighed together.
|
|
16
|
+
*/
|
|
17
|
+
export const filingStatusSwingShape = {
|
|
18
|
+
balance: z.number().min(0).max(100_000_000)
|
|
19
|
+
.describe("Current federal loan balance in dollars."),
|
|
20
|
+
ratePct: z.number().min(0).max(100)
|
|
21
|
+
.describe("Weighted average interest rate as a percentage, e.g. 6.54 for 6.54%."),
|
|
22
|
+
borrowerAgi: z.number().min(0).max(10_000_000)
|
|
23
|
+
.describe("The borrower's own annual adjusted gross income in dollars."),
|
|
24
|
+
spouseAgi: z.number().min(0).max(10_000_000)
|
|
25
|
+
.describe("The spouse's annual AGI in dollars. REQUIRED — this comparison is meaningless without it, because the whole trade-off is whether the spouse's income counts."),
|
|
26
|
+
dependents: z.number().int().min(0).max(20).default(0)
|
|
27
|
+
.describe("Number of qualifying dependents. A spouse is NOT a dependent."),
|
|
28
|
+
loanType: z.enum([
|
|
29
|
+
"direct_unsubsidized",
|
|
30
|
+
"direct_subsidized",
|
|
31
|
+
"grad_plus_legacy",
|
|
32
|
+
"parent_plus",
|
|
33
|
+
"direct_consolidation",
|
|
34
|
+
"direct_consolidation_with_plus",
|
|
35
|
+
]).default("direct_unsubsidized")
|
|
36
|
+
.describe("Federal loan type. Drives plan eligibility, not just the rate."),
|
|
37
|
+
};
|
|
38
|
+
const usd = (n) => n.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
|
|
39
|
+
export async function filingStatusSwing(args) {
|
|
40
|
+
const base = {
|
|
41
|
+
balance: args.balance,
|
|
42
|
+
ratePct: args.ratePct,
|
|
43
|
+
borrowerAgi: args.borrowerAgi,
|
|
44
|
+
dependents: args.dependents ?? 0,
|
|
45
|
+
loanType: args.loanType ?? "direct_unsubsidized",
|
|
46
|
+
};
|
|
47
|
+
// Two runs. Jointly counts the spouse's income; separately does not — that single
|
|
48
|
+
// difference is the entire decision.
|
|
49
|
+
const [joint, separate] = await Promise.all([
|
|
50
|
+
postJson("/public/widget/loan-calculator", {
|
|
51
|
+
...base,
|
|
52
|
+
filingStatus: "mfj",
|
|
53
|
+
spouseAgi: args.spouseAgi,
|
|
54
|
+
}),
|
|
55
|
+
postJson("/public/widget/loan-calculator", {
|
|
56
|
+
...base,
|
|
57
|
+
filingStatus: "mfs",
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
if (!joint.ok)
|
|
61
|
+
return { content: [{ type: "text", text: joint.message }], isError: true };
|
|
62
|
+
if (!separate.ok)
|
|
63
|
+
return { content: [{ type: "text", text: separate.message }], isError: true };
|
|
64
|
+
// Compare income-driven plans only. Standard is unaffected by filing status, so including
|
|
65
|
+
// it would dilute the swing with a row that cannot move.
|
|
66
|
+
const idrOnly = (r) => r.plans.filter((p) => p.plan !== "standard");
|
|
67
|
+
const bestJoint = idrOnly(joint.data).sort((a, b) => a.lifetimeCost - b.lifetimeCost)[0];
|
|
68
|
+
const bestSeparate = idrOnly(separate.data).sort((a, b) => a.lifetimeCost - b.lifetimeCost)[0];
|
|
69
|
+
if (!bestJoint || !bestSeparate) {
|
|
70
|
+
return {
|
|
71
|
+
content: [{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: "This loan book has no income-driven plan available, so filing status does not change the repayment maths here. " +
|
|
74
|
+
"Run the plan comparison tool to see which plans are open and which federal rule excludes the rest." + CTA,
|
|
75
|
+
}],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const monthlyDelta = bestJoint.monthlyPayment - bestSeparate.monthlyPayment;
|
|
79
|
+
const lifetimeDelta = bestJoint.lifetimeCost - bestSeparate.lifetimeCost;
|
|
80
|
+
const lines = [
|
|
81
|
+
`**Filing status changes this borrower's loan cost by ${usd(Math.abs(lifetimeDelta))} over the life of the loan.**`,
|
|
82
|
+
"",
|
|
83
|
+
`| | Filing jointly | Filing separately |`,
|
|
84
|
+
`|---|---|---|`,
|
|
85
|
+
`| Best income-driven plan | ${bestJoint.label} | ${bestSeparate.label} |`,
|
|
86
|
+
`| Monthly payment | ${usd(bestJoint.monthlyPayment)} | ${usd(bestSeparate.monthlyPayment)} |`,
|
|
87
|
+
`| Lifetime loan cost | ${usd(bestJoint.lifetimeCost)} | ${usd(bestSeparate.lifetimeCost)} |`,
|
|
88
|
+
"",
|
|
89
|
+
monthlyDelta > 0
|
|
90
|
+
? `Filing separately lowers the monthly payment by ${usd(Math.abs(monthlyDelta))}, because the spouse's income stops counting toward it.`
|
|
91
|
+
: `Filing jointly is the cheaper side on the loan alone here, by ${usd(Math.abs(monthlyDelta))} a month.`,
|
|
92
|
+
"",
|
|
93
|
+
"**This is only half the decision, and the half that is missing is the reason to get advice.**",
|
|
94
|
+
"",
|
|
95
|
+
"These figures are the LOAN side only. Filing separately also changes the tax return: separate",
|
|
96
|
+
"filers commonly lose the student loan interest deduction, education credits, and favourable",
|
|
97
|
+
"brackets, and in community-property states the income split is a further calculation. Finology",
|
|
98
|
+
"Software's engine does not model that tax cost, and it is frequently large enough to reverse",
|
|
99
|
+
"the answer above. The two have to be weighed together, against one household's actual return.",
|
|
100
|
+
"",
|
|
101
|
+
`Rules as of ${joint.data.asOf}. Source: ${joint.data.computedBy}.`,
|
|
102
|
+
];
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{ type: "text", text: lines.join("\n") + CTA },
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: "```json\n" + JSON.stringify({
|
|
109
|
+
filingJointly: { bestPlan: bestJoint, allPlans: joint.data.plans },
|
|
110
|
+
filingSeparately: { bestPlan: bestSeparate, allPlans: separate.data.plans },
|
|
111
|
+
monthlyDelta,
|
|
112
|
+
lifetimeDelta,
|
|
113
|
+
taxSideModelled: false,
|
|
114
|
+
}, null, 2) + "\n```",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=filingStatusSwing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filingStatusSwing.js","sourceRoot":"","sources":["../../src/tools/filingStatusSwing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finology/mcp-server",
|
|
3
3
|
"mcpName": "tech.finology/student-loan",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Deterministic 2026 federal student loan
|
|
4
|
+
"version": "0.3.0",
|
|
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",
|
|
8
8
|
"bin": {
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
"nslds",
|
|
30
30
|
"financial-planning",
|
|
31
31
|
"cfp",
|
|
32
|
-
"fintech"
|
|
32
|
+
"fintech",
|
|
33
|
+
"married-filing-separately",
|
|
34
|
+
"tax-filing-status",
|
|
35
|
+
"idr"
|
|
33
36
|
],
|
|
34
37
|
"homepage": "https://finology.tech",
|
|
35
38
|
"repository": {
|