@pairsystems/goodmem-mcp 0.1.15 → 0.1.17

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.
Files changed (3) hide show
  1. package/README.md +42 -0
  2. package/dist/index.js +101 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -167,11 +167,13 @@ Memory spaces are containers that associate a set of memories with an embedder.
167
167
  | `goodmem_memories_batch_create` | Create multiple memories in a batch |
168
168
  | `goodmem_memories_batch_delete` | Delete memories in batch |
169
169
  | `goodmem_memories_batch_get` | Get multiple memories by ID |
170
+ | `goodmem_memories_content` | Download memory content |
170
171
  | `goodmem_memories_create` | Create a new memory |
171
172
  | `goodmem_memories_delete` | Delete a memory |
172
173
  | `goodmem_memories_get` | Get a memory by ID |
173
174
  | `goodmem_memories_list` | List memories in a space |
174
175
  | `goodmem_memories_pages` | List memory page images |
176
+ | `goodmem_memories_pages_image` | Download memory page image content |
175
177
  | `goodmem_memories_retrieve` | Advanced semantic memory retrieval with JSON |
176
178
 
177
179
  ### OCR
@@ -223,6 +225,45 @@ Memory spaces are containers that associate a set of memories with an embedder.
223
225
  | `goodmem_ping_stream` | Stream ping probe results |
224
226
 
225
227
  <!-- AUTO-GENERATED MCP TOOL TABLE END -->
228
+ ## Binary content delivery
229
+
230
+ Two memory tools return raw binary payloads:
231
+
232
+ - `goodmem_memories_content` — original ingested bytes (text, image, PDF, …)
233
+ - `goodmem_memories_pages_image` — one rendered page image as bytes
234
+
235
+ The MCP server wraps the response in the appropriate MCP content block
236
+ based on the response MIME type:
237
+
238
+ | Server `Content-Type` | MCP block | What the agent gets |
239
+ |---|---|---|
240
+ | `image/*` | `image` block | base64 data + `mimeType`; Claude reads as vision input. |
241
+ | `text/*`, `application/json` | embedded `resource` with `text` | MIME-signaled text — preserves `text/markdown`, `text/x-python`, etc. |
242
+ | anything else (PDF, octet-stream, …) | embedded `resource` with `blob` | base64 blob + `mimeType`. Forward-compatible; current Claude models cannot natively render PDF bytes from an MCP block. |
243
+
244
+ ### Size cap
245
+
246
+ Both tools accept an optional `max_inline_bytes` parameter (units:
247
+ bytes) so the agent can self-limit per call:
248
+
249
+ | Knob | Value | Notes |
250
+ |---|---|---|
251
+ | **Hard server ceiling** | 64 MB (67,108,864 bytes) | Baked into the MCP server source. Above this, the tool always errors regardless of `max_inline_bytes`. |
252
+ | **Default cap** | 8 MB (8,388,608 bytes) | Used when the agent doesn't pass `max_inline_bytes`. |
253
+ | **Per-call override** | `max_inline_bytes` argument | The agent can raise it up to the hard ceiling, or lower it for tighter context-budget control. |
254
+
255
+ When the payload exceeds the effective cap, the tool throws an error
256
+ naming both the actual size and the ceiling — the agent can read the
257
+ error, retry with a larger `max_inline_bytes`, and succeed. Above the
258
+ hard ceiling, the only path is to fetch the content via REST directly
259
+ (`GET /v1/memories/{id}/content` or `/pages/{idx}/image`) rather than
260
+ through MCP.
261
+
262
+ These constants are not currently configurable via environment
263
+ variables. If you need to tune them per deployment, edit
264
+ `_clients_gen/mcp/emitter.py` (`ABSOLUTE_MAX_INLINE_BYTES`,
265
+ `DEFAULT_MAX_INLINE_BYTES`) and regenerate.
266
+
226
267
  ## Auto-inference
227
268
 
228
269
  When creating embedders, LLMs, or rerankers, provide `model_identifier` and the MCP server fills in `provider_type`, `endpoint_url`, dimensions, and other fields automatically from the built-in registry of 79 models. Inference happens client-side in the MCP layer before the request is sent.
