@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 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` | 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 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,10 +1,10 @@
1
1
  {
2
2
  "name": "@syndicai/stack-mcp",
3
- "version": "0.1.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": "./main.js"
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. Prefer local `path` for PDFs/images so bytes stay out of the model context; ' +
87
- 'the MCP process reads the file and POSTs JSON (text or binaryBase64) to Memory REST. ' +
88
- 'If status is "paused", poll memory_ingest_status do not re-ingest the same sourceKey while open.',
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, 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.'),
96
99
  path: z
97
100
  .string()
98
101
  .optional()
99
- .describe('Absolute or workspace-relative filesystem path; preferred for PDF/image ingest'),
100
- text: z.string().optional().describe('Plain text / source content'),
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('Base64 binary (small fixtures only; prefer path for real PDFs/images)'),
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. When paused, wait and poll again.',
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
  },