Skip to content

OmnixHQ/ucp-client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

51 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

@omnixhq/ucp-client

npm version CI License: MIT TypeScript Node.js

TypeScript client that connects to any UCP-compliant server, discovers what it supports, and gives your AI agent ready-to-use tools.

Why

Every AI agent that wants to buy something from a UCP store needs to discover capabilities, construct headers, handle idempotency, parse errors, manage escalation. That's a lot of boilerplate.

@omnixhq/ucp-client handles all of it. You connect, get tools, give them to the LLM — and the LLM orchestrates the checkout flow on its own.

Install

Two release channels track the two UCP spec tracks:

Stable (recommended)

Based on the stable UCP spec (@omnixhq/ucp-js-sdk@latest). Supports checkout, fulfillment, discount, order, and identity linking.

npm install @omnixhq/ucp-client

Draft

Based on the draft UCP spec (@omnixhq/ucp-js-sdk@next). Includes everything in stable plus capabilities still being finalized in the spec (catalog, cart, and others as they land).

npm install @omnixhq/ucp-client@next

Note: Draft builds track the spec draft and may have breaking changes between releases. Use stable in production.

Quick Start

import Anthropic from '@anthropic-ai/sdk';
import { UCPClient } from '@omnixhq/ucp-client';

// Connect to any UCP server — discovers capabilities automatically
const client = await UCPClient.connect({
  gatewayUrl: 'https://store.example.com',
  agentProfileUrl: 'https://your-app.com/.well-known/ucp',
});

// Get tools — only what this server supports, with schemas + executors
const tools = client.getAgentTools();
const anthropic = new Anthropic();
const messages: Anthropic.MessageParam[] = [
  { role: 'user', content: 'Buy me running shoes under $100' },
];

// Agent loop — Claude decides which tools to call and in what order
while (true) {
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 4096,
    tools: tools.map((t) => ({
      name: t.name,
      description: t.description,
      input_schema: t.parameters,
    })),
    messages,
  });

  // Add Claude's response to the conversation
  messages.push({ role: 'assistant', content: response.content });

  // Find tool calls and execute them
  const toolBlocks = response.content.filter((b) => b.type === 'tool_use');

  if (toolBlocks.length === 0) break;

  const toolResults: Anthropic.ToolResultBlockParam[] = [];
  for (const block of toolBlocks) {
    const tool = tools.find((t) => t.name === block.name);
    if (tool) {
      const result = await tool.execute(block.input as Record<string, unknown>);
      toolResults.push({
        type: 'tool_result',
        tool_use_id: block.id,
        content: JSON.stringify(result),
      });
    }
  }

  messages.push({ role: 'user', content: toolResults });
}

You write the loop. Claude decides the flow: search → create checkout → set shipping → complete → done.

Each tool returned by getAgentTools() has: name, description, parameters (JSON Schema), and execute(params) — everything an LLM needs.

Error handling

import {
  UCPError,
  UCPEscalationError,
  UCPIdempotencyConflictError,
  UCPOAuthError,
} from '@omnixhq/ucp-client';

try {
  await client.checkout.complete(sessionId, payload);
} catch (err) {
  if (err instanceof UCPEscalationError) {
    // Redirect buyer to err.continue_url for merchant-hosted checkout
  }
  if (err instanceof UCPIdempotencyConflictError) {
    // HTTP 409 — idempotency key reused with a different request body
  }
  if (err instanceof UCPOAuthError) {
    // OAuth token exchange / refresh / revocation failed — err.statusCode
  }
  if (err instanceof UCPError) {
    // err.code — e.g., 'PRODUCT_NOT_FOUND'
    // err.messages[] — all messages from the server
    // err.path — JSONPath to the field that caused the error
    // err.type — 'error' | 'warning' | 'info'
  }
}

catchErrors option

Pass { catchErrors: true } to any adapter to return errors as structured objects instead of throwing. The agent observes the failure and can decide what to do next — no try/catch needed in every tool call.

import { executeAnthropicToolCall } from '@omnixhq/ucp-client/anthropic';
import type { ToolErrorResult } from '@omnixhq/ucp-client';

const result = await executeAnthropicToolCall(agentTools, toolName, input, { catchErrors: true });

if (result && typeof result === 'object' && 'error' in result) {
  const err = result as ToolErrorResult;
  // { error: 'OUT_OF_STOCK: Item unavailable' }
  // { requires_escalation: true, continue_url: 'https://...' }
}

All five adapters (openai, anthropic, mcp, vercel-ai, langchain) support catchErrors.

Capabilities

The tools you get depend on what the server declares:

Server declares Tools you get
dev.ucp.shopping.checkout create_checkout, get_checkout, update_checkout, complete_checkout, cancel_checkout
dev.ucp.shopping.fulfillment + set_fulfillment, select_destination, select_fulfillment_option, create_fulfillment_method, update_fulfillment_method, update_fulfillment_group
dev.ucp.shopping.discount + apply_discount_codes
dev.ucp.shopping.order + get_order, update_order, update_order_line_item
dev.ucp.common.identity_linking + get_authorization_url, exchange_auth_code, refresh_access_token, revoke_token

Connect to a different server → get different tools. Your agent code stays the same.

Checking capabilities manually

If you need more control than getAgentTools():

const client = await UCPClient.connect(config);

