@syndicai/stack-mcp 0.1.1 → 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 +11 -9
- package/ingest-source.js +26 -13
- package/package.json +1 -1
- package/tools.js +10 -10
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` | Create/resume ingest (`path`
|
|
44
|
-
| `memory_ingest_status` | Poll job
|
|
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 |
|
|
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
|
@@ -84,33 +84,33 @@ 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 (or resume) a Memory ingest job (
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'
|
|
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 ingest — binary 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. ' +
|
|
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: 'Fetch status and checkpoint for a Memory ingest job. Typical statuses: pending/running
|
|
113
|
+
description: 'Fetch status and checkpoint for a Memory ingest job. Typical statuses: pending/running, paused (legacy), completed, failed. ' +
|
|
114
114
|
'Do not create a new ingest for the same sourceKey while a non-terminal job exists.',
|
|
115
115
|
inputSchema: {
|
|
116
116
|
jobId: z.string().describe('Ingest job id from memory_ingest'),
|