Claude Agent SDK

For a long-running Agent SDK daemon that needs to pause on a real decision instead of auto-approving or auto-denying — wire pendnt's MCP server in as an MCP server, then call request_approval from your canUseTool callback.

1. Register the MCP server

Both SDKs accept an MCP server config identical in shape to .mcp.json — point it at the same remote endpoint:

{
  "type": "http",
  "url": "https://api.pendnt.dev/mcp",
  "headers": { "Authorization": "Bearer YOUR_PENDNT_API_KEY" }
}

TypeScript

import { query } from "@anthropic-ai/claude-agent-sdk";

const PENDNT_MCP = {
  type: "http" as const,
  url: "https://api.pendnt.dev/mcp",
  headers: { Authorization: `Bearer ${process.env.PENDNT_API_KEY}` },
};

async function canUseTool(toolName: string, input: Record<string, unknown>) {
  // Only gate the decisions that matter — let routine, read-only tools through.
  if (!["Bash", "deploy", "send_email"].includes(toolName)) {
    return { behavior: "allow" as const, updatedInput: input };
  }

  const res = await fetch("https://api.pendnt.dev/v1/requests", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PENDNT_API_KEY}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      kind: "approval",
      title: `Allow ${toolName}?`,
      details: JSON.stringify(input, null, 2),
      timeout_s: 1800,
      wait_s: 25,
    }),
  });
  const { id, status, answer } = await res.json();

  if (status === "pending") {
    // Still no answer after 25s — keep long-polling the same durable id.
    const poll = await fetch(
      `https://api.pendnt.dev/v1/requests/${id}?wait_s=25`,
      { headers: { Authorization: `Bearer ${process.env.PENDNT_API_KEY}` } },
    );
    const row = await poll.json();
    return row.status === "approved"
      ? { behavior: "allow" as const, updatedInput: input }
      : { behavior: "deny" as const, message: "Not approved" };
  }

  return status === "approved"
    ? { behavior: "allow" as const, updatedInput: input }
    : { behavior: "deny" as const, message: answer?.text ?? "Not approved" };
}

for await (const msg of query({
  prompt: "Run the weekly report job and deploy if it passes",
  options: {
    mcpServers: { pendnt: PENDNT_MCP },
    canUseTool,
  },
})) {
  console.log(msg);
}

Python

import os
import json
import httpx
from claude_agent_sdk import ClaudeAgentOptions, query

PENDNT_API_KEY = os.environ["PENDNT_API_KEY"]
PENDNT_MCP = {
    "type": "http",
    "url": "https://api.pendnt.dev/mcp",
    "headers": {"Authorization": f"Bearer {PENDNT_API_KEY}"},
}


async def can_use_tool(tool_name: str, tool_input: dict) -> dict:
    if tool_name not in ("Bash", "deploy", "send_email"):
        return {"behavior": "allow", "updatedInput": tool_input}

    async with httpx.AsyncClient() as client:
        resp = await client.post(
            "https://api.pendnt.dev/v1/requests",
            headers={"Authorization": f"Bearer {PENDNT_API_KEY}"},
            json={
                "kind": "approval",
                "title": f"Allow {tool_name}?",
                "details": json.dumps(tool_input, indent=2),
                "timeout_s": 1800,
                "wait_s": 25,
            },
        )
        row = resp.json()

        while row["status"] == "pending":
            poll = await client.get(
                f"https://api.pendnt.dev/v1/requests/{row['id']}",
                params={"wait_s": 25},
                headers={"Authorization": f"Bearer {PENDNT_API_KEY}"},
            )
            row = poll.json()

    if row["status"] == "approved":
        return {"behavior": "allow", "updatedInput": tool_input}
    return {"behavior": "deny", "message": "Not approved"}


async def main():
    async for msg in query(
        prompt="Run the weekly report job and deploy if it passes",
        options=ClaudeAgentOptions(
            mcp_servers={"pendnt": PENDNT_MCP},
            can_use_tool=can_use_tool,
        ),
    ):
        print(msg)

Notes

Early access — launched August 2026.