@hydradb/mcp 0.0.1 → 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/README.md +103 -52
- package/dist/adapters.d.ts +37 -0
- package/dist/adapters.js +113 -0
- package/dist/adapters.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +55 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +5 -0
- package/dist/context.js.map +1 -0
- package/dist/descriptions.d.ts +114 -0
- package/dist/descriptions.js +149 -58
- package/dist/descriptions.js.map +1 -0
- package/dist/hydra/client.d.ts +146 -0
- package/dist/hydra/client.js +197 -0
- package/dist/hydra/client.js.map +1 -0
- package/dist/hydra/envelope.d.ts +15 -0
- package/dist/hydra/envelope.js +28 -0
- package/dist/hydra/envelope.js.map +1 -0
- package/dist/hydra/errors.d.ts +37 -0
- package/dist/hydra/errors.js +58 -0
- package/dist/hydra/errors.js.map +1 -0
- package/dist/hydra/index.d.ts +11 -0
- package/dist/hydra/index.js +11 -0
- package/dist/hydra/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.js +11 -3
- package/dist/logger.js.map +1 -0
- package/dist/server.d.ts +37 -0
- package/dist/server.js +363 -286
- package/dist/server.js.map +1 -0
- package/dist/tool-names.d.ts +19 -0
- package/dist/tool-names.js +40 -2
- package/dist/tool-names.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +5 -4
- package/dist/client.js +0 -144
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 {
|
|
4
|
+
import { toAddMemoryResponse, toMemoryList, toRecallResponse, toSourceList } from "./adapters.js";
|
|
5
|
+
import { resolveConfig } from "./config.js";
|
|
4
6
|
import { buildRecalledContext } from "./context.js";
|
|
5
|
-
import {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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:
|
|
50
|
+
version: SERVER_VERSION,
|
|
25
51
|
}, {
|
|
26
52
|
instructions: SERVER_INSTRUCTIONS,
|
|
27
53
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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 =
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
}
|