neuralmemory 1.6.1 → 1.7.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/src/tools.ts CHANGED
@@ -1,24 +1,22 @@
1
1
  /**
2
- * NeuralMemory tool definitions for OpenClaw.
2
+ * NeuralMemory dynamic tool proxy for OpenClaw.
3
3
  *
4
- * Each tool proxies to the MCP server via JSON-RPC.
4
+ * Fetches all available tools from the MCP server via `tools/list` and
5
+ * converts them into OpenClaw tool definitions. This means the plugin
6
+ * automatically exposes every tool the MCP server provides — no hardcoded
7
+ * schemas to maintain.
5
8
  *
6
- * Uses raw JSON Schema for parameters. Provider compatibility notes:
7
- * - `additionalProperties: false` required by OpenAI strict mode
8
- * - `number` instead of `integer` for Gemini compatibility
9
- * - No `maxLength`/`maxItems`/`minimum`/`maximum` some providers
10
- * reject schemas with constraint keywords; our MCP server validates
11
- *
12
- * Registers 6 core tools:
13
- * nmem_remember — Store a memory
14
- * nmem_recall — Query/search memories
15
- * nmem_context — Get recent context
16
- * nmem_todo — Quick TODO shortcut
17
- * nmem_stats — Brain statistics
18
- * nmem_health — Brain health diagnostics
9
+ * Provider compatibility:
10
+ * - Strips constraint keywords (`minimum`, `maximum`, `maxLength`,
11
+ * `maxItems`, `minLength`) that some providers reject
12
+ * - Adds `additionalProperties: false` on all object schemas for
13
+ * OpenAI strict mode
14
+ * - Ensures every object type has a `properties` field (required by
15
+ * Anthropic SDK validation)
16
+ * - Uses `number` instead of `integer` for Gemini compatibility
19
17
  */
20
18
 
21
- import type { NeuralMemoryMcpClient } from "./mcp-client.js";
19
+ import type { NeuralMemoryMcpClient, McpToolDefinition } from "./mcp-client.js";
22
20
 
23
21
  // ── Types ──────────────────────────────────────────────────
24
22
 
@@ -36,10 +34,104 @@ export type ToolDefinition = {
36
34
  readonly execute: (id: string, args: Record<string, unknown>) => Promise<unknown>;
37
35
  };
38
36
 
