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
| 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/Python 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({ 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 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({ 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
- TypeScript SDK Guide -- Direct SDK integration
- Python SDK Guide -- Python SDK integration
- Best Practices -- Production patterns, security, and FAQ
- Core Concepts -- Review data models and protocol fundamentals