Skip to content

Commit b29d5db

Browse files
author
azeth-sync[bot]
committed
v0.2.6: sync from monorepo 2026-03-07
1 parent 147a75f commit b29d5db

4 files changed

Lines changed: 81 additions & 18 deletions

File tree

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@azeth/mcp-server",
3-
"version": "0.2.5",
3+
"version": "0.2.6",
44
"mcpName": "io.github.azeth-protocol/mcp-server",
55
"type": "module",
66
"description": "MCP server for the Azeth trust infrastructure — smart accounts, payments, reputation, and discovery tools for AI agents",
@@ -41,8 +41,8 @@
4141
"clean": "rm -rf dist"
4242
},
4343
"dependencies": {
44-
"@azeth/common": "^0.2.5",
45-
"@azeth/sdk": "^0.2.5",
44+
"@azeth/common": "^0.2.6",
45+
"@azeth/sdk": "^0.2.6",
4646
"@modelcontextprotocol/sdk": "^1.0.0",
4747
"dotenv": "^16.4.0",
4848
"viem": "^2.21.0",

src/tools/payments.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { URL } from 'node:url';
33
import dns from 'node:dns/promises';
44
import { isAddress, parseUnits } from 'viem';
55
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
67
import { AzethError, AZETH_CONTRACTS, TOKENS, formatTokenAmount } from '@azeth/common';
8+
import type { AzethKit } from '@azeth/sdk';
79
import { createClient, resolveChain, validateAddress } from '../utils/client.js';
810
import { resolveAddress } from '../utils/resolve.js';
911
import { success, error, handleError, guardianRequiredError } from '../utils/response.js';
@@ -175,6 +177,45 @@ async function validateExternalUrl(urlStr: string): Promise<ValidatedUrl> {
175177
return { url: urlStr, pinnedIPv4 };
176178
}
177179

180+
/**
181+
* Apply smart account selection from the `smartAccount` tool parameter.
182+
* Accepts "#N" (1-based index from azeth_accounts) or a full address.
183+
* Returns a CallToolResult error if resolution fails, or null on success.
184+
*/
185+
function applySmartAccountSelection(client: AzethKit, smartAccount: string): CallToolResult | null {
186+
const accounts = client.smartAccounts;
187+
if (!accounts || accounts.length === 0) {
188+
return error('ACCOUNT_NOT_FOUND', 'No smart accounts found.', 'Use azeth_create_account to create one.');
189+
}
190+
191+
const indexMatch = smartAccount.match(/^#(\d+)$/);
192+
if (indexMatch) {
193+
const idx = parseInt(indexMatch[1]!) - 1;
194+
if (idx < 0 || idx >= accounts.length) {
195+
return error('INVALID_INPUT',
196+
`Account #${indexMatch[1]} not found. You have ${accounts.length} account(s).`,
197+
'Use azeth_accounts to list your accounts.');
198+
}
199+
client.setActiveAccount(accounts[idx]!);
200+
return null;
201+
}
202+
203+
if (/^0x[0-9a-fA-F]{40}$/i.test(smartAccount)) {
204+
try {
205+
client.setActiveAccount(smartAccount as `0x${string}`);
206+
} catch {
207+
return error('INVALID_INPUT',
208+
`Address ${smartAccount} is not one of your smart accounts.`,
209+
'Use azeth_accounts to list your accounts.');
210+
}
211+
return null;
212+
}
213+
214+
return error('INVALID_INPUT',
215+
'Invalid smartAccount format.',
216+
'Use "#N" (e.g., "#2") for account index, or a full Ethereum address. Run azeth_accounts to see your accounts.');
217+
}
218+
178219
/** Register payment-related MCP tools: azeth_pay, azeth_smart_pay, azeth_create_payment_agreement */
179220
export function registerPaymentTools(server: McpServer): void {
180221
// ──────────────────────────────────────────────
@@ -203,6 +244,7 @@ export function registerPaymentTools(server: McpServer): void {
203244
method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('HTTP method. Defaults to "GET".'),
204245
body: z.string().max(100_000).optional().describe('Request body for POST/PUT/PATCH requests (JSON string, max 100KB).'),
205246
maxAmount: z.string().max(32).optional().describe('Maximum USDC amount willing to pay (e.g., "5.00"). Rejects if service costs more.'),
247+
smartAccount: z.string().optional().describe('Smart account to pay from. Use "#1", "#2", etc. (index from azeth_accounts) or a full address. Defaults to your first smart account.'),
206248
}),
207249
},
208250
async (args) => {
@@ -216,6 +258,12 @@ export function registerPaymentTools(server: McpServer): void {
216258
let client;
217259
try {
218260
client = await createClient(args.chain);
261+
262+
// Apply smart account selection if specified
263+
if (args.smartAccount) {
264+
const selectionErr = applySmartAccountSelection(client, args.smartAccount);
265+
if (selectionErr) return selectionErr;
266+
}
219267
let maxAmount: bigint | undefined;
220268
if (args.maxAmount) {
221269
try {
@@ -353,12 +401,20 @@ export function registerPaymentTools(server: McpServer): void {
353401
maxAmount: z.string().max(32).optional().describe('Maximum USDC amount willing to pay per service (e.g., "1.00"). Rejects if service costs more.'),
354402
minReputation: z.coerce.number().min(0).max(100).optional().describe('Minimum reputation score (0-100) to consider. Services below this are excluded.'),
355403
autoFeedback: z.boolean().optional().describe('Automatically submit a reputation opinion after payment based on service quality. Defaults to false.'),
404+
smartAccount: z.string().optional().describe('Smart account to pay from. Use "#1", "#2", etc. (index from azeth_accounts) or a full address. Defaults to your first smart account.'),
356405
}),
357406
},
358407
async (args) => {
359408
let client;
360409
try {
361410
client = await createClient(args.chain);
411+
412+
// Apply smart account selection if specified
413+
if (args.smartAccount) {
414+
const selectionErr = applySmartAccountSelection(client, args.smartAccount);
415+
if (selectionErr) return selectionErr;
416+
}
417+
362418
let maxAmount: bigint | undefined;
363419
if (args.maxAmount) {
364420
try {

src/utils/error-selectors.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
interface ErrorInfo {
1313
name: string;
1414
description: string;
15+
suggestion?: string;
16+
}
17+
18+
/** Decoded error with human-readable message and optional remediation suggestion */
19+
export interface DecodedError {
20+
message: string;
21+
suggestion?: string;
1522
}
1623

1724
/** Pre-computed selector → error info */
@@ -38,18 +45,18 @@ const SELECTOR_MAP: Record<string, ErrorInfo | undefined> = {
3845
'0x0bbfcdcc': { name: 'NotTightening', description: 'Only tightening (reducing limits) can bypass the timelock.' },
3946
'0x6320ab2b': { name: 'NoPendingEmergency', description: 'No emergency withdrawal is pending.' },
4047
'0x4806710a': { name: 'ChangeAlreadyPending', description: 'A guardrail change is already pending.' },
41-
'0x24831e77': { name: 'ExecutorSpendExceedsLimit', description: 'Payment amount exceeds the guardian daily spend limit.' },
48+
'0x24831e77': { name: 'ExecutorSpendExceedsLimit', description: 'Payment amount exceeds the guardian daily spend limit.', suggestion: 'Check your limits with azeth_get_guardrails. Wait until tomorrow or increase the daily limit via the guardian.' },
4249

4350
// PaymentAgreementModule
4451
'0x95a68634': { name: 'SelfAgreement', description: 'Cannot create a payment agreement with yourself.' },
45-
'0xf84835a0': { name: 'TokenNotWhitelisted', description: 'The token is not in the guardian whitelist. Add it via setTokenWhitelist.' },
52+
'0xf84835a0': { name: 'TokenNotWhitelisted', description: 'The token is not in the guardian whitelist.', suggestion: 'Use azeth_whitelist_token to add the token to your guardian whitelist before retrying.' },
4653
'0xb5c6c3ab': { name: 'AgreementNotExists', description: 'The specified agreement does not exist.' },
4754
'0xfe1da89a': { name: 'InvalidAgreement', description: 'The agreement is invalid (already cancelled or completed).' },
48-
'0x9563bcf0': { name: 'GuardianLimitExceeded', description: 'The payment exceeds guardian spending limits.' },
55+
'0x9563bcf0': { name: 'GuardianLimitExceeded', description: 'The payment exceeds guardian spending limits.', suggestion: 'Check your limits with azeth_get_guardrails. Split into smaller amounts or increase limits via the guardian.' },
4956
'0x90b8ec18': { name: 'TransferFailed', description: 'The token transfer failed.' },
5057

5158
// ReputationModule
52-
'0xae525b83': { name: 'InsufficientPaymentUSD', description: 'You must pay at least $1 USD to the target before rating. Use azeth_get_net_paid to check your payment history.' },
59+
'0xae525b83': { name: 'InsufficientPaymentUSD', description: 'You must pay at least $1 USD to the target before rating.', suggestion: 'Payments via azeth_pay, azeth_smart_pay, azeth_transfer, and payment agreements all count toward the $1 minimum. Use azeth_get_net_paid to check your payment history.' },
5360
'0xcb02f599': { name: 'SelfRatingNotAllowed', description: 'You cannot rate yourself.' },
5461
'0x645c0b06': { name: 'SiblingRatingNotAllowed', description: 'You cannot rate accounts owned by the same EOA.' },
5562
'0x565c8a5a': { name: 'InvalidValueDecimals', description: 'Value decimals must be between 0 and 18.' },
@@ -69,7 +76,7 @@ const SELECTOR_MAP: Record<string, ErrorInfo | undefined> = {
6976
// Oracle
7077
'0x1f8f95a0': { name: 'InvalidOraclePrice', description: 'The oracle returned an invalid price.' },
7178
'0xbf16aab6': { name: 'UnsupportedToken', description: 'The oracle does not support this token.' },
72-
'0xcf479181': { name: 'InsufficientBalance', description: 'The smart account has insufficient token balance for this operation.' },
79+
'0xf4d678b8': { name: 'InsufficientBalance', description: 'The smart account has insufficient token balance for this operation.', suggestion: 'Use azeth_deposit to fund your smart account, or use the smartAccount parameter to select a different account. Run azeth_accounts to see all your accounts and their balances.' },
7380

7481
// Common (shared across multiple contracts)
7582
'0x0dc149f0': { name: 'AlreadyInitialized', description: 'This module is already initialized for the account.' },
@@ -93,31 +100,31 @@ const PANIC_SELECTOR = '4e487b71'; // Panic(uint256)
93100
* selector is buried inside the hex at a 32-byte ABI boundary.
94101
* 4. Error(string) decoding: extracts the revert string from standard Solidity reverts
95102
*/
96-
export function decodeErrorSelector(message: string): string | undefined {
103+
export function decodeErrorSelector(message: string): DecodedError | undefined {
97104
const hexMatches = message.matchAll(/0x([0-9a-fA-F]{8,})/g);
98105
for (const match of hexMatches) {
99106
const hexData = match[1]!.toLowerCase();
100107

101108
// Pass 1: Check the outer selector (first 8 chars)
102109
const outerSelector = `0x${hexData.slice(0, 8)}`;
103110
const outerKnown = SELECTOR_MAP[outerSelector];
104-
if (outerKnown) return `${outerKnown.name}: ${outerKnown.description}`;
111+
if (outerKnown) return { message: `${outerKnown.name}: ${outerKnown.description}`, suggestion: outerKnown.suggestion };
105112

106113
// Pass 1b: Try to decode Error(string) at the outer level
107114
const outerString = tryDecodeErrorString(hexData);
108-
if (outerString) return outerString;
115+
if (outerString) return { message: outerString };
109116

110117
// Pass 2: Scan interior at 64-char (32-byte) ABI word boundaries for known
111118
// selectors. ERC-4337 EntryPoint wraps inner reverts in FailedOpWithRevert
112119
// where the actual error selector is ABI-encoded at a word boundary.
113120
for (let i = 64; i + 8 <= hexData.length; i += 64) {
114121
const innerSelector = `0x${hexData.slice(i, i + 8)}`;
115122
const innerKnown = SELECTOR_MAP[innerSelector];
116-
if (innerKnown) return `${innerKnown.name}: ${innerKnown.description}`;
123+
if (innerKnown) return { message: `${innerKnown.name}: ${innerKnown.description}`, suggestion: innerKnown.suggestion };
117124

118125
// Try to decode inner Error(string) — common in EntryPoint FailedOpWithRevert
119126
const innerString = tryDecodeErrorString(hexData.slice(i));
120-
if (innerString) return innerString;
127+
if (innerString) return { message: innerString };
121128
}
122129
}
123130
return undefined;

src/utils/response.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ export function handleError(err: unknown): CallToolResult {
7777
if (decoded) {
7878
return error(
7979
err.code,
80-
decoded,
81-
getSuggestion(err.code, err.details),
80+
decoded.message,
81+
decoded.suggestion ?? getSuggestion(err.code, err.details),
8282
);
8383
}
8484
return error(
@@ -92,7 +92,7 @@ export function handleError(err: unknown): CallToolResult {
9292
// Attempt to decode contract revert selectors from the error message
9393
const decoded = decodeErrorSelector(err.message);
9494
if (decoded) {
95-
return error('CONTRACT_ERROR', decoded, getSuggestion('CONTRACT_ERROR'));
95+
return error('CONTRACT_ERROR', decoded.message, decoded.suggestion ?? getSuggestion('CONTRACT_ERROR'));
9696
}
9797
return error('UNKNOWN_ERROR', sanitizeErrorMessage(err.message));
9898
}
@@ -101,7 +101,7 @@ export function handleError(err: unknown): CallToolResult {
101101
const stringified = String(err);
102102
const decoded = decodeErrorSelector(stringified);
103103
if (decoded) {
104-
return error('CONTRACT_ERROR', decoded, getSuggestion('CONTRACT_ERROR'));
104+
return error('CONTRACT_ERROR', decoded.message, decoded.suggestion ?? getSuggestion('CONTRACT_ERROR'));
105105
}
106106
return error('UNKNOWN_ERROR', sanitizeErrorMessage(stringified));
107107
}
@@ -196,7 +196,7 @@ function getSuggestion(code: string, details?: Record<string, unknown>): string
196196
case 'ACCOUNT_EXISTS':
197197
return 'An account already exists. Use azeth_accounts to list existing accounts.';
198198
case 'INSUFFICIENT_PAYMENT':
199-
return 'You must pay the target agent at least $1 USD (via azeth_transfer) before rating them. Use azeth_get_net_paid to check your payment history.';
199+
return 'You must pay the target at least $1 USD before rating. Payments via azeth_pay, azeth_smart_pay, azeth_transfer, and payment agreements all count. Use azeth_get_net_paid to check your payment history.';
200200
case 'RECIPIENT_UNREACHABLE':
201201
return 'The recipient is not reachable on the XMTP network. Use azeth_check_reachability to verify before sending.';
202202
case 'SERVER_UNAVAILABLE':

0 commit comments

Comments
 (0)