mcp-chat-adapter
Version:
MCP server for OpenAI chat completion
219 lines • 7.57 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import { CONVERSATION_DIR } from './constants.js';
import { ConversationStorageError } from './types.js';
/**
* Sleep for a specified number of milliseconds
*/
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
* Validate create conversation arguments
*/
export const isValidCreateConversationArgs = (args) => {
if (typeof args !== 'object' || args === null) {
return false;
}
const candidate = args;
// Check optional string parameters
if (candidate.model !== undefined && typeof candidate.model !== 'string') {
return false;
}
if (candidate.system_prompt !== undefined && typeof candidate.system_prompt !== 'string') {
return false;
}
// Check optional parameters object
if (candidate.parameters !== undefined) {
if (typeof candidate.parameters !== 'object' || candidate.parameters === null) {
return false;
}
const optionalNumericParams = [
'max_tokens',
'temperature',
'top_p',
'frequency_penalty',
'presence_penalty',
];
for (const param of optionalNumericParams) {
if (candidate.parameters[param] !== undefined &&
typeof candidate.parameters[param] !== 'number') {
return false;
}
}
}
// Check optional metadata object
if (candidate.metadata !== undefined) {
if (typeof candidate.metadata !== 'object' || candidate.metadata === null) {
return false;
}
// Check title if present
if (candidate.metadata.title !== undefined && typeof candidate.metadata.title !== 'string') {
return false;
}
// Check tags if present
if (candidate.metadata.tags !== undefined) {
if (!Array.isArray(candidate.metadata.tags)) {
return false;
}
// Ensure all tags are strings
if (candidate.metadata.tags.some(tag => typeof tag !== 'string')) {
return false;
}
}
}
return true;
};
/**
* Validate chat arguments
*/
export const isValidChatArgs = (args) => {
if (typeof args !== 'object' || args === null) {
return false;
}
const candidate = args;
// conversation_id and message are required
if (typeof candidate.conversation_id !== 'string') {
return false;
}
if (typeof candidate.message !== 'string') {
return false;
}
// Check optional parameters object
if (candidate.parameters !== undefined) {
if (typeof candidate.parameters !== 'object' || candidate.parameters === null) {
return false;
}
const optionalNumericParams = [
'max_tokens',
'temperature',
'top_p',
'frequency_penalty',
'presence_penalty',
];
for (const param of optionalNumericParams) {
if (candidate.parameters[param] !== undefined &&
typeof candidate.parameters[param] !== 'number') {
return false;
}
}
}
return true;
};
/**
* Ensure the conversations directory exists
*/
export const ensureConversationDir = async () => {
try {
await fs.mkdir(CONVERSATION_DIR, { recursive: true });
}
catch (error) {
throw new ConversationStorageError(`Failed to create conversations directory: ${error.message}`);
}
};
/**
* Get conversation filename from ID
*/
export const getConversationFilePath = (conversationId) => {
return path.join(CONVERSATION_DIR, `${conversationId}.json`);
};
/**
* Read conversation from disk
*/
export const readConversation = async (conversationId) => {
try {
const filePath = getConversationFilePath(conversationId);
const data = await fs.readFile(filePath, 'utf-8');
return JSON.parse(data);
}
catch (error) {
throw new ConversationStorageError(`Failed to read conversation: ${error.message}`);
}
};
/**
* Write conversation to disk
*/
export const writeConversation = async (conversation) => {
try {
await ensureConversationDir();
const filePath = getConversationFilePath(conversation.id);
const data = JSON.stringify(conversation, null, 2);
await fs.writeFile(filePath, data, 'utf-8');
}
catch (error) {
throw new ConversationStorageError(`Failed to write conversation: ${error.message}`);
}
};
/**
* List all conversations
*/
export const listConversations = async (log) => {
try {
await ensureConversationDir();
const files = await fs.readdir(CONVERSATION_DIR);
const jsonFiles = files.filter(file => file.endsWith('.json'));
const metadataList = [];
for (const file of jsonFiles) {
try {
const conversationId = path.basename(file, '.json');
const conversationData = await readConversation(conversationId);
metadataList.push({
id: conversationData.id,
model: conversationData.model,
created_at: conversationData.created_at,
updated_at: conversationData.updated_at,
message_count: conversationData.messages.length,
metadata: conversationData.metadata
});
}
catch (error) {
log.warn('Storage', `Error reading conversation file ${file}: ${error.message}`);
// Skip invalid files
}
}
// Sort by updated_at timestamp, newest first
return metadataList.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
}
catch (error) {
throw new ConversationStorageError(`Failed to list conversations: ${error.message}`);
}
};
/**
* Delete a conversation
*/
export const deleteConversation = async (conversationId) => {
try {
const filePath = getConversationFilePath(conversationId);
await fs.unlink(filePath);
}
catch (error) {
throw new ConversationStorageError(`Failed to delete conversation: ${error.message}`);
}
};
/**
* Get the next available conversation ID by finding the highest existing ID and incrementing it
*
* Using sequential IDs (1, 2, 3, ...) instead of UUIDs makes for a better user experience,
* as they are easier to remember, type, and reference. This is especially helpful when users
* need to manually specify conversation IDs in the chat tool.
*/
export const getNextConversationId = async () => {
try {
await ensureConversationDir();
const files = await fs.readdir(CONVERSATION_DIR);
const jsonFiles = files.filter(file => file.endsWith('.json'));
let highestId = 0;
for (const file of jsonFiles) {
const fileBaseName = path.basename(file, '.json');
const idNumber = parseInt(fileBaseName, 10);
// Check if the filename is a valid integer
if (!isNaN(idNumber) && String(idNumber) === fileBaseName) {
highestId = Math.max(highestId, idNumber);
}
}
// Return the next ID as a string
return String(highestId + 1);
}
catch (error) {
throw new ConversationStorageError(`Failed to get next conversation ID: ${error.message}`);
}
};
//# sourceMappingURL=utils.js.map