Developer walkthrough

Build a small tool-call framework in JavaScript

Run a bounded JavaScript tool dispatcher with strict validation, permissions, correlated receipts and regression tests.

As of 2026-10-10

One call, four checks

Start with a local allocation preview. It accepts a ticker and percentage, validates them, and returns a receipt with the original call ID. The SPY 20% input is a teaching fixture, not a trading recommendation or a NexusTrade portfolio request.

Proposed call passes tool, permission and argument checks before a correlated receipt returns.
Executable local fixture. No model, market data, saved portfolio or orders.

Run the example

  1. 1. Save the files together

    Download dispatcher.mjs and dispatcher.test.mjs into an empty folder. Use Node.js 24, the version used to verify this example.

  2. 2. Run the scripted calls

    Run node dispatcher.mjs. You should see one successful preview, one invalid percentage and one unknown tool.

  3. 3. Test the failure paths

    Run node --test dispatcher.test.mjs. Tests cover duplicate IDs, concurrent reuse, unsupported fields, denied writes, exhausted budget and redacted errors.

Shell
node dispatcher.mjs
node --test dispatcher.test.mjs

The actual local output

These receipts were produced by running the downloadable fixture. call_3 fails at tool lookup: place_order is deliberately absent from the registry. A registered preview with permission write fails with PERMISSION_DENIED.

JSON
{"id":"call_1","status":"ok","result":{"symbol":"SPY","allocationPercent":20,"persisted":false}}
{"id":"call_2","status":"error","code":"INVALID_ARGUMENTS"}
{"id":"call_3","status":"error","code":"UNKNOWN_TOOL"}

The complete dispatcher

Read the complete executable implementation

Keep validation beside the handler. Check tool identity and permission before dispatch; then validate arguments. A model-provided tool name is data, so this dispatcher never evaluates it or imports a module from it.

JavaScript
// Teaching fixture: no market data, model, accounts, or persisted portfolios.
import { pathToFileURL } from "node:url";
export function validateDraft(args) {
  if (!args || typeof args !== "object" || Array.isArray(args) ||
      Object.keys(args).some(key => !["symbol", "allocationPercent"].includes(key)) ||
      typeof args.symbol !== "string" || !/^[A-Z]{1,5}$/.test(args.symbol) ||
      !Number.isFinite(args.allocationPercent) || args.allocationPercent <= 0 ||
      args.allocationPercent > 100) throw new Error("INVALID_ARGUMENTS");
  return { symbol: args.symbol, allocationPercent: args.allocationPercent, persisted: false };
}
export function createDispatcher({ maxCalls = 4, handler = validateDraft } = {}) {
  if (!Number.isInteger(maxCalls) || maxCalls < 1 || maxCalls > 100) throw new Error("INVALID_BUDGET");
  const receipts = new Map(); let calls = 0;
  return async call => {
    if (!call || typeof call.id !== "string" || !/^[a-zA-Z0-9_-]{1,64}$/.test(call.id))
      return { id: null, status: "error", code: "INVALID_CALL" };
    // Reject reused IDs: this process-local guard is not durable idempotency.
    if (receipts.has(call.id)) return { id: call.id, status: "error", code: "DUPLICATE_CALL" };
    if (calls >= maxCalls) return { id: call.id, status: "error", code: "CALL_LIMIT" };
    receipts.set(call.id, true);
    calls++;
    if (call.name !== "preview_allocation") return { id: call.id, status: "error", code: "UNKNOWN_TOOL" };
    if (call.permission !== "read") return { id: call.id, status: "error", code: "PERMISSION_DENIED" };
    try { return { id: call.id, status: "ok", result: await handler(call.args) }; }
    catch (error) { return { id: call.id, status: "error", code: error instanceof Error && error.message === "INVALID_ARGUMENTS" ? "INVALID_ARGUMENTS" : "TOOL_FAILED" }; }
  };
}
export async function demo(handler) {
  const dispatch = createDispatcher({ handler });
  const calls = [
    { id: "call_1", name: "preview_allocation", permission: "read", args: { symbol: "SPY", allocationPercent: 20 } },
    { id: "call_2", name: "preview_allocation", permission: "read", args: { symbol: "SPY", allocationPercent: 120 } },
    { id: "call_3", name: "place_order", permission: "write", args: {} },
  ];
  for (const call of calls) console.log(JSON.stringify(await dispatch(call)));
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await demo();

How this maps to NexusTrade

These file names explain the platform implementation. They are not supported public imports or HTTP endpoints. To connect an external client, use the published MCP setup guide.

  1. Schemas describe the request

    NexusTrade maintains structured MCP schemas in toolMapper.ts. structuredArgsValidation.ts checks required fields, types and constraints before structured prompt processing. Some tools have dedicated direct handlers.

  2. Preview and save are different operations

    The MCP build_portfolio handler validates and canonicalizes a draft without persistence. create_portfolio uses a separate structured handler. A validated preview does not authorize a save or deployment.

  3. Execution has its own context

    The internal agent executor creates an action-specific request ID and history snapshot, calls a prompt implementation, and processes its response. The tutorial illustrates correlation but does not reproduce that billing, recovery or persistence machinery.

Before putting this behind a model

Decode the provider response into this call envelope and return each receipt using that provider's tool-result format. Validate the whole envelope at that boundary. Cap response size as well as call count. This example only bounds calls; it does not implement provider transport.

The Map prevents duplicate execution within one process. A production write needs durable operation identity, transaction or downstream idempotency support, account authorization, timeout and cancellation handling, and audit storage. Do not retry a side effect because its response was lost. Read its persisted status first.

Keep handler exceptions out of public messages. Here unexpected failures become TOOL_FAILED. In production, record redacted diagnostics privately with the same call ID. A permission field supplied by a model is not authorization: derive access from your authenticated server context.

Troubleshooting

  1. ERR_MODULE_NOT_FOUND

    Keep dispatcher.test.mjs beside dispatcher.mjs and use the downloaded filenames.

  2. INVALID_ARGUMENTS

    Use an uppercase 1-to-5-letter ticker and a numeric percentage above 0 and at most 100. Extra fields are rejected.

  3. DUPLICATE_CALL or CALL_LIMIT

    Use a fresh ID for a genuinely new request. The default budget is four calls per dispatcher instance; retries do not reset it.

Continue exploring