@syndicai/stack-mcp 0.1.1 → 0.2.1
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 +11 -9
- package/ingest-source.js +26 -13
- package/package.json +1 -1
- package/tools.js +13 -12
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# `@syndicai/stack-mcp`
|
|
2
2
|
|
|
3
|
-
Local stdio MCP server for SyndicAI Stack. Agents (OpenCode, Cursor, etc.) talk to this process; it calls satellite Memory REST with your API key.
|
|
3
|
+
Local stdio MCP server for SyndicAI Stack. Agents (OpenCode, Cursor, etc.) talk to this process; it calls satellite Memory REST with your API key.
|
|
4
|
+
|
|
5
|
+
Ingest accepts **text / markdown / code only**. Convert PDFs and images to markdown on the client before calling `memory_ingest`. Prefer a local filesystem `path` so file bytes never enter the model context. Embeddings run on the shared stack-datalayer (CPU Granite), not on the satellite GPU.
|
|
4
6
|
|
|
5
7
|
## Configure
|
|
6
8
|
|
|
@@ -37,11 +39,11 @@ Monorepo / unpublished:
|
|
|
37
39
|
|
|
38
40
|
## Tools
|
|
39
41
|
|
|
40
|
-
| Tool | Purpose
|
|
41
|
-
| ---------------------- |
|
|
42
|
-
| `memory_search` | Hybrid retrieve
|
|
43
|
-
| `memory_ingest` |
|
|
44
|
-
| `memory_ingest_status` | Poll job
|
|
42
|
+
| Tool | Purpose |
|
|
43
|
+
| ---------------------- | ----------------------------------------------------------------------- |
|
|
44
|
+
| `memory_search` | Hybrid retrieve |
|
|
45
|
+
| `memory_ingest` | Start ingest async; returns `jobId` (`path` or `text`; PDF/image rejected) |
|
|
46
|
+
| `memory_ingest_status` | Poll job until `completed` / `failed` |
|
|
45
47
|
|
|
46
48
|
## Dev
|
|
47
49
|
|
|
@@ -55,10 +57,10 @@ Build output: `dist/apps/stack-mcp/` (workspace root), including copied `package
|
|
|
55
57
|
|
|
56
58
|
## Publish
|
|
57
59
|
|
|
60
|
+
From the monorepo root (builds into `dist/apps/stack-mcp`, then publishes that folder):
|
|
61
|
+
|
|
58
62
|
```bash
|
|
59
|
-
pnpm
|
|
60
|
-
cd dist/apps/stack-mcp
|
|
61
|
-
npm publish
|
|
63
|
+
pnpm publish:stack-mcp
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
Do **not** publish from `apps/stack-mcp` — compiled JS lives under workspace `dist/`.
|
package/ingest-source.js
CHANGED
|
@@ -7,6 +7,15 @@ const TEXT_CONTENT_TYPES = new Set([
|
|
|
7
7
|
'application/json',
|
|
8
8
|
'text/csv',
|
|
9
9
|
]);
|
|
10
|
+
const REJECTED_CONTENT_TYPES = new Set([
|
|
11
|
+
'application/pdf',
|
|
12
|
+
'image/png',
|
|
13
|
+
'image/jpeg',
|
|
14
|
+
'image/jpg',
|
|
15
|
+
'image/webp',
|
|
16
|
+
'image/gif',
|
|
17
|
+
]);
|
|
18
|
+
export const CONVERSION_REQUIRED_MESSAGE = 'PDF and image ingest is not supported. Convert to markdown or plain text on the client, then ingest text/markdown.';
|
|
10
19
|
/**
|
|
11
20
|
* Build the Memory REST ingest JSON body. When `path` is set, reads the file
|
|
12
21
|
* locally so the LLM never needs to supply bytes.
|
|
@@ -24,31 +33,35 @@ export async function buildIngestBody(args) {
|
|
|
24
33
|
};
|
|
25
34
|
if (args.path?.trim()) {
|
|
26
35
|
const filePath = args.path.trim();
|
|
27
|
-
const buf = await readFile(filePath);
|
|
28
36
|
const contentType = base.contentType || guessContentType(filePath);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
contentType,
|
|
33
|
-
text: buf.toString('utf8'),
|
|
34
|
-
path: filePath,
|
|
35
|
-
};
|
|
37
|
+
assertAllowedContentType(contentType);
|
|
38
|
+
if (!isTextContentType(contentType)) {
|
|
39
|
+
throw new Error(`${CONVERSION_REQUIRED_MESSAGE} Unsupported contentType for path ingest: ${contentType}`);
|
|
36
40
|
}
|
|
41
|
+
const buf = await readFile(filePath);
|
|
37
42
|
return {
|
|
38
43
|
...base,
|
|
39
44
|
contentType,
|
|
40
|
-
|
|
45
|
+
text: buf.toString('utf8'),
|
|
41
46
|
path: filePath,
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
|
-
if (args.text !== undefined
|
|
49
|
+
if (args.text !== undefined) {
|
|
50
|
+
assertAllowedContentType(base.contentType);
|
|
45
51
|
return {
|
|
46
52
|
...base,
|
|
47
53
|
text: args.text,
|
|
48
|
-
binaryBase64: args.binaryBase64,
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
|
-
throw new Error('Provide path
|
|
56
|
+
throw new Error('Provide path or text for memory_ingest');
|
|
57
|
+
}
|
|
58
|
+
function assertAllowedContentType(contentType) {
|
|
59
|
+
const mime = (contentType.toLowerCase().split(';')[0] ?? contentType)
|
|
60
|
+
.trim()
|
|
61
|
+
.toLowerCase();
|
|
62
|
+
if (REJECTED_CONTENT_TYPES.has(mime)) {
|
|
63
|
+
throw new Error(CONVERSION_REQUIRED_MESSAGE);
|
|
64
|
+
}
|
|
52
65
|
}
|
|
53
66
|
function isTextContentType(contentType) {
|
|
54
67
|
const mime = (contentType.toLowerCase().split(';')[0] ?? contentType)
|
|
@@ -87,6 +100,6 @@ function guessContentType(filePath) {
|
|
|
87
100
|
case '.jsx':
|
|
88
101
|
return 'text/javascript';
|
|
89
102
|
default:
|
|
90
|
-
return '
|
|
103
|
+
return 'text/plain';
|
|
91
104
|
}
|
|
92
105
|
}
|
package/package.json
CHANGED
package/tools.js
CHANGED
|
@@ -67,7 +67,7 @@ export function registerStackMcpTools(server, client) {
|
|
|
67
67
|
server.registerTool('memory_search', {
|
|
68
68
|
title: 'Search Memory',
|
|
69
69
|
description: 'Hybrid search over the squad Memory corpus (vector + lexical). Returns ranked chunk hits with content and scores. ' +
|
|
70
|
-
'Query
|
|
70
|
+
'Query embeddings run on the stack-datalayer (CPU Granite).',
|
|
71
71
|
inputSchema: {
|
|
72
72
|
query: z.string().describe('Natural-language search query'),
|
|
73
73
|
limit: z
|
|
@@ -84,34 +84,35 @@ export function registerStackMcpTools(server, client) {
|
|
|
84
84
|
}, async (args) => runMemorySearch(client, args));
|
|
85
85
|
server.registerTool('memory_ingest', {
|
|
86
86
|
title: 'Ingest into Memory',
|
|
87
|
-
description: 'Start
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
87
|
+
description: 'Start a Memory ingest job asynchronously (chunk → embed → store). Returns immediately with jobId and status ' +
|
|
88
|
+
'(usually "running"; validation failures may be "cancelled"). Accepts text/markdown/code only — convert PDFs/images ' +
|
|
89
|
+
'to markdown on the client first. Prefer local `path` so file bytes never enter the model context; or pass inline `text`. ' +
|
|
90
|
+
'Embeddings run on the stack-datalayer (CPU Granite). Poll memory_ingest_status with the jobId until completed/failed. ' +
|
|
91
|
+
'Do not open a second ingest for the same sourceKey while a non-terminal job exists.',
|
|
91
92
|
inputSchema: {
|
|
92
93
|
sourceKey: z
|
|
93
94
|
.string()
|
|
94
95
|
.describe('Stable source identity (e.g. repo path or doc id)'),
|
|
95
96
|
contentType: z
|
|
96
97
|
.string()
|
|
97
|
-
.describe('MIME type (e.g. text/plain,
|
|
98
|
+
.describe('MIME type (e.g. text/plain, text/markdown). PDF/image types are rejected.'),
|
|
98
99
|
path: z
|
|
99
100
|
.string()
|
|
100
101
|
.optional()
|
|
101
|
-
.describe('Absolute or workspace-relative filesystem path
|
|
102
|
-
text: z
|
|
103
|
-
binaryBase64: z
|
|
102
|
+
.describe('Absolute or workspace-relative filesystem path to a text/markdown/code file'),
|
|
103
|
+
text: z
|
|
104
104
|
.string()
|
|
105
105
|
.optional()
|
|
106
|
-
.describe('
|
|
106
|
+
.describe('Plain text / markdown / source content'),
|
|
107
107
|
uri: z.string().optional().describe('Optional source URI metadata'),
|
|
108
108
|
collectionId: z.string().optional().describe('Optional collection id'),
|
|
109
109
|
},
|
|
110
110
|
}, async (args) => runMemoryIngest(client, args));
|
|
111
111
|
server.registerTool('memory_ingest_status', {
|
|
112
112
|
title: 'Ingest job status',
|
|
113
|
-
description: '
|
|
114
|
-
'
|
|
113
|
+
description: 'Poll status and checkpoint for a Memory ingest job started via memory_ingest. ' +
|
|
114
|
+
'Statuses: running (in progress), completed, failed, cancelled. Optional legacy: paused. ' +
|
|
115
|
+
'Keep polling the same jobId until completed/failed; do not start a second ingest for the same sourceKey while open.',
|
|
115
116
|
inputSchema: {
|
|
116
117
|
jobId: z.string().describe('Ingest job id from memory_ingest'),
|
|
117
118
|
},
|