@syndicai/stack-mcp 0.2.0 → 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 CHANGED
@@ -39,11 +39,16 @@ Monorepo / unpublished:
39
39
 
40
40
  ## Tools
41
41
 
42
- | Tool | Purpose |
43
- | ---------------------- | ----------------------------------------------------------------------- |
44
- | `memory_search` | Hybrid retrieve |
45
- | `memory_ingest` | Create/resume ingest (`path` or `text`; PDF/image rejected) |
46
- | `memory_ingest_status` | Poll job |
42
+ | Tool | Purpose |
43
+ | --------------------------- | ------------------------------------------------------------------------------------------------ |
44
+ | `memory_list_collections` | List collections (name, type, visibility, stats, soft-TTL hints) |
45
+ | `memory_search` | Hybrid retrieve (optional `collectionId`; omit = all) |
46
+ | `memory_ingest` | Start ingest async; **requires `collectionId`**; returns `jobId` (text/markdown/code only) |
47
+ | `memory_ingest_status` | Poll job until `completed` / `failed` |
48
+ | `memory_delete` | Delete documents by `sourceId` within a collection |
49
+ | `memory_delete_collection` | Delete an entire collection (not the default General) |
50
+
51
+ Soft TTL on collections is hint-only — tools never auto-delete. When ingesting, call `memory_list_collections` and ask the user which collection to use if ambiguous.
47
52
 
48
53
  ## Dev
49
54
 
