Skip to main content

Best Practices

This guide covers best practices for using HG MCP Server tools effectively and efficiently.

Examples use v1 parameters

Unless a block says otherwise, the payloads below are MCP v1. v2 renamed and restructured several inputs — it takes batch arrays (domains, hg_ids) where v1 takes a single companyDomain, and it nests search filters under firmographics. The advice itself applies to both versions; only the parameter shapes differ. Check the tool's own reference page — v1 or v2 — for the exact schema of the version you call.

Rate Limiting​

Understanding Limits​

HG Insights enforces a per-minute limit on tool calls, shared across your whole organization:

  • 1,000 tool calls per minute per organization. All of your organization's API keys draw on the same window — the limit is per company, not per key, so adding keys does not raise your ceiling. Discovery/protocol traffic (initialize, tools/list, etc.) is tracked separately and rarely a constraint.
  • 20 tool calls per second per organization. A burst cap on top of the per-minute one — a clump of calls in a single second is rejected even when you are well inside your minute budget, with a Retry-After of about a second. Pace bulk work instead of firing it all at once.
  • Cache hits still count toward both limits. They cost zero credits, but they do not buy extra throughput. See Caching & Pricing.

Best Practices​

1. Batch Your Requests​

Instead of making sequential calls, batch independent requests:

❌ Bad - Sequential:

const companies = ['example1.com', 'example2.com', 'example3.com'];
for (const domain of companies) {
const data = await mcp.call('company_firmographic', { companyDomain: domain });
console.log(data);
}
// Takes 3+ seconds if each request takes 1s

✅ Good - Parallel:

const companies = ['example1.com', 'example2.com', 'example3.com'];
const promises = companies.map(domain =>
mcp.call('company_firmographic', { companyDomain: domain })
);
const results = await Promise.all(promises);
// Takes ~1 second total

2. Implement Exponential Backoff​

When you hit rate limits, use exponential backoff:

async function callWithRetry(tool, params, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await mcp.call(tool, params);
} catch (error) {
if (error.code === 'RATE_LIMIT_EXCEEDED' && i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}

3. Use Company Search Wisely​

search_companies can return hundreds of results. Limit results to what you need:

The parameter shape differs by MCP version — v1 takes flat filter fields, v2 nests them under firmographics. Pick the block matching the version you call; copying the wrong one fails schema validation.

❌ Bad — a broad filter with a large limit burns quota on rows you will not read:

{
"tool": "search_companies",
"parameters": { "industry_ids": [7], "limit": 500 }
}

✅ Good (v1) — narrow the filters and keep limit to what you will actually use:

{
"tool": "search_companies",
"parameters": {
"industry_ids": [7],
"employee_min": 1000,
"employee_max": 5000,
"limit": 50
}
}

✅ Good (v2) — same query, nested shape:

{
"tool": "search_companies",
"parameters": {
"firmographics": {
"industries": [{ "ids": [7], "inclusion_method": "ANY_PRESENT" }],
"employees": { "min": 1000, "max": 5000 }
},
"limit": 50
}
}

Resolve industry IDs with search_industries_naics_sic rather than guessing. Note limit caps differ too: up to 1000 on v1, but 100 on v2.

Caching Strategy​

Understanding HG Insights Caching​

HG Insights caches many tool responses, and a cache hit costs zero credits. Durations run much longer than most people assume — 30 days for most company data tools, 14 days for product reviews, 7 days for searches and contact lookups, 1 day for the catalog. Not every tool is cached, though: see Caching & Pricing for the complete per-tool table and the list of uncached tools.

The cache is org-scoped: every user and every API key in your organization shares one bucket, so the first caller pays and everyone after them reads for free until it expires.

Cache hits do still consume rate-limit budget. They cost no credits, but they do not buy extra throughput.

Best Practices​

1. Implement Client-Side Caching​

Add your own caching layer for frequently accessed data:

const cache = new Map();
const CACHE_TTL = 60 * 60 * 1000; // 1 hour

async function getCachedCompanyData(domain) {
const cacheKey = `firmographic:${domain}`;
const cached = cache.get(cacheKey);

if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}

const data = await mcp.call('company_firmographic', {
companyDomain: domain
});

cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
}

