Server Tools

Server tools execute business actions on your backend instead of in the browser. Use them for refunds, address changes, subscription edits, order mutations, ticket creation, entitlement checks, and any operation that needs privileged credentials, PII access, or an auditable authorization boundary.

The browser bundle never receives service-account keys, payment credentials, or long-lived API tokens. The SDK validates the model-supplied input, applies the declared approval policy, sends a typed request to your proxy, and renders the result as a transparent tool-call lifecycle in the UI.

Define a Business Action

import { createServerToolManifest, defineServerTool } from 'gecx-chat';

export const applyRefund = defineServerTool({
  name: 'apply_refund',
  description: 'Apply a partial refund after policy checks',
  inputSchema: {
    type: 'object',
    required: ['orderId', 'amountCents'],
    properties: {
      orderId: { type: 'string', pattern: '^ORD-' },
      amountCents: { type: 'integer', minimum: 1 },
      reason: { type: 'string', maxLength: 200 },
    },
  },
  outputSchema: {
    type: 'object',
    required: ['refundId', 'status'],
    properties: {
      refundId: { type: 'string' },
      status: { type: 'string', enum: ['queued'] },
    },
  },
  approvalPolicy: 'user_confirm',
  sideEffectLevel: 'state_changing',
  idempotency: {
    mode: 'required',
    duplicateBehavior: 'return_cached',
    ttlMs: 24 * 60 * 60_000,
  },
  auth: {
    required: true,
    scopes: ['orders.refund'],
    permissions: ['refund:create'],
  },
  audit: {
    classification: 'financial',
    redactInput: ['paymentToken'],
    redactOutput: ['processorTrace'],
  },
  timeoutMs: 5_000,
  http: { endpoint: '/chat/tool-call' },
});

export const serverToolManifest = createServerToolManifest([applyRefund]);

permissions.requiresUserApproval: true still works and is treated as approvalPolicy: 'user_confirm' for compatibility.

Manifest Shape

createServerToolManifest() returns a serializable document for policy review, agent instructions, proxy registration, or deployment checks:

{
  "version": 1,
  "generatedAt": "2026-05-13T00:00:00.000Z",
  "tools": [
    {
      "kind": "server",
      "name": "apply_refund",
      "description": "Apply a partial refund after policy checks",
      "inputSchema": {},
      "outputSchema": {},
      "approvalPolicy": "user_confirm",
      "idempotency": {
        "mode": "required",
        "duplicateBehavior": "return_cached",
        "ttlMs": 86400000,
        "headerName": "Idempotency-Key"
      },
      "auth": {
        "required": true,
        "scopes": ["orders.refund"],
        "permissions": ["refund:create"]
      },
      "audit": {
        "classification": "financial",
        "redactInput": ["paymentToken"],
        "redactOutput": ["processorTrace"]
      },
      "timeoutMs": 5000,
      "sideEffectLevel": "state_changing"
    }
  ]
}

Approval Policies

PolicyMeaning
autoNo approval gate. Use for read-only or harmless actions.
user_confirmThe host UI must confirm before the tool runs.
supervisor_approveThe server returns a pending outcome until a supervisor workflow approves it.
deniedThe action is blocked by policy and never executes.
async_pendingThe server accepted the request for asynchronous completion.

Do not auto-approve refunds, subscription changes, payment actions, address updates, order edits, or other state-changing examples unless your own security review explicitly allows that policy.

Example: computer_use

The bundled computer_use server tool is a good shape reference for high-stakes actions. It uses:

defineServerTool({
  name: 'computer_use',
  approvalPolicy: 'user_confirm',
  audit: { classification: 'regulated' },
  idempotency: 'required',
  sideEffectLevel: 'external_side_effect',
  // ...
});

approvalPolicy: 'user_confirm' gates session creation. The session itself surfaces a second approval tier inside the live ComputerUseSession for any action classified as high-risk (submit_form, download, navigate_external). audit.classification: 'regulated' ensures every action lands in the governance audit pipeline. See Computer-use.

Commerce governance and PCI lint

For commerce surfaces, configure governance.commerce to fail-close on PCI/PII leaks. Strict mode throws COMMERCE_PII_LEAK on PAN (Luhn-validated), CVV, SSN, email, phone, address, and EIN findings before the data can land in transcripts, debug bundles, approval surfaces, or analytics.

The companion gecx validate CLI applies a static PCI lint over your source tree — comment-aware, flags Luhn-valid card literals, CVV literals, and unguarded payment-method fields. Run it in CI.

Request and Response Envelopes

The SDK sends a stable idempotency key in both the header and JSON body:

{
  "name": "apply_refund",
  "input": { "orderId": "ORD-123", "amountCents": 1299 },
  "idempotencyKey": "tool-apply_refund-session-1-call-1",
  "context": {
    "sessionId": "session-1",
    "turnIndex": 0,
    "toolCallId": "call-1",
    "idempotencyKey": "tool-apply_refund-session-1-call-1"
  }
}

Your proxy returns one consistent envelope:

{
  "status": "completed",
  "output": { "refundId": "REF-123", "status": "queued" },
  "idempotencyKey": "tool-apply_refund-session-1-call-1"
}

Other outcomes use the same shape:

{ "status": "denied", "error": "not entitled", "errorCode": "SERVER_TOOL_UNAUTHORIZED" }
{ "status": "pending", "approvalPolicy": "supervisor_approve", "error": "pending supervisor approval" }
{ "status": "duplicate", "duplicateDisposition": "replayed", "output": { "refundId": "REF-123" } }
{ "status": "failed", "error": "refund window closed", "errorCode": "CUSTOMER_REFUND_DENIED" }

Proxy Handler

Use createServerToolHandler on your backend. The handler validates input and output schemas, enforces static approval policies, requires idempotency keys for state-changing definitions, de-dupes duplicates, threads ctx.signal, and emits audit-friendly events without logging raw input by default.

import { createServerToolHandler } from 'gecx-chat/server';

export const toolHandler = createServerToolHandler({
  allowedOrigins: ['https://app.example.com'],
  audit: (event) => console.log(JSON.stringify(event)),
  tools: {
    apply_refund: {
      inputSchema: applyRefund.inputSchema,
      outputSchema: applyRefund.outputSchema,
      approvalPolicy: 'user_confirm',
      sideEffectLevel: 'state_changing',
      idempotency: { mode: 'required', duplicateBehavior: 'return_cached' },
      audit: { classification: 'financial' },
      timeoutMs: 5_000,
      handler: async (input, ctx) => {
        const res = await fetch('https://orders.internal/refunds', {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${process.env.ORDER_SYSTEM_TOKEN}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(input),
          signal: ctx.signal,
        });
        return { output: await res.json() };
      },
    },
  },
});

UI Lifecycle

Server actions surface through the same tool-call part as client tools. UIs should render these states plainly: requested, awaiting_approval, pending_supervisor, async_pending, approved, executing, completed, failed, timed_out, denied, and duplicate.

Copy the recipes/tool-call-cards recipe for a small renderer that labels approval, pending, duplicate replay, and failure states without hiding the fact that a privileged server action is in progress.

Security Notes

  • Keep vendor credentials, service-account keys, and privileged refresh tokens only on the server.
  • Validate all input and output schemas. Unknown tools fail closed.
  • Require idempotency for state-changing actions and log duplicate disposition.
  • Redact tokens, credentials, payment traces, and PII from audit and analytics.
  • Treat supervisor approval and async completion as product/security policy decisions before public preview.

What's next

Source: docs/guides/server-tools.md