package/ingest-source.js CHANGED
@@ -24,11 +24,14 @@ export async function buildIngestBody(args) {
24
24
  if (!args.sourceKey?.trim() || !args.contentType?.trim()) {
25
25
  throw new Error('sourceKey and contentType are required');
26
26
  }
27
+ if (!args.collectionId?.trim()) {
28
+ throw new Error('collectionId is required. Call memory_list_collections and ask the user which Memory collection to use.');
29
+ }
27
30
  const base = {
28
31
  sourceKey: args.sourceKey.trim(),
29
32
  contentType: args.contentType.trim(),
30
33
  uri: args.uri,
31
- collectionId: args.collectionId,
34
+ collectionId: args.collectionId.trim(),
32
35
  path: args.path,
33
36
  };
34
37
  if (args.path?.trim()) {
package/memory-client.js CHANGED
@@ -50,6 +50,15 @@ export function createMemoryClient(config) {
50
50
  getIngest(jobId) {
51
51
  return request('GET', `/memory/ingest/${encodeURIComponent(jobId)}`);
52
52
  },
53
+ listCollections() {
54
+ return request('GET', '/memory/collections');
55
+ },
56
+ deleteBySource(params) {
57
+ return request('POST', '/memory/delete', params);
58
+ },
59
+ deleteCollection(collectionId) {
60
+ return request('DELETE', `/memory/collections/${encodeURIComponent(collectionId)}`);
61
+ },
53
62
  };
54
63
  }
55
64
  function extractErrorMessage(body) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syndicai/stack-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Local stdio MCP server for SyndicAI Stack (Memory and future tools)",
5
5
  "type": "module",
6
6
  "bin": {
package/tools.js CHANGED
@@ -5,6 +5,9 @@ export const MEMORY_MCP_TOOL_NAMES = [
5
5
  'memory_search',
6
6
  'memory_ingest',
7
7
  'memory_ingest_status',
8
+ 'memory_list_collections',
9
+ 'memory_delete',
10
+ 'memory_delete_collection',
8
11
  ];
9
12
  function textResult(data) {
10
13
  return {
@@ -42,8 +45,17 @@ export async function runMemorySearch(client, args) {
42
45
  }
43
46
  export async function runMemoryIngest(client, args) {
44
47
  try {
45
- const body = await buildIngestBody(args);
46
- const data = await client.ingest(body);
48
+ if (!args.collectionId?.trim()) {
49
+ return toolErrorResult(new Error('collectionId is required. Call memory_list_collections and ask the user which Memory collection to use.'));
50
+ }
51
+ const body = await buildIngestBody({
52
+ ...args,
53
+ collectionId: args.collectionId.trim(),
54
+ });
55
+ const data = await client.ingest({
56
+ ...body,
57
+ collectionId: args.collectionId.trim(),
58
+ });
47
59
  return textResult(data);
48
60
  }
49
61
  catch (err) {
@@ -62,12 +74,48 @@ export async function runMemoryIngestStatus(client, args) {
62
74
  return toolErrorResult(err);
63
75
  }
64
76
  }
77
+ export async function runMemoryListCollections(client) {
78
+ try {
79
+ const data = await client.listCollections();
80
+ return textResult(data);
81
+ }
82
+ catch (err) {
83
+ return toolErrorResult(err);
84
+ }
85
+ }
86
+ export async function runMemoryDelete(client, args) {
87
+ try {
88
+ if (!args.collectionId?.trim() || !args.sourceId?.trim()) {
89
+ return toolErrorResult(new Error('collectionId and sourceId are required'));
90
+ }
91
+ const data = await client.deleteBySource({
92
+ collectionId: args.collectionId.trim(),
93
+ sourceId: args.sourceId.trim(),
94
+ });
95
+ return textResult(data);
96
+ }
97
+ catch (err) {
98
+ return toolErrorResult(err);
99
+ }
100
+ }
101
+ export async function runMemoryDeleteCollection(client, args) {
102
+ try {
103
+ if (!args.collectionId?.trim()) {
104
+ return toolErrorResult(new Error('collectionId is required'));
105
+ }
106
+ const data = await client.deleteCollection(args.collectionId.trim());
107
+ return textResult(data);
108
+ }
109
+ catch (err) {
110
+ return toolErrorResult(err);
111
+ }
112
+ }
65
113
  /** Register Memory tools. Future Stack tools can be added here. */
66
114
  export function registerStackMcpTools(server, client) {
67
115
  server.registerTool('memory_search', {
68
116
  title: 'Search Memory',
69
117
  description: 'Hybrid search over the squad Memory corpus (vector + lexical). Returns ranked chunk hits with content and scores. ' +
70
- 'Query embedding shares the satellite GPU with coding chat: under load embeddings are throttled (lower concurrency) so search may be slower while users are chatting — wait and retry rather than assuming Memory is down.',
118
+ 'Query embeddings run on the stack-datalayer (CPU Granite). Optional collectionId filters to one collection; omit to search all.',
71
119
  inputSchema: {
72
120
  query: z.string().describe('Natural-language search query'),
73
121
  limit: z
@@ -82,12 +130,22 @@ export function registerStackMcpTools(server, client) {
82
130
  .describe('Optional collection filter; omit to search all'),
83
131
  },
84
132
  }, async (args) => runMemorySearch(client, args));
133
+ server.registerTool('memory_list_collections', {
134
+ title: 'List Memory collections',
135
+ description: 'List Memory collections with id, name, type (library|scratch), visibility (shared|personal), ' +
136
+ 'document/chunk stats, and soft-TTL hint fields (ttlExpiresAt, ttlExpired, cleanupHint). ' +
137
+ 'Soft TTL never auto-deletes — use memory_delete / memory_delete_collection for cleanup. ' +
138
+ 'Call this before memory_ingest and ask the user which collection to use when ambiguous. ' +
139
+ 'Personal visibility is cooperative segregation only (not a security boundary).',
140
+ inputSchema: {},
141
+ }, async () => runMemoryListCollections(client));
85
142
  server.registerTool('memory_ingest', {
86
143
  title: 'Ingest into Memory',
87
- description: 'Start (or resume) a Memory ingest job (chunk → embed → store). Accepts text/markdown/code only. ' +
88
- 'Convert PDFs/images to markdown on the client before ingestbinary PDF/image paths are rejected. ' +
89
- 'Prefer local `path` for files so the MCP process reads bytes and the model never sees them; or pass inline `text`. ' +
90
- 'Embeddings run on the stack-datalayer (CPU Granite). Poll memory_ingest_status until completed/failed. ' +
144
+ description: 'Start a Memory ingest job asynchronously (chunk → embed → store). Returns immediately with jobId and status ' +
145
+ '(usually "running"; validation failures may be "cancelled"). **Requires collectionId**call memory_list_collections ' +
146
+ 'first and ask the user which collection to use when ambiguous. Accepts text/markdown/code only convert PDFs/images ' +
147
+ 'to markdown on the client first. Prefer local `path` so file bytes never enter the model context; or pass inline `text`. ' +
148
+ 'Embeddings run on the stack-datalayer (CPU Granite). Poll memory_ingest_status with the jobId until completed/failed. ' +
91
149
  'Do not open a second ingest for the same sourceKey while a non-terminal job exists.',
92
150
  inputSchema: {
93
151
  sourceKey: z
@@ -105,15 +163,37 @@ export function registerStackMcpTools(server, client) {
105
163
  .optional()
106
164
  .describe('Plain text / markdown / source content'),
107
165
  uri: z.string().optional().describe('Optional source URI metadata'),
108
- collectionId: z.string().optional().describe('Optional collection id'),
166
+ collectionId: z
167
+ .string()
168
+ .describe('Required Memory collection id. List with memory_list_collections and ask the user which to use.'),
109
169
  },
110
170
  }, async (args) => runMemoryIngest(client, args));
111
171
  server.registerTool('memory_ingest_status', {
112
172
  title: 'Ingest job status',
113
- description: 'Fetch status and checkpoint for a Memory ingest job. Typical statuses: pending/running, paused (legacy), completed, failed. ' +
114
- 'Do not create a new ingest for the same sourceKey while a non-terminal job exists.',
173
+ description: 'Poll status and checkpoint for a Memory ingest job started via memory_ingest. ' +
174
+ 'Statuses: running (in progress), completed, failed, cancelled. Optional legacy: paused. ' +
175
+ 'Keep polling the same jobId until completed/failed; do not start a second ingest for the same sourceKey while open.',
115
176
  inputSchema: {
116
177
  jobId: z.string().describe('Ingest job id from memory_ingest'),
117
178
  },
118
179
  }, async (args) => runMemoryIngestStatus(client, args));
180
+ server.registerTool('memory_delete', {
181
+ title: 'Delete Memory by source',
182
+ description: 'Delete documents (and their chunks) for a sourceId within a Memory collection. ' +
183
+ 'Explicit cleanup only — soft TTL does not auto-delete.',
184
+ inputSchema: {
185
+ collectionId: z.string().describe('Memory collection id'),
186
+ sourceId: z
187
+ .string()
188
+ .describe('Source id / sourceKey previously used for ingest'),
189
+ },
190
+ }, async (args) => runMemoryDelete(client, args));
191
+ server.registerTool('memory_delete_collection', {
192
+ title: 'Delete Memory collection',
193
+ description: 'Delete an entire Memory collection and all of its documents/chunks. ' +
194
+ 'Cannot delete the default General collection. Explicit cleanup only — soft TTL does not auto-delete.',
195
+ inputSchema: {
196
+ collectionId: z.string().describe('Memory collection id to delete'),
197
+ },
198
+ }, async (args) => runMemoryDeleteCollection(client, args));
119
199
  }