@payware/mcp-server 1.3.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.env.example +4 -0
  2. package/README.md +18 -0
  3. package/package.json +7 -9
  4. package/src/auth/rsa-keys.js +12 -6
  5. package/src/auth/sandbox-setup.js +2 -0
  6. package/src/config/toolsets.js +104 -0
  7. package/src/core/auth/jwt-factory.js +4 -1
  8. package/src/core/auth/jwt-token.js +4 -0
  9. package/src/index.js +10 -6
  10. package/src/isv/data/index.js +15 -14
  11. package/src/isv/deep-links/delete-product-link.js +3 -2
  12. package/src/isv/deep-links/delete-transaction-link.js +3 -2
  13. package/src/isv/deep-links/get-product-link.js +8 -6
  14. package/src/isv/deep-links/get-transaction-link.js +8 -6
  15. package/src/isv/index.js +6 -0
  16. package/src/isv/invitations/index.js +5 -0
  17. package/src/isv/oauth2/get-token-info.js +3 -0
  18. package/src/isv/oauth2/obtain-token.js +3 -0
  19. package/src/isv/oauth2/token-helper.js +2 -0
  20. package/src/isv/poi/cancel-price.js +2 -0
  21. package/src/isv/poi/get-poi.js +2 -0
  22. package/src/isv/poi/get-qrcode.js +40 -9
  23. package/src/isv/poi/get-status.js +2 -0
  24. package/src/isv/poi/list-pois.js +2 -0
  25. package/src/isv/poi/set-price.js +2 -0
  26. package/src/isv/products/audios.js +8 -2
  27. package/src/isv/products/create-product.js +2 -0
  28. package/src/isv/products/delete-product.js +2 -0
  29. package/src/isv/products/get-product-image.js +2 -0
  30. package/src/isv/products/get-product.js +2 -0
  31. package/src/isv/products/list-products.js +2 -0
  32. package/src/isv/products/schedules.js +6 -0
  33. package/src/isv/products/update-product.js +2 -0
  34. package/src/isv/reference/index.js +2 -0
  35. package/src/isv/transactions/cancel-transaction.js +2 -0
  36. package/src/isv/transactions/create-transaction.js +14 -2
  37. package/src/isv/transactions/get-status.js +3 -1
  38. package/src/isv/transactions/process-transaction.js +14 -2
  39. package/src/isv/transactions/simulate-callback.js +2 -0
  40. package/src/isv/transactions/transaction-history.js +2 -0
  41. package/src/merchant/data/export-report.js +2 -0
  42. package/src/merchant/data/generate-report.js +2 -0
  43. package/src/merchant/data/get-export-list.js +2 -0
  44. package/src/merchant/data/get-report-requests.js +2 -0
  45. package/src/merchant/data/get-report-status.js +2 -0
  46. package/src/merchant/data/index.js +15 -10
  47. package/src/merchant/data/list-reports.js +2 -0
  48. package/src/merchant/deep-links/delete-product-link.js +3 -2
  49. package/src/merchant/deep-links/delete-transaction-link.js +3 -2
  50. package/src/merchant/deep-links/get-product-link.js +8 -6
  51. package/src/merchant/deep-links/get-transaction-link.js +8 -6
  52. package/src/merchant/index.js +7 -1
  53. package/src/merchant/logs/index.js +4 -0
  54. package/src/merchant/products/audios.js +15 -14
  55. package/src/merchant/products/create-product.js +3 -2
  56. package/src/merchant/products/delete-product.js +3 -2
  57. package/src/merchant/products/get-product-image.js +3 -2
  58. package/src/merchant/products/get-product.js +3 -2
  59. package/src/merchant/products/list-products.js +3 -2
  60. package/src/merchant/products/schedules.js +11 -10
  61. package/src/merchant/products/update-product.js +3 -2
  62. package/src/merchant/transactions/cancel-transaction.js +4 -3
  63. package/src/merchant/transactions/create-transaction.js +14 -2
  64. package/src/merchant/transactions/get-status.js +4 -3
  65. package/src/merchant/transactions/process-transaction.js +17 -6
  66. package/src/merchant/transactions/simulate-callback.js +3 -0
  67. package/src/merchant/transactions/transaction-history.js +5 -4
  68. package/src/payment-institution/data/index.js +15 -14
  69. package/src/payment-institution/deep-links/delete-transaction-link.js +3 -2
  70. package/src/payment-institution/deep-links/get-transaction-link.js +8 -6
  71. package/src/payment-institution/transactions/create-pi-transaction.js +15 -4
  72. package/src/payment-institution/transactions/finalize-transaction.js +7 -4
  73. package/src/payment-institution/transactions/get-transaction-history.js +7 -4
  74. package/src/payment-institution/transactions/get-transaction-status.js +15 -12
  75. package/src/payment-institution/transactions/process-pi-transaction.js +19 -8
  76. package/src/payment-institution/transactions/simulate-pi-callback.js +3 -0
  77. package/src/payment-institution/transactions/soundbite-transaction.js +5 -4
  78. package/src/proxy-server.js +25 -0
  79. package/src/shared/placements/api.js +51 -0
  80. package/src/shared/placements/tools.js +519 -0
  81. package/src/shared/poi/poi-create-api.js +3 -0
  82. package/src/shared/products/products-api.js +36 -25
  83. package/src/shared/shops/shops-api.js +2 -0
  84. package/src/shared/tool-annotations.js +53 -0
  85. package/src/tools/advanced-code-generator.js +3 -0
  86. package/src/utils/format-request.js +2 -0
  87. package/src/utils/json-formatter.js +2 -0
  88. package/src/utils/server-info.js +2 -0
  89. package/src/utils/code-examples.js +0 -646
