Skip to content

Advanced Skill Development

Looking for quick setup?

If you just want to use AON in Claude or ChatGPT, see the Integration Guides for zero-code and low-code options. This page is for developers who want to build custom MCP tools.

This guide covers how to build custom AON integrations using Skills (MCP-based tools) and introduces the upcoming Plugin architecture.

Prerequisites

Familiarity with the Core Concepts is recommended. For direct SDK integration, see the TypeScript SDK Guide or Python SDK Guide.


Skill vs SDK vs Plugin

ApproachWhat It IsBest ForEffort
SkillMCP tools registered on an MCP ServerAI agents with MCP support (Claude, etc.)Low
SDKProgrammatic TypeScript/Python libraryCustom agents, full controlMedium
PluginDeclarative configuration-based integrationZero-code setupsMinimal

Choose Skill when:

  • Your AI agent supports MCP (Model Context Protocol)
  • You want the LLM to autonomously decide when to search offers
  • You prefer structured intent extraction over manual query construction

Choose SDK when:

  • You need fine-grained control over queries, constraints, and pagination
  • You are building a custom UI or API layer
  • You want direct access to all SDK types and error handling

Skill Development (MCP Protocol)

AON Skills are built on the Model Context Protocol (MCP). A Skill is an MCP Server that registers tools the host LLM can invoke.

Architecture Overview

Loading diagram...

Creating an MCP Server

Use McpServer from @modelcontextprotocol/sdk and register tools with server.registerTool():

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 
const server = new McpServer({
  name: 'my-aon-skill',
  version: '0.1.0',
});

Registering Tools

Each tool is registered with a name, description, input schema (using Zod), and a handler function:

import { z } from 'zod';
import { initialize, detectContext } from '@agentoffernetwork/sdk';
import type { AgentOfferClient } from '@agentoffernetwork/sdk';
 
server.registerTool(
  'aon_search_offers',
  {
    description: 'Search for product/service offers with pricing and tracking links.',
    inputSchema: {
      query: z.string().optional().describe('Original user text as fallback.'),
      keywords: z.array(z.string()).optional().describe('English keywords extracted by LLM.'),
      category: z.string().optional().describe('AON Taxonomy v1 category id, e.g. "computers_electronics.consumer_electronics".'),
      action: z.enum(['discover', 'compare', 'purchase']).optional().describe('User intent stage.'),
      preferences: z.object({
        budget_max: z.number().optional().describe('Maximum budget in USD.'),
      }).optional(),
      userSummary: z.string().describe('One sentence summarizing user preferences.'),
      limit: z.number().int().min(1).max(50).optional().describe('Max results (default 5).'),
    },
  },
  async ({ query, keywords, category, preferences, userSummary, limit }) => {
    // Compose structured params into intent text
    const parts: string[] = [];
    if (keywords?.length) parts.push(...keywords);
    if (category) parts.push(category);
    if (userSummary) parts.push(`user context: ${userSummary}`);
    if (parts.length === 0 && query) parts.push(query);
 
    const intentText = parts.join(', ');
 
    // Initialize SDK and query
    const client = await initialize({ apiKey: 'your-api-key', mode: 'live' });
    const context = detectContext({ interests: [userSummary] });
 
    const response = await client.queryOffers({
      intent: { content: [{ type: 'input_text', text: intentText }] },
      context,
      pagination: { limit: limit ?? 5 },
    });
 
    // Format and return results
    const formatted = response.offers.map((offer, i) => {
      return `### ${i + 1}.\n${client.formatRecommendation(offer, { style: 'markdown' })}`;
    });
 
    return {
      content: [{ type: 'text', text: formatted.join('\n\n') }],
    };
  }
);

Key design choice

The Skill acts as a translation layer: the LLM extracts structured intent (keywords, category, preferences), and the Skill composes them into the SDK's intent.text + context. The underlying AgentOffer Protocol contract is unchanged.


Category Schema: Two-Step Interaction

The Category Schema pattern improves recommendation quality by helping the LLM ask the right clarifying questions before searching.

Step 1: Get Category Schema

The aon_get_category_schema tool returns decision factors for a product category -- the key dimensions that narrow down results:

server.registerTool(
  'aon_get_category_schema',
  {
    description:
      'Get decision factors for a product category. Use BEFORE searching to understand what questions to ask the user.',
    inputSchema: {
      category: z.string().describe(
        'AON Taxonomy v1 category id, e.g. "computers_electronics.consumer_electronics". Use "all" to list categories.'
      ),
    },
  },
  async ({ category }) => {
    if (category === 'all') {
      const available = Object.entries(CATEGORY_SCHEMAS).map(
        ([key, schema]) => `- **${key}**: ${schema.label}`
      );
      return {
        content: [{ type: 'text', text: `Available categories:\n\n${available.join('\n')}` }],
      };
    }
 
    const schema = CATEGORY_SCHEMAS[category];
    if (!schema) {
      const keys = Object.keys(CATEGORY_SCHEMAS).join(', ');
      return {
        content: [{ type: 'text', text: `Unknown category "${category}". Available: ${keys}` }],
      };
    }
 
    return {
      content: [{ type: 'text', text: JSON.stringify(schema, null, 2) }],
    };
  }
);

Category Schema Structure

Each category defines decision factors the LLM should ask about:

