Example file

agent-tool-endpoint.mjs

Node: an endpoint for agent tools, with a policy lookup (GET) and a callback booking (POST), API key check and Idempotency-Key handling.

agent-tool-endpoint.mjs · JavaScript (Node) Download
// An endpoint for a Subcontext agent tool, in Node 18 or later with no packages.
//
// It offers two operations you can add to a custom integration on Subcontext:
//
//   GET  /policies/{{ reference }}   look up a policy (the agent sends date_of_birth too)
//   POST /callbacks                  book a callback (the body comes from your body template)
//
// Subcontext sends your API key in the X-API-Key header (the integration's
// Auth type "Api key"). Writes carry an Idempotency-Key header that stays the
// same if the same tool call is sent again, so this file answers a repeat with
// the first answer instead of booking twice.
//
// Run it:
//   SUBCONTEXT_TOOL_KEY=A_LONG_RANDOM_VALUE node agent-tool-endpoint.mjs
// Subcontext only calls https addresses on the public internet, so put it
// behind your usual HTTPS web server or load balancer.

import { createServer } from "node:http";
import { randomUUID, timingSafeEqual } from "node:crypto";

// The value you store as a secret variable on the integration in Subcontext.
const API_KEY = process.env.SUBCONTEXT_TOOL_KEY;
const PORT = Number(process.env.PORT || 4100);
if (!API_KEY) throw new Error("Set SUBCONTEXT_TOOL_KEY to the API key Subcontext sends.");

// Stand-ins for your own systems.
const POLICIES = {
  "POL-1001": { date_of_birth: "1980-04-12", status: "active", renewal_date: "2027-03-01", monthly_premium: "18.40" },
};
const answeredWrites = new Map(); // Idempotency-Key -> the answer we gave

function send(res, status, body) {
  res.writeHead(status, { "Content-Type": "application/json" });
  res.end(JSON.stringify(body));
}

function keyMatches(given) {
  const a = Buffer.from(String(given || ""));
  const b = Buffer.from(API_KEY);
  return a.length === b.length && timingSafeEqual(a, b);
}

async function readJson(req) {
  let raw = "";
  for await (const chunk of req) raw += chunk;
  if (raw.length > 100_000) throw new Error("too large");
  return raw ? JSON.parse(raw) : {};
}

const server = createServer(async (req, res) => {
  // 1. Check the key. A 401 tells Subcontext the credentials are wrong.
  if (!keyMatches(req.headers["x-api-key"])) {
    return send(res, 401, { error: "Unknown API key." });
  }

  const url = new URL(req.url, "http://localhost");

  // 2. A read: GET /policies/POL-1001?date_of_birth=1980-04-12
  const lookup = url.pathname.match(/^\/policies\/([^/]+)$/);
  if (req.method === "GET" && lookup) {
    const reference = decodeURIComponent(lookup[1]).toUpperCase();
    const policy = POLICIES[reference];
    // Ask for something only the customer knows before giving anything away.
    if (!policy || policy.date_of_birth !== url.searchParams.get("date_of_birth")) {
      // The agent sees this status and message, so make the message useful.
      return send(res, 404, { error: "No policy matches that reference and date of birth. Ask the customer to check both." });
    }
    // Small, flat and only what the agent needs to answer.
    return send(res, 200, {
      reference,
      status: policy.status,
      renewal_date: policy.renewal_date,
      monthly_premium: policy.monthly_premium,
    });
  }

  // 3. A write: POST /callbacks with the body your template builds.
  if (req.method === "POST" && url.pathname === "/callbacks") {
    const idempotencyKey = req.headers["idempotency-key"];
    if (idempotencyKey && answeredWrites.has(idempotencyKey)) {
      const first = answeredWrites.get(idempotencyKey);
      return send(res, first.status, first.body);
    }

    let body;
    try {
      body = await readJson(req);
    } catch {
      return send(res, 400, { error: "The request body wasn't valid JSON." });
    }
    if (!body.phone) {
      return send(res, 422, { error: "A phone number is needed to book a callback. Ask the customer for one." });
    }

    const answer = {
      status: 201,
      body: {
        callback_id: randomUUID(),
        message: `Callback booked for ${body.preferred_time || "the next available slot"}.`,
      },
    };
    if (idempotencyKey) answeredWrites.set(idempotencyKey, answer);
    return send(res, answer.status, answer.body);
  }

  send(res, 404, { error: "No such operation." });
});

server.listen(PORT, () => console.log(`Agent tool endpoint on http://localhost:${PORT}`));

Stuck, or something missing?

Get in touch and a person on the team will help.

Contact us