agent-feedbacker 0.0.0-stage → 0.2.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.
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <path d="M9 4h14a6 6 0 0 1 6 6v8a6 6 0 0 1-6 6H13l-6 5v-5.3A6 6 0 0 1 3 18v-8a6 6 0 0 1 6-6z" fill="#ffe766" stroke="#1b1f2a" stroke-width="2" stroke-linejoin="round"/>
3
+ <rect x="9" y="10.5" width="14" height="2.8" rx="1.4" fill="#1b1f2a"/>
4
+ <rect x="9" y="15.5" width="8.5" height="2.8" rx="1.4" fill="#1b1f2a"/>
5
+ </svg>
package/src/mcp.mjs ADDED
@@ -0,0 +1,143 @@
1
+ // Minimal MCP server over stdio (newline-delimited JSON-RPC 2.0) with one tool,
2
+ // request_review, which blocks until the user submits the review page.
3
+
4
+ import fs from 'node:fs';
5
+ import readline from 'node:readline';
6
+ import { buildReview, hasChanges, startReview, toMarkdown } from './core.mjs';
7
+
8
+ const { version: VERSION } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
9
+ const PROTOCOL = '2025-06-18';
10
+
11
+ const TOOL = {
12
+ name: 'request_review',
13
+ title: 'Request review from the user',
14
+ description:
15
+ 'Open a merge-request-style review page in the user\'s browser with the diffs of the given git repos ' +
16
+ '(working tree, including untracked files, against a base). The user comments on lines, answers your ' +
17
+ 'questions and submits. Blocks until they do, then returns their feedback as markdown: a verdict ' +
18
+ '(approved / changes requested), a general comment, answers, and line comments with the code they refer to. ' +
19
+ 'Pass every repo and worktree you changed in this session, as absolute paths.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ repos: {
24
+ type: 'array',
25
+ minItems: 1,
26
+ description: 'Repos to review.',
27
+ items: {
28
+ type: 'object',
29
+ properties: {
30
+ path: { type: 'string', description: 'Absolute path to the repo (or any folder inside it).' },
31
+ base: { type: 'string', description: 'Commit, branch or tag to diff against. Default HEAD (uncommitted changes only). With a branch, the diff starts where HEAD forked from it, like a merge request.' },
32
+ exact: { type: 'boolean', description: 'Diff against base itself, not the point where HEAD forked from it. Set by the session-baseline hook; rarely needed by hand.' },
33
+ session: { type: 'boolean', description: 'Set by the session-baseline hook: base is where the repo stood when this session started.' },
34
+ worktrees: { type: 'boolean', description: 'Also review every other git worktree of this repo (e.g. parallel agents\' work), each against the same base.' },
35
+ },
36
+ required: ['path'],
37
+ },
38
+ },
39
+ summary: { type: 'string', description: 'Short summary of what you changed and why, shown at the top of the page.' },
40
+ questions: { type: 'array', items: { type: 'string' }, description: 'Questions for the user to answer next to the diff.' },
41
+ },
42
+ required: ['repos'],
43
+ },
44
+ };
45
+
46
+ const pending = new Map(); // request id -> close() of its review server
47
+
48
+ function send(msg) {
49
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...msg }) + '\n');
50
+ }
51
+
52
+ // To stderr and as an MCP log message, since not every client shows stderr.
53
+ function log(msg) {
54
+ process.stderr.write(`feedbacker: ${msg}\n`);
55
+ send({ method: 'notifications/message', params: { level: 'info', logger: 'feedbacker', data: msg } });
56
+ }
57
+
58
+ async function callTool(id, params) {
59
+ if (params?.name !== TOOL.name) throw rpcError(-32602, `Unknown tool: ${params?.name}`);
60
+ const args = params.arguments || {};
61
+ if (!Array.isArray(args.repos) || !args.repos.length) {
62
+ return toolError('`repos` must be a non-empty array of { path, base? }.');
63
+ }
64
+ const review = buildReview({
65
+ repos: args.repos.map((r) => (typeof r === 'string' ? r : { path: r.path, base: r.base, worktrees: r.worktrees, exact: r.exact, session: r.session })),
66
+ summary: args.summary || '',
67
+ questions: Array.isArray(args.questions) ? args.questions.map(String) : [],
68
+ });
69
+ const errors = review.repos.filter((r) => r.error).map((r) => `${r.path}: ${r.error}`);
70
+ if (!hasChanges(review) && !review.questions.length) {
71
+ return toolError(['No changes to review.', ...errors].join('\n'));
72
+ }
73
+
74
+ const open = !process.env.FEEDBACKER_NO_OPEN;
75
+ const { url, feedback, close } = await startReview(review, { open });
76
+ log(`waiting for review at ${url}`);
77
+
78
+ // Keep long waits alive for clients that reset their timeout on progress.
79
+ const token = params._meta?.progressToken;
80
+ let progress = 0;
81
+ const timer = token !== undefined
82
+ ? setInterval(() => send({ method: 'notifications/progress', params: { progressToken: token, progress: ++progress, message: 'Waiting for the user to submit the review' } }), 30_000)
83
+ : null;
84
+
85
+ try {
86
+ const fb = await Promise.race([feedback, new Promise((_, no) => pending.set(id, () => { close(); no(new Error('cancelled')); }))]);
87
+ let text = toMarkdown(review, fb);
88
+ if (errors.length) text += `\nRepos that could not be read:\n${errors.map((e) => `- ${e}`).join('\n')}\n`;
89
+ return { content: [{ type: 'text', text }] };
90
+ } finally {
91
+ clearInterval(timer);
92
+ pending.delete(id);
93
+ }
94
+ }
95
+
96
+ function toolError(text) {
97
+ return { content: [{ type: 'text', text }], isError: true };
98
+ }
99
+
100
+ function rpcError(code, message) {
101
+ return Object.assign(new Error(message), { code });
102
+ }
103
+
104
+ async function handle(msg) {
105
+ const { id, method, params } = msg;
106
+ if (method === 'initialize') {
107
+ return {
108
+ protocolVersion: params?.protocolVersion || PROTOCOL,
109
+ capabilities: { tools: {}, logging: {} },
110
+ serverInfo: { name: 'feedbacker', version: VERSION },
111
+ };
112
+ }
113
+ if (method === 'ping' || method === 'logging/setLevel') return {};
114
+ if (method === 'tools/list') return { tools: [TOOL] };
115
+ if (method === 'tools/call') return callTool(id, params);
116
+ throw rpcError(-32601, `Method not found: ${method}`);
117
+ }
118
+
119
+ export function runMcp() {
120
+ const rl = readline.createInterface({ input: process.stdin });
121
+ rl.on('line', async (line) => {
122
+ if (!line.trim()) return;
123
+ let msg;
124
+ try {
125
+ msg = JSON.parse(line);
126
+ } catch {
127
+ send({ id: null, error: { code: -32700, message: 'Parse error' } });
128
+ return;
129
+ }
130
+ if (msg.id === undefined) {
131
+ // Notifications: only cancellation matters to us.
132
+ if (msg.method === 'notifications/cancelled') pending.get(msg.params?.requestId)?.();
133
+ return;
134
+ }
135
+ try {
136
+ send({ id: msg.id, result: await handle(msg) });
137
+ } catch (e) {
138
+ if (e.message === 'cancelled') return;
139
+ send({ id: msg.id, error: { code: e.code || -32603, message: e.message } });
140
+ }
141
+ });
142
+ rl.on('close', () => process.exit(0));
143
+ }
package/src/page.html ADDED
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Feedbacker</title>
7
+ <link rel="icon" href="/dist/favicon.svg" type="image/svg+xml">
8
+ <link rel="stylesheet" href="/dist/app.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script src="/dist/app.js"></script>
13
+ </body>
14
+ </html>