From c495fe0958e27db613a54b852449f7a1f5fc1cbe Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 19 Feb 2026 15:01:37 +0000
Subject: [PATCH 01/11] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 5a93d129..b0b4f76e 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 177
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic%2Flithic-c24eebe942f400bff8922a6fbef1ce551ad14f61eb4da21b50d823a62ca42586.yml
openapi_spec_hash: b79ed927e625dedff69cea29131a34d9
-config_hash: 693dddc4721eef512d75ab6c60897794
+config_hash: fbc424e01cca916048d63adcadaa8750
From 6657e0d898b31c05c58a76c6babff472fc1dfbef Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 19 Feb 2026 16:38:14 +0000
Subject: [PATCH 02/11] chore: update mock server docs
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ffcaf95d..11db36d5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -68,7 +68,7 @@ $ pnpm link -—global lithic
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
```sh
-$ npx prism mock path/to/your/openapi.yml
+$ ./scripts/mock
```
```sh
From 342cb0725d8b9d8645e20e7c7ca7a386ce657f36 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 19 Feb 2026 17:49:43 +0000
Subject: [PATCH 03/11] feat(api): Add INTEREST_AND_FEES_PAUSED substatus to
financial account
---
.stats.yml | 4 ++--
src/resources/financial-accounts/financial-accounts.ts | 9 ++++++++-
src/resources/financial-accounts/loan-tapes.ts | 1 +
.../financial-accounts/statements/statements.ts | 1 +
4 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index b0b4f76e..1862b2d1 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 177
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic%2Flithic-c24eebe942f400bff8922a6fbef1ce551ad14f61eb4da21b50d823a62ca42586.yml
-openapi_spec_hash: b79ed927e625dedff69cea29131a34d9
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic%2Flithic-f85b60190db68921a3a877d0dd931670c27933ba1f5031fcdd27365e99adb5c9.yml
+openapi_spec_hash: 4828c2dc7543ce2a39774a9921c73c80
config_hash: fbc424e01cca916048d63adcadaa8750
diff --git a/src/resources/financial-accounts/financial-accounts.ts b/src/resources/financial-accounts/financial-accounts.ts
index f5cbebce..d3d6fd27 100644
--- a/src/resources/financial-accounts/financial-accounts.ts
+++ b/src/resources/financial-accounts/financial-accounts.ts
@@ -221,6 +221,7 @@ export interface FinancialAccount {
| 'END_USER_REQUEST'
| 'BANK_REQUEST'
| 'DELINQUENT'
+ | 'INTEREST_AND_FEES_PAUSED'
| null;
type:
@@ -533,7 +534,13 @@ export interface FinancialAccountUpdateStatusParams {
/**
* Substatus for the financial account
*/
- substatus: 'CHARGED_OFF_FRAUD' | 'END_USER_REQUEST' | 'BANK_REQUEST' | 'CHARGED_OFF_DELINQUENT' | null;
+ substatus:
+ | 'CHARGED_OFF_FRAUD'
+ | 'END_USER_REQUEST'
+ | 'BANK_REQUEST'
+ | 'CHARGED_OFF_DELINQUENT'
+ | 'INTEREST_AND_FEES_PAUSED'
+ | null;
/**
* User-defined status for the financial account
diff --git a/src/resources/financial-accounts/loan-tapes.ts b/src/resources/financial-accounts/loan-tapes.ts
index a03e61cd..e94fa7bf 100644
--- a/src/resources/financial-accounts/loan-tapes.ts
+++ b/src/resources/financial-accounts/loan-tapes.ts
@@ -220,6 +220,7 @@ export namespace LoanTape {
| 'END_USER_REQUEST'
| 'BANK_REQUEST'
| 'DELINQUENT'
+ | 'INTEREST_AND_FEES_PAUSED'
| null;
}
}
diff --git a/src/resources/financial-accounts/statements/statements.ts b/src/resources/financial-accounts/statements/statements.ts
index 2b2d813f..7ed7769d 100644
--- a/src/resources/financial-accounts/statements/statements.ts
+++ b/src/resources/financial-accounts/statements/statements.ts
@@ -233,6 +233,7 @@ export namespace Statement {
| 'END_USER_REQUEST'
| 'BANK_REQUEST'
| 'DELINQUENT'
+ | 'INTEREST_AND_FEES_PAUSED'
| null;
}
}
From 71e0dc6a360f33aacffbd34619282022d8128060 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 19 Feb 2026 22:14:45 +0000
Subject: [PATCH 04/11] chore(mcp): correctly update version in sync with sdk
---
release-please-config.json | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/release-please-config.json b/release-please-config.json
index b1909804..9b042792 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -68,6 +68,11 @@
"type": "json",
"path": "packages/mcp-server/package.json",
"jsonpath": "$.version"
+ },
+ {
+ "type": "json",
+ "path": "packages/mcp-server/manifest.json",
+ "jsonpath": "$.version"
}
]
}
From 49b3676c5eb963fb6608a65b3effb56e2162dc44 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 23 Feb 2026 16:35:32 +0000
Subject: [PATCH 05/11] fix(docs/contributing): correct pnpm link command
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 11db36d5..9e158d16 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -60,7 +60,7 @@ $ yarn link lithic
# With pnpm
$ pnpm link --global
$ cd ../my-package
-$ pnpm link -—global lithic
+$ pnpm link --global lithic
```
## Running tests
From c71b835642458ce89a801ac770b4c5c7e2788a60 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 23 Feb 2026 18:30:18 +0000
Subject: [PATCH 06/11] feat(api): Expose MIL interest schedules and loan tape
configuration endpoints
---
.stats.yml | 8 +-
MIGRATION.md | 4 +
api.md | 26 ++
bin/migration-config.json | 103 ++++++
packages/mcp-server/src/methods.ts | 36 +++
.../financial-accounts/financial-accounts.ts | 42 +++
src/resources/financial-accounts/index.ts | 16 +
.../interest-tier-schedule.ts | 292 ++++++++++++++++++
.../loan-tape-configuration.ts | 75 +++++
.../interest-tier-schedule.test.ts | 123 ++++++++
.../loan-tape-configuration.test.ts | 23 ++
11 files changed, 744 insertions(+), 4 deletions(-)
create mode 100644 src/resources/financial-accounts/interest-tier-schedule.ts
create mode 100644 src/resources/financial-accounts/loan-tape-configuration.ts
create mode 100644 tests/api-resources/financial-accounts/interest-tier-schedule.test.ts
create mode 100644 tests/api-resources/financial-accounts/loan-tape-configuration.test.ts
diff --git a/.stats.yml b/.stats.yml
index 1862b2d1..34e5c967 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 177
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic%2Flithic-f85b60190db68921a3a877d0dd931670c27933ba1f5031fcdd27365e99adb5c9.yml
-openapi_spec_hash: 4828c2dc7543ce2a39774a9921c73c80
-config_hash: fbc424e01cca916048d63adcadaa8750
+configured_endpoints: 183
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/lithic%2Flithic-f99894d5b6eda608756c9e5e9868c81c4ce8c74c4d8958370cc3799766a13d65.yml
+openapi_spec_hash: 2f364e16b58e5a9759fc9f772cb33f3c
+config_hash: 2ee394874b7eb4cbe06f044b7376a6ba
diff --git a/MIGRATION.md b/MIGRATION.md
index 93d5980a..9761563b 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -64,6 +64,9 @@ This affects the following methods:
- `client.financialAccounts.statements.retrieve()`
- `client.financialAccounts.statements.lineItems.list()`
- `client.financialAccounts.loanTapes.retrieve()`
+- `client.financialAccounts.interestTierSchedule.retrieve()`
+- `client.financialAccounts.interestTierSchedule.update()`
+- `client.financialAccounts.interestTierSchedule.delete()`
### URI encoded path parameters
@@ -127,6 +130,7 @@ client.example.list(undefined, { headers: { ... } });
- `client.financialAccounts.creditConfiguration.update()`
- `client.financialAccounts.statements.list()`
- `client.financialAccounts.loanTapes.list()`
+- `client.financialAccounts.interestTierSchedule.list()`
- `client.transactions.list()`
- `client.externalBankAccounts.list()`
- `client.externalBankAccounts.retryMicroDeposits()`
diff --git a/api.md b/api.md
index 2ccf54f9..5d146582 100644
--- a/api.md
+++ b/api.md
@@ -386,6 +386,32 @@ Methods:
- client.financialAccounts.loanTapes.retrieve(loanTapeToken, { ...params }) -> LoanTape
- client.financialAccounts.loanTapes.list(financialAccountToken, { ...params }) -> LoanTapesCursorPage
+## LoanTapeConfiguration
+
+Types:
+
+- LoanTapeConfiguration
+- LoanTapeRebuildConfiguration
+
+Methods:
+
+- client.financialAccounts.loanTapeConfiguration.retrieve(financialAccountToken) -> LoanTapeConfiguration
+
+## InterestTierSchedule
+
+Types:
+
+- CategoryTier
+- InterestTierSchedule
+
+Methods:
+
+- client.financialAccounts.interestTierSchedule.create(financialAccountToken, { ...params }) -> InterestTierSchedule
+- client.financialAccounts.interestTierSchedule.retrieve(effectiveDate, { ...params }) -> InterestTierSchedule
+- client.financialAccounts.interestTierSchedule.update(effectiveDate, { ...params }) -> InterestTierSchedule
+- client.financialAccounts.interestTierSchedule.list(financialAccountToken, { ...params }) -> InterestTierSchedulesSinglePage
+- client.financialAccounts.interestTierSchedule.delete(effectiveDate, { ...params }) -> void
+
# Transactions
Types:
diff --git a/bin/migration-config.json b/bin/migration-config.json
index 802f17f5..7717302e 100644
--- a/bin/migration-config.json
+++ b/bin/migration-config.json
@@ -336,6 +336,109 @@
"type": "options"
}
]
+ },
+ {
+ "base": "financialAccounts.interestTierSchedule",
+ "name": "retrieve",
+ "params": [
+ {
+ "type": "param",
+ "key": "effective_date",
+ "location": "path"
+ },
+ {
+ "type": "params",
+ "maybeOverload": false
+ },
+ {
+ "type": "options"
+ }
+ ],
+ "oldParams": [
+ {
+ "type": "param",
+ "key": "financial_account_token",
+ "location": "path"
+ },
+ {
+ "type": "param",
+ "key": "effective_date",
+ "location": "path"
+ },
+ {
+ "type": "options"
+ }
+ ]
+ },
+ {
+ "base": "financialAccounts.interestTierSchedule",
+ "name": "update",
+ "params": [
+ {
+ "type": "param",
+ "key": "effective_date",
+ "location": "path"
+ },
+ {
+ "type": "params",
+ "maybeOverload": false
+ },
+ {
+ "type": "options"
+ }
+ ],
+ "oldParams": [
+ {
+ "type": "param",
+ "key": "financial_account_token",
+ "location": "path"
+ },
+ {
+ "type": "param",
+ "key": "effective_date",
+ "location": "path"
+ },
+ {
+ "type": "params",
+ "maybeOverload": false
+ },
+ {
+ "type": "options"
+ }
+ ]
+ },
+ {
+ "base": "financialAccounts.interestTierSchedule",
+ "name": "delete",
+ "params": [
+ {
+ "type": "param",
+ "key": "effective_date",
+ "location": "path"
+ },
+ {
+ "type": "params",
+ "maybeOverload": false
+ },
+ {
+ "type": "options"
+ }
+ ],
+ "oldParams": [
+ {
+ "type": "param",
+ "key": "financial_account_token",
+ "location": "path"
+ },
+ {
+ "type": "param",
+ "key": "effective_date",
+ "location": "path"
+ },
+ {
+ "type": "options"
+ }
+ ]
}
]
}
diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts
index 349dc8ad..7c58d5dc 100644
--- a/packages/mcp-server/src/methods.ts
+++ b/packages/mcp-server/src/methods.ts
@@ -623,6 +623,42 @@ export const sdkMethods: SdkMethod[] = [
httpMethod: 'get',
httpPath: '/v1/financial_accounts/{financial_account_token}/loan_tapes',
},
+ {
+ clientCallName: 'client.financialAccounts.loanTapeConfiguration.retrieve',
+ fullyQualifiedName: 'financialAccounts.loanTapeConfiguration.retrieve',
+ httpMethod: 'get',
+ httpPath: '/v1/financial_accounts/{financial_account_token}/loan_tape_configuration',
+ },
+ {
+ clientCallName: 'client.financialAccounts.interestTierSchedule.create',
+ fullyQualifiedName: 'financialAccounts.interestTierSchedule.create',
+ httpMethod: 'post',
+ httpPath: '/v1/financial_accounts/{financial_account_token}/interest_tier_schedule',
+ },
+ {
+ clientCallName: 'client.financialAccounts.interestTierSchedule.retrieve',
+ fullyQualifiedName: 'financialAccounts.interestTierSchedule.retrieve',
+ httpMethod: 'get',
+ httpPath: '/v1/financial_accounts/{financial_account_token}/interest_tier_schedule/{effective_date}',
+ },
+ {
+ clientCallName: 'client.financialAccounts.interestTierSchedule.update',
+ fullyQualifiedName: 'financialAccounts.interestTierSchedule.update',
+ httpMethod: 'put',
+ httpPath: '/v1/financial_accounts/{financial_account_token}/interest_tier_schedule/{effective_date}',
+ },
+ {
+ clientCallName: 'client.financialAccounts.interestTierSchedule.list',
+ fullyQualifiedName: 'financialAccounts.interestTierSchedule.list',
+ httpMethod: 'get',
+ httpPath: '/v1/financial_accounts/{financial_account_token}/interest_tier_schedule',
+ },
+ {
+ clientCallName: 'client.financialAccounts.interestTierSchedule.delete',
+ fullyQualifiedName: 'financialAccounts.interestTierSchedule.delete',
+ httpMethod: 'delete',
+ httpPath: '/v1/financial_accounts/{financial_account_token}/interest_tier_schedule/{effective_date}',
+ },
{
clientCallName: 'client.transactions.retrieve',
fullyQualifiedName: 'transactions.retrieve',
diff --git a/src/resources/financial-accounts/financial-accounts.ts b/src/resources/financial-accounts/financial-accounts.ts
index d3d6fd27..e6b0249d 100644
--- a/src/resources/financial-accounts/financial-accounts.ts
+++ b/src/resources/financial-accounts/financial-accounts.ts
@@ -16,6 +16,24 @@ import {
FinancialTransactionRetrieveParams,
FinancialTransactions,
} from './financial-transactions';
+import * as InterestTierScheduleAPI from './interest-tier-schedule';
+import {
+ CategoryTier,
+ InterestTierSchedule,
+ InterestTierScheduleCreateParams,
+ InterestTierScheduleDeleteParams,
+ InterestTierScheduleListParams,
+ InterestTierScheduleResource,
+ InterestTierScheduleRetrieveParams,
+ InterestTierScheduleUpdateParams,
+ InterestTierSchedulesSinglePage,
+} from './interest-tier-schedule';
+import * as LoanTapeConfigurationAPI from './loan-tape-configuration';
+import {
+ LoanTapeConfiguration,
+ LoanTapeConfigurationResource,
+ LoanTapeRebuildConfiguration,
+} from './loan-tape-configuration';
import * as LoanTapesAPI from './loan-tapes';
import {
CategoryBalances,
@@ -47,6 +65,10 @@ export class FinancialAccounts extends APIResource {
new CreditConfigurationAPI.CreditConfiguration(this._client);
statements: StatementsAPI.Statements = new StatementsAPI.Statements(this._client);
loanTapes: LoanTapesAPI.LoanTapes = new LoanTapesAPI.LoanTapes(this._client);
+ loanTapeConfiguration: LoanTapeConfigurationAPI.LoanTapeConfigurationResource =
+ new LoanTapeConfigurationAPI.LoanTapeConfigurationResource(this._client);
+ interestTierSchedule: InterestTierScheduleAPI.InterestTierScheduleResource =
+ new InterestTierScheduleAPI.InterestTierScheduleResource(this._client);
/**
* Create a new financial account
@@ -552,6 +574,8 @@ FinancialAccounts.Balances = Balances;
FinancialAccounts.FinancialTransactions = FinancialTransactions;
FinancialAccounts.CreditConfiguration = CreditConfigurationAPICreditConfiguration;
FinancialAccounts.LoanTapes = LoanTapes;
+FinancialAccounts.LoanTapeConfigurationResource = LoanTapeConfigurationResource;
+FinancialAccounts.InterestTierScheduleResource = InterestTierScheduleResource;
export declare namespace FinancialAccounts {
export {
@@ -598,4 +622,22 @@ export declare namespace FinancialAccounts {
type LoanTapeRetrieveParams as LoanTapeRetrieveParams,
type LoanTapeListParams as LoanTapeListParams,
};
+
+ export {
+ LoanTapeConfigurationResource as LoanTapeConfigurationResource,
+ type LoanTapeConfiguration as LoanTapeConfiguration,
+ type LoanTapeRebuildConfiguration as LoanTapeRebuildConfiguration,
+ };
+
+ export {
+ InterestTierScheduleResource as InterestTierScheduleResource,
+ type CategoryTier as CategoryTier,
+ type InterestTierSchedule as InterestTierSchedule,
+ type InterestTierSchedulesSinglePage as InterestTierSchedulesSinglePage,
+ type InterestTierScheduleCreateParams as InterestTierScheduleCreateParams,
+ type InterestTierScheduleRetrieveParams as InterestTierScheduleRetrieveParams,
+ type InterestTierScheduleUpdateParams as InterestTierScheduleUpdateParams,
+ type InterestTierScheduleListParams as InterestTierScheduleListParams,
+ type InterestTierScheduleDeleteParams as InterestTierScheduleDeleteParams,
+ };
}
diff --git a/src/resources/financial-accounts/index.ts b/src/resources/financial-accounts/index.ts
index d5b39e0c..bad98ab6 100644
--- a/src/resources/financial-accounts/index.ts
+++ b/src/resources/financial-accounts/index.ts
@@ -27,6 +27,22 @@ export {
type FinancialTransactionRetrieveParams,
type FinancialTransactionListParams,
} from './financial-transactions';
+export {
+ InterestTierScheduleResource,
+ type CategoryTier,
+ type InterestTierSchedule,
+ type InterestTierScheduleCreateParams,
+ type InterestTierScheduleRetrieveParams,
+ type InterestTierScheduleUpdateParams,
+ type InterestTierScheduleListParams,
+ type InterestTierScheduleDeleteParams,
+ type InterestTierSchedulesSinglePage,
+} from './interest-tier-schedule';
+export {
+ LoanTapeConfigurationResource,
+ type LoanTapeConfiguration,
+ type LoanTapeRebuildConfiguration,
+} from './loan-tape-configuration';
export {
LoanTapes,
type CategoryBalances,
diff --git a/src/resources/financial-accounts/interest-tier-schedule.ts b/src/resources/financial-accounts/interest-tier-schedule.ts
new file mode 100644
index 00000000..7d5a4b21
--- /dev/null
+++ b/src/resources/financial-accounts/interest-tier-schedule.ts
@@ -0,0 +1,292 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../../core/resource';
+import { APIPromise } from '../../core/api-promise';
+import { PagePromise, SinglePage } from '../../core/pagination';
+import { RequestOptions } from '../../internal/request-options';
+import { path } from '../../internal/utils/path';
+
+export class InterestTierScheduleResource extends APIResource {
+ /**
+ * Create a new interest tier schedule entry for a supported financial account
+ *
+ * @example
+ * ```ts
+ * const interestTierSchedule =
+ * await client.financialAccounts.interestTierSchedule.create(
+ * '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ * {
+ * credit_product_token: 'credit_product_token',
+ * effective_date: '2019-12-27',
+ * },
+ * );
+ * ```
+ */
+ create(
+ financialAccountToken: string,
+ body: InterestTierScheduleCreateParams,
+ options?: RequestOptions,
+ ): APIPromise {
+ return this._client.post(path`/v1/financial_accounts/${financialAccountToken}/interest_tier_schedule`, {
+ body,
+ ...options,
+ });
+ }
+
+ /**
+ * Get a specific interest tier schedule by effective date
+ *
+ * @example
+ * ```ts
+ * const interestTierSchedule =
+ * await client.financialAccounts.interestTierSchedule.retrieve(
+ * '2019-12-27',
+ * {
+ * financial_account_token:
+ * '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ * },
+ * );
+ * ```
+ */
+ retrieve(
+ effectiveDate: string,
+ params: InterestTierScheduleRetrieveParams,
+ options?: RequestOptions,
+ ): APIPromise {
+ const { financial_account_token } = params;
+ return this._client.get(
+ path`/v1/financial_accounts/${financial_account_token}/interest_tier_schedule/${effectiveDate}`,
+ options,
+ );
+ }
+
+ /**
+ * Update an existing interest tier schedule
+ *
+ * @example
+ * ```ts
+ * const interestTierSchedule =
+ * await client.financialAccounts.interestTierSchedule.update(
+ * '2019-12-27',
+ * {
+ * financial_account_token:
+ * '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ * },
+ * );
+ * ```
+ */
+ update(
+ effectiveDate: string,
+ params: InterestTierScheduleUpdateParams,
+ options?: RequestOptions,
+ ): APIPromise {
+ const { financial_account_token, ...body } = params;
+ return this._client.put(
+ path`/v1/financial_accounts/${financial_account_token}/interest_tier_schedule/${effectiveDate}`,
+ { body, ...options },
+ );
+ }
+
+ /**
+ * List interest tier schedules for a financial account with optional date
+ * filtering.
+ *
+ * If no date parameters are provided, returns all tier schedules. If date
+ * parameters are provided, uses filtering to return matching schedules (max 100).
+ *
+ * - for_date: Returns exact match (takes precedence over other dates)
+ * - before_date: Returns schedules with effective_date <= before_date
+ * - after_date: Returns schedules with effective_date >= after_date
+ * - Both before_date and after_date: Returns schedules in range
+ *
+ * @example
+ * ```ts
+ * // Automatically fetches more pages as needed.
+ * for await (const interestTierSchedule of client.financialAccounts.interestTierSchedule.list(
+ * '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ * )) {
+ * // ...
+ * }
+ * ```
+ */
+ list(
+ financialAccountToken: string,
+ query: InterestTierScheduleListParams | null | undefined = {},
+ options?: RequestOptions,
+ ): PagePromise {
+ return this._client.getAPIList(
+ path`/v1/financial_accounts/${financialAccountToken}/interest_tier_schedule`,
+ SinglePage,
+ { query, ...options },
+ );
+ }
+
+ /**
+ * Delete an interest tier schedule entry.
+ *
+ * Returns:
+ *
+ * - 400 Bad Request: Invalid effective_date format OR attempting to delete the
+ * earliest tier schedule entry for a non-PENDING account
+ * - 404 Not Found: Tier schedule entry not found for the given effective_date OR
+ * ledger account not found
+ *
+ * Note: PENDING accounts can delete the earliest tier schedule entry (account
+ * hasn't opened yet). Active/non-PENDING accounts cannot delete the earliest entry
+ * to prevent orphaning the account.
+ *
+ * If the deleted tier schedule has a past effective_date and the account is
+ * ACTIVE, the loan tape rebuild configuration will be updated to trigger rebuilds
+ * from that date.
+ *
+ * @example
+ * ```ts
+ * await client.financialAccounts.interestTierSchedule.delete(
+ * '2019-12-27',
+ * {
+ * financial_account_token:
+ * '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ * },
+ * );
+ * ```
+ */
+ delete(
+ effectiveDate: string,
+ params: InterestTierScheduleDeleteParams,
+ options?: RequestOptions,
+ ): APIPromise {
+ const { financial_account_token } = params;
+ return this._client.delete(
+ path`/v1/financial_accounts/${financial_account_token}/interest_tier_schedule/${effectiveDate}`,
+ options,
+ );
+ }
+}
+
+export type InterestTierSchedulesSinglePage = SinglePage;
+
+/**
+ * Rate and rate cap for interest on a category
+ */
+export interface CategoryTier {
+ /**
+ * Maximum interest rate for this category, e.g. '0.0525' for 5.25%
+ */
+ cap_rate?: string;
+
+ /**
+ * Interest rate for this category, e.g. '0.0525' for 5.25%
+ */
+ rate?: string;
+}
+
+/**
+ * Entry in the Tier Schedule of an account
+ */
+export interface InterestTierSchedule {
+ /**
+ * Globally unique identifier for a credit product
+ */
+ credit_product_token: string;
+
+ /**
+ * Date the tier should be effective in YYYY-MM-DD format
+ */
+ effective_date: string;
+
+ /**
+ * Name of a tier contained in the credit product. Mutually exclusive with
+ * tier_rates
+ */
+ tier_name?: string;
+
+ /**
+ * Custom rates per category. Mutually exclusive with tier_name
+ */
+ tier_rates?: unknown;
+}
+
+export interface InterestTierScheduleCreateParams {
+ /**
+ * Globally unique identifier for a credit product
+ */
+ credit_product_token: string;
+
+ /**
+ * Date the tier should be effective in YYYY-MM-DD format
+ */
+ effective_date: string;
+
+ /**
+ * Name of a tier contained in the credit product. Mutually exclusive with
+ * tier_rates
+ */
+ tier_name?: string;
+
+ /**
+ * Custom rates per category. Mutually exclusive with tier_name
+ */
+ tier_rates?: unknown;
+}
+
+export interface InterestTierScheduleRetrieveParams {
+ /**
+ * Globally unique identifier for financial account
+ */
+ financial_account_token: string;
+}
+
+export interface InterestTierScheduleUpdateParams {
+ /**
+ * Path param: Globally unique identifier for financial account
+ */
+ financial_account_token: string;
+
+ /**
+ * Body param: Name of a tier contained in the credit product. Mutually exclusive
+ * with tier_rates
+ */
+ tier_name?: string;
+
+ /**
+ * Body param: Custom rates per category. Mutually exclusive with tier_name
+ */
+ tier_rates?: unknown;
+}
+
+export interface InterestTierScheduleListParams {
+ /**
+ * Return schedules with effective_date >= after_date (ISO format YYYY-MM-DD)
+ */
+ after_date?: string;
+
+ /**
+ * Return schedules with effective_date <= before_date (ISO format YYYY-MM-DD)
+ */
+ before_date?: string;
+
+ /**
+ * Return schedule with effective_date == for_date (ISO format YYYY-MM-DD)
+ */
+ for_date?: string;
+}
+
+export interface InterestTierScheduleDeleteParams {
+ /**
+ * Globally unique identifier for financial account
+ */
+ financial_account_token: string;
+}
+
+export declare namespace InterestTierScheduleResource {
+ export {
+ type CategoryTier as CategoryTier,
+ type InterestTierSchedule as InterestTierSchedule,
+ type InterestTierSchedulesSinglePage as InterestTierSchedulesSinglePage,
+ type InterestTierScheduleCreateParams as InterestTierScheduleCreateParams,
+ type InterestTierScheduleRetrieveParams as InterestTierScheduleRetrieveParams,
+ type InterestTierScheduleUpdateParams as InterestTierScheduleUpdateParams,
+ type InterestTierScheduleListParams as InterestTierScheduleListParams,
+ type InterestTierScheduleDeleteParams as InterestTierScheduleDeleteParams,
+ };
+}
diff --git a/src/resources/financial-accounts/loan-tape-configuration.ts b/src/resources/financial-accounts/loan-tape-configuration.ts
new file mode 100644
index 00000000..41a1272e
--- /dev/null
+++ b/src/resources/financial-accounts/loan-tape-configuration.ts
@@ -0,0 +1,75 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../../core/resource';
+import { APIPromise } from '../../core/api-promise';
+import { RequestOptions } from '../../internal/request-options';
+import { path } from '../../internal/utils/path';
+
+export class LoanTapeConfigurationResource extends APIResource {
+ /**
+ * Get the loan tape configuration for a given financial account.
+ *
+ * @example
+ * ```ts
+ * const loanTapeConfiguration =
+ * await client.financialAccounts.loanTapeConfiguration.retrieve(
+ * '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ * );
+ * ```
+ */
+ retrieve(financialAccountToken: string, options?: RequestOptions): APIPromise {
+ return this._client.get(
+ path`/v1/financial_accounts/${financialAccountToken}/loan_tape_configuration`,
+ options,
+ );
+ }
+}
+
+/**
+ * Configuration for loan tapes
+ */
+export interface LoanTapeConfiguration {
+ created_at: string;
+
+ financial_account_token: string;
+
+ instance_token: string;
+
+ updated_at: string;
+
+ credit_product_token?: string;
+
+ /**
+ * Configuration for building loan tapes
+ */
+ loan_tape_rebuild_configuration?: LoanTapeRebuildConfiguration;
+
+ tier_schedule_changed_at?: string;
+}
+
+/**
+ * Configuration for building loan tapes
+ */
+export interface LoanTapeRebuildConfiguration {
+ /**
+ * Whether the account's loan tapes need to be rebuilt or not
+ */
+ rebuild_needed: boolean;
+
+ /**
+ * The date for which the account's loan tapes were last rebuilt
+ */
+ last_rebuild?: string;
+
+ /**
+ * Date from which to start rebuilding from if the account requires a rebuild
+ */
+ rebuild_from?: string;
+}
+
+export declare namespace LoanTapeConfigurationResource {
+ export {
+ type LoanTapeConfiguration as LoanTapeConfiguration,
+ type LoanTapeRebuildConfiguration as LoanTapeRebuildConfiguration,
+ };
+}
diff --git a/tests/api-resources/financial-accounts/interest-tier-schedule.test.ts b/tests/api-resources/financial-accounts/interest-tier-schedule.test.ts
new file mode 100644
index 00000000..5312bb21
--- /dev/null
+++ b/tests/api-resources/financial-accounts/interest-tier-schedule.test.ts
@@ -0,0 +1,123 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import Lithic from 'lithic';
+
+const client = new Lithic({
+ apiKey: 'My Lithic API Key',
+ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
+});
+
+describe('resource interestTierSchedule', () => {
+ test('create: only required params', async () => {
+ const responsePromise = client.financialAccounts.interestTierSchedule.create(
+ '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ { credit_product_token: 'credit_product_token', effective_date: '2019-12-27' },
+ );
+ const rawResponse = await responsePromise.asResponse();
+ expect(rawResponse).toBeInstanceOf(Response);
+ const response = await responsePromise;
+ expect(response).not.toBeInstanceOf(Response);
+ const dataAndResponse = await responsePromise.withResponse();
+ expect(dataAndResponse.data).toBe(response);
+ expect(dataAndResponse.response).toBe(rawResponse);
+ });
+
+ test('create: required and optional params', async () => {
+ const response = await client.financialAccounts.interestTierSchedule.create(
+ '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ {
+ credit_product_token: 'credit_product_token',
+ effective_date: '2019-12-27',
+ tier_name: 'tier_name',
+ tier_rates: {},
+ },
+ );
+ });
+
+ test('retrieve: only required params', async () => {
+ const responsePromise = client.financialAccounts.interestTierSchedule.retrieve('2019-12-27', {
+ financial_account_token: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ });
+ const rawResponse = await responsePromise.asResponse();
+ expect(rawResponse).toBeInstanceOf(Response);
+ const response = await responsePromise;
+ expect(response).not.toBeInstanceOf(Response);
+ const dataAndResponse = await responsePromise.withResponse();
+ expect(dataAndResponse.data).toBe(response);
+ expect(dataAndResponse.response).toBe(rawResponse);
+ });
+
+ test('retrieve: required and optional params', async () => {
+ const response = await client.financialAccounts.interestTierSchedule.retrieve('2019-12-27', {
+ financial_account_token: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ });
+ });
+
+ test('update: only required params', async () => {
+ const responsePromise = client.financialAccounts.interestTierSchedule.update('2019-12-27', {
+ financial_account_token: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ });
+ const rawResponse = await responsePromise.asResponse();
+ expect(rawResponse).toBeInstanceOf(Response);
+ const response = await responsePromise;
+ expect(response).not.toBeInstanceOf(Response);
+ const dataAndResponse = await responsePromise.withResponse();
+ expect(dataAndResponse.data).toBe(response);
+ expect(dataAndResponse.response).toBe(rawResponse);
+ });
+
+ test('update: required and optional params', async () => {
+ const response = await client.financialAccounts.interestTierSchedule.update('2019-12-27', {
+ financial_account_token: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ tier_name: 'tier_name',
+ tier_rates: {},
+ });
+ });
+
+ test('list', async () => {
+ const responsePromise = client.financialAccounts.interestTierSchedule.list(
+ '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ );
+ const rawResponse = await responsePromise.asResponse();
+ expect(rawResponse).toBeInstanceOf(Response);
+ const response = await responsePromise;
+ expect(response).not.toBeInstanceOf(Response);
+ const dataAndResponse = await responsePromise.withResponse();
+ expect(dataAndResponse.data).toBe(response);
+ expect(dataAndResponse.response).toBe(rawResponse);
+ });
+
+ test('list: request options and params are passed correctly', async () => {
+ // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error
+ await expect(
+ client.financialAccounts.interestTierSchedule.list(
+ '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ {
+ after_date: '2019-12-27',
+ before_date: '2019-12-27',
+ for_date: '2019-12-27',
+ },
+ { path: '/_stainless_unknown_path' },
+ ),
+ ).rejects.toThrow(Lithic.NotFoundError);
+ });
+
+ test('delete: only required params', async () => {
+ const responsePromise = client.financialAccounts.interestTierSchedule.delete('2019-12-27', {
+ financial_account_token: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ });
+ const rawResponse = await responsePromise.asResponse();
+ expect(rawResponse).toBeInstanceOf(Response);
+ const response = await responsePromise;
+ expect(response).not.toBeInstanceOf(Response);
+ const dataAndResponse = await responsePromise.withResponse();
+ expect(dataAndResponse.data).toBe(response);
+ expect(dataAndResponse.response).toBe(rawResponse);
+ });
+
+ test('delete: required and optional params', async () => {
+ const response = await client.financialAccounts.interestTierSchedule.delete('2019-12-27', {
+ financial_account_token: '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ });
+ });
+});
diff --git a/tests/api-resources/financial-accounts/loan-tape-configuration.test.ts b/tests/api-resources/financial-accounts/loan-tape-configuration.test.ts
new file mode 100644
index 00000000..038abc1f
--- /dev/null
+++ b/tests/api-resources/financial-accounts/loan-tape-configuration.test.ts
@@ -0,0 +1,23 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import Lithic from 'lithic';
+
+const client = new Lithic({
+ apiKey: 'My Lithic API Key',
+ baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010',
+});
+
+describe('resource loanTapeConfiguration', () => {
+ test('retrieve', async () => {
+ const responsePromise = client.financialAccounts.loanTapeConfiguration.retrieve(
+ '182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e',
+ );
+ const rawResponse = await responsePromise.asResponse();
+ expect(rawResponse).toBeInstanceOf(Response);
+ const response = await responsePromise;
+ expect(response).not.toBeInstanceOf(Response);
+ const dataAndResponse = await responsePromise.withResponse();
+ expect(dataAndResponse.data).toBe(response);
+ expect(dataAndResponse.response).toBe(rawResponse);
+ });
+});
From 7e9cc7b6d308347668dec9dc874cb0c3048fa17b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Mon, 23 Feb 2026 23:09:34 +0000
Subject: [PATCH 07/11] chore(internal): upgrade @modelcontextprotocol/sdk and
hono
---
packages/mcp-server/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index dc282bc6..e45e0a60 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -32,7 +32,7 @@
"dependencies": {
"lithic": "file:../../dist/",
"@cloudflare/cabidela": "^0.2.4",
- "@modelcontextprotocol/sdk": "^1.25.2",
+ "@modelcontextprotocol/sdk": "^1.26.0",
"@valtown/deno-http-worker": "^0.0.21",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
From d7ce232aec15b1fd054e8ef2b29d19d54627fcea Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Feb 2026 20:07:29 +0000
Subject: [PATCH 08/11] chore(internal): make MCP code execution location
configurable via a flag
---
packages/mcp-server/Dockerfile | 14 +-
packages/mcp-server/src/code-tool-paths.cts | 3 +
packages/mcp-server/src/code-tool-types.ts | 1 +
packages/mcp-server/src/code-tool-worker.ts | 455 ++++++++++++++++++++
packages/mcp-server/src/code-tool.ts | 321 +++++++++++---
packages/mcp-server/src/options.ts | 12 +
packages/mcp-server/src/server.ts | 1 +
packages/mcp-server/tests/options.test.ts | 20 +-
8 files changed, 756 insertions(+), 71 deletions(-)
create mode 100644 packages/mcp-server/src/code-tool-paths.cts
create mode 100644 packages/mcp-server/src/code-tool-worker.ts
diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile
index f063d494..da345c80 100644
--- a/packages/mcp-server/Dockerfile
+++ b/packages/mcp-server/Dockerfile
@@ -37,9 +37,21 @@ COPY . .
RUN yarn install --frozen-lockfile && \
yarn build
-# Production stage
+FROM denoland/deno:bin-2.6.10 AS deno_installer
+FROM gcr.io/distroless/cc@sha256:66d87e170bc2c5e2b8cf853501141c3c55b4e502b8677595c57534df54a68cc5 AS cc
+
FROM node:24-alpine
+# Install deno
+COPY --from=deno_installer /deno /usr/local/bin/deno
+
+# Add in shared libraries needed by Deno
+COPY --from=cc --chown=root:root --chmod=755 /lib/*-linux-gnu/* /usr/local/lib/
+COPY --from=cc --chown=root:root --chmod=755 /lib/ld-linux-* /lib/
+
+RUN mkdir /lib64 && ln -s /usr/local/lib/ld-linux-* /lib64/
+ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib
+
# Add non-root user
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts
new file mode 100644
index 00000000..15ce7f55
--- /dev/null
+++ b/packages/mcp-server/src/code-tool-paths.cts
@@ -0,0 +1,3 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export const workerPath = require.resolve('./code-tool-worker.mjs');
diff --git a/packages/mcp-server/src/code-tool-types.ts b/packages/mcp-server/src/code-tool-types.ts
index a87741dc..4b944f3a 100644
--- a/packages/mcp-server/src/code-tool-types.ts
+++ b/packages/mcp-server/src/code-tool-types.ts
@@ -8,6 +8,7 @@ export type WorkerInput = {
client_opts: ClientOptions;
intent?: string | undefined;
};
+
export type WorkerOutput = {
is_error: boolean;
result: unknown | null;
diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts
new file mode 100644
index 00000000..082221b8
--- /dev/null
+++ b/packages/mcp-server/src/code-tool-worker.ts
@@ -0,0 +1,455 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import path from 'node:path';
+import util from 'node:util';
+import Fuse from 'fuse.js';
+import ts from 'typescript';
+import { WorkerOutput } from './code-tool-types';
+import { Lithic, ClientOptions } from 'lithic';
+
+function getRunFunctionSource(code: string): {
+ type: 'declaration' | 'expression';
+ client: string | undefined;
+ code: string;
+} | null {
+ const sourceFile = ts.createSourceFile('code.ts', code, ts.ScriptTarget.Latest, true);
+ const printer = ts.createPrinter();
+
+ for (const statement of sourceFile.statements) {
+ // Check for top-level function declarations
+ if (ts.isFunctionDeclaration(statement)) {
+ if (statement.name?.text === 'run') {
+ return {
+ type: 'declaration',
+ client: statement.parameters[0]?.name.getText(),
+ code: printer.printNode(ts.EmitHint.Unspecified, statement.body!, sourceFile),
+ };
+ }
+ }
+
+ // Check for variable declarations: const run = () => {} or const run = function() {}
+ if (ts.isVariableStatement(statement)) {
+ for (const declaration of statement.declarationList.declarations) {
+ if (
+ ts.isIdentifier(declaration.name) &&
+ declaration.name.text === 'run' &&
+ // Check if it's initialized with a function
+ declaration.initializer &&
+ (ts.isFunctionExpression(declaration.initializer) || ts.isArrowFunction(declaration.initializer))
+ ) {
+ return {
+ type: 'expression',
+ client: declaration.initializer.parameters[0]?.name.getText(),
+ code: printer.printNode(ts.EmitHint.Unspecified, declaration.initializer, sourceFile),
+ };
+ }
+ }
+ }
+ }
+
+ return null;
+}
+
+function getTSDiagnostics(code: string): string[] {
+ const functionSource = getRunFunctionSource(code)!;
+ const codeWithImport = [
+ 'import { Lithic } from "lithic";',
+ functionSource.type === 'declaration' ?
+ `async function run(${functionSource.client}: Lithic)`
+ : `const run: (${functionSource.client}: Lithic) => Promise =`,
+ functionSource.code,
+ ].join('\n');
+ const sourcePath = path.resolve('code.ts');
+ const ast = ts.createSourceFile(sourcePath, codeWithImport, ts.ScriptTarget.Latest, true);
+ const options = ts.getDefaultCompilerOptions();
+ options.target = ts.ScriptTarget.Latest;
+ options.module = ts.ModuleKind.NodeNext;
+ options.moduleResolution = ts.ModuleResolutionKind.NodeNext;
+ const host = ts.createCompilerHost(options, true);
+ const newHost: typeof host = {
+ ...host,
+ getSourceFile: (...args) => {
+ if (path.resolve(args[0]) === sourcePath) {
+ return ast;
+ }
+ return host.getSourceFile(...args);
+ },
+ readFile: (...args) => {
+ if (path.resolve(args[0]) === sourcePath) {
+ return codeWithImport;
+ }
+ return host.readFile(...args);
+ },
+ fileExists: (...args) => {
+ if (path.resolve(args[0]) === sourcePath) {
+ return true;
+ }
+ return host.fileExists(...args);
+ },
+ };
+ const program = ts.createProgram({
+ options,
+ rootNames: [sourcePath],
+ host: newHost,
+ });
+ const diagnostics = ts.getPreEmitDiagnostics(program, ast);
+ return diagnostics.map((d) => {
+ const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
+ if (!d.file || !d.start) return `- ${message}`;
+ const { line: lineNumber } = ts.getLineAndCharacterOfPosition(d.file, d.start);
+ const line = codeWithImport.split('\n').at(lineNumber)?.trim();
+ return line ? `- ${message}\n ${line}` : `- ${message}`;
+ });
+}
+
+const fuse = new Fuse(
+ [
+ 'client.apiStatus',
+ 'client.accounts.list',
+ 'client.accounts.retrieve',
+ 'client.accounts.retrieveSpendLimits',
+ 'client.accounts.update',
+ 'client.accountHolders.create',
+ 'client.accountHolders.list',
+ 'client.accountHolders.listDocuments',
+ 'client.accountHolders.retrieve',
+ 'client.accountHolders.retrieveDocument',
+ 'client.accountHolders.simulateEnrollmentDocumentReview',
+ 'client.accountHolders.simulateEnrollmentReview',
+ 'client.accountHolders.update',
+ 'client.accountHolders.uploadDocument',
+ 'client.authRules.v2.create',
+ 'client.authRules.v2.delete',
+ 'client.authRules.v2.draft',
+ 'client.authRules.v2.list',
+ 'client.authRules.v2.listResults',
+ 'client.authRules.v2.promote',
+ 'client.authRules.v2.retrieve',
+ 'client.authRules.v2.retrieveFeatures',
+ 'client.authRules.v2.retrieveReport',
+ 'client.authRules.v2.update',
+ 'client.authRules.v2.backtests.create',
+ 'client.authRules.v2.backtests.retrieve',
+ 'client.authStreamEnrollment.retrieveSecret',
+ 'client.authStreamEnrollment.rotateSecret',
+ 'client.tokenizationDecisioning.retrieveSecret',
+ 'client.tokenizationDecisioning.rotateSecret',
+ 'client.tokenizations.activate',
+ 'client.tokenizations.deactivate',
+ 'client.tokenizations.list',
+ 'client.tokenizations.pause',
+ 'client.tokenizations.resendActivationCode',
+ 'client.tokenizations.retrieve',
+ 'client.tokenizations.simulate',
+ 'client.tokenizations.unpause',
+ 'client.tokenizations.updateDigitalCardArt',
+ 'client.cards.convertPhysical',
+ 'client.cards.create',
+ 'client.cards.embed',
+ 'client.cards.list',
+ 'client.cards.provision',
+ 'client.cards.reissue',
+ 'client.cards.renew',
+ 'client.cards.retrieve',
+ 'client.cards.retrieveSpendLimits',
+ 'client.cards.searchByPan',
+ 'client.cards.update',
+ 'client.cards.webProvision',
+ 'client.cards.balances.list',
+ 'client.cards.financialTransactions.list',
+ 'client.cards.financialTransactions.retrieve',
+ 'client.cardBulkOrders.create',
+ 'client.cardBulkOrders.list',
+ 'client.cardBulkOrders.retrieve',
+ 'client.cardBulkOrders.update',
+ 'client.balances.list',
+ 'client.disputes.create',
+ 'client.disputes.delete',
+ 'client.disputes.deleteEvidence',
+ 'client.disputes.initiateEvidenceUpload',
+ 'client.disputes.list',
+ 'client.disputes.listEvidences',
+ 'client.disputes.retrieve',
+ 'client.disputes.retrieveEvidence',
+ 'client.disputes.update',
+ 'client.disputesV2.list',
+ 'client.disputesV2.retrieve',
+ 'client.events.list',
+ 'client.events.listAttempts',
+ 'client.events.retrieve',
+ 'client.events.subscriptions.create',
+ 'client.events.subscriptions.delete',
+ 'client.events.subscriptions.list',
+ 'client.events.subscriptions.listAttempts',
+ 'client.events.subscriptions.recover',
+ 'client.events.subscriptions.replayMissing',
+ 'client.events.subscriptions.retrieve',
+ 'client.events.subscriptions.retrieveSecret',
+ 'client.events.subscriptions.rotateSecret',
+ 'client.events.subscriptions.sendSimulatedExample',
+ 'client.events.subscriptions.update',
+ 'client.events.eventSubscriptions.resend',
+ 'client.transfers.create',
+ 'client.financialAccounts.create',
+ 'client.financialAccounts.list',
+ 'client.financialAccounts.registerAccountNumber',
+ 'client.financialAccounts.retrieve',
+ 'client.financialAccounts.update',
+ 'client.financialAccounts.updateStatus',
+ 'client.financialAccounts.balances.list',
+ 'client.financialAccounts.financialTransactions.list',
+ 'client.financialAccounts.financialTransactions.retrieve',
+ 'client.financialAccounts.creditConfiguration.retrieve',
+ 'client.financialAccounts.creditConfiguration.update',
+ 'client.financialAccounts.statements.list',
+ 'client.financialAccounts.statements.retrieve',
+ 'client.financialAccounts.statements.lineItems.list',
+ 'client.financialAccounts.loanTapes.list',
+ 'client.financialAccounts.loanTapes.retrieve',
+ 'client.financialAccounts.loanTapeConfiguration.retrieve',
+ 'client.financialAccounts.interestTierSchedule.create',
+ 'client.financialAccounts.interestTierSchedule.delete',
+ 'client.financialAccounts.interestTierSchedule.list',
+ 'client.financialAccounts.interestTierSchedule.retrieve',
+ 'client.financialAccounts.interestTierSchedule.update',
+ 'client.transactions.expireAuthorization',
+ 'client.transactions.list',
+ 'client.transactions.retrieve',
+ 'client.transactions.simulateAuthorization',
+ 'client.transactions.simulateAuthorizationAdvice',
+ 'client.transactions.simulateClearing',
+ 'client.transactions.simulateCreditAuthorization',
+ 'client.transactions.simulateCreditAuthorizationAdvice',
+ 'client.transactions.simulateReturn',
+ 'client.transactions.simulateReturnReversal',
+ 'client.transactions.simulateVoid',
+ 'client.transactions.enhancedCommercialData.retrieve',
+ 'client.transactions.events.enhancedCommercialData.retrieve',
+ 'client.responderEndpoints.checkStatus',
+ 'client.responderEndpoints.create',
+ 'client.responderEndpoints.delete',
+ 'client.externalBankAccounts.create',
+ 'client.externalBankAccounts.list',
+ 'client.externalBankAccounts.retrieve',
+ 'client.externalBankAccounts.retryMicroDeposits',
+ 'client.externalBankAccounts.retryPrenote',
+ 'client.externalBankAccounts.unpause',
+ 'client.externalBankAccounts.update',
+ 'client.externalBankAccounts.microDeposits.create',
+ 'client.payments.create',
+ 'client.payments.list',
+ 'client.payments.retrieve',
+ 'client.payments.retry',
+ 'client.payments.return',
+ 'client.payments.simulateAction',
+ 'client.payments.simulateReceipt',
+ 'client.payments.simulateRelease',
+ 'client.payments.simulateReturn',
+ 'client.threeDS.authentication.retrieve',
+ 'client.threeDS.authentication.simulate',
+ 'client.threeDS.authentication.simulateOtpEntry',
+ 'client.threeDS.decisioning.challengeResponse',
+ 'client.threeDS.decisioning.retrieveSecret',
+ 'client.threeDS.decisioning.rotateSecret',
+ 'client.reports.settlement.listDetails',
+ 'client.reports.settlement.summary',
+ 'client.reports.settlement.networkTotals.list',
+ 'client.reports.settlement.networkTotals.retrieve',
+ 'client.cardPrograms.list',
+ 'client.cardPrograms.retrieve',
+ 'client.digitalCardArt.list',
+ 'client.digitalCardArt.retrieve',
+ 'client.bookTransfers.create',
+ 'client.bookTransfers.list',
+ 'client.bookTransfers.retrieve',
+ 'client.bookTransfers.retry',
+ 'client.bookTransfers.reverse',
+ 'client.creditProducts.extendedCredit.retrieve',
+ 'client.creditProducts.primeRates.create',
+ 'client.creditProducts.primeRates.retrieve',
+ 'client.externalPayments.cancel',
+ 'client.externalPayments.create',
+ 'client.externalPayments.list',
+ 'client.externalPayments.release',
+ 'client.externalPayments.retrieve',
+ 'client.externalPayments.reverse',
+ 'client.externalPayments.settle',
+ 'client.managementOperations.create',
+ 'client.managementOperations.list',
+ 'client.managementOperations.retrieve',
+ 'client.managementOperations.reverse',
+ 'client.fundingEvents.list',
+ 'client.fundingEvents.retrieve',
+ 'client.fundingEvents.retrieveDetails',
+ 'client.fraud.transactions.report',
+ 'client.fraud.transactions.retrieve',
+ 'client.networkPrograms.list',
+ 'client.networkPrograms.retrieve',
+ 'client.accountActivity.list',
+ 'client.accountActivity.retrieveTransaction',
+ 'client.transferLimits.list',
+ 'client.webhooks.parsed',
+ ],
+ { threshold: 1, shouldSort: true },
+);
+
+function getMethodSuggestions(fullyQualifiedMethodName: string): string[] {
+ return fuse
+ .search(fullyQualifiedMethodName)
+ .map(({ item }) => item)
+ .slice(0, 5);
+}
+
+const proxyToObj = new WeakMap();
+const objToProxy = new WeakMap();
+
+type ClientProxyConfig = {
+ path: string[];
+ isBelievedBad?: boolean;
+};
+
+function makeSdkProxy(obj: T, { path, isBelievedBad = false }: ClientProxyConfig): T {
+ let proxy: T = objToProxy.get(obj);
+
+ if (!proxy) {
+ proxy = new Proxy(obj, {
+ get(target, prop, receiver) {
+ const propPath = [...path, String(prop)];
+ const value = Reflect.get(target, prop, receiver);
+
+ if (isBelievedBad || (!(prop in target) && value === undefined)) {
+ // If we're accessing a path that doesn't exist, it will probably eventually error.
+ // Let's proxy it and mark it bad so that we can control the error message.
+ // We proxy an empty class so that an invocation or construction attempt is possible.
+ return makeSdkProxy(class {}, { path: propPath, isBelievedBad: true });
+ }
+
+ if (value !== null && (typeof value === 'object' || typeof value === 'function')) {
+ return makeSdkProxy(value, { path: propPath, isBelievedBad });
+ }
+
+ return value;
+ },
+
+ apply(target, thisArg, args) {
+ if (isBelievedBad || typeof target !== 'function') {
+ const fullyQualifiedMethodName = path.join('.');
+ const suggestions = getMethodSuggestions(fullyQualifiedMethodName);
+ throw new Error(
+ `${fullyQualifiedMethodName} is not a function. Did you mean: ${suggestions.join(', ')}`,
+ );
+ }
+
+ return Reflect.apply(target, proxyToObj.get(thisArg) ?? thisArg, args);
+ },
+
+ construct(target, args, newTarget) {
+ if (isBelievedBad || typeof target !== 'function') {
+ const fullyQualifiedMethodName = path.join('.');
+ const suggestions = getMethodSuggestions(fullyQualifiedMethodName);
+ throw new Error(
+ `${fullyQualifiedMethodName} is not a constructor. Did you mean: ${suggestions.join(', ')}`,
+ );
+ }
+
+ return Reflect.construct(target, args, newTarget);
+ },
+ });
+
+ objToProxy.set(obj, proxy);
+ proxyToObj.set(proxy, obj);
+ }
+
+ return proxy;
+}
+
+function parseError(code: string, error: unknown): string | undefined {
+ if (!(error instanceof Error)) return;
+ const message = error.name ? `${error.name}: ${error.message}` : error.message;
+ try {
+ // Deno uses V8; the first ":LINE:COLUMN" is the top of stack.
+ const lineNumber = error.stack?.match(/:([0-9]+):[0-9]+/)?.[1];
+ // -1 for the zero-based indexing
+ const line =
+ lineNumber &&
+ code
+ .split('\n')
+ .at(parseInt(lineNumber, 10) - 1)
+ ?.trim();
+ return line ? `${message}\n at line ${lineNumber}\n ${line}` : message;
+ } catch {
+ return message;
+ }
+}
+
+const fetch = async (req: Request): Promise => {
+ const { opts, code } = (await req.json()) as { opts: ClientOptions; code: string };
+
+ const runFunctionSource = code ? getRunFunctionSource(code) : null;
+ if (!runFunctionSource) {
+ const message =
+ code ?
+ 'The code is missing a top-level `run` function.'
+ : 'The code argument is missing. Provide one containing a top-level `run` function.';
+ return Response.json(
+ {
+ is_error: true,
+ result: `${message} Write code within this template:\n\n\`\`\`\nasync function run(client) {\n // Fill this out\n}\n\`\`\``,
+ log_lines: [],
+ err_lines: [],
+ } satisfies WorkerOutput,
+ { status: 400, statusText: 'Code execution error' },
+ );
+ }
+
+ const diagnostics = getTSDiagnostics(code);
+ if (diagnostics.length > 0) {
+ return Response.json(
+ {
+ is_error: true,
+ result: `The code contains TypeScript diagnostics:\n${diagnostics.join('\n')}`,
+ log_lines: [],
+ err_lines: [],
+ } satisfies WorkerOutput,
+ { status: 400, statusText: 'Code execution error' },
+ );
+ }
+
+ const client = new Lithic({
+ ...opts,
+ });
+
+ const log_lines: string[] = [];
+ const err_lines: string[] = [];
+ const console = {
+ log: (...args: unknown[]) => {
+ log_lines.push(util.format(...args));
+ },
+ error: (...args: unknown[]) => {
+ err_lines.push(util.format(...args));
+ },
+ };
+ try {
+ let run_ = async (client: any) => {};
+ eval(`${code}\nrun_ = run;`);
+ const result = await run_(makeSdkProxy(client, { path: ['client'] }));
+ return Response.json({
+ is_error: false,
+ result,
+ log_lines,
+ err_lines,
+ } satisfies WorkerOutput);
+ } catch (e) {
+ return Response.json(
+ {
+ is_error: true,
+ result: parseError(code, e),
+ log_lines,
+ err_lines,
+ } satisfies WorkerOutput,
+ { status: 400, statusText: 'Code execution error' },
+ );
+ }
+};
+
+export default { fetch };
diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts
index 274d9943..50ed93c1 100644
--- a/packages/mcp-server/src/code-tool.ts
+++ b/packages/mcp-server/src/code-tool.ts
@@ -1,6 +1,12 @@
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+import fs from 'node:fs';
+import path from 'node:path';
+import url from 'node:url';
+import { newDenoHTTPWorker } from '@valtown/deno-http-worker';
+import { workerPath } from './code-tool-paths.cjs';
import {
+ ContentBlock,
McpRequestContext,
McpTool,
Metadata,
@@ -12,6 +18,8 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { readEnv, requireValue } from './util';
import { WorkerInput, WorkerOutput } from './code-tool-types';
import { SdkMethod } from './methods';
+import { McpCodeExecutionMode } from './options';
+import { ClientOptions } from 'lithic';
const prompt = `Runs JavaScript code to interact with the Lithic API.
@@ -40,9 +48,19 @@ Variables will not persist between calls, so make sure to return or log any data
* we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then
* a generic endpoint that can be used to invoke any endpoint with the provided arguments.
*
- * @param endpoints - The endpoints to include in the list.
+ * @param blockedMethods - The methods to block for code execution. Blocking is done by simple string
+ * matching, so it is not secure against obfuscation. For stronger security, block in the downstream API
+ * with limited API keys.
+ * @param codeExecutionMode - Whether to execute code in a local Deno environment or in a remote
+ * sandbox environment hosted by Stainless.
*/
-export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool {
+export function codeTool({
+ blockedMethods,
+ codeExecutionMode,
+}: {
+ blockedMethods: SdkMethod[] | undefined;
+ codeExecutionMode: McpCodeExecutionMode;
+}): McpTool {
const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] };
const tool: Tool = {
name: 'execute',
@@ -62,6 +80,7 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und
required: ['code'],
},
};
+
const handler = async ({
reqContext,
args,
@@ -70,9 +89,6 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und
args: any;
}): Promise => {
const code = args.code as string;
- const intent = args.intent as string | undefined;
- const client = reqContext.client;
-
// Do very basic blocking of code that includes forbidden method names.
//
// WARNING: This is not secure against obfuscation and other evasion methods. If
@@ -89,55 +105,258 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und
}
}
- const codeModeEndpoint =
- readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';
-
- // Setting a Stainless API key authenticates requests to the code tool endpoint.
- const res = await fetch(codeModeEndpoint, {
- method: 'POST',
- headers: {
- ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
- 'Content-Type': 'application/json',
- client_envs: JSON.stringify({
- LITHIC_API_KEY: requireValue(
- readEnv('LITHIC_API_KEY') ?? client.apiKey,
- 'set LITHIC_API_KEY environment variable or provide apiKey client option',
- ),
- LITHIC_WEBHOOK_SECRET: readEnv('LITHIC_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined,
- LITHIC_BASE_URL:
- readEnv('LITHIC_BASE_URL') ?? readEnv('LITHIC_ENVIRONMENT') ?
- undefined
- : client.baseURL ?? undefined,
- }),
- },
- body: JSON.stringify({
- project_name: 'lithic',
- code,
- intent,
- client_opts: { environment: (readEnv('LITHIC_ENVIRONMENT') || undefined) as any },
- } satisfies WorkerInput),
- });
+ if (codeExecutionMode === 'local') {
+ return await localDenoHandler({ reqContext, args });
+ } else {
+ return await remoteStainlessHandler({ reqContext, args });
+ }
+ };
+
+ return { metadata, tool, handler };
+}
+
+const remoteStainlessHandler = async ({
+ reqContext,
+ args,
+}: {
+ reqContext: McpRequestContext;
+ args: any;
+}): Promise => {
+ const code = args.code as string;
+ const intent = args.intent as string | undefined;
+ const client = reqContext.client;
- if (!res.ok) {
- throw new Error(
- `${res.status}: ${
- res.statusText
- } error when trying to contact Code Tool server. Details: ${await res.text()}`,
+ const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';
+
+ // Setting a Stainless API key authenticates requests to the code tool endpoint.
+ const res = await fetch(codeModeEndpoint, {
+ method: 'POST',
+ headers: {
+ ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
+ 'Content-Type': 'application/json',
+ client_envs: JSON.stringify({
+ LITHIC_API_KEY: requireValue(
+ readEnv('LITHIC_API_KEY') ?? client.apiKey,
+ 'set LITHIC_API_KEY environment variable or provide apiKey client option',
+ ),
+ LITHIC_WEBHOOK_SECRET: readEnv('LITHIC_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined,
+ LITHIC_BASE_URL:
+ readEnv('LITHIC_BASE_URL') ?? readEnv('LITHIC_ENVIRONMENT') ?
+ undefined
+ : client.baseURL ?? undefined,
+ }),
+ },
+ body: JSON.stringify({
+ project_name: 'lithic',
+ code,
+ intent,
+ client_opts: { environment: (readEnv('LITHIC_ENVIRONMENT') || undefined) as any },
+ } satisfies WorkerInput),
+ });
+
+ if (!res.ok) {
+ throw new Error(
+ `${res.status}: ${
+ res.statusText
+ } error when trying to contact Code Tool server. Details: ${await res.text()}`,
+ );
+ }
+
+ const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput;
+ const hasLogs = log_lines.length > 0 || err_lines.length > 0;
+ const output = {
+ result,
+ ...(log_lines.length > 0 && { log_lines }),
+ ...(err_lines.length > 0 && { err_lines }),
+ };
+ if (is_error) {
+ return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2));
+ }
+ return asTextContentResult(output);
+};
+
+const localDenoHandler = async ({
+ reqContext,
+ args,
+}: {
+ reqContext: McpRequestContext;
+ args: unknown;
+}): Promise => {
+ const client = reqContext.client;
+ const baseURLHostname = new URL(client.baseURL).hostname;
+ const { code } = args as { code: string };
+
+ let denoPath: string;
+
+ const packageRoot = path.resolve(path.dirname(workerPath), '..');
+ const packageNodeModulesPath = path.resolve(packageRoot, 'node_modules');
+
+ // Check if deno is in PATH
+ const { execSync } = await import('node:child_process');
+ try {
+ execSync('command -v deno', { stdio: 'ignore' });
+ denoPath = 'deno';
+ } catch {
+ try {
+ // Use deno binary in node_modules if it's found
+ const denoNodeModulesPath = path.resolve(packageNodeModulesPath, 'deno', 'bin.cjs');
+ await fs.promises.access(denoNodeModulesPath, fs.constants.X_OK);
+ denoPath = denoNodeModulesPath;
+ } catch {
+ return asErrorResult(
+ 'Deno is required for code execution but was not found. ' +
+ 'Install it from https://deno.land or run: npm install deno',
);
}
+ }
+
+ const allowReadPaths = [
+ 'code-tool-worker.mjs',
+ `${workerPath.replace(/([\/\\]node_modules)[\/\\].+$/, '$1')}/`,
+ packageRoot,
+ ];
- const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput;
- const hasLogs = log_lines.length > 0 || err_lines.length > 0;
- const output = {
- result,
- ...(log_lines.length > 0 && { log_lines }),
- ...(err_lines.length > 0 && { err_lines }),
- };
- if (is_error) {
- return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2));
+ // Follow symlinks in node_modules to allow read access to workspace-linked packages
+ try {
+ const sdkPkgName = 'lithic';
+ const sdkDir = path.resolve(packageNodeModulesPath, sdkPkgName);
+ const realSdkDir = fs.realpathSync(sdkDir);
+ if (realSdkDir !== sdkDir) {
+ allowReadPaths.push(realSdkDir);
}
- return asTextContentResult(output);
- };
+ } catch {
+ // Ignore if symlink resolution fails
+ }
- return { metadata, tool, handler };
-}
+ const allowRead = allowReadPaths.join(',');
+
+ const worker = await newDenoHTTPWorker(url.pathToFileURL(workerPath), {
+ denoExecutable: denoPath,
+ runFlags: [
+ `--node-modules-dir=manual`,
+ `--allow-read=${allowRead}`,
+ `--allow-net=${baseURLHostname}`,
+ // Allow environment variables because instantiating the client will try to read from them,
+ // even though they are not set.
+ '--allow-env',
+ ],
+ printOutput: true,
+ spawnOptions: {
+ cwd: path.dirname(workerPath),
+ },
+ });
+
+ try {
+ const resp = await new Promise((resolve, reject) => {
+ worker.addEventListener('exit', (exitCode) => {
+ reject(new Error(`Worker exited with code ${exitCode}`));
+ });
+
+ const opts: ClientOptions = {
+ baseURL: client.baseURL,
+ apiKey: client.apiKey,
+ webhookSecret: client.webhookSecret,
+ defaultHeaders: {
+ 'X-Stainless-MCP': 'true',
+ },
+ };
+
+ const req = worker.request(
+ 'http://localhost',
+ {
+ headers: {
+ 'content-type': 'application/json',
+ },
+ method: 'POST',
+ },
+ (resp) => {
+ const body: Uint8Array[] = [];
+ resp.on('error', (err) => {
+ reject(err);
+ });
+ resp.on('data', (chunk) => {
+ body.push(chunk);
+ });
+ resp.on('end', () => {
+ resolve(
+ new Response(Buffer.concat(body).toString(), {
+ status: resp.statusCode ?? 200,
+ headers: resp.headers as any,
+ }),
+ );
+ });
+ },
+ );
+
+ const body = JSON.stringify({
+ opts,
+ code,
+ });
+
+ req.write(body, (err) => {
+ if (err != null) {
+ reject(err);
+ }
+ });
+
+ req.end();
+ });
+
+ if (resp.status === 200) {
+ const { result, log_lines, err_lines } = (await resp.json()) as WorkerOutput;
+ const returnOutput: ContentBlock | null =
+ result == null ? null : (
+ {
+ type: 'text',
+ text: typeof result === 'string' ? result : JSON.stringify(result),
+ }
+ );
+ const logOutput: ContentBlock | null =
+ log_lines.length === 0 ?
+ null
+ : {
+ type: 'text',
+ text: log_lines.join('\n'),
+ };
+ const errOutput: ContentBlock | null =
+ err_lines.length === 0 ?
+ null
+ : {
+ type: 'text',
+ text: 'Error output:\n' + err_lines.join('\n'),
+ };
+ return {
+ content: [returnOutput, logOutput, errOutput].filter((block) => block !== null),
+ };
+ } else {
+ const { result, log_lines, err_lines } = (await resp.json()) as WorkerOutput;
+ const messageOutput: ContentBlock | null =
+ result == null ? null : (
+ {
+ type: 'text',
+ text: typeof result === 'string' ? result : JSON.stringify(result),
+ }
+ );
+ const logOutput: ContentBlock | null =
+ log_lines.length === 0 ?
+ null
+ : {
+ type: 'text',
+ text: log_lines.join('\n'),
+ };
+ const errOutput: ContentBlock | null =
+ err_lines.length === 0 ?
+ null
+ : {
+ type: 'text',
+ text: 'Error output:\n' + err_lines.join('\n'),
+ };
+ return {
+ content: [messageOutput, logOutput, errOutput].filter((block) => block !== null),
+ isError: true,
+ };
+ }
+ } finally {
+ worker.terminate();
+ }
+};
diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts
index 32a88713..9e9d15cd 100644
--- a/packages/mcp-server/src/options.ts
+++ b/packages/mcp-server/src/options.ts
@@ -19,8 +19,11 @@ export type McpOptions = {
codeAllowHttpGets?: boolean | undefined;
codeAllowedMethods?: string[] | undefined;
codeBlockedMethods?: string[] | undefined;
+ codeExecutionMode: McpCodeExecutionMode;
};
+export type McpCodeExecutionMode = 'stainless-sandbox' | 'local';
+
export function parseCLIOptions(): CLIOptions {
const opts = yargs(hideBin(process.argv))
.option('code-allow-http-gets', {
@@ -40,6 +43,13 @@ export function parseCLIOptions(): CLIOptions {
description:
'Methods to explicitly block for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.',
})
+ .option('code-execution-mode', {
+ type: 'string',
+ choices: ['stainless-sandbox', 'local'],
+ default: 'stainless-sandbox',
+ description:
+ "Where to run code execution in code tool; 'stainless-sandbox' will execute code in Stainless-hosted sandboxes whereas 'local' will execute code locally on the MCP server machine.",
+ })
.option('debug', { type: 'boolean', description: 'Enable debug logging' })
.option('no-tools', {
type: 'string',
@@ -93,6 +103,7 @@ export function parseCLIOptions(): CLIOptions {
codeAllowHttpGets: argv.codeAllowHttpGets,
codeAllowedMethods: argv.codeAllowedMethods,
codeBlockedMethods: argv.codeBlockedMethods,
+ codeExecutionMode: argv.codeExecutionMode as McpCodeExecutionMode,
transport,
port: argv.port,
socket: argv.socket,
@@ -124,6 +135,7 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M
: defaultOptions.includeDocsTools;
return {
+ codeExecutionMode: defaultOptions.codeExecutionMode,
...(docsTools !== undefined && { includeDocsTools: docsTools }),
};
}
diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts
index 65317fd1..993dfeaf 100644
--- a/packages/mcp-server/src/server.ts
+++ b/packages/mcp-server/src/server.ts
@@ -161,6 +161,7 @@ export function selectTools(options?: McpOptions): McpTool[] {
const includedTools = [
codeTool({
blockedMethods: blockedMethodsForCodeTool(options),
+ codeExecutionMode: options?.codeExecutionMode ?? 'stainless-sandbox',
}),
];
if (options?.includeDocsTools ?? true) {
diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts
index 7a2d5114..17306295 100644
--- a/packages/mcp-server/tests/options.test.ts
+++ b/packages/mcp-server/tests/options.test.ts
@@ -1,4 +1,4 @@
-import { parseCLIOptions, parseQueryOptions } from '../src/options';
+import { parseCLIOptions } from '../src/options';
// Mock process.argv
const mockArgv = (args: string[]) => {
@@ -30,21 +30,3 @@ describe('parseCLIOptions', () => {
cleanup();
});
});
-
-describe('parseQueryOptions', () => {
- const defaultOptions = {};
-
- it('default parsing should be empty', () => {
- const query = '';
- const result = parseQueryOptions(defaultOptions, query);
-
- expect(result).toEqual({});
- });
-
- it('should handle invalid query string gracefully', () => {
- const query = 'invalid=value&tools=invalid-operation';
-
- // Should throw due to Zod validation for invalid tools
- expect(() => parseQueryOptions(defaultOptions, query)).toThrow();
- });
-});
From d654204ff237b1ff579d704fd69518ff5f604dc0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 17:06:54 +0000
Subject: [PATCH 09/11] chore(internal): fix MCP Dockerfiles so they can be
built without buildkit
---
packages/mcp-server/Dockerfile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile
index da345c80..aa1c22a1 100644
--- a/packages/mcp-server/Dockerfile
+++ b/packages/mcp-server/Dockerfile
@@ -46,8 +46,8 @@ FROM node:24-alpine
COPY --from=deno_installer /deno /usr/local/bin/deno
# Add in shared libraries needed by Deno
-COPY --from=cc --chown=root:root --chmod=755 /lib/*-linux-gnu/* /usr/local/lib/
-COPY --from=cc --chown=root:root --chmod=755 /lib/ld-linux-* /lib/
+COPY --from=cc /lib/*-linux-gnu/* /usr/local/lib/
+COPY --from=cc /lib/ld-linux-* /lib/
RUN mkdir /lib64 && ln -s /usr/local/lib/ld-linux-* /lib64/
ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib
From b11e3337b479aa0b2eab7ae5aaa914d5ab0cd599 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 19:58:54 +0000
Subject: [PATCH 10/11] chore(internal): fix MCP Dockerfiles so they can be
built without buildkit
---
packages/mcp-server/Dockerfile | 15 ++++-----------
1 file changed, 4 insertions(+), 11 deletions(-)
diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile
index aa1c22a1..9529570b 100644
--- a/packages/mcp-server/Dockerfile
+++ b/packages/mcp-server/Dockerfile
@@ -37,19 +37,11 @@ COPY . .
RUN yarn install --frozen-lockfile && \
yarn build
-FROM denoland/deno:bin-2.6.10 AS deno_installer
-FROM gcr.io/distroless/cc@sha256:66d87e170bc2c5e2b8cf853501141c3c55b4e502b8677595c57534df54a68cc5 AS cc
+FROM denoland/deno:alpine-2.7.1
-FROM node:24-alpine
+# Install node and npm
+RUN apk add --no-cache nodejs npm
-# Install deno
-COPY --from=deno_installer /deno /usr/local/bin/deno
-
-# Add in shared libraries needed by Deno
-COPY --from=cc /lib/*-linux-gnu/* /usr/local/lib/
-COPY --from=cc /lib/ld-linux-* /lib/
-
-RUN mkdir /lib64 && ln -s /usr/local/lib/ld-linux-* /lib64/
ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib
# Add non-root user
@@ -69,6 +61,7 @@ COPY --from=builder /build/dist ./node_modules/lithic
# Change ownership to nodejs user
RUN chown -R nodejs:nodejs /app
+RUN chown -R nodejs:nodejs /deno-dir
# Switch to non-root user
USER nodejs
From bb0f200009133de35eda9f542b8ca328ff1dec71 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Feb 2026 19:59:22 +0000
Subject: [PATCH 11/11] release: 0.130.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 24 ++++++++++++++++++++++++
package.json | 2 +-
packages/mcp-server/manifest.json | 10 +++++++---
packages/mcp-server/package.json | 2 +-
packages/mcp-server/src/server.ts | 2 +-
src/version.ts | 2 +-
7 files changed, 36 insertions(+), 8 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 3eebe245..90ea33ab 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.129.0"
+ ".": "0.130.0"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb63768b..2f37cd08 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
# Changelog
+## 0.130.0 (2026-02-25)
+
+Full Changelog: [v0.129.0...v0.130.0](https://github.com/lithic-com/lithic-node/compare/v0.129.0...v0.130.0)
+
+### Features
+
+* **api:** Add INTEREST_AND_FEES_PAUSED substatus to financial account ([342cb07](https://github.com/lithic-com/lithic-node/commit/342cb0725d8b9d8645e20e7c7ca7a386ce657f36))
+* **api:** Expose MIL interest schedules and loan tape configuration endpoints ([c71b835](https://github.com/lithic-com/lithic-node/commit/c71b835642458ce89a801ac770b4c5c7e2788a60))
+
+
+### Bug Fixes
+
+* **docs/contributing:** correct pnpm link command ([49b3676](https://github.com/lithic-com/lithic-node/commit/49b3676c5eb963fb6608a65b3effb56e2162dc44))
+
+
+### Chores
+
+* **internal:** fix MCP Dockerfiles so they can be built without buildkit ([b11e333](https://github.com/lithic-com/lithic-node/commit/b11e3337b479aa0b2eab7ae5aaa914d5ab0cd599))
+* **internal:** fix MCP Dockerfiles so they can be built without buildkit ([d654204](https://github.com/lithic-com/lithic-node/commit/d654204ff237b1ff579d704fd69518ff5f604dc0))
+* **internal:** make MCP code execution location configurable via a flag ([d7ce232](https://github.com/lithic-com/lithic-node/commit/d7ce232aec15b1fd054e8ef2b29d19d54627fcea))
+* **internal:** upgrade @modelcontextprotocol/sdk and hono ([7e9cc7b](https://github.com/lithic-com/lithic-node/commit/7e9cc7b6d308347668dec9dc874cb0c3048fa17b))
+* **mcp:** correctly update version in sync with sdk ([71e0dc6](https://github.com/lithic-com/lithic-node/commit/71e0dc6a360f33aacffbd34619282022d8128060))
+* update mock server docs ([6657e0d](https://github.com/lithic-com/lithic-node/commit/6657e0d898b31c05c58a76c6babff472fc1dfbef))
+
## 0.129.0 (2026-02-19)
Full Changelog: [v0.128.0...v0.129.0](https://github.com/lithic-com/lithic-node/compare/v0.128.0...v0.129.0)
diff --git a/package.json b/package.json
index 05429a54..a3cc6c7d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "lithic",
- "version": "0.129.0",
+ "version": "0.130.0",
"description": "The official TypeScript library for the Lithic API",
"author": "Lithic ",
"types": "dist/index.d.ts",
diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json
index 1a67c16e..bf005118 100644
--- a/packages/mcp-server/manifest.json
+++ b/packages/mcp-server/manifest.json
@@ -1,7 +1,7 @@
{
"dxt_version": "0.2",
"name": "lithic-mcp",
- "version": "0.128.0",
+ "version": "0.130.0",
"description": "The official MCP Server for the Lithic API",
"author": {
"name": "Lithic",
@@ -18,7 +18,9 @@
"entry_point": "index.js",
"mcp_config": {
"command": "node",
- "args": ["${__dirname}/index.js"],
+ "args": [
+ "${__dirname}/index.js"
+ ],
"env": {
"LITHIC_API_KEY": "${user_config.LITHIC_API_KEY}",
"LITHIC_WEBHOOK_SECRET": "${user_config.LITHIC_WEBHOOK_SECRET}"
@@ -46,5 +48,7 @@
"node": ">=18.0.0"
}
},
- "keywords": ["api"]
+ "keywords": [
+ "api"
+ ]
}
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index e45e0a60..2a29d262 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "lithic-mcp",
- "version": "0.129.0",
+ "version": "0.130.0",
"description": "The official MCP Server for the Lithic API",
"author": "Lithic ",
"types": "dist/index.d.ts",
diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts
index 993dfeaf..ab1cab79 100644
--- a/packages/mcp-server/src/server.ts
+++ b/packages/mcp-server/src/server.ts
@@ -21,7 +21,7 @@ export const newMcpServer = async (stainlessApiKey: string | undefined) =>
new McpServer(
{
name: 'lithic_api',
- version: '0.129.0',
+ version: '0.130.0',
},
{
instructions: await getInstructions(stainlessApiKey),
diff --git a/src/version.ts b/src/version.ts
index 54ba18a0..bfde34bc 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export const VERSION = '0.129.0'; // x-release-please-version
+export const VERSION = '0.130.0'; // x-release-please-version