2. Avoid Redundant Calls​

Track what you've already fetched:

const fetchedCompanies = new Set();

async function fetchIfNeeded(domain) {
if (fetchedCompanies.has(domain)) {
console.log(`Already fetched ${domain}, skipping`);
return null;
}

fetchedCompanies.add(domain);
return await mcp.call('company_firmographic', { companyDomain: domain });
}

Error Handling​

Common Errors​

Error CodeMeaningSolution
MISSING_INTEGRATIONRequired integration not configuredConfigure integration in settings
INVALID_PARAMETERSParameters don't match schemaCheck parameter types and requirements
RATE_LIMIT_EXCEEDEDToo many requestsImplement backoff, reduce request rate
COMPANY_NOT_FOUNDCompany doesn't exist in databaseVerify domain is correct
INTERNAL_ERRORServer errorRetry once, then contact support

Robust Error Handling​

async function robustToolCall(tool, params) {
try {
return await mcp.call(tool, params);
} catch (error) {
// Log error details
console.error(`Tool ${tool} failed:`, {
code: error.code,
message: error.message,
params: params
});

// Handle specific errors
switch (error.code) {
case 'COMPANY_NOT_FOUND':
return { notFound: true, domain: params.companyDomain };

case 'RATE_LIMIT_EXCEEDED':
// Wait and retry
await new Promise(r => setTimeout(r, 5000));
return await mcp.call(tool, params);

case 'MISSING_INTEGRATION':
throw new Error(
`Required integration missing. Please configure: ${error.requiredIntegrations.join(', ')}`
);

default:
// Re-throw unknown errors
throw error;
}
}
}

Tool Composition​

Effective Tool Chaining​

Chain tools logically from broad to specific:

Research Workflow Pattern​

1. search_companies (broad discovery)
↓
2. company_firmographic (basic validation)
↓
3. company_technographic (tech fit check)
↓
4. company_intent (timing check)
↓
5. company_spend (budget validation)

Implementation​

async function qualifyCompany(domain) {
// Step 1: Basic info
const firmographic = await mcp.call('company_firmographic', {
companyDomain: domain
});

// Early exit if company too small
if (firmographic.data.employeeCount < 100) {
return { qualified: false, reason: 'Too small' };
}

// Step 2: Technology fit
const technographic = await mcp.call('company_technographic', {
companyDomain: domain
});

const hasTargetTech = technographic.data.technologies.some(
tech => tech.name === 'Salesforce'
);

if (!hasTargetTech) {
return { qualified: false, reason: 'No target technology' };
}

// Step 3: Buying signals
const intent = await mcp.call('company_intent', {
companyDomain: domain
});

if (intent.data.overallIntentScore > 70) {
return {
qualified: true,
score: intent.data.overallIntentScore,
firmographic: firmographic.data,
technologies: technographic.data
};
}

return { qualified: false, reason: 'Low intent' };
}

Parallel Processing​

When data isn't dependent, fetch in parallel:

async function getCompleteProfile(domain) {
// These calls don't depend on each other
const [firmographic, technographic, intent, spending] = await Promise.all([
mcp.call('company_firmographic', { companyDomain: domain }),
mcp.call('company_technographic', { companyDomain: domain }),
mcp.call('company_intent', { companyDomain: domain }),
mcp.call('company_spend', { companyDomain: domain })
]);

return {
firmographic: firmographic.data,
technographic: technographic.data,
intent: intent.data,
spending: spending.data
};
}

Data Quality​

Validating Results​

Always validate data before using it:

function validateFirmographic(data) {
const required = ['companyName', 'domain', 'employeeCount'];
const missing = required.filter(field => !data[field]);

if (missing.length > 0) {
console.warn(`Missing fields: ${missing.join(', ')}`);
return false;
}

// Validate ranges
if (data.employeeCount < 0) {
console.warn('Invalid employee count');
return false;
}

return true;
}

