import { z } from 'zod';
/**
* Zod schemas for runtime input validation.
*
* IMPORTANT: These Zod schemas are the source of truth for input validation.
* They are passed directly to the MCP SDK's registerTool() method.
*
* Note: Using z.strictObject() for strict mode (Zod 4 best practice).
*/
// Response format enum for all tools that return data
export const ResponseFormatEnum = z.enum(['json', 'markdown']);
export type ResponseFormat = z.infer<typeof ResponseFormatEnum>;
// Employee group types for filtering
// Values confirmed via API testing - documentation had incorrect values
export const EmployeeGroupTypeEnum = z.enum([
'ALL',
'FOUNDERS_AND_CEO',
'EXECUTIVES',
'FOUNDERS',
'LEADERSHIP',
'NON_LEADERSHIP',
'ADVISORS',
'NON_PARTNERS'
]);
export type EmployeeGroupType = z.infer<typeof EmployeeGroupTypeEnum>;
// Employee status for filtering
export const EmployeeStatusEnum = z.enum([
'ACTIVE',
'ACTIVE_AND_NOT_ACTIVE',
'NOT_ACTIVE'
]);
export type EmployeeStatus = z.infer<typeof EmployeeStatusEnum>;
// Numeric ID validator - ensures string IDs are numeric
const NumericIdSchema = z.string()
.regex(/^\d+$/, 'ID must be numeric (digits only)')
.describe('Numeric ID');
// ID that can be numeric or URN format
const IdOrUrnSchema = z.string()
.describe('Numeric ID or full URN (e.g., "18920281" or "urn:harmonic:company:18920281")');
// Common pagination schema with cursor-based pagination
export const CursorPaginationSchema = z.object({
size: z.number().int().min(1).max(100).optional().describe('Number of items to return (default: 25, max: 100)'),
cursor: z.string().optional().describe('Pagination cursor from previous response (page_info.next)')
});
// Common response format schema
export const ResponseFormatSchema = z.object({
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" for structured data or "markdown" for human-readable')
});
// ============================================================================
// Search Tools
// ============================================================================
/**
* Search companies (natural language) input schema
* @see GET /search/search_agent
*/
export const SearchCompaniesInputSchema = z.strictObject({
query: z.string().min(1).max(500).describe('Natural language search query (e.g., "AI startups in San Francisco", "fintech companies with Series A funding")'),
size: z.number().int().min(1).max(1000).optional().describe('Number of results to return (default: 25, max: 1000)'),
similarity_threshold: z.number().min(0).max(1).optional().describe('Minimum similarity score 0.0-1.0 for filtering results'),
cursor: z.string().optional().describe('Pagination cursor from previous response'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type SearchCompaniesInput = z.infer<typeof SearchCompaniesInputSchema>;
/**
* Search typeahead (autocomplete) input schema
* @see GET /search/typeahead
*/
export const SearchTypeaheadInputSchema = z.strictObject({
query: z.string().min(1).max(200).describe('Company name, partial name, or domain to search (e.g., "Harmonic", "stripe.com")'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type SearchTypeaheadInput = z.infer<typeof SearchTypeaheadInputSchema>;
/**
* Find similar companies input schema
* @see GET /search/similar_companies/{id}
*/
export const FindSimilarCompaniesInputSchema = z.strictObject({
company_id: IdOrUrnSchema.describe('Company ID or URN to find similar companies for'),
size: z.number().int().min(1).max(1000).optional().describe('Number of similar companies to return (default: 25, max: 1000)'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type FindSimilarCompaniesInput = z.infer<typeof FindSimilarCompaniesInputSchema>;
// ============================================================================
// Company Tools
// ============================================================================
/**
* Lookup company (by identifier) input schema
* @see POST /companies
*/
export const LookupCompanyInputSchema = z.strictObject({
website_domain: z.string().optional().describe('Company domain (e.g., "harmonic.ai", "stripe.com")'),
website_url: z.string().url().optional().describe('Full website URL'),
linkedin_url: z.string().url().optional().describe('LinkedIn company page URL'),
crunchbase_url: z.string().url().optional().describe('Crunchbase profile URL'),
pitchbook_url: z.string().url().optional().describe('Pitchbook profile URL'),
twitter_url: z.string().url().optional().describe('Twitter/X profile URL'),
instagram_url: z.string().url().optional().describe('Instagram profile URL'),
facebook_url: z.string().url().optional().describe('Facebook page URL'),
angellist_url: z.string().url().optional().describe('AngelList profile URL'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
}).refine(
data => data.website_domain || data.website_url || data.linkedin_url || data.crunchbase_url ||
data.pitchbook_url || data.twitter_url || data.instagram_url || data.facebook_url || data.angellist_url,
{ message: 'At least one identifier is required (website_domain, website_url, linkedin_url, crunchbase_url, pitchbook_url, twitter_url, instagram_url, facebook_url, or angellist_url)' }
);
export type LookupCompanyInput = z.infer<typeof LookupCompanyInputSchema>;
/**
* Get company by ID input schema
* @see GET /companies/{id}
*/
export const GetCompanyInputSchema = z.strictObject({
company_id: IdOrUrnSchema.describe('Company ID or URN'),
include_fields: z.array(z.string()).optional().describe('Specific fields to include (e.g., ["name", "funding", "headcount"])'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type GetCompanyInput = z.infer<typeof GetCompanyInputSchema>;
/**
* Get company employees input schema
* @see GET /companies/{id}/employees
*/
export const GetCompanyEmployeesInputSchema = z.strictObject({
company_id: IdOrUrnSchema.describe('Company ID or URN'),
size: z.number().int().min(1).max(100).optional().describe('Number of employees to return (default: 10, max: 100)'),
page: z.number().int().min(0).optional().describe('Page number for pagination (0-indexed)'),
employee_group_type: EmployeeGroupTypeEnum.optional().describe('Filter by employee type: ALL, FOUNDERS_AND_CEO, EXECUTIVES, FOUNDERS, LEADERSHIP, NON_LEADERSHIP, ADVISORS'),
employee_status: EmployeeStatusEnum.optional().describe('Filter by status: ACTIVE, ACTIVE_AND_NOT_ACTIVE, NOT_ACTIVE'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type GetCompanyEmployeesInput = z.infer<typeof GetCompanyEmployeesInputSchema>;
/**
* Get company connections (team network) input schema
* @see GET /companies/{id}/userConnections
*/
export const GetCompanyConnectionsInputSchema = z.strictObject({
company_id: IdOrUrnSchema.describe('Company ID or URN'),
first: z.number().int().min(1).max(100).optional().describe('Number of connections to return (default: 10, max: 100)'),
after: z.string().optional().describe('Cursor for pagination'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type GetCompanyConnectionsInput = z.infer<typeof GetCompanyConnectionsInputSchema>;
// ============================================================================
// Person Tools
// ============================================================================
/**
* Lookup person (by LinkedIn URL) input schema
* @see POST /persons
*/
export const LookupPersonInputSchema = z.strictObject({
linkedin_url: z.string().url().describe('LinkedIn profile URL (e.g., "https://linkedin.com/in/username")'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type LookupPersonInput = z.infer<typeof LookupPersonInputSchema>;
/**
* Get person by ID input schema
* @see GET /persons/{id}
*/
export const GetPersonInputSchema = z.strictObject({
person_id: IdOrUrnSchema.describe('Person ID or URN'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type GetPersonInput = z.infer<typeof GetPersonInputSchema>;
// ============================================================================
// Saved Search Tools
// ============================================================================
/**
* List saved searches input schema
* @see GET /savedSearches
*/
export const ListSavedSearchesInputSchema = z.strictObject({
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type ListSavedSearchesInput = z.infer<typeof ListSavedSearchesInputSchema>;
/**
* Get saved search results input schema
* @see GET /savedSearches:results/{id}
*
* WARNING: Response sizes are large (~27KB per result in practice).
* - size=3 (default): ~81KB
* - size=5 (max): ~135KB
* Field filtering does NOT work on this endpoint.
*/
export const GetSavedSearchResultsInputSchema = z.strictObject({
search_id: IdOrUrnSchema.describe('Saved search ID or URN'),
size: z.number().int().min(1).max(5).optional().describe('Number of results to return (default: 3, max: 5). Each result is ~27KB. Use pagination for more.'),
cursor: z.string().optional().describe('Pagination cursor from previous response'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type GetSavedSearchResultsInput = z.infer<typeof GetSavedSearchResultsInputSchema>;
/**
* Get saved search NET NEW results input schema
* @see GET /savedSearches:netNewResults/{id}
*
* PREREQUISITE: User must subscribe to the saved search via Harmonic UI first.
* Returns 404 if not subscribed.
*
* Response sizes are large (~27KB per result).
*/
export const GetSavedSearchNetNewResultsInputSchema = z.strictObject({
search_id: IdOrUrnSchema.describe('Saved search ID or URN'),
size: z.number().int().min(1).max(5).optional().describe('Number of results to return (default: 3, max: 5). Each result is ~27KB.'),
cursor: z.string().optional().describe('Pagination cursor from previous response'),
new_results_since: z.string().optional().describe('UTC datetime (e.g., 2024-09-21T00:00:00Z) to filter results. Defaults to subscription date.'),
response_format: ResponseFormatEnum.default('json').describe('Output format: "json" or "markdown"')
});
export type GetSavedSearchNetNewResultsInput = z.infer<typeof GetSavedSearchNetNewResultsInputSchema>;
/**
* Clear saved search net new results input schema
* @see POST /savedSearches:netNewResults/{id}:clear
*
* Marks all net new results as "seen" so they won't appear in subsequent net new queries.
*/
export const ClearSavedSearchNetNewInputSchema = z.strictObject({
search_id: IdOrUrnSchema.describe('Saved search ID or URN')
});
export type ClearSavedSearchNetNewInput = z.infer<typeof ClearSavedSearchNetNewInputSchema>;