#!/usr/bin/env node
import 'dotenv/config';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
// Zod schemas for input validation
import {
SearchCompaniesInputSchema,
SearchTypeaheadInputSchema,
FindSimilarCompaniesInputSchema,
LookupCompanyInputSchema,
GetCompanyInputSchema,
GetCompanyEmployeesInputSchema,
GetCompanyConnectionsInputSchema,
LookupPersonInputSchema,
GetPersonInputSchema,
ListSavedSearchesInputSchema,
GetSavedSearchResultsInputSchema,
GetSavedSearchNetNewResultsInputSchema,
ClearSavedSearchNetNewInputSchema
} from './schemas/inputs.js';
// Tool executors
import {
executeSearchCompanies,
executeSearchTypeahead,
executeFindSimilarCompanies
} from './tools/search.js';
import {
executeLookupCompany,
executeGetCompany,
executeGetCompanyEmployees,
executeGetCompanyConnections
} from './tools/companies.js';
import {
executeLookupPerson,
executeGetPerson
} from './tools/persons.js';
import {
executeListSavedSearches,
executeGetSavedSearchResults,
executeGetSavedSearchNetNewResults,
executeClearSavedSearchNetNew
} from './tools/saved-searches.js';
// Tool descriptions (kept separate for readability)
import {
SEARCH_COMPANIES_DESCRIPTION,
SEARCH_TYPEAHEAD_DESCRIPTION,
FIND_SIMILAR_COMPANIES_DESCRIPTION,
LOOKUP_COMPANY_DESCRIPTION,
GET_COMPANY_DESCRIPTION,
GET_COMPANY_EMPLOYEES_DESCRIPTION,
GET_COMPANY_CONNECTIONS_DESCRIPTION,
LOOKUP_PERSON_DESCRIPTION,
GET_PERSON_DESCRIPTION,
LIST_SAVED_SEARCHES_DESCRIPTION,
GET_SAVED_SEARCH_RESULTS_DESCRIPTION,
GET_SAVED_SEARCH_NET_NEW_RESULTS_DESCRIPTION,
CLEAR_SAVED_SEARCH_NET_NEW_DESCRIPTION
} from './tools/descriptions.js';
/**
* Helper to wrap executor string results into CallToolResult format.
* Detects errors by checking if the result starts with common error prefixes.
*/
function wrapResult(text: string): CallToolResult {
// Check if this is an error response
const isError = text.startsWith('Error:') ||
text.startsWith('Bad Request:') ||
text.startsWith('Authentication Failed:') ||
text.startsWith('Permission Denied:') ||
text.startsWith('Not Found:') ||
text.startsWith('Rate Limited:') ||
text.startsWith('Server Error:');
return {
content: [{ type: 'text', text }],
isError
};
}
/**
* Create and configure the MCP server with all tools
*/
function createServer(): McpServer {
const server = new McpServer({
name: 'harmonic-mcp-server',
version: '1.0.0'
});
// ============================================================================
// Search Tools
// ============================================================================
server.registerTool(
'harmonic_search_companies',
{
title: 'Search Companies (Natural Language)',
description: SEARCH_COMPANIES_DESCRIPTION,
inputSchema: SearchCompaniesInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeSearchCompanies(params);
return wrapResult(result);
}
);
server.registerTool(
'harmonic_search_typeahead',
{
title: 'Search Companies (Typeahead)',
description: SEARCH_TYPEAHEAD_DESCRIPTION,
inputSchema: SearchTypeaheadInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeSearchTypeahead(params);
return wrapResult(result);
}
);
server.registerTool(
'harmonic_find_similar_companies',
{
title: 'Find Similar Companies',
description: FIND_SIMILAR_COMPANIES_DESCRIPTION,
inputSchema: FindSimilarCompaniesInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeFindSimilarCompanies(params);
return wrapResult(result);
}
);
// ============================================================================
// Company Tools
// ============================================================================
server.registerTool(
'harmonic_lookup_company',
{
title: 'Lookup Company',
description: LOOKUP_COMPANY_DESCRIPTION,
inputSchema: LookupCompanyInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeLookupCompany(params);
return wrapResult(result);
}
);
server.registerTool(
'harmonic_get_company',
{
title: 'Get Company',
description: GET_COMPANY_DESCRIPTION,
inputSchema: GetCompanyInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeGetCompany(params);
return wrapResult(result);
}
);
server.registerTool(
'harmonic_get_company_employees',
{
title: 'Get Company Employees',
description: GET_COMPANY_EMPLOYEES_DESCRIPTION,
inputSchema: GetCompanyEmployeesInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeGetCompanyEmployees(params);
return wrapResult(result);
}
);
server.registerTool(
'harmonic_get_company_connections',
{
title: 'Get Team Connections',
description: GET_COMPANY_CONNECTIONS_DESCRIPTION,
inputSchema: GetCompanyConnectionsInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeGetCompanyConnections(params);
return wrapResult(result);
}
);
// ============================================================================
// Person Tools
// ============================================================================
server.registerTool(
'harmonic_lookup_person',
{
title: 'Lookup Person',
description: LOOKUP_PERSON_DESCRIPTION,
inputSchema: LookupPersonInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeLookupPerson(params);
return wrapResult(result);
}
);
server.registerTool(
'harmonic_get_person',
{
title: 'Get Person',
description: GET_PERSON_DESCRIPTION,
inputSchema: GetPersonInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeGetPerson(params);
return wrapResult(result);
}
);
// ============================================================================
// Saved Search Tools
// ============================================================================
server.registerTool(
'harmonic_list_saved_searches',
{
title: 'List Saved Searches',
description: LIST_SAVED_SEARCHES_DESCRIPTION,
inputSchema: ListSavedSearchesInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeListSavedSearches(params);
return wrapResult(result);
}
);
server.registerTool(
'harmonic_get_saved_search_results',
{
title: 'Get Saved Search Results',
description: GET_SAVED_SEARCH_RESULTS_DESCRIPTION,
inputSchema: GetSavedSearchResultsInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeGetSavedSearchResults(params);
return wrapResult(result);
}
);
// Net New Results - Get only new matches since last check
server.registerTool(
'harmonic_get_saved_search_net_new_results',
{
title: 'Get Saved Search Net New Results',
description: GET_SAVED_SEARCH_NET_NEW_RESULTS_DESCRIPTION,
inputSchema: GetSavedSearchNetNewResultsInputSchema,
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeGetSavedSearchNetNewResults(params);
return wrapResult(result);
}
);
// Clear Net New Results - Mark results as "seen"
server.registerTool(
'harmonic_clear_saved_search_net_new',
{
title: 'Clear Saved Search Net New Results',
description: CLEAR_SAVED_SEARCH_NET_NEW_DESCRIPTION,
inputSchema: ClearSavedSearchNetNewInputSchema,
annotations: {
readOnlyHint: false,
destructiveHint: false, // Not destructive, just marks as seen
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
const result = await executeClearSavedSearchNetNew(params);
return wrapResult(result);
}
);
return server;
}
/**
* Main entry point
*/
async function main(): Promise<void> {
// Validate API key is present
if (!process.env.HARMONIC_API_KEY) {
console.error('Error: HARMONIC_API_KEY environment variable is required');
console.error('Set it in your environment or .env file');
process.exit(1);
}
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
// Log to stderr so it doesn't interfere with stdio transport
console.error('Harmonic MCP server started');
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});