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.
@@ -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
+ }