Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions mcp-server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`. |
Expand Down
93 changes: 93 additions & 0 deletions mcp-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading