loopctl-mcp-server 2.36.0 → 2.37.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 +18 -1
- package/index.js +260 -0
- package/lib/http-helpers.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ REST endpoint (`PATCH /api/v1/tenants/me/llm-config`), and the docs — so you (
|
|
|
121
121
|
autonomous agent) can self-remediate without a human. Full agent-tenant lifecycle:
|
|
122
122
|
[`docs/onboarding-agent-tenant.md`](../docs/onboarding-agent-tenant.md).
|
|
123
123
|
|
|
124
|
-
## Tools (
|
|
124
|
+
## Tools (82)
|
|
125
125
|
|
|
126
126
|
### Project Tools
|
|
127
127
|
|
|
@@ -217,6 +217,23 @@ autonomous agent) can self-remediate without a human. Full agent-tenant lifecycl
|
|
|
217
217
|
| `knowledge_okf_export` | **Requires `LOOPCTL_USER_KEY`.** Export the wiki as a portable OKF (Open Knowledge Format) v0.1 bundle of markdown files. Writes to `out_dir`, or returns `{files, meta}` inline. |
|
|
218
218
|
| `knowledge_okf_import` | **Requires `LOOPCTL_USER_KEY`.** Import an OKF v0.1 bundle from a local directory. Creates or (with `merge`) updates articles; tolerates and preserves unknown frontmatter. |
|
|
219
219
|
|
|
220
|
+
### Agent Memory Tools (agent key)
|
|
221
|
+
|
|
222
|
+
`memory_*` is YOUR OWN scoped, private, accumulated working state — running notes, in-flight
|
|
223
|
+
task context, recall across sessions — NOT the shared knowledge wiki. Use `memory_*` for that
|
|
224
|
+
private per-scope state; use `knowledge_*` for curated knowledge other agents should see. Scope
|
|
225
|
+
(`tenant_id`/`subject_id`) is resolved server-side from your API key — none of these tools accept
|
|
226
|
+
a `tenant_id`/`subject_id`, so there is no NON-SUPERADMIN way to read or write into another
|
|
227
|
+
scope. (The one carve-out: `memory_list`'s `all_subjects` boolean IS a cross-subject read, but
|
|
228
|
+
it is enforced server-side and a no-op for a non-superadmin key — see below.)
|
|
229
|
+
|
|
230
|
+
| Tool | Description |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `memory_remember` | Write to your own working memory. `tier` selects the substrate: `long_term` (default; requires `text`, embedded asynchronously and later recalled by semantic similarity via `memory_recall`) or `session` (short-term; requires `session_id`, `content`, `expires_at` — pruned after expiry, not semantically recalled). Returns 201 with the stored memory. Optional: `confidence`, `tags`, `source_session_id`, `metadata` (long-term); `role` (session). |
|
|
233
|
+
| `memory_recall` | Semantically recall your own long-term memories most similar to `query`. When embedding generation is unavailable the response degrades to a recent-first text match with `meta.fallback: true` and a stable `meta.reason` (score is `null` on that path) — check `meta.fallback` before treating a short/empty result as a genuinely empty scope. `meta.total_count`/`meta.underfilled` are also returned. Optional: `limit`, `include_superseded`. |
|
|
234
|
+
| `memory_list` | List your own long-term memories, newest first, paginated with `meta.total_count/limit/offset` (the true scoped count, never silently capped by `limit`). Optional: `limit`, `offset`, `include_superseded`, `all_subjects` (superadmin only; ignored for non-superadmin keys). |
|
|
235
|
+
| `memory_forget` | Delete one of your own long-term memories by id. A foreign-subject, foreign-tenant, or unknown id returns 404 (no existence leak). Required: `id`. |
|
|
236
|
+
|
|
220
237
|
### Knowledge Management Tools (orchestrator key)
|
|
221
238
|
|
|
222
239
|
| Tool | Description |
|
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
projectsPath,
|
|
19
19
|
ingestionJobsPath,
|
|
20
20
|
llmUsagePath,
|
|
21
|
+
memoryPath,
|
|
21
22
|
parseJsonResponseBody,
|
|
22
23
|
} from "./lib/http-helpers.js";
|
|
23
24
|
import {
|
|
@@ -1064,6 +1065,100 @@ async function knowledgeCreate({
|
|
|
1064
1065
|
return toContent(result);
|
|
1065
1066
|
}
|
|
1066
1067
|
|
|
1068
|
+
// --- Agent Memory Tools (US-28.4) ---
|
|
1069
|
+
//
|
|
1070
|
+
// memory_* is YOUR own scoped, private, accumulated working state — recall
|
|
1071
|
+
// across sessions, running notes, in-flight task context. knowledge_* is the
|
|
1072
|
+
// curated, shared wiki. Scope (tenant_id/subject_id) is resolved SERVER-SIDE
|
|
1073
|
+
// from the API key (US-28.3) — these tools never accept or forward a
|
|
1074
|
+
// tenant_id/subject_id, so there is no NON-SUPERADMIN way to express a
|
|
1075
|
+
// cross-scope read/write here even by mistake. (The one carve-out:
|
|
1076
|
+
// memory_list's `all_subjects` boolean IS a cross-subject read, but it is
|
|
1077
|
+
// enforced server-side and a no-op for a non-superadmin key — see its
|
|
1078
|
+
// description below.) Routes through the shared apiCall/witness client
|
|
1079
|
+
// (same as every other write tool), so witness/STH persistence + the
|
|
1080
|
+
// transparent 412 self-heal on a fresh MCP process apply automatically
|
|
1081
|
+
// (AC-28.4.3) — no bespoke witness code needed.
|
|
1082
|
+
|
|
1083
|
+
async function memoryRemember({
|
|
1084
|
+
tier,
|
|
1085
|
+
text,
|
|
1086
|
+
confidence,
|
|
1087
|
+
tags,
|
|
1088
|
+
source_session_id,
|
|
1089
|
+
session_id,
|
|
1090
|
+
role,
|
|
1091
|
+
content,
|
|
1092
|
+
expires_at,
|
|
1093
|
+
metadata,
|
|
1094
|
+
}) {
|
|
1095
|
+
const payload = {};
|
|
1096
|
+
if (tier) payload.tier = tier;
|
|
1097
|
+
if (text != null) payload.text = text;
|
|
1098
|
+
if (confidence != null) payload.confidence = confidence;
|
|
1099
|
+
if (tags) payload.tags = tags;
|
|
1100
|
+
if (source_session_id) payload.source_session_id = source_session_id;
|
|
1101
|
+
if (session_id) payload.session_id = session_id;
|
|
1102
|
+
if (role) payload.role = role;
|
|
1103
|
+
if (content != null) payload.content = content;
|
|
1104
|
+
if (expires_at) payload.expires_at = expires_at;
|
|
1105
|
+
if (metadata != null) payload.metadata = metadata;
|
|
1106
|
+
|
|
1107
|
+
const result = await apiCall(
|
|
1108
|
+
"POST",
|
|
1109
|
+
"/api/v1/memory",
|
|
1110
|
+
payload,
|
|
1111
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1112
|
+
);
|
|
1113
|
+
return toContent(result);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
async function memoryRecall({ query, limit, include_superseded }) {
|
|
1117
|
+
const payload = { query };
|
|
1118
|
+
if (limit != null) payload.limit = limit;
|
|
1119
|
+
if (include_superseded != null) payload.include_superseded = include_superseded;
|
|
1120
|
+
|
|
1121
|
+
const result = await apiCall(
|
|
1122
|
+
"POST",
|
|
1123
|
+
"/api/v1/memory/recall",
|
|
1124
|
+
payload,
|
|
1125
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1126
|
+
);
|
|
1127
|
+
// Surface meta (fallback/reason/total_count/underfilled) so the caller can tell
|
|
1128
|
+
// a degraded recall from a genuinely empty scope (AC-28.4.4) — toContent already
|
|
1129
|
+
// preserves the full result (data + meta), we just keep this call explicit.
|
|
1130
|
+
return toContent(result);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
async function memoryList({ limit, offset, include_superseded, all_subjects }) {
|
|
1134
|
+
// all_subjects is superadmin-only server-side; a non-superadmin key sending
|
|
1135
|
+
// this is ignored (falls back to its own subject) rather than erroring — the
|
|
1136
|
+
// one deliberate cross-subject read this MCP surface can express (module
|
|
1137
|
+
// comment above), and only for a superadmin caller.
|
|
1138
|
+
const path = memoryPath({ limit, offset, include_superseded, all_subjects });
|
|
1139
|
+
const result = await apiCall("GET", path, null, process.env.LOOPCTL_AGENT_KEY);
|
|
1140
|
+
// Surface meta.total_count/limit/offset (AC-28.4.4).
|
|
1141
|
+
return toContent(result);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function memoryForget({ id }) {
|
|
1145
|
+
// Path-injection guard (mirrors knowledgeAgentUsage's UUID_RE check below).
|
|
1146
|
+
if (typeof id !== "string" || !UUID_RE.test(id)) {
|
|
1147
|
+
return {
|
|
1148
|
+
content: [{ type: "text", text: "Error: id must be a canonical UUID (8-4-4-4-12 hex)." }],
|
|
1149
|
+
isError: true,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const result = await apiCall(
|
|
1154
|
+
"DELETE",
|
|
1155
|
+
`/api/v1/memory/${id}`,
|
|
1156
|
+
null,
|
|
1157
|
+
process.env.LOOPCTL_AGENT_KEY,
|
|
1158
|
+
);
|
|
1159
|
+
return toContent(result);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1067
1162
|
// --- Knowledge Management Tools (orch key) ---
|
|
1068
1163
|
|
|
1069
1164
|
async function knowledgePublish({ article_id }) {
|
|
@@ -3052,6 +3147,158 @@ const TOOLS = [
|
|
|
3052
3147
|
},
|
|
3053
3148
|
},
|
|
3054
3149
|
|
|
3150
|
+
// Agent Memory Tools (US-28.4)
|
|
3151
|
+
{
|
|
3152
|
+
name: "memory_remember",
|
|
3153
|
+
description:
|
|
3154
|
+
"Write to YOUR OWN scoped, private, accumulated working memory — running notes, " +
|
|
3155
|
+
"in-flight task state, decisions you made this session — NOT the shared knowledge " +
|
|
3156
|
+
"wiki. Use memory_* for private per-scope working state across sessions; use " +
|
|
3157
|
+
"knowledge_* for curated, shared knowledge articles other agents should see. Scope " +
|
|
3158
|
+
"(tenant_id/subject_id) is resolved server-side from your API key — never pass or " +
|
|
3159
|
+
"expect a tenant_id/subject_id here; there is no way to write into another scope. " +
|
|
3160
|
+
"`tier` selects the substrate: `long_term` (default; requires `text`, embedded " +
|
|
3161
|
+
"asynchronously and later recalled by semantic similarity via memory_recall) or " +
|
|
3162
|
+
"`session` (short-term; requires `session_id`, `content`, `expires_at` — pruned " +
|
|
3163
|
+
"after expiry, not semantically recalled). Returns 201 with the stored memory.",
|
|
3164
|
+
inputSchema: {
|
|
3165
|
+
type: "object",
|
|
3166
|
+
properties: {
|
|
3167
|
+
tier: {
|
|
3168
|
+
type: "string",
|
|
3169
|
+
enum: ["long_term", "session"],
|
|
3170
|
+
description: "Memory substrate. Defaults to long_term.",
|
|
3171
|
+
},
|
|
3172
|
+
text: {
|
|
3173
|
+
type: "string",
|
|
3174
|
+
description: "Long-term memory content (required when tier=long_term).",
|
|
3175
|
+
},
|
|
3176
|
+
confidence: {
|
|
3177
|
+
type: "number",
|
|
3178
|
+
description: "Optional: confidence score (0.0-1.0) for a long-term memory.",
|
|
3179
|
+
},
|
|
3180
|
+
tags: {
|
|
3181
|
+
type: "array",
|
|
3182
|
+
items: { type: "string" },
|
|
3183
|
+
description: "Optional: tags for a long-term memory.",
|
|
3184
|
+
},
|
|
3185
|
+
source_session_id: {
|
|
3186
|
+
type: "string",
|
|
3187
|
+
description: "Optional: the session this long-term memory was distilled from.",
|
|
3188
|
+
},
|
|
3189
|
+
session_id: {
|
|
3190
|
+
type: "string",
|
|
3191
|
+
description: "Session identifier (required when tier=session).",
|
|
3192
|
+
},
|
|
3193
|
+
role: {
|
|
3194
|
+
type: "string",
|
|
3195
|
+
enum: ["user", "assistant", "system", "fact"],
|
|
3196
|
+
description: "Optional: speaker role for a session-tier turn.",
|
|
3197
|
+
},
|
|
3198
|
+
content: {
|
|
3199
|
+
type: "string",
|
|
3200
|
+
description: "Session turn content (required when tier=session).",
|
|
3201
|
+
},
|
|
3202
|
+
expires_at: {
|
|
3203
|
+
type: "string",
|
|
3204
|
+
format: "date-time",
|
|
3205
|
+
description: "Prune deadline (required when tier=session).",
|
|
3206
|
+
},
|
|
3207
|
+
metadata: {
|
|
3208
|
+
type: "object",
|
|
3209
|
+
description: "Optional: arbitrary structured metadata to attach to the memory.",
|
|
3210
|
+
},
|
|
3211
|
+
},
|
|
3212
|
+
required: [],
|
|
3213
|
+
},
|
|
3214
|
+
},
|
|
3215
|
+
{
|
|
3216
|
+
name: "memory_recall",
|
|
3217
|
+
description:
|
|
3218
|
+
"Semantically recall YOUR OWN long-term memories most similar to `query` — private, " +
|
|
3219
|
+
"scoped working state, NOT the shared knowledge wiki. Use memory_* for your scoped, " +
|
|
3220
|
+
"private, accumulated working state across sessions; use knowledge_* for curated, " +
|
|
3221
|
+
"shared knowledge articles. Scope is resolved server-side from your API key. When " +
|
|
3222
|
+
"embedding generation is unavailable the response degrades to a recent-first text " +
|
|
3223
|
+
"match with `meta.fallback: true` and a stable `meta.reason` (score is null on that " +
|
|
3224
|
+
"path) — check meta.fallback before treating a short/empty result as a genuinely " +
|
|
3225
|
+
"empty scope. `meta.total_count` and `meta.underfilled` are also returned so you can " +
|
|
3226
|
+
"distinguish a short page from a hard cap.",
|
|
3227
|
+
inputSchema: {
|
|
3228
|
+
type: "object",
|
|
3229
|
+
properties: {
|
|
3230
|
+
query: {
|
|
3231
|
+
type: "string",
|
|
3232
|
+
description: "Text to embed / match against.",
|
|
3233
|
+
},
|
|
3234
|
+
limit: {
|
|
3235
|
+
type: "integer",
|
|
3236
|
+
description: "Optional: max results, clamped to the vector-search max (no silent hard cap).",
|
|
3237
|
+
},
|
|
3238
|
+
include_superseded: {
|
|
3239
|
+
type: "boolean",
|
|
3240
|
+
description: "Optional: include superseded memories (default false).",
|
|
3241
|
+
},
|
|
3242
|
+
},
|
|
3243
|
+
required: ["query"],
|
|
3244
|
+
},
|
|
3245
|
+
},
|
|
3246
|
+
{
|
|
3247
|
+
name: "memory_list",
|
|
3248
|
+
description:
|
|
3249
|
+
"List YOUR OWN long-term memories, newest first — private, scoped working state, " +
|
|
3250
|
+
"NOT the shared knowledge wiki. Use memory_* for your scoped, private, accumulated " +
|
|
3251
|
+
"working state across sessions; use knowledge_* for curated, shared knowledge " +
|
|
3252
|
+
"articles. Scope is resolved server-side from your API key. Paginated with " +
|
|
3253
|
+
"`meta.total_count/limit/offset` (the true scoped count, never silently capped by " +
|
|
3254
|
+
"limit) so you can distinguish an empty scope from a short page.",
|
|
3255
|
+
inputSchema: {
|
|
3256
|
+
type: "object",
|
|
3257
|
+
properties: {
|
|
3258
|
+
limit: {
|
|
3259
|
+
type: "integer",
|
|
3260
|
+
description: "Optional: page size (default 50, max 200).",
|
|
3261
|
+
},
|
|
3262
|
+
offset: {
|
|
3263
|
+
type: "integer",
|
|
3264
|
+
description: "Optional: records to skip (default 0).",
|
|
3265
|
+
},
|
|
3266
|
+
include_superseded: {
|
|
3267
|
+
type: "boolean",
|
|
3268
|
+
description: "Optional: include superseded memories (default false).",
|
|
3269
|
+
},
|
|
3270
|
+
all_subjects: {
|
|
3271
|
+
type: "boolean",
|
|
3272
|
+
description:
|
|
3273
|
+
"Optional, superadmin only: list all subjects' memories in the tenant. Ignored " +
|
|
3274
|
+
"(falls back to your own subject) for non-superadmin keys.",
|
|
3275
|
+
},
|
|
3276
|
+
},
|
|
3277
|
+
required: [],
|
|
3278
|
+
},
|
|
3279
|
+
},
|
|
3280
|
+
{
|
|
3281
|
+
name: "memory_forget",
|
|
3282
|
+
description:
|
|
3283
|
+
"Delete one of YOUR OWN long-term memories by id — private, scoped working state, " +
|
|
3284
|
+
"NOT the shared knowledge wiki. Use memory_* for your scoped, private, accumulated " +
|
|
3285
|
+
"working state; use knowledge_* for curated, shared knowledge articles (delete those " +
|
|
3286
|
+
"with knowledge_delete). Scope is resolved server-side from your API key: a foreign-" +
|
|
3287
|
+
"subject, foreign-tenant, or unknown id returns 404 (no existence leak) rather than " +
|
|
3288
|
+
"revealing whether it exists in another scope.",
|
|
3289
|
+
inputSchema: {
|
|
3290
|
+
type: "object",
|
|
3291
|
+
properties: {
|
|
3292
|
+
id: {
|
|
3293
|
+
type: "string",
|
|
3294
|
+
format: "uuid",
|
|
3295
|
+
description: "The UUID of the memory to forget.",
|
|
3296
|
+
},
|
|
3297
|
+
},
|
|
3298
|
+
required: ["id"],
|
|
3299
|
+
},
|
|
3300
|
+
},
|
|
3301
|
+
|
|
3055
3302
|
// Knowledge Management Tools (orchestrator key)
|
|
3056
3303
|
{
|
|
3057
3304
|
name: "knowledge_publish",
|
|
@@ -4263,6 +4510,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4263
4510
|
case "knowledge_create":
|
|
4264
4511
|
return await knowledgeCreate(args);
|
|
4265
4512
|
|
|
4513
|
+
// Agent Memory Tools
|
|
4514
|
+
case "memory_remember":
|
|
4515
|
+
return await memoryRemember(args);
|
|
4516
|
+
|
|
4517
|
+
case "memory_recall":
|
|
4518
|
+
return await memoryRecall(args);
|
|
4519
|
+
|
|
4520
|
+
case "memory_list":
|
|
4521
|
+
return await memoryList(args);
|
|
4522
|
+
|
|
4523
|
+
case "memory_forget":
|
|
4524
|
+
return await memoryForget(args);
|
|
4525
|
+
|
|
4266
4526
|
// Knowledge Management Tools
|
|
4267
4527
|
case "knowledge_publish":
|
|
4268
4528
|
return await knowledgePublish(args);
|
package/lib/http-helpers.js
CHANGED
|
@@ -68,6 +68,24 @@ export function llmUsagePath({ from, to, limit, offset } = {}) {
|
|
|
68
68
|
])}`;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Path for `memory_list`, honoring limit/offset/include_superseded/all_subjects
|
|
73
|
+
* (US-28.4). Routes through the shared `buildQuery` helper (rather than a local
|
|
74
|
+
* `URLSearchParams` mirror) so the query-string construction is exercised by the
|
|
75
|
+
* SAME code the server ships.
|
|
76
|
+
*
|
|
77
|
+
* @param {{ limit?: number, offset?: number, include_superseded?: boolean, all_subjects?: boolean }} [args]
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function memoryPath({ limit, offset, include_superseded, all_subjects } = {}) {
|
|
81
|
+
return `/api/v1/memory${buildQuery([
|
|
82
|
+
["limit", limit],
|
|
83
|
+
["offset", offset],
|
|
84
|
+
["include_superseded", include_superseded],
|
|
85
|
+
["all_subjects", all_subjects],
|
|
86
|
+
])}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
/**
|
|
72
90
|
* Defensively parse the raw text body of a JSON-content-type HTTP response
|
|
73
91
|
* (#249, mcp-03).
|