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
| Policy | Meaning |
|---|---|
auto | No approval gate. Use for read-only or harmless actions. |
user_confirm | The host UI must confirm before the tool runs. |
supervisor_approve | The server returns a pending outcome until a supervisor workflow approves it. |
denied | The action is blocked by policy and never executes. |
async_pending | The 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
- Client Tools -- tools that execute in the browser.
- Proxy Deployment -- production proxy routes and security controls.
- Message Parts -- tool-call lifecycle rendering.
docs/guides/server-tools.md