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.
Run the example
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. Run the scripted calls
Run node dispatcher.mjs. You should see one successful preview, one invalid percentage and one unknown tool.
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.
node dispatcher.mjs
node --test dispatcher.test.mjsThe 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.
{"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.
// 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.
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.
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.
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
ERR_MODULE_NOT_FOUND
Keep dispatcher.test.mjs beside dispatcher.mjs and use the downloaded filenames.
INVALID_ARGUMENTS
Use an uppercase 1-to-5-letter ticker and a numeric percentage above 0 and at most 100. Extra fields are rejected.
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.