@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.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@open-and-async/mcp",
3
+ "version": "0.0.1",
4
+ "description": "Model Context Protocol server for the book Open and Async — async-first method tools plus the book's outline, taglines, and summaries. Ships the method, never the manuscript.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "author": "Ben Balter",
8
+ "homepage": "https://open-and-async.com",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/open-and-async/mcp.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/open-and-async/mcp/issues"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "async",
23
+ "remote-work",
24
+ "open-and-async",
25
+ "claude"
26
+ ],
27
+ "bin": {
28
+ "open-async-mcp": "src/index.js"
29
+ },
30
+ "files": [
31
+ "src/",
32
+ "data/book.json",
33
+ "README.md",
34
+ "CODE-LICENSE.md",
35
+ "DATA-LICENSE.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "scripts": {
42
+ "start": "node src/index.js",
43
+ "test": "node --test"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.29.0",
47
+ "zod": "^3.25.0"
48
+ }
49
+ }
package/src/data.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Loads the derived data artifact (data/book.json) and exposes shared helpers
3
+ * for the content tools and resources.
4
+ *
5
+ * The data file is built in the book repo by `just mcp-data`
6
+ * (script/build-mcp-data.js) and committed here as data/book.json. It contains
7
+ * ONLY already-public summaries (outline, TL;DRs, key-takeaways, taglines) and
8
+ * reviewed, paraphrased derived layers (frameworks, objections) — never
9
+ * verbatim book prose. See the book repo's docs/mcp-server-spec.md.
10
+ */
11
+
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const DATA_PATH = path.join(__dirname, "..", "data", "book.json");
18
+
19
+ /** @typedef {{ slug: string, title: string, tldr: string, anchor: string }} Chapter */
20
+
21
+ export const book = JSON.parse(fs.readFileSync(DATA_PATH, "utf8"));
22
+
23
+ export const BUY_URL = book.buyUrl || "https://open-and-async.com";
24
+
25
+ /** Every chapter, flattened out of the outline's section grouping. */
26
+ export const chapters = book.outline.flatMap((section) =>
27
+ section.chapters.map((ch) => ({ ...ch, section: section.section })),
28
+ );
29
+
30
+ /** Look up a chapter by its slug (anchor without the leading #). */
31
+ export function chapterBySlug(slug) {
32
+ const needle = String(slug || "")
33
+ .trim()
34
+ .replace(/^#/, "");
35
+ return chapters.find((ch) => ch.slug === needle) || null;
36
+ }
37
+
38
+ /**
39
+ * Guardrail: cap any text derived from the book at ~60 words so no single
40
+ * response reconstructs a chapter. Adds an ellipsis when truncated.
41
+ * @param {string} text
42
+ * @param {number} maxWords
43
+ * @returns {string}
44
+ */
45
+ export function capWords(text, maxWords = 60) {
46
+ const words = String(text || "").split(/\s+/).filter(Boolean);
47
+ if (words.length <= maxWords) return words.join(" ");
48
+ return words.slice(0, maxWords).join(" ") + "…";
49
+ }
50
+
51
+ /**
52
+ * Build the attribution + funnel line every content response must include.
53
+ * @param {Chapter & { section?: string }} chapter
54
+ * @returns {string}
55
+ */
56
+ export function cite(chapter) {
57
+ if (!chapter) return `— Open and Async. Get the book: ${BUY_URL}`;
58
+ return `— "${chapter.title}", Open and Async. Get the book: ${BUY_URL}`;
59
+ }
60
+
61
+ /** Wrap a tool result as MCP text content. */
62
+ export function text(body) {
63
+ return { content: [{ type: "text", text: body }] };
64
+ }
65
+
66
+ /** Title-cased edition string for "which edition is this?" answers. */
67
+ export const edition = `Open and Async — data v${book.version}`;
package/src/index.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Open and Async MCP server.
5
+ *
6
+ * Exposes the book's async-first *method* — decision-doc/ADR scaffolds, a
7
+ * meeting-to-async converter, a status-update rubric, an async standup, and a
8
+ * sync-vs-async triage — plus its already-public *summary* layer (outline,
9
+ * chapter TL;DRs, taglines, key-takeaways) and reviewed *derived* layer
10
+ * (frameworks, objections).
11
+ *
12
+ * It ships the method, never the manuscript: the only data file is
13
+ * data/book.json (built by `just mcp-data` in the book repo from already-public
14
+ * and reviewed-paraphrased content). No verbatim book prose is bundled.
15
+ *
16
+ * Transport: stdio. Run with `npx @open-and-async/mcp`.
17
+ */
18
+
19
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
+
22
+ import { book } from "./data.js";
23
+ import { registerMethodTools } from "./tools/methods.js";
24
+ import { registerContentTools } from "./tools/content.js";
25
+ import { registerResources } from "./resources.js";
26
+ import { registerPrompts } from "./prompts.js";
27
+
28
+ const server = new McpServer({
29
+ name: "open-async",
30
+ version: book.version,
31
+ });
32
+
33
+ registerMethodTools(server);
34
+ registerContentTools(server);
35
+ registerResources(server);
36
+ registerPrompts(server);
37
+
38
+ const transport = new StdioServerTransport();
39
+ await server.connect(transport);
40
+
41
+ // stderr is safe for logging on a stdio transport (stdout carries the protocol).
42
+ console.error(
43
+ `open-async MCP server running (data v${book.version}) — method + summary tools ready.`,
44
+ );
package/src/prompts.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Prompts — the "prompt pack," surfaced as native MCP prompts so users can
3
+ * invoke them directly from their client (e.g. slash commands). Each is a
4
+ * parameterized template that puts the book's method to work. No book prose.
5
+ */
6
+
7
+ import { z } from "zod";
8
+
9
+ /** Wrap a single user-role text message, the shape registerPrompt expects. */
10
+ function userMessage(text) {
11
+ return { messages: [{ role: "user", content: { type: "text", text } }] };
12
+ }
13
+
14
+ export function registerPrompts(server) {
15
+ server.registerPrompt(
16
+ "async-standup",
17
+ {
18
+ title: "Async standup",
19
+ description:
20
+ "Draft an async standup post from your day's work — outcome-first, " +
21
+ "blockers surfaced, everything linked.",
22
+ argsSchema: {
23
+ work: z
24
+ .string()
25
+ .optional()
26
+ .describe("What you did / are doing, in rough notes."),
27
+ },
28
+ },
29
+ ({ work }) =>
30
+ userMessage(
31
+ `Write my async standup as exactly three short lines — "🟢 Shipped/progress", ` +
32
+ `"🎯 Today/next", "🔴 Blockers" — outcome-first and with links where I name work. ` +
33
+ `If I have no blockers, say "none" explicitly. Here are my rough notes:\n\n` +
34
+ `${work || "(paste your notes here)"}`,
35
+ ),
36
+ );
37
+
38
+ server.registerPrompt(
39
+ "write-adr",
40
+ {
41
+ title: "Write an ADR",
42
+ description:
43
+ "Draft an architecture/architectural decision record from a decision " +
44
+ "and its options.",
45
+ argsSchema: {
46
+ decision: z.string().describe("The decision to record."),
47
+ options: z
48
+ .string()
49
+ .optional()
50
+ .describe("Options under consideration, comma-separated."),
51
+ },
52
+ },
53
+ ({ decision, options }) =>
54
+ userMessage(
55
+ `Draft a decision record (ADR) for: "${decision}". ` +
56
+ `${options ? `Options under consideration: ${options}. ` : ""}` +
57
+ `Use these sections: Context, Options considered (each with pros, cons, ` +
58
+ `cost, risk), Decision (chosen option + one-sentence reason), ` +
59
+ `Reversibility (one-way vs two-way door), and Open questions / dissent. ` +
60
+ `Keep it tight and async-friendly so someone can follow it without a meeting.`,
61
+ ),
62
+ );
63
+
64
+ server.registerPrompt(
65
+ "meeting-to-issue",
66
+ {
67
+ title: "Meeting → issue",
68
+ description:
69
+ "Convert a meeting agenda into the async artifact that should replace it.",
70
+ argsSchema: {
71
+ agenda: z.string().describe("The meeting agenda or purpose."),
72
+ },
73
+ },
74
+ ({ agenda }) =>
75
+ userMessage(
76
+ `Here's a meeting agenda:\n\n${agenda}\n\n` +
77
+ `Convert it into the async equivalent. For each topic, tell me: the ` +
78
+ `artifact that replaces it (issue, doc, or PR comment), who the decision ` +
79
+ `owner is, and a "decide by" date. Flag anything that genuinely needs to ` +
80
+ `stay synchronous (sensitive, high-ambiguity, or active incident) and why. ` +
81
+ `Default to async; treat a meeting as the escalation, not the norm.`,
82
+ ),
83
+ );
84
+
85
+ server.registerPrompt(
86
+ "weekly-update",
87
+ {
88
+ title: "Weekly update",
89
+ description:
90
+ "Draft a weekly update that doesn't suck — outcomes over activity, " +
91
+ "risks surfaced early, skimmable.",
92
+ argsSchema: {
93
+ notes: z
94
+ .string()
95
+ .optional()
96
+ .describe("Your raw notes from the week."),
97
+ },
98
+ },
99
+ ({ notes }) =>
100
+ userMessage(
101
+ `Turn my week into a status update that leads with outcomes, not activity. ` +
102
+ `Structure it as: Headline (one line), Shipped (bullets, each linking the ` +
103
+ `work), Risks/blockers (surface anything at risk *before* it's late — say ` +
104
+ `"no surprises"), and Next. Keep it skimmable. My raw notes:\n\n` +
105
+ `${notes || "(paste your notes here)"}`,
106
+ ),
107
+ );
108
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Resources — read-only reference data, surfaced as MCP resources so clients
3
+ * can attach them to context directly.
4
+ *
5
+ * Both expose already-public summary data only (no body prose).
6
+ */
7
+
8
+ import { book } from "./data.js";
9
+
10
+ export function registerResources(server) {
11
+ server.registerResource(
12
+ "outline",
13
+ "book://outline",
14
+ {
15
+ title: "Open and Async — outline",
16
+ description:
17
+ "Sections, chapters, and one-line TL;DRs. The book's table of contents.",
18
+ mimeType: "application/json",
19
+ },
20
+ async (uri) => ({
21
+ contents: [
22
+ {
23
+ uri: uri.href,
24
+ mimeType: "application/json",
25
+ text: JSON.stringify(
26
+ { version: book.version, outline: book.outline },
27
+ null,
28
+ 2,
29
+ ),
30
+ },
31
+ ],
32
+ }),
33
+ );
34
+
35
+ server.registerResource(
36
+ "taglines",
37
+ "book://taglines",
38
+ {
39
+ title: "Open and Async — taglines",
40
+ description:
41
+ "Shareable 'bumper-sticker' lines, each with its /q/<slug> quote-card URL.",
42
+ mimeType: "application/json",
43
+ },
44
+ async (uri) => ({
45
+ contents: [
46
+ {
47
+ uri: uri.href,
48
+ mimeType: "application/json",
49
+ text: JSON.stringify(
50
+ { version: book.version, taglines: book.taglines },
51
+ null,
52
+ 2,
53
+ ),
54
+ },
55
+ ],
56
+ }),
57
+ );
58
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Content tools — derived/summary only, capped, always cited, always funnel.
3
+ *
4
+ * These surface the book's already-public layer (outline, TL;DRs, taglines,
5
+ * key-takeaways) and its reviewed derived layer (frameworks, objections). Every
6
+ * response is capped at ~60 words from any one chapter, cites the chapter, and
7
+ * links to buy. No tool returns body text; nothing reconstructs a chapter.
8
+ */
9
+
10
+ import { z } from "zod";
11
+ import {
12
+ book,
13
+ chapters,
14
+ chapterBySlug,
15
+ capWords,
16
+ cite,
17
+ text,
18
+ BUY_URL,
19
+ } from "../data.js";
20
+
21
+ /** Lowercase haystack for keyword scoring. */
22
+ function tokens(s) {
23
+ return String(s || "")
24
+ .toLowerCase()
25
+ .split(/[^a-z0-9]+/)
26
+ .filter((w) => w.length > 2);
27
+ }
28
+
29
+ export function registerContentTools(server) {
30
+ server.registerTool(
31
+ "book_outline",
32
+ {
33
+ title: "Book outline",
34
+ description:
35
+ "The map of Open and Async: every section and chapter with a one-line " +
36
+ "TL;DR. Use it to find the right chapter for a topic.",
37
+ inputSchema: {},
38
+ },
39
+ async () => {
40
+ const lines = book.outline.map((section) => {
41
+ const chs = section.chapters
42
+ .map((ch) => ` - **${ch.title}** (${ch.slug}) — ${ch.tldr}`)
43
+ .join("\n");
44
+ return `### ${section.section}\n${chs}`;
45
+ });
46
+ return text(
47
+ [
48
+ `# Open and Async — outline (v${book.version})`,
49
+ ``,
50
+ ...lines,
51
+ ``,
52
+ `Read the book: ${BUY_URL}`,
53
+ ].join("\n"),
54
+ );
55
+ },
56
+ );
57
+
58
+ server.registerTool(
59
+ "get_chapter_summary",
60
+ {
61
+ title: "Get a chapter summary",
62
+ description:
63
+ "Return a chapter's TL;DR plus its taglines and a link to read the full " +
64
+ "chapter. Summary only — no body text.",
65
+ inputSchema: {
66
+ slug: z
67
+ .string()
68
+ .describe("Chapter slug, e.g. 'impact-over-input' (from book_outline)."),
69
+ },
70
+ },
71
+ async ({ slug }) => {
72
+ const ch = chapterBySlug(slug);
73
+ if (!ch) {
74
+ const names = chapters.map((c) => c.slug).join(", ");
75
+ return text(
76
+ `No chapter with slug "${slug}". Available slugs:\n${names}`,
77
+ );
78
+ }
79
+ const tags = book.taglines.filter((t) => t.chapter === ch.slug);
80
+ const tagLines = tags.length
81
+ ? tags.map((t) => `- ${t.text} (${t.card})`).join("\n")
82
+ : "_(no taglines for this chapter)_";
83
+
84
+ return text(
85
+ [
86
+ `## ${ch.title}`,
87
+ `_Section: ${ch.section}_`,
88
+ ``,
89
+ `**TL;DR:** ${ch.tldr}`,
90
+ ``,
91
+ `**Taglines:**`,
92
+ tagLines,
93
+ ``,
94
+ cite(ch),
95
+ ].join("\n"),
96
+ );
97
+ },
98
+ );
99
+
100
+ server.registerTool(
101
+ "search_principles",
102
+ {
103
+ title: "Search the book's principles",
104
+ description:
105
+ "Keyword search across the book's summary corpus (TL;DRs, key-takeaways, " +
106
+ "taglines, and reviewed frameworks). Returns short, cited snippets — a " +
107
+ "snippet view, not full text.",
108
+ inputSchema: {
109
+ query: z.string().describe("What you're looking for."),
110
+ limit: z
111
+ .number()
112
+ .int()
113
+ .min(1)
114
+ .max(10)
115
+ .optional()
116
+ .describe("Max results (default 5, capped at 10)."),
117
+ },
118
+ },
119
+ async ({ query, limit = 5 }) => {
120
+ const q = new Set(tokens(query));
121
+ const score = (s) => {
122
+ const t = tokens(s);
123
+ let n = 0;
124
+ for (const w of t) if (q.has(w)) n++;
125
+ return n;
126
+ };
127
+
128
+ const corpus = [];
129
+ for (const ch of chapters) {
130
+ corpus.push({ kind: "TL;DR", body: ch.tldr, ch });
131
+ }
132
+ for (const set of book.takeaways) {
133
+ for (const p of set.points) corpus.push({ kind: "Takeaway", body: p });
134
+ }
135
+ for (const t of book.taglines) {
136
+ corpus.push({
137
+ kind: "Tagline",
138
+ body: t.text,
139
+ ch: chapterBySlug(t.chapter),
140
+ });
141
+ }
142
+ for (const f of book.frameworks || []) {
143
+ corpus.push({
144
+ kind: "Framework",
145
+ body: f.guidance,
146
+ ch: chapterBySlug(f.anchor || f.chapter),
147
+ });
148
+ }
149
+
150
+ const ranked = corpus
151
+ .map((item) => ({ item, s: score(item.body) }))
152
+ .filter((r) => r.s > 0)
153
+ .sort((a, b) => b.s - a.s)
154
+ .slice(0, Math.min(limit, 10));
155
+
156
+ if (ranked.length === 0) {
157
+ return text(
158
+ `No matches for "${query}". Try book_outline to browse topics, then ` +
159
+ `get_chapter_summary for a specific chapter.\n\nRead the book: ${BUY_URL}`,
160
+ );
161
+ }
162
+
163
+ const out = ranked.map(({ item }) => {
164
+ const where = item.ch ? `\n${cite(item.ch)}` : "";
165
+ return `**[${item.kind}]** ${capWords(item.body)}${where}`;
166
+ });
167
+
168
+ return text(
169
+ [
170
+ `# Results for "${query}" (${ranked.length})`,
171
+ ``,
172
+ out.join("\n\n"),
173
+ ``,
174
+ `These are summaries. The full argument and stories are in the book: ${BUY_URL}`,
175
+ ].join("\n"),
176
+ );
177
+ },
178
+ );
179
+
180
+ server.registerTool(
181
+ "handle_objection",
182
+ {
183
+ title: "Handle an objection",
184
+ description:
185
+ "Map a common objection to open/async work ('async is slow', 'remote " +
186
+ "kills culture') to the book's reframe, with a chapter citation.",
187
+ inputSchema: {
188
+ objection: z.string().describe("The skepticism or pushback to address."),
189
+ },
190
+ },
191
+ async ({ objection }) => {
192
+ const objections = book.objections || [];
193
+ if (objections.length === 0) {
194
+ // Derived objection layer not yet reviewed/published — fall back to a
195
+ // pointer rather than inventing book content.
196
+ return text(
197
+ [
198
+ `The objection-handling layer is part of the book's reviewed derived ` +
199
+ `content and isn't bundled in this build yet.`,
200
+ ``,
201
+ `In the meantime, try \`search_principles\` with the core of the ` +
202
+ `objection (e.g. "${capWords(objection, 8)}") to find the relevant ` +
203
+ `chapter, then \`get_chapter_summary\`.`,
204
+ ``,
205
+ `The full reframe lives in the book: ${BUY_URL}`,
206
+ ].join("\n"),
207
+ );
208
+ }
209
+
210
+ const q = new Set(tokens(objection));
211
+ const best = objections
212
+ .map((o) => {
213
+ const t = tokens(`${o.trigger} ${o.reframe}`);
214
+ let n = 0;
215
+ for (const w of t) if (q.has(w)) n++;
216
+ return { o, n };
217
+ })
218
+ .sort((a, b) => b.n - a.n)[0];
219
+
220
+ if (!best || best.n === 0) {
221
+ return text(
222
+ `No direct match. Try \`search_principles\` for "${capWords(objection, 8)}".\n\nRead the book: ${BUY_URL}`,
223
+ );
224
+ }
225
+
226
+ const ch = chapterBySlug(best.o.anchor || best.o.chapter);
227
+ return text(
228
+ [
229
+ `**Objection:** "${best.o.trigger}"`,
230
+ ``,
231
+ `**Reframe:** ${capWords(best.o.reframe)}`,
232
+ ``,
233
+ cite(ch),
234
+ ].join("\n"),
235
+ );
236
+ },
237
+ );
238
+
239
+ server.registerTool(
240
+ "get_guidance",
241
+ {
242
+ title: "Get role-aware guidance",
243
+ description:
244
+ "Role-aware guidance (manager or IC) for a topic, paraphrased from the " +
245
+ "book's role callouts, with a chapter citation.",
246
+ inputSchema: {
247
+ topic: z.string().describe("The topic you want guidance on."),
248
+ role: z
249
+ .enum(["manager", "ic", "any"])
250
+ .optional()
251
+ .describe("Audience: 'manager', 'ic', or 'any' (default)."),
252
+ },
253
+ },
254
+ async ({ topic, role = "any" }) => {
255
+ const frameworks = book.frameworks || [];
256
+ if (frameworks.length === 0) {
257
+ return text(
258
+ [
259
+ `Role-aware guidance comes from the book's reviewed derived layer, ` +
260
+ `which isn't bundled in this build yet.`,
261
+ ``,
262
+ `Try \`search_principles\` for "${capWords(topic, 8)}" to find the ` +
263
+ `relevant chapter, then \`get_chapter_summary\`.`,
264
+ ``,
265
+ `Read the book: ${BUY_URL}`,
266
+ ].join("\n"),
267
+ );
268
+ }
269
+
270
+ const q = new Set(tokens(topic));
271
+ const ranked = frameworks
272
+ .filter((f) => role === "any" || !f.role || f.role === "any" || f.role === role)
273
+ .map((f) => {
274
+ const t = tokens(`${f.topic} ${f.guidance}`);
275
+ let n = 0;
276
+ for (const w of t) if (q.has(w)) n++;
277
+ return { f, n };
278
+ })
279
+ .filter((r) => r.n > 0)
280
+ .sort((a, b) => b.n - a.n)
281
+ .slice(0, 3);
282
+
283
+ if (ranked.length === 0) {
284
+ return text(
285
+ `No guidance matched "${topic}" for role "${role}". Try \`search_principles\`.\n\nRead the book: ${BUY_URL}`,
286
+ );
287
+ }
288
+
289
+ const out = ranked.map(({ f }) => {
290
+ const ch = chapterBySlug(f.anchor || f.chapter);
291
+ const label = f.role && f.role !== "any" ? ` _(${f.role})_` : "";
292
+ return `**${f.topic}**${label}\n${capWords(f.guidance)}\n${cite(ch)}`;
293
+ });
294
+
295
+ return text(out.join("\n\n"));
296
+ },
297
+ );
298
+
299
+ server.registerTool(
300
+ "get_taglines",
301
+ {
302
+ title: "Get taglines",
303
+ description:
304
+ "Return the book's shareable taglines and their quote-card URLs. " +
305
+ "Optionally filter to one chapter.",
306
+ inputSchema: {
307
+ chapter: z
308
+ .string()
309
+ .optional()
310
+ .describe("Chapter slug to filter by (omit for all)."),
311
+ },
312
+ },
313
+ async ({ chapter }) => {
314
+ let tags = book.taglines;
315
+ if (chapter) {
316
+ const slug = chapter.replace(/^#/, "");
317
+ tags = tags.filter((t) => t.chapter === slug);
318
+ if (tags.length === 0) {
319
+ return text(`No taglines for chapter "${chapter}".`);
320
+ }
321
+ }
322
+ const lines = tags.map(
323
+ (t) => `- **${t.text}**\n ${t.chapter_title} · ${t.card}`,
324
+ );
325
+ return text(
326
+ [`# Taglines${chapter ? ` — ${chapter}` : ""}`, ``, ...lines].join("\n"),
327
+ );
328
+ },
329
+ );
330
+ }