const result = await mcp.call('company_firmographic', {
companyDomain: 'example.com'
});

if (validateFirmographic(result.data)) {
// Use the data
processCompany(result.data);
} else {
// Handle invalid data
logDataQualityIssue(result);
}

Handling Missing Data​

Not all companies have complete data. Handle missing fields gracefully:

function safelyAccessData(firmographic) {
return {
name: firmographic.companyName || 'Unknown',
employees: firmographic.employeeCount || 'Not available',
revenue: firmographic.revenueRange || 'Not disclosed',
industry: firmographic.industry || 'Unknown',
location: firmographic.location?.city
? `${firmographic.location.city}, ${firmographic.location.state}`
: 'Location not available'
};
}

Performance Optimization​

1. Request Only What You Need​

Use specific filters to reduce data transfer:

❌ Bad:

const all = await mcp.call('company_technographic', {
companyDomain: 'example.com'
});
// Returns all 500+ technologies
const salesforceOnly = all.data.technologies.filter(
t => t.name === 'Salesforce'
);

✅ Good:

// Use get_product_category to resolve a category id, then fetch specific data
const categoryResult = await mcp.call('get_product_category', { categoryName: 'CRM' });
const salesforceCat = categoryResult.data;

const tech = await mcp.call('company_technographic', {
companyDomain: 'example.com',
categoryId: salesforceCat.category_id
});

2. Monitor Performance​

Track tool execution times:

async function timedCall(tool, params) {
const start = Date.now();
try {
const result = await mcp.call(tool, params);
const duration = Date.now() - start;

console.log(`${tool} completed in ${duration}ms`);

// Alert on slow calls
if (duration > 5000) {
console.warn(`Slow call detected: ${tool} took ${duration}ms`);
}

return result;
} catch (error) {
const duration = Date.now() - start;
console.error(`${tool} failed after ${duration}ms`);
throw error;
}
}

3. Use Streaming for Large Datasets​

For large result sets, process results as they arrive:

// v1 parameter shape. Filters are top-level, not wrapped in a `filters` object.
// Paginate on total_count — there is no `hasMore` flag in the response.
async function* streamCompanySearch(filters) {
const BATCH_SIZE = 50;
let offset = 0;

for (;;) {
const result = await mcp.call('search_companies', {
...filters,
limit: BATCH_SIZE,
offset
});

const companies = result.data.companies ?? [];
for (const company of companies) {
yield company;
}

offset += companies.length;
if (companies.length === 0 || offset >= result.data.total_count) break;
}
}

// Usage — industry_ids comes from search_industries_naics_sic, not a free-text name
for await (const company of streamCompanySearch({ industry_ids: [7] })) {
await processCompany(company);
}

Security​

1. Protect API Keys​

Never expose API keys in client-side code:

❌ Bad:

// Client-side code
const apiKey = 'pk_live_abc123'; // Exposed in browser!

✅ Good:

// Server-side only
const apiKey = process.env.PHOENIX_API_KEY;

// Client calls your backend
fetch('/api/company-data', {
method: 'POST',
body: JSON.stringify({ domain: 'example.com' })
});

2. Validate User Input​

Always validate domains and parameters from users:

function isValidDomain(domain) {
// Basic validation
const domainRegex = /^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}$/i;
return domainRegex.test(domain);
}

async function safeCompanyLookup(userInput) {
const domain = userInput.trim().toLowerCase();

if (!isValidDomain(domain)) {
throw new Error('Invalid domain format');
}

return await mcp.call('company_firmographic', {
companyDomain: domain
});
}

3. Implement Usage Limits​

Add application-level rate limiting:

const userLimits = new Map();

async function rateLimitedCall(userId, tool, params) {
const key = `${userId}:${Date.now() / 1000 / 60 | 0}`; // Per minute
const count = userLimits.get(key) || 0;

if (count >= 10) { // 10 requests per minute per user
throw new Error('User rate limit exceeded');
}

userLimits.set(key, count + 1);
return await mcp.call(tool, params);
}

Next Steps​