37
+ // ── Schema normalization ───────────────────────────────────
38
+
39
+ /** Keywords that some LLM providers reject in function schemas. */
40
+ const STRIP_KEYS = new Set([
41
+ "minimum",
42
+ "maximum",
43
+ "maxLength",
44
+ "minLength",
45
+ "maxItems",
46
+ "minItems",
47
+ "exclusiveMinimum",
48
+ "exclusiveMaximum",
49
+ ]);
50
+
51
+ /**
52
+ * Recursively normalize a JSON Schema node for provider compatibility:
53
+ * - Strip constraint keywords
54
+ * - Replace `integer` with `number` (Gemini compat)
55
+ * - Add `additionalProperties: false` to objects (OpenAI strict mode)
56
+ * - Ensure every object has `properties` (Anthropic SDK)
57
+ */
58
+ function normalizeSchema(node: unknown): unknown {
59
+ if (node === null || node === undefined || typeof node !== "object") {
60
+ return node;
61
+ }
62
+
63
+ if (Array.isArray(node)) {
64
+ return node.map(normalizeSchema);
65
+ }
66
+
67
+ const obj = node as Record<string, unknown>;
68
+ const result: Record<string, unknown> = {};
69
+
70
+ for (const [key, value] of Object.entries(obj)) {
71
+ if (STRIP_KEYS.has(key)) continue;
72
+
73
+ if (key === "type" && value === "integer") {
74
+ result[key] = "number";
75
+ } else if (key === "properties" && typeof value === "object" && value !== null) {
76
+ // Recurse into each property definition
77
+ const props: Record<string, unknown> = {};
78
+ for (const [propName, propSchema] of Object.entries(value as Record<string, unknown>)) {
79
+ props[propName] = normalizeSchema(propSchema);
80
+ }
81
+ result[key] = props;
82
+ } else if (key === "items" && typeof value === "object" && value !== null) {
83
+ result[key] = normalizeSchema(value);
84
+ } else if (
85
+ (key === "anyOf" || key === "oneOf" || key === "allOf") &&
86
+ Array.isArray(value)
87
+ ) {
88
+ result[key] = value.map(normalizeSchema);
89
+ } else {
90
+ result[key] = value;
91
+ }
92
+ }
93
+
94
+ // Ensure objects have `properties` and `additionalProperties`
95
+ if (result["type"] === "object") {
96
+ if (!("properties" in result) || result["properties"] === undefined) {
97
+ result["properties"] = {};
98
+ }
99
+ if (!("additionalProperties" in result)) {
100
+ result["additionalProperties"] = false;
101
+ }
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * Convert an MCP inputSchema into a provider-safe OpenClaw JsonSchema.
109
+ * Falls back to an empty-properties object if the schema is missing/invalid.
110
+ */
111
+ function toSafeSchema(inputSchema?: Record<string, unknown>): JsonSchema {
112
+ if (!inputSchema || typeof inputSchema !== "object") {
113
+ return { type: "object", properties: {}, additionalProperties: false };
114
+ }
115
+
116
+ const normalized = normalizeSchema(inputSchema) as Record<string, unknown>;
117
+
118
+ return {
119
+ type: "object",
120
+ properties: (normalized["properties"] ?? {}) as Record<string, unknown>,
121
+ ...(Array.isArray(normalized["required"]) && normalized["required"].length > 0
122
+ ? { required: normalized["required"] as string[] }
123
+ : {}),
124
+ additionalProperties: false,
125
+ };
126
+ }
127
+
39
128
  // ── Tool factory ───────────────────────────────────────────
40
129
 
41
- export function createTools(mcp: NeuralMemoryMcpClient): ToolDefinition[] {
42
- const call = async (
130
+ /**
131
+ * Create a tool call helper that auto-reconnects to MCP.
132
+ */
133
+ function makeCallFn(mcp: NeuralMemoryMcpClient) {
134
+ return async (
43
135
  toolName: string,
44
136
  args: Record<string, unknown>,
45
137
  ): Promise<unknown> => {
@@ -68,6 +160,43 @@ export function createTools(mcp: NeuralMemoryMcpClient): ToolDefinition[] {
68
160
  };
69
161
  }
70
162
  };
163
+ }
164
+
165
+ /**
166
+ * Convert a single MCP tool definition into an OpenClaw ToolDefinition.
167
+ */
168
+ function mcpToolToOpenClaw(
169
+ mcpTool: McpToolDefinition,
170
+ call: (name: string, args: Record<string, unknown>) => Promise<unknown>,
171
+ ): ToolDefinition {
172
+ return {
173
+ name: mcpTool.name,
174
+ description: mcpTool.description ?? `NeuralMemory tool: ${mcpTool.name}`,
175
+ parameters: toSafeSchema(mcpTool.inputSchema),
176
+ execute: (_id, args) => call(mcpTool.name, args),
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Fetch all tools from the MCP server and convert them to OpenClaw format.
182
+ * Must be called after MCP connection is established.
183
+ */
184
+ export async function createToolsFromMcp(
185
+ mcp: NeuralMemoryMcpClient,
186
+ ): Promise<ToolDefinition[]> {
187
+ const mcpTools = await mcp.listTools();
188
+ const call = makeCallFn(mcp);
189
+ return mcpTools.map((t) => mcpToolToOpenClaw(t, call));
190
+ }
191
+
192
+ /**
193
+ * Fallback: create minimal hardcoded tools if MCP tools/list fails.
194
+ * Ensures the plugin still works even if the MCP server is an older version.
195
+ */
196
+ export function createFallbackTools(
197
+ mcp: NeuralMemoryMcpClient,
198
+ ): ToolDefinition[] {
199
+ const call = makeCallFn(mcp);
71
200
 
72
201
  return [
73
202
  {
@@ -78,46 +207,27 @@ export function createTools(mcp: NeuralMemoryMcpClient): ToolDefinition[] {
78
207
  parameters: {
79
208
  type: "object",
80
209
  properties: {
81
- content: {
82
- type: "string",
83
- description: "The content to remember",
84
- },
210
+ content: { type: "string", description: "The content to remember" },
85
211
  type: {
86
212
  type: "string",
87
213
  enum: [
88
- "fact",
89
- "decision",
90
- "preference",
91
- "todo",
92
- "insight",
93
- "context",
94
- "instruction",
95
- "error",
96
- "workflow",
97
- "reference",
214
+ "fact", "decision", "preference", "todo", "insight",
215
+ "context", "instruction", "error", "workflow", "reference",
98
216
  ],
99
217
  description: "Memory type (auto-detected if not specified)",
100
218
  },
101
- priority: {
102
- type: "number",
103
- description: "Priority 0-10 (5=normal, 10=critical)",
104
- },
219
+ priority: { type: "number", description: "Priority 0-10 (5=normal, 10=critical)" },
105
220
  tags: {
106
221
  type: "array",
107
222
  items: { type: "string" },
108
223
  description: "Tags for categorization",
109
224
  },
110
- expires_days: {
111
- type: "number",
112
- description: "Days until memory expires (1-3650)",
113
- },
114
225
  },
115
226
  required: ["content"],
116
227
  additionalProperties: false,
117
228
  },
118
229
  execute: (_id, args) => call("nmem_remember", args),
119
230
  },
120
-
121
231
  {
122
232
  name: "nmem_recall",
123
233
  description:
@@ -126,96 +236,37 @@ export function createTools(mcp: NeuralMemoryMcpClient): ToolDefinition[] {
126
236
  parameters: {
127
237
  type: "object",
128
238
  properties: {
129
- query: {
130
- type: "string",
131
- description: "The query to search memories",
132
- },
133
- depth: {
134
- type: "number",
135
- description:
136
- "Search depth: 0=instant, 1=context, 2=habit, 3=deep",
137
- },
138
- max_tokens: {
139
- type: "number",
140
- description: "Maximum tokens in response (default: 500)",
141
- },
142
- min_confidence: {
143
- type: "number",
144
- description: "Minimum confidence threshold (0-1)",
145
- },
239
+ query: { type: "string", description: "The query to search memories" },
240
+ depth: { type: "number", description: "Search depth: 0=instant, 1=context, 2=habit, 3=deep" },
241
+ max_tokens: { type: "number", description: "Maximum tokens in response (default: 500)" },
146
242
  },
147
243
  required: ["query"],
148
244
  additionalProperties: false,
149
245
  },
150
246
  execute: (_id, args) => call("nmem_recall", args),
151
247
  },
152
-
153
248
  {
154
249
  name: "nmem_context",
155
- description:
156
- "Get recent context from NeuralMemory. Use this at the start of " +
157
- "tasks to inject relevant recent memories.",
250
+ description: "Get recent context from NeuralMemory.",
158
251
  parameters: {
159
252
  type: "object",
160
253
  properties: {
161
- limit: {
162
- type: "number",
163
- description: "Number of recent memories (default: 10, max: 200)",
164
- },
165
- fresh_only: {
166
- type: "boolean",
167
- description: "Only include memories less than 30 days old",
168
- },
254
+ limit: { type: "number", description: "Number of recent memories (default: 10)" },
169
255
  },
170
256
  additionalProperties: false,
171
257
  },
172
258
  execute: (_id, args) => call("nmem_context", args),
173
259
  },
174
-
175
- {
176
- name: "nmem_todo",
177
- description:
178
- "Quick shortcut to add a TODO memory with 30-day expiry.",
179
- parameters: {
180
- type: "object",
181
- properties: {
182
- task: {
183
- type: "string",
184
- description: "The task to remember",
185
- },
186
- priority: {
187
- type: "number",
188
- description: "Priority 0-10 (default: 5)",
189
- },
190
- },
191
- required: ["task"],
192
- additionalProperties: false,
193
- },
194
- execute: (_id, args) => call("nmem_todo", args),
195
- },
196
-
197
260
  {
198
261
  name: "nmem_stats",
199
- description:
200
- "Get brain statistics including memory counts and freshness.",
201
- parameters: {
202
- type: "object",
203
- properties: {},
204
- additionalProperties: false,
205
- },
262
+ description: "Get brain statistics including memory counts and freshness.",
263
+ parameters: { type: "object", properties: {}, additionalProperties: false },
206
264
  execute: (_id, args) => call("nmem_stats", args),
207
265
  },
208
-
209
266
  {
210
267
  name: "nmem_health",
211
- description:
212
- "Get brain health diagnostics including grade, purity score, " +
213
- "and recommendations.",
214
- parameters: {
215
- type: "object",
216
- properties: {},
217
- additionalProperties: false,
218
- },
268
+ description: "Get brain health diagnostics including grade and recommendations.",
269
+ parameters: { type: "object", properties: {}, additionalProperties: false },
219
270
  execute: (_id, args) => call("nmem_health", args),
220
271
  },
221
272
  ];