MCP Memory LibSQL
by joleyline
- src
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { ServerTransport } from './transports/transport.js';
import { SseServerTransport } from './transports/sse-transport.js';
import { TransportAdapter } from './transports/transport-adapter.js';
import {
CallToolRequestSchema,
ErrorCode,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { serverConfig } from './config/index.js';
import { logger } from './utils/logger.js';
import { toMcpError } from './utils/errors.js';
import { databaseService } from './services/database-service.js';
import { embeddingService } from './services/embedding-service.js';
import { createEntities, deleteEntity } from './services/entity-service.js';
import { createRelations, deleteRelation } from './services/relation-service.js';
import { readGraph, searchNodes } from './services/graph-service.js';
/**
* Main MCP server class for memory operations
*/
class MemoryServer {
private server: Server;
constructor() {
// Initialize the MCP server
this.server = new Server(
{
name: serverConfig.name,
version: serverConfig.version,
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// Set up request handlers
this.setupToolHandlers();
// Set up error handling
this.server.onerror = (error) => {
logger.error('MCP Server Error:', error);
};
// Set up process signal handling
process.on('SIGINT', async () => {
logger.info('Received SIGINT signal, shutting down...');
await this.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('Received SIGTERM signal, shutting down...');
await this.close();
process.exit(0);
});
}
/**
* Set up tool handlers for the MCP server
*/
private setupToolHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'create_entities',
description: 'Create new entities with observations and optional embeddings',
inputSchema: {
type: 'object',
properties: {
entities: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
entityType: {
type: 'string',
},
observations: {
type: 'array',
items: {
type: 'string',
},
},
embedding: {
type: 'array',
items: {
type: 'number',
},
description: 'Optional vector embedding for similarity search',
},
relations: {
type: 'array',
items: {
type: 'object',
properties: {
target: {
type: 'string',
},
relationType: {
type: 'string',
},
},
required: [
'target',
'relationType',
],
},
description: 'Optional relations to create with this entity',
},
},
required: [
'name',
'entityType',
'observations',
],
},
},
},
required: [
'entities',
],
},
},
{
name: 'search_nodes',
description: 'Search for entities and their relations using text or vector similarity',
inputSchema: {
type: 'object',
properties: {
query: {
oneOf: [
{
type: 'string',
description: 'Text search query',
},
{
type: 'array',
items: {
type: 'number',
},
description: 'Vector for similarity search',
},
],
},
includeEmbeddings: {
type: 'boolean',
description: 'Whether to include embeddings in the returned entities (default: false)',
},
},
required: [
'query',
],
},
},
{
name: 'read_graph',
description: 'Get recent entities and their relations',
inputSchema: {
type: 'object',
properties: {
includeEmbeddings: {
type: 'boolean',
description: 'Whether to include embeddings in the returned entities (default: false)',
},
},
required: [],
},
},
{
name: 'create_relations',
description: 'Create relations between entities',
inputSchema: {
type: 'object',
properties: {
relations: {
type: 'array',
items: {
type: 'object',
properties: {
source: {
type: 'string',
},
target: {
type: 'string',
},
type: {
type: 'string',
},
},
required: [
'source',
'target',
'type',
],
},
},
},
required: [
'relations',
],
},
},
{
name: 'delete_entity',
description: 'Delete an entity and all its associated data (observations and relations)',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the entity to delete',
},
},
required: [
'name',
],
},
},
{
name: 'delete_relation',
description: 'Delete a specific relation between entities',
inputSchema: {
type: 'object',
properties: {
source: {
type: 'string',
description: 'Source entity name',
},
target: {
type: 'string',
description: 'Target entity name',
},
type: {
type: 'string',
description: 'Type of relation',
},
},
required: [
'source',
'target',
'type',
],
},
},
],
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args = {} } = request.params;
logger.info(`Tool call: ${name}`, args);
// Define interfaces for tool arguments
interface EntityInput {
name: string;
entityType: string;
observations: string[];
embedding?: number[];
relations?: Array<{
target: string;
relationType: string;
}>;
}
interface SearchNodesInput {
query: string | number[];
includeEmbeddings?: boolean;
}
interface ReadGraphInput {
includeEmbeddings?: boolean;
}
interface RelationInput {
source: string;
target: string;
type: string;
}
interface DeleteEntityInput {
name: string;
}
interface DeleteRelationInput {
source: string;
target: string;
type: string;
}
switch (name) {
case 'create_entities': {
// Define the expected type for entities
interface EntityInput {
name: string;
entityType: string;
observations: string[];
embedding?: number[];
relations?: Array<{
target: string;
relationType: string;
}>;
}
// Type assertion with proper interface
const entities = args.entities as EntityInput[];
await createEntities(entities);
return {
content: [
{
type: 'text',
text: `Successfully processed ${entities.length} entities (created new or updated existing)`,
},
],
};
}
case 'search_nodes': {
// Safely access properties with type assertions for each property
if (!args.query) {
throw new McpError(
ErrorCode.InvalidParams,
'Missing required parameter: query'
);
}
const query = args.query as string | number[];
const includeEmbeddings = args.includeEmbeddings as boolean || false;
const result = await searchNodes(query, includeEmbeddings);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'read_graph': {
// Safely access properties with type assertions
const includeEmbeddings = args.includeEmbeddings as boolean || false;
// Use a fixed limit of 10 for the number of entities to return
const result = await readGraph(10, includeEmbeddings);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
}
case 'create_relations': {
// Define the expected type for relations
interface RelationInput {
source: string;
target: string;
type: string;
}
// Type assertion with proper interface
const relationInputs = args.relations as RelationInput[];
// Map to the format expected by createRelations
const relations = relationInputs.map(rel => ({
from: rel.source,
to: rel.target,
relationType: rel.type,
}));
await createRelations(relations);
return {
content: [
{
type: 'text',
text: `Successfully created ${relations.length} relations`,
},
],
};
}
case 'delete_entity': {
// Safely access properties with type assertions
if (!args.name) {
throw new McpError(
ErrorCode.InvalidParams,
'Missing required parameter: name'
);
}
const name = args.name as string;
await deleteEntity(name);
return {
content: [
{
type: 'text',
text: `Successfully deleted entity: ${name}`,
},
],
};
}
case 'delete_relation': {
// Safely access properties with type assertions
if (!args.source || !args.target || !args.type) {
throw new McpError(
ErrorCode.InvalidParams,
'Missing required parameters: source, target, or type'
);
}
const source = args.source as string;
const target = args.target as string;
const type = args.type as string;
await deleteRelation(source, target, type);
return {
content: [
{
type: 'text',
text: `Successfully deleted relation: ${source} -> ${target} (${type})`,
},
],
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
logger.error('Error handling tool call:', error);
const mcpError = toMcpError(error);
return {
content: [
{
type: 'text',
text: mcpError.message,
},
],
isError: true,
};
}
});
// We don't use resources, but we need to handle these requests
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [],
}));
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
resourceTemplates: [],
}));
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
throw new McpError(
ErrorCode.MethodNotFound,
`Resource not found: ${request.params.uri}`
);
});
}
/**
* Initialize the server and database
*/
public async initialize(): Promise<void> {
try {
logger.info('Initializing database...');
await databaseService.initialize();
logger.info('Database initialized successfully');
} catch (error) {
logger.error('Failed to initialize database:', error);
throw error;
}
}
/**
* Connect to the MCP transport
* @param transport - The MCP transport to connect to
*/
public async connect(transport: ServerTransport): Promise<void> {
try {
logger.info('Connecting to MCP transport...');
// Use type assertion to convert ServerTransport to any
// This is a workaround for the type incompatibility issue
await this.server.connect(transport as any);
logger.info('Connected to MCP transport successfully');
} catch (error) {
logger.error('Failed to connect to MCP transport:', error);
throw error;
}
}
/**
* Close the server and database connection
*/
public async close(): Promise<void> {
try {
logger.info('Closing server...');
await this.server.close();
logger.info('Server closed successfully');
logger.info('Closing database connection...');
await databaseService.close();
logger.info('Database connection closed successfully');
} catch (error) {
logger.error('Error during shutdown:', error);
}
}
/**
* Run the server
*/
public async run(): Promise<void> {
try {
// Initialize the server
await this.initialize();
// Connect to the MCP transport based on configuration
if (serverConfig.transport === 'sse') {
logger.info('Using SSE transport');
// Use the SseServerTransport directly with a type assertion
// This is a workaround for the type incompatibility issue
const transport = new SseServerTransport();
// Start the SSE transport server before connecting
// This is necessary because the HTTP server needs to be running
// before we can connect to it
logger.info('Starting SSE transport server...');
await transport.start();
await this.connect(transport);
logger.info(`${serverConfig.name} v${serverConfig.version} running on SSE transport`);
} else {
// Default to stdio transport
logger.info('Using stdio transport');
const transport = new StdioServerTransport();
// Use the server.connect method directly for StdioServerTransport
// since it's already compatible with the MCP SDK's Transport interface
await this.server.connect(transport);
logger.info(`${serverConfig.name} v${serverConfig.version} running on stdio`);
}
} catch (error) {
logger.error('Failed to start server:', error);
process.exit(1);
}
}
}
// Create and run the server
const server = new MemoryServer();
server.run().catch((error) => {
logger.error('Unhandled error:', error);
process.exit(1);
});