@@ -3,6 +3,7 @@ import { createJWTToken } from '../../core/auth/jwt-token.js';
3
3
  import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
4
4
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
5
5
  import { apiErrorResult } from '../../shared/api-errors.js';
6
+ import { CREATES } from '../../shared/tool-annotations.js';
6
7
 
7
8
  /**
8
9
  * Create a payment institution transaction via payware API
@@ -36,6 +37,7 @@ export async function createPITransaction({
36
37
  barBarHeight,
37
38
  barFontSize,
38
39
  barHumanReadableLocation,
40
+ idempotencyKey,
39
41
  useSandbox = true
40
42
  }) {
41
43
  if (!role || !['SRC', 'DST'].includes(role)) {
@@ -121,7 +123,8 @@ export async function createPITransaction({
121
123
  const headers = {
122
124
  'Authorization': `Bearer ${tokenData.token}`,
123
125
  'Content-Type': 'application/json',
124
- 'Api-Version': '1' // Required: current API version
126
+ 'Api-Version': '1', // Required: current API version
127
+ ...(idempotencyKey && { 'Idempotency-Key': idempotencyKey })
125
128
  };
126
129
 
127
130
  try {
@@ -138,6 +141,7 @@ export async function createPITransaction({
138
141
  success: true,
139
142
  transaction: response.data,
140
143
  requestId: response.headers['x-request-id'],
144
+ replayed: response.headers?.['idempotent-replayed'] === 'true',
141
145
  timestamp: new Date().toISOString()
142
146
  };
143
147
  } catch (error) {
@@ -150,6 +154,7 @@ export async function createPITransaction({
150
154
  */
151
155
  export const createPITransactionTool = {
152
156
  name: "payware_operations_create_transaction",
157
+ annotations: CREATES,
153
158
  description: `Create a new payware transaction as a payment institution. Payment institutions must specify their role and account details.
154
159
 
155
160
  **Payment Institution Features:**
@@ -183,6 +188,11 @@ export const createPITransactionTool = {
183
188
  inputSchema: {
184
189
  type: "object",
185
190
  properties: {
191
+ idempotencyKey: {
192
+ type: "string",
193
+ description: "Optional. Sent as the Idempotency-Key header. Use one value per payment (a UUID is typical) and the same value on every retry of it: a retry with the same key and body within 24 hours returns the transaction the first call created instead of a new one, and the result reports replayed: true. The same key with a different body is refused (422 ERR_IDEMPOTENCY_KEY_REUSED); a retry while the first call still runs gets 409 ERR_IDEMPOTENCY_KEY_IN_PROGRESS.",
194
+ pattern: "^[\\x21-\\x7E]{1,255}$"
195
+ },
186
196
  role: {
187
197
  type: "string",
188
198
  enum: ["SRC", "DST"],
@@ -245,8 +255,7 @@ export const createPITransactionTool = {
245
255
  },
246
256
  privateKey: {
247
257
  type: "string",
248
- description: "RSA private key for JWT token creation. Uses environment-specific private key as default.",
249
- default: getPrivateKeySafe(true)
258
+ description: "RSA private key for JWT token creation. Uses environment-specific private key as default."
250
259
  },
251
260
  passbackParams: {
252
261
  type: "string",
@@ -347,6 +356,7 @@ export const createPITransactionTool = {
347
356
  barBarHeight,
348
357
  barFontSize,
349
358
  barHumanReadableLocation,
359
+ idempotencyKey,
350
360
  useSandbox = true
351
361
  } = args;
352
362
 
@@ -396,6 +406,7 @@ export const createPITransactionTool = {
396
406
  barBarHeight,
397
407
  barFontSize,
398
408
  barHumanReadableLocation,
409
+ idempotencyKey,
399
410
  useSandbox
400
411
  });
401
412
 
@@ -437,7 +448,7 @@ ${JSON.stringify(result.transaction, null, 2)}
437
448
  **Next Steps:**
438
449
  1. Save the transaction ID: \`${result.transaction.transactionId || 'N/A'}\`
439
450
  2. ${role === 'SRC' ? 'Share transaction ID with payee for processing' : 'Wait for payer to process this transaction'}
440
- 3. Use \`payware_pi_get_transaction_status\` to check status
451
+ 3. Use \`payware_operations_get_transaction_status\` to check status
441
452
  4. Monitor callback notifications (if callback URL provided)
442
453
  5. ${role === 'SRC' ? 'Complete payment through payware interface' : 'Handle incoming payment processing'}`
443
454
  }]
