npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@omnixhq/ucp-client

v3.0.0

Published

Capability-aware TypeScript client for any UCP-compliant server

Readme

@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