Skip to main content
Glama

Ocean.io MCP Server

by Meerkats-Ai
index.ts19.9 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { Tool, CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios, { AxiosInstance } from 'axios'; import dotenv from 'dotenv'; dotenv.config(); // Tool definitions const FIND_LOOKALIKE_COMPANIES_TOOL: Tool = { name: 'ocean_find_lookalike_companies', description: 'Find lookalike companies based on a seed company.', inputSchema: { type: 'object', properties: { company_name: { type: 'string', description: 'The name of the seed company', }, company_domain: { type: 'string', description: 'The domain of the seed company (alternative to company_name)', }, company_id: { type: 'string', description: 'The Ocean.io ID of the seed company (alternative to company_name and company_domain)', }, limit: { type: 'number', description: 'Maximum number of lookalike companies to return (optional, default: 10)', }, min_similarity: { type: 'number', description: 'Minimum similarity score (0-1) for lookalike companies (optional)', } }, required: [], }, }; const SEARCH_COMPANIES_TOOL: Tool = { name: 'ocean_search_companies', description: 'Search for companies with advanced filtering options.', inputSchema: { type: 'object', properties: { size: { type: 'number', description: 'Number of results to return per page', }, from: { type: 'number', description: 'Starting index for pagination', }, searchAfter: { type: 'string', description: 'Token for pagination (alternative to from)', }, companiesFilters: { type: 'object', description: 'Filters for company search', properties: { lookalikeDomains: { type: 'array', items: { type: 'string' }, description: 'List of domains to find similar companies to', }, minScore: { type: 'number', description: 'Minimum similarity score (0-1) for lookalike companies', }, includeDomains: { type: 'array', items: { type: 'string' }, description: 'List of domains to include in results', }, excludeDomains: { type: 'array', items: { type: 'string' }, description: 'List of domains to exclude from results', }, companySizes: { type: 'array', items: { type: 'string' }, description: 'List of company size ranges (e.g., "2-10", "51-200")', }, ecommerce: { type: 'boolean', description: 'Filter for companies with e-commerce capabilities', }, socialMedias: { type: 'object', properties: { medias: { type: 'array', items: { type: 'string' }, description: 'List of social media platforms', }, mode: { type: 'string', enum: ['anyOf', 'allOf'], description: 'Match mode for social media platforms', }, }, description: 'Filter for companies with specific social media presence', }, yearFounded: { type: 'object', properties: { from: { type: 'number' }, to: { type: 'number' }, }, description: 'Range of years when companies were founded', }, revenues: { type: 'array', items: { type: 'string' }, description: 'List of revenue ranges (e.g., "0-1M", "1-10M")', }, countries: { type: 'array', items: { type: 'string' }, description: 'List of country codes to filter by', }, industries: { type: 'object', properties: { industries: { type: 'array', items: { type: 'string' }, }, mode: { type: 'string', enum: ['anyOf', 'allOf'], }, }, description: 'Filter for companies in specific industries', }, technologies: { type: 'object', properties: { technologies: { type: 'array', items: { type: 'string' }, }, mode: { type: 'string', enum: ['anyOf', 'allOf'], }, }, description: 'Filter for companies using specific technologies', }, }, }, peopleFilters: { type: 'object', description: 'Filters for people associated with companies', properties: { includeIds: { type: 'array', items: { type: 'string' }, description: 'List of person IDs to include', }, excludeIds: { type: 'array', items: { type: 'string' }, description: 'List of person IDs to exclude', }, seniorities: { type: 'array', items: { type: 'string' }, description: 'List of seniority levels', }, jobTitles: { type: 'array', items: { type: 'string' }, description: 'List of job titles to include', }, excludeJobTitles: { type: 'array', items: { type: 'string' }, description: 'List of job titles to exclude', }, departments: { type: 'array', items: { type: 'string' }, description: 'List of departments', }, countries: { type: 'array', items: { type: 'string' }, description: 'List of country codes', }, states: { type: 'array', items: { type: 'object', properties: { abbreviation: { type: 'string' }, country: { type: 'string' }, }, }, description: 'List of states/provinces', }, }, }, }, required: [], }, }; // Type definitions interface FindLookalikeCompaniesParams { company_name?: string; company_domain?: string; company_id?: string; limit?: number; min_similarity?: number; } interface SearchCompaniesParams { size?: number; from?: number; searchAfter?: string; companiesFilters?: { lookalikeDomains?: string[]; minScore?: number; includeDomains?: string[]; excludeDomains?: string[]; companySizes?: string[]; ecommerce?: boolean; socialMedias?: { medias: string[]; mode: 'anyOf' | 'allOf'; }; yearFounded?: { from: number; to: number; }; revenues?: string[]; countries?: string[]; industries?: { industries: string[]; mode: 'anyOf' | 'allOf'; }; technologies?: { technologies: string[]; mode: 'anyOf' | 'allOf'; }; // Additional filter properties can be added as needed }; peopleFilters?: { includeIds?: string[]; excludeIds?: string[]; seniorities?: string[]; jobTitles?: string[]; excludeJobTitles?: string[]; departments?: string[]; countries?: string[]; states?: Array<{ abbreviation: string; country: string; }>; // Additional filter properties can be added as needed }; } // Type guards function isFindLookalikeCompaniesParams(args: unknown): args is FindLookalikeCompaniesParams { if ( typeof args !== 'object' || args === null ) { return false; } // At least one of company_name, company_domain, or company_id must be provided if ( !('company_name' in args && typeof (args as { company_name: unknown }).company_name === 'string') && !('company_domain' in args && typeof (args as { company_domain: unknown }).company_domain === 'string') && !('company_id' in args && typeof (args as { company_id: unknown }).company_id === 'string') ) { return false; } // Optional parameters if ( 'limit' in args && (args as { limit: unknown }).limit !== undefined && typeof (args as { limit: unknown }).limit !== 'number' ) { return false; } if ( 'min_similarity' in args && (args as { min_similarity: unknown }).min_similarity !== undefined && typeof (args as { min_similarity: unknown }).min_similarity !== 'number' ) { return false; } return true; } function isSearchCompaniesParams(args: unknown): args is SearchCompaniesParams { if (typeof args !== 'object' || args === null) { return false; } // All parameters are optional, but we should validate their types if present const obj = args as Record<string, unknown>; // Validate number types for (const prop of ['size', 'from']) { if (prop in obj && obj[prop] !== undefined && typeof obj[prop] !== 'number') { return false; } } // Validate string types if ('searchAfter' in obj && obj.searchAfter !== undefined && typeof obj.searchAfter !== 'string') { return false; } // Validate companiesFilters and peopleFilters objects if ('companiesFilters' in obj && obj.companiesFilters !== undefined) { if (typeof obj.companiesFilters !== 'object' || obj.companiesFilters === null) { return false; } } if ('peopleFilters' in obj && obj.peopleFilters !== undefined) { if (typeof obj.peopleFilters !== 'object' || obj.peopleFilters === null) { return false; } } return true; } // Server implementation const server = new Server( { name: 'ocean-io-mcp', version: '1.0.0', }, { capabilities: { tools: {}, logging: {}, }, } ); // Get API key from environment variables const OCEAN_API_KEY = process.env.OCEAN_API_KEY; const OCEAN_API_URL_V1 = 'https://api.ocean.io/v1'; const OCEAN_API_URL_V2 = 'https://api.ocean.io/v2'; // Check if API key is provided if (!OCEAN_API_KEY) { console.error('Error: OCEAN_API_KEY environment variable is required'); process.exit(1); } // Configuration for retries and monitoring const CONFIG = { retry: { maxAttempts: Number(process.env.OCEAN_RETRY_MAX_ATTEMPTS) || 3, initialDelay: Number(process.env.OCEAN_RETRY_INITIAL_DELAY) || 1000, maxDelay: Number(process.env.OCEAN_RETRY_MAX_DELAY) || 10000, backoffFactor: Number(process.env.OCEAN_RETRY_BACKOFF_FACTOR) || 2, }, }; // Initialize Axios instances for API requests const apiClientV1: AxiosInstance = axios.create({ baseURL: OCEAN_API_URL_V1, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${OCEAN_API_KEY}` } }); const apiClientV2: AxiosInstance = axios.create({ baseURL: OCEAN_API_URL_V2, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${OCEAN_API_KEY}` } }); let isStdioTransport = false; function safeLog( level: | 'error' | 'debug' | 'info' | 'notice' | 'warning' | 'critical' | 'alert' | 'emergency', data: any ): void { if (isStdioTransport) { // For stdio transport, log to stderr to avoid protocol interference console.error( `[${level}] ${typeof data === 'object' ? JSON.stringify(data) : data}` ); } else { // For other transport types, use the normal logging mechanism server.sendLoggingMessage({ level, data }); } } // Add utility function for delay function delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } // Add retry logic with exponential backoff async function withRetry<T>( operation: () => Promise<T>, context: string, attempt = 1 ): Promise<T> { try { return await operation(); } catch (error) { const isRateLimit = error instanceof Error && (error.message.includes('rate limit') || error.message.includes('429')); if (isRateLimit && attempt < CONFIG.retry.maxAttempts) { const delayMs = Math.min( CONFIG.retry.initialDelay * Math.pow(CONFIG.retry.backoffFactor, attempt - 1), CONFIG.retry.maxDelay ); safeLog( 'warning', `Rate limit hit for ${context}. Attempt ${attempt}/${CONFIG.retry.maxAttempts}. Retrying in ${delayMs}ms` ); await delay(delayMs); return withRetry(operation, context, attempt + 1); } throw error; } } // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ FIND_LOOKALIKE_COMPANIES_TOOL, SEARCH_COMPANIES_TOOL, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const startTime = Date.now(); try { const { name, arguments: args } = request.params; // Log incoming request with timestamp safeLog( 'info', `[${new Date().toISOString()}] Received request for tool: ${name}` ); if (!args) { throw new Error('No arguments provided'); } switch (name) { case 'ocean_find_lookalike_companies': { if (!isFindLookalikeCompaniesParams(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for ocean_find_lookalike_companies. You must provide at least one of: company_name, company_domain, or company_id.' ); } try { // First, if we don't have a company_id, we need to find it let companyId = args.company_id; if (!companyId) { // Search for the company by name or domain const searchParams: any = {}; if (args.company_name) { searchParams.name = args.company_name; } if (args.company_domain) { searchParams.domain = args.company_domain; } const searchResponse = await withRetry( async () => apiClientV1.get('/companies/search', { params: searchParams }), 'search company' ); if (!searchResponse.data.companies || searchResponse.data.companies.length === 0) { return { content: [ { type: 'text', text: `No companies found matching the provided criteria.`, }, ], isError: true, }; } // Use the first company found companyId = searchResponse.data.companies[0].id; } // Now find lookalike companies const lookalikeParams: any = { company_id: companyId, limit: args.limit || 10 }; if (args.min_similarity !== undefined) { lookalikeParams.min_similarity = args.min_similarity; } const response = await withRetry( async () => apiClientV1.get('/companies/lookalikes', { params: lookalikeParams }), 'find lookalike companies' ); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], isError: false, }; } catch (error) { const errorMessage = axios.isAxiosError(error) ? `API Error: ${error.response?.data?.message || error.message}` : `Error: ${error instanceof Error ? error.message : String(error)}`; return { content: [{ type: 'text', text: errorMessage }], isError: true, }; } } case 'ocean_search_companies': { if (!isSearchCompaniesParams(args)) { throw new McpError( ErrorCode.InvalidParams, 'Invalid arguments for ocean_search_companies.' ); } try { // Prepare the request payload const payload: any = {}; // Add pagination parameters if (args.size !== undefined) { payload.size = args.size; } if (args.from !== undefined) { payload.from = args.from; } if (args.searchAfter !== undefined) { payload.searchAfter = args.searchAfter; } // Add filters if (args.companiesFilters) { payload.companiesFilters = args.companiesFilters; } if (args.peopleFilters) { payload.peopleFilters = args.peopleFilters; } // Make the API request to the v2 endpoint const response = await withRetry( async () => apiClientV2.post('/search/companies', payload, { params: { apiToken: OCEAN_API_KEY } }), 'search companies' ); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], isError: false, }; } catch (error) { const errorMessage = axios.isAxiosError(error) ? `API Error: ${error.response?.data?.message || error.message}` : `Error: ${error instanceof Error ? error.message : String(error)}`; return { content: [{ type: 'text', text: errorMessage }], isError: true, }; } } default: return { content: [ { type: 'text', text: `Unknown tool: ${name}` }, ], isError: true, }; } } catch (error) { // Log detailed error information safeLog('error', { message: `Request failed: ${ error instanceof Error ? error.message : String(error) }`, tool: request.params.name, arguments: request.params.arguments, timestamp: new Date().toISOString(), duration: Date.now() - startTime, }); return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } finally { // Log request completion with performance metrics safeLog('info', `Request completed in ${Date.now() - startTime}ms`); } }); // Server startup async function runServer() { try { console.error('Initializing Ocean.io MCP Server...'); const transport = new StdioServerTransport(); // Detect if we're using stdio transport isStdioTransport = transport instanceof StdioServerTransport; if (isStdioTransport) { console.error( 'Running in stdio mode, logging will be directed to stderr' ); } await server.connect(transport); // Now that we're connected, we can send logging messages safeLog('info', 'Ocean.io MCP Server initialized successfully'); safeLog( 'info', `Configuration: API URLs: V1: ${OCEAN_API_URL_V1}, V2: ${OCEAN_API_URL_V2}` ); console.error('Ocean.io MCP Server running on stdio'); } catch (error) { console.error('Fatal error running server:', error); process.exit(1); } } runServer().catch((error: any) => { console.error('Fatal error running server:', error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Meerkats-Ai/ocean-io-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server