@@ -3,6 +3,7 @@ import { createJWTToken } from '../../core/auth/jwt-token.js';
3
3
  import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
4
4
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
5
5
  import { apiErrorResult } from '../../shared/api-errors.js';
6
+ import { COMMITS } from '../../shared/tool-annotations.js';
6
7
 
7
8
  /**
8
9
  * Finalize a transaction as a payment institution via payware API
@@ -98,6 +99,7 @@ export async function finalizeTransaction({
98
99
  */
99
100
  export const finalizeTransactionTool = {
100
101
  name: "payware_operations_finalize_transaction",
102
+ annotations: COMMITS,
101
103
  description: `Finalize a transaction as a payment institution. This updates the transaction status after execution in your payment network.
102
104
 
103
105
  **Payment Institution Finalization:**
@@ -164,8 +166,7 @@ export const finalizeTransactionTool = {
164
166
  },
165
167
  privateKey: {
166
168
  type: "string",
167
- description: "RSA private key for JWT token creation. Uses environment-specific private key as default.",
168
- default: getPrivateKeySafe()
169
+ description: "RSA private key for JWT token creation. Uses environment-specific private key as default."
169
170
  },
170
171
  useSandbox: {
171
172
  type: "boolean",
@@ -233,6 +234,8 @@ export const finalizeTransactionTool = {
233
234
  privateKey,
234
235
  useSandbox
235
236
  });
237
+ // The success text quotes the endpoint. The helper above keeps its own baseUrl, so it needs one here.
238
+ const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
236
239
 
237
240
  if (result.success) {
238
241
  const statusEmojis = {
@@ -277,7 +280,7 @@ ${JSON.stringify(result.transaction, null, 2)}
277
280
  ${status === 'CONFIRMED' ?
278
281
  `1. ✅ Payment completed successfully
279
282
  2. Transaction funds should be processed in your network
280
- 3. Use \`payware_pi_get_transaction_history\` to retrieve details later` :
283
+ 3. Use \`payware_operations_get_transaction_history\` to retrieve details later` :
281
284
  `1. ⚠️ Payment not completed due to: ${statusMessage || status}
282
285
  2. Transaction marked as ${status} in payware system
283
286
  3. All parties notified via callbacks`
@@ -310,7 +313,7 @@ ${JSON.stringify(result.error.details || result.error, null, 2)}
310
313
  **Timestamp:** ${result.timestamp}
311
314
 
312
315
  **Common Finalization Issues:**
313
- 1. **ERR_MISSING_TRANSACTION**: Transaction ID not found or expired
316
+ 1. **ERR_MISSING_TRANSACTION**: Transaction ID not found. **ERR_TRANSACTION_EXPIRED** (also 404): the time to live ran out before the finalize arrived, and the transaction is closed as unpaid
314
317
  2. **ERR_TRANSACTION_NOT_PROCESSED**: Transaction hasn't been processed yet
315
318
  3. **ERR_ALREADY_PROCESSED**: Transaction already finalized
316
319
  4. **ERR_INVALID_STATUS**: Invalid status value provided
@@ -2,6 +2,7 @@ import axios from 'axios';
2
2
  import { createJWTToken } from '../../core/auth/jwt-token.js';
3
3
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
4
4
  import { apiErrorResult } from '../../shared/api-errors.js';
5
+ import { READS } from '../../shared/tool-annotations.js';
5
6
 
6
7
  /**
7
8
  * Get transaction history via payware API (completed/expired transactions)
@@ -54,6 +55,7 @@ export async function getPITransactionHistory({
54
55
  */
55
56
  export const getPITransactionHistoryTool = {
56
57
  name: "payware_operations_get_transaction_history",
58
+ annotations: READS,
57
59
  description: `Get the complete history of a completed or expired payware transaction as a payment institution.
58
60
 
59
61
  **Use Cases:**
@@ -86,8 +88,7 @@ export const getPITransactionHistoryTool = {
86
88
  },
87
89
  privateKey: {
88
90
  type: "string",
89
- description: "RSA private key for JWT token creation. Uses environment-specific private key as default.",
90
- default: getPrivateKeySafe()
91
+ description: "RSA private key for JWT token creation. Uses environment-specific private key as default."
91
92
  },
92
93
  useSandbox: {
93
94
  type: "boolean",
@@ -124,6 +125,8 @@ export const getPITransactionHistoryTool = {
124
125
  privateKey,
125
126
  useSandbox
126
127
  });
128
+ // The success text quotes the endpoint. The helper above keeps its own baseUrl, so it needs one here.
129
+ const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
127
130
 
128
131
  if (result.success) {
129
132
  const tx = result.transaction;
@@ -235,7 +238,7 @@ ${JSON.stringify(result.error.details || result.error, null, 2)}
235
238
  1. **ERR_MISSING_TRANSACTION (404)**: Transaction not found in history
236
239
  2. **HTTP 429 (Too Many Requests)**: API is throttled, implement retry logic
237
240
  3. **Authentication Issues**: Verify JWT token and partner credentials
238
- 4. **Transaction Still Active**: Use \`payware_pi_get_transaction_status\` for active transactions
241
+ 4. **Transaction Still Active**: Use \`payware_operations_get_transaction_status\` for active transactions
239
242
 
240
243
  **Troubleshooting:**
241
244
  1. Verify transaction ID is correct: 10 characters total, starts with 'pw' (standard), 'pr' (product), or 'ps' (soundbite)
@@ -251,7 +254,7 @@ ${JSON.stringify(result.error.details || result.error, null, 2)}
251
254
  - Avoid excessive polling of this endpoint
252
255
 
253
256
  **Alternative Actions:**
254
- - If transaction is still active: Use \`payware_pi_get_transaction_status\`
257
+ - If transaction is still active: Use \`payware_operations_get_transaction_status\`
255
258
  - If rate limited: Wait and retry after a delay
256
259
  - If transaction not found: Verify the transaction ID exists`
257
260
  }]
