@ai-setting/roy-task-plugin 0.6.1 → 0.7.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.
@@ -3,14 +3,25 @@
3
3
  * roy-task MCP server — exposes the task lifecycle to Claude Code as native
4
4
  * MCP tools.
5
5
  *
6
- * ## Protocol
6
+ * ## What this file is
7
+ *
8
+ * A **transport**: newline-delimited JSON-RPC 2.0 over stdin/stdout, and
9
+ * nothing else. The protocol handling, the tool catalogue, the tool
10
+ * execution and every task semantic live in
11
+ * `@ai-setting/roy-agent-core/env/task/mcp`.
7
12
  *
8
- * Newline-delimited JSON-RPC 2.0 over stdin/stdout, per the MCP stdio
9
- * transport. We implement exactly the methods a task server needs:
13
+ * Before Task #4619 this file (281 lines) plus `bridge.mjs` (482 lines) plus
14
+ * `tools.mjs` (240 lines) re-implemented the whole task surface: the tool
15
+ * catalogue was hand-copied, and the tools were raw SQL against roy's own
16
+ * database. Now the plugin contributes **transport + wiring** and nothing
17
+ * else.
18
+ *
19
+ * ## Protocol
10
20
  *
11
21
  * | method | direction | notes |
12
22
  * |-------------------------|-----------------|----------------------------------|
13
23
  * | `initialize` | client -> server | we negotiate the protocol version |
24
+ * | `server/discover` | client -> server | Claude Code 2.1.285's pre-handshake probe |
14
25
  * | `notifications/initialized` | client -> server | acknowledged by doing nothing |
15
26
  * | `tools/list` | client -> server | the tool catalogue |
16
27
  * | `tools/call` | client -> server | run one tool |
@@ -36,16 +47,25 @@
36
47
 
37
48
  import { createInterface } from "node:readline";
38
49
 
39
- import { callTool, openStore, BridgeError, defaultTaskDbPath } from "./bridge.mjs";
50
+ import { createServer, BridgeError, defaultTaskDbPath } from "./bridge.mjs";
40
51
  import {
41
- MCP_TOOLS,
52
+ JSON_RPC_ERRORS,
42
53
  SUPPORTED_PROTOCOL_VERSIONS,
43
54
  DEFAULT_PROTOCOL_VERSION,
44
- JSON_RPC_ERRORS,
45
55
  } from "./tools.mjs";
56
+ import { setStdoutStrictMode } from "@ai-setting/roy-agent-core/env/log-trace";
46
57
 
47
- const SERVER_NAME = "roy-task";
48
- const SERVER_VERSION = "0.5.0";
58
+ // ─────────────────────────────────────────────────────────────────────────
59
+ // stdout 归协议,stderr 归诊断(Task #4619)
60
+ //
61
+ // MCP 的 stdio 传输是「按行分隔的 JSON-RPC」,跑在 stdout 上。core 的 logger
62
+ // 原本把 `info` 打到 stdout,task store 的 migration 也用 `console.log` 打印
63
+ // 横幅 —— 实测本 server **26 行 stdout 里有 24 行不是 JSON**,客户端会因此
64
+ // 丢弃整个 server(Claude Code 报 `status: "failed"`)。
65
+ //
66
+ // 所以这里开 core 的 stdout-strict 模式:日志全走 stderr,stdout 只留给协议。
67
+ // ─────────────────────────────────────────────────────────────────────────
68
+ setStdoutStrictMode(true);
49
69
 
50
70
  /**
51
71
  * Write one JSON-RPC message to stdout.
@@ -71,23 +91,7 @@ function sendResult(id, result) {
71
91
  }
72
92
 
73
93
  function sendError(id, code, message, data) {
74
- const error = { code, message };
75
- if (data !== undefined) error.data = data;
76
- send({ jsonrpc: "2.0", id, error });
77
- }
78
-
79
- /**
80
- * Pick the protocol version to answer with.
81
- *
82
- * Echo the client's version when we know it; otherwise offer ours. This is
83
- * what the official SDK does, and it is what lets an older Claude Code talk to
84
- * a newer server without a hard failure.
85
- */
86
- export function negotiateProtocolVersion(requested) {
87
- if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
88
- return requested;
89
- }
90
- return DEFAULT_PROTOCOL_VERSION;
94
+ send({ jsonrpc: "2.0", id, error: { code, message, ...(data ? { data } : {}) } });
91
95
  }
92
96
 
93
97
  /** Turn a thrown error into an MCP tool result with `isError: true`. */