client.checkout; // CheckoutCapability | null
client.order; // OrderCapability | null
client.identityLinking; // IdentityLinkingCapability | null

if (client.checkout) {
  client.checkout.extensions.fulfillment; // boolean
  client.checkout.extensions.discount; // boolean
  client.checkout.extensions.buyerConsent; // boolean
  client.checkout.extensions.ap2Mandate; // boolean
}

console.log(Object.keys(client.paymentHandlers));
// e.g., ['com.google.pay', 'dev.shopify.shop_pay']

client.signingKeys; // JWK[] — EC P-256 keys for webhook verification

Webhook signature verification

UCP businesses sign webhook POST requests with a detached JWS in the Request-Signature header (RFC 7797). The JWT header MUST include a kid claim identifying the signing key.

Use createWebhookVerifier to get a stateful verifier that fetches and caches signing keys from the business's discovery profile. It automatically re-fetches on a kid cache miss to support zero-downtime key rotation.

import { createWebhookVerifier } from '@omnixhq/ucp-client';

const verifier = createWebhookVerifier('https://store.example.com');

// In your webhook handler — MUST respond quickly with 2xx, process async:
const valid = await verifier.verify(rawBody, req.headers['request-signature']);
if (!valid) return res.status(401).send('Invalid signature');

// Safe to process

Keys are loaded lazily on the first verify() call from <gatewayUrl>/.well-known/ucp and cached by kid. A kid not found in cache triggers one re-fetch (key rotation support).

If you already have signing keys loaded (e.g. from client.signingKeys), use verifyRequestSignature directly:

import { UCPClient, verifyRequestSignature } from '@omnixhq/ucp-client';

const client = await UCPClient.connect(config);
const valid = await verifyRequestSignature(rawBody, signature, client.signingKeys);

Parsing webhook payloads

After verifying the signature, parse the raw body into a typed WebhookEvent with parseWebhookEvent. Throws UCPError with code INVALID_WEBHOOK_PAYLOAD if the body is not valid JSON or doesn't match the UCP order event schema.

import { createWebhookVerifier, parseWebhookEvent } from '@omnixhq/ucp-client';

const verifier = createWebhookVerifier('https://store.example.com');

// In your webhook handler:
const valid = await verifier.verify(rawBody, req.headers['request-signature']);
if (!valid) return res.status(401).send('Invalid signature');

const event = parseWebhookEvent(rawBody);
// event.event_id, event.created_time, event.order
console.log(event.order.id);

Framework adapters

Ready-made adapters convert getAgentTools() output to each framework's native format — no manual mapping.

Framework Import Example
Anthropic SDK @omnixhq/ucp-client (built-in) examples/anthropic-agent-loop.ts
OpenAI SDK @omnixhq/ucp-client/openai examples/openai-agent-loop.ts
Vercel AI SDK @omnixhq/ucp-client/vercel-ai examples/vercel-ai-nextjs.ts
LangChain @omnixhq/ucp-client/langchain examples/langchain-agent.ts
MCP server @omnixhq/ucp-client/mcp examples/mcp-server.ts

OpenAI:

import { toOpenAITools, executeOpenAIToolCall } from '@omnixhq/ucp-client/openai';

const tools = toOpenAITools(client.getAgentTools());

// In your agent loop:
const response = await openai.chat.completions.create({ model: 'gpt-4o', tools, messages });

for (const call of response.choices[0].message.tool_calls ?? []) {
  const result = await executeOpenAIToolCall(
    agentTools,
    call.function.name,
    JSON.parse(call.function.arguments),
  );
}

Vercel AI SDK:

import { toVercelAITools } from '@omnixhq/ucp-client/vercel-ai';
import { jsonSchema, streamText } from 'ai';

const rawTools = toVercelAITools(client.getAgentTools());

// Wrap parameters with jsonSchema() for strict Vercel AI SDK typing:
const tools = Object.fromEntries(
  Object.entries(rawTools).map(([name, t]) => [
    name,
    { ...t, parameters: jsonSchema(t.parameters) },
  ]),
);

const result = await streamText({ model, tools, messages });

LangChain:

import { toLangChainTools } from '@omnixhq/ucp-client/langchain';
import { DynamicStructuredTool } from '@langchain/core/tools';
import { z } from 'zod';

const rawTools = toLangChainTools(client.getAgentTools());

const tools = rawTools.map(
  (t) =>
    new DynamicStructuredTool({
      name: t.name,
      description: t.description,
      schema: z.object({}),
      func: t.call,
    }),
);

MCP server:

import { toMCPTools, executeMCPToolCall } from '@omnixhq/ucp-client/mcp';

const agentTools = client.getAgentTools();

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: toMCPTools(agentTools),
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => ({
  content: [
    {
      type: 'text',
      text: JSON.stringify(
        await executeMCPToolCall(agentTools, req.params.name, req.params.arguments ?? {}),
      ),
    },
  ],
}));

Development

npm install
npm run build        # tsdown (dual ESM + CJS)
npm test             # vitest (unit tests)
npm run test:types   # type-level tests (vitest --typecheck.only)
npm run typecheck    # tsc --noEmit
npm run lint         # eslint
npm run check:exports # attw
npm run check:publish # publint

See CONTRIBUTING.md for code style and CLA.

License

MIT