Skip to main content
Glama

azeth_guardian_approve

Review and approve or reject guardian requests for Azeth smart account operations. Co-sign high-value transactions or deny them with optional reasons via XMTP messages.

Instructions

Review and approve or reject guardian approval requests from agents you protect.

Azeth smart accounts have a guardian who co-signs high-value operations. When an agent exceeds its autonomous spending limits, it sends you (the guardian) an approval request via XMTP. Use this tool to review and respond to those requests.

Three modes:

  1. No request_id: Lists all pending guardian approval requests from your XMTP inbox

  2. request_id + decision "approve": Co-signs the userOpHash and sends approval via XMTP

  3. request_id + decision "reject": Sends rejection with optional reason via XMTP

When approving, this tool signs the userOpHash with your AZETH_PRIVATE_KEY (which is the guardian key on your MCP instance) and sends the signature back to the requesting agent.

Returns: List of pending requests (mode 1), or confirmation of approve/reject (mode 2/3).

Example (list): { } Example (approve): { "request_id": "abc-123", "decision": "approve" } Example (reject): { "request_id": "abc-123", "decision": "reject", "reason": "Amount too high" }

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
request_idNoThe request ID to approve or reject. If omitted, lists all pending guardian approval requests from your XMTP messages.
decisionNoDecision: "approve" to co-sign the operation, "reject" to deny it. Required when request_id is provided.
reasonNoOptional reason for rejection. Only used when decision is "reject".
chainNoTarget chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").