@@ -98,23 +102,17 @@ function toolError(message) {
98
102
  };
99
103
  }
100
104
 
101
- /**
102
- * The server's identity, shared by `initialize` and `server/discover`.
103
- */
104
- const SERVER_INFO = { name: SERVER_NAME, version: SERVER_VERSION };
105
-
106
- /**
107
- * The capabilities we advertise.
108
- */
109
- const SERVER_CAPABILITIES = { tools: { listChanged: false } };
110
-
111
105
  /**
112
106
  * Handle one parsed JSON-RPC request.
113
107
  *
108
+ * The dispatch is delegated to core's MCP server; this function only adapts
109
+ * the **shape** (core returns `result` / `error` objects, this module writes
110
+ * them with the `jsonrpc` envelope).
111
+ *
114
112
  * Exported for tests: the pure part (dispatch) is separated from the I/O
115
113
  * part (stdin/stdout) so the protocol can be exercised without a pipe.
116
114
  */
117
- export function handleRequest(request, db) {
115
+ export async function handleRequest(request, server) {
118
116
  const { id, method, params } = request;
119
117
 
120
118
  // Notifications have no id and must not be answered.
@@ -155,9 +153,6 @@ export function handleRequest(request, db) {
155
153
  * `{"name":"plugin:roy-task:roy-task","status":"failed"}`; with it
156
154
  * handled the same event reported `status:"connected"`.)
157
155
  *
158
- * The response shape mirrors `initialize` — the server describes itself
159
- * — so the two handlers share `SERVER_INFO` / `SERVER_CAPABILITIES`.
160
- *
161
156
  * This method is **not** in @modelcontextprotocol/sdk 1.32.1, which is why
162
157
  * an SDK-based client never exercises it and a hand-rolled server can
163
158
  * pass its own tests while failing inside Claude Code.
@@ -182,7 +177,7 @@ export function handleRequest(request, db) {
182
177
  return { id, result: {} };
183
178
 
184
179
  case "tools/list":
185
- return { id, result: { tools: MCP_TOOLS } };
180
+ return { id, result: { tools: server.listTools() } };
186
181
 
187
182
  case "tools/call": {
188
183
  const toolName = params && params.name;
@@ -197,7 +192,7 @@ export function handleRequest(request, db) {
197
192
  };
198
193
  }
199
194
  try {
200
- const value = callTool(db, toolName, args);
195
+ const value = await server.callTool(toolName, args);
201
196
  return {
202
197
  id,
203
198
  result: {
@@ -224,12 +219,38 @@ export function handleRequest(request, db) {
224
219
  }
225
220
  }
226
221
 
222
+ /**
223
+ * Pick the protocol version to answer with.
224
+ *
225
+ * Echo the client's version when we know it; otherwise offer ours. This is
226
+ * what the official SDK does, and it is what lets an older Claude Code talk to
227
+ * a newer server without a hard failure.
228
+ */
229
+ export function negotiateProtocolVersion(requested) {
230
+ if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
231
+ return requested;
232
+ }
233
+ return DEFAULT_PROTOCOL_VERSION;
234
+ }
235
+
236
+ /**
237
+ * The server's identity, shared by `initialize` and `server/discover`.
238
+ */
239
+ const SERVER_NAME = "roy-task";
240
+ const SERVER_VERSION = "1.0.0";
241
+ const SERVER_INFO = { name: SERVER_NAME, version: SERVER_VERSION };
242
+
243
+ /**
244
+ * The capabilities we advertise.
245
+ */
246
+ const SERVER_CAPABILITIES = { tools: { listChanged: false } };
247
+
227
248
  /** Read newline-delimited JSON-RPC from stdin and answer on stdout. */
228
249
  export function main() {
229
250
  const dbPath = process.env.ROY_TASK_DB_PATH || defaultTaskDbPath();
230
- let db;
251
+ let server;
231
252
  try {
232
- db = openStore(dbPath);
253
+ server = createServer(dbPath);
233
254
  } catch (err) {
234
255
  process.stderr.write(
235
256
  `roy-task mcp server: cannot open task store at ${dbPath}: ${err && err.message}\n`,
@@ -256,16 +277,20 @@ export function main() {
256
277
  return;
257
278
  }
258
279
 
259
- const response = handleRequest(request, db);
260
- if (response) send(response);
280
+ Promise.resolve(handleRequest(request, server))
281
+ .then((response) => {
282
+ if (response) send(response);
283
+ })
284
+ .catch((err) => {
285
+ sendError(
286
+ request.id ?? null,
287
+ JSON_RPC_ERRORS.INTERNAL_ERROR,
288
+ err && err.message ? err.message : String(err),
289
+ );
290
+ });
261
291
  });
262
292
 
263
293
  rl.on("close", () => {
264
- try {
265
- db.close();
266
- } catch {
267
- // already closed
268
- }
269
294
  process.exit(0);
270
295
  });
271
296
  }
@@ -1,240 +1,54 @@
1
1
  /**
2
- * Zero-dependency MCP server exposing the roy task tools over stdio.
2
+ * The MCP tool catalogue — **derived from roy-agent's core**.
3
3
  *
4
- * ## Why hand-rolled instead of @modelcontextprotocol/sdk
4
+ * ## What this file used to be
5
5
  *
6
- * The published npm package (`@ai-setting/roy-task-plugin`) is installed on
7
- * plain Node.js machines with no native build step. Adding the SDK would add a
8
- * dependency tree to a package whose whole point is "install and it runs".
9
- * The MCP wire format we need is small and stable:
6
+ * Before Task #4619 it was a hand-written catalogue: 240 lines of
7
+ * hand-copied tool names, descriptions and JSON Schemas for the eight task
8
+ * tools. It was a **second copy** of core's tool definitions, so it drifted:
9
+ * core's `task_create` gained a `context` field, a `type` enum and a
10
+ * `project_path` field, and the plugin's copy did not — so a Claude Code
11
+ * client could not pass the arguments the real tool accepted.
10
12
  *
11
- * - newline-delimited JSON-RPC 2.0 on stdin/stdout
12
- * - `initialize` / `tools/list` / `tools/call` (plus `notifications/initialized`)
13
+ * ## What it is now
13
14
  *
14
- * That is implemented here in ~200 lines with nothing but `node:child_process`.
15
- * The protocol version is negotiated: we echo the client's requested version
16
- * when it is one we know, else we answer with our own.
15
+ * A ~20-line re-export of core's own tool definitions. The names,
16
+ * descriptions and JSON Schema come from the **same factories** the tool
17
+ * execution uses, so they cannot drift.
17
18
  *
18
- * ## Where the data comes from
19
- *
20
- * The task store is **roy-agent's** store (`~/.local/share/roy-agent/tasks.db`),
21
- * not the plugin's own `~/.roy-task-plugin/tasks.db`. The reason is
22
- * interoperability: the roy agent inside opencode/claude creates tasks through
23
- * `roy-agent`'s own task tools, and the progress card the daemon sends is
24
- * rendered from the *same* rows. If this MCP server wrote to a different
25
- * database, Claude would create tasks the card can never see.
26
- *
27
- * So the tools shell out to the `roy-agent tasks …` CLI, which is the stable,
28
- * already-migrated, already-schema-validated way to reach that store. We do
29
- * not re-implement the store, and we do not open the SQLite file ourselves.
30
- *
31
- * ## stdio hygiene
32
- *
33
- * This process is spawned by Claude Code as an MCP server. It must:
34
- *
35
- * - **read** stdin (that is the transport) — obviously not `ignore` here;
36
- * - **never** write anything but protocol messages to stdout. A stray
37
- * `console.log` would corrupt the stream, so every log line goes to stderr.
38
- *
39
- * The "stdin must be ignore" rule from the daemon's spawn adapter applies to
40
- * *agent* subprocesses, not to an MCP server — an MCP server whose stdin is
41
- * closed can never receive a single request.
19
+ * The MCP wire protocol constants (protocol versions, JSON-RPC error codes)
20
+ * also come from core now — they are part of the same contract.
42
21
  */
43
22
 
44
- export const SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
45
- "2025-11-25",
46
- "2025-06-18",
47
- "2025-03-26",
48
- "2024-11-05",
49
- "2024-10-07",
50
- ]);
23
+ import {
24
+ createTaskMcpToolDefinitions,
25
+ TASK_MCP_SUPPORTED_PROTOCOL_VERSIONS,
26
+ TASK_MCP_DEFAULT_PROTOCOL_VERSION,
27
+ TASK_MCP_JSON_RPC_ERRORS,
28
+ } from "@ai-setting/roy-agent-core/env/task/mcp";
29
+ import { TaskStoreBackend, createSQLiteTaskPort } from "@ai-setting/roy-agent-core/env/task/backend";
30
+ import { SQLiteTaskStore } from "@ai-setting/roy-agent-core/env/task/storage";
31
+
32
+ /** The MCP protocol versions we accept. */
33
+ export const SUPPORTED_PROTOCOL_VERSIONS = TASK_MCP_SUPPORTED_PROTOCOL_VERSIONS;
51
34
 
52
- export const DEFAULT_PROTOCOL_VERSION = "2025-11-25";
35
+ /** The protocol version we answer with when the client's is unknown. */
36
+ export const DEFAULT_PROTOCOL_VERSION = TASK_MCP_DEFAULT_PROTOCOL_VERSION;
53
37
 
54
38
  /** The JSON-RPC error codes we use. */
55
- export const JSON_RPC_ERRORS = Object.freeze({
56
- PARSE_ERROR: -32700,
57
- INVALID_REQUEST: -32600,
58
- METHOD_NOT_FOUND: -32601,
59
- INVALID_PARAMS: -32602,
60
- INTERNAL_ERROR: -32603,
61
- });
39
+ export const JSON_RPC_ERRORS = TASK_MCP_JSON_RPC_ERRORS;
62
40
 
63
41
  /**
64
- * The task tools we expose.
42
+ * The task tools we expose — derived from core's own tool factories.
65
43
  *
66
- * The names mirror roy-agent's own tool names (`task_create`, `task_list`, …)
67
- * so the mental model transfers one-to-one.
44
+ * The factories never touch the store while building a definition, so an
45
+ * **in-memory** store is enough here. Using the real path would create the
46
+ * database file as a side effect of *importing* this module — a surprising
47
+ * side effect for a module that is only supposed to describe the tool
48
+ * catalogue.
68
49
  */
69
- export const MCP_TOOLS = Object.freeze([
70
- {
71
- name: "task_tree",
72
- description:
73
- "Show the task tree. Call this BEFORE creating anything, to decide whether new work belongs under an existing root (parent_task_id) or is genuinely new.",
74
- inputSchema: {
75
- type: "object",
76
- properties: {
77
- depth: {
78
- type: "number",
79
- description: "Tree depth. 1 = roots + their direct children. -1 = unlimited.",
80
- },
81
- parent_task_id: {
82
- type: "number",
83
- description: "Only show the subtree under this task id.",
84
- },
85
- },
86
- },
87
- },
88
- {
89
- name: "task_create",
90
- description:
91
- "Create a tracked task. Task is a first-class citizen: create the task BEFORE doing the work, then update it as you go and complete it when done.",
92
- inputSchema: {
93
- type: "object",
94
- properties: {
95
- title: { type: "string", description: "Imperative, specific one-line title." },
96
- description: { type: "string", description: "What needs to be done and why." },
97
- goals_and_expected_deliverables: {
98
- type: "string",
99
- description: "Success criteria and expected outputs.",
100
- },
101
- priority: {
102
- type: "string",
103
- enum: ["low", "medium", "high"],
104
- description: "Task priority.",
105
- },
106
- type: {
107
- type: "string",
108
- enum: ["normal", "cycle", "longterm"],
109
- description: "normal = one-off; cycle = periodic; longterm = ongoing goal.",
110
- },
111
- parent_task_id: {
112
- type: "number",
113
- description: "Parent task id. Omit to create a root task.",
114
- },
115
- project_path: { type: "string", description: "Project root path." },
116
- context: {
117
- type: "string",
118
- description: "JSON string with worktree_path / branch / type etc.",
119
- },
120
- tags: {
121
- type: "array",
122
- items: { type: "string" },
123
- description: "Task tags.",
124
- },
125
- },
126
- required: ["title"],
127
- },
128
- },
129
- {
130
- name: "task_get",
131
- description: "Get one task's full detail, including its operation records.",
132
- inputSchema: {
133
- type: "object",
134
- properties: { id: { type: "number", description: "Task id." } },
135
- required: ["id"],
136
- },
137
- },
138
- {
139
- name: "task_list",
140
- description: "List tasks. Supports filtering by status / priority / type.",
141
- inputSchema: {
142
- type: "object",
143
- properties: {
144
- status: {
145
- type: "string",
146
- enum: ["todo", "active", "completed", "paused", "cancelled", "archived"],
147
- },
148
- priority: { type: "string", enum: ["low", "medium", "high"] },
149
- type: { type: "string", enum: ["normal", "cycle", "longterm"] },
150
- limit: { type: "number", description: "Max rows (default 20)." },
151
- offset: { type: "number", description: "Row offset." },
152
- },
153
- },
154
- },
155
- {
156
- name: "task_update",
157
- description:
158
- "Update a task's status, progress or description. Report progress as you go, not only at the end.",
159
- inputSchema: {
160
- type: "object",
161
- properties: {
162
- id: { type: "number", description: "Task id." },
163
- status: {
164
- type: "string",
165
- enum: ["todo", "active", "completed", "paused", "cancelled", "archived"],
166
- },
167
- progress: { type: "number", description: "0-100." },
168
- current_status: { type: "string", description: "One sentence: where you are right now." },
169
- description: { type: "string", description: "Replace the description." },
170
- goals_and_expected_deliverables: { type: "string", description: "Replace the goals." },
171
- priority: { type: "string", enum: ["low", "medium", "high"] },
172
- },
173
- required: ["id"],
174
- },
175
- },
176
- {
177
- name: "task_operation_create",
178
- description:
179
- "Record a milestone (what was reached) and, optionally, a process summary (how you got there, 100-150 chars). Milestones mark state transitions: RED->GREEN, blocker found, decision made.",
180
- inputSchema: {
181
- type: "object",
182
- properties: {
183
- task_id: { type: "number", description: "Task id." },
184
- milestone_type: {
185
- type: "string",
186
- enum: [
187
- "create",
188
- "progress",
189
- "milestone",
190
- "problem",
191
- "solution",
192
- "decision",
193
- "review",
194
- "completed",
195
- ],
196
- },
197
- milestone_title: { type: "string", description: "One-line milestone title." },
198
- milestone_description: { type: "string", description: "What was reached." },
199
- process_description: {
200
- type: "string",
201
- description: "How you got there. 100-150 characters, strictly.",
202
- },
203
- },
204
- required: ["task_id", "milestone_type", "milestone_title"],
205
- },
206
- },
207
- {
208
- name: "task_complete",
209
- description:
210
- "Close out a task. Provide deliverable_storage_path so the outcome is traceable.",
211
- inputSchema: {
212
- type: "object",
213
- properties: {
214
- id: { type: "number", description: "Task id." },
215
- deliverable_storage_path: {
216
- type: "string",
217
- description: "Where the work product lives (path / URL / branch).",
218
- },
219
- },
220
- required: ["id"],
221
- },
222
- },
223
- {
224
- name: "task_search",
225
- description:
226
- "Search tasks by keywords across title, description, goals, tags and operation records.",
227
- inputSchema: {
228
- type: "object",
229
- properties: {
230
- keywords: {
231
- type: "array",
232
- items: { type: "string" },
233
- description: "Keywords (AND logic).",
234
- },
235
- limit: { type: "number" },
236
- },
237
- required: ["keywords"],
238
- },
239
- },
240
- ]);
50
+ export const MCP_TOOLS = Object.freeze(
51
+ createTaskMcpToolDefinitions(
52
+ new TaskStoreBackend(createSQLiteTaskPort(new SQLiteTaskStore(":memory:"))),
53
+ ),
54
+ );
package/dist/cli.js CHANGED
@@ -1154,12 +1154,13 @@ function createRoyAgentCardPort(options = {}) {
1154
1154
  let inFlight;
1155
1155
  const loadNotifier = async () => {
1156
1156
  const mod = await import("@ai-setting/roy-agent-core/env/task/plugins");
1157
- const hook = mod.createLarkCliTaskNotifyHook({ enabled: true, cardMode: true });
1158
- const maybeNotifier = hook.cardNotifier;
1159
- if (!maybeNotifier) {
1160
- throw new Error("roy-agent did not expose cardNotifier from createLarkCliTaskNotifyHook");
1157
+ if (typeof mod.LarkCardPatchNotifier !== "function") {
1158
+ throw new Error("roy-agent did not export LarkCardPatchNotifier from env/task/plugins");
1161
1159
  }
1162
- notifier = maybeNotifier;
1160
+ notifier = new mod.LarkCardPatchNotifier({
1161
+ pinOnStart: true,
1162
+ unpinOnFinish: true
1163
+ });
1163
1164
  return notifier;
1164
1165
  };
1165
1166
  const getNotifier = () => {
@@ -2483,7 +2484,7 @@ function formatStatus(report) {
2483
2484
  }
2484
2485
 
2485
2486
  // ../../../packages/cli/src/main.ts
2486
- var VERSION = "0.6.1";
2487
+ var VERSION = "0.7.0";
2487
2488
  async function main(argv) {
2488
2489
  const parsed = parseArgs(argv);
2489
2490
  switch (parsed.command) {