@finology/mcp-server 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 Finology Software
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,79 @@
1
+ # Finology MCP Server — deterministic federal student loan math for AI agents
2
+
3
+ **Language models guess at federal student loan rules. This doesn't.**
4
+
5
+ `@finology/mcp-server` connects Claude, Cursor, or any MCP client directly to
6
+ [Finology Software](https://finology.tech)'s parity-verified federal repayment engine. Every
7
+ number is computed server-side against current federal rules and returned with a
8
+ `parityVerified` flag. No formulas are approximated, and none are hardcoded into this package.
9
+
10
+ ## Why this exists
11
+
12
+ Federal repayment eligibility is not arithmetic, it is regulation, and it is the part models get
13
+ wrong most confidently:
14
+
15
+ - **A Parent PLUS loan is not RAP-eligible.** Ask a model for the best plan on a Parent PLUS
16
+ balance and it will usually produce a RAP payment. This server returns the exclusion and the
17
+ rule behind it.
18
+ - **RAP is assessed on full AGI with no poverty-line shield**, on a sliding 1%–10% scale, less
19
+ $50 per *qualifying dependent* — and a spouse is not a dependent. Approximating it from older
20
+ IDR formulas gives the wrong answer.
21
+ - **A consolidation that repaid a PLUS loan is restricted to ICR.**
22
+ - **PAYE and SAVE are closed to new enrollment.**
23
+
24
+ ## Tools
25
+
26
+ ### `compare_federal_student_loan_repayment_plans`
27
+
28
+ Compares a balance across every plan the borrower is actually eligible for — RAP, IBR, ICR, PAYE,
29
+ SAVE, Graduated, Extended, tiered Standard — returning monthly payment, lifetime cost, projected
30
+ forgiveness and tax on forgiveness for each, **plus the plans that are excluded and the
31
+ eligibility rule that excludes them.**
32
+
33
+ ### `estimate_rap_monthly_payment`
34
+
35
+ The RAP monthly payment alone, for the common case. Requires the borrower's AGI and refuses to
36
+ proceed without it — an assumed zero returns the $10/month RAP floor, which reads exactly like a
37
+ real answer.
38
+
39
+ ## Install
40
+
41
+ Claude Desktop — add to `claude_desktop_config.json`:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "finology": {
47
+ "command": "npx",
48
+ "args": ["-y", "@finology/mcp-server"]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ No API key. No account. The calculation endpoints are public.
55
+
56
+ ## What is and is not in this repository
57
+
58
+ **Not here, deliberately:** the RAP/OBBB formulas, income-driven payment math, forgiveness and
59
+ tax-bomb projection, plan-eligibility rules, and NSLDS parsing. Those run on Finology's servers.
60
+
61
+ That is not only about protecting the engine. **Federal repayment rules move every year** —
62
+ poverty guidelines, tax brackets, RAP/OBBB implementation guidance, court challenges. A formula
63
+ copied into an npm package is wrong within months and cannot be corrected in the installs that
64
+ already exist. Calling a maintained endpoint is the only shape of this tool that stays true.
65
+
66
+ **Here:** the input schemas, the HTTP call, and error handling that passes the API's own
67
+ field-level messages through to the model instead of flattening them into "request failed."
68
+
69
+ ## Limits
70
+
71
+ These tools return figures. They do not produce client-facing deliverables, save borrower records,
72
+ parse NSLDS files, or track PSLF qualifying payments over time. For those, and for a book of
73
+ borrowers kept current as the rules change: **[finology.tech](https://finology.tech/?utm_source=mcp&utm_medium=ai_agent)**
74
+
75
+ Rate-limited per IP. Not financial advice.
76
+
77
+ ## License
78
+
79
+ MIT
@@ -0,0 +1,23 @@
1
+ export declare const BASE_URL: string;
2
+ /** A failure we can describe to the model, as opposed to one that crashes the transport. */
3
+ export interface ApiFailure {
4
+ ok: false;
5
+ message: string;
6
+ }
7
+ export interface ApiSuccess<T> {
8
+ ok: true;
9
+ data: T;
10
+ }
11
+ export type ApiResult<T> = ApiSuccess<T> | ApiFailure;
12
+ /**
13
+ * POST JSON and never throw.
14
+ *
15
+ * The upstream endpoints validate strictly: an absent required field is a 400 naming the
16
+ * field rather than a silently defaulted zero. Those messages are written to be read, so
17
+ * they are passed through to the model verbatim — a model that is told "Missing required
18
+ * field 'annualIncome'" will ask the user for income, whereas a generic "request failed"
19
+ * invites it to invent one.
20
+ */
21
+ export declare function postJson<T>(path: string, body: unknown): Promise<ApiResult<T>>;
22
+ /** Appended to every successful answer. The tools return numbers; the app returns deliverables. */
23
+ export declare const CTA: string;
package/dist/client.js ADDED
@@ -0,0 +1,81 @@
1
+ // The entire network surface of this package.
2
+ //
3
+ // DELIBERATE: there is no student loan math in this repository. Every number an agent
4
+ // receives is computed server-side by Finology Software's parity-verified engine and
5
+ // returned over HTTP. That is not an accident of packaging — federal repayment rules move
6
+ // every year (poverty guidelines, tax brackets, RAP/OBBB implementation guidance, court
7
+ // challenges), so a formula copied into an npm package is wrong within months and cannot
8
+ // be corrected in the installs that already exist. Calling a maintained endpoint is the
9
+ // only shape of this tool that stays true over time.
10
+ const DEFAULT_BASE_URL = "https://engine.finology.tech";
11
+ export const BASE_URL = (process.env.FINOLOGY_API_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
12
+ /**
13
+ * POST JSON and never throw.
14
+ *
15
+ * The upstream endpoints validate strictly: an absent required field is a 400 naming the
16
+ * field rather than a silently defaulted zero. Those messages are written to be read, so
17
+ * they are passed through to the model verbatim — a model that is told "Missing required
18
+ * field 'annualIncome'" will ask the user for income, whereas a generic "request failed"
19
+ * invites it to invent one.
20
+ */
21
+ export async function postJson(path, body) {
22
+ const url = `${BASE_URL}${path}`;
23
+ let response;
24
+ try {
25
+ const controller = new AbortController();
26
+ const timeout = setTimeout(() => controller.abort(), 30_000);
27
+ try {
28
+ response = await fetch(url, {
29
+ method: "POST",
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ "User-Agent": "finology-mcp-server/0.1.0",
33
+ },
34
+ body: JSON.stringify(body),
35
+ signal: controller.signal,
36
+ });
37
+ }
38
+ finally {
39
+ clearTimeout(timeout);
40
+ }
41
+ }
42
+ catch (error) {
43
+ const detail = error instanceof Error ? error.message : String(error);
44
+ return {
45
+ ok: false,
46
+ message: `Could not reach the Finology calculation service at ${BASE_URL}. ` +
47
+ `This is a connectivity problem, not a problem with the figures supplied. Detail: ${detail}`,
48
+ };
49
+ }
50
+ const raw = await response.text();
51
+ if (!response.ok) {
52
+ // The API returns { error, field } on a 400. Surface both so the model knows which
53
+ // input to go back and ask for.
54
+ let message = `The calculation service returned HTTP ${response.status}.`;
55
+ try {
56
+ const parsed = JSON.parse(raw);
57
+ if (parsed.error) {
58
+ message = parsed.field ? `${parsed.error} (field: ${parsed.field})` : parsed.error;
59
+ }
60
+ }
61
+ catch {
62
+ if (raw.trim())
63
+ message += ` ${raw.trim().slice(0, 400)}`;
64
+ }
65
+ if (response.status === 429) {
66
+ message = "Rate limited by the Finology calculation service. Wait a moment and try again.";
67
+ }
68
+ return { ok: false, message };
69
+ }
70
+ try {
71
+ return { ok: true, data: JSON.parse(raw) };
72
+ }
73
+ catch {
74
+ return { ok: false, message: "The calculation service returned a response that was not valid JSON." };
75
+ }
76
+ }
77
+ /** Appended to every successful answer. The tools return numbers; the app returns deliverables. */
78
+ export const CTA = "\n\n---\nThese figures come from Finology Software's parity-verified federal loan engine. " +
79
+ "To build a client-facing report, compare scenarios side by side, or keep a book of borrowers " +
80
+ "current as the rules change: https://finology.tech/?utm_source=mcp&utm_medium=ai_agent";
81
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { comparePlansShape, comparePlans } from "./tools/comparePlans.js";
5
+ import { rapPaymentShape, rapPayment } from "./tools/rapPayment.js";
6
+ import { BASE_URL } from "./client.js";
7
+ const server = new McpServer({
8
+ name: "finology-student-loan",
9
+ version: "0.1.0",
10
+ });
11
+ // Tool names and descriptions are the discovery surface: registries index them, and a model
12
+ // reads them to decide whether this tool answers the question in front of it. They are
13
+ // written to be matched on, which is why the plan names appear in full.
14
+ server.registerTool("compare_federal_student_loan_repayment_plans", {
15
+ title: "Compare federal student loan repayment plans",
16
+ description: "Compares a US federal student loan across every repayment plan the borrower is actually " +
17
+ "eligible for — RAP (Repayment Assistance Plan), IBR, ICR, PAYE, SAVE, Graduated, Extended " +
18
+ "and the tiered Standard plan — returning the monthly payment and lifetime cost for each, " +
19
+ "plus which plans are excluded and the eligibility rule that excludes them. Computed by " +
20
+ "Finology Software's parity-verified engine against current federal rules, not estimated. " +
21
+ "Use this instead of calculating federal repayment math yourself: eligibility depends on " +
22
+ "loan type and origination date in ways that are easy to get wrong (a Parent PLUS loan is " +
23
+ "not RAP-eligible; a consolidation that repaid a PLUS loan is restricted to ICR).",
24
+ inputSchema: comparePlansShape,
25
+ }, comparePlans);
26
+ server.registerTool("estimate_rap_monthly_payment", {
27
+ title: "Estimate a RAP monthly payment",
28
+ description: "Calculates the monthly payment under RAP, the Repayment Assistance Plan that takes effect " +
29
+ "1 July 2026 under the OBBB. RAP is assessed on full adjusted gross income with no " +
30
+ "poverty-line shield, on a sliding 1%–10% scale, less $50 per qualifying dependent, with a " +
31
+ "$10/month minimum — which is why approximating it from older IDR rules gives the wrong " +
32
+ "answer. Requires the borrower's annual AGI; ask for it rather than assuming, because an " +
33
+ "assumed zero returns the $10 floor and reads like a real answer.",
34
+ inputSchema: rapPaymentShape,
35
+ }, rapPayment);
36
+ async function main() {
37
+ const transport = new StdioServerTransport();
38
+ await server.connect(transport);
39
+ // stderr only: stdout is the JSON-RPC channel and anything written to it corrupts the stream.
40
+ console.error(`finology-student-loan MCP server ready (engine: ${BASE_URL})`);
41
+ }
42
+ main().catch((error) => {
43
+ console.error("Fatal error starting the Finology MCP server:", error);
44
+ process.exit(1);
45
+ });
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Mirrors PublicWidgetController.WidgetCalcRequest. Required upstream: balance, ratePct,
4
+ * borrowerAgi. Everything else has a documented default and an unrecognised value is a 400
5
+ * rather than a silent fallback, so the schema below deliberately does NOT paper over bad
6
+ * input with a guess.
7
+ */
8
+ export declare const comparePlansShape: {
9
+ balance: z.ZodNumber;
10
+ ratePct: z.ZodNumber;
11
+ borrowerAgi: z.ZodNumber;
12
+ filingStatus: z.ZodDefault<z.ZodEnum<["single", "mfj", "mfs", "hoh"]>>;
13
+ spouseAgi: z.ZodOptional<z.ZodNumber>;
14
+ dependents: z.ZodDefault<z.ZodNumber>;
15
+ loanType: z.ZodDefault<z.ZodEnum<["direct_unsubsidized", "direct_subsidized", "grad_plus_legacy", "parent_plus", "direct_consolidation", "direct_consolidation_with_plus"]>>;
16
+ };
17
+ export declare function comparePlans(args: Record<string, unknown>): Promise<{
18
+ content: {
19
+ type: "text";
20
+ text: string;
21
+ }[];
22
+ isError: boolean;
23
+ } | {
24
+ content: {
25
+ type: "text";
26
+ text: string;
27
+ }[];
28
+ isError?: undefined;
29
+ }>;
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ import { postJson, CTA } from "../client.js";
3
+ /**
4
+ * Mirrors PublicWidgetController.WidgetCalcRequest. Required upstream: balance, ratePct,
5
+ * borrowerAgi. Everything else has a documented default and an unrecognised value is a 400
6
+ * rather than a silent fallback, so the schema below deliberately does NOT paper over bad
7
+ * input with a guess.
8
+ */
9
+ export const comparePlansShape = {
10
+ balance: z.number().min(0).max(100_000_000)
11
+ .describe("Current federal loan balance in dollars."),
12
+ ratePct: z.number().min(0).max(100)
13
+ .describe("Weighted average interest rate as a percentage, e.g. 6.54 for 6.54%."),
14
+ borrowerAgi: z.number().min(0).max(10_000_000)
15
+ .describe("Borrower's adjusted gross income in dollars, annual."),
16
+ filingStatus: z.enum(["single", "mfj", "mfs", "hoh"]).default("single")
17
+ .describe("Tax filing status: single, mfj (married filing jointly), mfs (married filing separately), hoh (head of household)."),
18
+ spouseAgi: z.number().min(0).max(10_000_000).optional()
19
+ .describe("Spouse's annual AGI. Only used when filingStatus is mfj."),
20
+ dependents: z.number().int().min(0).max(20).default(0)
21
+ .describe("Number of qualifying dependents. A spouse is NOT a dependent."),
22
+ loanType: z.enum([
23
+ "direct_unsubsidized",
24
+ "direct_subsidized",
25
+ "grad_plus_legacy",
26
+ "parent_plus",
27
+ "direct_consolidation",
28
+ "direct_consolidation_with_plus",
29
+ ]).default("direct_unsubsidized")
30
+ .describe("Federal loan type. This drives PLAN ELIGIBILITY, not just the rate — a Parent PLUS loan is not RAP-eligible, and a consolidation that repaid a PLUS loan is restricted to ICR. Choose accurately."),
31
+ };
32
+ export async function comparePlans(args) {
33
+ const result = await postJson("/public/widget/loan-calculator", args);
34
+ if (!result.ok) {
35
+ return { content: [{ type: "text", text: result.message }], isError: true };
36
+ }
37
+ const d = result.data;
38
+ const lines = [];
39
+ lines.push(`Federal repayment plan comparison (rules as of ${d.asOf}):`);
40
+ lines.push("");
41
+ for (const plan of d.plans) {
42
+ lines.push(`- ${JSON.stringify(plan)}`);
43
+ }
44
+ if (d.excludedPlans?.length) {
45
+ lines.push("");
46
+ lines.push("Plans excluded, and why — an excluded plan is not an oversight, it is an eligibility rule:");
47
+ for (const ex of d.excludedPlans) {
48
+ lines.push(`- ${JSON.stringify(ex)}`);
49
+ }
50
+ }
51
+ lines.push("");
52
+ if (d.lowestMonthlyPlan)
53
+ lines.push(`Lowest monthly payment: ${d.lowestMonthlyPlan}`);
54
+ if (d.lowestLifetimeCostPlan)
55
+ lines.push(`Lowest lifetime cost: ${d.lowestLifetimeCostPlan}`);
56
+ lines.push(`Computed by: ${d.computedBy}`);
57
+ return {
58
+ content: [
59
+ { type: "text", text: lines.join("\n") + CTA },
60
+ { type: "text", text: "```json\n" + JSON.stringify(d, null, 2) + "\n```" },
61
+ ],
62
+ };
63
+ }
64
+ //# sourceMappingURL=comparePlans.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comparePlans.js","sourceRoot":"","sources":["../../src/tools/comparePlans.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Mirrors PublicRapCalculatorController.RapEstimateRequest. Only annualIncome is required
4
+ * upstream, and its absence is a 400 rather than a defaulted zero — a defaulted zero here
5
+ * would silently produce the RAP $10/month floor and present it as an answer.
6
+ *
7
+ * familySize is the HOUSEHOLD size (filer + spouse if filing jointly + dependents). RAP
8
+ * deducts $50 per QUALIFYING DEPENDENT and a spouse is not one, so the server removes the
9
+ * spouse before counting. A joint filer with a household under 2 is rejected rather than
10
+ * clamped, because a clamp would answer confidently from input already known to contradict
11
+ * itself.
12
+ */
13
+ export declare const rapPaymentShape: {
14
+ annualIncome: z.ZodNumber;
15
+ filingStatus: z.ZodDefault<z.ZodEnum<["Single", "MarriedFilingJointly", "MarriedFilingSeparately", "HeadOfHousehold"]>>;
16
+ spouseAnnualIncome: z.ZodOptional<z.ZodNumber>;
17
+ familySize: z.ZodDefault<z.ZodNumber>;
18
+ };
19
+ export declare function rapPayment(args: Record<string, unknown>): Promise<{
20
+ content: {
21
+ type: "text";
22
+ text: string;
23
+ }[];
24
+ isError: boolean;
25
+ } | {
26
+ content: {
27
+ type: "text";
28
+ text: string;
29
+ }[];
30
+ isError?: undefined;
31
+ }>;
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import { postJson, CTA } from "../client.js";
3
+ /**
4
+ * Mirrors PublicRapCalculatorController.RapEstimateRequest. Only annualIncome is required
5
+ * upstream, and its absence is a 400 rather than a defaulted zero — a defaulted zero here
6
+ * would silently produce the RAP $10/month floor and present it as an answer.
7
+ *
8
+ * familySize is the HOUSEHOLD size (filer + spouse if filing jointly + dependents). RAP
9
+ * deducts $50 per QUALIFYING DEPENDENT and a spouse is not one, so the server removes the
10
+ * spouse before counting. A joint filer with a household under 2 is rejected rather than
11
+ * clamped, because a clamp would answer confidently from input already known to contradict
12
+ * itself.
13
+ */
14
+ export const rapPaymentShape = {
15
+ annualIncome: z.number().min(0).max(10_000_000)
16
+ .describe("Borrower's annual adjusted gross income in dollars. REQUIRED — never guess or default this. If the user has not given it, ask."),
17
+ filingStatus: z.enum(["Single", "MarriedFilingJointly", "MarriedFilingSeparately", "HeadOfHousehold"]).default("Single")
18
+ .describe("Tax filing status."),
19
+ spouseAnnualIncome: z.number().min(0).max(10_000_000).optional()
20
+ .describe("Spouse's annual AGI. Only used when filingStatus is MarriedFilingJointly."),
21
+ familySize: z.number().int().min(1).max(20).default(1)
22
+ .describe("Household size: the filer, plus the spouse only if filing jointly, plus dependents. Must be at least 2 when filing jointly."),
23
+ };
24
+ const usd = (n) => n.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 2 });
25
+ export async function rapPayment(args) {
26
+ const result = await postJson("/public/rap-calculator/estimate", args);
27
+ if (!result.ok) {
28
+ return { content: [{ type: "text", text: result.message }], isError: true };
29
+ }
30
+ const d = result.data;
31
+ const text = [
32
+ `RAP (Repayment Assistance Plan) monthly payment: **${usd(d.monthlyPayment)}**`,
33
+ "",
34
+ `- AGI used: ${usd(d.annualIncomeUsed)}${d.filingStatus === "MarriedFilingJointly" ? " (borrower + spouse, joint filing)" : ""}`,
35
+ `- Qualifying dependents counted: ${d.dependents}`,
36
+ `- Filing status: ${d.filingStatus}`,
37
+ "",
38
+ "RAP is assessed on full AGI with no poverty-line shield, on a sliding 1%–10% scale, " +
39
+ "less $50 per qualifying dependent, with a $10/month minimum. This figure is the RAP " +
40
+ "payment only — it is not a comparison against IBR, ICR or the tiered Standard plan, " +
41
+ "and it does not project forgiveness. Use the plan comparison tool for that.",
42
+ ].join("\n");
43
+ return {
44
+ content: [
45
+ { type: "text", text: text + CTA },
46
+ { type: "text", text: "```json\n" + JSON.stringify(d, null, 2) + "\n```" },
47
+ ],
48
+ };
49
+ }
50
+ //# sourceMappingURL=rapPayment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rapPayment.js","sourceRoot":"","sources":["../../src/tools/rapPayment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC;SAC5C,QAAQ,CAAC,gIAAgI,CAAC;IAC7I,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;SACrH,QAAQ,CAAC,oBAAoB,CAAC;IACjC,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,QAAQ,EAAE;SAC7D,QAAQ,CAAC,2EAA2E,CAAC;IACxF,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;SACnD,QAAQ,CAAC,6HAA6H,CAAC;CAC3I,CAAC;AASF,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,CACxB,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,EAAE,CAAC,CAAC;AAE9F,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAA6B;IAC5D,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAc,iCAAiC,EAAE,IAAI,CAAC,CAAC;IAEpF,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACvF,CAAC;IAED,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IACtB,MAAM,IAAI,GAAG;QACX,sDAAsD,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI;QAC/E,EAAE;QACF,eAAe,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,YAAY,KAAK,sBAAsB,CAAC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC,EAAE,EAAE;QAChI,oCAAoC,CAAC,CAAC,UAAU,EAAE;QAClD,oBAAoB,CAAC,CAAC,YAAY,EAAE;QACpC,EAAE;QACF,sFAAsF;YACtF,sFAAsF;YACtF,sFAAsF;YACtF,6EAA6E;KAC9E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEb,OAAO;QACL,OAAO,EAAE;YACP,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,GAAG,GAAG,EAAE;YAC3C,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE;SACpF;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@finology/mcp-server",
3
+ "mcpName": "tech.finology/student-loan",
4
+ "version": "0.1.0",
5
+ "description": "Deterministic 2026 federal student loan repayment math for AI agents. RAP, IBR, ICR, PAYE, SAVE and PSLF calculations from Finology Software's parity-verified engine, so assistants stop guessing at federal repayment rules.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "finology-mcp": "dist/index.js"
10
+ },
11
+ "main": "dist/index.js",
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "keywords": [
18
+ "mcp",
19
+ "modelcontextprotocol",
20
+ "student-loan",
21
+ "student-debt",
22
+ "federal-student-loan",
23
+ "rap",
24
+ "repayment-assistance-plan",
25
+ "ibr",
26
+ "income-driven-repayment",
27
+ "pslf",
28
+ "loan-forgiveness",
29
+ "nslds",
30
+ "financial-planning",
31
+ "cfp",
32
+ "fintech"
33
+ ],
34
+ "homepage": "https://finology.tech",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/Finology-tech/finology-mcp.git"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc",
44
+ "dev": "tsx src/index.ts",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.0.0",
49
+ "zod": "^3.23.8"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.0.0",
53
+ "tsx": "^4.19.0",
54
+ "typescript": "^5.6.0"
55
+ }
56
+ }