mcp-learning-memory 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +394 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +424 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +8 -0
- package/dist/storage.d.ts +7 -0
- package/dist/storage.js +95 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.js +559 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +4 -0
- package/dist/validation.d.ts +31 -0
- package/dist/validation.js +153 -0
- package/package.json +47 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation and normalization
|
|
3
|
+
*/
|
|
4
|
+
export const LIMITS = {
|
|
5
|
+
TITLE_MAX: 200,
|
|
6
|
+
TAGS_MAX: 10,
|
|
7
|
+
TAG_LENGTH_MAX: 50,
|
|
8
|
+
CONTENT_MAX: 10000,
|
|
9
|
+
SOURCE_URL_MAX: 2000,
|
|
10
|
+
};
|
|
11
|
+
export function validateInsightInput(title, tags, content, sourceUrl) {
|
|
12
|
+
// Trim inputs for validation
|
|
13
|
+
const trimmedTitle = title.trim();
|
|
14
|
+
const trimmedContent = content.trim();
|
|
15
|
+
if (!trimmedTitle || trimmedTitle.length === 0) {
|
|
16
|
+
return 'Title is required';
|
|
17
|
+
}
|
|
18
|
+
if (trimmedTitle.length > LIMITS.TITLE_MAX) {
|
|
19
|
+
return `Title exceeds ${LIMITS.TITLE_MAX} characters`;
|
|
20
|
+
}
|
|
21
|
+
if (tags.length > LIMITS.TAGS_MAX) {
|
|
22
|
+
return `Maximum ${LIMITS.TAGS_MAX} tags allowed`;
|
|
23
|
+
}
|
|
24
|
+
// Validate tags after trimming
|
|
25
|
+
for (const tag of tags) {
|
|
26
|
+
const trimmedTag = tag.trim();
|
|
27
|
+
if (trimmedTag.length > LIMITS.TAG_LENGTH_MAX) {
|
|
28
|
+
return `Tag exceeds ${LIMITS.TAG_LENGTH_MAX} characters`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Normalize tags to check if we have at least one non-empty tag after normalization
|
|
32
|
+
const normalizedTags = normalizeTags(tags);
|
|
33
|
+
if (normalizedTags.length === 0) {
|
|
34
|
+
return 'At least one non-empty tag is required';
|
|
35
|
+
}
|
|
36
|
+
if (!trimmedContent || trimmedContent.length === 0) {
|
|
37
|
+
return 'Content is required';
|
|
38
|
+
}
|
|
39
|
+
if (trimmedContent.length > LIMITS.CONTENT_MAX) {
|
|
40
|
+
return `Content exceeds ${LIMITS.CONTENT_MAX} characters`;
|
|
41
|
+
}
|
|
42
|
+
// Validate sourceUrl if provided
|
|
43
|
+
if (sourceUrl !== undefined) {
|
|
44
|
+
const trimmedUrl = sourceUrl.trim();
|
|
45
|
+
if (trimmedUrl.length > LIMITS.SOURCE_URL_MAX) {
|
|
46
|
+
return `Source URL exceeds ${LIMITS.SOURCE_URL_MAX} characters`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
export function normalizeTags(tags) {
|
|
52
|
+
const normalized = tags
|
|
53
|
+
.map(tag => tag.trim().toLowerCase())
|
|
54
|
+
.filter(tag => tag.length > 0);
|
|
55
|
+
// Remove duplicates using Set
|
|
56
|
+
return Array.from(new Set(normalized));
|
|
57
|
+
}
|
|
58
|
+
export function generateId(nextNumber) {
|
|
59
|
+
const padded = nextNumber.toString().padStart(6, '0');
|
|
60
|
+
return `ace-${padded}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Calculate Levenshtein distance between two strings (case-insensitive)
|
|
64
|
+
* Used for detecting similar titles in auto-deduplication
|
|
65
|
+
*/
|
|
66
|
+
export function levenshteinDistance(str1, str2) {
|
|
67
|
+
const s1 = str1.toLowerCase();
|
|
68
|
+
const s2 = str2.toLowerCase();
|
|
69
|
+
const len1 = s1.length;
|
|
70
|
+
const len2 = s2.length;
|
|
71
|
+
// Create 2D array for dynamic programming
|
|
72
|
+
const matrix = Array(len1 + 1)
|
|
73
|
+
.fill(null)
|
|
74
|
+
.map(() => Array(len2 + 1).fill(0));
|
|
75
|
+
// Initialize first row and column
|
|
76
|
+
for (let i = 0; i <= len1; i++) {
|
|
77
|
+
matrix[i][0] = i;
|
|
78
|
+
}
|
|
79
|
+
for (let j = 0; j <= len2; j++) {
|
|
80
|
+
matrix[0][j] = j;
|
|
81
|
+
}
|
|
82
|
+
// Fill the matrix
|
|
83
|
+
for (let i = 1; i <= len1; i++) {
|
|
84
|
+
for (let j = 1; j <= len2; j++) {
|
|
85
|
+
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
|
86
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
|
|
87
|
+
matrix[i][j - 1] + 1, // insertion
|
|
88
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return matrix[len1][len2];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate parameters for list_index tool
|
|
96
|
+
*/
|
|
97
|
+
export function validateListIndexParams(params) {
|
|
98
|
+
if (params.tags !== undefined) {
|
|
99
|
+
if (!Array.isArray(params.tags)) {
|
|
100
|
+
return 'Tags must be an array';
|
|
101
|
+
}
|
|
102
|
+
if (params.tags.length > LIMITS.TAGS_MAX) {
|
|
103
|
+
return `Maximum ${LIMITS.TAGS_MAX} filter tags allowed`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (params.minHelpful !== undefined) {
|
|
107
|
+
if (typeof params.minHelpful !== 'number') {
|
|
108
|
+
return 'MinHelpful must be a number';
|
|
109
|
+
}
|
|
110
|
+
if (!Number.isFinite(params.minHelpful)) {
|
|
111
|
+
return 'MinHelpful must be a finite number';
|
|
112
|
+
}
|
|
113
|
+
if (!Number.isInteger(params.minHelpful)) {
|
|
114
|
+
return 'MinHelpful must be an integer';
|
|
115
|
+
}
|
|
116
|
+
if (params.minHelpful < 0) {
|
|
117
|
+
return 'MinHelpful must be >= 0';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (params.sortBy !== undefined) {
|
|
121
|
+
if (!['created', 'lastUsed', 'helpful'].includes(params.sortBy)) {
|
|
122
|
+
return 'SortBy must be one of: created, lastUsed, helpful';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (params.limit !== undefined) {
|
|
126
|
+
if (typeof params.limit !== 'number') {
|
|
127
|
+
return 'Limit must be a number';
|
|
128
|
+
}
|
|
129
|
+
if (!Number.isFinite(params.limit)) {
|
|
130
|
+
return 'Limit must be a finite number';
|
|
131
|
+
}
|
|
132
|
+
if (!Number.isInteger(params.limit)) {
|
|
133
|
+
return 'Limit must be an integer';
|
|
134
|
+
}
|
|
135
|
+
if (params.limit < 1) {
|
|
136
|
+
return 'Limit must be >= 1';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Validate search query for search_insights tool
|
|
143
|
+
*/
|
|
144
|
+
export function validateSearchQuery(query) {
|
|
145
|
+
const trimmed = query.trim();
|
|
146
|
+
if (trimmed.length === 0) {
|
|
147
|
+
return 'Query cannot be empty';
|
|
148
|
+
}
|
|
149
|
+
if (trimmed.length > LIMITS.TITLE_MAX) {
|
|
150
|
+
return `Query exceeds ${LIMITS.TITLE_MAX} characters`;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-learning-memory",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for AI agent memory that learns from feedback",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"memory",
|
|
16
|
+
"ai",
|
|
17
|
+
"agent",
|
|
18
|
+
"learning",
|
|
19
|
+
"claude",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"persistent-memory",
|
|
23
|
+
"feedback",
|
|
24
|
+
"llm",
|
|
25
|
+
"context"
|
|
26
|
+
],
|
|
27
|
+
"author": "nulone",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/nulone/mcp-learning-memory"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/nulone/mcp-learning-memory#readme",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"start": "node dist/index.js",
|
|
37
|
+
"test": "vitest run"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.0.0",
|
|
44
|
+
"typescript": "^5.0.0",
|
|
45
|
+
"vitest": "^1.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|