loopctl-mcp-server 2.35.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 }) {
@@ -1690,6 +1785,71 @@ async function signup({ name, slug, email }) {
1690
1785
  return toContent(result);
1691
1786
  }
1692
1787
 
1788
+ // US-26.7.2: opt-in WebAuthn trust-tier upgrade ceremony (agent_rooted ->
1789
+ // human_anchored) + authenticator revocation. All four require the EXACT
1790
+ // user-role key (LOOPCTL_USER_KEY) + ownership of `tenant_id` — mirroring
1791
+ // set_llm_config's exactKey:true pattern, since these mutate the tenant's
1792
+ // root of trust. NOT headless: completing enrollment/revocation requires an
1793
+ // INTERACTIVE WebAuthn client (a browser or a native FIDO2 library) with a
1794
+ // human present to touch the hardware authenticator — an agent alone cannot
1795
+ // produce a valid attestation or assertion, by design (see
1796
+ // docs/chain-of-custody-v2.md §9).
1797
+ async function requestAuthenticatorChallenge({ tenant_id }) {
1798
+ const result = await apiCall(
1799
+ "POST",
1800
+ `/api/v1/tenants/${tenant_id}/authenticators/challenge`,
1801
+ {},
1802
+ process.env.LOOPCTL_USER_KEY,
1803
+ { exactKey: true },
1804
+ );
1805
+ return toContent(result);
1806
+ }
1807
+
1808
+ async function enrollAuthenticator({
1809
+ tenant_id,
1810
+ challenge_id,
1811
+ attestation_object,
1812
+ client_data_json,
1813
+ credential_id,
1814
+ friendly_name,
1815
+ reauth_assertion,
1816
+ }) {
1817
+ const body = { challenge_id, attestation_object, client_data_json, credential_id };
1818
+ if (friendly_name != null) body.friendly_name = friendly_name;
1819
+ if (reauth_assertion != null) body.reauth_assertion = reauth_assertion;
1820
+
1821
+ const result = await apiCall(
1822
+ "POST",
1823
+ `/api/v1/tenants/${tenant_id}/authenticators`,
1824
+ body,
1825
+ process.env.LOOPCTL_USER_KEY,
1826
+ { exactKey: true },
1827
+ );
1828
+ return toContent(result);
1829
+ }
1830
+
1831
+ async function requestAuthenticatorRevokeChallenge({ tenant_id }) {
1832
+ const result = await apiCall(
1833
+ "POST",
1834
+ `/api/v1/tenants/${tenant_id}/authenticators/revoke-challenge`,
1835
+ {},
1836
+ process.env.LOOPCTL_USER_KEY,
1837
+ { exactKey: true },
1838
+ );
1839
+ return toContent(result);
1840
+ }
1841
+
1842
+ async function revokeAuthenticator({ tenant_id, authenticator_id, webauthn_assertion }) {
1843
+ const result = await apiCall(
1844
+ "DELETE",
1845
+ `/api/v1/tenants/${tenant_id}/authenticators/${authenticator_id}`,
1846
+ { webauthn_assertion },
1847
+ process.env.LOOPCTL_USER_KEY,
1848
+ { exactKey: true },
1849
+ );
1850
+ return toContent(result);
1851
+ }
1852
+
1693
1853
  // US-26: Signed Tree Head retrieval
1694
1854
  async function getSth({ tenant_id }) {
1695
1855
  const result = await apiCall("GET", `/api/v1/audit/sth/${tenant_id}`);
@@ -2987,6 +3147,158 @@ const TOOLS = [
2987
3147
  },
2988
3148
  },
2989
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
+
2990
3302
  // Knowledge Management Tools (orchestrator key)
2991
3303
  {
2992
3304
  name: "knowledge_publish",
@@ -3892,6 +4204,114 @@ const TOOLS = [
3892
4204
  required: ["name", "slug", "email"],
3893
4205
  },
3894
4206
  },
4207
+ {
4208
+ name: "request_authenticator_challenge",
4209
+ description:
4210
+ "Step 1 of the opt-in WebAuthn trust-tier upgrade ceremony (US-26.7.2): issues a " +
4211
+ "registration challenge for enrolling a hardware authenticator against an EXISTING " +
4212
+ "agent_rooted (KB-tier) tenant, promoting it to human_anchored on success. " +
4213
+ "IMPORTANT: completing this ceremony requires an INTERACTIVE WebAuthn client " +
4214
+ "(a browser calling navigator.credentials.create(), or a native FIDO2 library) " +
4215
+ "with a HUMAN present to physically touch the authenticator — an agent alone " +
4216
+ "cannot produce a valid attestation, by design. Use this tool to fetch the " +
4217
+ "challenge/rp/user/pubKeyCredParams payload, drive the WebAuthn ceremony in your " +
4218
+ "interactive client, then call enroll_authenticator with the result. If the " +
4219
+ "tenant is already human_anchored, the response includes reauth_required: true " +
4220
+ "plus a reauth_challenge — enroll_authenticator will need a fresh assertion from " +
4221
+ "an EXISTING authenticator too (see enroll_authenticator's description). Requires " +
4222
+ "LOOPCTL_USER_KEY (user role) bound to tenant_id.",
4223
+ inputSchema: {
4224
+ type: "object",
4225
+ properties: {
4226
+ tenant_id: { type: "string", description: "Tenant UUID to enroll an authenticator for." },
4227
+ },
4228
+ required: ["tenant_id"],
4229
+ },
4230
+ },
4231
+ {
4232
+ name: "enroll_authenticator",
4233
+ description:
4234
+ "Step 2 of the opt-in WebAuthn trust-tier upgrade ceremony: completes enrollment " +
4235
+ "with the attestation produced by navigator.credentials.create() against the " +
4236
+ "challenge from request_authenticator_challenge. On a tenant's FIRST enrollment " +
4237
+ "(zero prior authenticators) this call FLIPS the tenant from agent_rooted to " +
4238
+ "human_anchored, unlocking the work-breakdown / chain-of-custody surface " +
4239
+ "(projects, stories, dispatch, ...). On a SUBSEQUENT (backup) enrollment for an " +
4240
+ "already human_anchored tenant, `reauth_assertion` (a fresh WebAuthn assertion " +
4241
+ "from an EXISTING enrolled authenticator, from the challenge's reauth_challenge) " +
4242
+ "is REQUIRED — omitting it when required returns 401 reauth_required. All " +
4243
+ "binary fields are base64url encoded. Requires LOOPCTL_USER_KEY (user role) " +
4244
+ "bound to tenant_id. Cannot be completed by an agent alone — see " +
4245
+ "request_authenticator_challenge.",
4246
+ inputSchema: {
4247
+ type: "object",
4248
+ properties: {
4249
+ tenant_id: { type: "string", description: "Tenant UUID." },
4250
+ challenge_id: {
4251
+ type: "string",
4252
+ description: "challenge_id from request_authenticator_challenge.",
4253
+ },
4254
+ attestation_object: { type: "string", description: "Base64url attestation object." },
4255
+ client_data_json: { type: "string", description: "Base64url raw client data JSON." },
4256
+ credential_id: { type: "string", description: "Base64url credential id." },
4257
+ friendly_name: {
4258
+ type: "string",
4259
+ description: "Operator-supplied label (1..120 chars, default \"Authenticator\").",
4260
+ },
4261
+ reauth_assertion: {
4262
+ type: "object",
4263
+ description:
4264
+ "Required ONLY when enrolling a backup authenticator on an already " +
4265
+ "human_anchored tenant: a fresh assertion (challenge_id, credential_id, " +
4266
+ "authenticator_data, signature, client_data_json — all base64url) from an " +
4267
+ "EXISTING enrolled authenticator, bound to the reauth_challenge returned by " +
4268
+ "request_authenticator_challenge.",
4269
+ },
4270
+ },
4271
+ required: ["tenant_id", "challenge_id", "attestation_object", "client_data_json", "credential_id"],
4272
+ },
4273
+ },
4274
+ {
4275
+ name: "request_authenticator_revoke_challenge",
4276
+ description:
4277
+ "Issues a fresh-assertion challenge to authorize revoking one of a tenant's " +
4278
+ "enrolled WebAuthn authenticators. Requires an INTERACTIVE WebAuthn client + " +
4279
+ "human touch to produce the assertion revoke_authenticator needs — an agent " +
4280
+ "alone cannot authorize a revocation. Requires LOOPCTL_USER_KEY (user role) " +
4281
+ "bound to tenant_id.",
4282
+ inputSchema: {
4283
+ type: "object",
4284
+ properties: {
4285
+ tenant_id: { type: "string", description: "Tenant UUID." },
4286
+ },
4287
+ required: ["tenant_id"],
4288
+ },
4289
+ },
4290
+ {
4291
+ name: "revoke_authenticator",
4292
+ description:
4293
+ "Revokes an enrolled authenticator using the assertion from " +
4294
+ "request_authenticator_revoke_challenge (navigator.credentials.get() against an " +
4295
+ "EXISTING authenticator — human touch required). Refuses (409 last_authenticator) " +
4296
+ "to remove a human_anchored tenant's LAST authenticator — there is no " +
4297
+ "auto-downgrade; losing every authenticator is a fatal, human-recovery-only event " +
4298
+ "(docs/chain-of-custody-v2.md §9.3). Requires LOOPCTL_USER_KEY (user role) bound " +
4299
+ "to tenant_id.",
4300
+ inputSchema: {
4301
+ type: "object",
4302
+ properties: {
4303
+ tenant_id: { type: "string", description: "Tenant UUID." },
4304
+ authenticator_id: { type: "string", description: "UUID of the authenticator to revoke." },
4305
+ webauthn_assertion: {
4306
+ type: "object",
4307
+ description:
4308
+ "The assertion (challenge_id, credential_id, authenticator_data, signature, " +
4309
+ "client_data_json — all base64url) bound to the revoke-challenge.",
4310
+ },
4311
+ },
4312
+ required: ["tenant_id", "authenticator_id", "webauthn_assertion"],
4313
+ },
4314
+ },
3895
4315
  {
3896
4316
  name: "get_sth",
3897
4317
  description: "Get the latest Signed Tree Head for a tenant's audit chain. Public — no auth required.",
@@ -4090,6 +4510,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4090
4510
  case "knowledge_create":
4091
4511
  return await knowledgeCreate(args);
4092
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
+
4093
4526
  // Knowledge Management Tools
4094
4527
  case "knowledge_publish":
4095
4528
  return await knowledgePublish(args);
@@ -4182,6 +4615,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4182
4615
  case "signup":
4183
4616
  return await signup(args);
4184
4617
 
4618
+ case "request_authenticator_challenge":
4619
+ return await requestAuthenticatorChallenge(args);
4620
+
4621
+ case "enroll_authenticator":
4622
+ return await enrollAuthenticator(args);
4623
+
4624
+ case "request_authenticator_revoke_challenge":
4625
+ return await requestAuthenticatorRevokeChallenge(args);
4626
+
4627
+ case "revoke_authenticator":
4628
+ return await revokeAuthenticator(args);
4629
+
4185
4630
  case "get_sth":
4186
4631
  return await getSth(args);
4187
4632
 
@@ -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.35.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",