@syndicai/stack-mcp 0.1.0 → 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 +2 -2
- package/tools.js +13 -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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syndicai/stack-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local stdio MCP server for SyndicAI Stack (Memory and future tools)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"syndicai-stack-mcp": "
|
|
7
|
+
"syndicai-stack-mcp": "main.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"*.js"
|
package/tools.js
CHANGED
|
@@ -66,7 +66,8 @@ export async function runMemoryIngestStatus(client, args) {
|
|
|
66
66
|
export function registerStackMcpTools(server, client) {
|
|
67
67
|
server.registerTool('memory_search', {
|
|
68
68
|
title: 'Search Memory',
|
|
69
|
-
description: 'Hybrid search over the squad Memory corpus (vector + lexical). Returns ranked chunk hits with content and scores.'
|
|
69
|
+
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.',
|
|
70
71
|
inputSchema: {
|
|
71
72
|
query: z.string().describe('Natural-language search query'),
|
|
72
73
|
limit: z
|
|
@@ -83,32 +84,34 @@ export function registerStackMcpTools(server, client) {
|
|
|
83
84
|
}, async (args) => runMemorySearch(client, args));
|
|
84
85
|
server.registerTool('memory_ingest', {
|
|
85
86
|
title: 'Ingest into Memory',
|
|
86
|
-
description: 'Start (or resume) a Memory ingest job
|
|
87
|
-
'
|
|
88
|
-
'
|
|
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.',
|
|
89
92
|
inputSchema: {
|
|
90
93
|
sourceKey: z
|
|
91
94
|
.string()
|
|
92
95
|
.describe('Stable source identity (e.g. repo path or doc id)'),
|
|
93
96
|
contentType: z
|
|
94
97
|
.string()
|
|
95
|
-
.describe('MIME type (e.g. text/plain,
|
|
98
|
+
.describe('MIME type (e.g. text/plain, text/markdown). PDF/image types are rejected.'),
|
|
96
99
|
path: z
|
|
97
100
|
.string()
|
|
98
101
|
.optional()
|
|
99
|
-
.describe('Absolute or workspace-relative filesystem path
|
|
100
|
-
text: z
|
|
101
|
-
binaryBase64: z
|
|
102
|
+
.describe('Absolute or workspace-relative filesystem path to a text/markdown/code file'),
|
|
103
|
+
text: z
|
|
102
104
|
.string()
|
|
103
105
|
.optional()
|
|
104
|
-
.describe('
|
|
106
|
+
.describe('Plain text / markdown / source content'),
|
|
105
107
|
uri: z.string().optional().describe('Optional source URI metadata'),
|
|
106
108
|
collectionId: z.string().optional().describe('Optional collection id'),
|
|
107
109
|
},
|
|
108
110
|
}, async (args) => runMemoryIngest(client, args));
|
|
109
111
|
server.registerTool('memory_ingest_status', {
|
|
110
112
|
title: 'Ingest job status',
|
|
111
|
-
description: 'Fetch status and checkpoint for a Memory ingest job.
|
|
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.',
|
|
112
115
|
inputSchema: {
|
|
113
116
|
jobId: z.string().describe('Ingest job id from memory_ingest'),
|
|
114
117
|
},
|