mcp-memory-bucket 0.2.2 → 0.3.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 +47 -13
- package/dist/client/assets/{index-BxAZ48QK.js → index-DIO48C0V.js} +212 -113
- package/dist/client/index.html +1 -1
- package/dist/src/memory/repository.js +117 -0
- package/dist/src/memory/tools.js +52 -0
- package/dist/src/server.js +3 -1
- package/dist/src/shared/relocate-tool.js +32 -18
- package/dist/src/shared/relocate.js +9 -0
- package/dist/src/shared/search-tool.js +18 -0
- package/dist/src/skills/builtin/memory-bucket-authoring/SKILL.md +423 -7
- package/dist/src/skills/repository.js +129 -0
- package/dist/src/skills/tools.js +71 -0
- package/dist/src/store/db.js +21 -8
- package/dist/src/store/search.js +60 -0
- package/dist/src/store/sync.js +6 -2
- package/dist/src/web/routes.js +98 -4
- package/dist/src/web/ui-tool.js +1 -1
- package/package.json +1 -1
package/dist/src/skills/tools.js
CHANGED
|
@@ -12,6 +12,35 @@ export function registerSkillTools(mcp, repo) {
|
|
|
12
12
|
const items = repo.list(query, root);
|
|
13
13
|
return { content: [{ type: 'text', text: JSON.stringify(items, null, 2) }] };
|
|
14
14
|
});
|
|
15
|
+
mcp.tool('skill_search', 'Full-text search over skill description/body/tags (grep/find-like, ranked by relevance) — unlike skill_list\'s substring metadata filter, this searches the full markdown body. `query` is raw SQLite FTS5 MATCH syntax: bare words, "exact phrases", prefix* wildcards, AND/OR/NOT boolean operators; hyphenated/punctuated terms must be quoted, e.g. "blue-green". Can be combined with status/owner/tag filters. Returns ranked hits with a highlighted snippet, not the full body — call skill_get on a hit\'s name for that.', {
|
|
16
|
+
query: z.string().describe('FTS5 match expression, e.g. `deploy AND rollback` or `"blue green"`'),
|
|
17
|
+
status: z.enum(SKILL_STATUS).optional(),
|
|
18
|
+
owner: z.string().optional(),
|
|
19
|
+
tag: z.string().optional(),
|
|
20
|
+
limit: z.number().int().positive().max(100).optional(),
|
|
21
|
+
offset: z.number().int().nonnegative().optional(),
|
|
22
|
+
...(multiRoot ? { root: z.string().optional().describe(`filter to one root: ${rootNames}`) } : {}),
|
|
23
|
+
}, async ({ query, status, owner, tag, limit, offset, root }) => {
|
|
24
|
+
try {
|
|
25
|
+
const hits = repo.search(query, { root, status, owner, tag, limit, offset });
|
|
26
|
+
return { content: [{ type: 'text', text: JSON.stringify(hits, null, 2) }] };
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
mcp.tool('skill_bulk_update', 'Applies the same frontmatter change to many skills at once by name — e.g. add/remove a tag across a batch found via skill_search, or flip status for a group. add_tags/remove_tags merge or subtract per-skill; owner/status/extends overwrite uniformly when provided. Body is never touched. Returns per-name success/failure so one bad name doesn\'t abort the batch.', {
|
|
33
|
+
names: z.array(z.string()).min(1),
|
|
34
|
+
add_tags: z.array(z.string()).optional(),
|
|
35
|
+
remove_tags: z.array(z.string()).optional(),
|
|
36
|
+
owner: z.string().nullable().optional(),
|
|
37
|
+
status: z.enum(SKILL_STATUS).optional(),
|
|
38
|
+
extends: z.string().nullable().optional(),
|
|
39
|
+
deprecated: z.boolean().optional().describe('marks skills as deprecated (or un-deprecates when false) — independent of status'),
|
|
40
|
+
}, async ({ names, add_tags, remove_tags, owner, status, extends: extendsId, deprecated }) => {
|
|
41
|
+
const results = repo.bulkUpdate(names, { add_tags, remove_tags, owner, status, extends: extendsId, deprecated });
|
|
42
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
43
|
+
});
|
|
15
44
|
mcp.tool('skill_get', 'Fetches a single skill by name, including its full markdown body.', { name: z.string() }, async ({ name }) => {
|
|
16
45
|
const doc = repo.get(name);
|
|
17
46
|
if (!doc) {
|
|
@@ -19,6 +48,10 @@ export function registerSkillTools(mcp, repo) {
|
|
|
19
48
|
}
|
|
20
49
|
return { content: [{ type: 'text', text: JSON.stringify(doc, null, 2) }] };
|
|
21
50
|
});
|
|
51
|
+
mcp.tool('skill_bulk_get', 'Fetches many skills by name in one call, including full markdown bodies — e.g. hydrating a batch of skill_search hits. Missing names are simply omitted from the result, not errors.', { names: z.array(z.string()).min(1) }, async ({ names }) => {
|
|
52
|
+
const docs = repo.bulkGet(names);
|
|
53
|
+
return { content: [{ type: 'text', text: JSON.stringify(docs, null, 2) }] };
|
|
54
|
+
});
|
|
22
55
|
mcp.tool('skill_create', `Creates a new skill as <root>/[folder/]<name>/SKILL.md, per the agentskills.io open standard — a folder containing SKILL.md, optionally alongside scripts/references/assets subfolders you create separately on disk. ${AUTHORING_SKILL_HINT}`, {
|
|
23
56
|
name: z.string().describe(SKILL_NAME_DESCRIPTION),
|
|
24
57
|
description: z
|
|
@@ -44,6 +77,39 @@ export function registerSkillTools(mcp, repo) {
|
|
|
44
77
|
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
45
78
|
}
|
|
46
79
|
});
|
|
80
|
+
const skillEntrySchema = z.object({
|
|
81
|
+
name: z.string().describe(SKILL_NAME_DESCRIPTION),
|
|
82
|
+
description: z.string().max(1024).describe('required by spec: what the skill does AND when to use it'),
|
|
83
|
+
body: z.string().describe('markdown body of SKILL.md'),
|
|
84
|
+
license: z.string().optional(),
|
|
85
|
+
compatibility: z.string().max(500).optional(),
|
|
86
|
+
owner: z.string().optional(),
|
|
87
|
+
status: z.enum(SKILL_STATUS).optional(),
|
|
88
|
+
tags: z.array(z.string()).optional(),
|
|
89
|
+
trigger_phrases: z.array(z.string()).optional(),
|
|
90
|
+
extends: z.string().optional(),
|
|
91
|
+
folder: z.string().optional(),
|
|
92
|
+
...(multiRoot ? { root: z.string().describe(`which configured skill root to write into: ${rootNames}`) } : {}),
|
|
93
|
+
});
|
|
94
|
+
mcp.tool('skill_bulk_create', `Creates many skills in one call — each entry is the same shape as skill_create's args. Returns per-name success/failure so one bad entry (duplicate name, invalid name, existing directory) doesn't abort the rest of the batch. ${AUTHORING_SKILL_HINT}`, { entries: z.array(skillEntrySchema).min(1) }, async ({ entries }) => {
|
|
95
|
+
const results = repo.bulkCreate(entries.map((e) => ({
|
|
96
|
+
frontmatter: {
|
|
97
|
+
name: e.name,
|
|
98
|
+
description: e.description,
|
|
99
|
+
license: e.license,
|
|
100
|
+
compatibility: e.compatibility,
|
|
101
|
+
owner: e.owner,
|
|
102
|
+
status: e.status,
|
|
103
|
+
tags: e.tags,
|
|
104
|
+
trigger_phrases: e.trigger_phrases,
|
|
105
|
+
extends: e.extends,
|
|
106
|
+
},
|
|
107
|
+
body: e.body,
|
|
108
|
+
folder: e.folder,
|
|
109
|
+
root: e.root,
|
|
110
|
+
})));
|
|
111
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
112
|
+
});
|
|
47
113
|
mcp.tool('skill_update', `Edits an existing skill in place — frontmatter fields and/or body. Only provided fields change. Use skill_rename to change the name/folder. ${AUTHORING_SKILL_HINT}`, {
|
|
48
114
|
name: z.string(),
|
|
49
115
|
description: z.string().max(1024).optional(),
|
|
@@ -55,6 +121,7 @@ export function registerSkillTools(mcp, repo) {
|
|
|
55
121
|
tags: z.array(z.string()).optional(),
|
|
56
122
|
trigger_phrases: z.array(z.string()).optional(),
|
|
57
123
|
extends: z.string().optional(),
|
|
124
|
+
deprecated: z.boolean().optional().describe('marks the skill as deprecated (or un-deprecates when false) — independent of status'),
|
|
58
125
|
}, async ({ name, body, ...frontmatterFields }) => {
|
|
59
126
|
try {
|
|
60
127
|
const doc = repo.update(name, frontmatterFields, body);
|
|
@@ -85,4 +152,8 @@ export function registerSkillTools(mcp, repo) {
|
|
|
85
152
|
return { content: [{ type: 'text', text: err.message }], isError: true };
|
|
86
153
|
}
|
|
87
154
|
});
|
|
155
|
+
mcp.tool('skill_bulk_delete', 'Hard-deletes many skills by name in one call — e.g. cleaning up a batch found via skill_search/skill_list. No tombstone. Returns per-name success/failure so one bad name doesn\'t abort the rest of the batch.', { names: z.array(z.string()).min(1) }, async ({ names }) => {
|
|
156
|
+
const results = repo.bulkDelete(names);
|
|
157
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
|
|
158
|
+
});
|
|
88
159
|
}
|
package/dist/src/store/db.js
CHANGED
|
@@ -13,6 +13,8 @@ export function openCache(dbPath) {
|
|
|
13
13
|
extends TEXT,
|
|
14
14
|
source_path TEXT NOT NULL UNIQUE, -- path to SKILL.md
|
|
15
15
|
root TEXT NOT NULL DEFAULT '', -- name of the configured root this file lives under
|
|
16
|
+
deprecated INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
created_at TEXT,
|
|
16
18
|
body TEXT NOT NULL,
|
|
17
19
|
mtime_ms INTEGER NOT NULL
|
|
18
20
|
);
|
|
@@ -28,6 +30,8 @@ export function openCache(dbPath) {
|
|
|
28
30
|
related_to TEXT,
|
|
29
31
|
source_path TEXT NOT NULL UNIQUE,
|
|
30
32
|
root TEXT NOT NULL DEFAULT '', -- name of the configured root this file lives under
|
|
33
|
+
deprecated INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
created_at TEXT,
|
|
31
35
|
body TEXT NOT NULL,
|
|
32
36
|
mtime_ms INTEGER NOT NULL
|
|
33
37
|
);
|
|
@@ -43,17 +47,26 @@ export function openCache(dbPath) {
|
|
|
43
47
|
tokenize = 'porter unicode61'
|
|
44
48
|
);
|
|
45
49
|
`);
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
ensureColumns(db, 'skills', [
|
|
51
|
+
['root', "TEXT NOT NULL DEFAULT ''"],
|
|
52
|
+
['deprecated', 'INTEGER NOT NULL DEFAULT 0'],
|
|
53
|
+
['created_at', 'TEXT'],
|
|
54
|
+
]);
|
|
55
|
+
ensureColumns(db, 'memory_docs', [
|
|
56
|
+
['root', "TEXT NOT NULL DEFAULT ''"],
|
|
57
|
+
['deprecated', 'INTEGER NOT NULL DEFAULT 0'],
|
|
58
|
+
['created_at', 'TEXT'],
|
|
59
|
+
]);
|
|
48
60
|
backfillSearchIndex(db);
|
|
49
61
|
return db;
|
|
50
62
|
}
|
|
51
|
-
/** Migration for cache files created before
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
/** Migration for cache files created before a given column existed. Safe to call every startup. */
|
|
64
|
+
function ensureColumns(db, table, columns) {
|
|
65
|
+
const existing = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name));
|
|
66
|
+
for (const [name, ddlType] of columns) {
|
|
67
|
+
if (!existing.has(name))
|
|
68
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${ddlType}`);
|
|
69
|
+
}
|
|
57
70
|
}
|
|
58
71
|
/** One-time backfill for existing rows the first time search_index is introduced into a cache file. */
|
|
59
72
|
function backfillSearchIndex(db) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** A quoted FTS5 syntax error, translated into something an agent can act on without seeing raw SQLite internals. */
|
|
2
|
+
export class SearchQueryError extends Error {
|
|
3
|
+
constructor(query, cause) {
|
|
4
|
+
super(`invalid search query "${query}": ${cause.message}. ` +
|
|
5
|
+
`FTS5 syntax notes: hyphenated/punctuated words must be quoted (e.g. "blue-green"), ` +
|
|
6
|
+
`bare words are AND'd by default, use OR/NOT for other logic, "exact phrase" for phrases, and prefix* for prefix matching.`);
|
|
7
|
+
this.name = 'SearchQueryError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Full-text search over the shared FTS5 index, optionally scoped to one ref_table.
|
|
12
|
+
* `query` is passed through as raw FTS5 MATCH syntax — supports `AND`/`OR`/`NOT`,
|
|
13
|
+
* `"exact phrases"`, and `prefix*` — so callers get grep-like power for free.
|
|
14
|
+
* Ranked by bm25() (lower is better, so we negate for a "higher is better" score).
|
|
15
|
+
* Throws SearchQueryError on malformed FTS5 syntax instead of a raw SQLite error.
|
|
16
|
+
*/
|
|
17
|
+
export function searchIndex(db, query, opts = {}) {
|
|
18
|
+
const { table, limit = 20, offset = 0 } = opts;
|
|
19
|
+
try {
|
|
20
|
+
const rows = db
|
|
21
|
+
.prepare(`SELECT ref_table, ref_id,
|
|
22
|
+
snippet(search_index, 3, '<<', '>>', '…', 20) AS snippet,
|
|
23
|
+
-bm25(search_index) AS score
|
|
24
|
+
FROM search_index
|
|
25
|
+
WHERE search_index MATCH ? ${table ? 'AND ref_table = ?' : ''}
|
|
26
|
+
ORDER BY bm25(search_index)
|
|
27
|
+
LIMIT ? OFFSET ?`)
|
|
28
|
+
.all(...(table ? [query, table, limit, offset] : [query, limit, offset]));
|
|
29
|
+
return rows;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw new SearchQueryError(query, err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Full-text search across BOTH skills and memory docs in one ranked list —
|
|
37
|
+
* for the common case of "find where I put X" when the caller doesn't know
|
|
38
|
+
* which bucket it landed in. No metadata filters (doc_type/status/tag differ
|
|
39
|
+
* per table); use skill_search/memory_search directly when filtering by those.
|
|
40
|
+
*/
|
|
41
|
+
export function searchCombined(db, query, limit = 20, offset = 0) {
|
|
42
|
+
try {
|
|
43
|
+
return db
|
|
44
|
+
.prepare(`SELECT ref_table, ref_id AS id,
|
|
45
|
+
COALESCE(s.description, m.description) AS description,
|
|
46
|
+
COALESCE(s.root, m.root) AS root,
|
|
47
|
+
snippet(search_index, 3, '<<', '>>', '…', 20) AS snippet,
|
|
48
|
+
-bm25(search_index) AS score
|
|
49
|
+
FROM search_index
|
|
50
|
+
LEFT JOIN skills s ON search_index.ref_table = 'skills' AND s.id = search_index.ref_id
|
|
51
|
+
LEFT JOIN memory_docs m ON search_index.ref_table = 'memory_docs' AND m.id = search_index.ref_id
|
|
52
|
+
WHERE search_index MATCH ?
|
|
53
|
+
ORDER BY bm25(search_index)
|
|
54
|
+
LIMIT ? OFFSET ?`)
|
|
55
|
+
.all(query, limit, offset);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new SearchQueryError(query, err);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/src/store/sync.js
CHANGED
|
@@ -3,8 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import chokidar, {} from 'chokidar';
|
|
4
4
|
import { readMarkdownFile } from './markdown-file.js';
|
|
5
5
|
import { flattenTags } from './db.js';
|
|
6
|
-
const skillColumns = ['id', 'description', 'owner', 'status', 'tags', 'trigger_phrases', 'extends'];
|
|
7
|
-
const memoryColumns = ['id', 'key', 'key_type', 'description', 'doc_type', 'tags', 'status', 'related_to'];
|
|
6
|
+
const skillColumns = ['id', 'description', 'owner', 'status', 'tags', 'trigger_phrases', 'extends', 'deprecated', 'created_at'];
|
|
7
|
+
const memoryColumns = ['id', 'key', 'key_type', 'description', 'doc_type', 'tags', 'status', 'related_to', 'deprecated', 'created_at'];
|
|
8
8
|
export function skillSyncSpec(sources) {
|
|
9
9
|
return {
|
|
10
10
|
table: 'skills',
|
|
@@ -20,6 +20,8 @@ export function skillSyncSpec(sources) {
|
|
|
20
20
|
tags: JSON.stringify(fm.tags ?? []),
|
|
21
21
|
trigger_phrases: JSON.stringify(fm.trigger_phrases ?? []),
|
|
22
22
|
extends: fm.metadata?.extends ?? null,
|
|
23
|
+
deprecated: fm.deprecated ? 1 : 0,
|
|
24
|
+
created_at: fm.created_at ?? null,
|
|
23
25
|
}),
|
|
24
26
|
};
|
|
25
27
|
}
|
|
@@ -39,6 +41,8 @@ export function memorySyncSpec(sources) {
|
|
|
39
41
|
tags: JSON.stringify(fm.tags ?? []),
|
|
40
42
|
status: fm.status ?? 'active',
|
|
41
43
|
related_to: fm.related_to ?? null,
|
|
44
|
+
deprecated: fm.deprecated ? 1 : 0,
|
|
45
|
+
created_at: fm.created_at ?? null,
|
|
42
46
|
}),
|
|
43
47
|
};
|
|
44
48
|
}
|
package/dist/src/web/routes.js
CHANGED
|
@@ -23,6 +23,8 @@ function queryEntries(db, req) {
|
|
|
23
23
|
const keyTypes = asArray(req.query.key_type);
|
|
24
24
|
const roots = asArray(req.query.root);
|
|
25
25
|
const q = req.query.q?.trim();
|
|
26
|
+
const deprecatedParam = req.query.deprecated;
|
|
27
|
+
const deprecated = deprecatedParam === '0' || deprecatedParam === '1' ? deprecatedParam : undefined;
|
|
26
28
|
const matchedIds = q
|
|
27
29
|
? matchSearch(db, q)
|
|
28
30
|
: null;
|
|
@@ -31,10 +33,10 @@ function queryEntries(db, req) {
|
|
|
31
33
|
}
|
|
32
34
|
const results = [];
|
|
33
35
|
if (type === 'skill' || type === 'all') {
|
|
34
|
-
results.push(...queryTable(db, 'skills', { tags, statuses, owners, docTypes: [], keyTypes: [], roots }, matchedIds?.skills));
|
|
36
|
+
results.push(...queryTable(db, 'skills', { tags, statuses, owners, docTypes: [], keyTypes: [], roots, deprecated }, matchedIds?.skills));
|
|
35
37
|
}
|
|
36
38
|
if (type === 'memory' || type === 'all') {
|
|
37
|
-
results.push(...queryTable(db, 'memory_docs', { tags, statuses, owners: [], docTypes, keyTypes, roots }, matchedIds?.memory_docs));
|
|
39
|
+
results.push(...queryTable(db, 'memory_docs', { tags, statuses, owners: [], docTypes, keyTypes, roots, deprecated }, matchedIds?.memory_docs));
|
|
38
40
|
}
|
|
39
41
|
const sort = req.query.sort ?? 'mtime_desc';
|
|
40
42
|
results.sort((a, b) => {
|
|
@@ -42,6 +44,15 @@ function queryEntries(db, req) {
|
|
|
42
44
|
return a.mtime_ms - b.mtime_ms;
|
|
43
45
|
if (sort === 'name_asc')
|
|
44
46
|
return a.name.localeCompare(b.name);
|
|
47
|
+
if (sort === 'created_at_asc') {
|
|
48
|
+
if (!a.created_at && !b.created_at)
|
|
49
|
+
return 0;
|
|
50
|
+
if (!a.created_at)
|
|
51
|
+
return 1; // missing created_at sorts last
|
|
52
|
+
if (!b.created_at)
|
|
53
|
+
return -1;
|
|
54
|
+
return a.created_at.localeCompare(b.created_at);
|
|
55
|
+
}
|
|
45
56
|
return b.mtime_ms - a.mtime_ms; // mtime_desc, default
|
|
46
57
|
});
|
|
47
58
|
return results;
|
|
@@ -74,13 +85,17 @@ function queryTable(db, table, filters, restrictToIds) {
|
|
|
74
85
|
where += ` AND root IN (${filters.roots.map(() => '?').join(', ')})`;
|
|
75
86
|
params.push(...filters.roots);
|
|
76
87
|
}
|
|
88
|
+
if (filters.deprecated !== undefined) {
|
|
89
|
+
where += ` AND deprecated = ?`;
|
|
90
|
+
params.push(filters.deprecated === '1' ? 1 : 0);
|
|
91
|
+
}
|
|
77
92
|
if (restrictToIds) {
|
|
78
93
|
where += ` AND id IN (${[...restrictToIds].map(() => '?').join(', ')})`;
|
|
79
94
|
params.push(...restrictToIds);
|
|
80
95
|
}
|
|
81
96
|
if (table === 'skills') {
|
|
82
97
|
const rows = db
|
|
83
|
-
.prepare(`SELECT id, description, owner, status, tags, root, mtime_ms FROM skills WHERE ${where}`)
|
|
98
|
+
.prepare(`SELECT id, description, owner, status, tags, root, mtime_ms, deprecated, created_at FROM skills WHERE ${where}`)
|
|
84
99
|
.all(...params);
|
|
85
100
|
return rows.map((r) => ({
|
|
86
101
|
_table: 'skills',
|
|
@@ -94,10 +109,12 @@ function queryTable(db, table, filters, restrictToIds) {
|
|
|
94
109
|
key_type: null,
|
|
95
110
|
root: r.root,
|
|
96
111
|
mtime_ms: r.mtime_ms,
|
|
112
|
+
deprecated: !!r.deprecated,
|
|
113
|
+
created_at: r.created_at,
|
|
97
114
|
}));
|
|
98
115
|
}
|
|
99
116
|
const rows = db
|
|
100
|
-
.prepare(`SELECT id, key, description, doc_type, key_type, status, tags, root, mtime_ms FROM memory_docs WHERE ${where}`)
|
|
117
|
+
.prepare(`SELECT id, key, description, doc_type, key_type, status, tags, root, mtime_ms, deprecated, created_at FROM memory_docs WHERE ${where}`)
|
|
101
118
|
.all(...params);
|
|
102
119
|
return rows.map((r) => ({
|
|
103
120
|
_table: 'memory_docs',
|
|
@@ -111,6 +128,8 @@ function queryTable(db, table, filters, restrictToIds) {
|
|
|
111
128
|
key_type: r.key_type,
|
|
112
129
|
root: r.root,
|
|
113
130
|
mtime_ms: r.mtime_ms,
|
|
131
|
+
deprecated: !!r.deprecated,
|
|
132
|
+
created_at: r.created_at,
|
|
114
133
|
}));
|
|
115
134
|
}
|
|
116
135
|
/** Runs the FTS5 query once and buckets matching ids by source table. */
|
|
@@ -219,6 +238,81 @@ export function buildWebRouter(db, config, skillRepo, memoryRepo) {
|
|
|
219
238
|
const trigger_phrases = row.trigger_phrases ? JSON.parse(row.trigger_phrases) : undefined;
|
|
220
239
|
res.json({ ...row, tags, trigger_phrases });
|
|
221
240
|
});
|
|
241
|
+
router.patch('/api/entries/:table/:id/deprecated', (req, res) => {
|
|
242
|
+
const { table, id } = req.params;
|
|
243
|
+
const { deprecated } = req.body;
|
|
244
|
+
if (table !== 'skills' && table !== 'memory_docs') {
|
|
245
|
+
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!id) {
|
|
249
|
+
res.status(400).json({ error: 'id is required' });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (typeof deprecated !== 'boolean') {
|
|
253
|
+
res.status(400).json({ error: 'body must be { deprecated: boolean }' });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
if (table === 'skills')
|
|
258
|
+
skillRepo.update(id, { deprecated });
|
|
259
|
+
else
|
|
260
|
+
memoryRepo.update(id, { deprecated });
|
|
261
|
+
res.json({ id, deprecated });
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
res.status(404).json({ error: err.message });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
router.post('/api/entries/:table/bulk/deprecated', (req, res) => {
|
|
268
|
+
const { table } = req.params;
|
|
269
|
+
const { ids, deprecated } = req.body;
|
|
270
|
+
if (table !== 'skills' && table !== 'memory_docs') {
|
|
271
|
+
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (!Array.isArray(ids) || ids.length === 0 || typeof deprecated !== 'boolean') {
|
|
275
|
+
res.status(400).json({ error: 'body must be { ids: string[], deprecated: boolean }' });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const results = table === 'skills' ? skillRepo.bulkUpdate(ids, { deprecated }) : memoryRepo.bulkUpdate(ids, { deprecated });
|
|
279
|
+
res.json({ results });
|
|
280
|
+
});
|
|
281
|
+
router.delete('/api/entries/:table/:id', (req, res) => {
|
|
282
|
+
const { table, id } = req.params;
|
|
283
|
+
if (table !== 'skills' && table !== 'memory_docs') {
|
|
284
|
+
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!id) {
|
|
288
|
+
res.status(400).json({ error: 'id is required' });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
if (table === 'skills')
|
|
293
|
+
skillRepo.delete(id);
|
|
294
|
+
else
|
|
295
|
+
memoryRepo.delete(id);
|
|
296
|
+
res.json({ deleted: id });
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
res.status(404).json({ error: err.message });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
router.post('/api/entries/:table/bulk/delete', (req, res) => {
|
|
303
|
+
const { table } = req.params;
|
|
304
|
+
const { ids } = req.body;
|
|
305
|
+
if (table !== 'skills' && table !== 'memory_docs') {
|
|
306
|
+
res.status(400).json({ error: 'table must be "skills" or "memory_docs"' });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
310
|
+
res.status(400).json({ error: 'body must be { ids: string[] }' });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const results = table === 'skills' ? skillRepo.bulkDelete(ids) : memoryRepo.bulkDelete(ids);
|
|
314
|
+
res.json({ results });
|
|
315
|
+
});
|
|
222
316
|
router.get('/api/facets', (req, res) => {
|
|
223
317
|
const type = req.query.type ?? 'all';
|
|
224
318
|
res.json(buildFacets(db, type));
|
package/dist/src/web/ui-tool.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function registerUiTool(mcp, port) {
|
|
2
|
-
mcp.tool('bucket_open_ui', 'Returns the URL for the mem-bucket web viewer — a
|
|
2
|
+
mcp.tool('bucket_open_ui', 'Returns the URL for the mem-bucket web viewer — a browser UI for searching/filtering skills and memory docs by tag, status, owner, deprecated flag, and fulltext, sorting by creation date, and marking items deprecated or deleting them (single or bulk). For anything beyond that, use the skill_*/memory_* tools instead.', {}, async () => {
|
|
3
3
|
const url = `http://localhost:${port}/`;
|
|
4
4
|
return { content: [{ type: 'text', text: url }] };
|
|
5
5
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-memory-bucket",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server exposing skill_* (reusable coding patterns) and memory_* (point-in-time working context) tools over a markdown+frontmatter source, cached into SQLite at runtime.",
|
|
6
6
|
"repository": {
|