@@ -247,3 +288,4 @@ For self-hosted or local providers (vLLM, TEI, Ollama, custom endpoints), creden
247
288
  - [GoodMem on GitHub](https://github.com/PAIR-Systems-Inc/goodmem)
248
289
  - [Python SDK on PyPI](https://pypi.org/project/goodmem/)
249
290
  - [Claude Code Plugin](https://github.com/PAIR-Systems-Inc/goodmem-claude-code-plugin)
291
+
package/dist/index.js CHANGED
@@ -20988,7 +20988,7 @@ var StdioServerTransport = class {
20988
20988
  // dist/index.js
20989
20989
  var goodmemBaseUrl = process.env.GOODMEM_BASE_URL || "";
20990
20990
  var goodmemApiKey = process.env.GOODMEM_API_KEY || "";
20991
- var BASED_ON_GOODMEM_COMMIT = "62c6d77befdfab1bfe38ef917c6a202f4569eec7";
20991
+ var BASED_ON_GOODMEM_COMMIT = "d15028640e4a884f3a648db9b25aee4cdfab719d";
20992
20992
  function isConfigured() {
20993
20993
  return goodmemBaseUrl !== "";
20994
20994
  }
@@ -21002,6 +21002,8 @@ function _buildUrl(base, path) {
21002
21002
  return baseUrl;
21003
21003
  }
21004
21004
  var DEFAULT_TIMEOUT_MS = 6e4;
21005
+ var ABSOLUTE_MAX_INLINE_BYTES = 67108864;
21006
+ var DEFAULT_MAX_INLINE_BYTES = 8388608;
21005
21007
  function _trimBody(text) {
21006
21008
  return text.length > 500 ? text.slice(0, 500) + "...[truncated]" : text;
21007
21009
  }
@@ -21044,6 +21046,37 @@ async function callApi(method, path, body, query, timeoutMs) {
21044
21046
  }
21045
21047
  return resp.text();
21046
21048
  }
21049
+ async function callApiBinary(method, path, query, timeoutMs) {
21050
+ if (!isConfigured()) {
21051
+ throw new Error("GoodMem is not configured. Call the goodmem_configure tool first with your base_url and api_key, or set GOODMEM_BASE_URL and GOODMEM_API_KEY environment variables.");
21052
+ }
21053
+ const url = _buildUrl(goodmemBaseUrl, path);
21054
+ if (query) {
21055
+ for (const [k, v] of Object.entries(query)) {
21056
+ if (v !== void 0 && v !== null && v !== "")
21057
+ url.searchParams.set(k, v);
21058
+ }
21059
+ }
21060
+ const headers = {};
21061
+ if (goodmemApiKey !== "") {
21062
+ headers["x-api-key"] = goodmemApiKey;
21063
+ }
21064
+ const resp = await fetch(url.toString(), {
21065
+ method,
21066
+ headers,
21067
+ signal: AbortSignal.timeout(timeoutMs ?? DEFAULT_TIMEOUT_MS),
21068
+ // See callApi for the redirect-policy rationale.
21069
+ redirect: "error"
21070
+ });
21071
+ if (!resp.ok) {
21072
+ const text = await resp.text();
21073
+ throw new Error(`GoodMem API error ${resp.status}: ${_trimBody(text)}`);
21074
+ }
21075
+ const buf = await resp.arrayBuffer();
21076
+ const bytes = new Uint8Array(buf);
21077
+ const mimeType = (resp.headers.get("content-type") || "application/octet-stream").split(";")[0].trim();
21078
+ return { bytes, mimeType, sizeBytes: bytes.byteLength };
21079
+ }
21047
21080
  async function callApiNdjson(method, path, body, query, timeoutSec) {
21048
21081
  if (!isConfigured()) {
21049
21082
  throw new Error("GoodMem is not configured. Call the goodmem_configure tool first with your base_url and api_key, or set GOODMEM_BASE_URL and GOODMEM_API_KEY environment variables.");
@@ -21843,7 +21876,7 @@ function autoInferFromRegistry(registryType, args) {
21843
21876
  }
21844
21877
  var server = new McpServer({
21845
21878
  name: "goodmem",
21846
- version: "0.1.15"
21879
+ version: "0.1.17"
21847
21880
  });
21848
21881
  server.tool("goodmem_configure", "Configure GoodMem server connection. Call this before using other GoodMem tools if GOODMEM_BASE_URL and GOODMEM_API_KEY environment variables are not set. Session credentials override environment variables. For bootstrap workflows that need to call /v1/system/info or /v1/system/init before keys exist, api_key may be omitted; most other tools will return AuthenticationException until a real key is provided.", {
21849
21882
  base_url: external_exports.string().describe("GoodMem server URL (e.g., https://your-server.example.com)"),
@@ -21925,7 +21958,7 @@ ${allModels.join("\n")}` }] };
21925
21958
  });
21926
21959
  server.tool("goodmem_client_info", "Return the MCP client package version and the goodmem server commit it was built against. No server connection required.", {}, async () => {
21927
21960
  return { content: [{ type: "text", text: JSON.stringify({
21928
- mcp_version: "0.1.15",
21961
+ mcp_version: "0.1.17",
21929
21962
  based_on_goodmem_commit: BASED_ON_GOODMEM_COMMIT
21930
21963
  }, null, 2) }] };
21931
21964
  });
@@ -21972,14 +22005,14 @@ function registerTools() {
21972
22005
  display_name: external_exports.string().describe("Human-readable policy name."),
21973
22006
  description: external_exports.string().describe("Optional operator description.").optional(),
21974
22007
  condition: external_exports.object({
21975
- matchAll: external_exports.boolean().describe("When true, match every authenticated RetrieveMemory request. Defaults to false when omitted.").optional(),
22008
+ matchAll: external_exports.boolean().describe("When true, match every authenticated RetrieveMemory request. Must be omitted or false when anyOf contains clauses. Defaults to false when omitted.").optional(),
21976
22009
  anyOf: external_exports.array(external_exports.object({
21977
22010
  requestorUserIds: external_exports.array(external_exports.string()).describe("Authenticated requestor user UUID strings.").optional(),
21978
22011
  apiKeyIds: external_exports.array(external_exports.string()).describe("API key UUID strings used to authenticate RetrieveMemory requests.").optional(),
21979
22012
  spaceIds: external_exports.array(external_exports.string()).describe("Post-permission accessible space UUID strings.").optional(),
21980
22013
  apiKeyLabelSelectors: external_exports.record(external_exports.string(), external_exports.string()).describe("Exact API-key label selectors that must all match.").optional(),
21981
22014
  spaceLabelSelectors: external_exports.record(external_exports.string(), external_exports.string()).describe("Exact space label selectors that must match at least one accessible space.").optional()
21982
- }).passthrough()).describe("OR clauses used for scoped matching. Required when matchAll is false or omitted, and must contain at least one non-empty clause.").optional()
22015
+ }).passthrough()).describe("OR clauses used for scoped matching. Required and non-empty when matchAll is false or omitted. Must be omitted or empty when matchAll is true. Each clause must include at least one match dimension.").optional()
21983
22016
  }).passthrough().describe("Policy match condition."),
21984
22017
  active_from: external_exports.number().int().describe("Inclusive activation time in milliseconds since epoch.").optional(),
21985
22018
  active_until: external_exports.number().int().describe("Exclusive deactivation time in milliseconds since epoch.").optional(),
@@ -22586,6 +22619,29 @@ function registerTools() {
22586
22619
  const result = await callApi("POST", `/v1/memories:batchGet`, body);
22587
22620
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
22588
22621
  });
22622
+ server.tool("goodmem_memories_content", "Download memory content\n\nReturns the original binary payload for a memory. The response uses the memory's stored content type when available. Returns 404 when the memory does not have inline content; clients can check originalContentRef from the metadata endpoint to locate external content.", {
22623
+ id: external_exports.string().describe("The UUID of the memory to download"),
22624
+ max_inline_bytes: external_exports.number().int().positive().describe("Maximum bytes to deliver inline as a content block. Defaults to 8388608. Hard server ceiling: 67108864. If the payload exceeds this cap the tool errors with the actual size \u2014 retry with a larger value (up to the ceiling) to receive it.").optional()
22625
+ }, async (args) => {
22626
+ if (typeof args.id !== "string" || args.id === "") {
22627
+ throw new Error("'id' must be a non-empty string");
22628
+ }
22629
+ const _cap = Math.min(args.max_inline_bytes ?? DEFAULT_MAX_INLINE_BYTES, ABSOLUTE_MAX_INLINE_BYTES);
22630
+ const { bytes: _bytes, mimeType: _mimeType, sizeBytes: _sizeBytes } = await callApiBinary("GET", `/v1/memories/${encodeURIComponent(String(args.id))}/content`, void 0);
22631
+ if (_sizeBytes > _cap) {
22632
+ throw new Error(`Content is ${_sizeBytes} bytes, exceeded the inline cap of ${_cap} bytes (cap = min(max_inline_bytes ?? ${DEFAULT_MAX_INLINE_BYTES}, server absolute maximum ${ABSOLUTE_MAX_INLINE_BYTES})). Retry with max_inline_bytes=${_sizeBytes} or higher (up to ${ABSOLUTE_MAX_INLINE_BYTES}) to receive it inline.`);
22633
+ }
22634
+ const _uri = `goodmem://v1/memories/${encodeURIComponent(String(args.id))}/content`;
22635
+ const _b64 = Buffer.from(_bytes).toString("base64");
22636
+ if (_mimeType.startsWith("image/")) {
22637
+ return { content: [{ type: "image", data: _b64, mimeType: _mimeType }] };
22638
+ }
22639
+ if (_mimeType.startsWith("text/") || _mimeType === "application/json") {
22640
+ const _text = new TextDecoder("utf-8").decode(_bytes);
22641
+ return { content: [{ type: "resource", resource: { uri: _uri, mimeType: _mimeType, text: _text } }] };
22642
+ }
22643
+ return { content: [{ type: "resource", resource: { uri: _uri, mimeType: _mimeType, blob: _b64 } }] };
22644
+ });
22589
22645
  server.tool("goodmem_memories_create", "Create a memory from text content or base64-encoded binary content. Provide exactly one of `original_content` or `original_content_b64` per call; if `original_content_b64`, `content_type` is required. `original_content_ref` is a metadata URL pointer (the server does NOT fetch it) and may be set alongside the content source.", {
22590
22646
  memory_id: external_exports.string().describe("Optional client-provided UUID for the memory. If omitted, the server generates one. Returns ALREADY_EXISTS if the ID is already in use.").optional(),
22591
22647
  space_id: external_exports.string().describe("ID of the space where this memory will be stored"),
@@ -22732,6 +22788,40 @@ function registerTools() {
22732
22788
  const result = await callApi("GET", `/v1/memories/${encodeURIComponent(String(args.id))}/pages`, void 0, query);
22733
22789
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
22734
22790
  });
22791
+ server.tool("goodmem_memories_pages_image", "Download memory page image content\n\nDownloads inline bytes for one page image. The page index is required. The optional dpi and content type query parameters act as rendition filters; if omitted, the server returns the unique rendition for that page or rejects ambiguous matches.", {
22792
+ id: external_exports.string().describe("Memory UUID"),
22793
+ page_index: external_exports.number().int().describe("0-based page index"),
22794
+ dpi: external_exports.number().int().describe("Optional rendition filter. If omitted, the unique page-image rendition for the page is returned; if multiple renditions exist, specify dpi and/or content_type.").optional(),
22795
+ content_type: external_exports.string().describe("Optional rendition filter. MIME type of the desired page image, such as image/png.").optional(),
22796
+ max_inline_bytes: external_exports.number().int().positive().describe("Maximum bytes to deliver inline as a content block. Defaults to 8388608. Hard server ceiling: 67108864. If the payload exceeds this cap the tool errors with the actual size \u2014 retry with a larger value (up to the ceiling) to receive it.").optional()
22797
+ }, async (args) => {
22798
+ if (typeof args.id !== "string" || args.id === "") {
22799
+ throw new Error("'id' must be a non-empty string");
22800
+ }
22801
+ if (typeof args.page_index !== "number" || !Number.isFinite(args.page_index)) {
22802
+ throw new Error("'page_index' must be a finite number");
22803
+ }
22804
+ const query = {};
22805
+ if (args.dpi !== void 0)
22806
+ query["dpi"] = String(args.dpi);
22807
+ if (args.content_type !== void 0)
22808
+ query["contentType"] = String(args.content_type);
22809
+ const _cap = Math.min(args.max_inline_bytes ?? DEFAULT_MAX_INLINE_BYTES, ABSOLUTE_MAX_INLINE_BYTES);
22810
+ const { bytes: _bytes, mimeType: _mimeType, sizeBytes: _sizeBytes } = await callApiBinary("GET", `/v1/memories/${encodeURIComponent(String(args.id))}/pages/${encodeURIComponent(String(args.page_index))}/image`, query);
22811
+ if (_sizeBytes > _cap) {
22812
+ throw new Error(`Content is ${_sizeBytes} bytes, exceeded the inline cap of ${_cap} bytes (cap = min(max_inline_bytes ?? ${DEFAULT_MAX_INLINE_BYTES}, server absolute maximum ${ABSOLUTE_MAX_INLINE_BYTES})). Retry with max_inline_bytes=${_sizeBytes} or higher (up to ${ABSOLUTE_MAX_INLINE_BYTES}) to receive it inline.`);
22813
+ }
22814
+ const _uri = `goodmem://v1/memories/${encodeURIComponent(String(args.id))}/pages/${encodeURIComponent(String(args.page_index))}/image`;
22815
+ const _b64 = Buffer.from(_bytes).toString("base64");
22816
+ if (_mimeType.startsWith("image/")) {
22817
+ return { content: [{ type: "image", data: _b64, mimeType: _mimeType }] };
22818
+ }
22819
+ if (_mimeType.startsWith("text/") || _mimeType === "application/json") {
22820
+ const _text = new TextDecoder("utf-8").decode(_bytes);
22821
+ return { content: [{ type: "resource", resource: { uri: _uri, mimeType: _mimeType, text: _text } }] };
22822
+ }
22823
+ return { content: [{ type: "resource", resource: { uri: _uri, mimeType: _mimeType, blob: _b64 } }] };
22824
+ });
22735
22825
  server.tool("goodmem_memories_retrieve", "Performs a streaming semantic search across one or more memory spaces and returns matching chunks ranked by relevance as well as LLM-postprocessed results (like summarization, question-answering, etc.).", {
22736
22826
  message: external_exports.string().describe("Primary query/message for semantic search."),
22737
22827
  context: external_exports.array(external_exports.object({
@@ -23206,14 +23296,18 @@ async function healthCheck() {
23206
23296
  }
23207
23297
  try {
23208
23298
  const url = _buildUrl(goodmemBaseUrl, "/v1/system/info");
23299
+ const healthHeaders = {};
23300
+ if (goodmemApiKey !== "") {
23301
+ healthHeaders["x-api-key"] = goodmemApiKey;
23302
+ }
23209
23303
  const resp = await fetch(url.toString(), {
23210
- headers: { "x-api-key": goodmemApiKey },
23304
+ headers: healthHeaders,
23211
23305
  signal: AbortSignal.timeout(1e4),
23212
23306
  // See callApi for the redirect-policy rationale.
23213
23307
  redirect: "error"
23214
23308
  });
23215
23309
  if (resp.ok) {
23216
- const info = await resp.json();
23310
+ const info = await resp.json().catch(() => ({}));
23217
23311
  console.error(`[goodmem-mcp] Connected to ${goodmemBaseUrl} (server ${info.version ?? "unknown"}).`);
23218
23312
  } else {
23219
23313
  const text = await resp.text();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pairsystems/goodmem-mcp",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "GoodMem MCP server \u2014 exposes GoodMem API as MCP tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",