diff --git a/mcp-server/CHANGELOG.md b/mcp-server/CHANGELOG.md index f744791..4b48d62 100644 --- a/mcp-server/CHANGELOG.md +++ b/mcp-server/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to `loopctl-mcp-server` are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html) +## 2.2.0 — 2026-04-22 (Wiki curation tools) + +### Added + +- `knowledge_unpublish` — revert a published article back to draft. Hides it + from agent search/context without deleting; re-publish via + `knowledge_publish`. Requires `LOOPCTL_USER_KEY` (destructive, `role: :user`). +- `knowledge_archive` — soft-delete an article (draft or published). Hidden + from search/context/index; row retained for audit. Requires + `LOOPCTL_USER_KEY`. +- `knowledge_delete` — alias for `knowledge_archive` (DELETE verb on the REST + API archives under the hood). Requires `LOOPCTL_USER_KEY`. + +### Rationale + +Previously agents could create and publish articles but had no way to retract +bad drafts via MCP — low-signal articles (session summaries, commit recaps) +were piling up in the wiki with no cleanup path short of curl. These three +tools close the curation loop. All three stay at `role: :user` per the +"destructive ops above orchestrator" rule in `CLAUDE.md`. + ## 2.1.0 — 2026-04-17 (Agent ergonomics) ### Added diff --git a/mcp-server/README.md b/mcp-server/README.md index e9a0979..c3b2ac2 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -66,7 +66,7 @@ Or if installed locally: Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_KEY`. -## Tools (42) +## Tools (45) ### Project Tools @@ -145,6 +145,9 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K |---|---| | `knowledge_publish` | Publish a draft article, making it visible to all agents. Required: `article_id`. | | `knowledge_bulk_publish` | **Requires `LOOPCTL_USER_KEY`.** Atomically publish up to 100 drafts in a single call. Required: `article_ids` (array). | +| `knowledge_unpublish` | **Requires `LOOPCTL_USER_KEY`.** Revert a published article back to draft (hidden from search/context, not deleted). Required: `article_id`. | +| `knowledge_archive` | **Requires `LOOPCTL_USER_KEY`.** Soft-delete an article (draft or published). Row retained for audit; hidden from all reads. Required: `article_id`. | +| `knowledge_delete` | **Requires `LOOPCTL_USER_KEY`.** Alias for `knowledge_archive` — DELETE verb on the REST API archives under the hood. Required: `article_id`. | | `knowledge_drafts` | List draft (unpublished) knowledge articles with pagination. Optional: `limit` (default 20, max 20), `offset` (default 0), `project_id`. Returns `meta.total_count`. | | `knowledge_lint` | Run a lint check on the knowledge wiki to identify stale or low-coverage articles. Optional: `project_id`, `stale_days`, `min_coverage`, `max_per_category` (default 50, max 500). True totals returned in `summary.total_per_category`. | | `knowledge_export` | Export all knowledge articles as a ZIP archive. Returns a curl command for direct download (ZIP binary cannot be returned as MCP content). Optional: `project_id`. | diff --git a/mcp-server/index.js b/mcp-server/index.js index 4e6be67..068e7e0 100755 --- a/mcp-server/index.js +++ b/mcp-server/index.js @@ -652,6 +652,36 @@ async function knowledgeBulkPublish({ article_ids }) { return toContent(result); } +async function knowledgeUnpublish({ article_id }) { + const result = await apiCall( + "POST", + `/api/v1/articles/${article_id}/unpublish`, + null, + process.env.LOOPCTL_USER_KEY + ); + return toContent(result); +} + +async function knowledgeArchive({ article_id }) { + const result = await apiCall( + "POST", + `/api/v1/articles/${article_id}/archive`, + null, + process.env.LOOPCTL_USER_KEY + ); + return toContent(result); +} + +async function knowledgeDelete({ article_id }) { + const result = await apiCall( + "DELETE", + `/api/v1/articles/${article_id}`, + null, + process.env.LOOPCTL_USER_KEY + ); + return toContent(result); +} + async function knowledgeDrafts({ limit, offset, project_id }) { const params = new URLSearchParams(); params.set( @@ -1726,6 +1756,60 @@ const TOOLS = [ required: ["article_ids"], }, }, + { + name: "knowledge_unpublish", + description: + "Revert a published article back to draft state. The article stops being visible " + + "in agent search/context but is not deleted — re-publish with knowledge_publish. " + + "REQUIRES LOOPCTL_USER_KEY (user role — orchestrator role is NOT sufficient for " + + "this destructive operation).", + inputSchema: { + type: "object", + properties: { + article_id: { + type: "string", + description: "The UUID of the published article to unpublish.", + }, + }, + required: ["article_id"], + }, + }, + { + name: "knowledge_archive", + description: + "Archive an article (soft delete). The article is hidden from search, context, " + + "and the index but the row is retained for audit/history. Works for drafts and " + + "published articles. REQUIRES LOOPCTL_USER_KEY (user role — orchestrator role is " + + "NOT sufficient for this destructive operation).", + inputSchema: { + type: "object", + properties: { + article_id: { + type: "string", + description: "The UUID of the article to archive.", + }, + }, + required: ["article_id"], + }, + }, + { + name: "knowledge_delete", + description: + "Delete an article. Under the hood this performs the same soft-delete (archive) " + + "as knowledge_archive — use whichever name is clearer at the call site. The row " + + "is retained for audit; there is no hard delete. REQUIRES LOOPCTL_USER_KEY (user " + + "role — orchestrator role is NOT sufficient for this destructive operation).", + inputSchema: { + type: "object", + properties: { + article_id: { + type: "string", + description: "The UUID of the article to delete.", + }, + }, + required: ["article_id"], + }, + }, { name: "knowledge_drafts", description: @@ -2233,6 +2317,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { case "knowledge_bulk_publish": return await knowledgeBulkPublish(args); + case "knowledge_unpublish": + return await knowledgeUnpublish(args); + + case "knowledge_archive": + return await knowledgeArchive(args); + + case "knowledge_delete": + return await knowledgeDelete(args); + case "knowledge_drafts": return await knowledgeDrafts(args); diff --git a/mcp-server/package.json b/mcp-server/package.json index 094a2a6..32fbd05 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "loopctl-mcp-server", - "version": "2.1.0", + "version": "2.2.0", "description": "MCP server for loopctl — structural trust for AI development loops", "type": "module", "main": "index.js",