diff --git a/modelcontextprotocol/README.md b/modelcontextprotocol/README.md index c314362..3b3c918 100644 --- a/modelcontextprotocol/README.md +++ b/modelcontextprotocol/README.md @@ -1,4 +1,3 @@ - ## PayPal Model Context Protocol The PayPal [Model Context Protocol](https://modelcontextprotocol.com/) server allows you to integrate with PayPal APIs through function calling. This protocol supports various tools to interact with different PayPal services. @@ -84,6 +83,7 @@ Set `PAYPAL_ENVIRONMENT` value as either `SANDBOX` for stage testing and `PRODUC - `pay_order`: Process payment for an authorized order - `create_refund`: Process a refund for a captured payment. - `get_refund`: Get the details for a specific refund. +- `fetch_bin_data`: Fetch BIN (Bank Identification Number) meta data for the given BIN. (Note: This tool is enabled as `payments.fetchBinData` in the config, but the method name is `fetch_bin_data`.) **Dispute Management** diff --git a/modelcontextprotocol/src/index.ts b/modelcontextprotocol/src/index.ts index 4e9f674..b490880 100644 --- a/modelcontextprotocol/src/index.ts +++ b/modelcontextprotocol/src/index.ts @@ -50,6 +50,7 @@ const ACCEPTED_TOOLS = [ 'transactions.list', 'payments.createRefund', 'payments.getRefunds', + 'payments.fetchBinData', ]; export function parseArgs(args: string[]): Options { diff --git a/typescript/src/shared/api.ts b/typescript/src/shared/api.ts index 54f834b..cc73b72 100644 --- a/typescript/src/shared/api.ts +++ b/typescript/src/shared/api.ts @@ -26,7 +26,8 @@ import { updatePlan, createRefund, getRefund, - updateSubscription + updateSubscription, + fetchBinData } from './functions'; import type { Context } from './configuration'; @@ -47,7 +48,7 @@ class PayPalAPI { if (typeof paypalClientOrAccessToken === 'string') { this.accessToken = paypalClientOrAccessToken; - this.paypalClient = new PayPalClient({context: this.context, accessToken: this.accessToken }); + this.paypalClient = new PayPalClient({ context: this.context, accessToken: this.accessToken }); } else { this.paypalClient = paypalClientOrAccessToken; } @@ -132,6 +133,9 @@ class PayPalAPI { return createRefund(this.paypalClient, this.context, arg); case 'get_refund': return getRefund(this.paypalClient, this.context, arg); + case 'fetch_bin_data': + return fetchBinData(this.paypalClient, this.context, arg); + default: throw new Error(`Invalid method: ${method}`); } diff --git a/typescript/src/shared/functions.ts b/typescript/src/shared/functions.ts index 8ab9b58..db2ec87 100644 --- a/typescript/src/shared/functions.ts +++ b/typescript/src/shared/functions.ts @@ -29,7 +29,8 @@ import { getRefundParameters, createRefundParameters, updateSubscriptionParameters, - updatePlanParameters + updatePlanParameters, + fetchBinDataParameters } from "./parameters"; import {parseOrderDetails, parseUpdateSubscriptionPayload, toQueryString} from "./payloadUtils"; import { TypeOf } from "zod"; @@ -38,6 +39,15 @@ import PayPalClient from './client'; const logger = debug('agent-toolkit:functions'); +// Simple UUID v4 generator +function uuidv4(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + // === INVOICE FUNCTIONS === export async function createInvoice( @@ -882,6 +892,7 @@ export async function listTransactions( } } + export async function getRefund( client: PayPalClient, context: Context, @@ -908,6 +919,179 @@ export async function getRefund( } } + // === BIN DATA FUNCTIONS === + export async function fetchBinData( + client: PayPalClient, + context: Context, + params: TypeOf> + ): Promise { + logger('[fetchBinData] Starting to fetch BIN data'); + + const headers = await client.getHeaders(); + + // Validate BIN format before making the request + const invalidBins = params.bin.filter(bin => { + // BIN should be 6-8 digits and numeric + return !/^\d{6,8}$/.test(bin); + }); + + if (invalidBins.length > 0) { + const errorMessage = `Invalid BIN format(s): ${invalidBins.join(', ')}. BINs must be 6-8 digit numeric strings.`; + logger(`[fetchBinData] ${errorMessage}`); + return { + error: { + message: errorMessage, + type: 'validation_error', + invalid_bins: invalidBins, + valid_format: 'BINs must be 6-8 digit numeric strings (e.g., "400934", "4009348")' + } + }; + } + + // Check if we have a single BIN or multiple BINs + if (params.bin.length === 1) { + // Use single BIN endpoint for single BIN requests + const singleBin = params.bin[0]; + const url = `${client.getBaseUrl()}/v2/payments/bin-search`; + const requestBody = { bin: singleBin }; + + logger(`[fetchBinData] Using single BIN endpoint for BIN: ${singleBin}`); + logger(`[fetchBinData] API URL: ${url}`); + + try { + logger('[fetchBinData] Sending request to PayPal API (single BIN)'); + const response = await axios.post(url, requestBody, { headers }); + + logger(`[fetchBinData] Single BIN Response Details:`); + logger(`[fetchBinData] Status: ${response.status}`); + + // Return the direct response for single BIN requests + return response.data; + } catch (error: any) { + logger('[fetchBinData] Error in single BIN request:', error.message); + return handleBinDataError(error, params.bin); + } + } else { + // Use bulk endpoint for multiple BINs + const url = `${client.getBaseUrl()}/v2/payments/bin-search-bulk`; + logger(`[fetchBinData] Using bulk BIN endpoint for ${params.bin.length} BINs`); + logger(`[fetchBinData] API URL: ${url}`); + + // Prepare the operations array for the bulk request + const operations = params.bin.map(bin => { + // Generate a UUID v4 that matches the required pattern + const uuid = uuidv4(); + + return { + method: "POST", + path: "/v2/payments/bin-search", + bulk_id: uuid, + body: { + bin + } + }; + }); + + const requestBody = { + operations + }; + + logger(`[fetchBinData] URL: ${url}`); + + try { + logger('[fetchBinData] Sending request to PayPal API (bulk)'); + const response = await axios.post(url, requestBody, { headers }); + + logger(`[fetchBinData] Status: ${response.status}`); + + // Parse the bulk response and extract BIN data + if (response.data && response.data.operations && Array.isArray(response.data.operations)) { + const results = response.data.operations.map((operation: any) => { + if (operation.status && operation.status.code === "200" && operation.body) { + return { + success: true, + bin_data: operation.body, + bulk_id: operation.bulk_id + }; + } else { + return { + success: false, + error: operation.status || { code: "unknown", message: "Unknown error" }, + bulk_id: operation.bulk_id + }; + } + }); + + logger(`[fetchBinData] Parsed Results: ${JSON.stringify(results, null, 2)}`); + return { + operations: response.data.operations, + parsed_results: results + }; + } + + return response.data; + } catch (error: any) { + logger('[fetchBinData] Error in bulk request:', error.message); + return handleBinDataError(error, params.bin); + } + } + } + + // Helper function to handle BIN data errors + function handleBinDataError(error: any, providedBins: string[]): any { + logger('[handleBinDataError] Processing BIN data error'); + logger(`[handleBinDataError] Error Message: ${error.message}`); + + if (error.response) { + logger(`[handleBinDataError] Error Response Status: ${error.response.status}`); + + // Handle specific BIN lookup errors + if (error.response.status === 422) { + const errorData = error.response.data; + let binErrorMessage = 'BIN lookup failed with validation error'; + + if (errorData && errorData.details && Array.isArray(errorData.details)) { + const binErrors = errorData.details.map((detail: any) => { + if (detail.field && detail.field.includes('bin')) { + return `${detail.field}: ${detail.description || detail.issue || 'Invalid BIN format'}`; + } + return detail.description || detail.issue || 'Validation error'; + }).filter(Boolean); + + if (binErrors.length > 0) { + binErrorMessage = `BIN validation errors: ${binErrors.join('; ')}`; + } + } + + return { + error: { + message: binErrorMessage, + type: 'bin_validation_error', + status_code: 422, + provided_bins: providedBins, + suggestion: 'Please verify the BIN format. BINs should be 6-8 digit numeric strings representing the first digits of a credit/debit card number.', + raw_error: errorData + } + }; + } + } else if (error.request) { + logger(`[handleBinDataError] No Response Received`); + } else { + logger(`[handleBinDataError] Request Setup Error: ${error.message}`); + } + + // Return a structured error for better handling + return { + error: { + message: `PayPal API error: ${error.message}`, + type: 'paypal_api_error', + provided_bins: providedBins, + raw_error: error.response?.data || error.message + } + }; + } + + export async function updatePlan( client: PayPalClient, context: Context, @@ -1012,8 +1196,8 @@ function handleAxiosError(error: any): never { } catch (e) { // In case of parsing issues, throw a more generic error logger('[handleAxiosError] Error parsing response data, using raw data'); - logger(`[handleAxiosError] Throwing error with message: PayPal API error (${error.response.status}): ${error.response.data}`); - throw new Error(`PayPal API error (${error.response.status}): ${error.response.data}`); + logger(`[handleAxiosError] Throwing error with message: PayPal API error (${error.response.status}): ${JSON.stringify(error.response.data)}`); + throw new Error(`PayPal API error (${error.response.status}): ${JSON.stringify(error.response.data)}`); } } else if (error.request) { // The request was made but no response was received @@ -1028,4 +1212,3 @@ function handleAxiosError(error: any): never { throw new Error(`PayPal API error: ${error.message}`); } } - diff --git a/typescript/src/shared/parameters.ts b/typescript/src/shared/parameters.ts index b762e8b..a414b1d 100644 --- a/typescript/src/shared/parameters.ts +++ b/typescript/src/shared/parameters.ts @@ -84,6 +84,12 @@ export const generateInvoiceQrCodeParameters = (context: Context) => z.object({ height: z.number().default(300).describe("The QR code height") }).describe("generate invoice qr code request payload"); +// === BIN DATA PARAMETERS === + + export const fetchBinDataParameters = (context: Context) => z.object({ + bin: z.array(z.string().min(6)).max(8).describe('An array of BINs to fetch metadata for. Each BIN must be a numeric string of 6 to 8 digits.') + }); + export const updateProductParameters = (context: Context) => z.object({ diff --git a/typescript/src/shared/prompts.ts b/typescript/src/shared/prompts.ts index d2c4b0d..4cd9f63 100644 --- a/typescript/src/shared/prompts.ts +++ b/typescript/src/shared/prompts.ts @@ -268,3 +268,15 @@ Response details include: - Refunded amount and currency `; + +// === BIN DATA PROMPTS === + + export const fetchBinDataPrompt = (context: Context) => ` + Fetch BIN metadata in bulk. + This function allows you to fetch metadata for multiple Bank Identification Numbers (BINs) in a single request. + It's useful for retrieving card details like card type, issuing bank, and country information for multiple card numbers at once. + Required parameters: + - bin: An array of BINs to fetch metadata for. Each BIN must be a numeric string of 6 to 8 digits. + The API will return metadata for each BIN, including information such as card type, issuing bank, country, etc. + + `; diff --git a/typescript/src/shared/tools.ts b/typescript/src/shared/tools.ts index c47e7e5..5733cfe 100644 --- a/typescript/src/shared/tools.ts +++ b/typescript/src/shared/tools.ts @@ -31,6 +31,7 @@ import { updateSubscriptionPrompt, getRefundPrompt, createRefundPrompt, + fetchBinDataPrompt, } from './prompts'; import { @@ -64,6 +65,7 @@ import { updateSubscriptionParameters, getRefundParameters, createRefundParameters, + fetchBinDataParameters, } from './parameters'; import type { Context } from './configuration'; @@ -410,7 +412,18 @@ const tools = (context: Context): Tool[] => [ getRefunds: true, }, }, - } + }, + { + method: 'fetch_bin_data', + name: 'Fetch Bin Meta Data', + description: fetchBinDataPrompt(context), + parameters: fetchBinDataParameters(context), + actions: { + payments: { + fetchBinData: true, + }, + }, + } ]; const allActions = tools({}).reduce((acc, tool) => { Object.keys(tool.actions).forEach(product => {