doc2mcp 0.1.17 → 0.1.19
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 +27 -6
- package/dist/index.js +352 -204
- package/package.json +3 -1
- package/scripts/postinstall.js +48 -0
package/README.md
CHANGED
|
@@ -29,6 +29,20 @@ npm install -g doc2mcp
|
|
|
29
29
|
> Install with **`-g`** (global). The `doc2mcp` command only lands on your `PATH` when installed globally.
|
|
30
30
|
> If you ran `npm i doc2mcp` (without `-g`) and see `command not found: doc2mcp`, either reinstall with `-g`
|
|
31
31
|
> or run it through your package runner: `npx doc2mcp <docs-url>`.
|
|
32
|
+
>
|
|
33
|
+
> If `npm install -g doc2mcp` succeeds but `doc2mcp` is still `command not found`, your npm global bin folder is
|
|
34
|
+
> not on PATH. Run:
|
|
35
|
+
>
|
|
36
|
+
> ```bash
|
|
37
|
+
> echo 'export PATH="'$(npm prefix -g)'/bin:$PATH"' >> ~/.zshrc
|
|
38
|
+
> source ~/.zshrc
|
|
39
|
+
> ```
|
|
40
|
+
>
|
|
41
|
+
> Quick no-setup option:
|
|
42
|
+
>
|
|
43
|
+
> ```bash
|
|
44
|
+
> npx doc2mcp login
|
|
45
|
+
> ```
|
|
32
46
|
|
|
33
47
|
Other package managers:
|
|
34
48
|
|
|
@@ -54,6 +68,9 @@ doc2mcp https://docs.stripe.com
|
|
|
54
68
|
|
|
55
69
|
# 4. Chat with your docs without leaving the terminal
|
|
56
70
|
doc2mcp chat
|
|
71
|
+
|
|
72
|
+
# Or paste a docs URL directly into chat mode
|
|
73
|
+
doc2mcp chat https://uagents.fetch.ai/docs
|
|
57
74
|
```
|
|
58
75
|
|
|
59
76
|
That's it. The same hosted pipeline powers the [website](https://doc2mcp.site), so a project you
|
|
@@ -69,7 +86,7 @@ create in the CLI shows up in your dashboard and marketplace too.
|
|
|
69
86
|
| [`doc2mcp whoami`](#doc2mcp-whoami) | Show the account you're signed in as |
|
|
70
87
|
| [`doc2mcp list`](#doc2mcp-list) | List the MCP projects on your account |
|
|
71
88
|
| [`doc2mcp install <projectId>`](#doc2mcp-install-projectid) | Install an existing MCP into your editors |
|
|
72
|
-
| [`doc2mcp chat [
|
|
89
|
+
| [`doc2mcp chat [target]`](#doc2mcp-chat-target) | Chat with your docs in the terminal; target can be a project ID or docs URL |
|
|
73
90
|
| `doc2mcp --version` | Print the installed CLI version |
|
|
74
91
|
| `doc2mcp --help` | Show usage and all commands |
|
|
75
92
|
|
|
@@ -166,16 +183,19 @@ Existing config is merged, not overwritten.
|
|
|
166
183
|
|
|
167
184
|
---
|
|
168
185
|
|
|
169
|
-
### `doc2mcp chat [
|
|
186
|
+
### `doc2mcp chat [target]`
|
|
170
187
|
|
|
171
188
|
Chat with your docs **right from the terminal**. doc2mcp answers natural-language questions from
|
|
172
189
|
the crawled documentation — with cited sources — using the project's hosted MCP (the same
|
|
173
|
-
`ask_documentation` tool your editor calls). This is the Playground experience, in
|
|
190
|
+
`ask_documentation` tool your editor calls). This is the Playground experience, in a Claude Code-style shell loop.
|
|
174
191
|
|
|
175
192
|
```bash
|
|
176
|
-
# Interactive:
|
|
193
|
+
# Interactive: paste a docs URL, project ID, or choose an existing MCP
|
|
177
194
|
doc2mcp chat
|
|
178
195
|
|
|
196
|
+
# Paste a docs URL directly: doc2mcp converts it, then starts chat
|
|
197
|
+
doc2mcp chat https://uagents.fetch.ai/docs
|
|
198
|
+
|
|
179
199
|
# Skip the picker by passing a project ID
|
|
180
200
|
doc2mcp chat prj_123abc
|
|
181
201
|
|
|
@@ -184,7 +204,7 @@ doc2mcp chat prj_123abc -m "How do I authenticate requests?"
|
|
|
184
204
|
```
|
|
185
205
|
|
|
186
206
|
- With no arguments, you pick from your `ready` projects.
|
|
187
|
-
- Type `/exit`
|
|
207
|
+
- Type `/exit` to leave an interactive session.
|
|
188
208
|
- Each answer lists the source pages it used so you can verify it.
|
|
189
209
|
|
|
190
210
|
## Configuration
|
|
@@ -198,7 +218,8 @@ doc2mcp chat prj_123abc -m "How do I authenticate requests?"
|
|
|
198
218
|
|
|
199
219
|
| Symptom | Fix |
|
|
200
220
|
| --- | --- |
|
|
201
|
-
| `command not found: doc2mcp` | You installed locally or
|
|
221
|
+
| `command not found: doc2mcp` | You installed locally or npm's global bin is not on PATH. Use `npx doc2mcp <url>`, or add `$(npm prefix -g)/bin` to PATH in `~/.zshrc`. |
|
|
222
|
+
| `pnpm add -g doc2mcp` says `ERR_PNPM_NO_GLOBAL_BIN_DIR` | Run `pnpm setup`, then `source ~/.zshrc`, then retry `pnpm add -g doc2mcp`. |
|
|
202
223
|
| Browser doesn't open on `login` | Copy the printed URL into your browser manually, then approve. |
|
|
203
224
|
| `login` can't reach the server | Confirm you're online; for self-hosting set `DOC2MCP_API_URL` to your instance. |
|
|
204
225
|
| "Limit reached" | You've hit your plan's monthly conversion limit (shared across CLI and web). |
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc8 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/commands/account.ts
|
|
8
8
|
import pc from "picocolors";
|
|
@@ -74,9 +74,10 @@ async function runWhoami() {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// src/commands/chat.ts
|
|
77
|
-
import {
|
|
77
|
+
import { createInterface } from "readline/promises";
|
|
78
|
+
import { stdin as input, stdout as output } from "process";
|
|
78
79
|
import ora2 from "ora";
|
|
79
|
-
import
|
|
80
|
+
import pc7 from "picocolors";
|
|
80
81
|
|
|
81
82
|
// src/api.ts
|
|
82
83
|
import pc2 from "picocolors";
|
|
@@ -90,14 +91,14 @@ var ApiError = class extends Error {
|
|
|
90
91
|
}
|
|
91
92
|
};
|
|
92
93
|
async function parseJson(response) {
|
|
93
|
-
const
|
|
94
|
-
if (!
|
|
94
|
+
const text = await response.text();
|
|
95
|
+
if (!text) {
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
98
|
try {
|
|
98
|
-
return JSON.parse(
|
|
99
|
+
return JSON.parse(text);
|
|
99
100
|
} catch {
|
|
100
|
-
return { raw:
|
|
101
|
+
return { raw: text };
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
async function apiFetch(path, options = {}) {
|
|
@@ -138,10 +139,85 @@ function printError(error) {
|
|
|
138
139
|
`);
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
// src/markdown.ts
|
|
143
|
+
import pc3 from "picocolors";
|
|
144
|
+
var LINK = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
145
|
+
var INLINE_CODE = /`([^`]+)`/g;
|
|
146
|
+
var BOLD = /\*\*([^*]+)\*\*/g;
|
|
147
|
+
var ITALIC = /(^|[^*])\*([^*\n]+)\*/g;
|
|
148
|
+
var HEADING = /^(#{1,6})\s+(.*)$/;
|
|
149
|
+
var BULLET = /^(\s*)[-*]\s+(.*)$/;
|
|
150
|
+
var ORDERED = /^(\s*)(\d+)\.\s+(.*)$/;
|
|
151
|
+
var FENCE = /^```(\w*)\s*$/;
|
|
152
|
+
var BLOCKQUOTE = /^>\s?(.*)$/;
|
|
153
|
+
function renderInline(text) {
|
|
154
|
+
let out = text.replace(
|
|
155
|
+
LINK,
|
|
156
|
+
(_m, label, url) => `${pc3.cyan(pc3.underline(label))} ${pc3.dim(`(${url})`)}`
|
|
157
|
+
);
|
|
158
|
+
out = out.replace(INLINE_CODE, (_m, code) => pc3.yellow(code));
|
|
159
|
+
out = out.replace(BOLD, (_m, bold) => pc3.bold(bold));
|
|
160
|
+
out = out.replace(
|
|
161
|
+
ITALIC,
|
|
162
|
+
(_m, prefix, italic) => `${prefix}${pc3.italic(italic)}`
|
|
163
|
+
);
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
function renderMarkdown(markdown) {
|
|
167
|
+
const lines = markdown.split("\n");
|
|
168
|
+
const out = [];
|
|
169
|
+
let inCode = false;
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
const fence = line.match(FENCE);
|
|
172
|
+
if (fence) {
|
|
173
|
+
if (inCode) {
|
|
174
|
+
out.push(pc3.dim(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
175
|
+
inCode = false;
|
|
176
|
+
} else {
|
|
177
|
+
const lang = fence[1] || "code";
|
|
178
|
+
out.push(pc3.dim(` \u250C\u2500\u2500\u2500\u2500\u2500\u2500 ${lang}`));
|
|
179
|
+
inCode = true;
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (inCode) {
|
|
184
|
+
out.push(` ${pc3.green(line)}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const heading = line.match(HEADING);
|
|
188
|
+
if (heading) {
|
|
189
|
+
out.push(pc3.bold(pc3.cyan(renderInline(heading[2] ?? ""))));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const ordered = line.match(ORDERED);
|
|
193
|
+
if (ordered) {
|
|
194
|
+
out.push(
|
|
195
|
+
`${ordered[1] ?? ""}${pc3.cyan(`${ordered[2] ?? ""}.`)} ${renderInline(ordered[3] ?? "")}`
|
|
196
|
+
);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const bullet = line.match(BULLET);
|
|
200
|
+
if (bullet) {
|
|
201
|
+
out.push(`${bullet[1] ?? ""}${pc3.cyan("\u2022")} ${renderInline(bullet[2] ?? "")}`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const quote = line.match(BLOCKQUOTE);
|
|
205
|
+
if (quote) {
|
|
206
|
+
out.push(`${pc3.dim("\u2502")} ${pc3.dim(renderInline(quote[1] ?? ""))}`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
out.push(renderInline(line));
|
|
210
|
+
}
|
|
211
|
+
return out.join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/commands/convert.ts
|
|
215
|
+
import pc6 from "picocolors";
|
|
216
|
+
|
|
141
217
|
// src/commands/login.ts
|
|
142
218
|
import open from "open";
|
|
143
219
|
import ora from "ora";
|
|
144
|
-
import
|
|
220
|
+
import pc4 from "picocolors";
|
|
145
221
|
function sleep(ms) {
|
|
146
222
|
return new Promise((resolve) => {
|
|
147
223
|
setTimeout(resolve, ms);
|
|
@@ -159,13 +235,13 @@ async function runLogin() {
|
|
|
159
235
|
spinner.stop();
|
|
160
236
|
process.stdout.write(
|
|
161
237
|
`
|
|
162
|
-
${
|
|
163
|
-
${
|
|
238
|
+
${pc4.cyan("Open this link to authorize:")}
|
|
239
|
+
${pc4.bold(start.verifyUrl)}
|
|
164
240
|
|
|
165
241
|
`
|
|
166
242
|
);
|
|
167
243
|
process.stdout.write(
|
|
168
|
-
`${
|
|
244
|
+
`${pc4.dim("Code:")} ${pc4.bold(start.userCode)} ${pc4.dim("(also shown in browser)")}
|
|
169
245
|
|
|
170
246
|
`
|
|
171
247
|
);
|
|
@@ -173,7 +249,7 @@ ${pc3.bold(start.verifyUrl)}
|
|
|
173
249
|
await open(start.verifyUrl);
|
|
174
250
|
} catch {
|
|
175
251
|
process.stdout.write(
|
|
176
|
-
`${
|
|
252
|
+
`${pc4.yellow("Could not auto-open browser. Open the link manually.")}
|
|
177
253
|
|
|
178
254
|
`
|
|
179
255
|
);
|
|
@@ -199,7 +275,7 @@ ${pc3.bold(start.verifyUrl)}
|
|
|
199
275
|
user: poll.user
|
|
200
276
|
});
|
|
201
277
|
process.stdout.write(
|
|
202
|
-
`${
|
|
278
|
+
`${pc4.green("Logged in as")} ${poll.user.email}
|
|
203
279
|
`
|
|
204
280
|
);
|
|
205
281
|
return;
|
|
@@ -232,148 +308,6 @@ async function ensureLoggedIn() {
|
|
|
232
308
|
}
|
|
233
309
|
}
|
|
234
310
|
|
|
235
|
-
// src/commands/chat.ts
|
|
236
|
-
async function pickProject(explicitId) {
|
|
237
|
-
if (explicitId) {
|
|
238
|
-
return await apiFetch(`/api/cli/projects/${explicitId}`);
|
|
239
|
-
}
|
|
240
|
-
const data = await apiFetch(
|
|
241
|
-
"/api/cli/projects"
|
|
242
|
-
);
|
|
243
|
-
const ready = data.projects.filter((p) => p.status === "ready");
|
|
244
|
-
if (ready.length === 0) {
|
|
245
|
-
process.stdout.write(
|
|
246
|
-
`${pc4.yellow("No ready MCP projects yet.")} Create one: ${pc4.bold("doc2mcp <docs-url>")}
|
|
247
|
-
`
|
|
248
|
-
);
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
const choice = await select({
|
|
252
|
-
message: "Which docs do you want to chat with?",
|
|
253
|
-
options: ready.map((p) => ({
|
|
254
|
-
value: p.id,
|
|
255
|
-
label: p.name,
|
|
256
|
-
hint: p.sourceUrl ?? void 0
|
|
257
|
-
}))
|
|
258
|
-
});
|
|
259
|
-
if (isCancel(choice)) {
|
|
260
|
-
cancel("Cancelled.");
|
|
261
|
-
return null;
|
|
262
|
-
}
|
|
263
|
-
return await apiFetch(`/api/cli/projects/${choice}`);
|
|
264
|
-
}
|
|
265
|
-
async function askDocs(mcp, question) {
|
|
266
|
-
const response = await fetch(mcp.url, {
|
|
267
|
-
method: "POST",
|
|
268
|
-
headers: {
|
|
269
|
-
"Content-Type": "application/json",
|
|
270
|
-
Authorization: `Bearer ${mcp.token}`
|
|
271
|
-
},
|
|
272
|
-
body: JSON.stringify({
|
|
273
|
-
jsonrpc: "2.0",
|
|
274
|
-
id: 1,
|
|
275
|
-
method: "tools/call",
|
|
276
|
-
params: { name: "ask_documentation", arguments: { question } }
|
|
277
|
-
})
|
|
278
|
-
});
|
|
279
|
-
const payload = await response.json();
|
|
280
|
-
if (!response.ok || payload.error) {
|
|
281
|
-
throw new ApiError(
|
|
282
|
-
payload.error?.message ?? `MCP request failed (${response.status})`,
|
|
283
|
-
response.status,
|
|
284
|
-
payload
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
const raw = payload.result?.content?.[0]?.text ?? "";
|
|
288
|
-
try {
|
|
289
|
-
return JSON.parse(raw);
|
|
290
|
-
} catch {
|
|
291
|
-
return { question, answer: raw };
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
function renderAnswer(answer) {
|
|
295
|
-
process.stdout.write(`
|
|
296
|
-
${pc4.cyan("\u25C6")} ${answer.answer.trim()}
|
|
297
|
-
`);
|
|
298
|
-
if (answer.sources && answer.sources.length > 0) {
|
|
299
|
-
process.stdout.write(`
|
|
300
|
-
${pc4.dim("Sources:")}
|
|
301
|
-
`);
|
|
302
|
-
for (const source of answer.sources.slice(0, 6)) {
|
|
303
|
-
process.stdout.write(` ${pc4.dim("\u2022")} ${source.title} ${pc4.dim(source.url)}
|
|
304
|
-
`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
process.stdout.write("\n");
|
|
308
|
-
}
|
|
309
|
-
async function answerOnce(mcp, question) {
|
|
310
|
-
const spinner = ora2("Thinking\u2026").start();
|
|
311
|
-
try {
|
|
312
|
-
const answer = await askDocs(mcp, question);
|
|
313
|
-
spinner.stop();
|
|
314
|
-
renderAnswer(answer);
|
|
315
|
-
} catch (error) {
|
|
316
|
-
spinner.fail("Failed to get an answer");
|
|
317
|
-
printError(error);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
async function runChat(projectId, options = {}) {
|
|
321
|
-
try {
|
|
322
|
-
await ensureLoggedIn();
|
|
323
|
-
const detail = await pickProject(projectId);
|
|
324
|
-
if (!detail) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
if (!detail.mcp) {
|
|
328
|
-
process.stderr.write(
|
|
329
|
-
`${pc4.red("That project is not ready yet.")} Check: ${pc4.bold("doc2mcp list")}
|
|
330
|
-
`
|
|
331
|
-
);
|
|
332
|
-
process.exitCode = 1;
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const { mcp } = detail;
|
|
336
|
-
if (options.message) {
|
|
337
|
-
await answerOnce(mcp, options.message);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
intro(
|
|
341
|
-
`${pc4.bold(`Chatting with ${detail.project.name}`)} ${pc4.dim("\u2014 ask anything about these docs")}`
|
|
342
|
-
);
|
|
343
|
-
process.stdout.write(
|
|
344
|
-
`${pc4.dim("Type your question. Use /exit to leave.")}
|
|
345
|
-
`
|
|
346
|
-
);
|
|
347
|
-
let active = true;
|
|
348
|
-
while (active) {
|
|
349
|
-
const question = await text({
|
|
350
|
-
message: "You",
|
|
351
|
-
placeholder: "How do I authenticate requests?"
|
|
352
|
-
});
|
|
353
|
-
if (isCancel(question)) {
|
|
354
|
-
active = false;
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
const trimmed = String(question).trim();
|
|
358
|
-
if (!trimmed) {
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
362
|
-
active = false;
|
|
363
|
-
break;
|
|
364
|
-
}
|
|
365
|
-
await answerOnce(mcp, trimmed);
|
|
366
|
-
}
|
|
367
|
-
outro(pc4.dim("Bye \u2014 your docs MCP stays live for your editor."));
|
|
368
|
-
} catch (error) {
|
|
369
|
-
printError(error);
|
|
370
|
-
process.exitCode = 1;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// src/commands/convert.ts
|
|
375
|
-
import pc6 from "picocolors";
|
|
376
|
-
|
|
377
311
|
// src/commands/install.ts
|
|
378
312
|
import { confirm, multiselect } from "@clack/prompts";
|
|
379
313
|
import pc5 from "picocolors";
|
|
@@ -598,63 +532,69 @@ function printStatus(detail) {
|
|
|
598
532
|
`\r${pc6.cyan("Status:")} ${project.status.padEnd(12)} ${pc6.dim(project.name)}`
|
|
599
533
|
);
|
|
600
534
|
}
|
|
601
|
-
async function
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
process.stdout.write(`${pc6.bold("Converting")} ${sourceUrl}
|
|
535
|
+
async function convertUrlToProject(sourceUrl, options = { offerInstall: true }) {
|
|
536
|
+
await ensureLoggedIn();
|
|
537
|
+
process.stdout.write(`${pc6.bold("Converting")} ${sourceUrl}
|
|
605
538
|
`);
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
539
|
+
const created = await apiFetch("/api/cli/convert", {
|
|
540
|
+
method: "POST",
|
|
541
|
+
body: JSON.stringify({ sourceUrl })
|
|
542
|
+
});
|
|
543
|
+
process.stdout.write(`${pc6.dim("Project:")} ${created.id}
|
|
611
544
|
`);
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
`/api/cli/projects/${created.id}`
|
|
617
|
-
);
|
|
618
|
-
printStatus(detail);
|
|
619
|
-
if (terminal.has(detail.project.status)) {
|
|
620
|
-
process.stdout.write("\n");
|
|
621
|
-
break;
|
|
622
|
-
}
|
|
623
|
-
await sleep2(delayMs);
|
|
624
|
-
delayMs = Math.min(delayMs + 1e3, 1e4);
|
|
625
|
-
}
|
|
626
|
-
const finalDetail = await apiFetch(
|
|
545
|
+
let delayMs = 2e3;
|
|
546
|
+
const terminal = /* @__PURE__ */ new Set(["ready", "error"]);
|
|
547
|
+
while (true) {
|
|
548
|
+
const detail = await apiFetch(
|
|
627
549
|
`/api/cli/projects/${created.id}`
|
|
628
550
|
);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
process.
|
|
632
|
-
|
|
633
|
-
`
|
|
634
|
-
);
|
|
635
|
-
process.exitCode = 1;
|
|
636
|
-
return;
|
|
551
|
+
printStatus(detail);
|
|
552
|
+
if (terminal.has(detail.project.status)) {
|
|
553
|
+
process.stdout.write("\n");
|
|
554
|
+
break;
|
|
637
555
|
}
|
|
638
|
-
|
|
639
|
-
|
|
556
|
+
await sleep2(delayMs);
|
|
557
|
+
delayMs = Math.min(delayMs + 1e3, 1e4);
|
|
558
|
+
}
|
|
559
|
+
const finalDetail = await apiFetch(
|
|
560
|
+
`/api/cli/projects/${created.id}`
|
|
561
|
+
);
|
|
562
|
+
if (finalDetail.project.status === "error") {
|
|
563
|
+
const lastLog = finalDetail.project.logs.at(-1);
|
|
564
|
+
process.stderr.write(
|
|
565
|
+
`${pc6.red("Conversion failed.")}${lastLog ? ` ${lastLog.message}` : ""}
|
|
566
|
+
`
|
|
567
|
+
);
|
|
568
|
+
process.exitCode = 1;
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
if (!finalDetail.mcp || !finalDetail.install) {
|
|
572
|
+
process.stderr.write(`${pc6.red("MCP ready but missing install bundle.")}
|
|
640
573
|
`);
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
574
|
+
process.exitCode = 1;
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
process.stdout.write(`
|
|
645
578
|
${pc6.green("MCP ready")}
|
|
646
579
|
`);
|
|
647
|
-
|
|
580
|
+
process.stdout.write(`${pc6.bold("Server:")} ${finalDetail.mcp.serverName}
|
|
648
581
|
`);
|
|
649
|
-
|
|
582
|
+
process.stdout.write(`${pc6.bold("URL:")} ${finalDetail.mcp.url}
|
|
650
583
|
`);
|
|
651
|
-
|
|
584
|
+
process.stdout.write(`${pc6.bold("Token:")} ${finalDetail.mcp.token}
|
|
652
585
|
`);
|
|
653
|
-
|
|
654
|
-
|
|
586
|
+
process.stdout.write(
|
|
587
|
+
`${pc6.dim("Also listed in the doc2mcp marketplace when ready.")}
|
|
655
588
|
`
|
|
656
|
-
|
|
589
|
+
);
|
|
590
|
+
if (options.offerInstall) {
|
|
657
591
|
await promptInstall(finalDetail.install);
|
|
592
|
+
}
|
|
593
|
+
return finalDetail;
|
|
594
|
+
}
|
|
595
|
+
async function runConvert(sourceUrl) {
|
|
596
|
+
try {
|
|
597
|
+
await convertUrlToProject(sourceUrl, { offerInstall: true });
|
|
658
598
|
} catch (error) {
|
|
659
599
|
printError(error);
|
|
660
600
|
process.exitCode = 1;
|
|
@@ -683,9 +623,217 @@ async function runList() {
|
|
|
683
623
|
}
|
|
684
624
|
}
|
|
685
625
|
|
|
626
|
+
// src/commands/chat.ts
|
|
627
|
+
function isDocsUrl(value) {
|
|
628
|
+
try {
|
|
629
|
+
const url = new URL(value);
|
|
630
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
631
|
+
} catch {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function createPrompt() {
|
|
636
|
+
return createInterface({ input, output });
|
|
637
|
+
}
|
|
638
|
+
async function readLine(prompt) {
|
|
639
|
+
const rl = createPrompt();
|
|
640
|
+
try {
|
|
641
|
+
const answer = await rl.question(prompt);
|
|
642
|
+
return answer.trim();
|
|
643
|
+
} catch {
|
|
644
|
+
return null;
|
|
645
|
+
} finally {
|
|
646
|
+
rl.close();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async function listReadyProjects() {
|
|
650
|
+
const data = await apiFetch(
|
|
651
|
+
"/api/cli/projects"
|
|
652
|
+
);
|
|
653
|
+
return data.projects.filter((p) => p.status === "ready");
|
|
654
|
+
}
|
|
655
|
+
async function pickExistingProject() {
|
|
656
|
+
const ready = await listReadyProjects();
|
|
657
|
+
if (ready.length === 0) {
|
|
658
|
+
process.stdout.write(
|
|
659
|
+
`${pc7.yellow("No ready MCP projects yet.")} Paste a docs URL to create one.
|
|
660
|
+
`
|
|
661
|
+
);
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
process.stdout.write(`${pc7.dim("Ready MCPs")}
|
|
665
|
+
`);
|
|
666
|
+
ready.forEach((p, index) => {
|
|
667
|
+
process.stdout.write(
|
|
668
|
+
` ${pc7.cyan(String(index + 1).padStart(2, " "))}. ${pc7.bold(p.name)} ${pc7.dim(p.sourceUrl ?? p.id)}
|
|
669
|
+
`
|
|
670
|
+
);
|
|
671
|
+
});
|
|
672
|
+
const choice = await readLine(
|
|
673
|
+
`${pc7.bold(">")} choose number, paste URL, or paste project id: `
|
|
674
|
+
);
|
|
675
|
+
if (!choice) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
if (isDocsUrl(choice)) {
|
|
679
|
+
return await convertUrlToProject(choice, { offerInstall: true });
|
|
680
|
+
}
|
|
681
|
+
const chosenIndex = Number(choice);
|
|
682
|
+
if (Number.isInteger(chosenIndex) && chosenIndex > 0) {
|
|
683
|
+
const project = ready.at(chosenIndex - 1);
|
|
684
|
+
if (project) {
|
|
685
|
+
return await apiFetch(`/api/cli/projects/${project.id}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return await apiFetch(`/api/cli/projects/${choice}`);
|
|
689
|
+
}
|
|
690
|
+
async function resolveProject(target) {
|
|
691
|
+
if (target) {
|
|
692
|
+
if (isDocsUrl(target)) {
|
|
693
|
+
return await convertUrlToProject(target, { offerInstall: true });
|
|
694
|
+
}
|
|
695
|
+
return await apiFetch(`/api/cli/projects/${target}`);
|
|
696
|
+
}
|
|
697
|
+
process.stdout.write(
|
|
698
|
+
`${pc7.dim("Paste a docs URL to create a new MCP, a project id, or press Enter to pick an existing one.")}
|
|
699
|
+
|
|
700
|
+
`
|
|
701
|
+
);
|
|
702
|
+
const first = await readLine(`${pc7.cyan("\u203A")} docs url or project id: `);
|
|
703
|
+
if (!first) {
|
|
704
|
+
return await pickExistingProject();
|
|
705
|
+
}
|
|
706
|
+
if (isDocsUrl(first)) {
|
|
707
|
+
return await convertUrlToProject(first, { offerInstall: true });
|
|
708
|
+
}
|
|
709
|
+
return await apiFetch(`/api/cli/projects/${first}`);
|
|
710
|
+
}
|
|
711
|
+
async function askDocs(mcp, question) {
|
|
712
|
+
const response = await fetch(mcp.url, {
|
|
713
|
+
method: "POST",
|
|
714
|
+
headers: {
|
|
715
|
+
"Content-Type": "application/json",
|
|
716
|
+
Authorization: `Bearer ${mcp.token}`
|
|
717
|
+
},
|
|
718
|
+
body: JSON.stringify({
|
|
719
|
+
jsonrpc: "2.0",
|
|
720
|
+
id: 1,
|
|
721
|
+
method: "tools/call",
|
|
722
|
+
params: { name: "ask_documentation", arguments: { question } }
|
|
723
|
+
})
|
|
724
|
+
});
|
|
725
|
+
const payload = await response.json();
|
|
726
|
+
if (!response.ok || payload.error) {
|
|
727
|
+
throw new ApiError(
|
|
728
|
+
payload.error?.message ?? `MCP request failed (${response.status})`,
|
|
729
|
+
response.status,
|
|
730
|
+
payload
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
const raw = payload.result?.content?.[0]?.text ?? "";
|
|
734
|
+
try {
|
|
735
|
+
return JSON.parse(raw);
|
|
736
|
+
} catch {
|
|
737
|
+
return { question, answer: raw };
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function renderAnswer(answer) {
|
|
741
|
+
process.stdout.write(`
|
|
742
|
+
${pc7.green("\u25CF")} ${pc7.bold("doc2mcp")}
|
|
743
|
+
|
|
744
|
+
`);
|
|
745
|
+
process.stdout.write(`${renderMarkdown(answer.answer.trim())}
|
|
746
|
+
`);
|
|
747
|
+
if (answer.sources && answer.sources.length > 0) {
|
|
748
|
+
process.stdout.write(`
|
|
749
|
+
${pc7.dim("Sources")}
|
|
750
|
+
`);
|
|
751
|
+
for (const source of answer.sources.slice(0, 6)) {
|
|
752
|
+
process.stdout.write(
|
|
753
|
+
` ${pc7.cyan("\u2022")} ${source.title}
|
|
754
|
+
${pc7.dim(source.url)}
|
|
755
|
+
`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
process.stdout.write("\n");
|
|
760
|
+
}
|
|
761
|
+
async function answerOnce(mcp, question) {
|
|
762
|
+
const spinner = ora2("Thinking\u2026").start();
|
|
763
|
+
try {
|
|
764
|
+
const answer = await askDocs(mcp, question);
|
|
765
|
+
spinner.stop();
|
|
766
|
+
renderAnswer(answer);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
spinner.fail("Failed to get an answer");
|
|
769
|
+
printError(error);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function runChat(target, options = {}) {
|
|
773
|
+
try {
|
|
774
|
+
await ensureLoggedIn();
|
|
775
|
+
const detail = await resolveProject(target);
|
|
776
|
+
if (!detail) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (!detail.mcp) {
|
|
780
|
+
process.stderr.write(
|
|
781
|
+
`${pc7.red("That project is not ready yet.")} Check: ${pc7.bold("doc2mcp list")}
|
|
782
|
+
`
|
|
783
|
+
);
|
|
784
|
+
process.exitCode = 1;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const { mcp } = detail;
|
|
788
|
+
if (options.message) {
|
|
789
|
+
await answerOnce(mcp, options.message);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const title = ` doc2mcp chat \xB7 ${detail.project.name} `;
|
|
793
|
+
const bar = "\u2500".repeat(title.length);
|
|
794
|
+
process.stdout.write(`
|
|
795
|
+
${pc7.cyan(`\u256D${bar}\u256E`)}
|
|
796
|
+
`);
|
|
797
|
+
process.stdout.write(`${pc7.cyan("\u2502")}${pc7.bold(title)}${pc7.cyan("\u2502")}
|
|
798
|
+
`);
|
|
799
|
+
process.stdout.write(`${pc7.cyan(`\u2570${bar}\u256F`)}
|
|
800
|
+
`);
|
|
801
|
+
process.stdout.write(
|
|
802
|
+
`${pc7.dim("Ask anything about these docs. Type /exit to quit.")}
|
|
803
|
+
|
|
804
|
+
`
|
|
805
|
+
);
|
|
806
|
+
let active = true;
|
|
807
|
+
while (active) {
|
|
808
|
+
const question = await readLine(`${pc7.cyan("\u203A")} `);
|
|
809
|
+
if (question === null) {
|
|
810
|
+
active = false;
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
const trimmed = question.trim();
|
|
814
|
+
if (!trimmed) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
818
|
+
active = false;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
await answerOnce(mcp, trimmed);
|
|
822
|
+
}
|
|
823
|
+
process.stdout.write(
|
|
824
|
+
`
|
|
825
|
+
${pc7.dim("Bye \u2014 your docs MCP stays live for your editor.")}
|
|
826
|
+
`
|
|
827
|
+
);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
printError(error);
|
|
830
|
+
process.exitCode = 1;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
686
834
|
// src/index.ts
|
|
687
835
|
var program = new Command();
|
|
688
|
-
program.name("doc2mcp").description("Generate documentation MCP servers from your terminal").version("0.1.
|
|
836
|
+
program.name("doc2mcp").description("Generate documentation MCP servers from your terminal").version("0.1.19", "-v, --version", "Print the installed CLI version");
|
|
689
837
|
program.command("login").description("Authorize the CLI via browser").action(async () => {
|
|
690
838
|
await runLogin();
|
|
691
839
|
});
|
|
@@ -701,8 +849,8 @@ program.command("list").description("List your MCP projects").action(async () =>
|
|
|
701
849
|
program.command("install <projectId>").description("Install an existing MCP into Cursor, VS Code, Claude, or Windsurf").action(async (projectId) => {
|
|
702
850
|
await runInstallCommand(projectId);
|
|
703
851
|
});
|
|
704
|
-
program.command("chat [
|
|
705
|
-
await runChat(
|
|
852
|
+
program.command("chat [target]").description("Chat with docs in the terminal; target can be a project id or docs URL").option("-m, --message <text>", "Ask a single question and exit").action(async (target, options) => {
|
|
853
|
+
await runChat(target, options);
|
|
706
854
|
});
|
|
707
855
|
program.argument("[url]", "Documentation URL to convert").action(async (url) => {
|
|
708
856
|
if (!url) {
|
|
@@ -716,7 +864,7 @@ program.argument("[url]", "Documentation URL to convert").action(async (url) =>
|
|
|
716
864
|
}
|
|
717
865
|
} catch {
|
|
718
866
|
process.stderr.write(
|
|
719
|
-
`${
|
|
867
|
+
`${pc8.red("Error:")} Invalid URL. Example: doc2mcp https://docs.example.com
|
|
720
868
|
`
|
|
721
869
|
);
|
|
722
870
|
process.exitCode = 1;
|
|
@@ -726,7 +874,7 @@ program.argument("[url]", "Documentation URL to convert").action(async (url) =>
|
|
|
726
874
|
});
|
|
727
875
|
program.parseAsync(process.argv).catch((error) => {
|
|
728
876
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
729
|
-
process.stderr.write(`${
|
|
877
|
+
process.stderr.write(`${pc8.red("Error:")} ${message}
|
|
730
878
|
`);
|
|
731
879
|
process.exit(1);
|
|
732
880
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doc2mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Turn any documentation site into a hosted MCP server from your terminal — for Cursor, Claude, VS Code, Windsurf, and OpenAI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"assets",
|
|
20
20
|
"dist",
|
|
21
|
+
"scripts",
|
|
21
22
|
"README.md"
|
|
22
23
|
],
|
|
23
24
|
"engines": {
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"scripts": {
|
|
27
28
|
"build": "tsup",
|
|
28
29
|
"dev": "tsup --watch",
|
|
30
|
+
"postinstall": "node ./scripts/postinstall.js",
|
|
29
31
|
"prepublishOnly": "pnpm build"
|
|
30
32
|
},
|
|
31
33
|
"keywords": [
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
function isGlobalInstall() {
|
|
7
|
+
return (
|
|
8
|
+
process.env.npm_config_global === "true" ||
|
|
9
|
+
process.env.npm_config_location === "global"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function npmGlobalBin() {
|
|
14
|
+
try {
|
|
15
|
+
const prefix = execFileSync("npm", ["prefix", "-g"], {
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
18
|
+
}).trim();
|
|
19
|
+
return path.join(prefix, "bin");
|
|
20
|
+
} catch {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isGlobalInstall()) {
|
|
26
|
+
const binDir = npmGlobalBin();
|
|
27
|
+
const pathEntries = (process.env.PATH || "").split(path.delimiter);
|
|
28
|
+
if (binDir && !pathEntries.includes(binDir)) {
|
|
29
|
+
process.stdout.write(`
|
|
30
|
+
doc2mcp installed, but npm's global bin is not on your PATH.
|
|
31
|
+
|
|
32
|
+
Run this once for zsh:
|
|
33
|
+
echo 'export PATH="${binDir}:$PATH"' >> ~/.zshrc
|
|
34
|
+
source ~/.zshrc
|
|
35
|
+
|
|
36
|
+
Then try:
|
|
37
|
+
doc2mcp login
|
|
38
|
+
|
|
39
|
+
Quick alternative (no PATH setup):
|
|
40
|
+
npx doc2mcp login
|
|
41
|
+
|
|
42
|
+
For pnpm global installs, run:
|
|
43
|
+
pnpm setup
|
|
44
|
+
source ~/.zshrc
|
|
45
|
+
|
|
46
|
+
`);
|
|
47
|
+
}
|
|
48
|
+
}
|