Skip to content

MCP Server

The InstaPayBill API exposes a Model Context Protocol server so AI agents can query platform data directly — wallet balances, transactions, plans, billers and customers — without hand-rolling HTTP calls.

The MCP server is mounted on the same Express application as the REST API, at https://api.instapaybill.com/mcp.


Transport

StreamableHTTPServerTransport, configured for stateless JSON responses:

const transport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined,   // stateless
  enableJsonResponse: true,        // JSON, not SSE
});

No session id is issued and no SSE stream is kept open, so the server scales horizontally without sticky sessions.

The server rewrites the Accept header if needed, so clients that only send one of application/json or text/event-stream still work:

if (!hasJson || !hasSse) {
  req.headers.accept = [accept, 'application/json', 'text/event-stream']
    .filter(Boolean).join(', ');
}

The bundled MCP README is out of date

mcp/README.md and mcp/SUMMARY.md describe an older SSE design (GET /mcp/sse, POST /mcp/message) with 22 tools in 7 domain files. The live implementation is mcp/index.js with Streamable HTTP on app.all('/mcp') and 50 tools in 50 files. Use mcp/index.js as the source of truth.


Authentication

Two layers.

Layer 1 — X-API-Key

Every non-GET request must carry the shared service key:

X-API-Key: <MCP_API_KEY>

GET requests pass through unauthenticated — that is what makes /mcp/health and /mcp/tools publicly readable.

A missing or wrong key returns 401:

{ "error": "Unauthorized: valid X-API-Key is required" }

Layer 2 — a user Bearer token for tools/call

tools/call requests additionally require a user-scoped JWT:

Authorization: Bearer <user_access_token>

Without it:

{ "error": "Unauthorized: Bearer token is required for tools/call" }

The token is verified with HS256 against AGENT_TOKEN_SECRET (falling back to ACCESS_TOKEN_SECRET), with issuer AGENT_TOKEN_ISSUER (default api.instapaybill.com). It must carry a string sub.


Issuing a user token

POST/api/v1/agent/token

Exchange the service key and a customer email for a user access token.

curl -X POST https://api.instapaybill.com/api/v1/agent/token \
  -H "Content-Type: application/json" \
  -d '{
    "service_key": "<MCP_API_KEY>",
    "email": "[email protected]"
  }'
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

The token payload:

{
  "sub": "<userId>",
  "email": "[email protected]",
  "role": "user",
  "isAdmin": false,
  "type": "mcp-user-access"
}
Failure Response
Missing service_key or email 300 — "service_key and email are required"
Wrong service_key 401 — "Unauthorized: invalid service key"
Unknown email 404 — "User not found"

Tokens expire after AGENT_TOKEN_EXPIRES_IN_SECONDS (default 3600 seconds).


Discovery

Method Path Auth Returns
GET /mcp/health None { status, service, version, totalTools, tools }
GET /mcp/tools None [{ name, description }]
curl https://api.instapaybill.com/api/v1/mcp/health | jq '.totalTools'
{
  "status": "ok",
  "service": "utilities-mcp-server",
  "version": "1.0.0",
  "totalTools": 50,
  "tools": [{ "name": "get_wallet_balance", "description": "…" }]
}

Calling the server

curl -X POST https://api.instapaybill.com/mcp \
  -H "X-API-Key: <MCP_API_KEY>" \
  -H "Authorization: Bearer <user_access_token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_wallet_balance",
      "arguments": { "userId": "<userId>" }
    }
  }'

The response is JSON-RPC with the tool result as a single text content block:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      { "type": "text", "text": "{\n  \"success\": true,\n  \"data\": { … }\n}" }
    ]
  }
}

The tool result is JSON-stringified inside the text block, so a client must parse the text to get structured data.


Tool catalogue

50 tools, each a separate class in mcp/tools/. The registered name is declared in the file and is what you pass as params.name.

Wallet

