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