loopctl-mcp-server 2.36.0 → 2.38.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 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 (73)
124
+ ## Tools (83)
125
125
 
126
126
  ### Project Tools
127
127
 
@@ -217,6 +217,24 @@ 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
+ | `memory_promote` | Call at session end to compile this session's short-term (`session`-tier) memory into durable `long_term` memory — unlike `memory_remember` (a single explicit write), this compiles the whole session in one shot; fire it once at session end, not per turn. Returns 202 with `{job_id, session_id, status: "enqueued"}` — promotion runs asynchronously, so the resulting memory is recallable via `memory_recall` only after the worker drains. You can only promote your own sessions (scope resolved server-side from your key). Required: `session_id`. |
237
+
220
238
  ### Knowledge Management Tools (orchestrator key)
221
239
 
222
240
  | 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,111 @@ 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
+
1162
+ async function memoryPromote({ session_id }) {
1163
+ const payload = { session_id };
1164
+ const result = await apiCall(
1165
+ "POST",
1166
+ "/api/v1/memory/promote",
1167
+ payload,
1168
+ process.env.LOOPCTL_AGENT_KEY,
1169
+ );
1170
+ return toContent(result);
1171
+ }
1172
+
1067
1173
  // --- Knowledge Management Tools (orch key) ---
1068
1174
 
1069
1175
  async function knowledgePublish({ article_id }) {
@@ -3052,6 +3158,181 @@ const TOOLS = [
3052
3158
  },
3053
3159
  },
3054
3160
 
