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 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 (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);
@@ -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.37.0",
4
4
  "description": "MCP server for loopctl — structural trust for AI development loops",
5
5
  "type": "module",
6
6
  "main": "index.js",