Advanced Skill Development
Looking for quick setup?
If you just want to use AON in Claude, 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.
Skill vs SDK vs Plugin
| Approach | What It Is | Best For | Effort |
|---|---|---|---|
| Skill | MCP tools registered on an MCP Server | AI agents with MCP support (Claude, etc.) | Low |
| SDK | Programmatic TypeScript library | Custom agents, full control | Medium |
| Plugin | Declarative configuration-based integration | Zero-code setups | Minimal |
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
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({ placementId: 'plc_YOUR_PLACEMENT_ID', 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 Key | Label | Decision Factors |
|---|---|---|
computers_electronics.consumer_electronics | Electronics and Devices | Product type, use case, budget range, key features |
computers_electronics.computers.software | SaaS and Software Tools | Tool type, team size, monthly budget, key features |
jobs_education | Jobs and Education | Subject 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 SoonPlugins 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
| Feature | Skill (MCP) | SDK (Direct) |
|---|---|---|
| Integration effort | Register MCP tools, LLM handles the rest | Write query/event code manually |
| Intent extraction | LLM extracts structured intent automatically | Developer constructs intent manually |
| Constraint control | Via tool input schema (category, preferences) | Public QueryConstraints category surface |
| Pagination | Limit parameter on tool | Full offset/limit control |
| Event tracking | Handled within Skill handler | Developer calls reportClick |
| Error handling | Skill returns error text to LLM | Developer catches typed exceptions |
| Customization | Moderate (tool schema, formatRecommendation) | Full (any SDK method, custom rendering) |
| Deployment | MCP 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({ placementId: 'plc_YOUR_PLACEMENT_ID', 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
- TypeScript SDK Guide -- Direct SDK integration
- Best Practices -- Production patterns, security, and FAQ
- Core Concepts -- Review data models and protocol fundamentals