@@ -2,6 +2,7 @@ import axios from 'axios';
2
2
  import { createJWTToken } from '../../core/auth/jwt-token.js';
3
3
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
4
4
  import { apiErrorResult } from '../../shared/api-errors.js';
5
+ import { READS } from '../../shared/tool-annotations.js';
5
6
 
6
7
  /**
7
8
  * Get transaction status via payware API (active transactions)
@@ -54,6 +55,7 @@ export async function getPITransactionStatus({
54
55
  */
55
56
  export const getPITransactionStatusTool = {
56
57
  name: "payware_operations_get_transaction_status",
58
+ annotations: READS,
57
59
  description: `Get the current status of an active payware transaction as a financial institution.
58
60
 
59
61
  **Use Cases:**
@@ -96,7 +98,7 @@ Four rules a payer-facing app must follow:
96
98
  4. **A missing field is usually a plan boundary, not an error.** \`imageUrl\` and \`termsUrl\` are
97
99
  Standard and above; \`termsText\` and the descriptions are universal.
98
100
 
99
- For a \`pr\`/\`ps\` identifier the values are resolved live (product -> shop -> merchant fallback, then
101
+ For a \`pr\`, \`pl\` or \`ps\` identifier the values are resolved live (product -> shop -> merchant fallback, then
100
102
  plan gating); for the resulting \`pw\` transaction they are the snapshot frozen at creation, so what
101
103
  you get always reflects what the payer could have been shown at scan time.
102
104
 
@@ -105,14 +107,14 @@ strings at the smallest unit their own currency can be paid in - not always two
105
107
  **the value to echo back unchanged at finalize**; \`feeFixed\` and \`feeRate\` are informational, and
106
108
  recomputing the fee from them is rejected with ERR_FEE_MISMATCH.
107
109
 
108
- **Note:** This endpoint only returns active transactions. Use \`payware_pi_get_transaction_history\` for completed/expired transactions.`,
110
+ **Note:** This endpoint only returns active transactions. Use \`payware_operations_get_transaction_history\` for completed/expired transactions.`,
109
111
  inputSchema: {
110
112
  type: "object",
111
113
  properties: {
112
114
  transactionId: {
113
115
  type: "string",
114
- description: "Transaction ID to check status. Format: 2-char prefix + 8-char ID. Supports: 'pw' (standard), 'pr' (product), 'ps' (soundbite)",
115
- pattern: "^(pw|pr|ps)[0-9A-Za-z]{8}$"
116
+ description: "Transaction ID to check status. Format: 2-char prefix + 8-char ID. Supports: 'pw' (standard), 'pr' (product), 'pl' (placement: pays as its product, and the payment is attributed to the placement), 'ps' (soundbite)",
117
+ pattern: "^(pw|pr|pl|ps)[0-9A-Za-z]{8}$"
116
118
  },
117
119
  partnerId: {
118
120
  type: "string",
@@ -121,8 +123,7 @@ recomputing the fee from them is rejected with ERR_FEE_MISMATCH.
121
123
  },
122
124
  privateKey: {
123
125
  type: "string",
124
- description: "RSA private key for JWT token creation. Uses environment-specific private key as default.",
125
- default: getPrivateKeySafe()
126
+ description: "RSA private key for JWT token creation. Uses environment-specific private key as default."
126
127
  },
127
128
  useSandbox: {
128
129
  type: "boolean",
@@ -159,6 +160,8 @@ recomputing the fee from them is rejected with ERR_FEE_MISMATCH.
159
160
  privateKey,
160
161
  useSandbox
161
162
  });
163
+ // The success text quotes the endpoint. The helper above keeps its own baseUrl, so it needs one here.
164
+ const baseUrl = useSandbox ? getSandboxUrl() : getProductionUrl();
162
165
 
163
166
  if (result.success) {
164
167
  const tx = result.transaction;
@@ -209,8 +212,8 @@ ${JSON.stringify(result.transaction, null, 2)}
209
212
  **Next Steps:**
210
213
  1. ${!tx.payerAccount ? 'Transaction waiting for payer to process' : 'Transaction processed, waiting for finalization'}
211
214
  2. ${tx.timeToLive && parseInt(tx.timeToLive) < 60 ? '⚠️ Transaction expires soon!' : 'Monitor remaining TTL'}
212
- 3. Use \`payware_pi_process_transaction\` if you need to process this transaction
213
- 4. Use \`payware_pi_finalize_transaction\` to confirm/decline after processing
215
+ 3. Use \`payware_operations_process_transaction\` if you need to process this transaction
216
+ 4. Use \`payware_operations_finalize_transaction\` to confirm/decline after processing
214
217
 
215
218
  **⚠️ Transaction State:**
216
219
  - This is an **active** transaction
@@ -240,18 +243,18 @@ ${JSON.stringify(result.error.details || result.error, null, 2)}
240
243
 
241
244
  **Common Issues:**
242
245
  1. **ERR_MISSING_TRANSACTION (404)**: Transaction not found, expired, or already completed
243
- 2. **Invalid Transaction ID**: Check transaction ID format (10 chars: 'pw'/'pr'/'ps' + 8 chars)
246
+ 2. **Invalid Transaction ID**: Check transaction ID format (10 chars: 'pw'/'pr'/'pl'/'ps' + 8 chars)
244
247
  3. **Authentication Issues**: Verify JWT token and partner credentials
245
248
 
246
249
  **Troubleshooting:**
247
- 1. Verify transaction ID is correct: 10 characters total, starts with 'pw' (standard), 'pr' (product), or 'ps' (soundbite)
250
+ 1. Verify transaction ID is correct: 10 characters total, starts with 'pw' (standard), 'pr' (product), 'pl' (placement) or 'ps' (soundbite)
248
251
  2. Check if transaction has expired or been completed
249
- 3. Use \`payware_pi_get_transaction_history\` for completed/expired transactions
252
+ 3. Use \`payware_operations_get_transaction_history\` for completed/expired transactions
250
253
  4. Verify your partner credentials and API access
251
254
  5. Ensure transaction was created or you have access to it
252
255
 
253
256
  **Alternative Actions:**
254
- - If transaction is completed: Use \`payware_pi_get_transaction_history\`
257
+ - If transaction is completed: Use \`payware_operations_get_transaction_history\`
255
258
  - If transaction ID is wrong: Verify the correct transaction ID
256
259
  - If access issues: Check your payment institution credentials`
257
260
  }]
@@ -3,6 +3,7 @@ import { createJWTToken } from '../../core/auth/jwt-token.js';
3
3
  import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
4
4
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
5
5
  import { apiErrorResult } from '../../shared/api-errors.js';
6
+ import { COMMITS } from '../../shared/tool-annotations.js';
6
7
 
7
8
  /**
8
9
  * Process a transaction as a payment institution via payware API
@@ -27,6 +28,7 @@ export async function processPITransaction({
27
28
  deliveryAddress,
28
29
  // Payment method for POI transactions (Payment Choice Architecture)
29
30
  paymentMethod,
31
+ idempotencyKey,
30
32
  useSandbox = true
31
33
  }) {
32
34
  if (!transactionId) {
@@ -84,7 +86,8 @@ export async function processPITransaction({
84
86
  const headers = {
85
87
  'Authorization': `Bearer ${tokenData.token}`,
86
88
  'Content-Type': 'application/json',
87
- 'Api-Version': '1' // Required: current API version
89
+ 'Api-Version': '1', // Required: current API version
90
+ ...(idempotencyKey && { 'Idempotency-Key': idempotencyKey })
88
91
  };
89
92
 
90
93
  try {
@@ -100,6 +103,7 @@ export async function processPITransaction({
100
103
  success: true,
101
104
  transaction: response.data,
102
105
  requestId: response.headers['x-request-id'],
106
+ replayed: response.headers?.['idempotent-replayed'] === 'true',
103
107
  timestamp: new Date().toISOString()
104
108
  };
105
109
  } catch (error) {
@@ -112,6 +116,7 @@ export async function processPITransaction({
112
116
  */
113
117
  export const processPITransactionTool = {
114
118
  name: "payware_operations_process_transaction",
119
+ annotations: COMMITS,
115
120
  description: `Process a transaction as a payment institution. Payment institutions can process transactions created by others and must provide account/friendlyName details.
116
121
 
117
122
  **Use Cases:**
@@ -146,10 +151,15 @@ export const processPITransactionTool = {
146
151
  inputSchema: {
147
152
  type: "object",
148
153
  properties: {
154
+ idempotencyKey: {
155
+ type: "string",
156
+ description: "Optional. Sent as the Idempotency-Key header. Use one value per payment (a UUID is typical) and the same value on every retry of it: a retry with the same key and body within 24 hours returns the transaction the first call created instead of a new one, and the result reports replayed: true. The same key with a different body is refused (422 ERR_IDEMPOTENCY_KEY_REUSED); a retry while the first call still runs gets 409 ERR_IDEMPOTENCY_KEY_IN_PROGRESS.",
157
+ pattern: "^[\\x21-\\x7E]{1,255}$"
158
+ },
149
159
  transactionId: {
150
160
  type: "string",
151
- description: "Transaction ID to process. Format: 2-char prefix + 8-char ID. Supports: 'pw' (standard), 'pr' (product), 'ps' (soundbite)",
152
- pattern: "^(pw|pr|ps)[0-9A-Za-z]{8}$"
161
+ description: "Transaction ID to process. Format: 2-char prefix + 8-char ID. Supports: 'pw' (standard), 'pr' (product), 'pl' (placement: pays as its product, and the payment is attributed to the placement), 'ps' (soundbite)",
162
+ pattern: "^(pw|pr|pl|ps)[0-9A-Za-z]{8}$"
153
163
  },
154
164
  account: {
155
165
  type: "string",
@@ -205,8 +215,7 @@ export const processPITransactionTool = {
205
215
  },
206
216
  privateKey: {
207
217
  type: "string",
208
- description: "RSA private key for JWT token creation. Uses environment-specific private key as default.",
209
- default: getPrivateKeySafe()
218
+ description: "RSA private key for JWT token creation. Uses environment-specific private key as default."
210
219
  },
211
220
  // Payment method for POI transactions
212
221
  paymentMethod: {
@@ -293,6 +302,7 @@ export const processPITransactionTool = {
293
302
  privateKey = getPrivateKeySafe(args.useSandbox ?? true),
294
303
  paymentMethod,
295
304
  deliveryAddress,
305
+ idempotencyKey,
296
306
  useSandbox = true
297
307
  } = args;
298
308
 
@@ -339,6 +349,7 @@ export const processPITransactionTool = {
339
349
  privateKey,
340
350
  paymentMethod,
341
351
  deliveryAddress,
352
+ idempotencyKey,
342
353
  useSandbox
343
354
  });
344
355
 
@@ -373,7 +384,7 @@ ${JSON.stringify(result.transaction, null, 2)}
373
384
  1. **TRANSACTION_PROCESSED** callback will be sent to counterparty
374
385
  2. Wait for callback notification to your URL (if provided)
375
386
  3. Monitor transaction status for completion
376
- 4. Use \`payware_pi_finalize_transaction\` to confirm/decline the transaction
387
+ 4. Use \`payware_operations_finalize_transaction\` to confirm/decline the transaction
377
388
  5. Handle final TRANSACTION_FINALIZED callbacks
378
389
 
379
390
  **⚠️ Important:**
@@ -403,7 +414,7 @@ ${JSON.stringify(result.error.details || result.error, null, 2)}
403
414
  **Timestamp:** ${result.timestamp}
404
415
 
405
416
  **Common Payment Institution Issues:**
406
- 1. **ERR_MISSING_TRANSACTION**: Transaction ID not found or expired
417
+ 1. **ERR_MISSING_TRANSACTION**: Transaction ID not found. **ERR_TRANSACTION_EXPIRED** (also 404): its time to live has run out and it is closed as unpaid
407
418
  2. **ERR_ALREADY_PROCESSED**: Transaction already processed
408
419
  3. **ERR_MISSING_PARTNER_ACCOUNT**: Account parameter missing or empty
409
420
  4. **ERR_MISSING_FRIENDLY_NAME**: Friendly name parameter missing
@@ -412,7 +423,7 @@ ${JSON.stringify(result.error.details || result.error, null, 2)}
412
423
  7. **ERR_SPECIFYING_CURRENCY_NOT_ALLOWED**: Cannot change currency on merchant transactions
413
424
 
414
425
  **Troubleshooting:**
415
- 1. Verify transaction ID is correct: 10 characters total, starts with 'pw' (standard), 'pr' (product), or 'ps' (soundbite)
426
+ 1. Verify transaction ID is correct: 10 characters total, starts with 'pw' (standard), 'pr' (product), 'pl' (placement) or 'ps' (soundbite)
416
427
  2. Ensure account and friendlyName are provided
417
428
  3. Check currency matches original transaction currency
418
429
  4. Verify callback URL is provided (especially for payee role)
@@ -10,6 +10,8 @@
10
10
  * @param {Object} options - Additional options
11
11
  * @returns {Object} Mock callback payload
12
12
  */
13
+ import { LOCAL } from '../../shared/tool-annotations.js';
14
+
13
15
  export function generateMockPICallback(transactionId, callbackType = 'TRANSACTION_PROCESSED', options = {}) {
14
16
  const {
15
17
  amount = '57.00',
@@ -146,6 +148,7 @@ export async function simulatePICallbackDelivery(callbackUrl, payload) {
146
148
  */
147
149
  export const simulatePICallbackTool = {
148
150
  name: "payware_operations_simulate_callback",
151
+ annotations: LOCAL,
149
152
  description: "Simulate payment institution callback scenarios for testing webhook handling. Payment institutions receive TRANSACTION_PROCESSED when merchant processes their transaction, and TRANSACTION_FINALIZED when transaction is completed.",
150
153
  inputSchema: {
151
154
  type: "object",
@@ -4,6 +4,7 @@ import { createJWTToken } from '../../core/auth/jwt-token.js';
4
4
  import { getSandboxUrl, getProductionUrl, getPartnerIdSafe, getPrivateKeySafe } from '../../config/env.js';
5
5
  import { readFileSync } from 'fs';
6
6
  import { apiErrorResult } from '../../shared/api-errors.js';
7
+ import { CREATES } from '../../shared/tool-annotations.js';
7
8
 
8
9
  /**
9
10
  * Get transaction information using Soundbite file via payware API
@@ -73,6 +74,7 @@ export async function getSoundbiteTransaction({
73
74
  */
74
75
  export const soundbiteTransactionTool = {
75
76
  name: "payware_operations_soundbite_transaction",
77
+ annotations: CREATES,
76
78
  description: `Get transaction information using a Soundbite SDK output file. This allows payment institutions to process transactions using audio-based identifiers.
77
79
 
78
80
  **Soundbite Integration:**
@@ -105,8 +107,7 @@ export const soundbiteTransactionTool = {
105
107
  },
106
108
  privateKey: {
107
109
  type: "string",
108
- description: "RSA private key for JWT token creation. Uses environment-specific private key as default.",
109
- default: getPrivateKeySafe()
110
+ description: "RSA private key for JWT token creation. Uses environment-specific private key as default."
110
111
  },
111
112
  useSandbox: {
112
113
  type: "boolean",
@@ -199,8 +200,8 @@ ${JSON.stringify(result.transaction, null, 2)}
199
200
  1. Transaction successfully identified from soundbite file
200
201
  2. Use transaction ID: \`${tx.transactionId || 'N/A'}\` for further operations
201
202
  3. ${!tx.payerAccount ? 'Process this transaction if you are the payer' : 'Transaction ready for finalization'}
202
- 4. Use \`payware_pi_process_transaction\` to process payment
203
- 5. Use \`payware_pi_finalize_transaction\` to confirm/decline
203
+ 4. Use \`payware_operations_process_transaction\` to process payment
204
+ 5. Use \`payware_operations_finalize_transaction\` to confirm/decline
204
205
 
205
206
  **♿ Accessibility Note:**
206
207
  - Soundbite provides audio-based alternative to QR/barcode scanning
@@ -33,6 +33,31 @@ class PaywareMCPProxy {
33
33
  const allowedOrigins = (process.env.PAYWARE_MCP_PROXY_ORIGINS
34
34
  || `http://localhost:${this.port},http://127.0.0.1:${this.port}`)
35
35
  .split(',').map((o) => o.trim()).filter(Boolean);
36
+
37
+ // DNS rebinding and cross-site requests. The CORS allowlist below decides which pages may READ a
38
+ // response; it never refuses a request. A page whose hostname has been rebound to 127.0.0.1 is
39
+ // same-origin with this server, so CORS does not apply to it at all, and a cross-site form POST
40
+ // needs no preflight - either way the route runs and signs as the partner. Two checks refuse them:
41
+ // Host must be a name this server answers to (a rebound page sends its own hostname there), and an
42
+ // Origin, which browsers send on every POST, must be allowlisted. Clients that are not browsers -
43
+ // curl, the bridge - send no Origin and pass. PAYWARE_MCP_PROXY_HOSTS (comma-separated host:port)
44
+ // widens the Host list deliberately.
45
+ const bindHost = process.env.PAYWARE_MCP_PROXY_HOST || '127.0.0.1';
46
+ const allowedHosts = new Set((process.env.PAYWARE_MCP_PROXY_HOSTS
47
+ || `localhost:${this.port},127.0.0.1:${this.port},${bindHost}:${this.port}`)
48
+ .split(',').map((h) => h.trim().toLowerCase()).filter(Boolean));
49
+ this.app.use((req, res, next) => {
50
+ const host = (req.headers.host || '').toLowerCase();
51
+ if (!allowedHosts.has(host)) {
52
+ return res.status(403).json({ error: 'Forbidden', message: `Host ${host || '(none)'} is not allowed` });
53
+ }
54
+ const origin = req.headers.origin;
55
+ if (origin !== undefined && !allowedOrigins.includes(origin)) {
56
+ return res.status(403).json({ error: 'Forbidden', message: `Origin ${origin} is not allowed` });
57
+ }
58
+ next();
59
+ });
60
+
36
61
  this.app.use(cors({
37
62
  origin: allowedOrigins,
38
63
  credentials: true
@@ -0,0 +1,51 @@
1
+ import axios from 'axios';
2
+ import { createMinimizedJSON } from '../../core/utils/json-serializer.js';
3
+ import { getSandboxUrl, getProductionUrl } from '../../config/env.js';
4
+ import { createAuthHeaders } from '../auth-headers.js';
5
+ import { apiErrorResult } from '../api-errors.js';
6
+
7
+ /**
8
+ * One call to the campaigns, placements and airings API, as a merchant or as an ISV acting for one.
9
+ *
10
+ * Built on the shared `createAuthHeaders`, so the on-behalf JWT (`aud` = the merchant, `sub` = its
11
+ * OAuth2 token) and the content hash come from the same code every newer tool uses. The body is
12
+ * serialized once and that exact string is both hashed and sent; `transformRequest` stops axios from
13
+ * serializing it a second time.
14
+ *
15
+ * Never throws for an HTTP error: it returns `apiErrorResult`, whose `details` keeps the response
16
+ * body, which is where a refused replace-all lists its rows.
17
+ *
18
+ * @param {Object} params
19
+ * @param {string} params.method HTTP verb
20
+ * @param {string} params.path path under the API root, ids already encoded
21
+ * @param {Object} [params.query] query parameters; undefined values are left out
22
+ * @param {*} [params.body] request body, serialized deterministically
23
+ * @param {boolean} [params.binary] read the response as bytes (images)
24
+ * @param {Object} params.auth from the role: partnerType, credentials and useSandbox
25
+ */
26
+ export async function placementsRequest({ method, path, query, body, binary = false, auth }) {
27
+ const minimizedBody = body === undefined ? undefined : createMinimizedJSON(body);
28
+ const headers = await createAuthHeaders(auth.partnerType, { ...auth, requestBody: minimizedBody });
29
+ const baseUrl = auth.useSandbox ? getSandboxUrl() : getProductionUrl();
30
+ const params = query && Object.fromEntries(Object.entries(query).filter(([, v]) => v !== undefined && v !== null));
31
+
32
+ try {
33
+ const response = await axios({
34
+ method,
35
+ url: `${baseUrl}${path}`,
36
+ headers,
37
+ ...(params && Object.keys(params).length > 0 && { params }),
38
+ ...(minimizedBody !== undefined && { data: minimizedBody, transformRequest: [(data) => data] }),
39
+ ...(binary && { responseType: 'arraybuffer' })
40
+ });
41
+ return {
42
+ success: true,
43
+ status: response.status,
44
+ data: response.data,
45
+ contentType: response.headers['content-type'],
46
+ requestId: response.headers['x-request-id']
47
+ };
48
+ } catch (error) {
49
+ return apiErrorResult(error);
50
+ }
51
+ }