zerno-mcp 0.1.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 +17 -0
- package/dist/index.js +847 -0
- package/dist/ui/brain.js +444 -0
- package/dist/ui/widgets.js +372 -0
- package/dist/zerno-client.js +284 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ZERNO MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Connects OpenCode (or any MCP-compatible agent) to the ZERNO cloud product
|
|
6
|
+
* brain. Exposes tasks, stage checklist, execution policy, and upload tools.
|
|
7
|
+
*
|
|
8
|
+
* Required env vars (set in opencode.jsonc → mcp.zerno.environment):
|
|
9
|
+
* ZERNO_API_URL — e.g. https://app.zerno.one
|
|
10
|
+
* ZERNO_API_TOKEN — project-scoped token from ZERNO Settings → OpenCode
|
|
11
|
+
* ZERNO_PROJECT_ID — UUID of the ZERNO project
|
|
12
|
+
* ZERNO_CONNECTION_ID — UUID of OpenCodeConnection bound to the local repo
|
|
13
|
+
*/
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import * as client from "./zerno-client.js";
|
|
18
|
+
import { renderTaskCardHTML, renderTaskListHTML, renderBacklogHealthHTML, renderTaskCardMarkdown, renderTaskListMarkdown, renderBacklogHealthMarkdown, } from "./ui/widgets.js";
|
|
19
|
+
import { renderStrategyBriefHTML, renderStrategyBriefMarkdown, renderAlignmentHTML, renderAlignmentMarkdown, renderProductHealthHTML, renderProductHealthMarkdown, renderWeeklyReviewHTML, renderWeeklyReviewMarkdown, } from "./ui/brain.js";
|
|
20
|
+
const server = new Server({ name: "zerno-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
21
|
+
function asRecord(value) {
|
|
22
|
+
if (value && typeof value === "object") {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
function asString(value) {
|
|
28
|
+
return typeof value === "string" ? value : "";
|
|
29
|
+
}
|
|
30
|
+
function asTaskLike(value) {
|
|
31
|
+
return asRecord(value);
|
|
32
|
+
}
|
|
33
|
+
function asTaskList(value) {
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return value;
|
|
36
|
+
const rec = asRecord(value);
|
|
37
|
+
const items = rec.tasks ?? rec.items ?? rec.results;
|
|
38
|
+
return Array.isArray(items) ? items : [];
|
|
39
|
+
}
|
|
40
|
+
function renderForTool(name, args, result) {
|
|
41
|
+
switch (name) {
|
|
42
|
+
case "zerno_get_task":
|
|
43
|
+
case "zerno_update_task":
|
|
44
|
+
case "zerno_update_task_status":
|
|
45
|
+
case "zerno_upload_plan":
|
|
46
|
+
case "zerno_upload_report":
|
|
47
|
+
case "zerno_create_agent_intake_task":
|
|
48
|
+
case "zerno_merge_tasks": {
|
|
49
|
+
const task = asTaskLike(result);
|
|
50
|
+
return {
|
|
51
|
+
markdown: renderTaskCardMarkdown(task),
|
|
52
|
+
html: {
|
|
53
|
+
uri: `ui://zerno/task/${asString(task.id) || "result"}`,
|
|
54
|
+
html: renderTaskCardHTML(task),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
case "zerno_list_tasks":
|
|
59
|
+
case "zerno_list_ready_tasks": {
|
|
60
|
+
const tasks = asTaskList(result);
|
|
61
|
+
const heading = name === "zerno_list_ready_tasks" ? "Ready tasks" : "Backlog";
|
|
62
|
+
const args0 = args || {};
|
|
63
|
+
const headingFull = args0.order_by ? `${heading} · ${asString(args0.order_by)}` : heading;
|
|
64
|
+
return {
|
|
65
|
+
markdown: renderTaskListMarkdown(tasks, headingFull),
|
|
66
|
+
html: {
|
|
67
|
+
uri: `ui://zerno/task-list/${name}`,
|
|
68
|
+
html: renderTaskListHTML(tasks, headingFull),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
case "zerno_bulk_score_backlog": {
|
|
73
|
+
const tasks = asTaskList(result);
|
|
74
|
+
return {
|
|
75
|
+
markdown: renderTaskListMarkdown(tasks, "Scored"),
|
|
76
|
+
html: {
|
|
77
|
+
uri: `ui://zerno/task-list/scored`,
|
|
78
|
+
html: renderTaskListHTML(tasks, "Scored tasks"),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "zerno_get_backlog_health": {
|
|
83
|
+
const data = asRecord(result);
|
|
84
|
+
return {
|
|
85
|
+
markdown: renderBacklogHealthMarkdown(data),
|
|
86
|
+
html: {
|
|
87
|
+
uri: `ui://zerno/backlog-health`,
|
|
88
|
+
html: renderBacklogHealthHTML(data),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
case "zerno_strategy_brief": {
|
|
93
|
+
const data = asRecord(result);
|
|
94
|
+
return {
|
|
95
|
+
markdown: renderStrategyBriefMarkdown(data),
|
|
96
|
+
html: { uri: `ui://zerno/strategy-brief`, html: renderStrategyBriefHTML(data) },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
case "zerno_align_backlog": {
|
|
100
|
+
const data = asRecord(result);
|
|
101
|
+
return {
|
|
102
|
+
markdown: renderAlignmentMarkdown(data),
|
|
103
|
+
html: { uri: `ui://zerno/backlog-alignment`, html: renderAlignmentHTML(data) },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
case "zerno_product_health": {
|
|
107
|
+
const data = asRecord(result);
|
|
108
|
+
return {
|
|
109
|
+
markdown: renderProductHealthMarkdown(data),
|
|
110
|
+
html: { uri: `ui://zerno/product-health`, html: renderProductHealthHTML(data) },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
case "zerno_weekly_review": {
|
|
114
|
+
const data = asRecord(result);
|
|
115
|
+
return {
|
|
116
|
+
markdown: renderWeeklyReviewMarkdown(data),
|
|
117
|
+
html: { uri: `ui://zerno/weekly-review`, html: renderWeeklyReviewHTML(data) },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Tool definitions
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
128
|
+
tools: [
|
|
129
|
+
{
|
|
130
|
+
name: "zerno_get_runtime_config",
|
|
131
|
+
description: "Return MCP runtime binding (api_url, project_id, connection_id) for diagnostics.",
|
|
132
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "zerno_get_project_context",
|
|
136
|
+
description: "Get current ZERNO project context: stage, north star metric, weekly focus, next best action.",
|
|
137
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "zerno_get_stage",
|
|
141
|
+
description: "Get the current product stage with checklist items, do-not-do-yet list, and typical risks.",
|
|
142
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "zerno_list_ready_tasks",
|
|
146
|
+
description: "List tasks with status=ready or ready_for_agent in the ZERNO project. Prefer `zerno_list_tasks` for richer filtering.",
|
|
147
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "zerno_list_tasks",
|
|
151
|
+
description: "Powerful task search across the backlog. Filter by status[], priority[], theme[], assignee, score presence, staleness; order by ICE, priority, updated. Use this to enumerate the full backlog.",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
statuses: {
|
|
156
|
+
type: "array",
|
|
157
|
+
items: { type: "string" },
|
|
158
|
+
description: "e.g. ['backlog','ready','ready_for_agent','in_progress']",
|
|
159
|
+
},
|
|
160
|
+
priorities: { type: "array", items: { type: "string" } },
|
|
161
|
+
themes: { type: "array", items: { type: "string" } },
|
|
162
|
+
assignee_type: { type: "string", description: "agent | human" },
|
|
163
|
+
assignee: { type: "string" },
|
|
164
|
+
min_ice: { type: "number" },
|
|
165
|
+
product_moving: { type: "boolean" },
|
|
166
|
+
has_score: { type: "boolean", description: "true = only scored, false = only unscored" },
|
|
167
|
+
stale_days: { type: "number", description: "only tasks not updated for this many days" },
|
|
168
|
+
bet_ids: { type: "array", items: { type: "string" }, description: "Only tasks linked to one of these bet UUIDs" },
|
|
169
|
+
no_bet: { type: "boolean", description: "true = only tasks WITHOUT a bet (orphans); false = only tasks WITH a bet" },
|
|
170
|
+
include_merged: { type: "boolean", default: false },
|
|
171
|
+
include_deleted: { type: "boolean", default: false },
|
|
172
|
+
limit: { type: "number", default: 50 },
|
|
173
|
+
order_by: {
|
|
174
|
+
type: "string",
|
|
175
|
+
enum: ["updated_desc", "ice_desc", "ice_asc", "created_desc", "priority_then_ice"],
|
|
176
|
+
default: "updated_desc",
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
required: [],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "zerno_update_task",
|
|
184
|
+
description: "Patch task fields: title, status, priority, importance_score, impact_score, effort_score (each 0-10), product_moving (★ flag), theme tag, agent_notes (your scratch space), agent_fit (claude|codex|human). ICE is recomputed automatically when any score changes.",
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
task_id: { type: "string", description: "Task UUID" },
|
|
189
|
+
title: { type: "string" },
|
|
190
|
+
status: { type: "string" },
|
|
191
|
+
priority: { type: "string", description: "low | medium | high | urgent" },
|
|
192
|
+
importance_score: { type: "number", minimum: 0, maximum: 10 },
|
|
193
|
+
impact_score: { type: "number", minimum: 0, maximum: 10 },
|
|
194
|
+
effort_score: { type: "number", minimum: 0, maximum: 10 },
|
|
195
|
+
product_moving: { type: "boolean" },
|
|
196
|
+
theme: { type: "string", description: "Short cluster tag (e.g. mcp, ui, infra, copy)" },
|
|
197
|
+
agent_notes: { type: "string", description: "Persistent notes for future agent runs" },
|
|
198
|
+
agent_fit: { type: "string", description: "claude | codex | human | other" },
|
|
199
|
+
bet_id: { type: ["string", "null"], description: "Strategic bet UUID to link this task to (null to detach)" },
|
|
200
|
+
},
|
|
201
|
+
required: ["task_id"],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "zerno_bulk_score_backlog",
|
|
206
|
+
description: "Atomically apply scoring/priority/theme/agent_fit updates to many tasks at once. Use this after analyzing the full backlog to write back the proposed ICE scores, themes, and agent assignments in one call.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
items: {
|
|
211
|
+
type: "array",
|
|
212
|
+
items: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
task_id: { type: "string" },
|
|
216
|
+
importance_score: { type: "number", minimum: 0, maximum: 10 },
|
|
217
|
+
impact_score: { type: "number", minimum: 0, maximum: 10 },
|
|
218
|
+
effort_score: { type: "number", minimum: 0, maximum: 10 },
|
|
219
|
+
priority: { type: "string" },
|
|
220
|
+
product_moving: { type: "boolean" },
|
|
221
|
+
theme: { type: "string" },
|
|
222
|
+
agent_notes: { type: "string" },
|
|
223
|
+
agent_fit: { type: "string" },
|
|
224
|
+
bet_id: { type: ["string", "null"] },
|
|
225
|
+
},
|
|
226
|
+
required: ["task_id"],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
required: ["items"],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "zerno_submit_task_triage",
|
|
235
|
+
description: "Submit AI triage proposals for one or more backlog tasks. Each item is stored as a `pending` suggestion (size/readiness/risk/type/next_action/reason); the underlying task is NOT mutated. A human accepts or rejects from the web UI. Use this after analyzing the inbox to write back classifications in one call.",
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
items: {
|
|
240
|
+
type: "array",
|
|
241
|
+
items: {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
task_id: { type: "string", description: "Task UUID" },
|
|
245
|
+
triage_type: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "bug | product_task | ui_task | research | integration | tech_debt | refactoring | ops | unknown",
|
|
248
|
+
},
|
|
249
|
+
triage_size: { type: "string", description: "XS | S | M | L | XL" },
|
|
250
|
+
triage_readiness: {
|
|
251
|
+
type: "string",
|
|
252
|
+
description: "ready | needs_clarification | needs_design_decision | needs_technical_investigation | duplicate_or_merge | too_large | blocked | trash",
|
|
253
|
+
},
|
|
254
|
+
triage_risk: { type: "string", description: "low | medium | high" },
|
|
255
|
+
next_action: { type: "string", description: "One sentence on what to do next" },
|
|
256
|
+
reason: { type: "string", description: "Why this classification" },
|
|
257
|
+
confidence: { type: "string", description: "low | medium | high" },
|
|
258
|
+
},
|
|
259
|
+
required: ["task_id"],
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
source_agent: {
|
|
263
|
+
type: "string",
|
|
264
|
+
description: "Identifier of the agent that produced this triage (e.g. codex, claude_code)",
|
|
265
|
+
},
|
|
266
|
+
agent_session_id: { type: "string" },
|
|
267
|
+
},
|
|
268
|
+
required: ["items"],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "zerno_merge_tasks",
|
|
273
|
+
description: "Soft-merge a duplicate/subset task INTO a target task. Source is marked done and linked via merged_into_id. Use when two tasks describe the same work.",
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
task_id: { type: "string", description: "Source task to absorb (will be closed)" },
|
|
278
|
+
target_id: { type: "string", description: "Target task that keeps living" },
|
|
279
|
+
},
|
|
280
|
+
required: ["task_id", "target_id"],
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "zerno_get_backlog_health",
|
|
285
|
+
description: "Backlog dashboard: counts by status/priority/theme/assignee, ★ product movers, top-ICE, quick wins (high impact / low effort), unscored, stale (>7d). Call at the start of a triage session.",
|
|
286
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "zerno_strategy_brief",
|
|
290
|
+
description: "Strategic snapshot: stage, North Star metric, weekly focus, next best action, ACTIVE BETS (with hypothesis/target/kill criteria/task counts), open CONSTRAINTS, risks, problems, stage playbook (checklist + do-not-do-yet). Call this at the start of any strategic conversation: 'where are we going / why / what to NOT do'.",
|
|
291
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "zerno_align_backlog",
|
|
295
|
+
description: "Backlog alignment view: tasks grouped under each active strategic bet, plus ORPHAN tasks (no bet_id). Use this to find which tasks are noise (not connected to any strategic bet) and re-link or cut them via zerno_update_task / zerno_bulk_score_backlog / zerno_merge_tasks.",
|
|
296
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: "zerno_product_health",
|
|
300
|
+
description: "Product Health Score 0-100 with 5 blocks (Strategy clarity, Execution focus, Backlog hygiene, Delivery risk, Customer signal) + actionable recommendations. Pure read across ProjectState, Bets, Constraints, Risks, Problems, MemoryNotes and ProductTask. Run weekly.",
|
|
301
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "zerno_weekly_review",
|
|
305
|
+
description: "What happened in the last N days (default 7): tasks completed, started, blocked, recent decisions, stale tasks, agent token/$ usage. Built from TaskEvent + AgentRun + Decision. Use to produce the weekly product review and propose next-week focus.",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: { days: { type: "number", default: 7, minimum: 1, maximum: 60 } },
|
|
309
|
+
required: [],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "zerno_get_execution_policy",
|
|
314
|
+
description: "Get the execution policy: max bulk tasks, safe/risky task types, plan requirements.",
|
|
315
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "zerno_get_agent_settings",
|
|
319
|
+
description: "Get project-level agent settings and enabled instruction/skill presets such as Karpathy guidelines.",
|
|
320
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "zerno_get_task",
|
|
324
|
+
description: "Get full detail for a specific task by ID.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
properties: {
|
|
328
|
+
task_id: { type: "string", description: "Task UUID" },
|
|
329
|
+
},
|
|
330
|
+
required: ["task_id"],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "zerno_resume_task",
|
|
335
|
+
description: "Resume work on a task: returns the last checkpoint summary, recent events, and suggested next step. Call this first when starting a session on a task.",
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: { task_id: { type: "string", description: "Task UUID" } },
|
|
339
|
+
required: ["task_id"],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "zerno_checkpoint",
|
|
344
|
+
description: "Persist a short summary of where the work stands before ending a session or hitting limits, so the next session can resume cleanly.",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
task_id: { type: "string", description: "Task UUID" },
|
|
349
|
+
summary: { type: "string", description: "Short markdown summary, 1-5 lines" },
|
|
350
|
+
branch: { type: "string", description: "Current git branch" },
|
|
351
|
+
next_step: { type: "string", description: "What to do next on resume" },
|
|
352
|
+
},
|
|
353
|
+
required: ["task_id", "summary"],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "zerno_log_task_event",
|
|
358
|
+
description: "Record a free-form event for a task (commit, session_end, pr_open, etc). Used by hooks; agents normally do not call this directly.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
task_id: { type: "string", description: "Task UUID" },
|
|
363
|
+
event_type: { type: "string", description: "Event kind, e.g. commit | session_end | pr_open" },
|
|
364
|
+
summary: { type: "string", description: "Short human-readable summary" },
|
|
365
|
+
cost_tokens: { type: "number", minimum: 0 },
|
|
366
|
+
duration_ms: { type: "number", minimum: 0 },
|
|
367
|
+
},
|
|
368
|
+
required: ["task_id", "event_type"],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "zerno_upload_plan",
|
|
373
|
+
description: "Upload a planning document (markdown) for a task. Call before executing the task.",
|
|
374
|
+
inputSchema: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {
|
|
377
|
+
task_id: { type: "string", description: "Task UUID" },
|
|
378
|
+
markdown: { type: "string", description: "Full plan in markdown" },
|
|
379
|
+
agent_session_id: { type: "string", description: "Optional agent session ID" },
|
|
380
|
+
},
|
|
381
|
+
required: ["task_id", "markdown"],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "zerno_upload_report",
|
|
386
|
+
description: "Upload an execution report (markdown) for a completed task.",
|
|
387
|
+
inputSchema: {
|
|
388
|
+
type: "object",
|
|
389
|
+
properties: {
|
|
390
|
+
task_id: { type: "string", description: "Task UUID" },
|
|
391
|
+
markdown: { type: "string", description: "Report in markdown" },
|
|
392
|
+
agent_session_id: { type: "string", description: "Optional agent session ID" },
|
|
393
|
+
},
|
|
394
|
+
required: ["task_id", "markdown"],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "zerno_update_task_status",
|
|
399
|
+
description: "Update task status. Allowed values: backlog | ready | in_progress | waiting_limit | review | done.",
|
|
400
|
+
inputSchema: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
task_id: { type: "string", description: "Task UUID" },
|
|
404
|
+
status: {
|
|
405
|
+
type: "string",
|
|
406
|
+
enum: ["backlog", "ready", "in_progress", "waiting_limit", "review", "done"],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
required: ["task_id", "status"],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "zerno_create_agent_intake_task",
|
|
414
|
+
description: "Create a new review task in ZERNO from ad-hoc implementation report when there is no existing task.",
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {
|
|
418
|
+
title: { type: "string", description: "Short task title" },
|
|
419
|
+
summary: { type: "string", description: "Implementation summary" },
|
|
420
|
+
changed_files: {
|
|
421
|
+
type: "array",
|
|
422
|
+
items: { type: "string" },
|
|
423
|
+
description: "List of changed file paths",
|
|
424
|
+
},
|
|
425
|
+
screenshot_urls: {
|
|
426
|
+
type: "array",
|
|
427
|
+
items: { type: "string" },
|
|
428
|
+
description: "Screenshot URLs or file references",
|
|
429
|
+
},
|
|
430
|
+
checks: {
|
|
431
|
+
type: "array",
|
|
432
|
+
items: { type: "string" },
|
|
433
|
+
description: "Checks/tests summary entries",
|
|
434
|
+
},
|
|
435
|
+
safety_class: { type: "string", description: "Execution safety class, default bulk_safe" },
|
|
436
|
+
agent_session_id: { type: "string", description: "Optional agent session ID" },
|
|
437
|
+
agent_kind: { type: "string", description: "codex | claude | opencode | other" },
|
|
438
|
+
},
|
|
439
|
+
required: ["title", "summary"],
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: "zerno_memory_snapshot",
|
|
444
|
+
description: "Hero memory tool. Returns bundled project memory: state, stage playbook, active bets, open constraints, recent decisions, problems, risks, and pending agent notes. Pass task_id to attach the task being worked on; pass query to bias results to a topic. Call this at the START of every coding session.",
|
|
445
|
+
inputSchema: {
|
|
446
|
+
type: "object",
|
|
447
|
+
properties: {
|
|
448
|
+
task_id: { type: "string", description: "Optional task UUID being worked on" },
|
|
449
|
+
query: {
|
|
450
|
+
type: "string",
|
|
451
|
+
description: "Optional ILIKE filter applied to titles/markdown of bets/decisions/constraints",
|
|
452
|
+
},
|
|
453
|
+
decisions_limit: { type: "number" },
|
|
454
|
+
bets_limit: { type: "number" },
|
|
455
|
+
constraints_limit: { type: "number" },
|
|
456
|
+
pending_notes_limit: { type: "number" },
|
|
457
|
+
},
|
|
458
|
+
required: [],
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: "zerno_memory_search",
|
|
463
|
+
description: "Full-text-ish (ILIKE) search across decisions, bets, constraints, and pending memory notes for the project. Returns ranked hits with snippets.",
|
|
464
|
+
inputSchema: {
|
|
465
|
+
type: "object",
|
|
466
|
+
properties: {
|
|
467
|
+
query: { type: "string", minLength: 1 },
|
|
468
|
+
limit_per_kind: { type: "number", default: 5 },
|
|
469
|
+
},
|
|
470
|
+
required: ["query"],
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "zerno_memory_list_decisions",
|
|
475
|
+
description: "List recent product decisions for the project. Optional status filter.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: "object",
|
|
478
|
+
properties: {
|
|
479
|
+
status: { type: "string" },
|
|
480
|
+
limit: { type: "number" },
|
|
481
|
+
},
|
|
482
|
+
required: [],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: "zerno_memory_list_bets",
|
|
487
|
+
description: "List active product bets for the project. Optional status filter.",
|
|
488
|
+
inputSchema: {
|
|
489
|
+
type: "object",
|
|
490
|
+
properties: {
|
|
491
|
+
status: { type: "string" },
|
|
492
|
+
limit: { type: "number" },
|
|
493
|
+
},
|
|
494
|
+
required: [],
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "zerno_memory_list_constraints",
|
|
499
|
+
description: "List open constraints for the project. Optional status filter.",
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
status: { type: "string" },
|
|
504
|
+
limit: { type: "number" },
|
|
505
|
+
},
|
|
506
|
+
required: [],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: "zerno_memory_list_notes",
|
|
511
|
+
description: "List memory notes (agent inbox). Filter by status (pending|accepted|rejected|merged) or kind.",
|
|
512
|
+
inputSchema: {
|
|
513
|
+
type: "object",
|
|
514
|
+
properties: {
|
|
515
|
+
status: { type: "string" },
|
|
516
|
+
kind: { type: "string" },
|
|
517
|
+
limit: { type: "number" },
|
|
518
|
+
},
|
|
519
|
+
required: [],
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
name: "zerno_run_start",
|
|
524
|
+
description: "Start an agent run for telemetry (tokens, cost, duration). Returns a run_id you must pass to record_usage and finish_run.",
|
|
525
|
+
inputSchema: {
|
|
526
|
+
type: "object",
|
|
527
|
+
properties: {
|
|
528
|
+
agent_kind: {
|
|
529
|
+
type: "string",
|
|
530
|
+
description: "claude_code | cursor | codex | opencode | other",
|
|
531
|
+
},
|
|
532
|
+
agent_session_id: { type: "string" },
|
|
533
|
+
title: { type: "string", description: "Short label, e.g. user prompt summary" },
|
|
534
|
+
model: { type: "string", description: "Model id, e.g. claude-opus-4-7" },
|
|
535
|
+
related_task_id: { type: "string", description: "Optional ZERNO task UUID" },
|
|
536
|
+
started_at: { type: "string", description: "Optional ISO start time (defaults to now)" },
|
|
537
|
+
extra: { type: "object", description: "Free-form metadata" },
|
|
538
|
+
},
|
|
539
|
+
required: ["agent_kind"],
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "zerno_run_record_usage",
|
|
544
|
+
description: "Append token usage to a running run. Cost is computed server-side from the model. Call this after each turn / tool result.",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
run_id: { type: "string" },
|
|
549
|
+
input_tokens: { type: "number", default: 0 },
|
|
550
|
+
output_tokens: { type: "number", default: 0 },
|
|
551
|
+
cache_read_tokens: { type: "number", default: 0 },
|
|
552
|
+
cache_write_tokens: { type: "number", default: 0 },
|
|
553
|
+
model: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Override model for this delta (defaults to run.model)",
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
required: ["run_id"],
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "zerno_run_finish",
|
|
563
|
+
description: "Mark an agent run as succeeded | failed | cancelled. Optional summary/error_message.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {
|
|
567
|
+
run_id: { type: "string" },
|
|
568
|
+
status: {
|
|
569
|
+
type: "string",
|
|
570
|
+
enum: ["succeeded", "failed", "cancelled"],
|
|
571
|
+
default: "succeeded",
|
|
572
|
+
},
|
|
573
|
+
summary: { type: "string" },
|
|
574
|
+
error_message: { type: "string" },
|
|
575
|
+
ended_at: { type: "string" },
|
|
576
|
+
},
|
|
577
|
+
required: ["run_id"],
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "zerno_memory_capture_note",
|
|
582
|
+
description: "Append an agent-proposed note to the project memory inbox. Use this to record learnings, draft decisions, surfaced risks, or context that the human should curate. Agents NEVER write directly into Decision/Bet/Constraint — only here.",
|
|
583
|
+
inputSchema: {
|
|
584
|
+
type: "object",
|
|
585
|
+
properties: {
|
|
586
|
+
kind: {
|
|
587
|
+
type: "string",
|
|
588
|
+
enum: ["learning", "decision_draft", "context", "risk", "todo"],
|
|
589
|
+
},
|
|
590
|
+
title: { type: "string", minLength: 1, maxLength: 300 },
|
|
591
|
+
content_md: { type: "string", minLength: 1 },
|
|
592
|
+
confidence: { type: "string", enum: ["low", "medium", "high"], default: "medium" },
|
|
593
|
+
source: { type: "string", description: "Free-form source label, e.g. file path or url" },
|
|
594
|
+
agent_session_id: { type: "string" },
|
|
595
|
+
related_task_id: { type: "string", description: "Optional task UUID" },
|
|
596
|
+
},
|
|
597
|
+
required: ["kind", "title", "content_md"],
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
}));
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// Tool handlers
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
606
|
+
const { name, arguments: args } = req.params;
|
|
607
|
+
try {
|
|
608
|
+
let result;
|
|
609
|
+
switch (name) {
|
|
610
|
+
case "zerno_get_project_context":
|
|
611
|
+
result = await client.getProjectContext();
|
|
612
|
+
break;
|
|
613
|
+
case "zerno_get_runtime_config":
|
|
614
|
+
result = client.getRuntimeConfig();
|
|
615
|
+
break;
|
|
616
|
+
case "zerno_get_stage":
|
|
617
|
+
result = await client.getStage();
|
|
618
|
+
break;
|
|
619
|
+
case "zerno_list_ready_tasks":
|
|
620
|
+
result = await client.listReadyTasks();
|
|
621
|
+
break;
|
|
622
|
+
case "zerno_list_tasks":
|
|
623
|
+
result = await client.listTasks({
|
|
624
|
+
statuses: args?.statuses,
|
|
625
|
+
priorities: args?.priorities,
|
|
626
|
+
themes: args?.themes,
|
|
627
|
+
assignee_type: args?.assignee_type,
|
|
628
|
+
assignee: args?.assignee,
|
|
629
|
+
min_ice: args?.min_ice,
|
|
630
|
+
product_moving: args?.product_moving,
|
|
631
|
+
has_score: args?.has_score,
|
|
632
|
+
stale_days: args?.stale_days,
|
|
633
|
+
include_merged: args?.include_merged,
|
|
634
|
+
include_deleted: args?.include_deleted,
|
|
635
|
+
limit: args?.limit,
|
|
636
|
+
order_by: args?.order_by,
|
|
637
|
+
});
|
|
638
|
+
break;
|
|
639
|
+
case "zerno_update_task":
|
|
640
|
+
result = await client.updateTask(args.task_id, {
|
|
641
|
+
title: args?.title,
|
|
642
|
+
status: args?.status,
|
|
643
|
+
priority: args?.priority,
|
|
644
|
+
importance_score: args?.importance_score,
|
|
645
|
+
impact_score: args?.impact_score,
|
|
646
|
+
effort_score: args?.effort_score,
|
|
647
|
+
product_moving: args?.product_moving,
|
|
648
|
+
theme: args?.theme,
|
|
649
|
+
agent_notes: args?.agent_notes,
|
|
650
|
+
agent_fit: args?.agent_fit,
|
|
651
|
+
});
|
|
652
|
+
break;
|
|
653
|
+
case "zerno_bulk_score_backlog":
|
|
654
|
+
result = await client.bulkScoreBacklog(args.items ?? []);
|
|
655
|
+
break;
|
|
656
|
+
case "zerno_merge_tasks":
|
|
657
|
+
result = await client.mergeTasks(args.task_id, args.target_id);
|
|
658
|
+
break;
|
|
659
|
+
case "zerno_submit_task_triage":
|
|
660
|
+
result = await client.submitTaskTriage(args.items ?? [], {
|
|
661
|
+
source_agent: args?.source_agent,
|
|
662
|
+
agent_session_id: args?.agent_session_id,
|
|
663
|
+
});
|
|
664
|
+
break;
|
|
665
|
+
case "zerno_get_backlog_health":
|
|
666
|
+
result = await client.getBacklogHealth();
|
|
667
|
+
break;
|
|
668
|
+
case "zerno_strategy_brief":
|
|
669
|
+
result = await client.getStrategyBrief();
|
|
670
|
+
break;
|
|
671
|
+
case "zerno_align_backlog":
|
|
672
|
+
result = await client.getBacklogAlignment();
|
|
673
|
+
break;
|
|
674
|
+
case "zerno_product_health":
|
|
675
|
+
result = await client.getProductHealth();
|
|
676
|
+
break;
|
|
677
|
+
case "zerno_weekly_review":
|
|
678
|
+
result = await client.getWeeklyReview(args?.days);
|
|
679
|
+
break;
|
|
680
|
+
case "zerno_get_execution_policy":
|
|
681
|
+
result = await client.getExecutionPolicy();
|
|
682
|
+
break;
|
|
683
|
+
case "zerno_get_agent_settings":
|
|
684
|
+
result = await client.getAgentSettings();
|
|
685
|
+
break;
|
|
686
|
+
case "zerno_get_task":
|
|
687
|
+
result = await client.getTask(args.task_id);
|
|
688
|
+
break;
|
|
689
|
+
case "zerno_resume_task":
|
|
690
|
+
result = await client.resumeTask(args.task_id);
|
|
691
|
+
break;
|
|
692
|
+
case "zerno_checkpoint":
|
|
693
|
+
result = await client.checkpointTask(args.task_id, {
|
|
694
|
+
summary: args.summary,
|
|
695
|
+
branch: args?.branch,
|
|
696
|
+
next_step: args?.next_step,
|
|
697
|
+
extra: args?.extra,
|
|
698
|
+
});
|
|
699
|
+
break;
|
|
700
|
+
case "zerno_log_task_event":
|
|
701
|
+
result = await client.logTaskEvent(args.task_id, {
|
|
702
|
+
event_type: args.event_type,
|
|
703
|
+
summary: args?.summary,
|
|
704
|
+
cost_tokens: args?.cost_tokens,
|
|
705
|
+
duration_ms: args?.duration_ms,
|
|
706
|
+
});
|
|
707
|
+
break;
|
|
708
|
+
case "zerno_upload_plan":
|
|
709
|
+
result = await client.uploadPlan(args.task_id, args.markdown, args.agent_session_id);
|
|
710
|
+
break;
|
|
711
|
+
case "zerno_upload_report":
|
|
712
|
+
result = await client.uploadReport(args.task_id, args.markdown, args.agent_session_id);
|
|
713
|
+
break;
|
|
714
|
+
case "zerno_update_task_status":
|
|
715
|
+
result = await client.updateTaskStatus(args.task_id, args.status);
|
|
716
|
+
break;
|
|
717
|
+
case "zerno_create_agent_intake_task":
|
|
718
|
+
result = await client.createAgentIntakeTask({
|
|
719
|
+
title: args.title,
|
|
720
|
+
summary: args.summary,
|
|
721
|
+
changed_files: args?.changed_files ?? [],
|
|
722
|
+
screenshot_urls: args?.screenshot_urls ?? [],
|
|
723
|
+
checks: args?.checks ?? [],
|
|
724
|
+
safety_class: args?.safety_class ?? "bulk_safe",
|
|
725
|
+
agent_session_id: args?.agent_session_id,
|
|
726
|
+
agent_kind: args?.agent_kind,
|
|
727
|
+
});
|
|
728
|
+
break;
|
|
729
|
+
case "zerno_memory_snapshot":
|
|
730
|
+
result = await client.getMemorySnapshot({
|
|
731
|
+
task_id: args?.task_id,
|
|
732
|
+
query: args?.query,
|
|
733
|
+
decisions_limit: args?.decisions_limit,
|
|
734
|
+
bets_limit: args?.bets_limit,
|
|
735
|
+
constraints_limit: args?.constraints_limit,
|
|
736
|
+
pending_notes_limit: args?.pending_notes_limit,
|
|
737
|
+
});
|
|
738
|
+
break;
|
|
739
|
+
case "zerno_memory_search":
|
|
740
|
+
result = await client.searchMemory(args.query, args?.limit_per_kind);
|
|
741
|
+
break;
|
|
742
|
+
case "zerno_memory_list_decisions":
|
|
743
|
+
result = await client.listMemoryDecisions({
|
|
744
|
+
status: args?.status,
|
|
745
|
+
limit: args?.limit,
|
|
746
|
+
});
|
|
747
|
+
break;
|
|
748
|
+
case "zerno_memory_list_bets":
|
|
749
|
+
result = await client.listMemoryBets({
|
|
750
|
+
status: args?.status,
|
|
751
|
+
limit: args?.limit,
|
|
752
|
+
});
|
|
753
|
+
break;
|
|
754
|
+
case "zerno_memory_list_constraints":
|
|
755
|
+
result = await client.listMemoryConstraints({
|
|
756
|
+
status: args?.status,
|
|
757
|
+
limit: args?.limit,
|
|
758
|
+
});
|
|
759
|
+
break;
|
|
760
|
+
case "zerno_memory_list_notes":
|
|
761
|
+
result = await client.listMemoryNotes({
|
|
762
|
+
status: args?.status,
|
|
763
|
+
kind: args?.kind,
|
|
764
|
+
limit: args?.limit,
|
|
765
|
+
});
|
|
766
|
+
break;
|
|
767
|
+
case "zerno_run_start":
|
|
768
|
+
result = await client.startRun({
|
|
769
|
+
agent_kind: args.agent_kind,
|
|
770
|
+
agent_session_id: args?.agent_session_id,
|
|
771
|
+
title: args?.title,
|
|
772
|
+
model: args?.model,
|
|
773
|
+
related_task_id: args?.related_task_id,
|
|
774
|
+
started_at: args?.started_at,
|
|
775
|
+
extra: args?.extra,
|
|
776
|
+
});
|
|
777
|
+
break;
|
|
778
|
+
case "zerno_run_record_usage":
|
|
779
|
+
result = await client.recordRunUsage(args.run_id, {
|
|
780
|
+
input_tokens: args?.input_tokens,
|
|
781
|
+
output_tokens: args?.output_tokens,
|
|
782
|
+
cache_read_tokens: args?.cache_read_tokens,
|
|
783
|
+
cache_write_tokens: args?.cache_write_tokens,
|
|
784
|
+
model: args?.model,
|
|
785
|
+
});
|
|
786
|
+
break;
|
|
787
|
+
case "zerno_run_finish":
|
|
788
|
+
result = await client.finishRun(args.run_id, {
|
|
789
|
+
status: args?.status,
|
|
790
|
+
summary: args?.summary,
|
|
791
|
+
error_message: args?.error_message,
|
|
792
|
+
ended_at: args?.ended_at,
|
|
793
|
+
});
|
|
794
|
+
break;
|
|
795
|
+
case "zerno_memory_capture_note":
|
|
796
|
+
result = await client.appendMemoryNote({
|
|
797
|
+
kind: args.kind,
|
|
798
|
+
title: args.title,
|
|
799
|
+
content_md: args.content_md,
|
|
800
|
+
confidence: args?.confidence,
|
|
801
|
+
source: args?.source,
|
|
802
|
+
agent_session_id: args?.agent_session_id,
|
|
803
|
+
related_task_id: args?.related_task_id,
|
|
804
|
+
});
|
|
805
|
+
break;
|
|
806
|
+
default:
|
|
807
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
808
|
+
}
|
|
809
|
+
const rendered = renderForTool(name, (args ?? {}), result);
|
|
810
|
+
const fallbackJson = JSON.stringify(result, null, 2);
|
|
811
|
+
const content = [];
|
|
812
|
+
if (rendered?.html) {
|
|
813
|
+
content.push({
|
|
814
|
+
type: "resource",
|
|
815
|
+
resource: {
|
|
816
|
+
uri: rendered.html.uri,
|
|
817
|
+
mimeType: "text/html",
|
|
818
|
+
text: rendered.html.html,
|
|
819
|
+
},
|
|
820
|
+
_meta: {
|
|
821
|
+
"mcpui.dev/ui-preferred-frame-size": ["100%", "auto"],
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (rendered?.markdown) {
|
|
826
|
+
content.push({ type: "text", text: rendered.markdown });
|
|
827
|
+
}
|
|
828
|
+
content.push({ type: "text", text: fallbackJson });
|
|
829
|
+
return { content };
|
|
830
|
+
}
|
|
831
|
+
catch (err) {
|
|
832
|
+
return {
|
|
833
|
+
content: [
|
|
834
|
+
{
|
|
835
|
+
type: "text",
|
|
836
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
isError: true,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// Start
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
const transport = new StdioServerTransport();
|
|
847
|
+
await server.connect(transport);
|