@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 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. Prefer `path` for PDF/image ingest so file bytes never enter the model context.
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` preferred for binaries) |
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 nx run stack-mcp:build --tui=false
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
- if (isTextContentType(contentType)) {
30
- return {
31
- ...base,
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
- binaryBase64: buf.toString('base64'),
45
+ text: buf.toString('utf8'),
41
46
  path: filePath,
42
47
  };
43
48
  }
44
- if (args.text !== undefined || args.binaryBase64 !== 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, text, or binaryBase64 for memory_ingest');
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 'application/octet-stream';
103
+ return 'text/plain';
91
104
  }
92
105
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syndicai/stack-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
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
@@ -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 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
+ '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 (or resume) a Memory ingest job (OCR → chunk → embed → store). Prefer local `path` for PDFs/images so bytes stay out of the model context; ' +
88
- 'the MCP process reads the file and POSTs JSON (text or binaryBase64) to Memory REST. ' +
89
- 'Scheduling: coding chat is foreground; OCR stays concurrency 1 and is spaced under load; embeddings use a lower concurrency cap while chat is busy and a higher cap when idle — jobs usually keep progressing rather than pausing. ' +
90
- 'Poll memory_ingest_status until completed/failed. If status is "paused", keep polling the same jobId do not open a second ingest for the same sourceKey. Large PDFs can take many minutes when the squad is actively chatting.',
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, application/pdf, image/png). Used with path or inline payload.'),
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; preferred for PDF/image ingest'),
102
- text: z.string().optional().describe('Plain text / source content'),
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('Base64 binary (small fixtures only; prefer path for real PDFs/images)'),
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 (in progress, may be slow under chat load), paused (rare — keep polling), completed, failed. ' +
114
- 'Do not create a new ingest for the same sourceKey while a non-terminal job exists.',
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
  },