# Build a custom agent on the MCP server

> Connect your own agent to Anytime Capital Payments over MCP with an API key: raw JSON-RPC, the TypeScript and Python MCP SDKs, and the Claude API MCP connector.

Source: https://anytime-capital.com/docs/mcp-custom-agents · Markdown: https://anytime-capital.com/docs/mcp-custom-agents.md · All docs: https://anytime-capital.com/docs/llms-full.txt

The MCP server at `https://anytime-capital.com/api/mcp` is plain HTTP: any MCP client can use it, and so can your own code. Authenticate every call with an API key as `Authorization: Bearer ak_live_…`; the agent can do exactly what that key allows. Make the key for this agent alone, with the least access its job needs: see [Safe AI agents](https://anytime-capital.com/docs/ai-agent-safety.md).

| Detail | Value |
| --- | --- |
| Transport | Streamable HTTP, stateless: POST one JSON-RPC message (or a batch), get `application/json` back. No session id; `GET` answers `405`, which MCP clients take to mean there's no event stream. |
| Methods | `initialize`, `ping`, `tools/list`, `tools/call` |
| Tool results | `structuredContent` is the API's JSON object; on an error `isError` is true and it's `{ status, error: { code, message } }`, with `retryAfterSeconds` when the API asked to wait. |
| Limits | Each call counts against the key's [rate limit](https://anytime-capital.com/docs/rate-limits.md), as a REST request would. |
| Retries | Pass `idempotencyKey` on tools that change something: a retry with the same key returns the first result. |

## Raw JSON-RPC

```bash
# 1. Discover the tools (no key needed for initialize and tools/list)
curl https://anytime-capital.com/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

# 2. Call one with your key
curl https://anytime-capital.com/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $ANYTIME_API_KEY" \
  -H "MCP-Protocol-Version: 2025-06-18" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_invoices","arguments":{"status":"open","limit":10}}}'
```

## TypeScript (MCP SDK)

With the official MCP TypeScript SDK your code is the client: it lists the tools, and you decide which calls run. Check each tool's annotations and ask a person before anything that isn't read-only.

**agent.mts**

```typescript
// agent.mts — npm install @modelcontextprotocol/sdk
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(new URL("https://anytime-capital.com/api/mcp"), {
  requestInit: { headers: { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` } },
});
const client = new Client({ name: "my-payments-agent", version: "1.0.0" });
await client.connect(transport);

const { tools } = await client.listTools();
console.log(tools.map((tool) => tool.name));

// Your code decides what runs: ask a person before any tool that isn't read-only.
const tool = tools.find((candidate) => candidate.name === "list_invoices");
if (tool?.annotations?.readOnlyHint !== true) throw new Error("Needs a person's OK first");

const result = await client.callTool({ name: "list_invoices", arguments: { status: "open", limit: 10 } });
if (result.isError) console.error(result.structuredContent); // { status, error: { code, message } }
else console.log(result.structuredContent); // { object: "list", data: [...], hasMore, nextCursor }

await client.close();
```

## Python (MCP SDK)

**agent.py**

```python
# agent.py — pip install "mcp>=1.10"
import asyncio
import os

from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

URL = "https://anytime-capital.com/api/mcp"
HEADERS = {"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"}


async def main():
    async with streamablehttp_client(URL, headers=HEADERS) as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([tool.name for tool in tools.tools])

            result = await session.call_tool("list_invoices", {"status": "open", "limit": 10})
            print(result.structuredContent)  # the API's JSON, or { status, error } when result.isError


asyncio.run(main())
```

## Claude API (MCP connector)

Claude's Messages API can connect to the server for you: pass it in `mcp_servers` with your API key as `authorization_token`, and Claude calls the tools on Anthropic's side during the request. It runs the tools it chooses without asking you, so allowlist the ones the agent may use with `default_config` and `configs` (above: read-only tools only), and give it a key with no more access than that. The connector is in beta; see Anthropic's MCP connector documentation for its current limits.

**claude-agent.mts**

```typescript
// claude-agent.mts — npm install @anthropic-ai/sdk
import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY

const response = await anthropic.beta.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 16000,
  betas: ["mcp-client-2025-11-20", "server-side-fallback-2026-07-01"],
  fallbacks: "default",
  mcp_servers: [
    {
      type: "url",
      url: "https://anytime-capital.com/api/mcp",
      name: "anytime-capital",
      authorization_token: process.env.ANYTIME_API_KEY, // sent as "Authorization: Bearer …"
    },
  ],
  tools: [
    {
      type: "mcp_toolset",
      mcp_server_name: "anytime-capital",
      // Allowlist: only these tools exist for this agent, whatever the key allows.
      default_config: { enabled: false },
      configs: {
        list_invoices: { enabled: true },
        get_invoice: { enabled: true },
        list_payments: { enabled: true },
      },
    },
  ],
  messages: [{ role: "user", content: "Which invoices are past due, and how much is owed in total?" }],
});

if (response.stop_reason === "refusal") {
  console.error("Claude declined this request; rephrase it and try again.");
} else {
  for (const block of response.content) {
    if (block.type === "text") console.log(block.text);
  }
}
```

**claude_agent.py**

```python
# claude_agent.py — pip install anthropic
import os

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY

response = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=16000,
    betas=["mcp-client-2025-11-20", "server-side-fallback-2026-07-01"],
    fallbacks="default",
    mcp_servers=[
        {
            "type": "url",
            "url": "https://anytime-capital.com/api/mcp",
            "name": "anytime-capital",
            "authorization_token": os.environ["ANYTIME_API_KEY"],
        }
    ],
    tools=[
        {
            "type": "mcp_toolset",
            "mcp_server_name": "anytime-capital",
            "default_config": {"enabled": False},
            "configs": {
                "list_invoices": {"enabled": True},
                "get_invoice": {"enabled": True},
                "list_payments": {"enabled": True},
            },
        }
    ],
    messages=[{"role": "user", "content": "Which invoices are past due, and how much is owed in total?"}],
)

if response.stop_reason == "refusal":
    print("Claude declined this request; rephrase it and try again.")
else:
    for block in response.content:
        if block.type == "text":
            print(block.text)
```

> **Two keys, two places:** `ANTHROPIC_API_KEY` is your Claude API key; `ANYTIME_API_KEY` is your Anytime Capital key, which the connector sends to our server on each tool call. Keep both on your server.

## Patterns that work

- **Read, then propose, then act:** let the agent read freely with a Read only key, have it propose changes, and run them with a second, narrower key after a person approves.
- **Drafts:** have it create invoices with `send: false` and send them with `send_invoice` once checked.
- **One key per agent:** revoking one never breaks another, and the activity log shows which agent did what.
- **Webhooks for the rest:** an agent that needs to know when something happens should be woken by your [webhook](https://anytime-capital.com/docs/webhooks.md) handler rather than polling lists.

---

Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.
