import { getClient, extractCursor, extractIdFromUrn, formatPaginatedResponse, formatPaginatedMarkdown } from '../client.js';
import { formatError } from '../utils/errors.js';
import { ListSavedSearchesInput, GetSavedSearchResultsInput, GetSavedSearchNetNewResultsInput, ClearSavedSearchNetNewInput } from '../schemas/inputs.js';
// ============================================================================
// Response Types
// ============================================================================
interface SavedSearchQuery {
filter_group?: unknown;
controlled_filter_group?: unknown;
}
interface SavedSearch {
id: number;
entity_urn: string;
name: string;
type: 'COMPANIES_LIST' | 'PERSONS';
is_private: boolean;
created_at?: string;
updated_at?: string;
query?: SavedSearchQuery;
}
interface SavedSearchResultCompany {
id: number;
entity_urn: string;
name: string;
description?: string;
headcount?: number;
funding?: {
funding_total?: number;
funding_stage?: string;
};
location?: {
city?: string;
country?: string;
};
contact?: {
primary_email?: string;
};
}
interface SavedSearchResultsResponse {
count: number;
page_info?: {
next?: string;
has_next?: boolean;
};
results: SavedSearchResultCompany[];
}
/**
* Net New Results response has a different structure:
* - Uses "urns" field instead of "results"
* - Contains full company/person objects
*/
interface NetNewResultsResponse {
urns: SavedSearchResultCompany[];
page_info?: {
next?: string;
current?: string | null;
has_next?: boolean;
};
}
// ============================================================================
// Formatters
// ============================================================================
function formatSavedSearchMarkdown(search: SavedSearch, index: number): string {
const lines: string[] = [];
lines.push(`${index + 1}. **${search.name}** (ID: ${search.id})`);
lines.push(` - Type: ${search.type}`);
lines.push(` - Private: ${search.is_private ? 'Yes' : 'No'}`);
if (search.updated_at) {
lines.push(` - Last Updated: ${search.updated_at.slice(0, 10)}`);
}
return lines.join('\n');
}
function formatSavedSearchResultMarkdown(result: SavedSearchResultCompany, index: number): string {
const lines: string[] = [];
lines.push(`## ${index + 1}. ${result.name} (ID: ${result.id})`);
if (result.description) {
lines.push(`*${result.description.slice(0, 200)}${result.description.length > 200 ? '...' : ''}*`);
}
const details: string[] = [];
if (result.location) {
const loc = [result.location.city, result.location.country].filter(Boolean).join(', ');
if (loc) details.push(`Location: ${loc}`);
}
if (result.headcount) {
details.push(`Headcount: ${result.headcount}`);
}
if (result.funding) {
if (result.funding.funding_stage) {
details.push(`Stage: ${result.funding.funding_stage}`);
}
if (result.funding.funding_total) {
details.push(`Funding: $${(result.funding.funding_total / 1000000).toFixed(1)}M`);
}
}
if (result.contact?.primary_email) {
details.push(`Email: ${result.contact.primary_email}`);
}
if (details.length > 0) {
lines.push('');
for (const detail of details) {
lines.push(`- ${detail}`);
}
}
return lines.join('\n');
}
// ============================================================================
// Executors
// ============================================================================
/**
* Execute list saved searches tool
*
* @see GET /savedSearches
*/
export async function executeListSavedSearches(input: ListSavedSearchesInput): Promise<string> {
try {
const client = getClient();
const searches = await client.get<SavedSearch[]>('/savedSearches');
if (input.response_format === 'markdown') {
const lines: string[] = [];
lines.push('# Saved Searches');
lines.push('');
lines.push(`**Total:** ${searches.length} saved searches`);
lines.push('');
if (searches.length === 0) {
lines.push('No saved searches found.');
lines.push('');
lines.push('Create saved searches in the Harmonic web UI to enable deal flow monitoring.');
} else {
// Group by type
const companiesSearches = searches.filter(s => s.type === 'COMPANIES_LIST');
const personsSearches = searches.filter(s => s.type === 'PERSONS');
if (companiesSearches.length > 0) {
lines.push('## Company Searches');
lines.push('');
for (let i = 0; i < companiesSearches.length; i++) {
lines.push(formatSavedSearchMarkdown(companiesSearches[i], i));
lines.push('');
}
}
if (personsSearches.length > 0) {
lines.push('## Person Searches');
lines.push('');
for (let i = 0; i < personsSearches.length; i++) {
lines.push(formatSavedSearchMarkdown(personsSearches[i], i));
lines.push('');
}
}
}
lines.push('---');
lines.push('*Use harmonic_get_saved_search_results with the ID to get matching results.*');
return lines.join('\n');
}
return JSON.stringify({
data: searches,
count: searches.length,
summary: `Found ${searches.length} saved searches`
}, null, 2);
} catch (error) {
return formatError(error);
}
}
/**
* Execute get saved search results tool
*
* WARNING: Response sizes are large (~16KB per result).
* - size=5 (default): ~77KB
* - size=10 (max): ~156KB
* - size=50 (old default): ~804KB - would overwhelm context!
*
* Field filtering does NOT work on this endpoint.
*
* @see GET /savedSearches:results/{id}
*/
export async function executeGetSavedSearchResults(input: GetSavedSearchResultsInput): Promise<string> {
try {
const client = getClient();
const searchId = extractIdFromUrn(input.search_id);
// Enforce size limits: default 3, max 5
// Each result is ~27KB in practice, so size=5 returns ~135KB
const size = Math.min(input.size || 3, 5);
const params: Record<string, string | number | undefined> = {
size
};
if (input.cursor) {
params.cursor = input.cursor;
}
const response = await client.get<SavedSearchResultsResponse>(`/savedSearches:results/${searchId}`, params);
const nextCursor = extractCursor(response);
if (input.response_format === 'markdown') {
const lines: string[] = [];
lines.push(`# Saved Search Results`);
lines.push('');
lines.push(`**Search ID:** ${searchId}`);
lines.push(`**Total matches:** ${response.count}`);
lines.push(`**Showing:** ${response.results.length} results`);
lines.push('');
if (response.results.length === 0) {
lines.push('No results found for this saved search.');
} else {
for (let i = 0; i < response.results.length; i++) {
lines.push(formatSavedSearchResultMarkdown(response.results[i], i));
lines.push('');
}
}
if (nextCursor) {
lines.push('---');
lines.push(`*More results available. Use cursor: \`${nextCursor}\`*`);
}
return lines.join('\n');
}
const result = formatPaginatedResponse(response.results, nextCursor, response.count, 'results');
return JSON.stringify({
...result,
searchId,
}, null, 2);
} catch (error) {
return formatError(error);
}
}
/**
* Execute get saved search NET NEW results tool
*
* Returns only NEW results since the user subscribed to the search (or since new_results_since date).
*
* PREREQUISITE: User must subscribe to the saved search via Harmonic UI first.
* Returns 404 "Not Found" if not subscribed.
*
* Key differences from regular results:
* - Response field is "urns" not "results"
* - Only returns companies/people that newly match the search criteria
*
* @see GET /savedSearches:netNewResults/{id}
*/
export async function executeGetSavedSearchNetNewResults(input: GetSavedSearchNetNewResultsInput): Promise<string> {
try {
const client = getClient();
const searchId = extractIdFromUrn(input.search_id);
// Enforce size limits: default 3, max 5
// Each result is ~27KB in practice
const size = Math.min(input.size || 3, 5);
const params: Record<string, string | number | undefined> = {
size
};
if (input.cursor) {
params.cursor = input.cursor;
}
if (input.new_results_since) {
params.new_results_since = input.new_results_since;
}
const response = await client.get<NetNewResultsResponse>(`/savedSearches:netNewResults/${searchId}`, params);
// Extract cursor from page_info
const nextCursor = response.page_info?.next || null;
const hasMore = response.page_info?.has_next || false;
if (input.response_format === 'markdown') {
const lines: string[] = [];
lines.push(`# Net New Results`);
lines.push('');
lines.push(`**Search ID:** ${searchId}`);
lines.push(`**New matches:** ${response.urns.length} results`);
lines.push('');
if (response.urns.length === 0) {
lines.push('No new results since your last check.');
lines.push('');
lines.push('*New results appear when companies/people newly match your search criteria.*');
} else {
for (let i = 0; i < response.urns.length; i++) {
lines.push(formatSavedSearchResultMarkdown(response.urns[i], i));
lines.push('');
}
}
if (nextCursor) {
lines.push('---');
lines.push(`*More results available. Use cursor: \`${nextCursor}\`*`);
}
lines.push('');
lines.push('---');
lines.push('*Use harmonic_clear_saved_search_net_new to mark these as "seen".*');
return lines.join('\n');
}
return JSON.stringify({
data: response.urns,
count: response.urns.length,
hasMore,
nextCursor,
searchId,
note: 'Use harmonic_clear_saved_search_net_new to mark results as seen'
}, null, 2);
} catch (error) {
// Provide helpful error message for 404 (not subscribed)
const errorStr = formatError(error);
if (errorStr.includes('Not Found') || errorStr.includes('404')) {
return 'Error: Saved search not found or not subscribed.\n\nTo use net new results, you must first subscribe to the saved search via the Harmonic web UI.\n\nSteps:\n1. Go to Harmonic web console\n2. Find your saved search\n3. Click "Subscribe" to enable net new tracking\n4. Try this tool again';
}
return errorStr;
}
}
/**
* Execute clear saved search net new results tool
*
* Marks all net new results as "seen" so they won't appear in subsequent queries.
* Call this after processing net new results to reset the queue.
*
* @see POST /savedSearches:netNewResults/{id}:clear
*/
export async function executeClearSavedSearchNetNew(input: ClearSavedSearchNetNewInput): Promise<string> {
try {
const client = getClient();
const searchId = extractIdFromUrn(input.search_id);
// POST to :clear endpoint with empty body
await client.post<unknown>(`/savedSearches:netNewResults/${searchId}:clear`);
return JSON.stringify({
success: true,
searchId,
message: 'Net new results cleared. Future calls to harmonic_get_saved_search_net_new_results will only return results that become new matches after this point.'
}, null, 2);
} catch (error) {
const errorStr = formatError(error);
if (errorStr.includes('Not Found') || errorStr.includes('404')) {
return 'Error: Saved search not found or not subscribed.\n\nTo clear net new results, you must first subscribe to the saved search via the Harmonic web UI.';
}
return errorStr;
}
}