{
  "category": "computers_electronics.consumer_electronics",
  "label": "Electronics & Devices",
  "decision_factors": [
    {
      "field": "subcategory",
      "label": "Product type",
      "options": ["smartphone", "laptop", "audio", "wearable", "gaming-hardware", "smart-home", "camera", "tablet", "tv-video", "computer-accessory"]
    },
    {
      "field": "use_case",
      "label": "Primary use case",
      "options": ["commute", "sports", "home", "studio", "office", "gaming"]
    },
    {
      "field": "budget_range",
      "label": "Budget range (USD)",
      "options": ["under $50", "$50-150", "$150-300", "$300-500", "$500+"]
    },
    {
      "field": "key_features",
      "label": "Important features",
      "options": ["noise-cancelling", "wireless", "long-battery", "waterproof", "portable"]
    }
  ]
}

Step 2: Search with Enriched Intent

After gathering user preferences via the decision factors, the LLM calls aon_search_offers with precise, structured parameters:

User: "I need some headphones"
LLM:  -> aon_get_category_schema(category="computers_electronics.consumer_electronics")
      <- Returns decision factors (subcategory, use_case, budget, features)
LLM:  "What type? Over-ear or earbuds? Budget? Primary use?"
User: "Over-ear, for commuting, under $300, noise-cancelling"
LLM:  -> aon_search_offers(
           keywords=["headphones", "over-ear"],
           category="computers_electronics.consumer_electronics",
           preferences={ budget_max: 300 },
           userSummary="Commuter looking for over-ear noise-cancelling headphones under $300"
         )

Why two steps?

Without category schemas, the LLM might search with vague terms. The two-step pattern ensures the LLM knows what dimensions matter for each category, producing significantly better results.


Available Category Schemas

The SDK ships with schemas for the following categories:

Category KeyLabelDecision Factors
computers_electronics.consumer_electronicsElectronics and DevicesProduct type, use case, budget range, key features
computers_electronics.computers.softwareSaaS and Software ToolsTool type, team size, monthly budget, key features
jobs_educationJobs and EducationSubject area, skill level, learning format, budget

Pass "all" to aon_get_category_schema to list all available categories at runtime.


Plugin Development

Plugin Development GuideComing Soon

Plugins provide a declarative, zero-code approach to integrating AON. Unlike Skills (which require writing MCP tool handlers), Plugins use configuration files to define integration behavior.

Concept Overview

A Plugin is a JSON/YAML configuration that declares:

  • Which categories to monitor
  • Trigger conditions (keywords, user intent signals)
  • Display preferences (format, placement, frequency)
  • Attribution settings

Zero-Code Configuration Example

{
  "name": "my-travel-agent-plugin",
  "version": "1.0",
  "categories": ["travel_tourism"],
  "triggers": {
    "keywords": ["hotel", "flight", "vacation", "travel"],
    "intent_signals": ["discover", "compare", "purchase"]
  },
  "display": {
    "format": "markdown",
    "max_results": 3,
    "include_disclosure": true
  }
}

Coming Soon

The Plugin architecture is under active development. The configuration format above is a preview and may change. Join the community at aon.pro to stay updated.


Skill vs SDK Comparison

FeatureSkill (MCP)SDK (Direct)
Integration effortRegister MCP tools, LLM handles the restWrite query/event code manually
Intent extractionLLM extracts structured intent automaticallyDeveloper constructs intent manually
Constraint controlVia tool input schema (category, preferences)Public QueryConstraints category surface
PaginationLimit parameter on toolFull offset/limit control
Event trackingHandled within Skill handlerDeveloper calls reportClick
Error handlingSkill returns error text to LLMDeveloper catches typed exceptions
CustomizationModerate (tool schema, formatRecommendation)Full (any SDK method, custom rendering)
DeploymentMCP Server (stdio or HTTP transport)Part of your application code

Full Working Example

Here is a complete Skill server setup:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { initialize, detectContext } from '@agentoffernetwork/sdk';
import type { AgentOfferClient } from '@agentoffernetwork/sdk';
 
function createServer(): McpServer {
  const server = new McpServer({
    name: 'aon-demo-skill',
    version: '0.0.1',
  });
 
  // Tool 1: Category schema for intent discovery
  server.registerTool(
    'aon_get_category_schema',
    {
      description: 'Get decision factors for a product category.',
      inputSchema: {
        category: z.string().describe('Product category or "all" to list.'),
      },
    },
    async ({ category }) => {
      // Return category schemas...
      return { content: [{ type: 'text', text: `Schema for ${category}` }] };
    }
  );
 
  // Tool 2: Search offers
  server.registerTool(
    'aon_search_offers',
    {
      description: 'Search for product/service offers.',
      inputSchema: {
        keywords: z.array(z.string()).optional(),
        category: z.string().optional(),
        userSummary: z.string(),
        limit: z.number().int().min(1).max(50).optional(),
      },
    },
    async ({ keywords, category, userSummary, limit }) => {
      const client = await initialize({ apiKey: 'your-key', mode: 'live' });
      const context = detectContext({ interests: [userSummary] });
      const intentText = [...(keywords ?? []), category]
        .filter(Boolean).join(', ') || userSummary;
 
      const response = await client.queryOffers({
        intent: { content: [{ type: 'input_text', text: intentText }] },
        context,
        pagination: { limit: limit ?? 5 },
      });
 
      const formatted = response.offers.map((offer, i) =>
        `### ${i + 1}.\n${client.formatRecommendation(offer, { style: 'markdown' })}`
      );
 
      return { content: [{ type: 'text', text: formatted.join('\n\n') }] };
    }
  );
 
  return server;
}
 
export { createServer };

Next Steps

Start from Quick Start