@hydradb/mcp 1.0.0 → 1.1.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/dist/server.js CHANGED
@@ -1,76 +1,85 @@
1
+ import { createRequire } from "node:module";
1
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import { z } from "zod";
3
- import { HydraDBClient } from "./client.js";
4
+ import { toAddMemoryResponse, toMemoryList, toRecallResponse, toSourceList } from "./adapters.js";
5
+ import { resolveConfig } from "./config.js";
4
6
  import { buildRecalledContext } from "./context.js";
5
- import { TOOL_DESCRIPTIONS, SERVER_INSTRUCTIONS } from "./descriptions.js";
7
+ import { SERVER_INSTRUCTIONS, TOOL_DESCRIPTIONS } from "./descriptions.js";
8
+ import { HydraDB } from "./hydra/index.js";
6
9
  import { logger } from "./logger.js";
7
- import { TOOL_NAMES } from "./tool-names.js";
8
- const DEFAULT_SUB_TENANT = "hydra-db-mcp";
9
- function getConfig() {
10
- const apiKey = process.env.HYDRA_DB_API_KEY;
11
- if (!apiKey) {
12
- throw new Error("HYDRA_DB_API_KEY environment variable is required");
13
- }
14
- const tenantId = process.env.HYDRA_DB_TENANT_ID;
15
- if (!tenantId) {
16
- throw new Error("HYDRA_DB_TENANT_ID environment variable is required");
17
- }
18
- const subTenantId = process.env.HYDRA_DB_SUB_TENANT_ID || DEFAULT_SUB_TENANT;
19
- return { apiKey, tenantId, subTenantId };
10
+ import { ALIAS_REPLACEMENTS, DEPRECATED_TOOL_NAMES, TOOL_NAMES } from "./tool-names.js";
11
+ // Host-owned default: silently attached to ingest so Hydra DB extracts the kind
12
+ // of personal context this server cares about. Injected here (not in the
13
+ // portable wrapper) because it is MCP-specific host behaviour.
14
+ const INGEST_INSTRUCTIONS = "Focus on extracting user preferences, habits, opinions, likes, dislikes, " +
15
+ "goals, and recurring themes. Capture any stated or implied personal context " +
16
+ "that would help personalise future interactions.";
17
+ // Read the version from package.json rather than repeating it here: the literal
18
+ // this replaces sat at 1.0.0 through the whole 1.x line, so every client saw
19
+ // stale version metadata. `../package.json` resolves to the package root from
20
+ // both `src/` (tsx) and `dist/` (published build).
21
+ const require = createRequire(import.meta.url);
22
+ const { version: SERVER_VERSION } = require("../package.json");
23
+ function textResult(text) {
24
+ return { content: [{ type: "text", text }] };
25
+ }
26
+ const turnSchema = z.object({
27
+ user: z.string().describe("The user's message"),
28
+ assistant: z.string().describe("The assistant's response"),
29
+ });
30
+ // Deprecated aliases emit exactly one stderr warning PER PROCESS naming the
31
+ // canonical replacement (CONTRACT §3). The dedupe state is module-scoped so the
32
+ // guarantee holds across multiple server instances in the same process, and is
33
+ // intentionally NOT routed through `logger` — the warning must surface
34
+ // regardless of HYDRA_DB_LOG_LEVEL.
35
+ const warnedAliases = new Set();
36
+ function warnDeprecatedAlias(name) {
37
+ if (warnedAliases.has(name))
38
+ return;
39
+ warnedAliases.add(name);
40
+ const replacement = ALIAS_REPLACEMENTS[name] ?? "a canonical tool";
41
+ console.error(`[hydradb-mcp] Tool "${name}" is deprecated and will be removed in a future major version; use "${replacement}" instead.`);
20
42
  }
21
- export function createHydraDBServer() {
43
+ /** Test-only: reset the once-per-process alias warning dedupe. */
44
+ export function __resetAliasWarnings() {
45
+ warnedAliases.clear();
46
+ }
47
+ export function createHydraDBServer(hydraOverride) {
22
48
  const server = new McpServer({
23
49
  name: "hydradb-mcp",
24
- version: "1.0.0",
50
+ version: SERVER_VERSION,
25
51
  }, {
26
52
  instructions: SERVER_INSTRUCTIONS,
27
53
  });
28
- const config = getConfig();
29
- const client = new HydraDBClient(config.apiKey, config.tenantId, config.subTenantId);
30
- // --- Search ---
31
- const desc = TOOL_DESCRIPTIONS[TOOL_NAMES.SEARCH];
32
- server.registerTool(TOOL_NAMES.SEARCH, {
33
- title: desc.title,
34
- description: desc.description,
35
- inputSchema: {
36
- query: z.string().describe(desc.params.query),
37
- max_results: z
38
- .number()
39
- .min(1)
40
- .max(50)
41
- .optional()
42
- .describe(desc.params.max_results),
43
- mode: z
44
- .enum(["fast", "thinking"])
45
- .optional()
46
- .describe(desc.params.mode),
47
- graph_context: z
48
- .boolean()
49
- .optional()
50
- .describe(desc.params.graph_context),
51
- },
52
- annotations: {
53
- readOnlyHint: true,
54
- openWorldHint: true,
55
- idempotentHint: true,
56
- },
57
- }, async (args) => {
58
- const { query, max_results, mode, graph_context, } = args;
59
- logger.debug(`${TOOL_NAMES.SEARCH}: "${query}"`);
60
- const res = await client.recall(query, {
61
- maxResults: max_results ?? 10,
62
- mode: mode ?? "thinking",
63
- graphContext: graph_context ?? true,
54
+ let hydra;
55
+ if (hydraOverride) {
56
+ hydra = hydraOverride;
57
+ }
58
+ else {
59
+ const config = resolveConfig();
60
+ hydra = new HydraDB({
61
+ token: config.apiKey,
62
+ database: config.database,
63
+ collection: config.collection,
64
+ ...(config.baseUrl != null ? { baseUrl: config.baseUrl } : {}),
64
65
  });
66
+ logger.info(`Hydra DB connected (database=${config.database}, collection=${config.collection})`);
67
+ }
68
+ // --- Handlers (shared by canonical tools and their deprecated aliases) ---
69
+ async function runQuery(args) {
70
+ logger.debug(`${TOOL_NAMES.QUERY}: "${args.query}"`);
71
+ const raw = await hydra.context.query({
72
+ query: args.query,
73
+ kind: "memory",
74
+ maxResults: args.max_results ?? 10,
75
+ mode: args.mode ?? "thinking",
76
+ graphContext: args.graph_context ?? true,
77
+ alpha: 0.8,
78
+ recencyBias: 0,
79
+ });
80
+ const res = toRecallResponse(raw);
65
81
  if (!res.chunks || res.chunks.length === 0) {
66
- return {
67
- content: [
68
- {
69
- type: "text",
70
- text: "No relevant memories found in Hydra DB.",
71
- },
72
- ],
73
- };
82
+ return textResult("No relevant memories found in Hydra DB.");
74
83
  }
75
84
  const contextStr = buildRecalledContext(res);
76
85
  const summary = res.chunks.slice(0, 10).map((c, i) => {
@@ -82,240 +91,308 @@ export function createHydraDBServer() {
82
91
  : c.chunk_content;
83
92
  return `${i + 1}. ${snippet}${score}`;
84
93
  });
85
- return {
86
- content: [
87
- {
88
- type: "text",
89
- text: `Found ${res.chunks.length} memories:\n\n${summary.join("\n")}\n\n---\nFull context:\n${contextStr}`,
90
- },
91
- ],
92
- };
93
- });
94
- // --- Store ---
95
- const storeDesc = TOOL_DESCRIPTIONS[TOOL_NAMES.STORE];
96
- server.registerTool(TOOL_NAMES.STORE, {
97
- title: storeDesc.title,
98
- description: storeDesc.description,
99
- inputSchema: {
100
- text: z.string().describe(storeDesc.params.text),
101
- title: z
102
- .string()
103
- .optional()
104
- .describe(storeDesc.params.title),
105
- source_id: z
106
- .string()
107
- .optional()
108
- .describe(storeDesc.params.source_id),
109
- infer: z
110
- .boolean()
111
- .optional()
112
- .describe(storeDesc.params.infer),
113
- is_markdown: z
114
- .boolean()
115
- .optional()
116
- .describe(storeDesc.params.is_markdown),
117
- },
118
- }, async (args) => {
119
- const { text, title, source_id, infer, is_markdown } = args;
120
- logger.debug(`${TOOL_NAMES.STORE}: "${text.slice(0, 50)}..."`);
121
- const res = await client.ingestText(text, {
122
- sourceId: source_id,
123
- title: title ?? "MCP Memory",
124
- infer: infer ?? true,
125
- isMarkdown: is_markdown ?? false,
94
+ return textResult(`Found ${res.chunks.length} memories:\n\n${summary.join("\n")}\n\n---\nFull context:\n${contextStr}`);
95
+ }
96
+ async function runStore(args) {
97
+ logger.debug(`${TOOL_NAMES.INGEST}: "${args.text.slice(0, 50)}..."`);
98
+ const raw = await hydra.context.ingest({
99
+ kind: "memory",
100
+ text: args.text,
101
+ title: args.title ?? "MCP Memory",
102
+ sourceId: args.source_id,
103
+ infer: args.infer ?? true,
104
+ isMarkdown: args.is_markdown ?? false,
105
+ customInstructions: INGEST_INSTRUCTIONS,
106
+ upsert: true,
126
107
  });
127
- const preview = text.length > 80 ? `${text.slice(0, 80)}...` : text;
128
- return {
129
- content: [
130
- {
131
- type: "text",
132
- text: `Saved to Hydra DB (${res.success_count} success, ${res.failed_count} failed): "${preview}"`,
133
- },
134
- ],
135
- };
136
- });
137
- // --- Ingest Conversation ---
138
- const ingestDesc = TOOL_DESCRIPTIONS[TOOL_NAMES.INGEST_CONVERSATION];
139
- const turnSchema = z.object({
140
- user: z.string().describe("The user's message"),
141
- assistant: z.string().describe("The assistant's response"),
142
- });
143
- server.registerTool(TOOL_NAMES.INGEST_CONVERSATION, {
144
- title: ingestDesc.title,
145
- description: ingestDesc.description,
146
- inputSchema: {
147
- turns: z
148
- .array(turnSchema)
149
- .min(1)
150
- .describe(ingestDesc.params.turns),
151
- source_id: z
152
- .string()
153
- .describe(ingestDesc.params.source_id),
154
- user_name: z
155
- .string()
156
- .optional()
157
- .describe(ingestDesc.params.user_name),
158
- },
159
- }, async (args) => {
160
- const { turns, source_id, user_name } = args;
161
- logger.debug(`${TOOL_NAMES.INGEST_CONVERSATION}: ${turns.length} turns -> ${source_id}`);
162
- const res = await client.ingestConversation(turns, source_id, user_name);
163
- return {
164
- content: [
165
- {
166
- type: "text",
167
- text: `Ingested ${turns.length} conversation turn(s) into Hydra DB (source: ${source_id}, success: ${res.success_count}, failed: ${res.failed_count})`,
168
- },
169
- ],
170
- };
171
- });
172
- // --- List Memories ---
173
- const listDesc = TOOL_DESCRIPTIONS[TOOL_NAMES.LIST_MEMORIES];
174
- server.registerTool(TOOL_NAMES.LIST_MEMORIES, {
175
- title: listDesc.title,
176
- description: listDesc.description,
177
- inputSchema: {},
178
- annotations: {
179
- readOnlyHint: true,
180
- idempotentHint: true,
181
- },
182
- }, async () => {
183
- logger.debug(TOOL_NAMES.LIST_MEMORIES);
184
- const res = await client.listMemories();
185
- const memories = res.user_memories ?? [];
108
+ const res = toAddMemoryResponse(raw);
109
+ const preview = args.text.length > 80 ? `${args.text.slice(0, 80)}...` : args.text;
110
+ return textResult(`Saved to Hydra DB (${res.success_count} success, ${res.failed_count} failed): "${preview}"`);
111
+ }
112
+ async function runIngestConversation(turns, sourceId, opts) {
113
+ logger.debug(`${TOOL_NAMES.INGEST}: ${turns.length} turns -> ${sourceId}`);
114
+ const raw = await hydra.context.ingest({
115
+ kind: "memory",
116
+ pairs: turns,
117
+ sourceId,
118
+ userName: opts?.userName ?? "User",
119
+ infer: opts?.infer ?? true,
120
+ title: opts?.title,
121
+ isMarkdown: opts?.isMarkdown,
122
+ customInstructions: INGEST_INSTRUCTIONS,
123
+ upsert: true,
124
+ });
125
+ const res = toAddMemoryResponse(raw);
126
+ return textResult(`Ingested ${turns.length} conversation turn(s) into Hydra DB (source: ${sourceId}, success: ${res.success_count}, failed: ${res.failed_count})`);
127
+ }
128
+ async function runListMemories() {
129
+ logger.debug(TOOL_NAMES.LIST);
130
+ const raw = await hydra.context.list({ kind: "memory" });
131
+ const memories = toMemoryList(raw);
186
132
  if (memories.length === 0) {
187
- return {
188
- content: [
189
- {
190
- type: "text",
191
- text: "No memories stored yet.",
192
- },
193
- ],
194
- };
133
+ return textResult("No memories stored yet.");
195
134
  }
196
135
  const lines = memories.map((m, i) => `${i + 1}. [${m.memory_id}] ${m.memory_content.slice(0, 150)}`);
197
- return {
198
- content: [
199
- {
200
- type: "text",
201
- text: `${memories.length} memories:\n\n${lines.join("\n")}`,
202
- },
203
- ],
204
- };
205
- });
206
- // --- Delete Memory ---
207
- const deleteDesc = TOOL_DESCRIPTIONS[TOOL_NAMES.DELETE_MEMORY];
208
- server.registerTool(TOOL_NAMES.DELETE_MEMORY, {
209
- title: deleteDesc.title,
210
- description: deleteDesc.description,
211
- inputSchema: {
212
- memory_id: z.string().describe(deleteDesc.params.memory_id),
213
- },
214
- }, async (args) => {
215
- const { memory_id } = args;
216
- logger.debug(`${TOOL_NAMES.DELETE_MEMORY}: ${memory_id}`);
217
- const res = await client.deleteMemory(memory_id);
218
- if (res.user_memory_deleted) {
219
- return {
220
- content: [
221
- {
222
- type: "text",
223
- text: `Deleted memory: ${memory_id}`,
224
- },
225
- ],
226
- };
227
- }
228
- return {
229
- content: [
230
- {
231
- type: "text",
232
- text: `Memory ${memory_id} was not found or already deleted.`,
233
- },
234
- ],
235
- };
236
- });
237
- // --- Fetch Content ---
238
- const fetchDesc = TOOL_DESCRIPTIONS[TOOL_NAMES.FETCH_CONTENT];
239
- server.registerTool(TOOL_NAMES.FETCH_CONTENT, {
240
- title: fetchDesc.title,
241
- description: fetchDesc.description,
242
- inputSchema: {
243
- source_id: z.string().describe(fetchDesc.params.source_id),
244
- mode: z
245
- .enum(["content", "url", "both"])
246
- .optional()
247
- .describe(fetchDesc.params.mode),
248
- },
249
- annotations: {
250
- readOnlyHint: true,
251
- idempotentHint: true,
252
- },
253
- }, async (args) => {
254
- const { source_id, mode } = args;
255
- logger.debug(`${TOOL_NAMES.FETCH_CONTENT}: ${source_id}`);
256
- const res = await client.fetchContent(source_id, mode ?? "content");
257
- if (!res.success || res.error) {
258
- return {
259
- content: [
260
- {
261
- type: "text",
262
- text: `Could not fetch source ${source_id}: ${res.error ?? "unknown error"}`,
263
- },
264
- ],
265
- };
266
- }
267
- const content = res.content ?? res.content_base64 ?? "(no text content)";
268
- return {
269
- content: [
270
- {
271
- type: "text",
272
- text: `Source: ${source_id}\n\n${content}`,
273
- },
274
- ],
275
- };
276
- });
277
- // --- List Sources ---
278
- const sourcesDesc = TOOL_DESCRIPTIONS[TOOL_NAMES.LIST_SOURCES];
279
- server.registerTool(TOOL_NAMES.LIST_SOURCES, {
280
- title: sourcesDesc.title,
281
- description: sourcesDesc.description,
282
- inputSchema: {
283
- source_ids: z
284
- .array(z.string())
285
- .optional()
286
- .describe(sourcesDesc.params.source_ids),
287
- },
288
- annotations: {
289
- readOnlyHint: true,
290
- idempotentHint: true,
291
- },
292
- }, async (args) => {
293
- const { source_ids } = args;
294
- logger.debug(TOOL_NAMES.LIST_SOURCES);
295
- const res = await client.listSources(source_ids);
296
- if (!res.sources || res.sources.length === 0) {
297
- return {
298
- content: [
299
- {
300
- type: "text",
301
- text: "No sources found.",
302
- },
303
- ],
304
- };
136
+ return textResult(`${memories.length} memories:\n\n${lines.join("\n")}`);
137
+ }
138
+ async function runListSources(args) {
139
+ logger.debug(TOOL_NAMES.LIST);
140
+ const raw = await hydra.context.list({
141
+ kind: "knowledge",
142
+ ids: args.source_ids,
143
+ });
144
+ const { sources, total } = toSourceList(raw);
145
+ if (sources.length === 0) {
146
+ return textResult("No sources found.");
305
147
  }
306
- const lines = res.sources.map((s, i) => {
148
+ const lines = sources.map((s, i) => {
307
149
  const title = s.title ? ` — ${s.title}` : "";
308
150
  const type = s.type ? ` (${s.type})` : "";
309
151
  return `${i + 1}. [${s.id}]${title}${type}`;
310
152
  });
311
- return {
312
- content: [
313
- {
314
- type: "text",
315
- text: `${res.total} sources:\n\n${lines.join("\n")}`,
316
- },
317
- ],
318
- };
153
+ return textResult(`${total} sources:\n\n${lines.join("\n")}`);
154
+ }
155
+ async function runInspect(args) {
156
+ logger.debug(`${TOOL_NAMES.INSPECT}: ${args.source_id}`);
157
+ const res = await hydra.context.inspect({
158
+ id: args.source_id,
159
+ mode: args.mode ?? "content",
160
+ });
161
+ // Soft failure: return a normal (non-error) text result, matching v1.
162
+ if (!res.success || res.error) {
163
+ return textResult(`Could not fetch source ${args.source_id}: ${res.error ?? "unknown error"}`);
164
+ }
165
+ const content = res.content ?? res.contentBase64 ?? "(no text content)";
166
+ return textResult(`Source: ${args.source_id}\n\n${content}`);
167
+ }
168
+ /**
169
+ * Three outcomes, not two. A delete that removed nothing is either the
170
+ * benign idempotent case (the server succeeded, there was nothing there) or
171
+ * a refusal (the server returned success:false and told us why). Collapsing
172
+ * both into "not found or already deleted" states a cause we did not
173
+ * observe, and it is the reassuring one: the caller is told their data is
174
+ * gone when the server just declined to remove it.
175
+ */
176
+ function deleteReport(kind, id, res, removed) {
177
+ const noun = kind === "knowledge" ? "source" : "memory";
178
+ if (removed) {
179
+ return textResult(`Deleted ${noun}: ${id}`);
180
+ }
181
+ if (res.success === false) {
182
+ const reason = deleteFailureReason(res);
183
+ return textResult(`Could NOT delete ${noun} ${id} — the server refused the request` +
184
+ `${reason ? `: ${reason}` : " and gave no reason"}. ` +
185
+ `The ${noun} has not been removed.`);
186
+ }
187
+ const Noun = noun.charAt(0).toUpperCase() + noun.slice(1);
188
+ return textResult(`${Noun} ${id} was not found or already deleted.`);
189
+ }
190
+ /** The server's own explanation, preferring the per-item error over the summary. */
191
+ function deleteFailureReason(res) {
192
+ const items = Array.isArray(res.results) ? res.results : [];
193
+ for (const item of items) {
194
+ if (item != null && typeof item === "object") {
195
+ const error = item.error;
196
+ if (typeof error === "string" && error !== "")
197
+ return error;
198
+ }
199
+ }
200
+ return res.message !== "" ? res.message : undefined;
201
+ }
202
+ async function runDelete(args) {
203
+ const kind = args.kind ?? "memory";
204
+ logger.debug(`${TOOL_NAMES.DELETE}: ${kind} ${args.id}`);
205
+ const res = await hydra.context.delete({ ids: [args.id], kind });
206
+ const removed = (res.userMemoryDeleted ?? 0) > 0 || (res.deletedCount ?? 0) > 0;
207
+ if (!removed) {
208
+ logger.warn(`${TOOL_NAMES.DELETE}: removed nothing for ${kind} ${args.id}`, { success: res.success, message: res.message, results: res.results });
209
+ }
210
+ return deleteReport(kind, args.id, res, removed);
211
+ }
212
+ // --- Registration helper ---
213
+ function register(name, inputSchema, handler, annotations) {
214
+ const desc = TOOL_DESCRIPTIONS[name];
215
+ const isDeprecated = DEPRECATED_TOOL_NAMES.includes(name);
216
+ const wrapped = isDeprecated
217
+ ? (args) => {
218
+ warnDeprecatedAlias(name);
219
+ return handler(args);
220
+ }
221
+ : handler;
222
+ server.registerTool(name, {
223
+ title: desc.title,
224
+ description: desc.description,
225
+ inputSchema: inputSchema,
226
+ ...(annotations ? { annotations } : {}),
227
+ }, wrapped);
228
+ }
229
+ // --- Input schemas ---
230
+ const querySchema = {
231
+ query: z.string().describe(TOOL_DESCRIPTIONS[TOOL_NAMES.QUERY].params.query),
232
+ max_results: z
233
+ .number()
234
+ .min(1)
235
+ .max(50)
236
+ .optional()
237
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.QUERY].params.max_results),
238
+ mode: z
239
+ .enum(["fast", "thinking"])
240
+ .optional()
241
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.QUERY].params.mode),
242
+ graph_context: z
243
+ .boolean()
244
+ .optional()
245
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.QUERY].params.graph_context),
246
+ };
247
+ const storeSchema = {
248
+ text: z.string().describe(TOOL_DESCRIPTIONS[TOOL_NAMES.STORE].params.text),
249
+ title: z
250
+ .string()
251
+ .optional()
252
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.STORE].params.title),
253
+ source_id: z
254
+ .string()
255
+ .optional()
256
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.STORE].params.source_id),
257
+ infer: z
258
+ .boolean()
259
+ .optional()
260
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.STORE].params.infer),
261
+ is_markdown: z
262
+ .boolean()
263
+ .optional()
264
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.STORE].params.is_markdown),
265
+ };
266
+ const ingestSchema = {
267
+ ...storeSchema,
268
+ // Canonical ingest accepts EITHER `text` or `turns`, so `text` is optional
269
+ // here (the `hydra_db_store` alias keeps it required).
270
+ text: z
271
+ .string()
272
+ .optional()
273
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.INGEST].params.text),
274
+ turns: z
275
+ .array(turnSchema)
276
+ .min(1)
277
+ .optional()
278
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.INGEST].params.turns),
279
+ user_name: z
280
+ .string()
281
+ .optional()
282
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.INGEST].params.user_name),
283
+ };
284
+ const conversationSchema = {
285
+ turns: z
286
+ .array(turnSchema)
287
+ .min(1)
288
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.INGEST_CONVERSATION].params.turns),
289
+ source_id: z
290
+ .string()
291
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.INGEST_CONVERSATION].params.source_id),
292
+ user_name: z
293
+ .string()
294
+ .optional()
295
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.INGEST_CONVERSATION].params.user_name),
296
+ };
297
+ const listSchema = {
298
+ kind: z
299
+ .enum(["memory", "knowledge"])
300
+ .optional()
301
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.LIST].params.kind),
302
+ source_ids: z
303
+ .array(z.string())
304
+ .optional()
305
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.LIST].params.source_ids),
306
+ };
307
+ const listSourcesSchema = {
308
+ source_ids: z
309
+ .array(z.string())
310
+ .optional()
311
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.LIST_SOURCES].params.source_ids),
312
+ };
313
+ const inspectSchema = {
314
+ source_id: z
315
+ .string()
316
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.INSPECT].params.source_id),
317
+ mode: z
318
+ .enum(["content", "url", "both"])
319
+ .optional()
320
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.INSPECT].params.mode),
321
+ };
322
+ const deleteSchema = {
323
+ id: z.string().describe(TOOL_DESCRIPTIONS[TOOL_NAMES.DELETE].params.id),
324
+ kind: z
325
+ .enum(["memory", "knowledge"])
326
+ .optional()
327
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.DELETE].params.kind),
328
+ };
329
+ const deleteMemorySchema = {
330
+ memory_id: z
331
+ .string()
332
+ .describe(TOOL_DESCRIPTIONS[TOOL_NAMES.DELETE_MEMORY].params.memory_id),
333
+ };
334
+ const readOnly = { readOnlyHint: true, idempotentHint: true };
335
+ const searchAnnotations = {
336
+ readOnlyHint: true,
337
+ openWorldHint: true,
338
+ idempotentHint: true,
339
+ };
340
+ // --- Canonical tools ---
341
+ register(TOOL_NAMES.QUERY, querySchema, (args) => runQuery(args), searchAnnotations);
342
+ register(TOOL_NAMES.INGEST, ingestSchema, async (args) => {
343
+ const a = args;
344
+ const hasTurns = a.turns != null && a.turns.length > 0;
345
+ // `text` and `turns` are mutually exclusive — reject rather than silently
346
+ // dropping one (the documented "exactly one" contract).
347
+ if (hasTurns && a.text != null) {
348
+ throw new Error(`${TOOL_NAMES.INGEST} accepts either \`text\` (a note) or \`turns\` (a conversation), not both.`);
349
+ }
350
+ if (a.turns != null && a.turns.length > 0) {
351
+ const sourceId = a.source_id ?? `mcp-conversation-${Date.now()}`;
352
+ // Forward every option the canonical schema accepts so none is
353
+ // silently dropped on the conversation path.
354
+ return runIngestConversation(a.turns, sourceId, {
355
+ userName: a.user_name,
356
+ infer: a.infer,
357
+ title: a.title,
358
+ isMarkdown: a.is_markdown,
359
+ });
360
+ }
361
+ if (a.text != null) {
362
+ return runStore({
363
+ text: a.text,
364
+ title: a.title,
365
+ source_id: a.source_id,
366
+ infer: a.infer,
367
+ is_markdown: a.is_markdown,
368
+ });
369
+ }
370
+ throw new Error(`${TOOL_NAMES.INGEST} requires either \`text\` (a note) or \`turns\` (a conversation).`);
371
+ });
372
+ register(TOOL_NAMES.LIST, listSchema, (args) => {
373
+ const a = args;
374
+ if ((a.kind ?? "memory") === "knowledge") {
375
+ return runListSources({ source_ids: a.source_ids });
376
+ }
377
+ return runListMemories();
378
+ }, readOnly);
379
+ register(TOOL_NAMES.INSPECT, inspectSchema, (args) => runInspect(args), readOnly);
380
+ register(TOOL_NAMES.DELETE, deleteSchema, (args) => runDelete(args));
381
+ // --- Deprecated aliases ---
382
+ register(TOOL_NAMES.SEARCH, querySchema, (args) => runQuery(args), searchAnnotations);
383
+ register(TOOL_NAMES.STORE, storeSchema, (args) => runStore(args));
384
+ register(TOOL_NAMES.INGEST_CONVERSATION, conversationSchema, (args) => {
385
+ const a = args;
386
+ // The deprecated alias keeps its historical shape (user_name only; infer
387
+ // on, no title/markdown). The canonical hydradb_ingest forwards the rest.
388
+ return runIngestConversation(a.turns, a.source_id, { userName: a.user_name });
389
+ });
390
+ register(TOOL_NAMES.LIST_MEMORIES, {}, () => runListMemories(), readOnly);
391
+ register(TOOL_NAMES.LIST_SOURCES, listSourcesSchema, (args) => runListSources(args), readOnly);
392
+ register(TOOL_NAMES.FETCH_CONTENT, inspectSchema, (args) => runInspect(args), readOnly);
393
+ register(TOOL_NAMES.DELETE_MEMORY, deleteMemorySchema, (args) => {
394
+ const { memory_id } = args;
395
+ return runDelete({ id: memory_id, kind: "memory" });
319
396
  });
320
397
  return server.server;
321
398
  }