Tool Purpose
get_wallet_balance All wallet balances for a user
get_wallet_data The wallet record
get_wallet_history Wallet ledger entries
get_wallet_user The wallet's owner
get_wallet_stat Wallet statistics
get_wallet_bonus_history Bonus history
get_wallet_funding Funding history
get_wallet_manual_history Manual funding history

Transactions

Tool Purpose
get_transactions Transaction list with filters
get_transaction_details A single transaction
get_transaction_by_reference Look up by reference
get_transactions_stat Transaction analytics
find_transactions_stat Search transaction stats
get_data_transactions Data transactions
get_key_patron_transactions_stat High-value customer stats

Profit and analytics

Tool Purpose
get_transactions_profit_stat Profit statistics
get_transaction_profit_comparison Profit comparison
get_transaction_analytics_by_product Per-product analytics
get_manual_approval Manual approval queue

Users

Tool Purpose
get_user_details A user's profile
get_users User list
get_users_by_email Look up by email
get_user_total_amount Total transacted per user
get_referrals A user's referrals
get_user_context A user's stored context
get_user_contexts_batch Context for many users

Data plans and catalogue

Tool Purpose
get_normal_data_plans Active data plans
get_transformed_data_plans Data plans in agent shape
get_data_plans_by_network Plans for one network
get_available_networks Networks with active plans
get_cables_plan Cable plans
get_cables_type Cable types
get_e_lectricity_disco Electricity discos
get_education_plans Education exam plans
get_category Product categories
get_biller Biller records
get_billers_balance Biller balances

Validation

Tool Purpose
validate_cables_i_u_c Validate a smart card
validate_e_lectricity_meter_no Validate a meter number

Notifications

Tool Purpose
get_all_notifications User notifications
get_admin_notifications Admin notifications
get_user_notification_admin Notification admin view

Automation and upgrades

Tool Purpose
get_auto_buy_list Scheduled auto-buy jobs
get_become_an_agent_top_user Top agent-approval candidates
get_upgrade_details Agent upgrade request details

Miscellaneous

Tool Purpose
get_settings Platform settings
get_gateways Payment gateways
get_card_saved Saved cards
get_active_rooms Active voice-assistant rooms
get_room_details A single room

Three tool names are misspelled — use them verbatim

The names contain a stray capital E from the original class names:

  • get_e_lectricity_disco (not get_electricity_disco)
  • validate_e_lectricity_meter_no (not validate_electricity_meter_no)
  • validate_cables_i_u_c (not validate_cables_iuc)

These are the registered names. A tools/call with the "corrected" name fails with an unknown-tool error.


How tools execute

Each tool calls a service function directly, not the HTTP endpoint. It synthesises a request/response pair:

const mockReq = { query: { … }, user: { isAdmin: true, _id: 'admin' }, userAuth: 'admin' };
const mockRes = { status: (code) => ({ json: (data) => { … } }), json: (data) => { … } };
await serviceFn(mockReq, mockRes);

The MCP surface is fully privileged

Every tool executes with isAdmin: true and userAuth: 'admin'. There is no per-tool permission check and no scoping to the token's sub.

The X-API-Key is the real control point. Anyone holding it can read any user's data through these tools. Treat it as a production secret, issue user tokens only for accounts you intend to expose, and never ship the service key to a client.

Note that the user token identifies who is asking but does not limit what they can read.


Tool schemas

Each tool declares a Zod schema, which becomes the MCP inputSchema. Two schema styles are used:

  • Object schemas — most tools. Arguments are a named object.
  • String schemas — a few tools take a bare string argument.

The Accept header must include both application/json and text/event-stream for strict MCP clients; the server normalises it either way.


Errors

The MCP layer uses its own error shape, not the REST envelope:

{ "error": "Unauthorized: valid X-API-Key is required" }
{ "error": "Unauthorized: Bearer token is required for tools/call" }
{ "error": "Unauthorized: token subject (sub) is required" }
{ "error": "Internal server error" }

A tool that throws returns a JSON-RPC error object.