@open-and-async/mcp 0.0.1

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,305 @@
1
+ /**
2
+ * Method tools — pure utility, zero book prose.
3
+ *
4
+ * These render the book's frameworks as *actions*: templates and rubrics a team
5
+ * can run, not content to read. They contain no manuscript text, so they carry
6
+ * no extraction risk and ship instantly. They're also the best demo of the
7
+ * book's own "agentic workflows" thesis — the method, in the user's editor.
8
+ */
9
+
10
+ import { z } from "zod";
11
+ import { text } from "../data.js";
12
+
13
+ export function registerMethodTools(server) {
14
+ server.registerTool(
15
+ "draft_decision_doc",
16
+ {
17
+ title: "Draft a decision doc (ADR)",
18
+ description:
19
+ "Turn a decision plus its options into a structured, async-friendly " +
20
+ "decision record (ADR): context, options with tradeoffs, the decision, " +
21
+ "and its reversibility. Write the decision down so others can follow it " +
22
+ "without a meeting.",
23
+ inputSchema: {
24
+ decision: z.string().describe("The decision to be made, in one line."),
25
+ options: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe("Candidate options. Two or three is plenty."),
29
+ context: z
30
+ .string()
31
+ .optional()
32
+ .describe("Background: what forces this decision now."),
33
+ },
34
+ },
35
+ async ({ decision, options = [], context }) => {
36
+ const opts =
37
+ options.length > 0
38
+ ? options
39
+ : ["Option A", "Option B", "Do nothing (status quo)"];
40
+ const optionBlocks = opts
41
+ .map(
42
+ (o, i) =>
43
+ `### Option ${i + 1}: ${o}\n\n` +
44
+ `- **Pros:** \n- **Cons:** \n- **Cost / effort:** \n- **Risk:** `,
45
+ )
46
+ .join("\n\n");
47
+
48
+ const doc = [
49
+ `# Decision: ${decision}`,
50
+ ``,
51
+ `**Status:** Proposed · **Driver (DRI):** @you · **Date:** YYYY-MM-DD`,
52
+ ``,
53
+ `## Context`,
54
+ ``,
55
+ context ? context : `_Why are we deciding this now? What changed?_`,
56
+ ``,
57
+ `## Options considered`,
58
+ ``,
59
+ optionBlocks,
60
+ ``,
61
+ `## Decision`,
62
+ ``,
63
+ `_State the chosen option and the one-sentence reason._`,
64
+ ``,
65
+ `## Reversibility`,
66
+ ``,
67
+ `_Is this a one-way door or a two-way door? If we're wrong, what's the ` +
68
+ `cost to undo it? (Two-way doors deserve a faster, lighter process.)_`,
69
+ ``,
70
+ `## Open questions / dissent`,
71
+ ``,
72
+ `_Capture unresolved concerns here so silence isn't mistaken for ` +
73
+ `consent. Invite disagreement explicitly._`,
74
+ ].join("\n");
75
+
76
+ return text(doc);
77
+ },
78
+ );
79
+
80
+ server.registerTool(
81
+ "convert_meeting_to_async",
82
+ {
83
+ title: "Convert a meeting to async",
84
+ description:
85
+ "Given a meeting's purpose or agenda, propose the async equivalent: " +
86
+ "the artifact that replaces it, where it lives, who decides, and the " +
87
+ "deadline. Meetings are a point of escalation, not the default.",
88
+ inputSchema: {
89
+ purpose: z
90
+ .string()
91
+ .describe("Why the meeting exists / what it's meant to accomplish."),
92
+ agenda: z
93
+ .array(z.string())
94
+ .optional()
95
+ .describe("Agenda items, if any."),
96
+ },
97
+ },
98
+ async ({ purpose, agenda = [] }) => {
99
+ const items =
100
+ agenda.length > 0
101
+ ? agenda
102
+ : ["(no agenda supplied — list the topics the meeting would cover)"];
103
+ const rows = items
104
+ .map(
105
+ (item) =>
106
+ `| ${item} | Issue / doc / PR comment? | @owner | When? |`,
107
+ )
108
+ .join("\n");
109
+
110
+ const out = [
111
+ `## Async replacement for: ${purpose}`,
112
+ ``,
113
+ `**First question — does this need to be synchronous at all?** Most ` +
114
+ `meetings exist to share information or make a decision; both are ` +
115
+ `better done in writing, where they're searchable, inclusive of every ` +
116
+ `time zone, and create a durable record.`,
117
+ ``,
118
+ `### The artifact that replaces it`,
119
+ ``,
120
+ `- **Format:** an issue (for a decision or task), a doc/PR (for a ` +
121
+ `proposal to review), or a recorded Loom + thread (if something must ` +
122
+ `be shown).`,
123
+ `- **Where it lives:** link it from the relevant repo/project so it has ` +
124
+ `a URL and is discoverable.`,
125
+ `- **Decision owner (DRI):** name one person who decides.`,
126
+ `- **Deadline:** set a "decide by" date so async doesn't mean ` +
127
+ `never.`,
128
+ ``,
129
+ `### Agenda → async`,
130
+ ``,
131
+ `| Topic | Replace with | Owner | Decide by |`,
132
+ `| --- | --- | --- | --- |`,
133
+ rows,
134
+ ``,
135
+ `### Keep it sync only if`,
136
+ ``,
137
+ `- It's a genuine debate with high ambiguity and fast back-and-forth, or`,
138
+ `- It's sensitive/personal (feedback, conflict, bad news), or`,
139
+ `- You've tried async and it's thrashing.`,
140
+ ``,
141
+ `If you do meet, the meeting's job is to **produce the artifact above** ` +
142
+ `— pre-read sent, notes and decision posted back to the URL after.`,
143
+ ].join("\n");
144
+
145
+ return text(out);
146
+ },
147
+ );
148
+
149
+ server.registerTool(
150
+ "score_status_update",
151
+ {
152
+ title: "Score a status update",
153
+ description:
154
+ "Score a draft status update against the 'work loudly / no surprises' " +
155
+ "rubric and suggest fixes. Good updates surface blockers early, state " +
156
+ "outcomes over activity, and link to the work.",
157
+ inputSchema: {
158
+ update: z.string().describe("The draft status update to score."),
159
+ },
160
+ },
161
+ async ({ update }) => {
162
+ const u = update.toLowerCase();
163
+ const checks = [
164
+ {
165
+ name: "Outcome over activity",
166
+ pass: /\b(ship|shipped|done|landed|merged|decided|unblocked|delivered|launched|fixed|resolved)\b/.test(
167
+ u,
168
+ ),
169
+ fix: "Lead with what changed for others, not what you were busy with. “Worked on X” → “Shipped X; it unblocks Y.”",
170
+ },
171
+ {
172
+ name: "Names blockers / risks",
173
+ pass: /\b(block|blocked|blocker|risk|stuck|waiting on|delayed|slip|at risk|need help)\b/.test(
174
+ u,
175
+ ),
176
+ fix: "No surprises: name what's at risk *before* it's late. If nothing is blocked, say so explicitly (“no blockers”).",
177
+ },
178
+ {
179
+ name: "Links to the work (URL-first)",
180
+ pass: /(https?:\/\/|#\d+|\bPR\b|\bissue\b|\/pull\/|\/issues\/)/i.test(
181
+ update,
182
+ ),
183
+ fix: "Link the issue/PR/doc. A status update without a URL makes people ask follow-up questions — the opposite of working loudly.",
184
+ },
185
+ {
186
+ name: "Clear next step / ask",
187
+ pass: /\b(next|then|will|plan to|need|asking|by (mon|tue|wed|thu|fri|monday|tuesday|wednesday|thursday|friday|\d))/.test(
188
+ u,
189
+ ),
190
+ fix: "End with the next concrete step and any ask, with a date. Make it trivial for someone to help.",
191
+ },
192
+ {
193
+ name: "Skimmable (not a wall of text)",
194
+ pass: update.length < 600 || /[-*]\s|\n/.test(update),
195
+ fix: "Break it into bullets. A wall of text gets skimmed or skipped; structure it so the headline lands in two seconds.",
196
+ },
197
+ ];
198
+
199
+ const passed = checks.filter((c) => c.pass).length;
200
+ const score = Math.round((passed / checks.length) * 100);
201
+ const lines = checks.map(
202
+ (c) => `${c.pass ? "✅" : "⚠️"} **${c.name}**${c.pass ? "" : ` — ${c.fix}`}`,
203
+ );
204
+
205
+ const out = [
206
+ `## Status update score: ${score}/100 (${passed}/${checks.length} checks)`,
207
+ ``,
208
+ ...lines,
209
+ ``,
210
+ passed === checks.length
211
+ ? `This update works loudly: outcome-first, no surprises, linked, and skimmable. Ship it.`
212
+ : `Tighten the ⚠️ items above. The bar: someone three time zones away should know what changed and what's at risk without asking you a single follow-up.`,
213
+ ].join("\n");
214
+
215
+ return text(out);
216
+ },
217
+ );
218
+
219
+ server.registerTool(
220
+ "run_async_standup",
221
+ {
222
+ title: "Async standup template",
223
+ description:
224
+ "Return a structured async-standup prompt a team can adopt in any chat " +
225
+ "or issue — replaces the daily sync standup with a written, searchable " +
226
+ "thread that respects every time zone.",
227
+ inputSchema: {
228
+ cadence: z
229
+ .string()
230
+ .optional()
231
+ .describe("e.g. 'daily', 'twice a week'. Defaults to daily."),
232
+ },
233
+ },
234
+ async ({ cadence = "daily" }) => {
235
+ const out = [
236
+ `## Async standup (${cadence})`,
237
+ ``,
238
+ `Post in the team thread by your local mid-morning. Keep it to three ` +
239
+ `lines. Link everything.`,
240
+ ``,
241
+ `**🟢 Shipped / progress:** What moved since last time? Link the issue/PR.`,
242
+ `**🎯 Today / next:** What are you picking up next?`,
243
+ `**🔴 Blockers:** What's in your way, and who can unblock it? (Write ` +
244
+ `“none” if clear — silence reads as invisibility, not “fine”.)`,
245
+ ``,
246
+ `_Why async: a written standup is searchable, inclusive of people who ` +
247
+ `were asleep during your sync, and creates a record of decisions and ` +
248
+ `blockers. The thread *is* the meeting._`,
249
+ ].join("\n");
250
+ return text(out);
251
+ },
252
+ );
253
+
254
+ server.registerTool(
255
+ "triage_sync_vs_async",
256
+ {
257
+ title: "Triage: sync or async?",
258
+ description:
259
+ "Recommend whether a task should be handled synchronously or " +
260
+ "asynchronously, using the decision rule from 'meetings are a point of " +
261
+ "escalation.' Async is the default; sync is the escalation.",
262
+ inputSchema: {
263
+ task: z
264
+ .string()
265
+ .describe("The task, conversation, or decision to triage."),
266
+ },
267
+ },
268
+ async ({ task }) => {
269
+ const t = task.toLowerCase();
270
+ const syncSignals = [
271
+ { re: /\b(conflict|tension|disagree|argument|heated|frustrat)/, why: "interpersonal tension — sync is kinder and faster" },
272
+ { re: /\b(fire|outage|incident|urgent|sev|down|broke|emergency)/, why: "active incident — real-time coordination wins" },
273
+ { re: /\b(feedback|review conversation|one.?on.?one|1:1|performance|raise|promotion|let go|fired|layoff)/, why: "sensitive/personal — deliver it live, follow up in writing" },
274
+ { re: /\b(brainstorm|ideate|explore|ambiguous|unclear|figure out|messy|open.?ended)/, why: "high ambiguity with fast back-and-forth — sync to converge, then write it up" },
275
+ ];
276
+ const hit = syncSignals.find((s) => s.re.test(t));
277
+
278
+ const verdict = hit ? "Lean SYNC" : "Default ASYNC";
279
+ const reason = hit
280
+ ? hit.why
281
+ : "this is shareable information or a decision with a clear owner — write it down, set a decide-by date, and let people respond on their own time";
282
+
283
+ const out = [
284
+ `## ${verdict}: ${task}`,
285
+ ``,
286
+ `**Why:** ${reason}.`,
287
+ ``,
288
+ `**The rule:** async is the default; sync is the escalation. Reach for ` +
289
+ `a meeting only when async is genuinely failing (thrashing, ` +
290
+ `ambiguity, or it's sensitive/personal). Otherwise the meeting is a ` +
291
+ `tax on everyone who isn't in the room.`,
292
+ ``,
293
+ hit
294
+ ? `**Even if you meet:** send a written pre-read, keep it small, and ` +
295
+ `post the decision and notes back to a URL so the people who ` +
296
+ `weren't there aren't left guessing.`
297
+ : `**To do it async well:** pick the artifact (issue/doc/PR), name one ` +
298
+ `decision owner, set a “decide by” date, and explicitly invite ` +
299
+ `dissent so silence isn't mistaken for agreement.`,
300
+ ].join("\n");
301
+
302
+ return text(out);
303
+ },
304
+ );
305
+ }