chore: sync lockfile and payload-ai extensions for release v1.9.10
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m20s
Monorepo Pipeline / 🧹 Lint (push) Successful in 4m27s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m35s
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Failing after 17s
Monorepo Pipeline / 🐳 Build Build-Base (push) Failing after 17s
Monorepo Pipeline / 🐳 Build Production Runtime (push) Failing after 17s
Monorepo Pipeline / 🚀 Release (push) Successful in 1m33s

This commit is contained in:
2026-03-03 12:40:41 +01:00
parent 24fde20030
commit 79d221de5e
22 changed files with 838 additions and 325 deletions

View File

@@ -0,0 +1,78 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { QdrantMemoryService } from './qdrant.js';
async function main() {
const server = new McpServer({
name: '@mintel/memory-mcp',
version: '1.0.0',
});
const qdrantService = new QdrantMemoryService(process.env.QDRANT_URL || 'http://localhost:6333');
// Initialize embedding model and Qdrant connection
try {
await qdrantService.initialize();
} catch (e) {
console.error('Failed to initialize local dependencies. Exiting.');
process.exit(1);
}
server.tool(
'store_memory',
'Store a new piece of knowledge/memory into the vector database. Use this to remember architectural decisions, preferences, aliases, etc.',
{
label: z.string().describe('A short, descriptive label or title for the memory (e.g., "Architektur-Entscheidungen")'),
content: z.string().describe('The actual content to remember (e.g., "In diesem Projekt nutzen wir lieber Composition over Inheritance.")'),
},
async (args) => {
const success = await qdrantService.storeMemory(args.label, args.content);
if (success) {
return {
content: [{ type: 'text', text: `Successfully stored memory: [${args.label}]` }],
};
} else {
return {
content: [{ type: 'text', text: `Failed to store memory: [${args.label}]` }],
isError: true,
};
}
}
);
server.tool(
'retrieve_memory',
'Retrieve relevant memories from the vector database based on a semantic search query.',
{
query: z.string().describe('The search query to find relevant memories.'),
limit: z.number().optional().describe('Maximum number of results to return (default: 5)'),
},
async (args) => {
const results = await qdrantService.retrieveMemory(args.query, args.limit || 5);
if (results.length === 0) {
return {
content: [{ type: 'text', text: 'No relevant memories found.' }],
};
}
const formattedResults = results
.map(r => `- [${r.label}] (Score: ${r.score.toFixed(3)}): ${r.content}`)
.join('\n');
return {
content: [{ type: 'text', text: `Found ${results.length} memories:\n\n${formattedResults}` }],
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Memory MCP server is running and ready to accept connections over stdio.');
}
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});

View File

@@ -0,0 +1,89 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { QdrantMemoryService } from './qdrant.js';
vi.mock('@xenova/transformers', () => {
return {
env: { allowRemoteModels: false, localModelPath: './models' },
pipeline: vi.fn().mockResolvedValue(async (text: string) => {
// Mock embedding generation: returns an array of 384 numbers
return { data: new Float32Array(384).fill(0.1) };
}),
};
});
const mockCreateCollection = vi.fn();
const mockGetCollections = vi.fn().mockResolvedValue({ collections: [] });
const mockUpsert = vi.fn();
const mockSearch = vi.fn().mockResolvedValue([
{
id: 'test-id',
version: 1,
score: 0.9,
payload: { label: 'Test Label', content: 'Test Content' }
}
]);
vi.mock('@qdrant/js-client-rest', () => {
return {
QdrantClient: vi.fn().mockImplementation(() => {
return {
getCollections: mockGetCollections,
createCollection: mockCreateCollection,
upsert: mockUpsert,
search: mockSearch
};
})
};
});
describe('QdrantMemoryService', () => {
let service: QdrantMemoryService;
beforeEach(() => {
vi.clearAllMocks();
service = new QdrantMemoryService('http://localhost:6333');
});
it('should initialize and create collection if missing', async () => {
mockGetCollections.mockResolvedValueOnce({ collections: [] });
await service.initialize();
expect(mockGetCollections).toHaveBeenCalled();
expect(mockCreateCollection).toHaveBeenCalledWith('mcp_memory', expect.any(Object));
});
it('should not create collection if it already exists', async () => {
mockGetCollections.mockResolvedValueOnce({ collections: [{ name: 'mcp_memory' }] });
await service.initialize();
expect(mockCreateCollection).not.toHaveBeenCalled();
});
it('should store memory', async () => {
await service.initialize();
const result = await service.storeMemory('Design', 'Composition over Inheritance');
expect(result).toBe(true);
expect(mockUpsert).toHaveBeenCalledWith('mcp_memory', expect.objectContaining({
wait: true,
points: expect.arrayContaining([
expect.objectContaining({
payload: expect.objectContaining({
label: 'Design',
content: 'Composition over Inheritance'
})
})
])
}));
});
it('should retrieve memory', async () => {
await service.initialize();
const results = await service.retrieveMemory('Design');
expect(results).toHaveLength(1);
expect(results[0].label).toBe('Test Label');
expect(results[0].content).toBe('Test Content');
expect(results[0].score).toBe(0.9);
});
});

View File

@@ -0,0 +1,110 @@
import { pipeline, env } from '@xenova/transformers';
import { QdrantClient } from '@qdrant/js-client-rest';
// Be sure to set local caching options for transformers
env.allowRemoteModels = true;
env.localModelPath = './models';
export class QdrantMemoryService {
private client: QdrantClient;
private collectionName = 'mcp_memory';
private embedder: any = null;
constructor(url: string = 'http://localhost:6333') {
this.client = new QdrantClient({ url });
}
/**
* Initializes the embedding model and the Qdrant collection
*/
async initialize() {
// 1. Load the embedding model (using a lightweight model suitable for semantic search)
console.error('Loading embedding model...');
this.embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
// 2. Ensure collection exists
console.error(`Checking for collection: ${this.collectionName}`);
try {
const collections = await this.client.getCollections();
const exists = collections.collections.some(c => c.name === this.collectionName);
if (!exists) {
console.error(`Creating collection: ${this.collectionName}`);
await this.client.createCollection(this.collectionName, {
vectors: {
size: 384, // size for all-MiniLM-L6-v2
distance: 'Cosine'
}
});
console.error('Collection created successfully.');
}
} catch (e) {
console.error('Failed to initialize Qdrant collection:', e);
throw e;
}
}
/**
* Generates a vector embedding for the given text
*/
private async getEmbedding(text: string): Promise<number[]> {
if (!this.embedder) {
throw new Error('Embedder not initialized. Call initialize() first.');
}
const output = await this.embedder(text, { pooling: 'mean', normalize: true });
return Array.from(output.data);
}
/**
* Stores a memory entry into Qdrant
*/
async storeMemory(label: string, content: string): Promise<boolean> {
try {
const fullText = `${label}: ${content}`;
const vector = await this.getEmbedding(fullText);
const id = crypto.randomUUID();
await this.client.upsert(this.collectionName, {
wait: true,
points: [
{
id,
vector,
payload: {
label,
content,
timestamp: new Date().toISOString()
}
}
]
});
return true;
} catch (e) {
console.error('Failed to store memory:', e);
return false;
}
}
/**
* Retrieves memory entries relevant to the query
*/
async retrieveMemory(query: string, limit: number = 5): Promise<Array<{ label: string, content: string, score: number }>> {
try {
const vector = await this.getEmbedding(query);
const searchResults = await this.client.search(this.collectionName, {
vector,
limit,
with_payload: true
});
return searchResults.map(result => ({
label: String(result.payload?.label || ''),
content: String(result.payload?.content || ''),
score: result.score
}));
} catch (e) {
console.error('Failed to retrieve memory:', e);
return [];
}
}
}