@pairsystems/goodmem-mcp 0.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/index.d.ts +2 -0
- package/dist/index.js +965 -0
- package/package.json +24 -0
- package/src/index.ts +1105 -0
- package/tsconfig.json +16 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// DO NOT EDIT — generated by _client_gen/mcp/emitter.py from api_ir.json.
|
|
3
|
+
// Regenerate with: cd clients/v2 && python -c "from _client_gen.mcp.emitter import generate_mcp; generate_mcp()"
|
|
4
|
+
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Configuration — session overrides take precedence over env vars
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
let goodmemBaseUrl = process.env.GOODMEM_BASE_URL || "";
|
|
14
|
+
let goodmemApiKey = process.env.GOODMEM_API_KEY || "";
|
|
15
|
+
|
|
16
|
+
function isConfigured(): boolean {
|
|
17
|
+
return goodmemBaseUrl !== "" && goodmemApiKey !== "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// HTTP client
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
async function callApi(
|
|
25
|
+
method: string,
|
|
26
|
+
path: string,
|
|
27
|
+
body?: unknown,
|
|
28
|
+
query?: Record<string, string>,
|
|
29
|
+
): Promise<unknown> {
|
|
30
|
+
if (!isConfigured()) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"GoodMem is not configured. Call the goodmem_configure tool first with "
|
|
33
|
+
+ "your base_url and api_key, or set GOODMEM_BASE_URL and GOODMEM_API_KEY "
|
|
34
|
+
+ "environment variables."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const url = new URL(path, goodmemBaseUrl);
|
|
38
|
+
if (query) {
|
|
39
|
+
for (const [k, v] of Object.entries(query)) {
|
|
40
|
+
if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, v);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const headers: Record<string, string> = {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
"x-api-key": goodmemApiKey,
|
|
46
|
+
};
|
|
47
|
+
const resp = await fetch(url.toString(), {
|
|
48
|
+
method,
|
|
49
|
+
headers,
|
|
50
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
51
|
+
});
|
|
52
|
+
if (!resp.ok) {
|
|
53
|
+
const text = await resp.text();
|
|
54
|
+
throw new Error(`GoodMem API error ${resp.status}: ${text}`);
|
|
55
|
+
}
|
|
56
|
+
const ct = resp.headers.get("content-type") || "";
|
|
57
|
+
if (ct.includes("application/json")) {
|
|
58
|
+
return resp.json();
|
|
59
|
+
}
|
|
60
|
+
return resp.text();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function callApiNdjson(
|
|
64
|
+
method: string,
|
|
65
|
+
path: string,
|
|
66
|
+
body?: unknown,
|
|
67
|
+
query?: Record<string, string>,
|
|
68
|
+
): Promise<unknown[]> {
|
|
69
|
+
if (!isConfigured()) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"GoodMem is not configured. Call the goodmem_configure tool first with "
|
|
72
|
+
+ "your base_url and api_key, or set GOODMEM_BASE_URL and GOODMEM_API_KEY "
|
|
73
|
+
+ "environment variables."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const url = new URL(path, goodmemBaseUrl);
|
|
77
|
+
if (query) {
|
|
78
|
+
for (const [k, v] of Object.entries(query)) {
|
|
79
|
+
if (v !== undefined && v !== null && v !== "") url.searchParams.set(k, v);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const headers: Record<string, string> = {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"Accept": "application/x-ndjson",
|
|
85
|
+
"x-api-key": goodmemApiKey,
|
|
86
|
+
};
|
|
87
|
+
const resp = await fetch(url.toString(), {
|
|
88
|
+
method,
|
|
89
|
+
headers,
|
|
90
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
91
|
+
});
|
|
92
|
+
if (!resp.ok) {
|
|
93
|
+
const text = await resp.text();
|
|
94
|
+
throw new Error(`GoodMem API error ${resp.status}: ${text}`);
|
|
95
|
+
}
|
|
96
|
+
const text = await resp.text();
|
|
97
|
+
return text.split("\n").filter(Boolean).map(line => JSON.parse(line));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// MCP Server
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
const server = new McpServer({
|
|
105
|
+
name: "goodmem",
|
|
106
|
+
version: "0.1.0",
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Configuration tool — set credentials at runtime from chat
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
server.tool(
|
|
114
|
+
"goodmem_configure",
|
|
115
|
+
"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.",
|
|
116
|
+
{
|
|
117
|
+
base_url: z.string().describe("GoodMem server URL (e.g., https://your-server.example.com)"),
|
|
118
|
+
api_key: z.string().describe("GoodMem API key (starts with gm_)"),
|
|
119
|
+
},
|
|
120
|
+
async (args) => {
|
|
121
|
+
goodmemBaseUrl = args.base_url;
|
|
122
|
+
goodmemApiKey = args.api_key;
|
|
123
|
+
// Verify connection
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL("/v1/system/info", goodmemBaseUrl);
|
|
126
|
+
const resp = await fetch(url.toString(), {
|
|
127
|
+
headers: { "x-api-key": goodmemApiKey },
|
|
128
|
+
});
|
|
129
|
+
if (!resp.ok) {
|
|
130
|
+
const text = await resp.text();
|
|
131
|
+
return { content: [{ type: "text" as const, text: `Credentials saved but server returned error ${resp.status}: ${text}. Check your base_url and api_key.` }] };
|
|
132
|
+
}
|
|
133
|
+
const info = await resp.json() as Record<string, unknown>;
|
|
134
|
+
return { content: [{ type: "text" as const, text: `Connected to GoodMem at ${goodmemBaseUrl}. Server version: ${(info as Record<string, unknown>).version ?? "unknown"}.` }] };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return { content: [{ type: "text" as const, text: `Credentials saved but could not connect to ${goodmemBaseUrl}: ${err}. Check your base_url.` }] };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
function registerTools() {
|
|
142
|
+
// === admin ===
|
|
143
|
+
|
|
144
|
+
server.tool(
|
|
145
|
+
"goodmem_admin_background_jobs_purge",
|
|
146
|
+
"Purge completed background jobs",
|
|
147
|
+
{
|
|
148
|
+
older_than: z.string().describe("ISO-8601 timestamp cutoff; only terminal jobs older than this instant are eligible.").optional(),
|
|
149
|
+
statuses: z.array(z.string()).describe("Optional terminal background job statuses to target for purging.").optional(),
|
|
150
|
+
dry_run: z.boolean().describe("If true, report purge counts without deleting any rows.").optional(),
|
|
151
|
+
limit: z.number().int().describe("Maximum number of jobs to purge in this request.").optional()
|
|
152
|
+
},
|
|
153
|
+
async (args) => {
|
|
154
|
+
const body: Record<string, unknown> = {};
|
|
155
|
+
if (args.older_than !== undefined) body["olderThan"] = args.older_than;
|
|
156
|
+
if (args.statuses !== undefined) body["statuses"] = args.statuses;
|
|
157
|
+
if (args.dry_run !== undefined) body["dryRun"] = args.dry_run;
|
|
158
|
+
if (args.limit !== undefined) body["limit"] = args.limit;
|
|
159
|
+
const result = await callApi("POST", `/v1/admin/background-jobs:purge`, body);
|
|
160
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
server.tool(
|
|
165
|
+
"goodmem_admin_drain",
|
|
166
|
+
"Request the server to enter drain mode",
|
|
167
|
+
{
|
|
168
|
+
timeout_sec: z.number().int().describe("Maximum seconds to wait for the server to quiesce before returning.").optional(),
|
|
169
|
+
reason: z.string().describe("Human-readable reason for initiating drain mode.").optional(),
|
|
170
|
+
wait_for_quiesce: z.boolean().describe("If true, wait for in-flight requests to complete and the server to reach QUIESCED before responding.").optional()
|
|
171
|
+
},
|
|
172
|
+
async (args) => {
|
|
173
|
+
const body: Record<string, unknown> = {};
|
|
174
|
+
if (args.timeout_sec !== undefined) body["timeoutSec"] = args.timeout_sec;
|
|
175
|
+
if (args.reason !== undefined) body["reason"] = args.reason;
|
|
176
|
+
if (args.wait_for_quiesce !== undefined) body["waitForQuiesce"] = args.wait_for_quiesce;
|
|
177
|
+
const result = await callApi("POST", `/v1/admin:drain`, body);
|
|
178
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
server.tool(
|
|
183
|
+
"goodmem_admin_license_reload",
|
|
184
|
+
"Reload the active license from disk",
|
|
185
|
+
{},
|
|
186
|
+
async (args) => {
|
|
187
|
+
const result = await callApi("POST", `/v1/admin/license:reload`, undefined);
|
|
188
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
// === apikeys ===
|
|
195
|
+
|
|
196
|
+
server.tool(
|
|
197
|
+
"goodmem_apikeys_create",
|
|
198
|
+
"Create a new API key",
|
|
199
|
+
{
|
|
200
|
+
labels: z.record(z.string(), z.string()).describe("Key-value pairs of metadata associated with the API key. Used for organization and filtering.").optional(),
|
|
201
|
+
expires_at: z.number().int().describe("Expiration timestamp in milliseconds since epoch. If not provided, the key does not expire.").optional(),
|
|
202
|
+
api_key_id: z.string().describe("Optional client-provided UUID for idempotent creation. If not provided, server generates a new UUID. Returns ALREADY_EXISTS if ID is already in use.").optional()
|
|
203
|
+
},
|
|
204
|
+
async (args) => {
|
|
205
|
+
const body: Record<string, unknown> = {};
|
|
206
|
+
if (args.labels !== undefined) body["labels"] = args.labels;
|
|
207
|
+
if (args.expires_at !== undefined) body["expiresAt"] = args.expires_at;
|
|
208
|
+
if (args.api_key_id !== undefined) body["apiKeyId"] = args.api_key_id;
|
|
209
|
+
const result = await callApi("POST", `/v1/apikeys`, body);
|
|
210
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
server.tool(
|
|
215
|
+
"goodmem_apikeys_delete",
|
|
216
|
+
"Delete an API key",
|
|
217
|
+
{
|
|
218
|
+
id: z.string().describe("The unique identifier of the API key to delete")
|
|
219
|
+
},
|
|
220
|
+
async (args) => {
|
|
221
|
+
await callApi("DELETE", `/v1/apikeys/${encodeURIComponent(String(args.id))}`, undefined);
|
|
222
|
+
return { content: [{ type: "text" as const, text: "Deleted successfully." }] };
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
server.tool(
|
|
227
|
+
"goodmem_apikeys_list",
|
|
228
|
+
"List API keys",
|
|
229
|
+
{},
|
|
230
|
+
async (args) => {
|
|
231
|
+
const result = await callApi("GET", `/v1/apikeys`, undefined);
|
|
232
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
server.tool(
|
|
237
|
+
"goodmem_apikeys_update",
|
|
238
|
+
"Update an API key",
|
|
239
|
+
{
|
|
240
|
+
id: z.string().describe("The unique identifier of the API key to update"),
|
|
241
|
+
status: z.string().describe("New status for the API key. Allowed values: ACTIVE, INACTIVE.").optional(),
|
|
242
|
+
replace_labels: z.record(z.string(), z.string()).describe("Replace all existing labels with this set. Mutually exclusive with mergeLabels.").optional(),
|
|
243
|
+
merge_labels: z.record(z.string(), z.string()).describe("Merge these labels with existing ones. Mutually exclusive with replaceLabels.").optional()
|
|
244
|
+
},
|
|
245
|
+
async (args) => {
|
|
246
|
+
const body: Record<string, unknown> = {};
|
|
247
|
+
if (args.status !== undefined) body["status"] = args.status;
|
|
248
|
+
if (args.replace_labels !== undefined) body["replaceLabels"] = args.replace_labels;
|
|
249
|
+
if (args.merge_labels !== undefined) body["mergeLabels"] = args.merge_labels;
|
|
250
|
+
const result = await callApi("PUT", `/v1/apikeys/${encodeURIComponent(String(args.id))}`, body);
|
|
251
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
// === embedders ===
|
|
258
|
+
|
|
259
|
+
server.tool(
|
|
260
|
+
"goodmem_embedders_create",
|
|
261
|
+
"Create a new embedder",
|
|
262
|
+
{
|
|
263
|
+
display_name: z.string().describe("User-facing name of the embedder"),
|
|
264
|
+
description: z.string().describe("Description of the embedder").optional(),
|
|
265
|
+
provider_type: z.enum(["OPENAI", "VLLM", "TEI", "LLAMA_CPP", "VOYAGE", "COHERE", "JINA"]).describe("Type of embedding provider"),
|
|
266
|
+
endpoint_url: z.string().describe("API endpoint URL"),
|
|
267
|
+
api_path: z.string().describe("API path for embeddings request (defaults: Cohere /v2/embed, TEI /embed, others /embeddings)").optional(),
|
|
268
|
+
model_identifier: z.string().describe("Model identifier"),
|
|
269
|
+
dimensionality: z.number().int().describe("Output vector dimensions"),
|
|
270
|
+
distribution_type: z.enum(["DENSE", "SPARSE"]).describe("Type of embedding distribution (DENSE or SPARSE)"),
|
|
271
|
+
max_sequence_length: z.number().int().describe("Maximum input sequence length").optional(),
|
|
272
|
+
supported_modalities: z.array(z.enum(["TEXT", "IMAGE", "AUDIO", "VIDEO"])).describe("Supported content modalities (defaults to TEXT if not provided)").optional(),
|
|
273
|
+
credentials: z.object({
|
|
274
|
+
kind: z.enum(["CREDENTIAL_KIND_UNSPECIFIED", "CREDENTIAL_KIND_API_KEY", "CREDENTIAL_KIND_GCP_ADC"]).describe("Selected credential strategy"),
|
|
275
|
+
apiKey: z.any().describe("Configuration when kind is CREDENTIAL_KIND_API_KEY").optional(),
|
|
276
|
+
gcpAdc: z.any().describe("Configuration when kind is CREDENTIAL_KIND_GCP_ADC").optional(),
|
|
277
|
+
labels: z.record(z.string(), z.string()).describe("Optional annotations to aid operators (e.g., \"owner=vertex\")").optional()
|
|
278
|
+
}).passthrough().describe("Structured credential payload describing how to authenticate with the provider. Required for SaaS providers such as COHERE, JINA, and VOYAGE; optional for local or proxy providers.").optional(),
|
|
279
|
+
labels: z.record(z.string(), z.string()).describe("User-defined labels for categorization").optional(),
|
|
280
|
+
version: z.string().describe("Version information").optional(),
|
|
281
|
+
monitoring_endpoint: z.string().describe("Monitoring endpoint URL").optional(),
|
|
282
|
+
owner_id: z.string().describe("Optional owner ID. If not provided, derived from the authentication context. Requires CREATE_EMBEDDER_ANY permission if specified.").optional(),
|
|
283
|
+
embedder_id: z.string().describe("Optional client-provided UUID for idempotent creation. If not provided, server generates a new UUID. Returns ALREADY_EXISTS if ID is already in use.").optional()
|
|
284
|
+
},
|
|
285
|
+
async (args) => {
|
|
286
|
+
const body: Record<string, unknown> = {};
|
|
287
|
+
if (args.display_name !== undefined) body["displayName"] = args.display_name;
|
|
288
|
+
if (args.description !== undefined) body["description"] = args.description;
|
|
289
|
+
if (args.provider_type !== undefined) body["providerType"] = args.provider_type;
|
|
290
|
+
if (args.endpoint_url !== undefined) body["endpointUrl"] = args.endpoint_url;
|
|
291
|
+
if (args.api_path !== undefined) body["apiPath"] = args.api_path;
|
|
292
|
+
if (args.model_identifier !== undefined) body["modelIdentifier"] = args.model_identifier;
|
|
293
|
+
if (args.dimensionality !== undefined) body["dimensionality"] = args.dimensionality;
|
|
294
|
+
if (args.distribution_type !== undefined) body["distributionType"] = args.distribution_type;
|
|
295
|
+
if (args.max_sequence_length !== undefined) body["maxSequenceLength"] = args.max_sequence_length;
|
|
296
|
+
if (args.supported_modalities !== undefined) body["supportedModalities"] = args.supported_modalities;
|
|
297
|
+
if (args.credentials !== undefined) body["credentials"] = args.credentials;
|
|
298
|
+
if (args.labels !== undefined) body["labels"] = args.labels;
|
|
299
|
+
if (args.version !== undefined) body["version"] = args.version;
|
|
300
|
+
if (args.monitoring_endpoint !== undefined) body["monitoringEndpoint"] = args.monitoring_endpoint;
|
|
301
|
+
if (args.owner_id !== undefined) body["ownerId"] = args.owner_id;
|
|
302
|
+
if (args.embedder_id !== undefined) body["embedderId"] = args.embedder_id;
|
|
303
|
+
const result = await callApi("POST", `/v1/embedders`, body);
|
|
304
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
server.tool(
|
|
309
|
+
"goodmem_embedders_delete",
|
|
310
|
+
"Delete an embedder",
|
|
311
|
+
{
|
|
312
|
+
id: z.string().describe("The unique identifier of the embedder to delete")
|
|
313
|
+
},
|
|
314
|
+
async (args) => {
|
|
315
|
+
await callApi("DELETE", `/v1/embedders/${encodeURIComponent(String(args.id))}`, undefined);
|
|
316
|
+
return { content: [{ type: "text" as const, text: "Deleted successfully." }] };
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
server.tool(
|
|
321
|
+
"goodmem_embedders_get",
|
|
322
|
+
"Get an embedder by ID",
|
|
323
|
+
{
|
|
324
|
+
id: z.string().describe("The unique identifier of the embedder to retrieve")
|
|
325
|
+
},
|
|
326
|
+
async (args) => {
|
|
327
|
+
const result = await callApi("GET", `/v1/embedders/${encodeURIComponent(String(args.id))}`, undefined);
|
|
328
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
server.tool(
|
|
333
|
+
"goodmem_embedders_list",
|
|
334
|
+
"List embedders",
|
|
335
|
+
{
|
|
336
|
+
owner_id: z.string().describe("Filter embedders by owner ID. With LIST_EMBEDDER_ANY permission, omitting this shows all accessible embedders; providing it filters by that owner. With LIST_EMBEDDER_OWN permission, only your own embedders are shown regardless of this parameter.").optional(),
|
|
337
|
+
provider_type: z.enum(["OPENAI", "VLLM", "TEI", "LLAMA_CPP", "VOYAGE", "COHERE", "JINA"]).describe("Filter embedders by provider type. Allowed values match the ProviderType schema.").optional()
|
|
338
|
+
},
|
|
339
|
+
async (args) => {
|
|
340
|
+
const query: Record<string, string> = {};
|
|
341
|
+
if (args.owner_id !== undefined) query["ownerId"] = String(args.owner_id);
|
|
342
|
+
if (args.provider_type !== undefined) query["providerType"] = String(args.provider_type);
|
|
343
|
+
const result = await callApi("GET", `/v1/embedders`, undefined, query);
|
|
344
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
server.tool(
|
|
349
|
+
"goodmem_embedders_update",
|
|
350
|
+
"Update an embedder",
|
|
351
|
+
{
|
|
352
|
+
id: z.string().describe("The unique identifier of the embedder to update"),
|
|
353
|
+
display_name: z.string().describe("User-facing name of the embedder").optional(),
|
|
354
|
+
description: z.string().describe("Description of the embedder").optional(),
|
|
355
|
+
endpoint_url: z.string().describe("API endpoint URL").optional(),
|
|
356
|
+
api_path: z.string().describe("API path for embeddings request").optional(),
|
|
357
|
+
model_identifier: z.string().describe("Model identifier").optional(),
|
|
358
|
+
dimensionality: z.number().int().describe("Output vector dimensions").optional(),
|
|
359
|
+
max_sequence_length: z.number().int().describe("Maximum input sequence length").optional(),
|
|
360
|
+
supported_modalities: z.array(z.enum(["TEXT", "IMAGE", "AUDIO", "VIDEO"])).describe("Supported content modalities").optional(),
|
|
361
|
+
credentials: z.object({
|
|
362
|
+
kind: z.enum(["CREDENTIAL_KIND_UNSPECIFIED", "CREDENTIAL_KIND_API_KEY", "CREDENTIAL_KIND_GCP_ADC"]).describe("Selected credential strategy"),
|
|
363
|
+
apiKey: z.any().describe("Configuration when kind is CREDENTIAL_KIND_API_KEY").optional(),
|
|
364
|
+
gcpAdc: z.any().describe("Configuration when kind is CREDENTIAL_KIND_GCP_ADC").optional(),
|
|
365
|
+
labels: z.record(z.string(), z.string()).describe("Optional annotations to aid operators (e.g., \"owner=vertex\")").optional()
|
|
366
|
+
}).passthrough().describe("Structured credential payload describing how to authenticate with the provider").optional(),
|
|
367
|
+
replace_labels: z.record(z.string(), z.string()).describe("Replace all existing labels with these (mutually exclusive with mergeLabels)").optional(),
|
|
368
|
+
merge_labels: z.record(z.string(), z.string()).describe("Merge these labels with existing ones (mutually exclusive with replaceLabels)").optional(),
|
|
369
|
+
version: z.string().describe("Version information").optional(),
|
|
370
|
+
monitoring_endpoint: z.string().describe("Monitoring endpoint URL").optional()
|
|
371
|
+
},
|
|
372
|
+
async (args) => {
|
|
373
|
+
const body: Record<string, unknown> = {};
|
|
374
|
+
if (args.display_name !== undefined) body["displayName"] = args.display_name;
|
|
375
|
+
if (args.description !== undefined) body["description"] = args.description;
|
|
376
|
+
if (args.endpoint_url !== undefined) body["endpointUrl"] = args.endpoint_url;
|
|
377
|
+
if (args.api_path !== undefined) body["apiPath"] = args.api_path;
|
|
378
|
+
if (args.model_identifier !== undefined) body["modelIdentifier"] = args.model_identifier;
|
|
379
|
+
if (args.dimensionality !== undefined) body["dimensionality"] = args.dimensionality;
|
|
380
|
+
if (args.max_sequence_length !== undefined) body["maxSequenceLength"] = args.max_sequence_length;
|
|
381
|
+
if (args.supported_modalities !== undefined) body["supportedModalities"] = args.supported_modalities;
|
|
382
|
+
if (args.credentials !== undefined) body["credentials"] = args.credentials;
|
|
383
|
+
if (args.replace_labels !== undefined) body["replaceLabels"] = args.replace_labels;
|
|
384
|
+
if (args.merge_labels !== undefined) body["mergeLabels"] = args.merge_labels;
|
|
385
|
+
if (args.version !== undefined) body["version"] = args.version;
|
|
386
|
+
if (args.monitoring_endpoint !== undefined) body["monitoringEndpoint"] = args.monitoring_endpoint;
|
|
387
|
+
const result = await callApi("PUT", `/v1/embedders/${encodeURIComponent(String(args.id))}`, body);
|
|
388
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
// === llms ===
|
|
395
|
+
|
|
396
|
+
server.tool(
|
|
397
|
+
"goodmem_llms_create",
|
|
398
|
+
"Create a new LLM",
|
|
399
|
+
{
|
|
400
|
+
display_name: z.string().describe("User-facing name of the LLM"),
|
|
401
|
+
description: z.string().describe("Description of the LLM").optional(),
|
|
402
|
+
provider_type: z.enum(["OPENAI", "LITELLM_PROXY", "OPEN_ROUTER", "VLLM", "OLLAMA", "LLAMA_CPP", "CUSTOM_OPENAI_COMPATIBLE"]).describe("Type of LLM provider"),
|
|
403
|
+
endpoint_url: z.string().describe("API endpoint base URL (OpenAI-compatible base, typically ends with /v1)"),
|
|
404
|
+
api_path: z.string().describe("API path for chat/completions request (defaults to /chat/completions if not provided)").optional(),
|
|
405
|
+
model_identifier: z.string().describe("Model identifier"),
|
|
406
|
+
supported_modalities: z.array(z.enum(["TEXT", "IMAGE", "AUDIO", "VIDEO"])).describe("Supported content modalities (defaults to TEXT if not provided)").optional(),
|
|
407
|
+
credentials: z.object({
|
|
408
|
+
kind: z.enum(["CREDENTIAL_KIND_UNSPECIFIED", "CREDENTIAL_KIND_API_KEY", "CREDENTIAL_KIND_GCP_ADC"]).describe("Selected credential strategy"),
|
|
409
|
+
apiKey: z.any().describe("Configuration when kind is CREDENTIAL_KIND_API_KEY").optional(),
|
|
410
|
+
gcpAdc: z.any().describe("Configuration when kind is CREDENTIAL_KIND_GCP_ADC").optional(),
|
|
411
|
+
labels: z.record(z.string(), z.string()).describe("Optional annotations to aid operators (e.g., \"owner=vertex\")").optional()
|
|
412
|
+
}).passthrough().describe("Structured credential payload describing how to authenticate with the provider. Omit for deployments that do not require credentials.").optional(),
|
|
413
|
+
labels: z.record(z.string(), z.string()).describe("User-defined labels for categorization").optional(),
|
|
414
|
+
version: z.string().describe("Version information").optional(),
|
|
415
|
+
monitoring_endpoint: z.string().describe("Monitoring endpoint URL").optional(),
|
|
416
|
+
capabilities: z.object({
|
|
417
|
+
supportsChat: z.boolean().describe("Supports conversational/chat completion format with message roles").optional(),
|
|
418
|
+
supportsCompletion: z.boolean().describe("Supports raw text completion with prompt continuation").optional(),
|
|
419
|
+
supportsFunctionCalling: z.boolean().describe("Supports function/tool calling with structured responses").optional(),
|
|
420
|
+
supportsSystemMessages: z.boolean().describe("Supports system prompts to define model behavior and context").optional(),
|
|
421
|
+
supportsStreaming: z.boolean().describe("Supports real-time token streaming during generation").optional(),
|
|
422
|
+
supportsSamplingParameters: z.boolean().describe("Supports sampling parameters like temperature, top_p, and top_k for generation control").optional()
|
|
423
|
+
}).passthrough().describe("LLM capabilities defining supported features and modes. Optional - server infers capabilities from model identifier if not provided.").optional(),
|
|
424
|
+
default_sampling_params: z.object({
|
|
425
|
+
maxTokens: z.number().int().describe("Maximum tokens to generate (>0 if set; provider-dependent limits apply)").optional(),
|
|
426
|
+
temperature: z.number().describe("Sampling temperature 0.0-2.0 (0.0=deterministic, 2.0=highly random)").optional(),
|
|
427
|
+
topP: z.number().describe("Nucleus sampling threshold 0.0-1.0 (smaller values focus on higher probability tokens)").optional(),
|
|
428
|
+
topK: z.number().int().describe("Top-k sampling limit (>0 if set; primarily for local/open-source models)").optional(),
|
|
429
|
+
frequencyPenalty: z.number().describe("Frequency penalty -2.0 to 2.0 (positive values reduce repetition based on frequency)").optional(),
|
|
430
|
+
presencePenalty: z.number().describe("Presence penalty -2.0 to 2.0 (positive values encourage topic diversity)").optional(),
|
|
431
|
+
stopSequences: z.any().describe("Generation stop sequences (\u226410 sequences; each \u2264100 chars; generation halts on exact match)").optional()
|
|
432
|
+
}).passthrough().describe("Default sampling parameters for generation requests").optional(),
|
|
433
|
+
max_context_length: z.number().int().describe("Maximum context window size in tokens").optional(),
|
|
434
|
+
client_config: z.record(z.string(), z.string()).describe("Provider-specific client configuration as flexible JSON structure").optional(),
|
|
435
|
+
owner_id: z.string().describe("Optional owner ID. If not provided, derived from the authentication context. Requires CREATE_LLM_ANY permission if specified.").optional(),
|
|
436
|
+
llm_id: z.string().describe("Optional client-provided UUID for idempotent creation. If not provided, server generates a new UUID. Returns ALREADY_EXISTS if ID is already in use.").optional()
|
|
437
|
+
},
|
|
438
|
+
async (args) => {
|
|
439
|
+
const body: Record<string, unknown> = {};
|
|
440
|
+
if (args.display_name !== undefined) body["displayName"] = args.display_name;
|
|
441
|
+
if (args.description !== undefined) body["description"] = args.description;
|
|
442
|
+
if (args.provider_type !== undefined) body["providerType"] = args.provider_type;
|
|
443
|
+
if (args.endpoint_url !== undefined) body["endpointUrl"] = args.endpoint_url;
|
|
444
|
+
if (args.api_path !== undefined) body["apiPath"] = args.api_path;
|
|
445
|
+
if (args.model_identifier !== undefined) body["modelIdentifier"] = args.model_identifier;
|
|
446
|
+
if (args.supported_modalities !== undefined) body["supportedModalities"] = args.supported_modalities;
|
|
447
|
+
if (args.credentials !== undefined) body["credentials"] = args.credentials;
|
|
448
|
+
if (args.labels !== undefined) body["labels"] = args.labels;
|
|
449
|
+
if (args.version !== undefined) body["version"] = args.version;
|
|
450
|
+
if (args.monitoring_endpoint !== undefined) body["monitoringEndpoint"] = args.monitoring_endpoint;
|
|
451
|
+
if (args.capabilities !== undefined) body["capabilities"] = args.capabilities;
|
|
452
|
+
if (args.default_sampling_params !== undefined) body["defaultSamplingParams"] = args.default_sampling_params;
|
|
453
|
+
if (args.max_context_length !== undefined) body["maxContextLength"] = args.max_context_length;
|
|
454
|
+
if (args.client_config !== undefined) body["clientConfig"] = args.client_config;
|
|
455
|
+
if (args.owner_id !== undefined) body["ownerId"] = args.owner_id;
|
|
456
|
+
if (args.llm_id !== undefined) body["llmId"] = args.llm_id;
|
|
457
|
+
const result = await callApi("POST", `/v1/llms`, body);
|
|
458
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
server.tool(
|
|
463
|
+
"goodmem_llms_delete",
|
|
464
|
+
"Delete an LLM",
|
|
465
|
+
{
|
|
466
|
+
id: z.string().describe("The unique identifier of the LLM to delete")
|
|
467
|
+
},
|
|
468
|
+
async (args) => {
|
|
469
|
+
await callApi("DELETE", `/v1/llms/${encodeURIComponent(String(args.id))}`, undefined);
|
|
470
|
+
return { content: [{ type: "text" as const, text: "Deleted successfully." }] };
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
server.tool(
|
|
475
|
+
"goodmem_llms_get",
|
|
476
|
+
"Get an LLM by ID",
|
|
477
|
+
{
|
|
478
|
+
id: z.string().describe("The unique identifier of the LLM to retrieve")
|
|
479
|
+
},
|
|
480
|
+
async (args) => {
|
|
481
|
+
const result = await callApi("GET", `/v1/llms/${encodeURIComponent(String(args.id))}`, undefined);
|
|
482
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
server.tool(
|
|
487
|
+
"goodmem_llms_list",
|
|
488
|
+
"List LLMs",
|
|
489
|
+
{
|
|
490
|
+
owner_id: z.string().describe("Filter LLMs by owner ID. With LIST_LLM_ANY permission, omitting this shows all accessible LLMs; providing it filters by that owner. With LIST_LLM_OWN permission, only your own LLMs are shown regardless of this parameter.").optional(),
|
|
491
|
+
provider_type: z.enum(["OPENAI", "LITELLM_PROXY", "OPEN_ROUTER", "VLLM", "OLLAMA", "LLAMA_CPP", "CUSTOM_OPENAI_COMPATIBLE"]).describe("Filter LLMs by provider type. Allowed values match the LLMProviderType schema.").optional()
|
|
492
|
+
},
|
|
493
|
+
async (args) => {
|
|
494
|
+
const query: Record<string, string> = {};
|
|
495
|
+
if (args.owner_id !== undefined) query["ownerId"] = String(args.owner_id);
|
|
496
|
+
if (args.provider_type !== undefined) query["providerType"] = String(args.provider_type);
|
|
497
|
+
const result = await callApi("GET", `/v1/llms`, undefined, query);
|
|
498
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
server.tool(
|
|
503
|
+
"goodmem_llms_update",
|
|
504
|
+
"Update an LLM",
|
|
505
|
+
{
|
|
506
|
+
id: z.string().describe("The unique identifier of the LLM to update"),
|
|
507
|
+
display_name: z.string().describe("Update display name").optional(),
|
|
508
|
+
description: z.string().describe("Update description").optional(),
|
|
509
|
+
endpoint_url: z.string().describe("Update endpoint base URL (OpenAI-compatible base, typically ends with /v1)").optional(),
|
|
510
|
+
api_path: z.string().describe("Update API path").optional(),
|
|
511
|
+
model_identifier: z.string().describe("Update model identifier (cannot be empty)").optional(),
|
|
512
|
+
supported_modalities: z.array(z.enum(["TEXT", "IMAGE", "AUDIO", "VIDEO"])).describe("Update supported modalities (if array contains \u22651 elements, replaces stored set; if empty or omitted, no change)").optional(),
|
|
513
|
+
credentials: z.object({
|
|
514
|
+
kind: z.enum(["CREDENTIAL_KIND_UNSPECIFIED", "CREDENTIAL_KIND_API_KEY", "CREDENTIAL_KIND_GCP_ADC"]).describe("Selected credential strategy"),
|
|
515
|
+
apiKey: z.any().describe("Configuration when kind is CREDENTIAL_KIND_API_KEY").optional(),
|
|
516
|
+
gcpAdc: z.any().describe("Configuration when kind is CREDENTIAL_KIND_GCP_ADC").optional(),
|
|
517
|
+
labels: z.record(z.string(), z.string()).describe("Optional annotations to aid operators (e.g., \"owner=vertex\")").optional()
|
|
518
|
+
}).passthrough().describe("Update credentials").optional(),
|
|
519
|
+
version: z.string().describe("Update version information").optional(),
|
|
520
|
+
monitoring_endpoint: z.string().describe("Update monitoring endpoint URL").optional(),
|
|
521
|
+
capabilities: z.object({
|
|
522
|
+
supportsChat: z.boolean().describe("Supports conversational/chat completion format with message roles").optional(),
|
|
523
|
+
supportsCompletion: z.boolean().describe("Supports raw text completion with prompt continuation").optional(),
|
|
524
|
+
supportsFunctionCalling: z.boolean().describe("Supports function/tool calling with structured responses").optional(),
|
|
525
|
+
supportsSystemMessages: z.boolean().describe("Supports system prompts to define model behavior and context").optional(),
|
|
526
|
+
supportsStreaming: z.boolean().describe("Supports real-time token streaming during generation").optional(),
|
|
527
|
+
supportsSamplingParameters: z.boolean().describe("Supports sampling parameters like temperature, top_p, and top_k for generation control").optional()
|
|
528
|
+
}).passthrough().describe("Update LLM capabilities (replaces entire capability set; clients MUST send all flags)").optional(),
|
|
529
|
+
default_sampling_params: z.object({
|
|
530
|
+
maxTokens: z.number().int().describe("Maximum tokens to generate (>0 if set; provider-dependent limits apply)").optional(),
|
|
531
|
+
temperature: z.number().describe("Sampling temperature 0.0-2.0 (0.0=deterministic, 2.0=highly random)").optional(),
|
|
532
|
+
topP: z.number().describe("Nucleus sampling threshold 0.0-1.0 (smaller values focus on higher probability tokens)").optional(),
|
|
533
|
+
topK: z.number().int().describe("Top-k sampling limit (>0 if set; primarily for local/open-source models)").optional(),
|
|
534
|
+
frequencyPenalty: z.number().describe("Frequency penalty -2.0 to 2.0 (positive values reduce repetition based on frequency)").optional(),
|
|
535
|
+
presencePenalty: z.number().describe("Presence penalty -2.0 to 2.0 (positive values encourage topic diversity)").optional(),
|
|
536
|
+
stopSequences: z.any().describe("Generation stop sequences (\u226410 sequences; each \u2264100 chars; generation halts on exact match)").optional()
|
|
537
|
+
}).passthrough().describe("Update default sampling parameters").optional(),
|
|
538
|
+
max_context_length: z.number().int().describe("Update maximum context window size in tokens").optional(),
|
|
539
|
+
client_config: z.record(z.string(), z.string()).describe("Update provider-specific client configuration (replaces entire config; no merging)").optional(),
|
|
540
|
+
replace_labels: z.record(z.string(), z.string()).describe("Replace all existing labels with this set. Empty map clears all labels. Cannot be used with mergeLabels.").optional(),
|
|
541
|
+
merge_labels: z.record(z.string(), z.string()).describe("Merge with existing labels: upserts with overwrite. Labels not mentioned are preserved. Cannot be used with replaceLabels.").optional()
|
|
542
|
+
},
|
|
543
|
+
async (args) => {
|
|
544
|
+
const body: Record<string, unknown> = {};
|
|
545
|
+
if (args.display_name !== undefined) body["displayName"] = args.display_name;
|
|
546
|
+
if (args.description !== undefined) body["description"] = args.description;
|
|
547
|
+
if (args.endpoint_url !== undefined) body["endpointUrl"] = args.endpoint_url;
|
|
548
|
+
if (args.api_path !== undefined) body["apiPath"] = args.api_path;
|
|
549
|
+
if (args.model_identifier !== undefined) body["modelIdentifier"] = args.model_identifier;
|
|
550
|
+
if (args.supported_modalities !== undefined) body["supportedModalities"] = args.supported_modalities;
|
|
551
|
+
if (args.credentials !== undefined) body["credentials"] = args.credentials;
|
|
552
|
+
if (args.version !== undefined) body["version"] = args.version;
|
|
553
|
+
if (args.monitoring_endpoint !== undefined) body["monitoringEndpoint"] = args.monitoring_endpoint;
|
|
554
|
+
if (args.capabilities !== undefined) body["capabilities"] = args.capabilities;
|
|
555
|
+
if (args.default_sampling_params !== undefined) body["defaultSamplingParams"] = args.default_sampling_params;
|
|
556
|
+
if (args.max_context_length !== undefined) body["maxContextLength"] = args.max_context_length;
|
|
557
|
+
if (args.client_config !== undefined) body["clientConfig"] = args.client_config;
|
|
558
|
+
if (args.replace_labels !== undefined) body["replaceLabels"] = args.replace_labels;
|
|
559
|
+
if (args.merge_labels !== undefined) body["mergeLabels"] = args.merge_labels;
|
|
560
|
+
const result = await callApi("PUT", `/v1/llms/${encodeURIComponent(String(args.id))}`, body);
|
|
561
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
// === memories ===
|
|
568
|
+
|
|
569
|
+
server.tool(
|
|
570
|
+
"goodmem_memories_batch_create",
|
|
571
|
+
"Create multiple memories in a batch",
|
|
572
|
+
{
|
|
573
|
+
requests: z.array(z.record(z.string(), z.unknown()).describe("JsonMemoryCreationRequest object")).describe("Array of memory creation requests.")
|
|
574
|
+
},
|
|
575
|
+
async (args) => {
|
|
576
|
+
const body: Record<string, unknown> = {};
|
|
577
|
+
if (args.requests !== undefined) body["requests"] = args.requests;
|
|
578
|
+
const result = await callApi("POST", `/v1/memories:batchCreate`, body);
|
|
579
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
server.tool(
|
|
584
|
+
"goodmem_memories_batch_delete",
|
|
585
|
+
"Delete memories in batch",
|
|
586
|
+
{
|
|
587
|
+
requests: z.array(z.object({
|
|
588
|
+
memoryId: z.string().describe("Deletes one specific memory by UUID").optional(),
|
|
589
|
+
filterSelector: z.any().describe("Deletes a filtered set of memories within a specific space").optional()
|
|
590
|
+
}).passthrough()).describe("Array of delete selectors")
|
|
591
|
+
},
|
|
592
|
+
async (args) => {
|
|
593
|
+
const body: Record<string, unknown> = {};
|
|
594
|
+
if (args.requests !== undefined) body["requests"] = args.requests;
|
|
595
|
+
const result = await callApi("POST", `/v1/memories:batchDelete`, body);
|
|
596
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
server.tool(
|
|
601
|
+
"goodmem_memories_batch_get",
|
|
602
|
+
"Get multiple memories by ID",
|
|
603
|
+
{
|
|
604
|
+
memory_ids: z.array(z.string()).describe("Array of memory IDs to retrieve"),
|
|
605
|
+
include_content: z.boolean().describe("Whether to include the original content in the response").optional(),
|
|
606
|
+
include_processing_history: z.boolean().describe("Whether to include background job processing history for each memory").optional()
|
|
607
|
+
},
|
|
608
|
+
async (args) => {
|
|
609
|
+
const body: Record<string, unknown> = {};
|
|
610
|
+
if (args.memory_ids !== undefined) body["memoryIds"] = args.memory_ids;
|
|
611
|
+
if (args.include_content !== undefined) body["includeContent"] = args.include_content;
|
|
612
|
+
if (args.include_processing_history !== undefined) body["includeProcessingHistory"] = args.include_processing_history;
|
|
613
|
+
const result = await callApi("POST", `/v1/memories:batchGet`, body);
|
|
614
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
615
|
+
}
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
server.tool(
|
|
619
|
+
"goodmem_memories_create",
|
|
620
|
+
"Create a new memory",
|
|
621
|
+
{
|
|
622
|
+
memory_id: z.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(),
|
|
623
|
+
space_id: z.string().describe("ID of the space where this memory will be stored"),
|
|
624
|
+
original_content: z.string().describe("Original content as plain text (use either this or originalContentB64)").optional(),
|
|
625
|
+
original_content_b64: z.string().describe("Original content as base64-encoded binary data (use either this or originalContent)").optional(),
|
|
626
|
+
original_content_ref: z.string().describe("Reference to external content location").optional(),
|
|
627
|
+
content_type: z.string().describe("MIME type of the content"),
|
|
628
|
+
metadata: z.record(z.string(), z.string()).describe("Additional metadata for the memory").optional(),
|
|
629
|
+
chunking_config: z.object({
|
|
630
|
+
none: z.any().describe("No chunking strategy - preserve original content as single unit").optional(),
|
|
631
|
+
recursive: z.any().describe("Recursive hierarchical chunking strategy with configurable separators").optional(),
|
|
632
|
+
sentence: z.any().describe("Sentence-based chunking strategy with language detection").optional()
|
|
633
|
+
}).passthrough().describe("Chunking strategy for this memory (if not provided, uses space default)").optional(),
|
|
634
|
+
extract_page_images: z.boolean().describe("Optional hint to extract page images for eligible document types (for example, PDFs)").optional(),
|
|
635
|
+
file_field: z.string().describe("Optional multipart file field name to bind binary content; required when multiple files are uploaded in a batch multipart request.").optional()
|
|
636
|
+
},
|
|
637
|
+
async (args) => {
|
|
638
|
+
const body: Record<string, unknown> = {};
|
|
639
|
+
if (args.memory_id !== undefined) body["memoryId"] = args.memory_id;
|
|
640
|
+
if (args.space_id !== undefined) body["spaceId"] = args.space_id;
|
|
641
|
+
if (args.original_content !== undefined) body["originalContent"] = args.original_content;
|
|
642
|
+
if (args.original_content_b64 !== undefined) body["originalContentB64"] = args.original_content_b64;
|
|
643
|
+
if (args.original_content_ref !== undefined) body["originalContentRef"] = args.original_content_ref;
|
|
644
|
+
if (args.content_type !== undefined) body["contentType"] = args.content_type;
|
|
645
|
+
if (args.metadata !== undefined) body["metadata"] = args.metadata;
|
|
646
|
+
if (args.chunking_config !== undefined) body["chunkingConfig"] = args.chunking_config;
|
|
647
|
+
if (args.extract_page_images !== undefined) body["extractPageImages"] = args.extract_page_images;
|
|
648
|
+
if (args.file_field !== undefined) body["fileField"] = args.file_field;
|
|
649
|
+
const result = await callApi("POST", `/v1/memories`, body);
|
|
650
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
server.tool(
|
|
655
|
+
"goodmem_memories_delete",
|
|
656
|
+
"Delete a memory",
|
|
657
|
+
{
|
|
658
|
+
id: z.string().describe("The unique identifier of the memory to delete")
|
|
659
|
+
},
|
|
660
|
+
async (args) => {
|
|
661
|
+
await callApi("DELETE", `/v1/memories/${encodeURIComponent(String(args.id))}`, undefined);
|
|
662
|
+
return { content: [{ type: "text" as const, text: "Deleted successfully." }] };
|
|
663
|
+
}
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
server.tool(
|
|
667
|
+
"goodmem_memories_get",
|
|
668
|
+
"Get a memory by ID",
|
|
669
|
+
{
|
|
670
|
+
id: z.string().describe("The unique identifier of the memory to retrieve"),
|
|
671
|
+
include_content: z.boolean().describe("Whether to include the original content in the response (defaults to false). The snake_case alias include_content is also accepted.").optional(),
|
|
672
|
+
include_processing_history: z.boolean().describe("Whether to include background job processing history in the response (defaults to false). The snake_case alias include_processing_history is also accepted.").optional()
|
|
673
|
+
},
|
|
674
|
+
async (args) => {
|
|
675
|
+
const query: Record<string, string> = {};
|
|
676
|
+
if (args.include_content !== undefined) query["includeContent"] = String(args.include_content);
|
|
677
|
+
if (args.include_processing_history !== undefined) query["includeProcessingHistory"] = String(args.include_processing_history);
|
|
678
|
+
const result = await callApi("GET", `/v1/memories/${encodeURIComponent(String(args.id))}`, undefined, query);
|
|
679
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
server.tool(
|
|
684
|
+
"goodmem_memories_list",
|
|
685
|
+
"List memories in a space",
|
|
686
|
+
{
|
|
687
|
+
space_id: z.string().describe("The unique identifier of the space containing the memories"),
|
|
688
|
+
include_content: z.boolean().describe("Whether to include the original content in the response (defaults to false). The snake_case alias include_content is also accepted.").optional(),
|
|
689
|
+
include_processing_history: z.boolean().describe("Whether to include background job processing history in the response (defaults to false). The snake_case alias include_processing_history is also accepted.").optional(),
|
|
690
|
+
status_filter: z.string().describe("Filter memories by processing status (PENDING, PROCESSING, COMPLETED, FAILED). The snake_case alias status_filter is also accepted.").optional(),
|
|
691
|
+
filter: z.string().describe("Optional metadata filter expression for list results").optional(),
|
|
692
|
+
max_results: z.number().int().describe("Maximum number of results per page (defaults to 50, clamped to [1, 500]). The snake_case alias max_results is also accepted.").optional(),
|
|
693
|
+
next_token: z.string().describe("Opaque pagination token for the next page. URL-safe Base64 without padding; do not parse or construct it. The snake_case alias next_token is also accepted.").optional(),
|
|
694
|
+
sort_by: z.string().describe("Field to sort by (e.g., 'created_at'). The snake_case alias sort_by is also accepted.").optional(),
|
|
695
|
+
sort_order: z.enum(["ASCENDING", "DESCENDING", "SORT_ORDER_UNSPECIFIED"]).describe("Sort direction (ASCENDING or DESCENDING). The snake_case alias sort_order is also accepted.").optional()
|
|
696
|
+
},
|
|
697
|
+
async (args) => {
|
|
698
|
+
const query: Record<string, string> = {};
|
|
699
|
+
if (args.include_content !== undefined) query["includeContent"] = String(args.include_content);
|
|
700
|
+
if (args.include_processing_history !== undefined) query["includeProcessingHistory"] = String(args.include_processing_history);
|
|
701
|
+
if (args.status_filter !== undefined) query["statusFilter"] = String(args.status_filter);
|
|
702
|
+
if (args.filter !== undefined) query["filter"] = String(args.filter);
|
|
703
|
+
if (args.max_results !== undefined) query["maxResults"] = String(args.max_results);
|
|
704
|
+
if (args.next_token !== undefined) query["nextToken"] = String(args.next_token);
|
|
705
|
+
if (args.sort_by !== undefined) query["sortBy"] = String(args.sort_by);
|
|
706
|
+
if (args.sort_order !== undefined) query["sortOrder"] = String(args.sort_order);
|
|
707
|
+
const result = await callApi("GET", `/v1/spaces/${encodeURIComponent(String(args.space_id))}/memories`, undefined, query);
|
|
708
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
709
|
+
}
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
server.tool(
|
|
713
|
+
"goodmem_memories_pages",
|
|
714
|
+
"List memory page images",
|
|
715
|
+
{
|
|
716
|
+
id: z.string().describe("Memory UUID"),
|
|
717
|
+
start_page_index: z.number().int().describe("Optional lower bound for returned page indices, inclusive. The snake_case alias start_page_index is also accepted.").optional(),
|
|
718
|
+
end_page_index: z.number().int().describe("Optional upper bound for returned page indices, inclusive. The snake_case alias end_page_index is also accepted.").optional(),
|
|
719
|
+
dpi: z.number().int().describe("Optional rendition filter for page-image DPI.").optional(),
|
|
720
|
+
content_type: z.string().describe("Optional rendition filter for page-image MIME type, such as image/png. The snake_case alias content_type is also accepted.").optional(),
|
|
721
|
+
max_results: z.number().int().describe("Maximum number of results per page. The snake_case alias max_results is also accepted.").optional(),
|
|
722
|
+
next_token: z.string().describe("Opaque pagination token for the next page. The snake_case alias next_token is also accepted. Do not parse or construct it.").optional()
|
|
723
|
+
},
|
|
724
|
+
async (args) => {
|
|
725
|
+
const query: Record<string, string> = {};
|
|
726
|
+
if (args.start_page_index !== undefined) query["startPageIndex"] = String(args.start_page_index);
|
|
727
|
+
if (args.end_page_index !== undefined) query["endPageIndex"] = String(args.end_page_index);
|
|
728
|
+
if (args.dpi !== undefined) query["dpi"] = String(args.dpi);
|
|
729
|
+
if (args.content_type !== undefined) query["contentType"] = String(args.content_type);
|
|
730
|
+
if (args.max_results !== undefined) query["maxResults"] = String(args.max_results);
|
|
731
|
+
if (args.next_token !== undefined) query["nextToken"] = String(args.next_token);
|
|
732
|
+
const result = await callApi("GET", `/v1/memories/${encodeURIComponent(String(args.id))}/pages`, undefined, query);
|
|
733
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
734
|
+
}
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
server.tool(
|
|
738
|
+
"goodmem_memories_retrieve",
|
|
739
|
+
"Advanced semantic memory retrieval with JSON",
|
|
740
|
+
{
|
|
741
|
+
message: z.string().describe("Primary query/message for semantic search."),
|
|
742
|
+
context: z.array(z.object({
|
|
743
|
+
text: z.string().describe("Text content for this context item.").optional(),
|
|
744
|
+
binary: z.any().describe("Binary content for this context item.").optional()
|
|
745
|
+
}).passthrough()).describe("Optional context items (text or binary) to provide additional context for the search.").optional(),
|
|
746
|
+
space_keys: z.array(z.object({
|
|
747
|
+
spaceId: z.string().describe("The unique identifier for the space to search."),
|
|
748
|
+
embedderWeights: z.any().describe("Optional per-embedder weight overrides for this space. If not specified, database defaults are used.").optional(),
|
|
749
|
+
filter: z.string().describe("Optional filter expression that must evaluate to true for memories in this space.").optional()
|
|
750
|
+
}).passthrough()).describe("List of spaces to search with optional per-embedder weight overrides."),
|
|
751
|
+
requested_size: z.number().int().describe("Maximum number of memories to retrieve.").optional(),
|
|
752
|
+
fetch_memory: z.boolean().describe("Whether to include full Memory objects in the response.").optional(),
|
|
753
|
+
fetch_memory_content: z.boolean().describe("Whether to include memory content in the response. Requires fetchMemory=true.").optional(),
|
|
754
|
+
hnsw: z.object({
|
|
755
|
+
efSearch: z.number().int().describe("HNSW candidate list size (1..1000).").optional(),
|
|
756
|
+
iterativeScan: z.enum(["ITERATIVE_SCAN_UNSPECIFIED", "ITERATIVE_SCAN_OFF", "ITERATIVE_SCAN_RELAXED_ORDER", "ITERATIVE_SCAN_STRICT_ORDER"]).describe("HNSW iterative scan mode. Use POST retrieve for this advanced tuning control.").optional(),
|
|
757
|
+
maxScanTuples: z.number().int().describe("Maximum tuples to scan during iterative filtering (1..2147483647).").optional(),
|
|
758
|
+
scanMemMultiplier: z.number().describe("Multiplier on work_mem for iterative scanning (1.0..1000.0).").optional()
|
|
759
|
+
}).passthrough().describe("Optional request-level HNSW tuning overrides. Advanced usage; available on POST retrieve.").optional(),
|
|
760
|
+
post_processor: z.object({
|
|
761
|
+
name: z.string().describe("Fully qualified factory class name of the post-processor to apply."),
|
|
762
|
+
config: z.record(z.string(), z.string()).describe("Configuration parameters for the post-processor. Fields depend on the selected processor; see the linked documentation for the built-in ChatPostProcessor schema.").optional()
|
|
763
|
+
}).passthrough().describe("Optional post-processor configuration to transform retrieval results.").optional()
|
|
764
|
+
},
|
|
765
|
+
async (args) => {
|
|
766
|
+
const body: Record<string, unknown> = {};
|
|
767
|
+
if (args.message !== undefined) body["message"] = args.message;
|
|
768
|
+
if (args.context !== undefined) body["context"] = args.context;
|
|
769
|
+
if (args.space_keys !== undefined) body["spaceKeys"] = args.space_keys;
|
|
770
|
+
if (args.requested_size !== undefined) body["requestedSize"] = args.requested_size;
|
|
771
|
+
if (args.fetch_memory !== undefined) body["fetchMemory"] = args.fetch_memory;
|
|
772
|
+
if (args.fetch_memory_content !== undefined) body["fetchMemoryContent"] = args.fetch_memory_content;
|
|
773
|
+
if (args.hnsw !== undefined) body["hnsw"] = args.hnsw;
|
|
774
|
+
if (args.post_processor !== undefined) body["postProcessor"] = args.post_processor;
|
|
775
|
+
const result = await callApiNdjson("POST", `/v1/memories:retrieve`, body);
|
|
776
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
777
|
+
}
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
// === ocr ===
|
|
783
|
+
|
|
784
|
+
server.tool(
|
|
785
|
+
"goodmem_ocr_document",
|
|
786
|
+
"Run OCR on a document or image",
|
|
787
|
+
{
|
|
788
|
+
content: z.string().describe("Base64-encoded document bytes"),
|
|
789
|
+
format: z.enum(["AUTO", "PDF", "TIFF", "PNG", "JPEG", "BMP"]).describe("Input format hint (AUTO, PDF, TIFF, PNG, JPEG, BMP)").optional(),
|
|
790
|
+
include_raw_json: z.boolean().describe("Include raw OCR JSON payload in the response").optional(),
|
|
791
|
+
include_markdown: z.boolean().describe("Include markdown rendering in the response").optional(),
|
|
792
|
+
start_page: z.number().int().describe("0-based inclusive start page").optional(),
|
|
793
|
+
end_page: z.number().int().describe("0-based inclusive end page").optional()
|
|
794
|
+
},
|
|
795
|
+
async (args) => {
|
|
796
|
+
const body: Record<string, unknown> = {};
|
|
797
|
+
if (args.content !== undefined) body["content"] = args.content;
|
|
798
|
+
if (args.format !== undefined) body["format"] = args.format;
|
|
799
|
+
if (args.include_raw_json !== undefined) body["includeRawJson"] = args.include_raw_json;
|
|
800
|
+
if (args.include_markdown !== undefined) body["includeMarkdown"] = args.include_markdown;
|
|
801
|
+
if (args.start_page !== undefined) body["startPage"] = args.start_page;
|
|
802
|
+
if (args.end_page !== undefined) body["endPage"] = args.end_page;
|
|
803
|
+
const result = await callApi("POST", `/v1/ocr:document`, body);
|
|
804
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
// === rerankers ===
|
|
811
|
+
|
|
812
|
+
server.tool(
|
|
813
|
+
"goodmem_rerankers_create",
|
|
814
|
+
"Create a new reranker",
|
|
815
|
+
{
|
|
816
|
+
display_name: z.string().describe("User-facing name of the reranker"),
|
|
817
|
+
description: z.string().describe("Description of the reranker").optional(),
|
|
818
|
+
provider_type: z.enum(["OPENAI", "VLLM", "TEI", "LLAMA_CPP", "VOYAGE", "COHERE", "JINA"]).describe("Type of reranking provider"),
|
|
819
|
+
endpoint_url: z.string().describe("API endpoint URL"),
|
|
820
|
+
api_path: z.string().describe("API path for reranking request (defaults: Cohere /v2/rerank, Jina /v1/rerank, others /rerank)").optional(),
|
|
821
|
+
model_identifier: z.string().describe("Model identifier"),
|
|
822
|
+
supported_modalities: z.array(z.enum(["TEXT", "IMAGE", "AUDIO", "VIDEO"])).describe("Supported content modalities (defaults to TEXT if not provided)").optional(),
|
|
823
|
+
credentials: z.object({
|
|
824
|
+
kind: z.enum(["CREDENTIAL_KIND_UNSPECIFIED", "CREDENTIAL_KIND_API_KEY", "CREDENTIAL_KIND_GCP_ADC"]).describe("Selected credential strategy"),
|
|
825
|
+
apiKey: z.any().describe("Configuration when kind is CREDENTIAL_KIND_API_KEY").optional(),
|
|
826
|
+
gcpAdc: z.any().describe("Configuration when kind is CREDENTIAL_KIND_GCP_ADC").optional(),
|
|
827
|
+
labels: z.record(z.string(), z.string()).describe("Optional annotations to aid operators (e.g., \"owner=vertex\")").optional()
|
|
828
|
+
}).passthrough().describe("Structured credential payload describing how to authenticate with the provider. Required for SaaS providers; optional for local deployments.").optional(),
|
|
829
|
+
labels: z.record(z.string(), z.string()).describe("User-defined labels for categorization").optional(),
|
|
830
|
+
version: z.string().describe("Version information").optional(),
|
|
831
|
+
monitoring_endpoint: z.string().describe("Monitoring endpoint URL").optional(),
|
|
832
|
+
owner_id: z.string().describe("Optional owner ID. If not provided, derived from the authentication context. Requires CREATE_RERANKER_ANY permission if specified.").optional(),
|
|
833
|
+
reranker_id: z.string().describe("Optional client-provided UUID for idempotent creation. If not provided, server generates a new UUID. Returns ALREADY_EXISTS if ID is already in use.").optional()
|
|
834
|
+
},
|
|
835
|
+
async (args) => {
|
|
836
|
+
const body: Record<string, unknown> = {};
|
|
837
|
+
if (args.display_name !== undefined) body["displayName"] = args.display_name;
|
|
838
|
+
if (args.description !== undefined) body["description"] = args.description;
|
|
839
|
+
if (args.provider_type !== undefined) body["providerType"] = args.provider_type;
|
|
840
|
+
if (args.endpoint_url !== undefined) body["endpointUrl"] = args.endpoint_url;
|
|
841
|
+
if (args.api_path !== undefined) body["apiPath"] = args.api_path;
|
|
842
|
+
if (args.model_identifier !== undefined) body["modelIdentifier"] = args.model_identifier;
|
|
843
|
+
if (args.supported_modalities !== undefined) body["supportedModalities"] = args.supported_modalities;
|
|
844
|
+
if (args.credentials !== undefined) body["credentials"] = args.credentials;
|
|
845
|
+
if (args.labels !== undefined) body["labels"] = args.labels;
|
|
846
|
+
if (args.version !== undefined) body["version"] = args.version;
|
|
847
|
+
if (args.monitoring_endpoint !== undefined) body["monitoringEndpoint"] = args.monitoring_endpoint;
|
|
848
|
+
if (args.owner_id !== undefined) body["ownerId"] = args.owner_id;
|
|
849
|
+
if (args.reranker_id !== undefined) body["rerankerId"] = args.reranker_id;
|
|
850
|
+
const result = await callApi("POST", `/v1/rerankers`, body);
|
|
851
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
server.tool(
|
|
856
|
+
"goodmem_rerankers_delete",
|
|
857
|
+
"Delete a reranker",
|
|
858
|
+
{
|
|
859
|
+
id: z.string().describe("The unique identifier of the reranker to delete")
|
|
860
|
+
},
|
|
861
|
+
async (args) => {
|
|
862
|
+
await callApi("DELETE", `/v1/rerankers/${encodeURIComponent(String(args.id))}`, undefined);
|
|
863
|
+
return { content: [{ type: "text" as const, text: "Deleted successfully." }] };
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
server.tool(
|
|
868
|
+
"goodmem_rerankers_get",
|
|
869
|
+
"Get a reranker by ID",
|
|
870
|
+
{
|
|
871
|
+
id: z.string().describe("The unique identifier of the reranker to retrieve")
|
|
872
|
+
},
|
|
873
|
+
async (args) => {
|
|
874
|
+
const result = await callApi("GET", `/v1/rerankers/${encodeURIComponent(String(args.id))}`, undefined);
|
|
875
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
876
|
+
}
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
server.tool(
|
|
880
|
+
"goodmem_rerankers_list",
|
|
881
|
+
"List rerankers",
|
|
882
|
+
{
|
|
883
|
+
owner_id: z.string().describe("Filter rerankers by owner ID. With LIST_RERANKER_ANY permission, omitting this shows all accessible rerankers; providing it filters by that owner. With LIST_RERANKER_OWN permission, only your own rerankers are shown regardless of this parameter (PERMISSION_DENIED if set to another user).").optional(),
|
|
884
|
+
provider_type: z.enum(["OPENAI", "VLLM", "TEI", "LLAMA_CPP", "VOYAGE", "COHERE", "JINA"]).describe("Filter rerankers by provider type. Allowed values match the ProviderType schema.").optional()
|
|
885
|
+
},
|
|
886
|
+
async (args) => {
|
|
887
|
+
const query: Record<string, string> = {};
|
|
888
|
+
if (args.owner_id !== undefined) query["ownerId"] = String(args.owner_id);
|
|
889
|
+
if (args.provider_type !== undefined) query["providerType"] = String(args.provider_type);
|
|
890
|
+
const result = await callApi("GET", `/v1/rerankers`, undefined, query);
|
|
891
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
892
|
+
}
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
server.tool(
|
|
896
|
+
"goodmem_rerankers_update",
|
|
897
|
+
"Update a reranker",
|
|
898
|
+
{
|
|
899
|
+
id: z.string().describe("The unique identifier of the reranker to update"),
|
|
900
|
+
display_name: z.string().describe("User-facing name of the reranker").optional(),
|
|
901
|
+
description: z.string().describe("Description of the reranker").optional(),
|
|
902
|
+
endpoint_url: z.string().describe("API endpoint URL").optional(),
|
|
903
|
+
api_path: z.string().describe("API path for reranking request").optional(),
|
|
904
|
+
model_identifier: z.string().describe("Model identifier").optional(),
|
|
905
|
+
supported_modalities: z.array(z.enum(["TEXT", "IMAGE", "AUDIO", "VIDEO"])).describe("Supported content modalities").optional(),
|
|
906
|
+
credentials: z.object({
|
|
907
|
+
kind: z.enum(["CREDENTIAL_KIND_UNSPECIFIED", "CREDENTIAL_KIND_API_KEY", "CREDENTIAL_KIND_GCP_ADC"]).describe("Selected credential strategy"),
|
|
908
|
+
apiKey: z.any().describe("Configuration when kind is CREDENTIAL_KIND_API_KEY").optional(),
|
|
909
|
+
gcpAdc: z.any().describe("Configuration when kind is CREDENTIAL_KIND_GCP_ADC").optional(),
|
|
910
|
+
labels: z.record(z.string(), z.string()).describe("Optional annotations to aid operators (e.g., \"owner=vertex\")").optional()
|
|
911
|
+
}).passthrough().describe("Structured credential payload describing how to authenticate with the provider. Omit to keep existing credentials.").optional(),
|
|
912
|
+
replace_labels: z.record(z.string(), z.string()).describe("Replace all existing labels with these (mutually exclusive with mergeLabels)").optional(),
|
|
913
|
+
merge_labels: z.record(z.string(), z.string()).describe("Merge these labels with existing ones (mutually exclusive with replaceLabels)").optional(),
|
|
914
|
+
version: z.string().describe("Version information").optional(),
|
|
915
|
+
monitoring_endpoint: z.string().describe("Monitoring endpoint URL").optional()
|
|
916
|
+
},
|
|
917
|
+
async (args) => {
|
|
918
|
+
const body: Record<string, unknown> = {};
|
|
919
|
+
if (args.display_name !== undefined) body["displayName"] = args.display_name;
|
|
920
|
+
if (args.description !== undefined) body["description"] = args.description;
|
|
921
|
+
if (args.endpoint_url !== undefined) body["endpointUrl"] = args.endpoint_url;
|
|
922
|
+
if (args.api_path !== undefined) body["apiPath"] = args.api_path;
|
|
923
|
+
if (args.model_identifier !== undefined) body["modelIdentifier"] = args.model_identifier;
|
|
924
|
+
if (args.supported_modalities !== undefined) body["supportedModalities"] = args.supported_modalities;
|
|
925
|
+
if (args.credentials !== undefined) body["credentials"] = args.credentials;
|
|
926
|
+
if (args.replace_labels !== undefined) body["replaceLabels"] = args.replace_labels;
|
|
927
|
+
if (args.merge_labels !== undefined) body["mergeLabels"] = args.merge_labels;
|
|
928
|
+
if (args.version !== undefined) body["version"] = args.version;
|
|
929
|
+
if (args.monitoring_endpoint !== undefined) body["monitoringEndpoint"] = args.monitoring_endpoint;
|
|
930
|
+
const result = await callApi("PUT", `/v1/rerankers/${encodeURIComponent(String(args.id))}`, body);
|
|
931
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
932
|
+
}
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
// === spaces ===
|
|
938
|
+
|
|
939
|
+
server.tool(
|
|
940
|
+
"goodmem_spaces_create",
|
|
941
|
+
"Create a new Space",
|
|
942
|
+
{
|
|
943
|
+
name: z.string().describe("The desired name for the space. Must be unique within the user's scope."),
|
|
944
|
+
labels: z.record(z.string(), z.string()).describe("A set of key-value pairs to categorize or tag the space. Used for filtering and organizational purposes.").optional(),
|
|
945
|
+
space_embedders: z.array(z.object({
|
|
946
|
+
embedderId: z.string().describe("The unique identifier for the embedder to associate with the space."),
|
|
947
|
+
defaultRetrievalWeight: z.number().describe("Relative weight for this embedder used by default during retrieval. If omitted, defaults to 1.0; values need not sum to 1 and can be overridden per request.").optional()
|
|
948
|
+
}).passthrough()).describe("List of embedder configurations to associate with this space. Each specifies an embedder ID and a relative default retrieval weight used when no per-request overrides are provided."),
|
|
949
|
+
public_read: z.boolean().describe("Indicates if the space and its memories can be read by unauthenticated users or users other than the owner. Defaults to false.").optional(),
|
|
950
|
+
owner_id: z.string().describe("Optional owner ID. If not provided, derived from the authentication context. Requires CREATE_SPACE_ANY permission if specified.").optional(),
|
|
951
|
+
default_chunking_config: z.object({
|
|
952
|
+
none: z.any().describe("No chunking strategy - preserve original content as single unit").optional(),
|
|
953
|
+
recursive: z.any().describe("Recursive hierarchical chunking strategy with configurable separators").optional(),
|
|
954
|
+
sentence: z.any().describe("Sentence-based chunking strategy with language detection").optional()
|
|
955
|
+
}).passthrough().describe("Default chunking strategy for memories in this space"),
|
|
956
|
+
space_id: z.string().describe("Optional client-provided UUID for idempotent creation. If not provided, server generates a new UUID. Returns ALREADY_EXISTS if ID is already in use.").optional()
|
|
957
|
+
},
|
|
958
|
+
async (args) => {
|
|
959
|
+
const body: Record<string, unknown> = {};
|
|
960
|
+
if (args.name !== undefined) body["name"] = args.name;
|
|
961
|
+
if (args.labels !== undefined) body["labels"] = args.labels;
|
|
962
|
+
if (args.space_embedders !== undefined) body["spaceEmbedders"] = args.space_embedders;
|
|
963
|
+
if (args.public_read !== undefined) body["publicRead"] = args.public_read;
|
|
964
|
+
if (args.owner_id !== undefined) body["ownerId"] = args.owner_id;
|
|
965
|
+
if (args.default_chunking_config !== undefined) body["defaultChunkingConfig"] = args.default_chunking_config;
|
|
966
|
+
if (args.space_id !== undefined) body["spaceId"] = args.space_id;
|
|
967
|
+
const result = await callApi("POST", `/v1/spaces`, body);
|
|
968
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
server.tool(
|
|
973
|
+
"goodmem_spaces_delete",
|
|
974
|
+
"Delete a space",
|
|
975
|
+
{
|
|
976
|
+
id: z.string().describe("The unique identifier of the space to delete")
|
|
977
|
+
},
|
|
978
|
+
async (args) => {
|
|
979
|
+
await callApi("DELETE", `/v1/spaces/${encodeURIComponent(String(args.id))}`, undefined);
|
|
980
|
+
return { content: [{ type: "text" as const, text: "Deleted successfully." }] };
|
|
981
|
+
}
|
|
982
|
+
);
|
|
983
|
+
|
|
984
|
+
server.tool(
|
|
985
|
+
"goodmem_spaces_get",
|
|
986
|
+
"Get a space by ID",
|
|
987
|
+
{
|
|
988
|
+
id: z.string().describe("The unique identifier of the space to retrieve")
|
|
989
|
+
},
|
|
990
|
+
async (args) => {
|
|
991
|
+
const result = await callApi("GET", `/v1/spaces/${encodeURIComponent(String(args.id))}`, undefined);
|
|
992
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
993
|
+
}
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
server.tool(
|
|
997
|
+
"goodmem_spaces_list",
|
|
998
|
+
"List spaces",
|
|
999
|
+
{
|
|
1000
|
+
owner_id: z.string().describe("Filter spaces by owner ID. With LIST_SPACE_ANY permission and ownerId omitted, returns all visible spaces. Otherwise returns caller-owned spaces only. Specifying ownerId without LIST_SPACE_ANY returns PERMISSION_DENIED.").optional(),
|
|
1001
|
+
name_filter: z.string().describe("Filter spaces by name using glob pattern matching").optional(),
|
|
1002
|
+
max_results: z.number().int().describe("Maximum number of results to return in a single page (defaults to 50, clamped to [1, 1000])").optional(),
|
|
1003
|
+
next_token: z.string().describe("Pagination token for retrieving the next set of results").optional(),
|
|
1004
|
+
sort_by: z.string().describe("Field to sort by: 'created_time', 'updated_time', or 'name' (default: 'created_time'). Unsupported values return INVALID_ARGUMENT.").optional(),
|
|
1005
|
+
sort_order: z.enum(["ASCENDING", "DESCENDING", "SORT_ORDER_UNSPECIFIED"]).describe("Sort order (ASCENDING or DESCENDING, default: DESCENDING)").optional()
|
|
1006
|
+
},
|
|
1007
|
+
async (args) => {
|
|
1008
|
+
const query: Record<string, string> = {};
|
|
1009
|
+
if (args.owner_id !== undefined) query["ownerId"] = String(args.owner_id);
|
|
1010
|
+
if (args.name_filter !== undefined) query["nameFilter"] = String(args.name_filter);
|
|
1011
|
+
if (args.max_results !== undefined) query["maxResults"] = String(args.max_results);
|
|
1012
|
+
if (args.next_token !== undefined) query["nextToken"] = String(args.next_token);
|
|
1013
|
+
if (args.sort_by !== undefined) query["sortBy"] = String(args.sort_by);
|
|
1014
|
+
if (args.sort_order !== undefined) query["sortOrder"] = String(args.sort_order);
|
|
1015
|
+
const result = await callApi("GET", `/v1/spaces`, undefined, query);
|
|
1016
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
1017
|
+
}
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
server.tool(
|
|
1021
|
+
"goodmem_spaces_update",
|
|
1022
|
+
"Update a space",
|
|
1023
|
+
{
|
|
1024
|
+
id: z.string().describe("The unique identifier of the space to update"),
|
|
1025
|
+
name: z.string().describe("The new name for the space.").optional(),
|
|
1026
|
+
public_read: z.boolean().describe("Whether the space is publicly readable by all users.").optional(),
|
|
1027
|
+
replace_labels: z.record(z.string(), z.string()).describe("Labels to replace all existing labels. Mutually exclusive with mergeLabels.").optional(),
|
|
1028
|
+
merge_labels: z.record(z.string(), z.string()).describe("Labels to merge with existing labels. Mutually exclusive with replaceLabels.").optional()
|
|
1029
|
+
},
|
|
1030
|
+
async (args) => {
|
|
1031
|
+
const body: Record<string, unknown> = {};
|
|
1032
|
+
if (args.name !== undefined) body["name"] = args.name;
|
|
1033
|
+
if (args.public_read !== undefined) body["publicRead"] = args.public_read;
|
|
1034
|
+
if (args.replace_labels !== undefined) body["replaceLabels"] = args.replace_labels;
|
|
1035
|
+
if (args.merge_labels !== undefined) body["mergeLabels"] = args.merge_labels;
|
|
1036
|
+
const result = await callApi("PUT", `/v1/spaces/${encodeURIComponent(String(args.id))}`, body);
|
|
1037
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
// === system ===
|
|
1044
|
+
|
|
1045
|
+
server.tool(
|
|
1046
|
+
"goodmem_system_info",
|
|
1047
|
+
"Retrieve server build metadata",
|
|
1048
|
+
{},
|
|
1049
|
+
async (args) => {
|
|
1050
|
+
const result = await callApi("GET", `/v1/system/info`, undefined);
|
|
1051
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
server.tool(
|
|
1056
|
+
"goodmem_system_init",
|
|
1057
|
+
"Initialize the system",
|
|
1058
|
+
{},
|
|
1059
|
+
async (args) => {
|
|
1060
|
+
const result = await callApi("POST", `/v1/system/init`, undefined);
|
|
1061
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
1062
|
+
}
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
// === users ===
|
|
1068
|
+
|
|
1069
|
+
server.tool(
|
|
1070
|
+
"goodmem_users_get",
|
|
1071
|
+
"Get a user by ID",
|
|
1072
|
+
{
|
|
1073
|
+
id: z.string().describe("The unique identifier of the user to retrieve").optional(),
|
|
1074
|
+
email: z.string().describe("The email address of the user to retrieve").optional()
|
|
1075
|
+
},
|
|
1076
|
+
async (args) => {
|
|
1077
|
+
const result = await callApi("GET", `/v1/users/${encodeURIComponent(String(args.id))}`, undefined);
|
|
1078
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
1079
|
+
}
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
server.tool(
|
|
1083
|
+
"goodmem_users_me",
|
|
1084
|
+
"Get current user profile",
|
|
1085
|
+
{},
|
|
1086
|
+
async (args) => {
|
|
1087
|
+
const result = await callApi("GET", `/v1/users/me`, undefined);
|
|
1088
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
|
|
1089
|
+
}
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
registerTools();
|
|
1096
|
+
|
|
1097
|
+
async function main() {
|
|
1098
|
+
const transport = new StdioServerTransport();
|
|
1099
|
+
await server.connect(transport);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
main().catch((err) => {
|
|
1103
|
+
console.error("Fatal:", err);
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
});
|