3161
+ // Agent Memory Tools (US-28.4)
3162
+ {
3163
+ name: "memory_remember",
3164
+ description:
3165
+ "Write to YOUR OWN scoped, private, accumulated working memory — running notes, " +
3166
+ "in-flight task state, decisions you made this session — NOT the shared knowledge " +
3167
+ "wiki. Use memory_* for private per-scope working state across sessions; use " +
3168
+ "knowledge_* for curated, shared knowledge articles other agents should see. Scope " +
3169
+ "(tenant_id/subject_id) is resolved server-side from your API key — never pass or " +
3170
+ "expect a tenant_id/subject_id here; there is no way to write into another scope. " +
3171
+ "`tier` selects the substrate: `long_term` (default; requires `text`, embedded " +
3172
+ "asynchronously and later recalled by semantic similarity via memory_recall) or " +
3173
+ "`session` (short-term; requires `session_id`, `content`, `expires_at` — pruned " +
3174
+ "after expiry, not semantically recalled). Returns 201 with the stored memory.",
3175
+ inputSchema: {
3176
+ type: "object",
3177
+ properties: {
3178
+ tier: {
3179
+ type: "string",
3180
+ enum: ["long_term", "session"],
3181
+ description: "Memory substrate. Defaults to long_term.",
3182
+ },
3183
+ text: {
3184
+ type: "string",
3185
+ description: "Long-term memory content (required when tier=long_term).",
3186
+ },
3187
+ confidence: {
3188
+ type: "number",
3189
+ description: "Optional: confidence score (0.0-1.0) for a long-term memory.",
3190
+ },
3191
+ tags: {
3192
+ type: "array",
3193
+ items: { type: "string" },
3194
+ description: "Optional: tags for a long-term memory.",
3195
+ },
3196
+ source_session_id: {
3197
+ type: "string",
3198
+ description: "Optional: the session this long-term memory was distilled from.",
3199
+ },
3200
+ session_id: {
3201
+ type: "string",
3202
+ description: "Session identifier (required when tier=session).",
3203
+ },
3204
+ role: {
3205
+ type: "string",
3206
+ enum: ["user", "assistant", "system", "fact"],
3207
+ description: "Optional: speaker role for a session-tier turn.",
3208
+ },
3209
+ content: {
3210
+ type: "string",
3211
+ description: "Session turn content (required when tier=session).",
3212
+ },
3213
+ expires_at: {
3214
+ type: "string",
3215
+ format: "date-time",
3216
+ description: "Prune deadline (required when tier=session).",
3217
+ },
3218
+ metadata: {
3219
+ type: "object",
3220
+ description: "Optional: arbitrary structured metadata to attach to the memory.",
3221
+ },
3222
+ },
3223
+ required: [],
3224
+ },
3225
+ },
3226
+ {
3227
+ name: "memory_recall",
3228
+ description:
3229
+ "Semantically recall YOUR OWN long-term memories most similar to `query` — private, " +
3230
+ "scoped working state, NOT the shared knowledge wiki. Use memory_* for your scoped, " +
3231
+ "private, accumulated working state across sessions; use knowledge_* for curated, " +
3232
+ "shared knowledge articles. Scope is resolved server-side from your API key. When " +
3233
+ "embedding generation is unavailable the response degrades to a recent-first text " +
3234
+ "match with `meta.fallback: true` and a stable `meta.reason` (score is null on that " +
3235
+ "path) — check meta.fallback before treating a short/empty result as a genuinely " +
3236
+ "empty scope. `meta.total_count` and `meta.underfilled` are also returned so you can " +
3237
+ "distinguish a short page from a hard cap.",
3238
+ inputSchema: {
3239
+ type: "object",
3240
+ properties: {
3241
+ query: {
3242
+ type: "string",
3243
+ description: "Text to embed / match against.",
3244
+ },
3245
+ limit: {
3246
+ type: "integer",
3247
+ description: "Optional: max results, clamped to the vector-search max (no silent hard cap).",
3248
+ },
3249
+ include_superseded: {
3250
+ type: "boolean",
3251
+ description: "Optional: include superseded memories (default false).",
3252
+ },
3253
+ },
3254
+ required: ["query"],
3255
+ },
3256
+ },
3257
+ {
3258
+ name: "memory_list",
3259
+ description:
3260
+ "List YOUR OWN long-term memories, newest first — private, scoped working state, " +
3261
+ "NOT the shared knowledge wiki. Use memory_* for your scoped, private, accumulated " +
3262
+ "working state across sessions; use knowledge_* for curated, shared knowledge " +
3263
+ "articles. Scope is resolved server-side from your API key. Paginated with " +
3264
+ "`meta.total_count/limit/offset` (the true scoped count, never silently capped by " +
3265
+ "limit) so you can distinguish an empty scope from a short page.",
3266
+ inputSchema: {
3267
+ type: "object",
3268
+ properties: {
3269
+ limit: {
3270
+ type: "integer",
3271
+ description: "Optional: page size (default 50, max 200).",
3272
+ },
3273
+ offset: {
3274
+ type: "integer",
3275
+ description: "Optional: records to skip (default 0).",
3276
+ },
3277
+ include_superseded: {
3278
+ type: "boolean",
3279
+ description: "Optional: include superseded memories (default false).",
3280
+ },
3281
+ all_subjects: {
3282
+ type: "boolean",
3283
+ description:
3284
+ "Optional, superadmin only: list all subjects' memories in the tenant. Ignored " +
3285
+ "(falls back to your own subject) for non-superadmin keys.",
3286
+ },
3287
+ },
3288
+ required: [],
3289
+ },
3290
+ },
3291
+ {
3292
+ name: "memory_forget",
3293
+ description:
3294
+ "Delete one of YOUR OWN long-term memories by id — private, scoped working state, " +
3295
+ "NOT the shared knowledge wiki. Use memory_* for your scoped, private, accumulated " +
3296
+ "working state; use knowledge_* for curated, shared knowledge articles (delete those " +
3297
+ "with knowledge_delete). Scope is resolved server-side from your API key: a foreign-" +
3298
+ "subject, foreign-tenant, or unknown id returns 404 (no existence leak) rather than " +
3299
+ "revealing whether it exists in another scope.",
3300
+ inputSchema: {
3301
+ type: "object",
3302
+ properties: {
3303
+ id: {
3304
+ type: "string",
3305
+ format: "uuid",
3306
+ description: "The UUID of the memory to forget.",
3307
+ },
3308
+ },
3309
+ required: ["id"],
3310
+ },
3311
+ },
3312
+ {
3313
+ name: "memory_promote",
3314
+ description:
3315
+ "Call at session end to compile this session's short-term memory into durable long-term memory" +
3316
+ " — private, scoped working state, NOT the shared knowledge wiki. Unlike " +
3317
+ "memory_remember, which writes a single explicit fact, this compiles the WHOLE session's " +
3318
+ "session-tier memory in one shot — fire it once at session end, not per turn. Scope " +
3319
+ "(tenant_id/subject_id) is resolved server-side from your API key: you can only promote " +
3320
+ "your OWN sessions. Returns 202 Accepted with `{job_id, session_id, status: \"enqueued\"}` " +
3321
+ "— promotion runs asynchronously via a background worker, so the resulting long-term " +
3322
+ "memory becomes recallable via memory_recall only after that worker drains, not " +
3323
+ "immediately.",
3324
+ inputSchema: {
3325
+ type: "object",
3326
+ properties: {
3327
+ session_id: {
3328
+ type: "string",
3329
+ description: "The session whose short-term memory to compile into long-term memory.",
3330
+ },
3331
+ },
3332
+ required: ["session_id"],
3333
+ },
3334
+ },
3335
+
3055
3336
  // Knowledge Management Tools (orchestrator key)
3056
3337
  {
3057
3338
  name: "knowledge_publish",
@@ -4263,6 +4544,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4263
4544
  case "knowledge_create":
4264
4545
  return await knowledgeCreate(args);
4265
4546
 
4547
+ // Agent Memory Tools
4548
+ case "memory_remember":
4549
+ return await memoryRemember(args);
4550
+
4551
+ case "memory_recall":
4552
+ return await memoryRecall(args);
4553
+
4554
+ case "memory_list":
4555
+ return await memoryList(args);
4556
+
4557
+ case "memory_forget":
4558
+ return await memoryForget(args);
4559
+
4560
+ case "memory_promote":
4561
+ return await memoryPromote(args);
4562
+
4266
4563
  // Knowledge Management Tools
4267
4564
  case "knowledge_publish":
4268
4565
  return await knowledgePublish(args);
@@ -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).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopctl-mcp-server",
3
- "version": "2.36.0",
3
+ "version": "2.38.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",