Implementation Reference

  • Registration and implementation of the 'azeth_guardian_approve' tool. It handles both listing pending requests (no request_id) and approving/rejecting specific requests (with request_id and decision).
    server.registerTool(
      'azeth_guardian_approve',
      {
        description: [
          'Review and approve or reject guardian approval requests from agents you protect.',
          '',
          'Azeth smart accounts have a guardian who co-signs high-value operations.',
          'When an agent exceeds its autonomous spending limits, it sends you (the guardian)',
          'an approval request via XMTP. Use this tool to review and respond to those requests.',
          '',
          'Three modes:',
          '  1. No request_id: Lists all pending guardian approval requests from your XMTP inbox',
          '  2. request_id + decision "approve": Co-signs the userOpHash and sends approval via XMTP',
          '  3. request_id + decision "reject": Sends rejection with optional reason via XMTP',
          '',
          'When approving, this tool signs the userOpHash with your AZETH_PRIVATE_KEY (which is',
          'the guardian key on your MCP instance) and sends the signature back to the requesting agent.',
          '',
          'Returns: List of pending requests (mode 1), or confirmation of approve/reject (mode 2/3).',
          '',
          'Example (list): { }',
          'Example (approve): { "request_id": "abc-123", "decision": "approve" }',
          'Example (reject): { "request_id": "abc-123", "decision": "reject", "reason": "Amount too high" }',
        ].join('\n'),
        inputSchema: z.object({
          request_id: z.string().optional().describe(
            'The request ID to approve or reject. If omitted, lists all pending guardian approval requests from your XMTP messages.',
          ),
          decision: z.enum(['approve', 'reject']).optional().describe(
            'Decision: "approve" to co-sign the operation, "reject" to deny it. Required when request_id is provided.',
          ),
          reason: z.string().optional().describe(
            'Optional reason for rejection. Only used when decision is "reject".',
          ),
          chain: z.string().optional().describe('Target chain. Defaults to AZETH_CHAIN env var or "baseSepolia". Accepts "base", "baseSepolia", "ethereumSepolia", "ethereum" (and aliases like "base-sepolia", "eth-sepolia", "sepolia", "eth", "mainnet").'),
        }),
      },
      async (args) => {
        let client;
        try {
          client = await createClient(args.chain);
    
          if (!args.request_id) {
            // ── Mode 1: List pending requests from XMTP ──
            const conversations = await client.getConversations();
            const pendingRequests: Array<{
              requestId: string;
              from: string;
              account: string;
              operation: GuardianApprovalRequest['operation'];
              reason: string;
              limits: GuardianApprovalRequest['limits'];
              expiresAt: string;
              userOpHash: string;
            }> = [];
    
            for (const conv of conversations) {
              try {
                const messages = await client.getMessages(conv.peerAddress as `0x${string}`, 20);
                for (const msg of messages) {
                  const parsed = tryParseGuardianRequest(msg.content);
                  if (!parsed) continue;
    
                  // Skip expired requests
                  const expiresAtMs = new Date(parsed.expiresAt).getTime();
                  if (Date.now() > expiresAtMs) continue;
    
                  pendingRequests.push({
                    requestId: parsed.requestId,
                    from: conv.peerAddress,
                    account: parsed.account,
                    operation: parsed.operation,
                    reason: parsed.reason,
                    limits: parsed.limits,
                    expiresAt: parsed.expiresAt,
                    userOpHash: parsed.userOpHash,
                  });
                }
              } catch {
                // Skip conversations that fail to load
              }
            }
    
            if (pendingRequests.length === 0) {
              return success({
                message: 'No pending guardian approval requests found.',
                pendingCount: 0,
                requests: [],
              });
            }
    
            return success({
              message: `Found ${pendingRequests.length} pending guardian approval request(s).`,
              pendingCount: pendingRequests.length,
              requests: pendingRequests.map(r => ({
                requestId: r.requestId,
                from: r.from ? `${r.from.slice(0, 6)}...${r.from.slice(-4)}` : undefined,
                account: r.account ? `${r.account.slice(0, 6)}...${r.account.slice(-4)}` : undefined,
                operationType: r.operation.type,
                description: r.operation.description,
                amount: r.operation.amount,
                usdValue: r.operation.usdValue,
                to: r.operation.to,
                reason: r.reason,
                expiresAt: r.expiresAt,
                limits: r.limits,
              })),
              hint: 'To approve: { "request_id": "<id>", "decision": "approve" }. To reject: { "request_id": "<id>", "decision": "reject", "reason": "..." }',
            });
          }
    
          // ── Mode 2/3: Approve or reject a specific request ──
    
          if (!args.decision) {
            return error(
              'INVALID_INPUT',
              'When request_id is provided, decision ("approve" or "reject") is required.',
            );
          }
    
          // Find the request in XMTP messages
          let foundRequest: GuardianApprovalRequest | null = null;
          let senderAddress: `0x${string}` | null = null;
    
          const conversations = await client.getConversations();
          for (const conv of conversations) {
            try {
              const messages = await client.getMessages(conv.peerAddress as `0x${string}`, 20);
              for (const msg of messages) {
                const parsed = tryParseGuardianRequest(msg.content);
                if (parsed && parsed.requestId === args.request_id) {
                  foundRequest = parsed;
                  senderAddress = conv.peerAddress as `0x${string}`;
                  break;
                }
              }
              if (foundRequest) break;
            } catch {
              // Skip conversations that fail to load
            }
          }
    
          if (!foundRequest || !senderAddress) {
            return error(
              'SERVICE_NOT_FOUND',
              'Guardian approval request not found or expired. Use this tool without request_id to list pending requests.',
            );
          }
    
          // Check expiry
          const expiresAtMs = new Date(foundRequest.expiresAt).getTime();
          if (Date.now() > expiresAtMs) {
            return error(
              'SESSION_EXPIRED',
              `Guardian approval request "${args.request_id}" has expired. The agent will need to retry the operation.`,
            );
          }
    
          if (args.decision === 'approve') {
            // Sign the userOpHash with the guardian's private key (AZETH_PRIVATE_KEY)
            const privateKey = process.env['AZETH_PRIVATE_KEY'];
            if (!privateKey) {
              return error(
                'UNAUTHORIZED',
                'AZETH_PRIVATE_KEY is required to sign guardian approvals.',
              );
            }
    
            // Dynamic import to avoid top-level viem dependency
            const { privateKeyToAccount } = await import('viem/accounts');
            const guardianAccount = privateKeyToAccount(privateKey as `0x${string}`);
    
            // Sign the userOpHash with EIP-191 personal sign (matching GuardianModule expectations)
            const signature = await guardianAccount.signMessage({
              message: { raw: foundRequest.userOpHash as `0x${string}` },
            });
    
            // Send approval response via XMTP
            const response: GuardianApprovalResponse = {
              type: 'azeth:guardian_response',
              version: '1.0',
              requestId: args.request_id,
              decision: 'approved',
              signature: signature as `0x${string}`,
            };
    
            await client.sendMessage({
              to: senderAddress,
              content: JSON.stringify(response),
            });
    
            return success({
              message: `Approved guardian request "${args.request_id}". Signature sent to ${senderAddress} via XMTP.`,
              requestId: args.request_id,
              decision: 'approved',
              operation: foundRequest.operation.description,
              account: foundRequest.account,
              sentTo: senderAddress,
            });
          } else {
            // Reject — send rejection response via XMTP
            const response: GuardianApprovalResponse = {
              type: 'azeth:guardian_response',
              version: '1.0',
              requestId: args.request_id,
              decision: 'rejected',
              reason: args.reason,
            };
    
            await client.sendMessage({
              to: senderAddress,
              content: JSON.stringify(response),
            });
    
            return success({
              message: `Rejected guardian request "${args.request_id}".${args.reason ? ` Reason: ${args.reason}` : ''}`,
              requestId: args.request_id,
              decision: 'rejected',
              reason: args.reason ?? 'No reason provided',
              operation: foundRequest.operation.description,
              account: foundRequest.account,
              sentTo: senderAddress,
            });
          }
        } catch (err) {
          return handleError(err);
        } finally {
          try { await client?.destroy(); } catch { /* prevent destroy from masking the original error */ }
        }
      },
    );
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full behavioral disclosure burden. It successfully explains critical behavioral traits: it signs userOpHash with AZETH_PRIVATE_KEY, transmits via XMTP, and returns different structures based on mode. Minor gap: could clarify irreversibility of approvals or network confirmation behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Well-structured with clear sectioning (concept explanation, three modes, technical mechanism, returns, examples). Front-loaded with purpose. Slightly verbose but every sentence adds necessary context for a security-critical blockchain operation. Examples are appropriately placed at end.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Lacks output schema (has_output_schema: false), so description must compensate for return values. It mentions returns at high level ('List of pending requests' or 'confirmation') but does not describe the structure/shape of these return objects (e.g., what fields comprise a 'request' object), which limits the agent's ability to interpret results.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, establishing baseline 3. The description adds significant value through the 'Three modes' narrative that contextualizes parameter combinations (e.g., request_id omission triggers list mode), and provides concrete JSON examples showing how parameters interact, which aids agent reasoning beyond schema definitions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action (review/approve/reject) and resource (guardian approval requests). It distinguishes itself from sibling `azeth_guardian_status` by emphasizing the active response capability ('Use this tool to review and respond') versus passive status checking.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use ('When an agent exceeds its autonomous spending limits... sends you an approval request'). Documents three distinct operational modes (list, approve, reject) with clear prerequisites for each, effectively serving as usage guidelines.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/azeth-protocol/mcp-azeth'

If you have feedback or need assistance with the MCP directory API, please join our Discord server