imo-publications-mcp-server
Version:
MCP server for IMO (International Maritime Organization) publications - Node.js TypeScript version
533 lines (526 loc) • 24.6 kB
JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { connectToMongoDB } from "./utils/mongodb.js";
import { searchIMOPublications, getIMOPublicationsList } from "./utils/typesense.js";
import { logger } from './utils/api.js';
import { CohereClientV2 } from 'cohere-ai';
let imoConfig;
function safeGetArgs(args, defaultValues) {
if (!args || typeof args !== 'object') {
return defaultValues;
}
const result = { ...defaultValues };
for (const key in defaultValues) {
if (args[key] !== undefined) {
result[key] = args[key];
}
}
return result;
}
function parseArgs() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
IMO Publications MCP Server v1.0.0
DESCRIPTION:
MCP server for International Maritime Organization (IMO) publications.
Provides access to IMO regulations, conventions, codes, and guidelines.
USAGE:
node index.js [OPTIONS]
OPTIONS:
--mongo-uri <uri> MongoDB connection URI
--db-name <name> MongoDB database name (default: imo_publications)
--typesense-host <host> Typesense server host
--typesense-port <port> Typesense server port
--typesense-protocol <protocol> Typesense protocol (http/https)
--typesense-api-key <key> Typesense API key
--non-interactive Run in non-interactive mode (for MCP)
--help, -h Show this help message
ENVIRONMENT VARIABLES:
MONGODB_URI MongoDB connection URI
MONGODB_DB_NAME MongoDB database name
TYPESENSE_HOST Typesense server host
TYPESENSE_PORT Typesense server port
TYPESENSE_PROTOCOL Typesense protocol
TYPESENSE_API_KEY Typesense API key
TOOLS:
• list_imo_publications - List all available IMO publications
• get_by_imo_publication_name - Find publications by name
• smart_imo_publication_search - Universal search with filters
• get_table_schema - Get database schema information
For more information, visit: https://github.com/your-org/imo-publications-mcp-server
`);
process.exit(0);
}
const config = {
mongoUri: '',
dbName: 'imo_publications'
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--mongo-uri' && i + 1 < args.length) {
config.mongoUri = args[++i];
}
else if (arg === '--db-name' && i + 1 < args.length) {
config.dbName = args[++i];
}
else if (arg === '--typesense-host' && i + 1 < args.length) {
config.typesenseHost = args[++i];
}
else if (arg === '--typesense-port' && i + 1 < args.length) {
config.typesensePort = args[++i];
}
else if (arg === '--typesense-protocol' && i + 1 < args.length) {
config.typesenseProtocol = args[++i];
}
else if (arg === '--typesense-api-key' && i + 1 < args.length) {
config.typesenseApiKey = args[++i];
}
}
config.mongoUri = config.mongoUri || process.env.MONGODB_URI || '';
config.dbName = config.dbName || process.env.MONGODB_DB_NAME || 'imo_publications';
config.typesenseHost = config.typesenseHost || process.env.TYPESENSE_HOST || '';
config.typesensePort = config.typesensePort || process.env.TYPESENSE_PORT || '';
config.typesenseProtocol = config.typesenseProtocol || process.env.TYPESENSE_PROTOCOL || '';
config.typesenseApiKey = config.typesenseApiKey || process.env.TYPESENSE_API_KEY || '';
if (!config.mongoUri) {
throw new Error('MongoDB URI is required. Use --mongo-uri argument or set MONGODB_URI environment variable.');
}
if (!config.typesenseHost) {
throw new Error('Typesense host is required. Use --typesense-host argument or set TYPESENSE_HOST environment variable.');
}
if (!config.typesensePort) {
throw new Error('Typesense port is required. Use --typesense-port argument or set TYPESENSE_PORT environment variable.');
}
if (!config.typesenseProtocol) {
throw new Error('Typesense protocol is required. Use --typesense-protocol argument or set TYPESENSE_PROTOCOL environment variable.');
}
if (!config.typesenseApiKey) {
throw new Error('Typesense API key is required. Use --typesense-api-key argument or set TYPESENSE_API_KEY environment variable.');
}
return config;
}
const server = new Server({
name: "imo-publications-mcp-server",
version: "1.0.0"
}, {
capabilities: {
resources: {
read: true,
list: true,
templates: true
},
tools: {
list: true,
call: true
},
prompts: {
list: true,
get: true
}
}
});
server.setRequestHandler(ListResourcesRequestSchema, async () => {
logger.log('Received list resources request');
return { resources: [] };
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
logger.log('Received read resource request: ' + JSON.stringify(request));
throw new Error("Resource reading not implemented");
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: [
{
name: "list_imo_publications",
description: "Provides a complete catalog of all available International Maritime Organization (IMO) publications in the system, including critical regulations like SOLAS, MARPOL, and STCW. Use this tool to see which official IMO regulatory documents, codes, and guidelines are available for reference.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false
}
},
{
name: "get_by_imo_publication_name",
description: "Locates specific IMO publications using any part of the document name or reference number. This tool is helpful when you know a portion of the publication's title or reference number and need to retrieve the full document.",
inputSchema: {
type: "object",
properties: {
document_name: {
type: "string",
description: "A text snippet containing part of the IMO publication name or number. Example: 'SOLAS', 'MARPOL', 'GMDSS'."
}
},
required: ["document_name"],
additionalProperties: false
}
},
{
name: "smart_imo_publication_search",
description: "Universal search tool for IMO publications. This is the primary tool for finding any information in the IMO publication database. It intelligently adapts search strategy based on query intent and can handle everything from specific lookups to general browsing.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Natural language search query. Leave empty for browsing mode. Examples: 'SOLAS fire safety', 'MARPOL Annex VI', 'GMDSS requirements', 'chapter II-2'"
},
search_type: {
type: "string",
description: "Search strategy. Fixed to 'semantic' for conceptual queries.",
enum: ["semantic"],
default: "semantic"
},
filters: {
type: "object",
description: "Filters to narrow search results. All filters are optional and use exact matching",
properties: {
document_name: {
type: "string",
description: "Exact or partial name of the IMO publication"
},
chapter: {
type: "string",
description: "Chapter name or number to search within"
},
section: {
type: "string",
description: "Section name to search within"
},
page_range: {
type: "array",
items: {
type: "number"
},
minItems: 2,
maxItems: 2,
description: "Page range to search within [start_page, end_page]"
}
}
},
max_results: {
type: "number",
description: "Maximum number of results to return",
default: 7,
minimum: 1,
maximum: 10
}
},
required: [],
additionalProperties: false
}
},
{
name: "get_table_schema",
description: "This tool retrieves Typesense schema and instructions on how to query a typesense table for a specific category.",
inputSchema: {
type: "object",
required: ["category"],
properties: {
category: {
type: "string",
description: "The category for which to retrieve the Typesense schema (e.g., imo_publication).",
enum: ["imo_publication"]
}
}
}
}
] };
});
function getListOfArtifacts(functionName, results) {
const artifacts = [];
for (let i = 0; i < results.length; i++) {
const result = results[i];
const url = result.url || result.documentLink;
const title = result.title || result.documentName;
if (url) {
const artifactData = {
id: `msg_browser_imo${i}`,
parentTaskId: "task_imo_publication_search_7d8f9g",
timestamp: Math.floor(Date.now() / 1000),
agent: {
id: "agent_imo_browser",
name: "IMO Publications",
type: "qna"
},
messageType: "action",
action: {
tool: "browser",
operation: "browsing",
params: {
url: title || url,
pageTitle: `Tool response for ${functionName}`,
visual: {
icon: "browser",
color: "#2D8CFF"
},
stream: {
type: "vnc",
streamId: "stream_browser_1",
target: "browser"
}
}
},
content: `Viewed page: ${functionName}`,
artifacts: [{
id: `artifact_webpage_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
type: "browser_view",
content: {
url: url,
title: functionName,
screenshot: "",
textContent: `Observed output of cmd \`${functionName}\` executed:`,
extractedInfo: {}
},
metadata: {
domainName: "imo.org",
visitTimestamp: Date.now(),
category: "web_page"
}
}],
status: "completed"
};
const artifact = {
type: "text",
text: JSON.stringify(artifactData, null, 2),
title: title || url,
format: "json"
};
artifacts.push(artifact);
}
}
return artifacts;
}
server.setRequestHandler(CallToolRequestSchema, async (request) => {
logger.log('Received call tool request: ' + JSON.stringify(request));
switch (request.params.name) {
case "list_imo_publications":
try {
const documentNames = await getIMOPublicationsList(imoConfig);
const content = {
type: "text",
text: JSON.stringify(documentNames, null, 2),
title: "List of IMO Publications",
format: "json"
};
return {
content: [content]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Error fetching list of IMO publications: ${errorMessage}`, error);
throw new Error(`Failed to fetch IMO publications list: ${errorMessage}`);
}
case "get_by_imo_publication_name":
try {
const args = request.params.arguments || {};
const documentName = args.document_name;
if (!documentName) {
throw new Error("document_name is required");
}
const searchResults = await searchIMOPublications({
query: documentName,
searchType: 'name',
include_fields: ['documentName', 'chapter', 'revDate', 'revNo', 'summary', 'documentLink', 'originalText']
}, imoConfig);
let finalResults = searchResults.results || [];
if (finalResults.length > 0 && finalResults.length <= 50 && process.env.COHERE_API_KEY) {
try {
const cohere = new CohereClientV2({
token: process.env.COHERE_API_KEY,
});
const docsWithOriginals = finalResults
.map((hit) => {
if (hit.originalText) {
return {
text: hit.originalText,
original: hit
};
}
return null;
})
.filter(Boolean);
if (docsWithOriginals.length > 0) {
const reranked = await cohere.rerank({
model: 'rerank-english-v3.0',
query: documentName,
documents: docsWithOriginals.map((doc) => doc.text),
topN: Math.min(10, docsWithOriginals.length),
returnDocuments: false
});
finalResults = reranked.results.map((result) => {
const original = docsWithOriginals[result.index].original;
return {
...original,
rerank_score: result.relevanceScore
};
});
}
}
catch (rerankError) {
logger.error('Cohere reranking failed, using original results', rerankError);
}
}
const linkData = finalResults.map((document) => ({
title: document.documentName,
url: document.documentLink
})).filter((item) => item.url);
const artifacts = getListOfArtifacts("get_by_imo_publication_name", linkData);
const content = {
type: "text",
text: JSON.stringify(finalResults, null, 2),
title: "IMO Publication Name Search Results",
format: "json"
};
return {
content: [content, ...artifacts]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Error searching IMO publications by name: ${errorMessage}`, error);
throw new Error(`Failed to search by publication name: ${errorMessage}`);
}
case "smart_imo_publication_search":
try {
logger.log('=== START smart_imo_publication_search ===');
const args = request.params.arguments || {};
const { query = '', search_type = 'semantic', filters = {}, max_results = 7, } = args;
logger.log(`Input arguments: ${JSON.stringify(args)}`);
logger.log(`Query: "${query}", Search type: ${search_type}, Max results: ${max_results}`);
logger.log(`Filters: ${JSON.stringify(filters)}`);
const searchParams = {
query: query || '*',
documentName: filters.document_name,
chapter: filters.chapter,
section: filters.section,
pageRange: filters.page_range,
maxResults: max_results,
searchType: (search_type === 'semantic' ? 'semantic' : 'exact'),
include_fields: ['documentHeader', 'documentName', 'chapter', 'section', 'revNo', 'originalText', 'documentLink']
};
logger.log(`Calling searchIMOPublications with params: ${JSON.stringify(searchParams)}`);
logger.log(`Config: ${JSON.stringify({ ...imoConfig, typesenseApiKey: '***REDACTED***' })}`);
const searchResults = await searchIMOPublications(searchParams, imoConfig);
logger.log(`Search completed. Type: ${searchResults.searchType}, Found: ${searchResults.found} results`);
const linkData = searchResults.results?.map((document) => ({
title: document.documentName,
url: document.documentLink
})).filter((item) => item.url) || [];
logger.log(`Generated ${linkData.length} artifacts from ${searchResults.results?.length || 0} results`);
const artifacts = getListOfArtifacts("smart_imo_publication_search", linkData);
const content = {
type: "text",
text: JSON.stringify({
searchType: searchResults.searchType,
found: searchResults.found,
results: searchResults.results
}, null, 2),
title: "Smart IMO Publication Search Results",
format: "json"
};
logger.log(`=== END smart_imo_publication_search === Returning ${searchResults.results?.length || 0} results`);
return {
content: [content, ...artifacts]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const errorStack = error instanceof Error ? error.stack : '';
logger.error(`=== ERROR in smart_imo_publication_search ===`);
logger.error(`Error in smart IMO publication search: ${errorMessage}`, error);
return {
content: [{
type: "text",
text: JSON.stringify({
error: true,
message: `Failed to perform smart search: ${errorMessage}`,
details: errorStack,
timestamp: new Date().toISOString()
}, null, 2),
title: "Smart Search Error",
format: "json"
}]
};
}
case "get_table_schema":
try {
const args = request.params.arguments || {};
const category = args.category;
if (category !== 'imo_publication') {
throw new Error("Only 'imo_publication' category is supported");
}
const schema = {
collection: 'imo_publication',
fields: [
{ name: 'documentName', type: 'string', facet: true },
{ name: 'content', type: 'string' },
{ name: 'documentLink', type: 'string', optional: true },
{ name: 'chapter', type: 'string', facet: true, optional: true },
{ name: 'section', type: 'string', facet: true, optional: true },
{ name: 'page', type: 'int32', facet: true, optional: true },
{ name: 'originalText', type: 'string', optional: true },
{ name: 'embedding', type: 'float[]', optional: true }
],
default_sorting_field: 'documentName'
};
const content = {
type: "text",
text: JSON.stringify(schema, null, 2),
title: "IMO Publication Typesense Schema",
format: "json"
};
return {
content: [content]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Error getting table schema: ${errorMessage}`, error);
throw new Error(`Failed to get table schema: ${errorMessage}`);
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
});
server.setRequestHandler(ListPromptsRequestSchema, async () => {
logger.log('Received list prompts request');
return { prompts: [] };
});
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
logger.log('Received get prompt request: ' + JSON.stringify(request));
throw new Error("Prompt handling not implemented");
});
async function main() {
try {
imoConfig = parseArgs();
logger.log('Starting server with configuration: ' + JSON.stringify({
...imoConfig,
typesenseApiKey: '***REDACTED***'
}));
await connectToMongoDB(imoConfig);
logger.log('Successfully connected to MongoDB');
const transport = new StdioServerTransport();
await server.connect(transport);
logger.log('IMO Publications MCP Server running on stdio');
}
catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
process.on('SIGINT', async () => {
logger.log('Received SIGINT, shutting down gracefully...');
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.log('Received SIGTERM, shutting down gracefully...');
process.exit(0);
});
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((error) => {
logger.error('Unhandled error in main:', error);
process.exit(1);
});
}