Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modelcontextprotocol/README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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**

Expand Down
1 change: 1 addition & 0 deletions modelcontextprotocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const ACCEPTED_TOOLS = [
'transactions.list',
'payments.createRefund',
'payments.getRefunds',
'payments.fetchBinData',
];

export function parseArgs(args: string[]): Options {
Expand Down
8 changes: 6 additions & 2 deletions typescript/src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
updatePlan,
createRefund,
getRefund,
updateSubscription
updateSubscription,
fetchBinData
} from './functions';

import type { Context } from './configuration';
Expand All @@ -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;
}
Expand Down Expand Up @@ -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}`);
}
Expand Down
191 changes: 187 additions & 4 deletions typescript/src/shared/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import {
getRefundParameters,
createRefundParameters,
updateSubscriptionParameters,
updatePlanParameters
updatePlanParameters,
fetchBinDataParameters
} from "./parameters";
import {parseOrderDetails, parseUpdateSubscriptionPayload, toQueryString} from "./payloadUtils";
import { TypeOf } from "zod";
Expand All @@ -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(
Expand Down Expand Up @@ -882,6 +892,7 @@ export async function listTransactions(
}
}


export async function getRefund(
client: PayPalClient,
context: Context,
Expand All @@ -908,6 +919,179 @@ export async function getRefund(
}
}

// === BIN DATA FUNCTIONS ===
export async function fetchBinData(
client: PayPalClient,
context: Context,
params: TypeOf<ReturnType<typeof fetchBinDataParameters>>
): Promise<any> {
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
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log only the URL, DebugId and response status or error info incase for error. Please remove rest of the logs


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}`);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure logs wont any data unless it is an error trace


// 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,
Expand Down Expand Up @@ -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
Expand All @@ -1028,4 +1212,3 @@ function handleAxiosError(error: any): never {
throw new Error(`PayPal API error: ${error.message}`);
}
}

6 changes: 6 additions & 0 deletions typescript/src/shared/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
12 changes: 12 additions & 0 deletions typescript/src/shared/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.

`;
15 changes: 14 additions & 1 deletion typescript/src/shared/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
updateSubscriptionPrompt,
getRefundPrompt,
createRefundPrompt,
fetchBinDataPrompt,
} from './prompts';

import {
Expand Down Expand Up @@ -64,6 +65,7 @@ import {
updateSubscriptionParameters,
getRefundParameters,
createRefundParameters,
fetchBinDataParameters,
} from './parameters';

import type { Context } from './configuration';
Expand Down Expand Up @@ -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 => {
Expand Down