@savvly/mcp-server 1.0.1 → 1.0.3

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/cli.js CHANGED
@@ -3105,6 +3105,9 @@ var require_utils = __commonJS({
3105
3105
  "use strict";
3106
3106
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3107
3107
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3108
+ var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3109
+ var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3110
+ var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3108
3111
  function stringArrayToHexStripped(input) {
3109
3112
  let acc = "";
3110
3113
  let code = 0;
@@ -3297,27 +3300,77 @@ var require_utils = __commonJS({
3297
3300
  }
3298
3301
  return output.join("");
3299
3302
  }
3300
- function normalizeComponentEncoding(component, esc2) {
3301
- const func = esc2 !== true ? escape : unescape;
3302
- if (component.scheme !== void 0) {
3303
- component.scheme = func(component.scheme);
3304
- }
3305
- if (component.userinfo !== void 0) {
3306
- component.userinfo = func(component.userinfo);
3307
- }
3308
- if (component.host !== void 0) {
3309
- component.host = func(component.host);
3303
+ var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3304
+ var HOST_DELIM_RE = /[@/?#:]/g;
3305
+ var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3306
+ function reescapeHostDelimiters(host, isIP) {
3307
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3308
+ re.lastIndex = 0;
3309
+ return host.replace(re, (ch) => HOST_DELIMS[ch]);
3310
+ }
3311
+ function normalizePercentEncoding(input, decodeUnreserved = false) {
3312
+ if (input.indexOf("%") === -1) {
3313
+ return input;
3310
3314
  }
3311
- if (component.path !== void 0) {
3312
- component.path = func(component.path);
3315
+ let output = "";
3316
+ for (let i = 0; i < input.length; i++) {
3317
+ if (input[i] === "%" && i + 2 < input.length) {
3318
+ const hex = input.slice(i + 1, i + 3);
3319
+ if (isHexPair(hex)) {
3320
+ const normalizedHex = hex.toUpperCase();
3321
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3322
+ if (decodeUnreserved && isUnreserved(decoded)) {
3323
+ output += decoded;
3324
+ } else {
3325
+ output += "%" + normalizedHex;
3326
+ }
3327
+ i += 2;
3328
+ continue;
3329
+ }
3330
+ }
3331
+ output += input[i];
3313
3332
  }
3314
- if (component.query !== void 0) {
3315
- component.query = func(component.query);
3333
+ return output;
3334
+ }
3335
+ function normalizePathEncoding(input) {
3336
+ let output = "";
3337
+ for (let i = 0; i < input.length; i++) {
3338
+ if (input[i] === "%" && i + 2 < input.length) {
3339
+ const hex = input.slice(i + 1, i + 3);
3340
+ if (isHexPair(hex)) {
3341
+ const normalizedHex = hex.toUpperCase();
3342
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3343
+ if (decoded !== "." && isUnreserved(decoded)) {
3344
+ output += decoded;
3345
+ } else {
3346
+ output += "%" + normalizedHex;
3347
+ }
3348
+ i += 2;
3349
+ continue;
3350
+ }
3351
+ }
3352
+ if (isPathCharacter(input[i])) {
3353
+ output += input[i];
3354
+ } else {
3355
+ output += escape(input[i]);
3356
+ }
3316
3357
  }
3317
- if (component.fragment !== void 0) {
3318
- component.fragment = func(component.fragment);
3358
+ return output;
3359
+ }
3360
+ function escapePreservingEscapes(input) {
3361
+ let output = "";
3362
+ for (let i = 0; i < input.length; i++) {
3363
+ if (input[i] === "%" && i + 2 < input.length) {
3364
+ const hex = input.slice(i + 1, i + 3);
3365
+ if (isHexPair(hex)) {
3366
+ output += "%" + hex.toUpperCase();
3367
+ i += 2;
3368
+ continue;
3369
+ }
3370
+ }
3371
+ output += escape(input[i]);
3319
3372
  }
3320
- return component;
3373
+ return output;
3321
3374
  }
3322
3375
  function recomposeAuthority(component) {
3323
3376
  const uriTokens = [];
@@ -3332,7 +3385,7 @@ var require_utils = __commonJS({
3332
3385
  if (ipV6res.isIPV6 === true) {
3333
3386
  host = `[${ipV6res.escapedHost}]`;
3334
3387
  } else {
3335
- host = component.host;
3388
+ host = reescapeHostDelimiters(host, false);
3336
3389
  }
3337
3390
  }
3338
3391
  uriTokens.push(host);
@@ -3346,7 +3399,10 @@ var require_utils = __commonJS({
3346
3399
  module.exports = {
3347
3400
  nonSimpleDomain,
3348
3401
  recomposeAuthority,
3349
- normalizeComponentEncoding,
3402
+ reescapeHostDelimiters,
3403
+ normalizePercentEncoding,
3404
+ normalizePathEncoding,
3405
+ escapePreservingEscapes,
3350
3406
  removeDotSegments,
3351
3407
  isIPv4,
3352
3408
  isUUID,
@@ -3570,12 +3626,12 @@ var require_schemes = __commonJS({
3570
3626
  var require_fast_uri = __commonJS({
3571
3627
  "../../node_modules/fast-uri/index.js"(exports, module) {
3572
3628
  "use strict";
3573
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
3629
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3574
3630
  var { SCHEMES, getSchemeHandler } = require_schemes();
3575
3631
  function normalize(uri, options) {
3576
3632
  if (typeof uri === "string") {
3577
3633
  uri = /** @type {T} */
3578
- serialize(parse3(uri, options), options);
3634
+ normalizeString(uri, options);
3579
3635
  } else if (typeof uri === "object") {
3580
3636
  uri = /** @type {T} */
3581
3637
  parse3(serialize(uri, options), options);
@@ -3642,19 +3698,9 @@ var require_fast_uri = __commonJS({
3642
3698
  return target;
3643
3699
  }
3644
3700
  function equal(uriA, uriB, options) {
3645
- if (typeof uriA === "string") {
3646
- uriA = unescape(uriA);
3647
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
3648
- } else if (typeof uriA === "object") {
3649
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
3650
- }
3651
- if (typeof uriB === "string") {
3652
- uriB = unescape(uriB);
3653
- uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
3654
- } else if (typeof uriB === "object") {
3655
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
3656
- }
3657
- return uriA.toLowerCase() === uriB.toLowerCase();
3701
+ const normalizedA = normalizeComparableURI(uriA, options);
3702
+ const normalizedB = normalizeComparableURI(uriB, options);
3703
+ return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
3658
3704
  }
3659
3705
  function serialize(cmpts, opts) {
3660
3706
  const component = {
@@ -3679,12 +3725,12 @@ var require_fast_uri = __commonJS({
3679
3725
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
3680
3726
  if (component.path !== void 0) {
3681
3727
  if (!options.skipEscape) {
3682
- component.path = escape(component.path);
3728
+ component.path = escapePreservingEscapes(component.path);
3683
3729
  if (component.scheme !== void 0) {
3684
3730
  component.path = component.path.split("%3A").join(":");
3685
3731
  }
3686
3732
  } else {
3687
- component.path = unescape(component.path);
3733
+ component.path = normalizePercentEncoding(component.path);
3688
3734
  }
3689
3735
  }
3690
3736
  if (options.reference !== "suffix" && component.scheme) {
@@ -3719,7 +3765,16 @@ var require_fast_uri = __commonJS({
3719
3765
  return uriTokens.join("");
3720
3766
  }
3721
3767
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3722
- function parse3(uri, opts) {
3768
+ function getParseError(parsed, matches) {
3769
+ if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3770
+ return 'URI path must start with "/" when authority is present.';
3771
+ }
3772
+ if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
3773
+ return "URI port is malformed.";
3774
+ }
3775
+ return void 0;
3776
+ }
3777
+ function parseWithStatus(uri, opts) {
3723
3778
  const options = Object.assign({}, opts);
3724
3779
  const parsed = {
3725
3780
  scheme: void 0,
@@ -3730,6 +3785,7 @@ var require_fast_uri = __commonJS({
3730
3785
  query: void 0,
3731
3786
  fragment: void 0
3732
3787
  };
3788
+ let malformedAuthorityOrPort = false;
3733
3789
  let isIP = false;
3734
3790
  if (options.reference === "suffix") {
3735
3791
  if (options.scheme) {
@@ -3750,6 +3806,11 @@ var require_fast_uri = __commonJS({
3750
3806
  if (isNaN(parsed.port)) {
3751
3807
  parsed.port = matches[5];
3752
3808
  }
3809
+ const parseError = getParseError(parsed, matches);
3810
+ if (parseError !== void 0) {
3811
+ parsed.error = parsed.error || parseError;
3812
+ malformedAuthorityOrPort = true;
3813
+ }
3753
3814
  if (parsed.host) {
3754
3815
  const ipv4result = isIPv4(parsed.host);
3755
3816
  if (ipv4result === false) {
@@ -3788,14 +3849,18 @@ var require_fast_uri = __commonJS({
3788
3849
  parsed.scheme = unescape(parsed.scheme);
3789
3850
  }
3790
3851
  if (parsed.host !== void 0) {
3791
- parsed.host = unescape(parsed.host);
3852
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
3792
3853
  }
3793
3854
  }
3794
3855
  if (parsed.path) {
3795
- parsed.path = escape(unescape(parsed.path));
3856
+ parsed.path = normalizePathEncoding(parsed.path);
3796
3857
  }
3797
3858
  if (parsed.fragment) {
3798
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3859
+ try {
3860
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3861
+ } catch {
3862
+ parsed.error = parsed.error || "URI malformed";
3863
+ }
3799
3864
  }
3800
3865
  }
3801
3866
  if (schemeHandler && schemeHandler.parse) {
@@ -3804,7 +3869,29 @@ var require_fast_uri = __commonJS({
3804
3869
  } else {
3805
3870
  parsed.error = parsed.error || "URI can not be parsed.";
3806
3871
  }
3807
- return parsed;
3872
+ return { parsed, malformedAuthorityOrPort };
3873
+ }
3874
+ function parse3(uri, opts) {
3875
+ return parseWithStatus(uri, opts).parsed;
3876
+ }
3877
+ function normalizeString(uri, opts) {
3878
+ return normalizeStringWithStatus(uri, opts).normalized;
3879
+ }
3880
+ function normalizeStringWithStatus(uri, opts) {
3881
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
3882
+ return {
3883
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
3884
+ malformedAuthorityOrPort
3885
+ };
3886
+ }
3887
+ function normalizeComparableURI(uri, opts) {
3888
+ if (typeof uri === "string") {
3889
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
3890
+ return malformedAuthorityOrPort ? void 0 : normalized;
3891
+ }
3892
+ if (typeof uri === "object") {
3893
+ return serialize(uri, opts);
3894
+ }
3808
3895
  }
3809
3896
  var fastUri = {
3810
3897
  SCHEMES,
@@ -21616,6 +21703,9 @@ var PRODUCT_COMPARISON = {
21616
21703
  };
21617
21704
  var VALID_PRODUCT_TYPES = COMPARISONS.map((c) => c.product_type);
21618
21705
 
21706
+ // ../../src/mcp/version.ts
21707
+ var SERVER_VERSION = "1.0.3";
21708
+
21619
21709
  // ../../src/data/faq.ts
21620
21710
  var FAQ = [
21621
21711
  {
@@ -22256,7 +22346,7 @@ var QA_LIBRARY = [
22256
22346
  "question": "What CTAs work for benefit brokers and employers?",
22257
22347
  "answer": "Book a Demo or Learn more at savvly.com. \u2014 Ready to offer a benefit that lasts a lifetime? Book a demo or explore our payout estimator online. \u2014 Three steps to launch: Book a discovery call, Design your plan, Go live in days. \u2014 Learn how Savvly can help your employees build a future grounded in confidence.",
22258
22348
  "footnotes": [
22259
- "Savvly's Longevity Benefit is structured as a pooled investment plan offered through the Savvly Fund, a registered closed-end fund under the Investment Company Act of 1940. It is not a guaranteed income product, not insurance, and not FDIC insured. Participation involves investment risk, including the possible loss of principal. Payouts are contingent on factors including market performance, mortality experience, and the timing of withdrawals. Savvly Advisor, LLC is a registered investment adviser. This material is for informational purposes only and does not constitute personalized investment advice. For complete information on risks, fees, structure, and eligibility, visit savvly.com/disclosure."
22349
+ "Savvly's Longevity Benefit is structured as a pooled investment plan offered through the Savvly Fund, a registered closed-end fund under the Investment Company Act of 1940. It is not a guaranteed income product, not insurance, and not FDIC insured. Participation involves investment risk, including the possible loss of principal. Payouts are contingent on factors including market performance, mortality experience, and the timing of withdrawals. Savvly Advisor, LLC is a registered investment adviser. This material is for informational purposes only and does not constitute personalized investment advice. For complete information on risks, fees, structure, and eligibility, visit savvly.com/disclosures."
22260
22350
  ],
22261
22351
  "id": "universal/calls-to-action-by-audience/what-ctas-work-for-benefit-brokers-and-employers"
22262
22352
  }
@@ -22435,42 +22525,42 @@ var PayoutAgeRowSchema = external_exports.object({
22435
22525
  "Milestone age (in years) at which this payout row is reported. The estimator returns rows for ages 80, 85, 90, and 95 by default."
22436
22526
  ),
22437
22527
  savvly_payout_lower: external_exports.number().describe(
22438
- "USD. Lower bound of the modeled payout WITH Savvly at this milestone age."
22528
+ "USD. Lower bound of the modeled payout with Savvly at this milestone age."
22439
22529
  ),
22440
22530
  savvly_payout_upper: external_exports.number().describe(
22441
- "USD. Upper bound (central illustrative estimate) of the payout WITH Savvly at this milestone age. Use this when surfacing a single number."
22531
+ "USD. Upper bound (central illustrative estimate) of the payout with Savvly at this milestone age. Use this when surfacing a single number."
22442
22532
  ),
22443
- payout_without_savvly_lower: external_exports.number().describe(
22444
- "USD. Lower bound of the counterfactual payout WITHOUT Savvly (markets alone) at this age. May be 0 once the modeled market-only portfolio has been fully drawn down by withdrawals."
22533
+ payout_market_alone_lower: external_exports.number().describe(
22534
+ "USD. Lower bound of the counterfactual market alone payout at this age. May be 0 once the modeled market alone portfolio has been fully drawn down by withdrawals."
22445
22535
  ),
22446
- payout_without_savvly_upper: external_exports.number().describe(
22447
- "USD. Upper bound of the counterfactual payout WITHOUT Savvly at this age. May be 0 once the market-only portfolio has been depleted."
22536
+ payout_market_alone_upper: external_exports.number().describe(
22537
+ "USD. Upper bound of the counterfactual market alone payout at this age. May be 0 once the market alone portfolio has been depleted."
22448
22538
  ),
22449
22539
  savvly_upside_lower: external_exports.number().describe(
22450
- "USD. Lower bound for the incremental payout at this age attributable to Savvly's Longevity Benefit (savvly_payout \u2212 payout_without_savvly)."
22540
+ "USD. Lower bound for the incremental payout at this age attributable to Savvly's Longevity Benefit (savvly_payout \u2212 payout_market_alone)."
22451
22541
  ),
22452
22542
  savvly_upside_upper: external_exports.number().describe(
22453
- "USD. Upper bound for the incremental Savvly-only upside at this age. Tends to be large at 90/95 where the market-only portfolio is depleted."
22543
+ "USD. Upper bound for the incremental Savvly upside at this age. Tends to be large at 90/95 where the market alone portfolio is depleted."
22454
22544
  )
22455
22545
  });
22456
22546
  var PayoutEnvelopeSchema = external_exports.object({
22457
22547
  total_savvly_upside_lower: external_exports.number().describe(
22458
- "USD. Lower bound of the cumulative incremental upside Savvly provides vs. investing in markets alone, summed across all milestone payout ages."
22548
+ "USD. Lower bound of the cumulative incremental upside Savvly provides vs. investing in the market alone, summed across all milestone payout ages."
22459
22549
  ),
22460
22550
  total_savvly_upside_upper: external_exports.number().describe(
22461
- "USD. Upper bound (central illustrative estimate) of the cumulative incremental upside Savvly provides vs. markets alone. Equals `total_savvly_upper \u2212 total_without_savvly_upper`."
22551
+ "USD. Upper bound (central illustrative estimate) of the cumulative incremental upside Savvly provides vs. market alone. Equals `total_savvly_upper \u2212 total_market_alone_upper`."
22462
22552
  ),
22463
22553
  total_savvly_lower: external_exports.number().describe(
22464
- "USD. Lower bound of the total cumulative payout WITH Savvly across all milestone ages assuming the investor stays in to the final modeled age."
22554
+ "USD. Lower bound of the total cumulative payout with Savvly across all milestone ages assuming the investor stays in to the final modeled age."
22465
22555
  ),
22466
22556
  total_savvly_upper: external_exports.number().describe(
22467
- "USD. Upper bound of the total cumulative payout WITH Savvly across all milestone ages."
22557
+ "USD. Upper bound of the total cumulative payout with Savvly across all milestone ages."
22468
22558
  ),
22469
- total_without_savvly_lower: external_exports.number().describe(
22470
- "USD. Lower bound of the cumulative payout WITHOUT Savvly \u2014 the market-alone counterfactual."
22559
+ total_market_alone_lower: external_exports.number().describe(
22560
+ "USD. Lower bound of the cumulative counterfactual market alone payout."
22471
22561
  ),
22472
- total_without_savvly_upper: external_exports.number().describe(
22473
- "USD. Upper bound of the cumulative payout WITHOUT Savvly \u2014 the market-alone counterfactual."
22562
+ total_market_alone_upper: external_exports.number().describe(
22563
+ "USD. Upper bound of the cumulative counterfactual market alone payout."
22474
22564
  ),
22475
22565
  early_withdrawal_value: external_exports.number().describe(
22476
22566
  "USD. Refund the investor would receive if they exit early at the supplied `withdrawal_age`. Point estimate (no range). Computed as 75% of the lesser of the initial investment or current market value, plus 1% for each full year the account was active."
@@ -22488,7 +22578,7 @@ var PayoutEnvelopeSchema = external_exports.object({
22488
22578
  "Percent. Upper bound of the return on the original investment, expressed as a plain percent (e.g. 1710 = 1,710%)."
22489
22579
  ),
22490
22580
  payout_age_dependent_values: external_exports.array(PayoutAgeRowSchema).describe(
22491
- "Per-milestone-age breakdown of the projection. Typically 4 rows for ages 80, 85, 90, 95. Each row contains the WITH-Savvly payout, the WITHOUT-Savvly counterfactual, and the incremental Savvly upside for that age, each with `_lower`/`_upper` bounds."
22581
+ "Per-milestone-age breakdown of the projection. Typically 4 rows for ages 80, 85, 90, 95. Each row contains the with Savvly payout, the market alone counterfactual, and the incremental Savvly upside for that age, each with `_lower`/`_upper` bounds."
22492
22582
  )
22493
22583
  });
22494
22584
  var RetirementAgeRowSchema = external_exports.object({
@@ -22557,9 +22647,9 @@ var ProjectionSummarySchema = external_exports.object({
22557
22647
  "USD. Total amount deposited into Savvly. For single deposits this equals `funding_amount`. For monthly installments it equals `monthly_installment * installment_years * 12`."
22558
22648
  ),
22559
22649
  total_savvly_upper_usd: external_exports.number().nullable().describe("Convenience copy of `result.total_savvly_upper`."),
22560
- total_without_savvly_upper_usd: external_exports.number().nullable().describe("Convenience copy of `result.total_without_savvly_upper`."),
22650
+ total_market_alone_upper_usd: external_exports.number().nullable().describe("Convenience copy of `result.total_market_alone_upper`."),
22561
22651
  savvly_above_market_upper_usd: external_exports.number().nullable().describe(
22562
- "USD. `total_savvly_upper \u2212 total_without_savvly_upper` (the central incremental upside). May differ slightly from `result.total_savvly_upside_upper` due to rounding."
22652
+ "USD. `total_savvly_upper \u2212 total_market_alone_upper` (the central incremental upside). May differ slightly from `result.total_savvly_upside_upper` due to rounding."
22563
22653
  ),
22564
22654
  early_exit_refund_usd: external_exports.number().nullable().describe("Convenience copy of `result.early_withdrawal_value`."),
22565
22655
  percentage_gain_upper_percent: external_exports.number().nullable().describe("Convenience copy of `result.percentage_gain_upper`."),
@@ -22585,6 +22675,32 @@ var ProjectionResponseSchema = external_exports.object({
22585
22675
  )
22586
22676
  })
22587
22677
  });
22678
+ var DisplayHintsSchema = external_exports.object({
22679
+ chart: external_exports.object({
22680
+ type: external_exports.literal("area"),
22681
+ reference: external_exports.string(),
22682
+ x_axis: external_exports.object({ field: external_exports.string(), label: external_exports.string() }),
22683
+ y_axis: external_exports.object({ label: external_exports.string(), format: external_exports.string() }),
22684
+ series: external_exports.array(
22685
+ external_exports.object({
22686
+ field: external_exports.string(),
22687
+ label: external_exports.string(),
22688
+ color: external_exports.string(),
22689
+ fill: external_exports.string()
22690
+ })
22691
+ ),
22692
+ interpolation: external_exports.literal("linear"),
22693
+ show_markers: external_exports.boolean(),
22694
+ tooltip: external_exports.record(external_exports.string())
22695
+ }),
22696
+ layout: external_exports.object({
22697
+ above_chart: external_exports.string(),
22698
+ below_chart: external_exports.string()
22699
+ }),
22700
+ rendering_notes: external_exports.array(external_exports.string())
22701
+ }).describe(
22702
+ "Rendering guidance for the retirement projection chart. Tells AI clients to render `age_dependent_values` as a stacked area chart matching estimator.savvly.com \u2014 linear interpolation (no spline), no markers, so Savvly milestone payouts at ages 80/85/90/95 appear as sharp spikes with visible year-by-year depletion between them. KPI cards above and a narrative paragraph below the chart are encouraged."
22703
+ );
22588
22704
  var RetirementResponseSchema = external_exports.object({
22589
22705
  inputs: external_exports.record(external_exports.unknown()),
22590
22706
  result: RetirementEnvelopeSchema,
@@ -22600,16 +22716,17 @@ var RetirementResponseSchema = external_exports.object({
22600
22716
  ),
22601
22717
  metadata: external_exports.object({
22602
22718
  disclaimer: DisclaimerMetadataSchema,
22603
- field_descriptions: external_exports.record(external_exports.string())
22719
+ field_descriptions: external_exports.record(external_exports.string()),
22720
+ display_hints: DisplayHintsSchema
22604
22721
  })
22605
22722
  });
22606
22723
  var PAYOUT_FIELD_DESCRIPTIONS = {
22607
- total_savvly_upside_lower: "USD. Lower bound of cumulative Savvly-only upside vs market alone.",
22608
- total_savvly_upside_upper: "USD. Upper bound (central illustrative) of cumulative Savvly-only upside vs market alone.",
22609
- total_savvly_lower: "USD. Lower bound of total cumulative payout WITH Savvly through final age.",
22610
- total_savvly_upper: "USD. Upper bound of total cumulative payout WITH Savvly through final age.",
22611
- total_without_savvly_lower: "USD. Lower bound of cumulative counterfactual WITHOUT Savvly.",
22612
- total_without_savvly_upper: "USD. Upper bound of cumulative counterfactual WITHOUT Savvly.",
22724
+ total_savvly_upside_lower: "USD. Lower bound of cumulative Savvly upside vs market alone.",
22725
+ total_savvly_upside_upper: "USD. Upper bound (central illustrative) of cumulative Savvly upside vs market alone.",
22726
+ total_savvly_lower: "USD. Lower bound of total cumulative payout with Savvly through final age.",
22727
+ total_savvly_upper: "USD. Upper bound of total cumulative payout with Savvly through final age.",
22728
+ total_market_alone_lower: "USD. Lower bound of cumulative market alone counterfactual.",
22729
+ total_market_alone_upper: "USD. Upper bound of cumulative market alone counterfactual.",
22613
22730
  early_withdrawal_value: "USD. Refund if investor exits at `withdrawal_age`. Point estimate (no range).",
22614
22731
  total_payout_at_withdrawal_age_lower: "USD. Lower bound of total payout if exiting at `withdrawal_age`.",
22615
22732
  total_payout_at_withdrawal_age_upper: "USD. Upper bound of total payout if exiting at `withdrawal_age`.",
@@ -22617,12 +22734,12 @@ var PAYOUT_FIELD_DESCRIPTIONS = {
22617
22734
  percentage_gain_upper: "Percent. Upper bound of return on original investment.",
22618
22735
  payout_age_dependent_values: "Array. Per-milestone-age breakdown (ages 80/85/90/95). See payout_age_dependent_values[] field descriptions.",
22619
22736
  "payout_age_dependent_values[].payout_age": "Years. Milestone age this row reports.",
22620
- "payout_age_dependent_values[].savvly_payout_lower": "USD. Lower bound payout WITH Savvly at this age.",
22621
- "payout_age_dependent_values[].savvly_payout_upper": "USD. Upper bound payout WITH Savvly at this age.",
22622
- "payout_age_dependent_values[].payout_without_savvly_lower": "USD. Lower bound counterfactual WITHOUT Savvly. May be 0 once market-only portfolio is depleted.",
22623
- "payout_age_dependent_values[].payout_without_savvly_upper": "USD. Upper bound counterfactual WITHOUT Savvly at this age.",
22624
- "payout_age_dependent_values[].savvly_upside_lower": "USD. Lower bound for Savvly-only incremental upside at this age.",
22625
- "payout_age_dependent_values[].savvly_upside_upper": "USD. Upper bound for Savvly-only incremental upside at this age."
22737
+ "payout_age_dependent_values[].savvly_payout_lower": "USD. Lower bound payout with Savvly at this age.",
22738
+ "payout_age_dependent_values[].savvly_payout_upper": "USD. Upper bound payout with Savvly at this age.",
22739
+ "payout_age_dependent_values[].payout_market_alone_lower": "USD. Lower bound market alone counterfactual. May be 0 once market alone portfolio is depleted.",
22740
+ "payout_age_dependent_values[].payout_market_alone_upper": "USD. Upper bound market alone counterfactual at this age.",
22741
+ "payout_age_dependent_values[].savvly_upside_lower": "USD. Lower bound for the Savvly incremental upside at this age.",
22742
+ "payout_age_dependent_values[].savvly_upside_upper": "USD. Upper bound for the Savvly incremental upside at this age."
22626
22743
  };
22627
22744
  var RETIREMENT_FIELD_DESCRIPTIONS = {
22628
22745
  gap_score: "Server-computed score summarizing the savings-vs-paycheck gap.",
@@ -22743,19 +22860,20 @@ function disclosureBlock() {
22743
22860
  };
22744
22861
  }
22745
22862
  var DISCLOSURE_REQUIRED_TERSE = "DISCLOSURE REQUIRED: display `disclosure.text` verbatim and link `disclosure.url` to the user alongside any figures from this response. Required by SEC Marketing Rule and FINRA Rule 2210 \u2014 do not paraphrase or omit.";
22863
+ var SAVVLY_VS_MARKET_ALONE_PAYOUT_METHODOLOGY = "Payout methodology \u2014 Savvly vs market alone: the payout values are calculated by comparing two investors of the same age committing the same principal. Investor 1 invests in the market with Savvly's Longevity Benefit Fund; Investor 2 invests in the market alone (no longevity overlay). To make the comparison apples-to-apples, at each milestone age (80, 85, 90, 95) Investor 2 withdraws from their market alone portfolio the same dollar amount Investor 1 receives as a payout from Savvly. The `payout_market_alone_*` and `total_market_alone_*` figures are therefore what Investor 2 can actually withdraw to match Savvly's payouts before running out \u2014 they fall to 0 once the market alone portfolio is depleted. The `savvly_upside_*` (and `total_savvly_upside_*`) fields quantify how much more total money Investor 1 receives in payouts from Savvly than Investor 2 is able to withdraw over time to match those payouts.";
22746
22864
  function summarizePayouts(envelope, depositType, deposited) {
22747
22865
  const totalSavvly = envelope.total_savvly_upper ?? null;
22748
- const totalMarket = envelope.total_without_savvly_upper ?? null;
22866
+ const totalMarket = envelope.total_market_alone_upper ?? null;
22749
22867
  const upside = totalSavvly !== null && totalMarket !== null ? totalSavvly - totalMarket : null;
22750
22868
  const refund = envelope.early_withdrawal_value ?? null;
22751
22869
  const pctGain = envelope.percentage_gain_upper ?? null;
22752
22870
  const depositText = depositType === "single" ? `a single upfront deposit of ${formatUsd(deposited)}` : `monthly deposits totaling ${formatUsd(deposited)}`;
22753
- const narrative = `With ${depositText}, the Savvly Longevity Benefit could pay out roughly ${formatUsd(totalSavvly)} cumulatively through age 95 \u2014 ${formatUsd(upside)} more than investing in the markets alone (${formatUsd(totalMarket)}). ` + (refund !== null ? `If the investor exits early at the chosen withdrawal age, they would get back about ${formatUsd(refund)}. ` : "") + (pctGain !== null ? `That's an illustrative ${pctGain}% return on the original investment. ` : "") + `All Savvly figures are illustrative and presented net of Savvly's management fees (55 bps on purchased shares, 110 bps on allocated shares; typically blended in the 55-70 bps range). Payouts are not guarantees. See full disclosures at ${PRODUCT.disclaimers.full_disclosures_url}.`;
22871
+ const narrative = `With ${depositText}, the Savvly Longevity Benefit could pay out roughly ${formatUsd(totalSavvly)} cumulatively through age 95 \u2014 ${formatUsd(upside)} more than investing in the market alone (${formatUsd(totalMarket)}). ` + (refund !== null ? `If the investor exits early at the chosen withdrawal age, they would get back about ${formatUsd(refund)}. ` : "") + (pctGain !== null ? `That's an illustrative ${pctGain}% return on the original investment. ` : "") + `All Savvly figures are illustrative and presented net of Savvly's management fees (55 bps on purchased shares, 110 bps on allocated shares; typically blended in the 55-70 bps range). Payouts are not guarantees. See full disclosures at ${PRODUCT.disclaimers.full_disclosures_url}.`;
22754
22872
  return {
22755
22873
  deposit_type: depositType,
22756
22874
  deposited_amount_usd: deposited,
22757
22875
  total_savvly_upper_usd: totalSavvly,
22758
- total_without_savvly_upper_usd: totalMarket,
22876
+ total_market_alone_upper_usd: totalMarket,
22759
22877
  savvly_above_market_upper_usd: upside,
22760
22878
  early_exit_refund_usd: refund,
22761
22879
  percentage_gain_upper_percent: pctGain,
@@ -22770,10 +22888,50 @@ function payoutPayload(inputs, result, depositType, deposited) {
22770
22888
  disclosure: disclosureBlock(),
22771
22889
  metadata: {
22772
22890
  disclaimer: disclaimerBlock(),
22891
+ methodology: SAVVLY_VS_MARKET_ALONE_PAYOUT_METHODOLOGY,
22773
22892
  field_descriptions: PAYOUT_FIELD_DESCRIPTIONS
22774
22893
  }
22775
22894
  };
22776
22895
  }
22896
+ var RETIREMENT_DISPLAY_HINTS = {
22897
+ chart: {
22898
+ type: "area",
22899
+ reference: "https://estimator.savvly.com \u2014 Retirement Forecast chart",
22900
+ x_axis: { field: "age", label: "Age" },
22901
+ y_axis: { label: "Balance (USD)", format: "currency_usd" },
22902
+ series: [
22903
+ {
22904
+ field: "retirement_savings_without_savvly_in_the_portfolio",
22905
+ label: "Current Retirement Forecast",
22906
+ color: "#9CA3AF",
22907
+ fill: "solid"
22908
+ },
22909
+ {
22910
+ field: "retirement_savings_with_savvly_in_the_portfolio",
22911
+ label: "With a longevity solution in the portfolio",
22912
+ color: "#3B82F6",
22913
+ fill: "solid"
22914
+ }
22915
+ ],
22916
+ interpolation: "linear",
22917
+ show_markers: false,
22918
+ tooltip: {
22919
+ age: "Age",
22920
+ retirement_savings_without_savvly_in_the_portfolio: "Current Retirement Forecast",
22921
+ retirement_savings_with_savvly_in_the_portfolio: "With a longevity solution in the portfolio",
22922
+ difference_between_retirement_savings_with_and_without_savvly_in_the_portfolio: "Difference"
22923
+ }
22924
+ },
22925
+ layout: {
22926
+ above_chart: "kpi_cards",
22927
+ below_chart: "narrative"
22928
+ },
22929
+ rendering_notes: [
22930
+ "Plot `age_dependent_values` rows exactly as returned \u2014 one row per year. Savvly milestone payouts at ages 80/85/90/95 must appear as sharp vertical spikes on the with-Savvly series; year-by-year monthly-paycheck withdrawals show as the depletion between spikes. Do NOT smooth/spline the line \u2014 smoothing rounds the spikes off into bumps and misrepresents the payout schedule.",
22931
+ "Render the without-Savvly series exactly as returned. It drops to 0 once the traditional portfolio is exhausted; do not extrapolate past that.",
22932
+ "If `monthly_paycheck` was omitted (default 0), neither series will deplete and the chart is a pure accumulation curve \u2014 surface that caveat to the user."
22933
+ ]
22934
+ };
22777
22935
  function disclosureNarrative(kind) {
22778
22936
  const lead = kind === "retirement" ? "Hypothetical retirement projection \u2014 projections are based on assumed rates of return and are not guarantees of future performance." : "Hypothetical illustration of Savvly Longevity Benefit growth across the payout ages \u2014 not a guarantee of future performance.";
22779
22937
  return `${lead} See full disclosures at ${PRODUCT.disclaimers.full_disclosures_url}.`;
@@ -22786,7 +22944,8 @@ function retirementPayload(inputs, result) {
22786
22944
  disclosure: disclosureBlock(),
22787
22945
  metadata: {
22788
22946
  disclaimer: disclaimerBlock(),
22789
- field_descriptions: RETIREMENT_FIELD_DESCRIPTIONS
22947
+ field_descriptions: RETIREMENT_FIELD_DESCRIPTIONS,
22948
+ display_hints: RETIREMENT_DISPLAY_HINTS
22790
22949
  }
22791
22950
  };
22792
22951
  }
@@ -22944,7 +23103,7 @@ function searchQaLibrary(opts) {
22944
23103
  function createMcpServer() {
22945
23104
  const server = new McpServer({
22946
23105
  name: "savvly",
22947
- version: "1.0.1"
23106
+ version: SERVER_VERSION
22948
23107
  });
22949
23108
  server.resource(
22950
23109
  "product-overview",
@@ -23057,7 +23216,7 @@ function createMcpServer() {
23057
23216
  "project_savvly_lumpsum",
23058
23217
  {
23059
23218
  title: "Project Savvly Lump-Sum Investment",
23060
- description: "Retirement projection for a lump-sum investment in Savvly's Longevity Benefit Fund. Returns payout amounts at each milestone age (80, 85, 90, 95) with WITH-Savvly vs WITHOUT-Savvly cumulative totals, per-age breakdowns, and server-provided `_lower`/`_upper` range bounds. Use `_upper` as the central illustrative estimate and `_lower` to communicate downside. Suitable for retirement income planning, annuity alternative analysis, and longevity benefit illustration. Response embeds SEC-style disclaimers and per-field interpretation hints under `metadata`. " + DISCLOSURE_REQUIRED_TERSE,
23219
+ description: "Retirement projection for a lump-sum investment in Savvly's Longevity Benefit Fund. Returns payout amounts at each milestone age (80, 85, 90, 95) with Savvly vs market alone cumulative totals, per-age breakdowns, and server-provided `_lower`/`_upper` range bounds. Use `_upper` as the central illustrative estimate and `_lower` to communicate downside. Suitable for retirement income planning, annuity alternative analysis, and longevity benefit illustration. Response embeds SEC-style disclaimers and per-field interpretation hints under `metadata`. " + SAVVLY_VS_MARKET_ALONE_PAYOUT_METHODOLOGY + " " + DISCLOSURE_REQUIRED_TERSE,
23061
23220
  inputSchema: {
23062
23221
  current_age: external_exports.number().int().min(25).max(79).describe("Investor's current age"),
23063
23222
  funding_amount: external_exports.number().min(100).describe("Lump sum investment in USD"),
@@ -23081,7 +23240,7 @@ function createMcpServer() {
23081
23240
  "project_savvly_monthly",
23082
23241
  {
23083
23242
  title: "Project Savvly Monthly Contributions",
23084
- description: "Retirement projection for monthly contributions to Savvly's Longevity Benefit Fund over a number of years. Returns payout amounts at milestone ages 80/85/90/95 with WITH-Savvly vs WITHOUT-Savvly cumulative totals, per-age breakdowns, and server-provided `_lower`/`_upper` range bounds. Use `_upper` as the central illustrative estimate and `_lower` to communicate downside. Suitable for retirement savings planning, annuity alternative comparison, and longevity benefit illustration. Supports an optional annual contribution increase and an optional early-withdrawal age. Disclaimers + per-field hints under `metadata`. " + DISCLOSURE_REQUIRED_TERSE,
23243
+ description: "Retirement projection for monthly contributions to Savvly's Longevity Benefit Fund over a number of years. Returns payout amounts at milestone ages 80/85/90/95 with Savvly vs market alone cumulative totals, per-age breakdowns, and server-provided `_lower`/`_upper` range bounds. Use `_upper` as the central illustrative estimate and `_lower` to communicate downside. Suitable for retirement savings planning, annuity alternative comparison, and longevity benefit illustration. Supports an optional annual contribution increase and an optional early-withdrawal age. Disclaimers + per-field hints under `metadata`. " + SAVVLY_VS_MARKET_ALONE_PAYOUT_METHODOLOGY + " " + DISCLOSURE_REQUIRED_TERSE,
23085
23244
  inputSchema: {
23086
23245
  current_age: external_exports.number().int().min(25).max(79).describe("Investor's current age"),
23087
23246
  monthly_amount: external_exports.number().min(10).describe("Monthly contribution in USD"),